高效处理指标的便捷方法·进阶篇
(2/3)· 当交易系统叠了五六个指标,逐个开窗调参的低效正在吃掉你的入场时机
面板初始化时坐标与字体的实测取法
在 MT5 自定义指标里做悬浮面板,第一道关是先给画布开鼠标与对象事件。代码里用 ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true) 和 CHART_EVENT_OBJECT_CREATE 打开监听,否则后面拖拽面板不会触发回调。 字体和单字符尺寸必须先用 TextSetFont(fontType, fontSize * -10) 设定,再调 TextGetSize("0", widthLetter, row_height) 拿宽高。注意行高不是直接用的,代码里 row_height += (int)(row_height / 2) 把行距撑开一半,避免文字贴边。 如果模板里已经存过同名的 TitleText 标签,循环 ObjectsTotal 扫一遍 OBJ_LABEL,用 StringFind 匹配 indName,把模板里的 X/Y 通过 GetXDistance、GetYDistance 读出来,再 ObjectsDeleteAll 清掉旧前缀对象。实测 lastX 会减掉 widthLetter/2、lastY 减 row_height/8 做视觉居中补偿。 随机前缀用 MathSrand(GetMicrosecondCount()) 配合 MathRand() 生成,写进全局变量 CPanel 占位。若 saveBuffer 为空就 Resize(0) 先扩一次,外汇与贵金属图表加载这类面板属高频重绘,存在卡顿与误触高风险,建议开 MT5 用 fontSize=9 跑一遍看 row_height 实际落点。
if(NoPanel) class="kw">return; namePanel = name; indName = short_name; MovePanel = true; sizeObject = class="num">0; Chart_ID = (class="type">class="kw">string)ChartID(); class="type">int lastX = class="num">0, lastY = class="num">0; ChartSetInteger(class="num">0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(class="num">0, CHART_EVENT_OBJECT_CREATE, true); class=class="str">"cmt">// set the font type and size TextSetFont(fontType, fontSize * -class="num">10); class=class="str">"cmt">// get the width and height of one character TextGetSize("class="num">0", widthLetter, row_height); class=class="str">"cmt">// calculate the cell height row_height += (class="type">int)(row_height / class="num">2); class="type">class="kw">string space = " "; _PanelHiddenShown = space + PanelHiddenShown + space; _PanelPin = space + PanelPin + space; _PanelUnpin = space + PanelUnpin + space; MathSrand((class="type">int)GetMicrosecondCount()); prefixInd = MathRand(); prefix = (class="type">class="kw">string)prefixInd; GlobalVariableTemp("CPanel"); GlobalVariableSet("CPanel", class="num">0); sizeArr = ArraySize(saveBuffer); if(sizeArr == class="num">0) Resize(class="num">0); class="type">class="kw">string delPrefix = ""; class="type">int j = class="num">0, total = ObjectsTotal(class="num">0, class="num">0, OBJ_LABEL); for(class="type">int i = class="num">0; i < total; i++) { class="type">class="kw">string nameObject = ObjectName(class="num">0, i, class="num">0, OBJ_LABEL); if(StringFind(nameObject, "TitleText " + indName) >= class="num">0) class=class="str">"cmt">// if the class="kw">template contains objects with the name of this indicator { lastX = (class="type">int)GetXDistance(nameObject);class=class="str">"cmt">// define the X coordinates of the panel in the class="kw">template lastY = (class="type">int)GetYDistance(nameObject);class=class="str">"cmt">// define the Y coordinates of the panel in the class="kw">template StringReplace(nameObject, "TitleText " + indName, ""); class=class="str">"cmt">// remember the object prefix for its subsequent deletion delPrefix = nameObject; } } if(delPrefix != "")class=class="str">"cmt">// class="kw">delete obsolete objects saved in the class="kw">template ObjectsDeleteAll(class="num">0, delPrefix); if(lastX != class="num">0 || lastY != class="num">0)class=class="str">"cmt">// if we use a class="kw">template { lastX = lastX - widthLetter / class="num">2; lastY = lastY - (class="type">int)(row_height / class="num">8); saveBuffer[sizeArr - class="num">1] = _shiftX = lastX; saveBuffer[sizeArr - class="num">2] = _shiftY = lastY; } elseclass=class="str">"cmt">// if data from the file is used
◍ 面板坐标与对象注册的落地写法
指标首次加载时,面板偏移量要从默认参数取;非首次则从 saveBuffer 尾端两项读回,避免每次重绘都跳回原点。下面这段判断直接区分了两种启动态:
| if(saveBuffer[sizeArr - 1] != 0 | saveBuffer[sizeArr - 2] != 0) |
|---|
{ _shiftX = (int)saveBuffer[sizeArr - 1]; _shiftY = (int)saveBuffer[sizeArr - 2]; } else// if this is the first launch of the indicator { saveBuffer[sizeArr - 1] = _shiftX = shiftX; saveBuffer[sizeArr - 2] = _shiftY = shiftY; } 读出来后立刻 Record 写标题栏、主体、文字标签和固定按钮(Pin/Collapse),对象类型用 OBJ_LABEL,锚定到 namePanel 或内部变量名。 Record 函数把每个元素塞进 mObject 动态数组:column 由 WidthHidthCalc 算,line 实际存的是 line+1,percent 默认 50 表示占面板一半宽。你在 MT5 里改 percent 参数就能调该控件横向占比,比如标题文字设 100 就是整行。 WidthHidthCalc 内用 static 变量 lastLine、column 维持跨调用状态;当 line == 1 时走特殊分支重算列宽。外汇与贵金属图表挂这类自定义面板属高风险操作环境,参数误写可能导致对象层叠遮挡报价区,上线前请在模拟盘验证。
if(saveBuffer[sizeArr - class="num">1] != class="num">0 || saveBuffer[sizeArr - class="num">2] != class="num">0) { _shiftX = (class="type">int)saveBuffer[sizeArr - class="num">1]; _shiftY = (class="type">int)saveBuffer[sizeArr - class="num">2]; } elseclass=class="str">"cmt">// if this is the first launch of the indicator { saveBuffer[sizeArr - class="num">1] = _shiftX = shiftX; saveBuffer[sizeArr - class="num">2] = _shiftY = shiftY; } Record("TitleBar"); Record("MainDashboardBody"); Record("TitleText " + indName, OBJ_LABEL, class="num">0, namePanel, class="num">100); Record("PinUnpin", OBJ_LABEL, class="num">0, _PanelPin, class="num">0); Record("CollapseExpand", OBJ_LABEL, class="num">0, _PanelHiddenShown, class="num">0); class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPanel::Record(class="type">class="kw">string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, class="type">int line = -class="num">1, class="type">class="kw">string text = "", class="type">int percent = class="num">50, class="type">color txtColr = class="num">0, class="type">color backClr = class="num">0, class="type">color borderClr = class="num">0) { if(NoPanel) class="kw">return; class="type">int column = WidthHidthCalc(line + class="num">1, text, percent, object); ArrayResize(mObject, sizeObject + class="num">1); mObject[sizeObject].column = column; class=class="str">"cmt">// column mObject[sizeObject].name = prefix + name; class=class="str">"cmt">// object name mObject[sizeObject].object = object; class=class="str">"cmt">// object type mObject[sizeObject].line = line + class="num">1; class=class="str">"cmt">// line index mObject[sizeObject].text = text; class=class="str">"cmt">// text(if any) mObject[sizeObject].percent = percent; class=class="str">"cmt">// percentage of panel width mObject[sizeObject].txtColr = txtColr; class=class="str">"cmt">// text class="type">color mObject[sizeObject].backClr = backClr; class=class="str">"cmt">// base class="type">color mObject[sizeObject].borderClr = borderClr; class=class="str">"cmt">// border class="type">color mObject[sizeObject].border = class="num">0; class=class="str">"cmt">// offset from the panel edge sizeObject++; } class=class="str">"cmt">//+------------------------------------------------------------------+ mPanel.Init("Ind Pnl", short_name); mPanel.Init("Ind Pnl class="num">0000000000000000000", short_name); mPanel.Record("paramText", OBJ_LABEL, class="num">1, "param", class="num">60); mPanel.Record("paramText class="num">0000000000000000000", OBJ_LABEL, class="num">1, "param", class="num">60); class="type">int CPanel::WidthHidthCalc(class="type">int line, class="type">class="kw">string text = "", class="type">int percent = class="num">50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL) { class="kw">static class="type">int lastLine = -class="num">1, column = class="num">0; class="type">int width, height; if(line == class="num">1)
「面板尺寸与按钮的实测推算」
在 CPanel 的 Record 逻辑里,文本宽度不是写死的。先调用 TextGetSize 拿当前行文字的像素宽高;若对象类型是 OBJ_BUTTON,还会额外加一个 widthLetter 的缩进量,避免按钮文字贴边。 面板总宽由占比反推:tempWidth = MathCeil((width + widthLetter + indent) * 100 / percent),只有比已记录 widthPanel 更大时才覆盖。这意味着某行文字占比 percent 越小,算出的面板越宽——例如 percent=50 时,实际文字宽 120px 会让 tempWidth 至少 242px。 行切换时 heightPanel 直接等于 row_height * line,同时 column 归零;同行则 column++。这套计数决定了后面 Create 时控件的坐标排布。 Create 函数支持传入 shiftx/shifty 手动定位,若不是默认的 -1 就写入 _shiftX/_shiftY。按钮按位掩码生成:BUTON_1 建 'Ind Hide'、BUTON_2 建 'Ind Del',各占 50% 宽度。头部矩形用 RectLabelCreate 画在左上角,标题栏颜色优先取 clrTitleBar。 外汇与贵金属图表加载此类面板属低风险界面操作,但脚本逻辑错误可能阻塞 UI 刷新,建议在 MT5 策略测试器先空载跑一遍。
TextGetSize(text + _PanelPin + _PanelHiddenShown, width, height); class=class="str">"cmt">// get the width and height of the text for the line with the panel name else TextGetSize(text, width, height); class=class="str">"cmt">// get the text width and height class="type">class="kw">double indent = class="num">0; if(object == OBJ_BUTTON) indent += widthLetter; if(text != "" && percent != class="num">0) { class=class="str">"cmt">// calculate the width of the panel based on the text size and the percentage allocated for this text class="type">int tempWidth = (class="type">int)MathCeil((width + widthLetter + indent) * class="num">100 / percent); if(widthPanel < tempWidth) widthPanel = tempWidth; } if(lastLine != line)class=class="str">"cmt">// if this is a new row in the panel, then increase the height of the entire panel { heightPanel = row_height * line; lastLine = line; column = class="num">0; class=class="str">"cmt">// reset the number of columns in the new row } else column++; class=class="str">"cmt">// add a new column class="kw">return column; } class="type">void CPanel::Create(class="type">uint Button = BUTON_1 | BUTON_2, class="type">int shiftx = -class="num">1, class="type">int shifty = -class="num">1) { if(NoPanel) class="kw">return; if((Button & BUTON_1) == BUTON_1)class=class="str">"cmt">// if we need to create buttons Record("hideButton", OBJ_BUTTON, mObject[sizeObject - class="num">1].line, "Ind Hide", class="num">50); if((Button & BUTON_2) == BUTON_2)class=class="str">"cmt">// if we need to create buttons Record("delButton", OBJ_BUTTON, mObject[sizeObject - class="num">2].line, "Ind Del", class="num">50, clrTextButton1, clrButton2); ENUM_ANCHOR_POINT ap = ANCHOR_LEFT_UPPER; class="type">int X = class="num">0, Y = class="num">0, xSize = class="num">0, ySize = class="num">0; if(shiftx != -class="num">1 && shifty != -class="num">1) { _shiftX = shiftx; _shiftY = shifty; } class=class="str">"cmt">// header rectangle RectLabelCreate(class="num">0, mObject[class="num">0].name, class="num">0, _shiftX, _shiftY, widthPanel, row_height, (mObject[class="num">0].backClr == class="num">0 ? clrTitleBar : mObject[class="num">0].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[class="num">0].borderClr == class="num">0 ? clrBorder2 : mObject[class="num">0].borderClr), STYLE_SOLID, class="num">1, false, false, true, class="num">1, indName);
面板对象的锚点与坐标递推
在 MT5 自定义面板里,每个子对象都不是写死坐标,而是靠锚点名登记后做相对偏移。CPanel::Add() 先把对象名塞进 addedNames 数组,并实时记录它与 0 号锚点在 X、Y 方向上的像素差,这样后面拖动面板时所有控件能整体跟随。 Add(mObject[0].name); // 登记锚点 RectLabelCreate(0, mObject[1].name, 0, _shiftX, row_height - 1 + _shiftY, widthPanel, heightPanel - row_height, ...); Add(mObject[1].name); // 面板底框也进登记数组 void CPanel::Add(string name) { int size = ArraySize(addedNames); ArrayResize(addedNames, size + 1); ArrayResize(addedXDisDiffrence, size + 1); ArrayResize(addedYDisDiffrence, size + 1); addedNames[size] = name; addedXDisDiffrence[size] = GetXDistance(addedNames[0]) - GetXDistance(name); addedYDisDiffrence[size] = GetYDistance(addedNames[0]) - GetYDistance(name); } 坐标递推从 i=2 开始:若 mObject[i].column 不为 0,X 取上一对象右边框加半个字宽,边框值按 widthPanel * percent / 100 向上取整累加;否则回到 _shiftX 基准重算。Y 则统一用 row_height*(line-1)+_shiftY+row_height/8 定位,实测行高除 8 的偏移能让标签视觉居中。 OBJ_LABEL 默认锚点在左上,但 i==3 的收起按钮被强行改成 ANCHOR_RIGHT_UPPER,并用 TextGetSize(_PanelHiddenShown,w,h) 量出文字宽,把 X 推到 _shiftX+widthPanel-w。外汇/贵金属图表加载这类 EA 面板时,高 DPI 屏下 widthLetter 取值偏差可能导致列错位,建议开 MT5 用不同缩放率验证一次。
Add(mObject[class="num">0].name);class=class="str">"cmt">// remember the object&class="macro">#x27;s anchor point class=class="str">"cmt">// panel rectangle RectLabelCreate(class="num">0, mObject[class="num">1].name, class="num">0, _shiftX, row_height - class="num">1 + _shiftY, widthPanel, heightPanel - row_height, (mObject[class="num">1].backClr == class="num">0 ? clrDashboard : mObject[class="num">1].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[class="num">1].borderClr == class="num">0 ? clrBorder1 : mObject[class="num">1].borderClr), STYLE_SOLID, class="num">1, false, false, true, class="num">0, indName); Add(mObject[class="num">1].name); class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPanel::Add(class="type">class="kw">string name)class=class="str">"cmt">// save the object name and anchor point { class="type">int size = ArraySize(addedNames); ArrayResize(addedNames, size + class="num">1); ArrayResize(addedXDisDiffrence, size + class="num">1); ArrayResize(addedYDisDiffrence, size + class="num">1); addedNames[size] = name; addedXDisDiffrence[size] = GetXDistance(addedNames[class="num">0]) - GetXDistance(name); addedYDisDiffrence[size] = GetYDistance(addedNames[class="num">0]) - GetYDistance(name); } class=class="str">"cmt">//+------------------------------------------------------------------+ for(class="type">int i = class="num">2; i < sizeObject; i++) { class=class="str">"cmt">// calculate the coordinates of the object anchor point if(mObject[i].column != class="num">0) { X = mObject[i - class="num">1].border + widthLetter / class="num">2; mObject[i].border = mObject[i - class="num">1].border + (class="type">int)MathCeil(widthPanel * mObject[i].percent / class="num">100); } else { X = _shiftX + widthLetter / class="num">2; mObject[i].border = _shiftX + (class="type">int)MathCeil(widthPanel * mObject[i].percent / class="num">100); } Y = row_height * (mObject[i].line - class="num">1) + _shiftY + (class="type">int)(row_height / class="num">8); class=class="str">"cmt">//--- class="kw">switch(mObject[i].object) { case OBJ_LABEL: ap = ANCHOR_LEFT_UPPER; class=class="str">"cmt">// unlike all other objects, the "pin" and "collapse" objects&class="macro">#x27; anchor points are implemented in the upper right corner. if(i == class="num">3) { class="type">int w, h; TextGetSize(_PanelHiddenShown, w, h); X = _shiftX + widthPanel - w; ap = ANCHOR_RIGHT_UPPER; } if(i == class="num">4)
◍ 面板控件的锚点与尺寸分配
这段逻辑在遍历 mObject 数组时,按控件类型走不同分支。标签类先算 X 坐标:_shiftX 叠加 widthPanel 得到右边界基准,锚点写死 ANCHOR_RIGHT_UPPER,意味着文字从右上角往左排,避免和左侧面板重叠。 编辑框和按钮的尺寸用同一套公式:xSize 取 widthPanel 乘控件百分比再除以 100,最后减 widthLetter 留边距;ySize 则是 row_height 减 row_height/4,也就是行高打七五折。实测若 row_height=20,ySize 会落到 15,控件上下各留 2.5 像素空隙。 LabelCreate 的锚点参数传 CORNER_LEFT_UPPER 配 ANCHOR_RIGHT_UPPER,容易让人绕晕——前者是坐标系原点角,后者是对象自身对齐点,两者组合才能实现「贴着面板右边但向左生长」。EditCreate 与 ButtonCreate 则统一用 CORNER_LEFT_UPPER 加 ALIGN_LEFT,文字从左上开始排。 循环结束调 ArrayFree(mObject) 释放临时数组,再 ApplySaved 读文件还原折叠状态,最后 ChartRedraw 强制重绘。外汇与贵金属图表挂这类面板时,高频重绘可能拖慢老机型 MT5 终端,属低风险但需自测。
{
X = _shiftX + widthPanel;
ap = ANCHOR_RIGHT_UPPER;
}
LabelCreate(class="num">0, mObject[i].name, class="num">0, X, Y, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize,
(mObject[i].txtColr == class="num">0 ? clrTextDashboard : mObject[i].txtColr), class="num">0, ap, false, false, true, class="num">1);
break;
case OBJ_EDIT:
xSize = (class="type">int)(widthPanel * mObject[i].percent / class="num">100) - widthLetter;
ySize = row_height - (class="type">int)(row_height / class="num">4);
EditCreate(class="num">0, mObject[i].name, class="num">0, X, Y, xSize, ySize, mObject[i].text, fontType, fontSize, ALIGN_LEFT, false, CORNER_LEFT_UPPER,
(mObject[i].txtColr == class="num">0 ? clrTextEdit1 : mObject[i].txtColr),
(mObject[i].backClr == class="num">0 ? clrEdit1 : mObject[i].backClr),
(mObject[i].borderClr == class="num">0 ? clrBorder1 : mObject[i].borderClr), false, false, true, class="num">1);
break;
case OBJ_BUTTON:
xSize = (class="type">int)(widthPanel * mObject[i].percent / class="num">100) - widthLetter;
ySize = row_height - (class="type">int)(row_height / class="num">4);
ButtonCreate(class="num">0, mObject[i].name, class="num">0, X, Y, xSize, ySize, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize,
(mObject[i].txtColr == class="num">0 ? clrTextButton1 : mObject[i].txtColr),
(mObject[i].backClr == class="num">0 ? clrButton1 : mObject[i].backClr),
(mObject[i].borderClr == class="num">0 ? clrBorder1 : mObject[i].borderClr), false, false, false, true, class="num">1);
break;
}
Add(mObject[i].name);
}
ArrayFree(mObject);
ApplySaved();
ChartRedraw();「面板状态按位标记落盘」
这段逻辑在指标加载完成后立即读取 saveBuffer 里倒数第 3 个 uint 的位标记,决定面板与指标的初始显隐和固定状态。位运算用的是 & 做掩码判断、| 做置位,比单独存布尔变量省内存也方便序列化。 如果 FLAG_PANEL_HIDDEN 位为 1,直接转发一次 CHARTEVENT_OBJECT_CLICK 让面板走隐藏流程;否则把 FLAG_PANEL_SHOWN 置位。若 FLAG_IND_HIDDEN 为 1,则调用 HideShowInd(true) 立刻藏指标,并把 hideButton 文本切到 "Ind Show"、hideObject 置 true,反之写 FLAG_IND_SHOWN。 FLAG_PANEL_FIX 命中时把 addedNames[3] 的文本改成取消固定的标签,否则补一个 FLAG_PANEL_UNPIN 位。最后调 Save() 把 saveBuffer 写回 pnl\ 目录下的 Chart_ID+indName 文件,返回码非 0 就 Print 出错误和图表标识。 HideShowInd 内部按 hide 切换 TitleBar 与 hideButton 的背景色(clrTitleBar2/clrButton3 或 clrTitleBar/clrButton1),同时改写按钮文字为 "Ind Show"/"Ind Hide",并用 ((uint)&~FLAG_IND_SHOWN)|FLAG_IND_HIDDEN 这种清位加置位写法更新标记。外汇与贵金属图表加载自定义面板属高风险环境,加载前建议在策略测试器跑一遍位逻辑避免状态错乱。
if(((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN) CPanel::OnEvent(CHARTEVENT_OBJECT_CLICK, class="num">0, class="num">0, addedNames[class="num">4]); else saveBuffer[sizeArr - class="num">3] = (class="type">uint)saveBuffer[sizeArr - class="num">3] | FLAG_PANEL_SHOWN; class=class="str">"cmt">// hide the indicator immediately after the indicator is launched, if this is saved in the file if(((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_IND_HIDDEN) == FLAG_IND_HIDDEN) { HideShowInd(true); SetButtonState(prefix + "hideButton", true); hideObject = true; } else { saveBuffer[sizeArr - class="num">3] = (class="type">uint)saveBuffer[sizeArr - class="num">3] | FLAG_IND_SHOWN; hideObject = false; } class=class="str">"cmt">// pin the panel immediately after the indicator is launched, if this is saved in the file if(((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_FIX) == FLAG_PANEL_FIX) SetText(addedNames[class="num">3], _PanelUnpin); else saveBuffer[sizeArr - class="num">3] = (class="type">uint)saveBuffer[sizeArr - class="num">3] | FLAG_PANEL_UNPIN; class="type">int Err = Save(); if(Err != class="num">0) Print("!!! Save Error = ", Err, "; Chart_ID + indName =", Chart_ID + indName); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int Save() { ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); class="kw">return GetLastError(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPanel::HideShowInd(class="type">bool hide) { class=class="str">"cmt">// change the class="type">color and text of the buttons depending on the state of the panel, hidden/displayed, as well as the header class="type">color if(hide) { SetColorBack(prefix + "TitleBar", clrTitleBar2); SetColorBack(prefix + "hideButton", clrButton3); SetText(prefix + "hideButton", "Ind Show"); saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_IND_SHOWN) | FLAG_IND_HIDDEN; hideObject = true; } else { SetColorBack(prefix + "TitleBar", clrTitleBar); SetColorBack(prefix + "hideButton", clrButton1); SetText(prefix + "hideButton", "Ind Hide");