ea-health-monitor/mql5/Include/EAHealth/Panel.mqh
Allan Niñal Allan Niñal cbccffd5a3 EA Health Monitor: free MT5 and MT4 indicator source, Python reference, tests and README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:15:46 +08:00
290 lines
10 KiB
MQL5
//+------------------------------------------------------------------+
//| Panel.mqh |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| A grid panel made of chart objects that all start with one |
//| prefix: title bar, header row, one row label per row and one |
//| button per cell. Only cells whose text or colours changed are |
//| touched. Everything is removed by Destroy() (call it in |
//| OnDeinit). If the chart refuses objects (e.g. in the tester) the |
//| panel simply stays silent. |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#property strict
enum ENUM_SD_CORNER
{
SD_TOP_LEFT=0, // Top left
SD_TOP_RIGHT=1, // Top right
SD_BOTTOM_LEFT=2, // Bottom left
SD_BOTTOM_RIGHT=3 // Bottom right
};
class CSDPanel
{
private:
string m_p;
long m_chart;
int m_corner,m_ox,m_oy,m_fs;
bool m_collapsed,m_ok,m_built;
int m_rows,m_cols;
string m_title;
string m_rowhead; // header of the row-label column ("Symbol" unless set)
string m_heads[],m_labels[];
string m_txt[],m_tip[];
color m_fg[],m_bg[];
color m_c_panel,m_c_text,m_c_head;
int m_lw,m_cw,m_rh,m_pad,m_W,m_H,m_x0,m_y0,m_cwpx,m_chpx;
bool Put(const ENUM_OBJECT type,const string name,const int x,const int y,const int w,const int h,
const string txt,const color fg,const color bg,const bool bold);
void Size();
public:
CSDPanel(): m_rowhead("Symbol"),m_chart(0),m_collapsed(false),m_ok(true),m_built(false),m_rows(0),m_cols(0) {}
void Setup(const string prefix,const string title,const int corner,const int ox,const int oy,const int fontsize,
const color c_panel,const color c_text,const color c_head);
void Layout(const string &heads[],const string &labels[]);
void Rebuild();
void SetTitle(const string t);
void SetRowHead(const string h) { m_rowhead=h; }
void SetCell(const int r,const int c,const string txt,const color fg,const color bg,const string tip);
bool Click(const string name,int &row,int &col);
bool ChartResized();
void Destroy();
string Prefix() const { return(m_p); }
};
void CSDPanel::Setup(const string prefix,const string title,const int corner,const int ox,const int oy,
const int fontsize,const color c_panel,const color c_text,const color c_head)
{
m_p=prefix; m_title=title; m_corner=corner; m_ox=ox; m_oy=oy; m_fs=MathMax(7,fontsize);
m_c_panel=c_panel; m_c_text=c_text; m_c_head=c_head;
m_chart=ChartID();
m_collapsed=false;
m_ok=true;
}
void CSDPanel::Size()
{
m_rh=m_fs*2+6;
m_pad=4;
m_cwpx=(int)ChartGetInteger(m_chart,CHART_WIDTH_IN_PIXELS);
m_chpx=(int)ChartGetInteger(m_chart,CHART_HEIGHT_IN_PIXELS);
int maxl=StringLen(m_rowhead);
for(int i=0; i<m_rows; i++)
maxl=MathMax(maxl,StringLen(m_labels[i]));
m_lw=(int)(maxl*m_fs*0.95)+14;
int maxc=6;
for(int i=0; i<m_rows*m_cols; i++)
maxc=MathMax(maxc,StringLen(m_txt[i]));
for(int i=0; i<m_cols; i++)
maxc=MathMax(maxc,StringLen(m_heads[i]));
m_cw=(int)(maxc*m_fs*0.95)+12;
int body=(m_collapsed ? 0 : m_lw+m_cols*(m_cw+2)+2);
m_W=m_pad*2+MathMax(body,(int)(StringLen(m_title)*m_fs*1.0)+m_rh*2);
m_H=m_pad*2+m_rh+(m_collapsed ? 0 : (m_rh+2)*(m_rows+1));
m_x0=((m_corner==SD_TOP_RIGHT || m_corner==SD_BOTTOM_RIGHT) ? m_cwpx-m_W-m_ox : m_ox);
m_y0=((m_corner==SD_BOTTOM_LEFT || m_corner==SD_BOTTOM_RIGHT) ? m_chpx-m_H-m_oy : m_oy);
m_x0=MathMax(0,m_x0);
m_y0=MathMax(0,m_y0);
}
bool CSDPanel::Put(const ENUM_OBJECT type,const string name,const int x,const int y,const int w,const int h,
const string txt,const color fg,const color bg,const bool bold)
{
if(ObjectFind(m_chart,name)<0 && !ObjectCreate(m_chart,name,type,0,0,0))
return(false);
ObjectSetInteger(m_chart,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(m_chart,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(m_chart,name,OBJPROP_YDISTANCE,y);
if(type!=OBJ_LABEL)
{
ObjectSetInteger(m_chart,name,OBJPROP_XSIZE,w);
ObjectSetInteger(m_chart,name,OBJPROP_YSIZE,h);
}
ObjectSetInteger(m_chart,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(m_chart,name,OBJPROP_SELECTED,false);
ObjectSetInteger(m_chart,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(m_chart,name,OBJPROP_BACK,false);
ObjectSetInteger(m_chart,name,OBJPROP_ZORDER,type==OBJ_BUTTON ? 10 : 1);
ObjectSetInteger(m_chart,name,OBJPROP_COLOR,fg);
if(type==OBJ_BUTTON || type==OBJ_RECTANGLE_LABEL)
{
ObjectSetInteger(m_chart,name,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(m_chart,name,OBJPROP_BORDER_COLOR,bg);
if(type==OBJ_RECTANGLE_LABEL)
{
ObjectSetInteger(m_chart,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
ObjectSetInteger(m_chart,name,OBJPROP_COLOR,m_c_head);
}
}
if(type!=OBJ_RECTANGLE_LABEL)
{
ObjectSetString(m_chart,name,OBJPROP_FONT,bold ? "Arial Bold" : "Arial");
ObjectSetInteger(m_chart,name,OBJPROP_FONTSIZE,m_fs);
ObjectSetString(m_chart,name,OBJPROP_TEXT,txt);
}
return(true);
}
//--- (re)create every object from the cached text
void CSDPanel::Rebuild()
{
if(!m_ok)
return;
ObjectsDeleteAll(m_chart,m_p);
Size();
if(!Put(OBJ_RECTANGLE_LABEL,m_p+"bg",m_x0,m_y0,m_W,m_H,"",m_c_text,m_c_panel,false))
{
m_ok=false; // this chart takes no objects (tester, handle without a chart)
return;
}
Put(OBJ_LABEL,m_p+"title",m_x0+m_pad+4,m_y0+m_pad+m_fs/2,0,0,m_title,m_c_text,0,true);
Put(OBJ_BUTTON,m_p+"min",m_x0+m_W-m_pad-m_rh,m_y0+m_pad,m_rh,m_rh,m_collapsed ? "+" : "-",m_c_text,m_c_panel,true);
m_built=true;
if(m_collapsed)
{
ChartRedraw(m_chart);
return;
}
int y=m_y0+m_pad+m_rh+2;
Put(OBJ_LABEL,m_p+"hs",m_x0+m_pad+6,y+m_fs/2,0,0,m_rowhead,m_c_head,0,true);
for(int c=0; c<m_cols; c++)
{
int x=m_x0+m_pad+m_lw+2+c*(m_cw+2);
string n=m_p+"h"+IntegerToString(c);
Put(OBJ_LABEL,n,x+(m_cw-(int)(StringLen(m_heads[c])*m_fs*0.8))/2,y+m_fs/2,0,0,m_heads[c],m_c_head,0,true);
}
for(int r=0; r<m_rows; r++)
{
int yy=y+(m_rh+2)*(r+1);
Put(OBJ_BUTTON,m_p+"r"+IntegerToString(r),m_x0+m_pad,yy,m_lw,m_rh,m_labels[r],m_c_text,m_c_panel,true);
for(int c=0; c<m_cols; c++)
{
string n=m_p+"c"+IntegerToString(r)+"_"+IntegerToString(c);
int i=r*m_cols+c;
Put(OBJ_BUTTON,n,m_x0+m_pad+m_lw+2+c*(m_cw+2),yy,m_cw,m_rh,m_txt[i],m_fg[i],m_bg[i],true);
ObjectSetString(m_chart,n,OBJPROP_TOOLTIP,m_tip[i]);
}
}
ChartRedraw(m_chart);
}
void CSDPanel::Layout(const string &heads[],const string &labels[])
{
m_cols=ArraySize(heads);
m_rows=ArraySize(labels);
ArrayResize(m_heads,m_cols);
ArrayResize(m_labels,m_rows);
ArrayCopy(m_heads,heads);
ArrayCopy(m_labels,labels);
int n=m_rows*m_cols;
ArrayResize(m_txt,n);
ArrayResize(m_tip,n);
ArrayResize(m_fg,n);
ArrayResize(m_bg,n);
for(int i=0; i<n; i++)
{
m_txt[i]="loading";
m_tip[i]="";
m_fg[i]=m_c_head;
m_bg[i]=m_c_panel;
}
Rebuild();
}
void CSDPanel::SetTitle(const string t)
{
if(t==m_title)
return;
m_title=t;
if(m_ok && m_built)
ObjectSetString(m_chart,m_p+"title",OBJPROP_TEXT,t);
}
void CSDPanel::SetCell(const int r,const int c,const string txt,const color fg,const color bg,const string tip)
{
int i=r*m_cols+c;
if(i<0 || i>=m_rows*m_cols)
return;
bool chg=(m_txt[i]!=txt || m_fg[i]!=fg || m_bg[i]!=bg);
if(!chg && m_tip[i]==tip)
return;
m_txt[i]=txt; m_fg[i]=fg; m_bg[i]=bg; m_tip[i]=tip;
if(!m_ok || !m_built || m_collapsed)
return;
string n=m_p+"c"+IntegerToString(r)+"_"+IntegerToString(c);
if(chg)
{
ObjectSetString(m_chart,n,OBJPROP_TEXT,txt);
ObjectSetInteger(m_chart,n,OBJPROP_COLOR,fg);
ObjectSetInteger(m_chart,n,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(m_chart,n,OBJPROP_BORDER_COLOR,bg);
}
ObjectSetString(m_chart,n,OBJPROP_TOOLTIP,tip);
}
//--- a click on one of our objects: row/col of the cell, (row,-1) for a row label, (-2,0) when handled (collapse)
bool CSDPanel::Click(const string name,int &row,int &col)
{
if(StringFind(name,m_p)!=0 || !m_ok)
return(false);
string rest=StringSubstr(name,StringLen(m_p));
ObjectSetInteger(m_chart,name,OBJPROP_STATE,false);
if(rest=="min")
{
m_collapsed=!m_collapsed;
Rebuild();
row=-2;
col=0;
return(true);
}
if(StringGetCharacter(rest,0)=='c')
{
string p[];
if(StringSplit(StringSubstr(rest,1),'_',p)==2)
{
row=(int)StringToInteger(p[0]);
col=(int)StringToInteger(p[1]);
ChartRedraw(m_chart);
return(true);
}
}
if(StringGetCharacter(rest,0)=='r')
{
row=(int)StringToInteger(StringSubstr(rest,1));
col=-1;
ChartRedraw(m_chart);
return(true);
}
return(false);
}
//--- true (and re-lays out) when the chart size changed and the panel is anchored to a right/bottom corner
bool CSDPanel::ChartResized()
{
if(!m_ok || !m_built)
return(false);
int w=(int)ChartGetInteger(m_chart,CHART_WIDTH_IN_PIXELS),h=(int)ChartGetInteger(m_chart,CHART_HEIGHT_IN_PIXELS);
if(w==m_cwpx && h==m_chpx)
return(false);
if(m_corner==SD_TOP_LEFT)
{
m_cwpx=w;
m_chpx=h;
return(false);
}
Rebuild();
return(true);
}
void CSDPanel::Destroy()
{
if(m_p!="")
{
ObjectsDeleteAll(m_chart,m_p);
ChartRedraw(m_chart);
}
m_built=false;
}
//+------------------------------------------------------------------+