2026-10-21 16:07:46 +08:00 | | | //+------------------------------------------------------------------+
|
| | | //| Panel.mqh |
|
| | | //| Copyright 2026, Allan Niñal |
|
| | | //| https://www.allanninal.dev/mql5 |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Chart panel drawing: cached labels and boxes (unchanged text is |
|
| | | //| never resent), sections, "label value" rows and buttons. Light |
|
| | | //| and dark themes. |
|
| | | //+------------------------------------------------------------------+
|
| | | // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| | | #ifndef RTM_PANEL_MQH
|
| | | #define RTM_PANEL_MQH
|
| | |
|
2026-10-31 14:29:00 +08:00 | | | enum ENUM_PRG_CORNER
|
2026-10-21 16:07:46 +08:00 | | | {
|
2026-10-31 14:29:00 +08:00 | | | PRG_LEFT=0, // Top left
|
| | | PRG_RIGHT=1 // Top right
|
2026-10-21 16:07:46 +08:00 | | | };
|
| | |
|
2026-10-31 14:29:00 +08:00 | | | #define PRG_PREFIX "RTM_" // chart object prefix (not "PRG_": Prop Rules Guard may share the chart)
|
2026-10-21 16:07:46 +08:00 | | | #define UI_W 318 // panel width
|
| | | #define UI_PAD 10 // inner padding
|
| | | #define UI_VAL 112 // value column
|
| | | #define UI_ROW 16 // row height
|
| | | #define UI_HEAD 24 // title bar height
|
| | | #define UI_GAP 6 // space above a section title
|
| | | #define UI_CHAR 7 // Consolas 9 advance width, px
|
| | |
|
2026-10-31 14:29:00 +08:00 | | | ENUM_PRG_CORNER g_ui_corner=PRG_LEFT;
|
2026-10-21 16:07:46 +08:00 | | | int g_ui_x=10,g_ui_y=26,g_ui_font=9;
|
| | | bool g_ui_light=false;
|
2026-10-31 14:29:00 +08:00 | | | bool g_ui_cb=false; // colour-blind friendly: blue/orange instead of green/red (Chart Kit tokens)
|
2026-10-21 16:07:46 +08:00 | | | bool g_panel_dirty=false;
|
| | |
|
| | | color UiBack() { return(g_ui_light ? C'250,250,251' : C'24,26,30'); }
|
| | | color UiText() { return(g_ui_light ? C'44,48,56' : C'206,212,220'); }
|
| | | color UiLabel() { return(g_ui_light ? C'124,131,142' : C'122,130,142'); }
|
| | | color UiHead() { return(g_ui_light ? C'92,100,112' : C'150,160,175'); }
|
2026-10-31 14:29:00 +08:00 | | | color UiDim() { return(g_ui_light ? C'140,146,157' : C'96,102,112'); }
|
| | | color UiPos() { return(g_ui_cb ? (g_ui_light ? C'0,114,178' : C'86,180,233') : (g_ui_light ? C'38,118,66' : C'126,192,143')); }
|
| | | color UiNeg() { return(g_ui_cb ? (g_ui_light ? C'213,94,0' : C'230,159,0') : (g_ui_light ? C'176,58,58' : C'226,124,124')); }
|
| | | color UiWarn() { return(g_ui_cb ? (g_ui_light ? C'150,120,0' : C'240,228,66') : (g_ui_light ? C'160,112,28' : C'220,178,116')); }
|
2026-10-21 16:07:46 +08:00 | | | color UiAccent() { return(g_ui_light ? C'48,92,148' : C'132,164,206'); }
|
| | | color UiRule() { return(g_ui_light ? C'220,223,228' : C'48,52,58'); }
|
2026-10-31 14:29:00 +08:00 | | | color UiTrack() { return(g_ui_light ? C'232,234,238' : C'40,43,49'); }
|
2026-10-21 16:07:46 +08:00 | | |
|
| | | //--- per-object cache of what was last sent
|
| | | string g_cache_name[];
|
| | | string g_cache_txt[];
|
| | | color g_cache_clr[],g_cache_fill[];
|
| | | int g_cache_x[],g_cache_y[],g_cache_w[],g_cache_h[];
|
| | | int g_cursor=0;
|
| | |
|
| | | int CacheSlot(const string name)
|
| | | {
|
| | | int n=ArraySize(g_cache_name);
|
| | | if(g_cursor<n && g_cache_name[g_cursor]==name)
|
| | | return(g_cursor++);
|
| | | for(int i=0; i<n; i++)
|
| | | if(g_cache_name[i]==name)
|
| | | {
|
| | | g_cursor=i+1;
|
| | | return(i);
|
| | | }
|
| | | ArrayResize(g_cache_name,n+1,32);
|
| | | ArrayResize(g_cache_txt,n+1,32);
|
| | | ArrayResize(g_cache_clr,n+1,32);
|
| | | ArrayResize(g_cache_x,n+1,32);
|
| | | ArrayResize(g_cache_y,n+1,32);
|
| | | ArrayResize(g_cache_w,n+1,32);
|
| | | ArrayResize(g_cache_h,n+1,32);
|
| | | ArrayResize(g_cache_fill,n+1,32);
|
| | | g_cache_name[n]=name;
|
| | | g_cache_txt[n]="\x01";
|
| | | g_cache_clr[n]=clrNONE;
|
| | | g_cache_fill[n]=clrNONE;
|
| | | g_cache_x[n]=g_cache_y[n]=g_cache_w[n]=g_cache_h[n]=INT_MIN;
|
| | | g_cursor=n+1;
|
| | | return(n);
|
| | | }
|
| | |
|
| | | void CacheClear(void)
|
| | | {
|
| | | ArrayResize(g_cache_name,0);
|
| | | ArrayResize(g_cache_txt,0);
|
| | | ArrayResize(g_cache_clr,0);
|
| | | ArrayResize(g_cache_x,0);
|
| | | ArrayResize(g_cache_y,0);
|
| | | ArrayResize(g_cache_w,0);
|
| | | ArrayResize(g_cache_h,0);
|
| | | ArrayResize(g_cache_fill,0);
|
| | | g_cursor=0;
|
| | | g_panel_dirty=true;
|
| | | }
|
| | |
|
| | | void SetInt(const string name,const ENUM_OBJECT_PROPERTY_INTEGER prop,const long v,int &cache)
|
| | | {
|
| | | if(cache==(int)v)
|
| | | return;
|
| | | ObjectSetInteger(0,name,prop,v);
|
| | | cache=(int)v;
|
| | | g_panel_dirty=true;
|
| | | }
|
| | |
|
2026-10-31 14:29:00 +08:00 | | | int PanelX(const int x) { return(g_ui_corner==PRG_LEFT ? g_ui_x+x : g_ui_x+UI_W-x); }
|
| | | ENUM_BASE_CORNER PanelCorner(void) { return(g_ui_corner==PRG_LEFT ? CORNER_LEFT_UPPER : CORNER_RIGHT_UPPER); }
|
2026-10-21 16:07:46 +08:00 | | |
|
| | | void Text(const string id,const int x,const int y,const string text,const color clr,
|
| | | const int size=0,const string font="Consolas")
|
| | | {
|
2026-10-31 14:29:00 +08:00 | | | string name=PRG_PREFIX+id;
|
2026-10-21 16:07:46 +08:00 | | | string txt=(text=="") ? " " : text; // an empty label would show the word "Label"
|
| | | int slot=CacheSlot(name);
|
| | | if(g_cache_x[slot]==INT_MIN && ObjectFind(0,name)<0)
|
| | | {
|
| | | ObjectCreate(0,name,OBJ_LABEL,0,0,0);
|
| | | ObjectSetInteger(0,name,OBJPROP_CORNER,PanelCorner());
|
| | | ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
|
| | | ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| | | ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| | | ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| | | ObjectSetString(0,name,OBJPROP_FONT,font);
|
| | | ObjectSetInteger(0,name,OBJPROP_FONTSIZE,size>0 ? size : g_ui_font);
|
| | | }
|
| | | SetInt(name,OBJPROP_XDISTANCE,PanelX(x),g_cache_x[slot]);
|
| | | SetInt(name,OBJPROP_YDISTANCE,g_ui_y+y,g_cache_y[slot]);
|
| | | if(g_cache_txt[slot]!=txt)
|
| | | {
|
| | | ObjectSetString(0,name,OBJPROP_TEXT,txt);
|
| | | g_cache_txt[slot]=txt;
|
| | | g_panel_dirty=true;
|
| | | }
|
| | | if(g_cache_clr[slot]!=clr)
|
| | | {
|
| | | ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
|
| | | g_cache_clr[slot]=clr;
|
| | | g_panel_dirty=true;
|
| | | }
|
| | | }
|
| | |
|
| | | void Box(const string id,const int x,const int y,const int w,const int h,const color fill,const color border)
|
| | | {
|
2026-10-31 14:29:00 +08:00 | | | string name=PRG_PREFIX+id;
|
2026-10-21 16:07:46 +08:00 | | | int slot=CacheSlot(name);
|
| | | if(g_cache_x[slot]==INT_MIN && ObjectFind(0,name)<0)
|
| | | {
|
| | | ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0);
|
| | | ObjectSetInteger(0,name,OBJPROP_CORNER,PanelCorner());
|
| | | ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
|
| | | ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| | | ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| | | ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| | | }
|
| | | //--- a rectangle label is anchored at its top-left corner, so on the right corner shift it by its width
|
2026-10-31 14:29:00 +08:00 | | | SetInt(name,OBJPROP_XDISTANCE,g_ui_corner==PRG_LEFT ? g_ui_x+x : g_ui_x+UI_W-x,g_cache_x[slot]);
|
2026-10-21 16:07:46 +08:00 | | | SetInt(name,OBJPROP_YDISTANCE,g_ui_y+y,g_cache_y[slot]);
|
| | | SetInt(name,OBJPROP_XSIZE,MathMax(w,1),g_cache_w[slot]);
|
| | | SetInt(name,OBJPROP_YSIZE,MathMax(h,1),g_cache_h[slot]);
|
| | | if(g_cache_fill[slot]!=fill)
|
| | | {
|
| | | ObjectSetInteger(0,name,OBJPROP_BGCOLOR,fill);
|
| | | g_cache_fill[slot]=fill;
|
| | | g_panel_dirty=true;
|
| | | }
|
| | | if(g_cache_clr[slot]!=border)
|
| | | {
|
| | | ObjectSetInteger(0,name,OBJPROP_COLOR,border);
|
| | | g_cache_clr[slot]=border;
|
| | | g_panel_dirty=true;
|
| | | }
|
| | | }
|
| | |
|
2026-10-31 14:29:00 +08:00 | | | void Button(const string id,const int x,const int y,const int w,const string txt,const color fg=clrNONE)
|
2026-10-21 16:07:46 +08:00 | | | {
|
2026-10-31 14:29:00 +08:00 | | | string name=PRG_PREFIX+id;
|
2026-10-21 16:07:46 +08:00 | | | if(ObjectFind(0,name)<0)
|
| | | {
|
| | | ObjectCreate(0,name,OBJ_BUTTON,0,0,0);
|
| | | ObjectSetInteger(0,name,OBJPROP_CORNER,PanelCorner());
|
| | | ObjectSetInteger(0,name,OBJPROP_YSIZE,18);
|
| | | ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| | | ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| | | ObjectSetString(0,name,OBJPROP_FONT,"Consolas");
|
| | | ObjectSetInteger(0,name,OBJPROP_FONTSIZE,g_ui_font);
|
| | | }
|
| | | ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
|
2026-10-31 14:29:00 +08:00 | | | ObjectSetInteger(0,name,OBJPROP_XDISTANCE,g_ui_corner==PRG_LEFT ? g_ui_x+x : g_ui_x+UI_W-x);
|
2026-10-21 16:07:46 +08:00 | | | ObjectSetInteger(0,name,OBJPROP_YDISTANCE,g_ui_y+y);
|
| | | ObjectSetInteger(0,name,OBJPROP_BGCOLOR,UiBack());
|
2026-10-31 14:29:00 +08:00 | | | ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,fg==clrNONE ? UiRule() : fg);
|
| | | ObjectSetInteger(0,name,OBJPROP_COLOR,fg==clrNONE ? UiHead() : fg);
|
2026-10-21 16:07:46 +08:00 | | | if(ObjectGetString(0,name,OBJPROP_TEXT)!=txt)
|
| | | ObjectSetString(0,name,OBJPROP_TEXT,txt);
|
| | | ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
| | | }
|
| | |
|
| | | //--- one "label value" row; returns the next y
|
| | | int Row(const string id,const int y,const string label,const string value,const color clr)
|
| | | {
|
| | | int fit=(UI_W-UI_VAL-UI_PAD)/UI_CHAR;
|
| | | Text(id+"_l",UI_PAD,y,label,UiLabel());
|
| | | Text(id+"_v",UI_VAL,y,StringLen(value)>fit ? StringSubstr(value,0,fit-1)+"~" : value,clr);
|
| | | return(y+UI_ROW);
|
| | | }
|
| | |
|
| | | int Section(const string id,const int y,const string title)
|
| | | {
|
| | | int top=y+UI_GAP;
|
| | | Box(id+"_r",UI_PAD,top+UI_ROW-3,UI_W-2*UI_PAD,1,UiRule(),UiRule());
|
| | | Text(id+"_t",UI_PAD,top,title,UiHead(),g_ui_font-1);
|
| | | return(top+UI_ROW+2);
|
| | | }
|
| | |
|
| | | void PanelDelete(void)
|
| | | {
|
2026-10-31 14:29:00 +08:00 | | | ObjectsDeleteAll(0,PRG_PREFIX);
|
2026-10-21 16:07:46 +08:00 | | | CacheClear();
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Formatting |
|
| | | //+------------------------------------------------------------------+
|
| | | string Thousands(const double v,const int digits=2)
|
| | | {
|
| | | string s=DoubleToString(MathAbs(v),digits);
|
| | | int dot=StringFind(s,".");
|
| | | string ip=dot>=0 ? StringSubstr(s,0,dot) : s;
|
| | | string fp=dot>=0 ? StringSubstr(s,dot) : "";
|
| | | string out="";
|
| | | int n=StringLen(ip);
|
| | | for(int i=0; i<n; i++)
|
| | | {
|
| | | if(i>0 && (n-i)%3==0)
|
| | | out+=",";
|
| | | out+=StringSubstr(ip,i,1);
|
| | | }
|
| | | return((v<0 ? "-" : "")+out+fp);
|
| | | }
|
| | | string Money(const double v) { return(Thousands(v)); }
|
2026-10-31 14:29:00 +08:00 | | | string SMoney(const double v) { return((v>0 ? "+" : "")+Thousands(v)); }
|
| | | color Sign(const double v) { return(v>0 ? UiPos() : (v<0 ? UiNeg() : UiDim())); }
|
2026-10-21 16:07:46 +08:00 | | | string Pct(const double v,const int d=1) { return(DoubleToString(v,d)+"%"); }
|
| | |
|
| | | #endif
|
| | | //+------------------------------------------------------------------+
|