| //+------------------------------------------------------------------+
|
| //| 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;
|
| }
|
| //+------------------------------------------------------------------+
|