vitals-account-monitor/mql4/Include/Vitals/VtCard.mqh
Allan Niñal Allan Niñal d54b15105b Alerts on by default; regenerated from the private core
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:54:51 +08:00
307 lines
11 KiB
MQL5
//+------------------------------------------------------------------+
//| 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 <Canvas/Canvas.mqh>
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].x2 && y>=zones[i].y1 && y<zones[i].y2)
{ id=zones[i].id; arg=zones[i].arg; return(true); }
return(false);
}
bool Inside(const int x,const int y) const { return(ok && x>=px && x<px+pw && y>=py && y<py+ph); }
//--- the frame: background, border, header; returns the y (logical) where the tabs start
void Frame(const double h_logical)
{
cv.Erase(0);
Fill(0,0,VT_CARD_W,h_logical,pal.bg,pal.bg_a);
Fill(0,0,VT_CARD_W,28,pal.head);
HLine(0,VT_CARD_W,28,pal.edge);
Box(0,0,VT_CARD_W,h_logical,pal.edge);
}
void Update() { if(ok) cv.Update(true); }
};
#endif
//+------------------------------------------------------------------+