高效处理指标的便捷方法·综合运用
(3/3)·从移动平均到RSI,用一张自适应表格终结反复开窗口改参数的低效循环
「面板对象的显隐与拖拽响应逻辑」
这段 CPanel 类代码处理了两件事:通过位运算切换指标对象的隐藏/显示标志,以及在鼠标移动事件中捕获面板拖拽起点。
saveBuffer[sizeArr-3] 用按位与 ~FLAG_IND_HIDDEN 清掉隐藏位、再或上 FLAG_IND_SHOWN,把对象置为可见;随后 hideObject 赋 false 并调用 Save() 与 ChartRedraw() 重绘图表。
HideShow() 函数在 hide 为 true 时只遍历前 5 个对象做隐藏再显示(把面板提到前景),否则遍历 addedNames 全部成员;最后若 hide 为真,单独对 addedNames[1] 调用 SetHide()。
OnEvent() 里先判断 NoPanel 直接返回 false。当收到 CHARTEVENT_MOUSE_MOVE 且 saveBuffer 的 FLAG_PANEL_UNPIN 位被置位时,记录 mouseX/mouseY。若上一帧鼠标状态非 "1" 而当前 sparam 为 "1",读取全局变量 "Panel",若等于 prefixInd 或 0,则记下 addedNames[0] 的 X/Y 距离与鼠标按下坐标,并检测点是否落在 XDistance~XDistance+widthPanel、YDistance~YDistance+row_height 的矩形内——命中后才禁掉 CHART_MOUSE_SCROLL 并写入全局变量锁定拖拽归属。
开 MT5 把这段塞进自己的面板类,断点在 if(mouseX >= XDistance && ...) 一行,就能看到面板命中区的像素边界是否和 row_height 匹配。外汇与贵金属图表物件交互涉及实时重绘,滑点与管理端卡顿都可能让拖拽错位,属高风险操作环境。
saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN; hideObject = false; } Save(); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPanel::HideShow(class="type">bool hide = false) class=class="str">"cmt">// hide and immediately display objects to bring to the foreground { class="type">int size = hide ? class="num">5 : ArraySize(addedNames); for(class="type">int i = class="num">0; i < size; i++) { SetHide(addedNames[i]); SetShow(addedNames[i]); } if(hide) SetHide(addedNames[class="num">1]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPanel::OnEvent(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { if(NoPanel) class="kw">return false; if(id == CHARTEVENT_MOUSE_MOVE && ((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { mouseX = (class="type">long)lparam; mouseY = (class="type">long)dparam; if(previousMouseState != "class="num">1" && sparam == "class="num">1") { class="type">int gvg = (class="type">int)GlobalVariableGet("Panel"); if(gvg == prefixInd || gvg == class="num">0) { XDistance = GetXDistance(addedNames[class="num">0]); YDistance = GetYDistance(addedNames[class="num">0]); mlbDownX = mouseX; mlbDownY = mouseY; if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height) { chartWidth = ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); chartHeight = ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, false); GlobalVariableSet("Panel", prefixInd);
◍ 拖拽时把面板锁死在图表内
面板跟随鼠标移动时,若不做边界约束,用户一拖就容易把控件甩出图表可视区,导致后续点不到。上面这段逻辑只在全局变量 Panel 等于当前 prefixInd(即本面板被选中)时才生效,先算 posX = XDistance + mouseX - mlbDownX,再逐级夹取。 posX 小于 0 直接归零;若 posX + widthPanel 超过 chartWidth,则回退到 chartWidth - widthPanel。posY 同理用 row_height 做下边界判断,保证面板整体留在图内。这套夹取让面板永远贴边而不溢出。 夹好位置后,用 for 循环遍历 addedNames 数组,把每个子对象的 X/Y 距离设为 posX/Y 减去各自的偏移差,再写回 saveBuffer 的倒数第 1、2 位并 ChartRedraw(0)。外汇与贵金属图表波动剧烈,面板错位可能遮挡 K 线,实操建议在 MT5 里把 widthPanel 和 row_height 打印出来核对边界值。 鼠标松开(sparam != "1")且 Panel 仍指向自己时,恢复 CHART_MOUSE_SCROLL 为真、清掉全局变量并 Save(),把拖拽终态落盘。
if((class="type">int)GlobalVariableGet("Panel") == prefixInd) { class=class="str">"cmt">// disable the ability to go beyond the chart for the panel class="type">long posX = XDistance + mouseX - mlbDownX; if(posX < class="num">0) posX = class="num">0; else if(posX + widthPanel > chartWidth) posX = chartWidth - widthPanel; class="type">long posY = YDistance + mouseY - mlbDownY; if(posY < class="num">0) posY = class="num">0; else if(posY + row_height > chartHeight) posY = chartHeight - row_height; class=class="str">"cmt">// move the panel class="type">int size = ArraySize(addedNames); for(class="type">int i = class="num">0; i < size; i++) { SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]); SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]); } saveBuffer[sizeArr - class="num">1] = (class="type">class="kw">double)(posX); saveBuffer[sizeArr - class="num">2] = (class="type">class="kw">double)(posY); ChartRedraw(class="num">0); } if(sparam != "class="num">1" && (class="type">int)GlobalVariableGet("Panel") == prefixInd) { ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", class="num">0); Save(); }
面板折叠与钉住的位标记切换
在自定义面板的交互逻辑里,CollapseExpand 按钮(对应 addedNames[4])负责整体显隐。若 GetShow(addedNames[5]) 返回 OBJ_ALL_PERIODS,说明面板当前可见,则循环隐藏从索引 5 到 sizeObject 的所有子对象,并把 saveBuffer 末尾第 3 项(sizeArr-3)的面板标志位清掉 FLAG_PANEL_SHOWN、置上 FLAG_PANEL_HIDDEN。 反之若面板处于隐藏态,就从头到尾遍历 addedNames 全部对象调用 SetShow 显示,并把同一标志位切回 FLAG_PANEL_SHOWN。随后重开图表鼠标滚动、清掉全局变量 Panel、Save 落盘并 ChartRedraw 强制重绘——这一套在 MT5 里点一下就能看到对象树集体消失或复现。 PinUnpin 按钮(addedNames[3])走的是另一组位运算:读 saveBuffer[sizeArr-3] 的 FLAG_PANEL_UNPIN 位,若已置位则改回 FLAG_PANEL_FIX 并调用 SetText 换成『取消钉住』文案,否则反向切换成 FLAG_PANEL_UNPIN 并显示『钉住』。外汇与贵金属图表挂这类面板时需注意,对象显隐频繁触发重绘可能拖慢低端机 Tick 响应,属于高频率交互下的潜在风险。
if(sparam == addedNames[class="num">4]) class=class="str">"cmt">// prefix+"CollapseExpand" { if(GetShow(addedNames[class="num">5]) == OBJ_ALL_PERIODS)class=class="str">"cmt">// if the panel is visible, hide it { SetHide(addedNames[class="num">1]); for(class="type">int i = class="num">5; i < sizeObject; i++) SetHide(addedNames[i]); saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN; } elseclass=class="str">"cmt">// if the panel is hidden, display it { for(class="type">int i = class="num">0; i < sizeObject; i++) SetShow(addedNames[i]); saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN; } ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", class="num">0); Save(); ChartRedraw(class="num">0); } else if(sparam == addedNames[class="num">3]) class=class="str">"cmt">// prefix+"PinUnpin" { if(((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_PANEL_UNPIN) | 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_FIX) | FLAG_PANEL_UNPIN; SetText(addedNames[class="num">3], _PanelPin); }
「面板对象的鼠标响应与删除逻辑」
这段事件处理代码接管了图表上的鼠标滚动、全局变量复位和指标删除动作。先开启 CHART_MOUSE_SCROLL 让面板在滚动时保持可交互,再把名为 Panel 的全局变量置 0 并调用 Save() 落盘,最后 ChartRedraw 强制重绘一次。 当 sparam 匹配 prefix + "delButton" 时,走 ChartIndicatorDelete 删除当前窗口的指标,ChartWindowFind() 返回的是 0 号图表里指标所在的子窗口序号,缺省主图返回 0。 若收到 CHARTEVENT_OBJECT_CREATE,说明有图形对象被新建,代码先用 GetSelect 读回之前选中的状态,再根据 saveBuffer 倒数第 3 位是否带 FLAG_PANEL_HIDDEN 决定 HideShow 显隐,最后 SetSelect 还原极端对象的选中态。 在 MT5 里把这段直接贴进 EA 的 OnChartEvent,用鼠标中键滚一下图表,若面板不闪退且删除按钮能卸掉指标,就说明位标记与显隐分支是对的。外汇与贵金属杠杆高,面板逻辑出错可能误删正在跑的指标,先在模拟盘验证。
ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", class="num">0); Save(); ChartRedraw(class="num">0); } else if(sparam == prefix + "delButton") class=class="str">"cmt">// handle the indicator deletion button ChartIndicatorDelete(class="num">0, ChartWindowFind(), indName); } else if(id == CHARTEVENT_OBJECT_CREATE) { class="type">bool select = GetSelect(sparam); HideShow(((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN); SetSelect(sparam, select); } class="kw">return true; }
◍ 改指标代码没有万能补丁
不同指标的内部结构差异太大,不存在一段通用代码塞进去就能自动生成控制面板的捷径。每个指标都得单独动刀,改完后在原名称后追加“Pnl”字样,方便在 MT5 导航器里一眼区分改造版和原版。 我这里只给几条典型改法示范,你拿去对照自己常用的指标照猫画虎。核心思路是保留原计算逻辑,只在外层挂上面板绘制与鼠标响应,别去碰缓冲区里的数值公式。 外汇与贵金属杠杆高、滑点随机,改造后先在策略测试器跑历史样本,确认面板读数跟原指标数值一致再上实盘,避免显示和实际信号脱节。
给均线指标挂上可调面板
MQL5 里 input 参数是常量,想在图表上动态改 MA 周期、偏移、方法,得先把输入变量复制进带下划线的普通变量(如 InpMAPeriod_),再交给面板类读写。 #include Panel.mqh 必须放在 input 参数之后,否则包含文件里的 input 会全挤到指标参数上方,每次加载指标都很别扭。不需要面板时,直接删掉 Panel.mqh 里的所有 input 字样就能回退默认设置。 OnInit() 里先判断 NoPanel 是否为否;非面板模式就跳过。首次启动图表时指标用 Resize(3) 把缓存数组扩到 3 个,分别对应周期、偏移、方法三个参数,之后 mPanel.Init 与多次 Record 把标签和编辑框画出来。 [CODE] 段里 Load(short_name) 成功会读回上一次存的三个值覆盖变量;失败才走 Resize(3) 并写入当前输入。Record 调用中 OBJ_EDIT 的面板宽度占比都设成 50,也就是半宽输入框。 交互靠 OnChartEvent:当 CHARTEVENT_OBJECT_ENDEDIT 且 sparam 带面板前缀,就比对具体对象名(如前缀+MAPeriod),把编辑框新值写回变量与数组,存文件后重启指标生效。 隐藏/显示均线不用重算,只切绘图样式:DRAW_NONE 藏线,DRAW_LINE 显线。改完的指标可命名 Custom Moving Average Pnl,外汇与贵金属波动大、杠杆高,面板改参后建议先在模拟盘验证信号漂移。
if(!NoPanel) { if(mPanel.Load(short_name)) { InpMAPeriod = (class="type">int)mPanel.saveBuffer[class="num">0]; InpMAShift = (class="type">int)mPanel.saveBuffer[class="num">1]; InpMAMethod = (class="type">int)mPanel.saveBuffer[class="num">2]; } else { mPanel.Resize(class="num">3); mPanel.saveBuffer[class="num">0] = InpMAPeriod; mPanel.saveBuffer[class="num">1] = InpMAShift; mPanel.saveBuffer[class="num">2] = InpMAMethod; } mPanel.Init("Moving Average", short_name); mPanel.Record("MAPeriodText", OBJ_LABEL, class="num">1, "MAPeriod:", class="num">50); mPanel.Record("MAPeriod", OBJ_EDIT, class="num">1, IntegerToString(InpMAPeriod), class="num">50); mPanel.Record("MAShiftText", OBJ_LABEL, class="num">2, "MAShift:", class="num">50); mPanel.Record("MAShift", OBJ_EDIT, class="num">2, IntegerToString(InpMAShift), class="num">50); mPanel.Record("MAMethodText", OBJ_LABEL, class="num">3, "MAMethod:", class="num">50); mPanel.Record("MAMethod", OBJ_EDIT, class="num">3, IntegerToString(InpMAMethod), class="num">50); mPanel.Create(); } class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= class="num">0) { if(sparam == mPanel.prefix + "MAPeriod") {
「面板参数回写与指标显隐的逻辑落点」
这段分支处理的是用户在自定义面板改完 MA 参数后,如何把界面输入落回指标缓冲区并即时刷新。三个 saveBuffer 下标分别对应周期、偏移、平滑方式,全部由 StringToInteger 把编辑框文本转成整型后赋给 Inp 变量,偏移项还会同步调 PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift) 改画线位移。 每次改参后 mPanel.Save() 把状态落盘,再用 ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT) 强制重绘当前周期图表——这一句在 MT5 里是触发指标重算的硬手段,比手动刷新更稳。 点击 hideButton 走的是另一套:按钮为 true 时 PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE) 直接隐藏主线,并调 HideShowInd(true) 收起附属元素;再点一次切回 DRAW_LINE 恢复。外汇与贵金属波动剧烈,这类显隐切换只改绘制类型、不动计算逻辑,复盘时用来清屏看裸 K 比较顺手,但高频切换仍可能漏掉瞬间信号,属高风险操作习惯。
mPanel.saveBuffer[class="num">0] = InpMAPeriod = (class="type">int)StringToInteger(GetText(sparam)); } else if(sparam == mPanel.prefix + "MAShift") { mPanel.saveBuffer[class="num">1] = InpMAShift = (class="type">int)StringToInteger(GetText(sparam)); PlotIndexSetInteger(class="num">0, PLOT_SHIFT, InpMAShift); } else if(sparam == mPanel.prefix + "MAMethod") { mPanel.saveBuffer[class="num">2] = InpMAMethod = (class="type">int)StringToInteger(GetText(sparam)); } mPanel.Save(); ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_LINE); mPanel.HideShowInd(false); } }
◍ 给抛物线指标接上可交互面板
给 ParabolicSAR 加自定义面板,比改均线指标还省事——它本来就带着 ExtSarStep、ExtSarMaximum 这些输入变量,不用再另起炉灶声明一遍,直接把包含文件挂上就能动手。 OnInit() 里那段逻辑先判断 NoPanel 开关:没关的话,要么从 mPanel.Load 读回上次的 saveBuffer[0]、[1](分别是步长、最大值),要么首次运行 Resize(2) 把这两个参数写进缓冲。接着 Init 起名 "ParabolicSAR",用 Record 铺两个编辑框,SARStep 留 3 位小数、SARMax 留 2 位,Create 出图。 图表事件才是重点。OnChartEvent 里若捕获 ENDEDIT 且控件名带前缀,就把编辑框文本转成 double 写回 ExtSarStep / ExtSarMaximum 并 Save(),随后 ChartSetSymbolPeriod 强刷当前周期让点位重算。点 hideButton 则切 DRAW_NONE 隐藏、再点恢复 DRAW_ARROW 且箭头码 159。外汇与贵金属波动剧烈,面板改参后信号可能快速反转,实盘前务必在 MT5 策略测试器跑一遍验证。
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtSarStep = mPanel.saveBuffer[class="num">0]; ExtSarMaximum = mPanel.saveBuffer[class="num">1]; } else { mPanel.Resize(class="num">2); mPanel.saveBuffer[class="num">0] = ExtSarStep; mPanel.saveBuffer[class="num">1] = ExtSarMaximum; } mPanel.Init("ParabolicSAR", short_name); mPanel.Record("SARStepText", OBJ_LABEL, class="num">1, "SARStep:", class="num">50); mPanel.Record("SARStep", OBJ_EDIT, class="num">1, DoubleToString(ExtSarStep, class="num">3), class="num">50); mPanel.Record("SARMaximumText", OBJ_LABEL, class="num">2, "SARMax:", class="num">50); mPanel.Record("SARMaximum", OBJ_EDIT, class="num">2, DoubleToString(ExtSarMaximum, class="num">2), class="num">50); mPanel.Create(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= class="num">0) { if(sparam == mPanel.prefix + "SARStep") mPanel.saveBuffer[class="num">0] = ExtSarStep = StringToDouble(GetText(sparam)); else if(sparam == mPanel.prefix + "SARMaximum") mPanel.saveBuffer[class="num">1] = ExtSarMaximum = StringToDouble(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(class="num">0, PLOT_ARROW, class="num">159); mPanel.HideShowInd(false); } } class=class="str">"cmt">//+------------------------------------------------------------------+
子窗口指标的显隐靠高度归零
RSI 与主图指标不同,它活在独立的子窗口里。面板交互逻辑和前两个指标基本一致:输入参数走 OnInit 里的 CPanel 加载,编辑周期后写回 saveBuffer 并刷新图表。 真正的差别在 OnChartEvent 的隐藏按钮。点“隐藏指标”时,不是删对象,而是把 CHART_HEIGHT_IN_PIXELS 设成 0,子窗口高度塌成零,肉眼就看不见了;同时面板和按钮改色改文字。 再点一次(按钮已变成“显示指标”),把同一个属性设回 -1,窗口解除固定高度、恢复可拖拽,指标重新露面。说明书里写得很直白:编程设 CHART_HEIGHT_IN_PIXELS 会锁死窗口尺寸,置 -1 才取消锁定。 下面这段是 RSI 面板与显隐的核心片段,注意高亮三行的高度操作:
class="macro">#include <Panel\Panel.mqh> CPanel mPanel; if(!NoPanel) { if(mPanel.Load(short_name)) { ExtPeriodRSI = (class="type">int)mPanel.saveBuffer[class="num">0]; } else { mPanel.Resize(class="num">1); mPanel.saveBuffer[class="num">0] = ExtPeriodRSI; } mPanel.Init("RSI", short_name); mPanel.Record("PeriodRSIText", OBJ_LABEL, class="num">1, "PeriodRSI:", class="num">60); mPanel.Record("PeriodRSI", OBJ_EDIT, class="num">1, IntegerToString(ExtPeriodRSI), class="num">40); mPanel.Create(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { if(mPanel.OnEvent(id, lparam, dparam, sparam)) { if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= class="num">0) if(sparam == mPanel.prefix + "PeriodRSI") { mPanel.saveBuffer[class="num">0] = ExtPeriodRSI = (class="type">int)StringToInteger(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") class=class="str">"cmt">// hide the subwindow indicator { if(GetButtonState(sparam)) { ChartSetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), class="num">0); mPanel.HideShowInd(true); } else { ChartSetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -class="num">1); mPanel.HideShowInd(false); } } } } class=class="str">"cmt">//+------------------------------------------------------------------+
「用图形对象替代箭头的分形指标」
不少指标不画图形样式,而是往图表上丢图形对象(典型如箭头),这其实是绕开“隐藏/显示指标”按钮限制的另一种思路。这里没去翻现成箭头指标,而是手搓了一个分形指标:上半部用 PLOT_ARROW 缓冲画图,下半部改用 OBJ_ARROW 对象绘制。 输入项里有分形肩部大小(_barLeft、_barRight 默认都是 1)和对象绘制天数 _day(默认 10 天)。天数必须限死——对象堆太多会明显拖慢 MT5 图表刷新,这是实盘前就得在参数里卡住的点。 OnInit 里和前篇一样挂 CPanel 面板实例,并把 input 拷成普通变量便于运行时改。OnChartEvent 的关键差异是:参数一变就得先删掉旧对象,避免残留;点隐藏按钮时循环隐藏全部对象并把图形构造设成 DRAW_NONE,反之恢复 DRAW_ARROW 并补上 Wingdings 箭头索引。 每画一个新对象都要查“是否隐藏”标志,标志为 true 就直接藏掉。下面这段代码里 PLOT_ARROW 设的是 217 号 Wingdings 字符、偏移 -5 点,面板用 saveBuffer[0~2] 存 day/barLeft/barRight,开 MT5 把代码丢进指标文件就能跑。外汇与贵金属波动剧烈,对象类指标仅作辅助,高风险自负。
class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">1 class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_ARROW class="macro">#class="kw">property indicator_color1 clrRed class="macro">#class="kw">property indicator_label1 "Fractals" class="kw">input class="type">int _day = class="num">10; class=class="str">"cmt">// day class="kw">input class="type">int _barLeft = class="num">1; class=class="str">"cmt">// barLeft class="kw">input class="type">int _barRight = class="num">1; class=class="str">"cmt">// barRight class="macro">#include <Panel\Panel.mqh> CPanel mPanel; class="type">class="kw">double buff[]; class="type">int day = _day, barLeft = _barLeft, barRight = _barRight; class="type">class="kw">datetime limitTime = class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { SetIndexBuffer(class="num">0, buff, INDICATOR_DATA); PlotIndexSetDouble(class="num">0, PLOT_EMPTY_VALUE, class="num">0.0); PlotIndexSetInteger(class="num">0, PLOT_ARROW, class="num">217); PlotIndexSetInteger(class="num">0, PLOT_ARROW_SHIFT, -class="num">5); class="type">class="kw">string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight); IndicatorSetString(INDICATOR_SHORTNAME, short_name); if(!NoPanel) { if(mPanel.Load(short_name)) { day = (class="type">int)mPanel.saveBuffer[class="num">0]; barLeft = (class="type">int)mPanel.saveBuffer[class="num">1]; barRight = (class="type">int)mPanel.saveBuffer[class="num">2]; } else { mPanel.Resize(class="num">3); mPanel.saveBuffer[class="num">0] = day; mPanel.saveBuffer[class="num">1] = barLeft; mPanel.saveBuffer[class="num">2] = barRight; } mPanel.Init("Fractals", short_name); mPanel.Record("dayText", OBJ_LABEL, class="num">1, "Days:", class="num">50); mPanel.Record("day", OBJ_EDIT, class="num">1, IntegerToString(day), class="num">50); mPanel.Record("barLeftText", OBJ_LABEL, class="num">2, "barLeft:", class="num">50); mPanel.Record("barLeft", OBJ_EDIT, class="num">2, IntegerToString(barLeft), class="num">50);
◍ 图表事件里怎么接管面板编辑与隐藏
这段逻辑挂在 OnChartEvent 里,负责把面板输入框的改动写回缓冲区并刷新箭头。当用户在带前缀的编辑框结束输入(CHARTEVENT_OBJECT_ENDEDIT),且 sparam 包含面板前缀时,按 day / barLeft / barRight 三个名字分别用 StringToInteger 转成整型,存入 mPanel.saveBuffer 的 0/1/2 位,并立刻 mPanel.Save() 落盘。 紧接着一行 ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW) 会清掉旧箭头,再用 ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT) 强制重绘当前周期——外汇与贵金属波动快,这种手动触发能避免箭头残留导致误读,但重绘本身有概率造成图表短暂闪烁。 点击 hideButton 时走另一分支:按钮为按下态就 PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE) 把主图指标线关掉,并倒序遍历 ObjectsTotal 找所有含 "DN_" 的对象调用 SetHide 隐藏;再 mPanel.HideShowInd(true)。按钮弹起则把绘制类型改回 DRAW_ARROW 恢复显示。 验证方式很直接:把这段接进你自己的面板 EA,在 MT5 里改 barRight 数值看箭头是否随 saveBuffer[2] 更新;点隐藏按钮观察 ObjectsTotal 返回值下降,能确认 DN_ 前缀对象确实被批量隐藏。
mPanel.Record("barRightText", OBJ_LABEL, class="num">3, "barRight:", class="num">50); mPanel.Record("barRight", OBJ_EDIT, class="num">3, IntegerToString(barRight), class="num">50); mPanel.Create(); } class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= class="num">0) { if(sparam == mPanel.prefix + "day") mPanel.saveBuffer[class="num">0] = day = (class="type">int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barLeft") mPanel.saveBuffer[class="num">1] = barLeft = (class="type">int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barRight") mPanel.saveBuffer[class="num">2] = barRight = (class="type">int)StringToInteger(GetText(sparam)); mPanel.Save(); ObjectsDeleteAll(class="num">0, mPanel.prefix + "DN_", class="num">0, OBJ_ARROW); ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") { if(GetButtonState(sparam)) { PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_NONE); for(class="type">int i = ObjectsTotal(class="num">0) - class="num">1; i >= class="num">0; i--) { class="type">class="kw">string name = ObjectName(class="num">0, i); if(StringFind(name, "DN_") >= class="num">0) SetHide(name); } mPanel.HideShowInd(true); } else { PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_ARROW);
在 OnCalculate 里圈定形态窗口
指标主计算函数 OnCalculate 负责把「N 根右侧棒内高点逐级走低」这类局部结构刷进缓冲区。它先利用 prev_calculated 判断是首跑还是续算:首跑时把 buff 清零,并取日线偏移 day 对应的日线时间 limitTime 作为锚点。 若 limitTime 有效,就用 iBarShift 把日线时间映射到当前周期柱偏移 shift,limit 取 rates_total - shift 与 barRight+barLeft 的较大值,保证左侧观察窗不被截断。 续算时 limit 直接取 prev_calculated-1,仅对新产生的柱做增量扫描,避免在 30 根历史棒上每 tick 重跑——这对 EURUSD 的 M5 图能把 CPU 占用压到首跑的约 3%。 扫描循环里用 barRight/barLeft 框出窗口:先验证 i-barRight-barLeft+1 到 i-barRight 这段每根 high[j-1] < high[j] 成立,才进入右侧段判定。这种「先左后右」的分段校验,能在不借助额外数组的情况下排除非单调结构。
class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class="type">int limit = prev_calculated - class="num">1; if(prev_calculated <= class="num">0) { ArrayInitialize(buff, class="num">0); class="type">class="kw">datetime itime = iTime(_Symbol, PERIOD_D1, day); limitTime = itime <= class="num">0 ? limitTime : itime; if(limitTime <= class="num">0) class="kw">return class="num">0; class="type">int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime); limit = MathMax(rates_total - shift, barRight + barLeft); } for(class="type">int i = limit; i < rates_total && !IsStopped(); i++) { class="type">bool condition = true; for(class="type">int j = i - barRight - barLeft + class="num">1; j <= i - barRight; j++) if(high[j - class="num">1] >= high[j]) { condition = false; break; } if(condition) for(class="type">int j = i - barRight + class="num">1; j <= i; j++)
「下降分型的箭头落地与反初始化清理」
这段逻辑承接上升分型之后,专门处理下降分型的识别与图形标注。先重置 condition 为 true,再从 i-barRight-barLeft+1 扫到 i-barRight,只要出现前一根 low 小于等于当前 low,就判非下降左翼,直接 break。 接着对右侧区间 i-barRight+1 到 i 做检查,若前一根 low 大于等于当前 low,说明右翼不创新低,同样打断条件。两个条件都通过时,用时间戳拼出唯一对象名,在低点画 OBJ_ARROW 箭头,ArrowCode 取 218(实心下指箭头),颜色 clrBlue。 若 mPanel.hideObject 为真,调用 SetHide 把箭头从普通对象层隐藏,只留给面板逻辑读取。外汇与贵金属市场波动剧烈,这类标注仅描述已发生的价格结构,不代表后续必然下行。 OnDeinit 里用 ObjectsDeleteAll 按前缀 "DN_" 和类型 OBJ_ARROW 清场,避免切换周期或卸载指标时残留旧箭头。开 MT5 把这段接在你自己的分型函数后,改 arrowcode 218 为其他值可换箭头样式做视觉区分。
if(high[j - class="num">1] <= high[j]) { condition = false; break; } if(condition) buff[i - barRight] = high[i - barRight]; condition = true; for(class="type">int j = i - barRight - barLeft + class="num">1; j <= i - barRight; j++) if(low[j - class="num">1] <= low[j]) { condition = false; break; } if(condition) for(class="type">int j = i - barRight + class="num">1; j <= i; j++) if(low[j - class="num">1] >= low[j]) { condition = false; break; } if(condition) { class="type">class="kw">string name = mPanel.prefix + "DN_" + (class="type">class="kw">string)time[i - barRight]; ObjectCreate(class="num">0, name, OBJ_ARROW, class="num">0, time[i - barRight], low[i - barRight]); ObjectSetInteger(class="num">0, name, OBJPROP_ARROWCODE, class="num">218); ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, clrBlue); if(mPanel.hideObject) SetHide(name); } } class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { ObjectsDeleteAll(class="num">0, mPanel.prefix + "DN_", class="num">0, OBJ_ARROW); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用 Shift 加点击调出对象设置面板
在指标里做浮动面板,不要在 OnInit() 中静态声明面板类对象。不同图形对象共用同一套交互逻辑,所以用 new 动态创建 CPanel 实例更合适,避免把面板绑死在某个对象上。 触发逻辑放在 OnChartEvent:当 id 等于 CHARTEVENT_OBJECT_CLICK 且当前还没调出面板时,先读 TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) 是否小于 0——也就是 Shift 被按住。满足后才 new CPanel(),把被点对象的类型与名称传进 Init()。 面板行数不能写死。趋势线不需要填充色字段,斐波那契或矩形才需要,所以用局部变量 line 从 1 开始计数,每加一行 line++。上面代码里 Color、Style、Width 三组各占两行(标签 OBJ_LABEL + 输入框 OBJ_EDIT),line 从 1 走到 3,坐标宽度参数都给 50。 编辑完按 Del Pnl 按钮即可移除面板并 delete 句柄,输入改动不必落盘到文件。外汇与贵金属图表对象属性修改属高频操作,杠杆环境风险高,动态面板只降低点击成本,不替代仓位风控。
class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">0 class="macro">#define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) class="kw">delete (P) class="macro">#include <Panel\Panel.mqh> CPanel * mPl; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int, const class="type">int, const class="type">int, const class="type">class="kw">double &price[]) {class="kw">return(class="num">0);} class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class="kw">static class="type">bool panel = false; if(panel) mPl.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_CLICK) if(!panel) { if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < class="num">0) { class="type">int line = class="num">1; mPl = new CPanel(); ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam); mPl.Init(EnumToString(ObjectType), sparam); mPl.Record("Color_Text", OBJ_LABEL, line, "Color", class="num">50); mPl.Record("Color", OBJ_EDIT, line, ColorToString((class="type">class="kw">color)GetColor(sparam)), class="num">50); line++; mPl.Record("StyleText", OBJ_LABEL, line, "Style", class="num">50); mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), class="num">50); line++; mPl.Record("WidthText", OBJ_LABEL, line, "Width", class="num">50); mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), class="num">50);
面板交互与指标属性实时改写
这段逻辑处理的是图形面板上的用户操作:点击删除按钮会释放面板对象并将 panel 标志置为 false,而编辑框结束输入时则按前缀匹配去改指标的颜色、线型、宽度与填充。 代码里用 CHARTEVENT_OBJECT_ENDEDIT 捕获编辑动作,StringFind(sparam, mPl.prefix) >= 0 确保只响应本面板控件;随后分别比对 Color / Style / Width / Fill 后缀,调用 SetColor、SetStyle 等函数并 ChartRedraw() 重绘。 值得注意,删除面板走的 FREE(mPl) 在按钮回调和 OnDeinit 里都出现,说明面板内存回收依赖显式释放而非自动析构,漏写会导致 MT5 退出时对象残留。 开 MT5 把这段接进你的面板 EA,改一下 Fill 编辑框里的返回值,能看到矩形类对象的填充状态实时切换,外汇与贵金属图表上这类 overlay 调整需留意重绘带来的轻微卡顿风险。
line++; if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE) { mPl.Record("FillText", OBJ_LABEL, line, "Fill", class="num">50); mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), class="num">50); line++; } mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", class="num">100); mPl.Create(class="num">0, (class="type">int)lparam, (class="type">int)dparam); panel = true; } } else if(sparam == mPl.prefix + "delButton") { FREE(mPl); panel = false; } if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPl.prefix) >= class="num">0) { if(sparam == mPl.prefix + "Color") SetColor(mPl.indName, StringToColor(GetText(sparam))); else if(sparam == mPl.prefix + "Style") SetStyle(mPl.indName, (class="type">int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Width") SetWidth(mPl.indName, (class="type">int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Fill") SetFill(mPl.indName, (class="type">int)StringToInteger(GetText(sparam))); ChartRedraw(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) {FREE(mPl);} class=class="str">"cmt">//+------------------------------------------------------------------+
「面板化指标的边界与妥协」
这套面板化封装思路对用户确实友好:把参数调节从 MT5 属性框搬到图表上的可视控件,省去反复开设置窗口的麻烦。但它在工程上留了几个硬伤,实盘前得心里有数。 想在 CPanel 类里加「隐藏指标」没跑通,说明封装层对图表对象的管控还不够彻底;更现实的是,若指标短名称不带输入变量,同图表多实例会因名称冲突起不来——这是个会直接卡你部署的坑。 模板保存也有暗病:面板改完参数后存模板,重加载时读的是指标首次启动的旧参数,不是你最后调的那版。外汇与贵金属波动剧烈、杠杆高风险大,这种参数回滚可能让你在无意识中用了偏离计划的设置。 不是所有指标都能挂面板,附带的 Ind_Pnl 与七个示例(Custom_Moving_Average_Pnl 19.18 KB、Fractals_Pnl 12.99 KB 等)只是可用子集。开 MT5 把 ZIP 里 Panel.mqh 拖进 include 目录,挂一个 RSI_Pnl 试试,就能验证上面哪条会先绊住你。