#include "xy.h"
#include <stdio.h>
#include <math.h>
#include "wutils.h"
/*********
* Skale *
*********/
Scale::Scale(XY*_parent,UINT _side,char*_name):
O(0),F(1),preci(0),
parent(_parent){
side=_side;
name=_name;
format="%.2f";
range.init(0,10);
font[0]=new Gdiplus::Font(Gdiplus::FontFamily::GenericSansSerif(),10);
font[1]=new Gdiplus::Font(Gdiplus::FontFamily::GenericSansSerif(),10,Gdiplus::FontStyleBold);
brush[0]=new Gdiplus::SolidBrush(color[0]);
brush[1]=new Gdiplus::SolidBrush(color[1]);
}
Scale::~Scale() {
delete brush[1];
delete brush[0];
delete font[1];
delete font[0];
}
static Gdiplus::Status MeasureStringU(Gdiplus::Graphics&g, const char*s,const Gdiplus::Font*fnt,const Gdiplus::PointF&org,const Gdiplus::StringFormat*sf,Gdiplus::RectF&box) {
wchar_t buf[64];
int l=MultiByteToWideChar(CP_UTF8,0,s,-1,buf,elemof(buf))-1;
return g.MeasureString(buf,l,fnt,org,sf,&box);
}
static Gdiplus::Status circle(Gdiplus::Graphics&g,const Gdiplus::Pen*pen,const Gdiplus::PointF¢er,float dia=10.0) {
return g.DrawEllipse(pen,center.X-dia/2,center.Y-dia/2,dia,dia);
}
static Gdiplus::Status DrawStringU(Gdiplus::Graphics&g,const char*s,const Gdiplus::Font*fnt,const Gdiplus::PointF&org,const Gdiplus::StringFormat*sf,const Gdiplus::Brush*brush) {
wchar_t buf[64];
int l=MultiByteToWideChar(CP_UTF8,0,s,-1,buf,elemof(buf))-1;
// DEBUG
// Gdiplus::Pen pen(Gdiplus::Color::Aqua);
// circle(g,&pen,org);
return g.DrawString(buf,l,fnt,org,sf,brush);
}
void Scale::tickstringformat(Gdiplus::StringFormat&sf) {
sf.SetLineAlignment(side&1?Gdiplus::StringAlignmentCenter:side&2?Gdiplus::StringAlignmentNear:Gdiplus::StringAlignmentFar);
sf.SetAlignment(side&1?side&2?Gdiplus::StringAlignmentNear:Gdiplus::StringAlignmentFar:Gdiplus::StringAlignmentCenter);
}
void Scale::tickposition(float v, Gdiplus::PointF p[2]) {
if (side&1) {
p[0].X=float(rc[2-(side&2)]); p[0].Y=scale(v);
p[1].X=p[0].X+(side&2?ticklen:-ticklen); p[1].Y=p[0].Y;
}else{
p[0].Y=float(rc[3-(side&2)]); p[0].X=scale(v);
p[1].Y=p[0].Y+(side&2?ticklen:-ticklen); p[1].X=p[0].X;
}
}
void Scale::namestringformat(Gdiplus::StringFormat&sf) {
if (side&1) {
// sf.SetFormatFlags(Gdiplus::StringFormatFlagsDirectionVertical);
sf.SetAlignment(Gdiplus::StringAlignmentCenter);
sf.SetLineAlignment(Gdiplus::StringAlignmentNear);
}else{
sf.SetAlignment(Gdiplus::StringAlignmentCenter);
sf.SetLineAlignment(side&2?Gdiplus::StringAlignmentFar:Gdiplus::StringAlignmentNear);
}
}
void Scale::nameposition(Gdiplus::Graphics&g,Gdiplus::PointF&p) {
if (side&1) {
p.Y=float(rc[side&2]); // an der Außenkante
p.X=-(rc.top+rc.bottom)*0.5F; // in der Mitte (X/Y vertauscht wegen Textrotation)
g.RotateTransform(side&2?90.0F:-90.0F);
if (side&2) {p.X=-p.X; p.Y=-p.Y;}
}else{
p.X=(rc.left+rc.right)*0.5F;
p.Y=float(rc[1+(side&2)]); // bottom/top
}
}
void Scale::onPaint(Gdiplus::Graphics&g) {
using namespace Gdiplus;
StringFormat sf; tickstringformat(sf);
Pen pen(color[0]);
for each(Label lbl in ticks) {
PointF p[2]; tickposition(lbl.position,p);
g.DrawLines(&pen,p,2);
DrawStringU(g,lbl.string,font[0],p[1],&sf,brush[0]);
}
if (name) {
namestringformat(sf);
PointF p; nameposition(g,p);
DrawStringU(g,name,font[1],p,&sf,brush[1]);
g.ResetTransform();
}
}
// Der Unterschied zwischen „DirectionVertical“ und „Rotation“ fällt erst bei Chinesisch/Japanisch auf.
void Scale::onSize(Gdiplus::Graphics&g) {
calcFO();
distributeTicks(g);
}
static char getlog10(float v) {return (char)::floorf(::log10f(v));}
static float getpow10(char c) {return ::powf(10,c);}
static char normal10(float v, float&f) {
char nk=v?-getlog10(fabsf(v)):0; // Liefert Nachkommastellen
f=getpow10(nk); // Liefert Faktor (10^nk) mit dem v multipliziert wurde
return nk;
}
char round125(float&v,bool up) { // Schrittweite auf ein Vielfaches von 1, 2 oder 5 auf- oder abrunden
float f;
char nk=normal10(v,f);
v*=f; // Liefert Wert zwischen 1 und <10
bool neg=v<0; if (neg) {v=-v; up=!up;}
if (up) {
if (v>5) {v=10;nk--;}
else if (v>2) v=5;
else if (v>1) v=2;
}else{
if (v>=5) v=5;
else if (v>=2) v=2;
else v=1;
}
v/=f;
if (neg) v=-v;
return nk; // liefert Nachkommastellen für printf
}
static float roundToNext(float v,float step) {
bool neg=step<0; if (neg) {v=-v; step=-step;} // falls Schrittweite negativ
v/=step; // Schritte
v=::ceilf(v); // Ganzzahlige Schritte
v*=step;
return neg?-v:v;
}
float round2(float v,float l,bool up) {
float f;
normal10(l,f);
f*=10; // Länge zweistellig (10..99)
v*=f;
v=(up?ceilf:floorf)(v); // gröbere Stückelung wäre hier zu tun
v/=f;
return v;
}
void Scale::distributeTicks(Gdiplus::Graphics&g) {
float step=(range.e-range.a)/numberExtent.Height; // Y, linear, analog: Nötige Schrittweite
setmax(step,preci);
char nk=round125(step,step>=0);
float first=roundToNext(range.a,step);
ticks=labels; // Kopier-Aktion
addTick(g,range.a,-1); // Enden der Skale immer beschriften (mit voller Präzision??)
addTick(g,range.e,-1);
if (nk<0) nk=0;
for (;range.inside(first);first+=step) addTick(g,first,nk);
}
bool Scale::addTick(Gdiplus::Graphics&g,float v,char nk) {
char s[32];
if (nk<0) nk=0;
int l;
if (strchr(format,'*')) l=sprintf(s,format,nk,v);
else l=sprintf(s,format,v);
Label lbl;
Gdiplus::StringFormat sf; tickstringformat(sf);
Gdiplus::PointF pt[2]; tickposition(v,pt);
MeasureStringU(g,s,font[0],pt[1],&sf,lbl.box);
for each (Label label in ticks) {
if (label.box.IntersectsWith(lbl.box)) return false;
}
lbl.string=newcopy(s,l+1);
lbl.position=v;
ticks.push_back(lbl);
return true;
}
void Scale::needSpace(Gdiplus::Graphics&g,NEED&need) {
// TODO: Iteriere über alle Labels, füge Achsenbezeichnung hinzu
Gdiplus::PointF pt;
Gdiplus::RectF bbox;
g.MeasureString(L"0",1,font[0],pt,&bbox);
numberExtent.Width=bbox.Width;
numberExtent.Height=bbox.Height;
need.l=need.r=(int)bbox.Width>>1; // Hälfte
need.h=(int)bbox.Height;
if (name && *name) need.h<<=1; // 2×
need.h+=ticklen;
}
/***********
* XY-Graf *
***********/
XY::XY(HWND Wnd):wnd(Wnd) {
hPlotLegend=0;
hScaleLegend=0;
hCursorLegend=0;
hMiniature=0;
Gdiplus::GdiplusStartupInput gpsi;
Gdiplus::GdiplusStartup(&gdiplusToken,&gpsi,0);
xscales.push_back(new Scale(this,::BOTTOMX,"Zeit"));
yscales.push_back(new Scale(this,::LEFTY,"Wert"));
}
XY::~XY() {
for each(Scale*xs in xscales) delete xs;
for each(Scale*ys in yscales) delete ys;
Gdiplus::GdiplusShutdown(gdiplusToken);
}
void XY::onSize(int cx, int cy) {
Gdiplus::Graphics g(wnd);
SetRect(&rcPlot,0,0,cx,cy);
// Pass 1: Größe der Plot-Ränder bestimmen
Rect extra;
SetRectEmpty(&extra);
std::vector<int> heights(xscales.size()+yscales.size()); // zum Speichern der Höhen
int i=0;
for each(Scale*xs in xscales) {
Scale::NEED need;
xs->needSpace(g,need); // Größe ermitteln
heights[i++]=need.h; // Höhe/Breite retten für später
if (xs->side&2) rcPlot.bottom-=need.h; else rcPlot.top+=need.h;
setmax(extra.left,need.l);
setmax(extra.right,need.r);
}
for each(Scale*ys in yscales) {
Scale::NEED need;
ys->needSpace(g,need);
heights[i++]=need.h;
if (ys->side&2) rcPlot.right-=need.h; else rcPlot.left+=need.h;
setmax(extra.top,need.l);
setmax(extra.bottom,need.r);
}
setmax(rcPlot.left,extra.left);
setmax(rcPlot.top,extra.top);
setmin(rcPlot.right,cx-extra.right);
setmin(rcPlot.bottom,cy-extra.bottom);
// Pass 2: Skalen platzieren
i=0;
Rect grow; // Ein Rechteck was von innen nach außen wachsen wird
CopyRect(&grow,&rcPlot);
for each(Scale*xs in xscales) {
int h=heights[i++];
if (xs->side&2) { // unten
SetRect(&xs->rc,rcPlot.left,grow.bottom,rcPlot.right,grow.bottom+h);
grow.bottom+=h;
}else{ // oben
SetRect(&xs->rc,rcPlot.left,grow.top-h,rcPlot.right,grow.top);
grow.top-=h;
}
xs->onSize(g); // nach Größenänderung Skalierungsfaktoren anpassen
}
for each(Scale*ys in yscales) {
int w=heights[i++];
if (ys->side&2) { // rechts
SetRect(&ys->rc,grow.right,rcPlot.top,grow.right+w,rcPlot.bottom);
grow.right+=w;
}else{ // links
SetRect(&ys->rc,grow.left-w,rcPlot.top,grow.left,rcPlot.bottom);
grow.left-=w;
}
ys->onSize(g);
}
// Pass 3: Plots neu zeichnen
for each(Plot*pl in plots) pl->onSize(g);
InvalidateRect(wnd,NULL,TRUE);
}
void XY::onPaint(Gdiplus::Graphics&g) {
Rect rcClient;
GetClientRect(wnd,&rcClient);
g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
g.DrawLine(new Gdiplus::Pen(Gdiplus::Color()),rcClient.left,rcClient.top,rcClient.right,rcClient.bottom);
Gdiplus::Pen pen(Gdiplus::Color(255,0,0,255),3);
g.DrawLine(&pen,rcPlot.left,rcPlot.bottom,rcPlot.right,rcPlot.top);
#if 0
{using namespace Gdiplus;
static const wchar_t vText[]= L"Vertikaler Text: الباب 你好";
Font*font = new Font(FontFamily::GenericSansSerif(),14);
PointF pt(40,20);
StringFormat sf;
sf.SetLineAlignment(StringAlignmentCenter);
Brush*brush = new SolidBrush(Color(255, 0, 0, 255));
Pen*pen=new Pen(Color(0,255,0));
RectF bbox;
g.MeasureString(vText,-1,font,pt,&sf,&bbox);
sf.SetFormatFlags(StringFormatFlagsDirectionVertical); // Nur für Chinesisch relevant!
circle(g,pen,pt,bbox.Height);
g.DrawString(vText,-1,font,pt,&sf,brush);
sf.SetFormatFlags(0);
g.RotateTransform(90);
g.MeasureString(vText,-1,font,pt,&sf,&bbox);
pt.X+=bbox.Height;
PointF pt2=pt;
g.TransformPoints(CoordinateSpaceWorld,CoordinateSpacePage,&pt2,1);
circle(g,pen,pt2,bbox.Height);
g.DrawString(L"Von links lesbar: الباب 你好",-1,font,pt2,&sf,brush); // von links lesbar (auch das Chinesisch)
g.RotateTransform(180);
pt.X+=bbox.Height;
pt2=pt;
g.TransformPoints(CoordinateSpaceWorld,CoordinateSpacePage,&pt2,1);
sf.SetAlignment(StringAlignmentFar);
circle(g,pen,pt2,bbox.Height);
g.DrawString(L"Von rechts lesbar: الباب 你好",-1,font,pt2,&sf,brush); // von rechts lesbar
delete font;
delete brush;
delete pen;
}
#else
Gdiplus::Font fnt(Gdiplus::GenericSansSerifFontFamily,10);
Gdiplus::PointF org(20,20);
Gdiplus::SolidBrush br(Gdiplus::Color(128,0,255));
g.DrawString(L"Hier",-1,&fnt,org,&br);
#endif
// MoveToEx(ps.hdc,rcPlot.left,rcPlot.bottom,0);
// LineTo(ps.hdc,rcPlot.right,rcPlot.top);
for each(Scale*xs in xscales) xs->onPaint(g); // alle X-Skalen
// for(auto ys:yscales) ys->onPaint(ps); //MSVC2017
for each (Scale*ys in yscales) ys->onPaint(g); // alle Y-Skalen
for each (Plot*pl in plots) pl->onPaint(g);
}
Scale* XY::getXScale(char&idx) {
if ((byte)idx>=xscales.size()) {
idx=xscales.size();
xscales.push_back(new Scale(this,idx&1?TOPX:BOTTOMX)); // abwechselnd unten und oben dazu
}
return xscales[idx];
}
Scale* XY::getYScale(char&idx) {
if ((byte)idx>=yscales.size()) {
idx=yscales.size();
yscales.push_back(new Scale(this,idx&1?RIGHTY:LEFTY)); // abwechselnd links und rechts dazu
}
return yscales[idx];
}
Gdiplus::Color XY::getPlotColor(char&idx) {
if ((byte)idx>plots.size()) idx=plots.size();
static const Gdiplus::Color defColors[]={Gdiplus::Color::Green,Gdiplus::Color::Blue,Gdiplus::Color::Red,Gdiplus::Color::Brown};
return defColors[idx%elemof(defColors)];
}
void XY::repositionAll() {
Rect r;
::GetClientRect(wnd,&r);
onSize(r.right,r.bottom);
InvalidateRect(wnd,0,TRUE);
}
void XY::beginChange(BYTE) {needReposition=false;}
void XY::endChange(BYTE what) {
if (needReposition) {
KillTimer(wnd,123);
timer_running=false;
repositionAll();
}else{
if (!timer_running) {
SetTimer(wnd,123,1000,0);
timer_running=true;
}
InvalidateRect(wnd,&rcPlot,TRUE);
}
}
// Nachgucken ob Skalen schrumpfen dürfen
void XY::shrinkScales() {
KillTimer(wnd,123);
timer_running=false;
bool needReposition=false;
Range needrange;
for each (Scale*xs in xscales) {
for each (Plot*pl in xs->plots) {
needrange.expand(pl->rangeX);
}
if (xs->range.set2(needrange,Range::SHRINK)&Range::SHRINK) needReposition=true;
}
needrange.reset();
for each (Scale*ys in yscales) {
for each (Plot*pl in ys->plots) {
needrange.expand(pl->rangeY);
}
if (ys->range.set2(needrange,Range::SHRINK)&Range::SHRINK) needReposition=true;
}
if (needReposition) repositionAll();
}
LRESULT XY::wndproc(UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_SIZE: onSize(GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam)); return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
Gdiplus::Graphics g(BeginPaint(wnd,&ps));
onPaint(g);
EndPaint(wnd,&ps);
}return 0;
case WM_PRINTCLIENT: {
Gdiplus::Graphics g((HDC)wParam);
onPaint(g);
}return 0;
case WM_TIMER: if (wParam==123) shrinkScales(); break;
case PM_SETPLOT: {
Plot*plot=new Plot(this,*(PLOT*)lParam);
char idx=(char)wParam;
if ((byte)idx<plots.size()) {delete plots[idx]; plots[idx]=plot;}
else {idx=plots.size(); plots.push_back(plot);}
return idx;
}
case PM_ADDSAMPLES: {
FLEXDATA data={(void*)lParam};
beginChange(PLOTDATA);
for each (Plot*pl in plots) pl->putdata(data);
endChange(PLOTDATA);
}return 0;
// case PM_ADDVALUE:
// case PM_ADDVALUES:
// case PM_ADDPAIR:
// case PM_ADDPAIRS:
// case PM_ADDTUPLE:
// case PM_ADDSIGNAL:
// case PM_ADDSIGNALS:
}
return DefWindowProc(wnd,msg,wParam,lParam);
}
LRESULT CALLBACK XY::wndproc(HWND Wnd, UINT msg, WPARAM wParam, LPARAM lParam) {
XY*self=(XY*)GetWindowLong(Wnd,0);
if (!self) {
self=new XY(Wnd);
SetWindowLong(Wnd,0,(LONG_PTR)self);
}
LRESULT ret=self->wndproc(msg,wParam,lParam);
if (msg==WM_NCDESTROY) delete self;
return ret;
}
BOOL XY::init() {
static const WNDCLASS wc={
CS_DBLCLKS|CS_HREDRAW|CS_VREDRAW|CS_PARENTDC,
wndproc,
0,
sizeof(XY*),
0,
0,
0,
HBRUSH(COLOR_WINDOW+1),
0,TEXT("plot")
};
return RegisterClass(&wc);
}
BOOL PlotInit() {return XY::init();}
Detected encoding: UTF-8 | 0
|