//+------------------------------------------------------------------+ //| VtCard.mqh | //| Copyright 2026, Allan Niñal | //| https://www.allanninal.dev/mql5 | //+------------------------------------------------------------------+ //| The Vitals card: one CCanvas bitmap label on the chart. | //| - Scale = screen DPI / 96 x the UI-scale input. | //| - Theme: Auto (from the chart background), Dark or Light. | //| - Drag by the header, fold to the header, click the tabs; | //| chart scrolling is paused while the mouse is down on it. | //| - Position, fold and tab are kept per chart in terminal global | //| variables. | //| Everything is drawn in logical units (pixels at 100 % scale) and | //| multiplied by the scale. If the chart refuses the object (the | //| strategy tester) the card stays silent. | //+------------------------------------------------------------------+ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) #ifndef VTCARD_MQH #define VTCARD_MQH #include enum ENUM_VT_THEME { VT_THEME_AUTO=0, // Auto (from the chart background) VT_THEME_DARK=1, // Dark VT_THEME_LIGHT=2 // Light }; #define VT_CARD_W 460 // logical width #define VT_ZMAX 96 struct VtPalette { color bg; uchar bg_a; color edge,head,ink,label,dim,pos,neg,warn,acc,track,rule; }; struct VtZone { int x1,y1,x2,y2; int id,arg; }; //--- zone ids #define VTZ_HEAD 1 #define VTZ_FOLD 2 #define VTZ_TAB 3 #define VTZ_PREV 4 #define VTZ_NEXT 5 #define VTZ_CSV 6 #define VTZ_HTML 7 #define VTZ_ALERT 8 //--- text kinds #define VTK_SANS 0 #define VTK_MONO 1 #define VTK_SMALL 2 #define VTK_BIG 3 #define VTK_SEC 4 #define VTK_BRAND 5 #define VTK_MONOS 6 // small mono class CVtCard { public: CCanvas cv; string name; long chart; bool ok; int px,py; // position on the chart (pixels) int pw,ph; // size (pixels) double scale; bool light; bool folded; int tab; VtPalette pal; VtZone zones[VT_ZMAX]; int nz; //--- font cache string m_face; int m_size,m_flags; //--- interaction bool m_down,m_drag,m_hit_card; int m_press_x,m_press_y,m_off_x,m_off_y; bool m_scroll_saved; bool m_scroll_orig; bool m_mouse_orig; //--- fonts (changeable for tests) string face_sans,face_mono; CVtCard(): ok(false),px(12),py(22),pw(0),ph(0),scale(1.0),light(false),folded(false),tab(0),nz(0), m_down(false),m_drag(false),m_hit_card(false),m_scroll_saved(false) { face_sans="Segoe UI"; face_mono="Consolas"; m_face=""; m_size=0; m_flags=0; } //--- scale: DPI/96 x the UI-scale input static double ScaleFor(const double ui) { double dpi=(double)TerminalInfoInteger(TERMINAL_SCREEN_DPI); if(dpi<=0) dpi=96; return(MathMax(0.5,MathMin(4.0,dpi/96.0*ui))); } int S(const double v) const { return((int)MathRound(v*scale)); } void SetTheme(const ENUM_VT_THEME t) { bool l=false; if(t==VT_THEME_LIGHT) l=true; else if(t==VT_THEME_AUTO) { color bg=(color)ChartGetInteger(chart,CHART_COLOR_BACKGROUND); double lum=0.2126*(bg&0xFF)+0.7152*((bg>>8)&0xFF)+0.0722*((bg>>16)&0xFF); l=(lum>127.5); } light=l; if(!light) { pal.bg=C'21,26,30'; pal.bg_a=255; pal.edge=C'42,51,56'; pal.head=C'26,32,36'; pal.ink=C'215,222,226'; pal.label=C'139,151,158'; pal.dim=C'91,102,108'; pal.pos=C'98,184,140'; pal.neg=C'224,123,112'; pal.warn=C'217,166,72'; pal.acc=C'115,169,214'; pal.track=C'37,45,50'; pal.rule=C'35,43,47'; } else { pal.bg=C'250,251,250'; pal.bg_a=255; pal.edge=C'214,219,221'; pal.head=C'239,242,241'; pal.ink=C'30,37,40'; pal.label=C'95,107,113'; pal.dim=C'154,164,169'; pal.pos=C'46,134,87'; pal.neg=C'193,73,62'; pal.warn=C'168,118,26'; pal.acc=C'47,111,166'; pal.track=C'228,232,232'; pal.rule=C'227,231,231'; } } bool Create(const string objname,const int w_logical,const double h_logical) { chart=ChartID(); name=objname; pw=S(w_logical); ph=S(h_logical); ObjectDelete(chart,name); ok=cv.CreateBitmapLabel(chart,0,name,px,py,pw,ph,COLOR_FORMAT_ARGB_NORMALIZE); if(ok) { ObjectSetInteger(chart,name,OBJPROP_SELECTABLE,false); ObjectSetInteger(chart,name,OBJPROP_HIDDEN,true); ObjectSetInteger(chart,name,OBJPROP_BACK,false); ObjectSetInteger(chart,name,OBJPROP_ZORDER,100); ObjectSetInteger(chart,name,OBJPROP_CORNER,CORNER_LEFT_UPPER); ObjectSetInteger(chart,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER); } return(ok); } void Destroy() { if(ok) cv.Destroy(); ok=false; ObjectDelete(chart,name); } //--- change the bitmap size (logical height); the content is redrawn by the caller bool Resize(const double h_logical) { int nh=S(h_logical); if(!ok || nh==ph) return(true); ph=nh; return(cv.Resize(pw,ph)); } void MoveTo(const int x,const int y) { px=x; py=y; if(ok) { ObjectSetInteger(chart,name,OBJPROP_XDISTANCE,px); ObjectSetInteger(chart,name,OBJPROP_YDISTANCE,py); } } void Clamp() { int cw=(int)ChartGetInteger(chart,CHART_WIDTH_IN_PIXELS); int ch=(int)ChartGetInteger(chart,CHART_HEIGHT_IN_PIXELS); int nx=MathMax(0,MathMin(px,cw-pw)); int ny=MathMax(0,MathMin(py,ch-MathMin(ph,S(34)))); // always keep the header reachable if(cw<=pw) nx=0; if(nx!=px || ny!=py) MoveTo(nx,ny); } //--- state kept per chart string Key(const string k) const { return("VITALS_"+IntegerToString(chart)+"_"+k); } void Load(const int defx,const int defy) { px=S(defx); py=S(defy); if(GlobalVariableCheck(Key("x"))) { px=(int)GlobalVariableGet(Key("x")); py=(int)GlobalVariableGet(Key("y")); } folded=(GlobalVariableCheck(Key("fold")) && GlobalVariableGet(Key("fold"))>0.5); tab=(GlobalVariableCheck(Key("tab")) ? (int)GlobalVariableGet(Key("tab")) : 0); } void Save() { GlobalVariableSet(Key("x"),px); GlobalVariableSet(Key("y"),py); GlobalVariableSet(Key("fold"),folded ? 1 : 0); GlobalVariableSet(Key("tab"),tab); } //--- drawing ------------------------------------------------------------------------------------------------------- //--- the card is fully opaque: a translucent colour is pre-blended with the card background instead of letting the // canvas lower the pixel alpha (candles showed through bars drawn with alpha < 255) uint A(const color c,const int alpha=255) const { if(alpha>=255) return(ColorToARGB(c,255)); int r=(c&0xFF),g=((c>>8)&0xFF),b=((c>>16)&0xFF); int br=(pal.bg&0xFF),bgg=((pal.bg>>8)&0xFF),bb=((pal.bg>>16)&0xFF); color m=(color)(((r*alpha+br*(255-alpha))/255) | (((g*alpha+bgg*(255-alpha))/255)<<8) | (((b*alpha+bb*(255-alpha))/255)<<16)); return(ColorToARGB(m,255)); } void Font(const int kind) { string f=face_sans; int sz=16,fl=FW_NORMAL; switch(kind) { case VTK_MONO: f=face_mono; sz=14; break; case VTK_SMALL: sz=14; break; case VTK_BIG: f=face_mono; sz=23; fl=FW_NORMAL; break; case VTK_SEC: sz=12; fl=FW_SEMIBOLD; break; case VTK_BRAND: sz=15; fl=FW_SEMIBOLD; break; case VTK_MONOS: f=face_mono; sz=13; break; default: sz=16; break; } int px_size=S(sz); if(f!=m_face || px_size!=m_size || fl!=m_flags) { cv.FontSet(f,px_size,fl); m_face=f; m_size=px_size; m_flags=fl; } } //--- text at logical x,y; align: 0 left, 1 right (x = right edge), 2 centre void Txt(const double x,const double y,const string s,const color clr,const int kind=VTK_SANS,const int align=0, const int alpha=255) { if(s=="") return; Font(kind); uint fl=(align==1 ? TA_RIGHT : (align==2 ? TA_CENTER : TA_LEFT))|TA_TOP; cv.TextOut(S(x),S(y),s,A(clr,alpha),fl); } int TextW(const string s,const int kind) { Font(kind); int w,h; cv.TextSize(s,w,h); return((int)MathRound(w/scale)); } void Fill(const double x1,const double y1,const double x2,const double y2,const color clr,const int alpha=255) { cv.FillRectangle(S(x1),S(y1),MathMax(S(x1),S(x2)-1),MathMax(S(y1),S(y2)-1),A(clr,alpha)); } void Box(const double x1,const double y1,const double x2,const double y2,const color clr,const int alpha=255) { cv.Rectangle(S(x1),S(y1),MathMax(S(x1),S(x2)-1),MathMax(S(y1),S(y2)-1),A(clr,alpha)); } void HLine(const double x1,const double x2,const double y,const color clr,const int alpha=255) { int yy=S(y); cv.FillRectangle(S(x1),yy,MathMax(S(x1),S(x2)-1),yy,A(clr,alpha)); } void VLine(const double x,const double y1,const double y2,const color clr,const int alpha=255) { int xx=S(x); cv.FillRectangle(xx,S(y1),xx,MathMax(S(y1),S(y2)-1),A(clr,alpha)); } //--- hit zones void ZoneReset() { nz=0; } void Zone(const double x1,const double y1,const double x2,const double y2,const int id,const int arg=0) { if(nz>=VT_ZMAX) return; zones[nz].x1=S(x1); zones[nz].y1=S(y1); zones[nz].x2=S(x2); zones[nz].y2=S(y2); zones[nz].id=id; zones[nz].arg=arg; nz++; } bool ZoneAt(const int x,const int y,int &id,int &arg) const // x,y relative to the card { for(int i=nz-1; i>=0; i--) if(x>=zones[i].x1 && x=zones[i].y1 && y=px && x=py && y