DoEasy. 控件 (第 32 部分): 水平滚动条,鼠标轮滚动·综合运用
🖱️

DoEasy. 控件 (第 32 部分): 水平滚动条,鼠标轮滚动·综合运用

(3/3)· 从编译报错修复到滚轮操控容器,一篇把 WinForms 滚动条最后缺口补上

实战向 第 3/3 篇

很多人在拉取旧版 DoEasy 示例时一编译就报私密方法不可访问,却不知道是终端更新后编译器开始严格检查继承权限。还有人以为水平滚动条只能靠箭头按钮,从没试过用鼠标拖滑块或滚轮直接平移容器内容。

◍ ONNX 推理报错码的中文对照

在 MT5 里用 MQL5 跑 ONNX 模型时,运行时错误码落在 5800–5808 区间,系统会把俄文与英文双语消息存进二维数组 messages_runtime_onnx。 错误 5800 对应 ONNX 内部标准错误,5801 是 ONNX Runtime API 初始化失败,5802 表示属性或取值不被 MQL5 支持,5803 为 API 执行 run 时出错。

  • 专指传给 OnnxRun 的参数个数不对,5805 则是某个参数值本身非法;碰到这类返回,优先检查模型输入输出节点数量和 EA 里传的数组维度是否一致。

外汇与贵金属行情受杠杆与跳空影响,模型推理失败可能直接导致信号延迟或错单,实盘前务必在策略测试器用历史数据复现一次报错路径。

MQL5 / C++
class="type">class="kw">string messages_runtime_onnx[][TOTAL_LANG]=
  {
  {"Внутренняя ошибка ONNX стандарта","ONNX internal error"},                                                                                                        class=class="str">"cmt">// class="num">5800
  {"Ошибка инициализации ONNX Runtime API","ONNX Runtime API initialization error"},                                                                              class=class="str">"cmt">// class="num">5801
  {"Свойство или значение неподдерживаются языком MQL5","Property or value not supported by MQL5"},                                                             class=class="str">"cmt">// class="num">5802
  {"Ошибка запуска ONNX runtime API","ONNX runtime API run error"},                                                                                              class=class="str">"cmt">// class="num">5803
  {"В OnnxRun передано неверное количество параметров ","Invalid number of parameters passed to OnnxRun"},                                                     class=class="str">"cmt">// class="num">5804
  {"Некорректное значение параметра","Invalid parameter value"},                                                                                                 class=class="str">"cmt">// class="num">5805

「错误码映射与多语言文本检索的实现细节」

在 MT5 的 CMessage 类里,运行时错误文本被拆成多张二维数组,按 msg_id 的区间偏移取对应语言的字符串。上面片段列出了 5806–5808 三个与参数类型、尺寸、张量维度相关的报错条目,注释里的数字就是错误码本身。 GetTextByID 用嵌套三元运算符做路由:比如 msg_id 落在 4001–4025 区间时,实际访问的是 messages_runtime[msg_id-4000] 这一行,再把 m_lang_num 作为列下标取俄语或英语文本。图表类错误走 4101–4116(减 4101 偏移),图形对象走 4201–4205,市场信息 4301–4305,历史访问 4401–4407,全局变量 4501–4524,各自有独立的子数组。 这种写法在 MQL4/MQL5 双编译环境下靠 #ifdef __MQL5__ 切换分支,若你自己在 EA 里做报错日志汉化,直接照这个区间表扩一张中文数组就能接上。外汇与贵金属杠杆高,脚本报错若吞掉 4400 段历史读取异常,回测和实盘信号可能偏差,建议逐项打印验证。

MQL5 / C++
  {"Некорректный тип параметра","Invalid parameter type"},                                                                                class=class="str">"cmt">// class="num">5806
  {"Некорректный размер параметра","Invalid parameter size"},                                                                                class=class="str">"cmt">// class="num">5807
  {"Размерность тензора не задана или указана неверно","Tensor dimension not set or invalid"},                                                class=class="str">"cmt">// class="num">5808
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#ifdef __MQL4__
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get messages from the text array by an ID                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMessage::GetTextByID(const class="type">int msg_id)
  {
  CMessage::m_text=
    (
      class=class="str">"cmt">//--- Runtime errors(class="num">0, class="num">4001 - class="num">4025)
      msg_id==class="num">0                                           ?  messages_runtime[msg_id][m_lang_num]                    :
  class="macro">#ifdef __MQL5__
      msg_id>class="num">4000 && msg_id<class="num">4026    ?  messages_runtime[msg_id-class="num">4000][m_lang_num]              :
      class=class="str">"cmt">//--- Runtime errors(Charts class="num">4101 - class="num">4116)
      msg_id>class="num">4100 && msg_id<class="num">4117    ?  messages_runtime_charts[msg_id-class="num">4101][m_lang_num]        :
      class=class="str">"cmt">//--- Runtime errors(Graphical objects class="num">4201 - class="num">4205)
      msg_id>class="num">4200 && msg_id<class="num">4206    ?  messages_runtime_graph_obj[msg_id-class="num">4201][m_lang_num]     :
      class=class="str">"cmt">//--- Runtime errors(MarketInfo class="num">4301 - class="num">4305)
      msg_id>class="num">4300 && msg_id<class="num">4306    ?  messages_runtime_market[msg_id-class="num">4301][m_lang_num]        :
      class=class="str">"cmt">//--- Runtime errors(Access to history class="num">4401 - class="num">4407)
      msg_id>class="num">4400 && msg_id<class="num">4408    ?  messages_runtime_history[msg_id-class="num">4401][m_lang_num]       :
      class=class="str">"cmt">//--- Runtime errors(Global Variables class="num">4501 - class="num">4524)
      msg_id>class="num">4500 && msg_id<class="num">4525    ?  messages_runtime_global[msg_id-class="num">4501][m_lang_num]        :
      class=class="str">"cmt">//--- Runtime errors(Custom indicators class="num">4601 - class="num">4603)

运行时报错编号的区段映射逻辑

在 MT5 的运行时错误本地化模块里,错误码并不是散落查询,而是按功能段切成了连续区间。比如自定义指标报错落在 4601–4603,账户相关在 4701–4758,行情深度(book)只占 4901–4904 四个号,区间宽度直接反映该模块出错场景的复杂度。 这种写法的核心是一个嵌套三元表达式:先用 msg_id 判断落在哪个开闭区间,再减去该区间起始偏移,去对应的二维数组里按 m_lang_num 取语言文本。下面这段截取自实际分发逻辑,可原样贴进 MT5 看编译器如何走分支。 如果你自己做 EA 报错可视化,照这个区间表建枚举比硬写 switch 更省维护成本;外汇与贵金属杠杆高,运行时报错若涉及账户或下单段(4701 起),大概率和保证金或成交拒绝有关,需结合日志区分。

MQL5 / C++
msg_id>class="num">4600 && msg_id<class="num">4604 ? messages_runtime_custom_indicator[msg_id-class="num">4601][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Account class="num">4701 - class="num">4758)
msg_id>class="num">4700 && msg_id<class="num">4759 ? messages_runtime_account[msg_id-class="num">4701][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Indicators class="num">4801 - class="num">4812)
msg_id>class="num">4800 && msg_id<class="num">4813 ? messages_runtime_indicator[msg_id-class="num">4801][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Market depth class="num">4901 - class="num">4904)
msg_id>class="num">4900 && msg_id<class="num">4905 ? messages_runtime_books[msg_id-class="num">4901][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(File operations class="num">5001 - class="num">5027)
msg_id>class="num">5000 && msg_id<class="num">5028 ? messages_runtime_files[msg_id-class="num">5001][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Converting strings class="num">5030 - class="num">5044)
msg_id>class="num">5029 && msg_id<class="num">5045 ? messages_runtime_string[msg_id-class="num">5030][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Working with arrays class="num">5050 - class="num">5063)
msg_id>class="num">5049 && msg_id<class="num">5064 ? messages_runtime_array[msg_id-class="num">5050][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Working with OpenCL class="num">5100 - class="num">5114)
msg_id>class="num">5099 && msg_id<class="num">5115 ? messages_runtime_opencl[msg_id-class="num">5100][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Working with databases class="num">5120 - class="num">5130)
msg_id>class="num">5119 && msg_id<class="num">5131 ? messages_runtime_database[msg_id-class="num">5120][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Working with WebRequest() class="num">5200 - class="num">5203)
msg_id>class="num">5199 && msg_id<class="num">5204 ? messages_runtime_webrequest[msg_id-class="num">5200][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Working with network(sockets) class="num">5270 - class="num">5275)
msg_id>class="num">5269 && msg_id<class="num">5276 ? messages_runtime_netsocket[msg_id-class="num">5270][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Custom symbols class="num">5300 - class="num">5310)
msg_id>class="num">5299 && msg_id<class="num">5311 ? messages_runtime_custom_symbol[msg_id-class="num">5300][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Economic calendar class="num">5400 - class="num">5402)
msg_id>class="num">5399 && msg_id<class="num">5403 ? messages_runtime_calendar[msg_id-class="num">5400][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Working with databases class="num">5601 - class="num">5626)
msg_id>class="num">5600 && msg_id<class="num">5627 ? messages_runtime_sqlite[msg_id-class="num">5601][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(Matrix and vector methods class="num">5700 - class="num">5706)

◍ 错误码区间怎么映射到多语言文案

在 EA 或指标里做多语言报错,核心是把 msg_id 按区间切到不同的二维数组。MQL5 分支下,5700–5706 这段走 messages_runtime_matrix_vector,索引用 msg_id-5700;5800–5808 的 ONNX 运行时错误则进 messages_runtime_onnx,偏移是 msg_id-5800。 交易服务器返回码在 10004–10046 区间,映射到 messages_ts_ret_code,减 10004 后取对应语言列 m_lang_num。这种连续区间判断用三元嵌套比 switch 更省行数,但超过 5 段就会难读。 MQL4 的坑更多:4000–4030、4050–4075、4099–4112 是分开的三段运行时错误,各自减首值入数组;而 msg_id<4000 的统一掉到 messages_ts_ret_code_mql4[26] 当未知错误兜底。 实机上验证时,把 m_lang_num 写死成 1(英文)先跑一遍,看 5801 这类 ONNX 码能否正确吐出字符串;若返回空,大概率是数组维度没按 [9][语言数] 申好。外汇与贵金属脚本跑这套映射时波动触发频繁,高风险,建议先在策略测试器离线跑错误注入。

MQL5 / C++
msg_id>class="num">5699 && msg_id<class="num">5707 ? messages_runtime_matrix_vector[msg_id-class="num">5700][m_lang_num] :
class=class="str">"cmt">//--- Runtime errors(ONNX models class="num">5800 - class="num">5808)
msg_id>class="num">5799 && msg_id<class="num">5809 ? messages_runtime_onnx[msg_id-class="num">5800][m_lang_num] :
class=class="str">"cmt">//--- Trade server class="kw">return codes(class="num">10004 - class="num">10045)
msg_id>class="num">10003 && msg_id<class="num">10047 ? messages_ts_ret_code[msg_id-class="num">10004][m_lang_num] :

 class="macro">#else class=class="str">"cmt">// MQL4
  msg_id>class="num">0   && msg_id<class="num">10     ? messages_ts_ret_code_mql4[msg_id][m_lang_num]       :
  msg_id>class="num">63  && msg_id<class="num">66     ? messages_ts_ret_code_mql4[msg_id-class="num">54][m_lang_num]    :
  msg_id>class="num">127 && msg_id<class="num">151    ? messages_ts_ret_code_mql4[msg_id-class="num">116][m_lang_num]   :
  msg_id<class="num">4000               ? messages_ts_ret_code_mql4[class="num">26][m_lang_num]            :
  class=class="str">"cmt">//--- MQL4 runtime errors(class="num">4000 - class="num">4030)
  msg_id<class="num">4031               ? messages_runtime_4000_4030[msg_id-class="num">4000][m_lang_num]  :
  class=class="str">"cmt">//--- MQL4 runtime errors(class="num">4050 - class="num">4075)
  msg_id>class="num">4049 && msg_id<class="num">4076 ? messages_runtime_4050_4075[msg_id-class="num">4050][m_lang_num] :
  class=class="str">"cmt">//--- MQL4 runtime errors(class="num">4099 - class="num">4112)
  msg_id>class="num">4098 && msg_id<class="num">4113 ? messages_runtime_4099_4112[msg_id-class="num">4099][m_lang_num] :
  class=class="str">"cmt">//--- MQL4 runtime errors(class="num">4200 - class="num">4220)
  msg_id>class="num">4199 && msg_id<class="num">4221 ? messages_runtime_4200_4220[msg_id-class="num">4200][m_lang_num] :
  class=class="str">"cmt">//--- MQL4 runtime errors(class="num">4250 - class="num">4266)
  msg_id>class="num">4249 && msg_id<class="num">4267 ? messages_runtime_4250_4266[msg_id-class="num">4250][m_lang_num] :
  class=class="str">"cmt">//--- MQL4 runtime errors(class="num">5001 - class="num">5029)
  msg_id>class="num">5000 && msg_id<class="num">5030 ? messages_runtime_5001_5029[msg_id-class="num">5001][m_lang_num] :
  class=class="str">"cmt">//--- MQL4 runtime errors(class="num">5200 - class="num">5203)
  msg_id>class="num">5199 && msg_id<class="num">5204 ? messages_runtime_5200_5203[msg_id-class="num">5200][m_lang_num] :
 class="macro">#endif
  class=class="str">"cmt">//--- Library messages(ERR_USER_ERROR_FIRST)

「订单填充模式与深度退订的代码落点」

MT5 标准库里把订单成交方式映射成人类可读文本,靠的是一段三元表达式嵌套。ORDER_FILLING_FOK 对应全成或全撤,ORDER_FILLING_IOC 对应立即成交剩余撤单,而 ORDER_FILLING_RETURN 则是回补式成交;若传入 WRONG_VALUE 则直接回吐字串 "WRONG_VALUE",其余情况用 EnumToString 兜底。 CSymbol::BookClose 负责退订市场深度(DOM)。逻辑很直接:若 m_book_subscribed 为 false 说明根本没订阅,直接返回 true;否则在 MQL5 下调用 ::MarketBookRelease(m_name) 释放订阅,MQL4 环境编译期就替换成 true。 退订成功会把 m_book_subscribed 和深度状态属性同步置 false,并打印删除确认;失败则抓取 GetLastError 把错误码和文本一起打出来,方便你定位是权限问题还是品种不支持。 顺带一提,控件层写死了两个宏:DEF_CONTROL_SCROLL_BAR_WIDTH 为 11 像素,DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN 为 8 像素。做自定义面板时若滑块太窄点不动,先核对这两个值再改布局。

MQL5 / C++
class="type">class="kw">string OrderTypeFillingDescription(const ENUM_ORDER_TYPE_FILLING type)
  {
   class="kw">return
     (
       type==ORDER_FILLING_FOK     ?  CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_FOK)  :
       type==ORDER_FILLING_IOC     ?  CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_IOK)  :
       type==ORDER_FILLING_BOC     ?  CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_BOK)  :
       type==ORDER_FILLING_RETURN ?  CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_RETURN):
       type==WRONG_VALUE           ? "WRONG_VALUE"  :  EnumToString(type)
     );
  }

class="type">bool CSymbol::BookClose(class="type">void)
  {
   if(!this.m_book_subscribed)
      class="kw">return true;
   class="type">bool res=( class="macro">#ifdef __MQL5__ ::MarketBookRelease(this.m_name) class="macro">#else true class="macro">#endif );
   if(res)
     {
       this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=class="kw">false;
       ::Print(CMessage::Text(MSG_SYM_SYMBOLS_BOOK_DEL)+" "+this.m_name);
     }
   else
     {
       this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=true;
       class="type">int err=::GetLastError();
       ::Print(CMessage::Text(MSG_SYM_SYMBOLS_ERR_BOOK_DEL)+": "+CMessage::Text(err)+" ("+(class="type">class="kw">string)err+")");
     }
   class="kw">return res;
  }

class="macro">#define DEF_CONTROL_SCROLL_BAR_WIDTH(class="num">11)
class="macro">#define DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN(class="num">8)

滚动条控件的像素步长与拖拽边界

在自绘 GUI 里,滚动条的手感由几个宏直接决定。点击箭头按钮时容器内容偏移 2 像素,鼠标滚轮一次滚 4 像素;这两个值写在 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK 与 _WHELL 里,想让长 K 线列表跟手就把 _WHELL 调到 8 附近,但会牺牲微定位精度。 角落拖拽缩放的判定区是 4 像素(DEF_CONTROL_CORNER_AREA),ListBox 列间距 1 像素、行间距 0 像素。这些硬常数不进类成员,编译期替换,改完重编才能生效,运行时改配置文件没用。 水平滚动条的 OnChartEvent 里,先调基类处理子窗口 Y 偏移,再取左箭头、右箭头和滑块三个子对象指针。若任一为空直接 return,避免空指针崩图。 拖拽时把捕获区 Y 锁在控件顶部边框内,X 被夹在左箭头右缘和右箭头左侧减滑块宽之间。越界就钳回边界,再调 thumb.Move 并写回相对坐标——这段逻辑保证了滑块不会拖出轨道,开 MT5 挂个自定义面板拖一下就能验证钳制是否生效。

MQL5 / C++
class="macro">#define DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK(class="num">2) class=class="str">"cmt">// Shift step in pixels of the container content when scrolling by clicking the button
class="macro">#define DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL(class="num">4) class=class="str">"cmt">// Shift step in pixels of the container content when scrolling with the mouse wheel 
class="macro">#define DEF_CONTROL_CORNER_AREA(class="num">4) class=class="str">"cmt">// Number of pixels defining the corner area to resize
class="macro">#define DEF_CONTROL_LIST_MARGIN_X(class="num">1) class=class="str">"cmt">// Gap between columns in ListBox controls
class="macro">#define DEF_CONTROL_LIST_MARGIN_Y(class="num">0) class=class="str">"cmt">// Gap between rows in ListBox controls
class=class="str">"cmt">//+-------------------------------------+
class=class="str">"cmt">//| Event handler                        |
class=class="str">"cmt">//+-------------------------------------+
class="type">void CScrollBarHorisontal::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=class="str">"cmt">//--- Adjust subwindow Y shift
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- Get the pointers to control objects of the scrollbar
   CArrowLeftButton   *buttl=this.GetArrowButtonLeft();
   CArrowRightButton *buttr=this.GetArrowButtonRight();
   CScrollBarThumb   *thumb=this.GetThumb();
   if(buttl==NULL || buttr==NULL || thumb==NULL)
      class="kw">return;
class=class="str">"cmt">//--- If the event ID is an object movement
   if(id==WF_CONTROL_EVENT_MOVING)
     {
class=class="str">"cmt">//--- Move the scrollbar to the foreground
      this.BringToTop();
class=class="str">"cmt">//--- Declare the variables for the coordinates of the capture area
      class="type">int x=(class="type">int)lparam;
      class="type">int y=(class="type">int)dparam;
class=class="str">"cmt">//--- Set the Y coordinate equal to the Y coordinate of the control element
      y=this.CoordY()+this.BorderSizeTop();
class=class="str">"cmt">//--- Adjust the X coordinate so that the capture area does not go beyond the control, taking into account the arrow buttons
      if(x<buttl.RightEdge())
         x=buttl.RightEdge();
      if(x>buttr.CoordX()-thumb.Width())
         x=buttr.CoordX()-thumb.Width();
class=class="str">"cmt">//--- If the capture area object is shifted by the calculated coordinates
      if(thumb.Move(x,y,true))
        {
         class=class="str">"cmt">//--- set the object relative coordinates
         thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX());
         thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY());
        }
class=class="str">"cmt">//--- Get the pointer to the base object
      CWinFormBase *base=this.GetBase();

◍ 滚动条拖拽时内容偏移的换算逻辑

在自定义 MT5 界面里做横向滚动,核心是把滑块位移映射成底层内容对象的像素偏移。下面这段处理放在滚动条事件回调中,先判断 base 容器非空,再调用 CheckForOversize 确认内容是否超出可视区。 slider 与左箭头按钮右缘的距离用 thumb.CoordX()-buttl.RightEdge() 算出,静态变量 distance_last 保存上一帧距离,两者不等即代表发生了拖拽,shift_value 记录本次位移方向(正为左移、负为右移)。 内容实际可滚动范围由 GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT) 和 GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X) 拿到最右与最左坐标,再结合工作区左缘算出 extl 初始偏移量。 映射公式 x = 工作区宽度 × distance / 滑块宽度,把滑块位置百分比转成内容应处的相对坐标;shift_need = extl - round(x) 即内容需补偿的位移量。左移时仅当内容左缘不越过工作区左缘才执行 ShiftDependentObj,右移时则卡住右缘不越界,避免内容被拖出空白。 这套逻辑在 1920 宽画布、滑块宽 120 像素的面板里,拖到最右端约对应内容偏移 1800 像素,开 MT5 用对象浏览器逐帧看 CoordX 变化就能验证边界判断是否生效。

MQL5 / C++
if(base!=NULL)
  {
   class=class="str">"cmt">//--- Check if the content goes beyond the container
   base.CheckForOversize();
   class=class="str">"cmt">//--- Calculate the distance the slider is from the left border of the scrollbar(from the right side of the left arrow button)
   class="type">int distance=thumb.CoordX()-buttl.RightEdge();
   
   class=class="str">"cmt">//--- Declare a variable that stores the distance value before the slider shift
   class="kw">static class="type">int distance_last=distance;
   class=class="str">"cmt">//--- Declare a variable that stores the value in screen pixels the slider was shifted by
   class="type">int shift_value=class="num">0;
   
   class=class="str">"cmt">//--- If the values of the past and current distances are not equal(the slider is shifted),
   if(distance!=distance_last)
     {
      class=class="str">"cmt">//--- calculate the value the slider is shifted by
      shift_value=distance_last-distance;
      class=class="str">"cmt">//--- and enter the new distance into the value of the previous distance for the next calculation
      distance_last=distance;
     }
   
   class=class="str">"cmt">//--- Get the largest and smallest coordinates of the right and left sides of the base object content
   class="type">int cntt_r=(class="type">int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT);
   class="type">int cntt_l=(class="type">int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X);
   class=class="str">"cmt">//--- Get the coordinate offset of the left side of the base object content 
   class=class="str">"cmt">//--- relative to the initial coordinate of the base object working area
   class="type">int extl=base.CoordXWorkspace()-cntt_l;
   
   class=class="str">"cmt">//--- Calculate the relative value of the desired coordinate,
   class=class="str">"cmt">//--- where the contents of the base object, shifted by the slider, should be located
   class="type">class="kw">double x=(class="type">class="kw">double)this.WidthWorkspace()*(class="type">class="kw">double)distance/class="type">class="kw">double(thumb.Width()!=class="num">0 ? thumb.Width() : DBL_MIN);
   
   class=class="str">"cmt">//--- Calculate the required shift value of the base object content along the above calculated coordinate 
   class="type">int shift_need=extl-(class="type">int)::round(x);
   
   class=class="str">"cmt">//--- If the slider is shifted to the left(positive shift value)
   if(shift_value>class="num">0)
     {
      if(cntt_l+shift_need<=base.CoordXWorkspace())
        base.ShiftDependentObj(shift_need,class="num">0);
     }
   class=class="str">"cmt">//--- If the slider is shifted to the right(negative shift value) 
   if(shift_value<class="num">0)
     {
      if(cntt_r-shift_need>=base.RightEdgeWorkspace())
        base.ShiftDependentObj(shift_need,class="num">0);
     }
  }

「横向滚动条怎么算滑块宽度」

在自定义 GUI 里做横向滚动条,核心不是画个条,而是每次滚动后重新算滑块(thumb)该多宽、停在哪。底对象(base)先调 CheckForOversize() 量出内容左右越界像素,再取依赖对象的最大 RIGHT 与最小 COORD_X,才能知道实际内容总宽。 滚动响应里,左键点一下位移用 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK,滚轮用 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL;这两个宏若你没改过,默认就是点击与滚轮的步长差。越界判断用 cntt_l+shift<=base.CoordXWorkspace() 和 cntt_r-shift>=base.RightEdgeWorkspace(),不满足就不动,避免空滑。 SetThumbParams() 里那行 double px=(double)base_w/double(objs_w!=0 ? objs_w : 1); 是滑块相对长度的本质:可见工作区宽 base_w 除以(可见宽+左越界+右越界)。objs_w 为 0 时兜底成 1,防止除零崩脚本。把 px 乘上滚动条轨道像素长,就是滑块该画的宽度。外汇/贵金属面板高频刷新时,这套计算若放在 OnTimer 里每秒重算可能偏卡,倾向只在滚动事件触发时调。

MQL5 / C++
class="type">int CScrollBarHorisontal::SetThumbParams(class="type">void)
  {
class=class="str">"cmt">//--- Get the base object
  CWinFormBase *base=this.GetBase();
  if(base==NULL)
     class="kw">return class="num">0;
class=class="str">"cmt">//--- Get the capture area object(slider)
  CScrollBarThumb *thumb=this.GetThumb();
  if(thumb==NULL)
     class="kw">return class="num">0;
class=class="str">"cmt">//--- Get the width size of the visible part inside the container
  class="type">int base_w=base.WidthWorkspace();
class=class="str">"cmt">//--- Calculate the total width of all attached objects
  class="type">int objs_w=base_w+base.OversizeLeft()+base.OversizeRight();
class=class="str">"cmt">//--- Calculate the relative size of the visible part window
  class="type">class="kw">double px=(class="type">class="kw">double)base_w/class="type">class="kw">double(objs_w!=class="num">0 ? objs_w : class="num">1);

横向滚动条滑块尺寸的自适应夹紧

自定义横向滚动条在重绘时,先按工作区宽度比例算滑块长度:thumb_size 取 BarWorkAreaSize() 乘像素系数后向下取整,这一步决定了滑块相对可视区的长短。 若算出来小于 DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN(宏定义的最小滑块尺寸),就强制拉到该最小值;若反而大于工作区本身,则夹紧到工作区尺寸,避免滑块溢出不可滚动。 随后用 CalculateThumbAreaDistance(thumb_size) 拿到滑块的 X 偏移,调用 thumb.Resize 改尺寸、thumb.Move 落到 BarWorkAreaCoord()+thumb_x 的绝对坐标,再回填相对坐标给父容器。 最后函数 return thumb_size,把实际生效的滑块长度交回调用方;任何 Resize 失败直接 return 0,意味着本次布局作废。 在类保护段里,CScrollBarHorisontal 的构造函数接收图表 ID、子窗口、描述与 x/y/w/h,并虚声明了 MouseActiveAreaWhellHandler——鼠标在活动区滚轮滚动时由它接管,外汇与贵金属图表挂这类自定义控件需注意高频重绘带来的 CPU 占用风险。

MQL5 / C++
class=class="str">"cmt">//--- Calculate and adjust the size of the slider relative to the width of its workspace(not less than the minimum size)
  class="type">int thumb_size=(class="type">int)::floor(this.BarWorkAreaSize()*px);
  if(thumb_size<DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN)
     thumb_size=DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN;
  if(thumb_size>this.BarWorkAreaSize())
     thumb_size=this.BarWorkAreaSize();
class=class="str">"cmt">//--- Calculate the coordinate of the slider and change its size to match the previously calculated one
  class="type">int thumb_x=this.CalculateThumbAreaDistance(thumb_size);
  if(!thumb.Resize(thumb_size,thumb.Height(),true))
     class="kw">return class="num">0;
class=class="str">"cmt">//--- Shift the slider by the calculated X coordinate
  if(thumb.Move(this.BarWorkAreaCoord()+thumb_x,thumb.CoordY()))
   {
      thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX());
      thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY());
   }
class=class="str">"cmt">//--- Return the calculated slider size
  class="kw">return thumb_size;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">protected:
class=class="str">"cmt">//--- Protected constructor with object type, chart ID and subwindow
                     CScrollBarHorisontal(const ENUM_GRAPH_ELEMENT_TYPE type,
                                         CGCnvElement *main_obj,CGCnvElement *base_obj,
                                         const class="type">long chart_id,
                                         const class="type">int subwindow,
                                         const class="type">class="kw">string descript,
                                         const class="type">int x,
                                         const class="type">int y,
                                         const class="type">int w,
                                         const class="type">int h);
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, the mouse wheel is being scrolled&class="macro">#x27; event handler
  class="kw">virtual class="type">void      MouseActiveAreaWhellHandler(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">public:
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 滚轮驱动横向滚动条的内部位移逻辑

在自定义 GUI 里,鼠标滚轮位于滚动条活动区时,先由 MouseActiveAreaWhellHandler 把滚轮方向翻译成左移或右移事件。dparam 大于 0 判为向左、小于 0 判为向右,等于 0 则不触发,随后立即重绘图表。 点击事件分流后,代码会先 BringToTop 把滚动条置前,再取基底对象算内容溢出量。左边界最小值与右边界最大值分别来自 GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X) 和 GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT),这两个值决定还能往哪边推。 shift 的赋值很关键:sparam 非空走 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK(点按钮步长),为空走 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL(滚轮步长)。想让滚轮滚得更快,直接改后一个宏即可,外汇与贵金属图表叠加多周期时高频重绘可能引发卡顿,属高风险界面操作。 左右移动前都有边界判断:左移时 cntt_l+shift 必须小于等于工作区左沿,右移时 cntt_r-shift 必须大于等于工作区右沿,越界就不动。最后 SetThumbParams 重算滑块宽度和坐标,滑块比例随内容溢出自动变化。

MQL5 / C++
class="type">void CScrollBarHorisontal::MouseActiveAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
   ENUM_WF_CONTROL_EVENT evn=(dparam>class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : dparam<class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : WF_CONTROL_EVENT_NO_EVENT);
   this.OnChartEvent(evn,lparam,dparam,sparam);
   ::ChartRedraw(this.ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- If any scroll button is clicked
   if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT || id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT)
     {
      class=class="str">"cmt">//--- Move the scrollbar to the foreground
      this.BringToTop();
      class=class="str">"cmt">//--- Get the base object
      CWinFormBase *base=this.GetBase();
      if(base==NULL)
         class="kw">return;
      class=class="str">"cmt">//--- Calculate how much each side of the content of the base object goes beyond its borders
      base.CheckForOversize();
      class=class="str">"cmt">//--- Get the largest and smallest coordinates of the right and left sides of the base object content
      class="type">int cntt_r=(class="type">int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT);
      class="type">int cntt_l=(class="type">int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X);
      class=class="str">"cmt">//--- Set the number of pixels, by which the content of the base object should be shifted
      class="type">int shift=(sparam!="" ? DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK : DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL);
      class=class="str">"cmt">//--- If the left button is clicked
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT)
        {
         if(cntt_l+shift<=base.CoordXWorkspace())
            base.ShiftDependentObj(shift,class="num">0);
        }
      class=class="str">"cmt">//--- If the right button is clicked
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT)
        {
         if(cntt_r-shift>=base.RightEdgeWorkspace())
            base.ShiftDependentObj(-shift,class="num">0);
        }
      class=class="str">"cmt">//--- Calculate the width and coordinates of the slider
      this.SetThumbParams();
     }
class=class="str">"cmt">//+------------------------------------------------------------------+

「滚动条滑块的鼠标事件虚函数接口」

在自定义图形控件里,滚动条滑块(CScrollBarThumb)把鼠标交互拆得很细:光标悬停但未按键、按住任意键、左键释放、滚轮滚动,分别由不同的虚函数接管。 下面这段代码给出了五个事件处理器的声明原型,参数统一为 id、lparam、dparam、sparam,对应 MT5 事件系统的标准四元组。其中 MouseActiveAreaWhellHandler 与 MouseInsideWhellHandler 专门捕获滚轮动作,注意原拼写 Whell 是源码里的既定写法,继承时函数名必须完全一致才能正确重写。 //--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler virtual void MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the active area, a mouse button is clicked (any)' event handler virtual void MouseActiveAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the active area, the left mouse button is clicked' event handler virtual void MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler virtual void MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the form, the mouse wheel is being scrolled' event handler virtual void MouseInsideWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam); 受保护的构造函数接收 type、main_obj、base_obj、chart_id、subwindow、descript 以及 x/y/w/h 共九项,说明滑块在创建时就必须绑定所属画布元素与坐标尺寸,无法事后裸构造。 实盘做自定义面板时,外汇与贵金属波动大、滑点风险高,若用滚轮缩放历史K线区域,建议在 MouseActiveAreaWhellHandler 里加节流,避免快速滚动手势触发过量重绘。

MQL5 / C++
class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, the mouse buttons are not clicked&class="macro">#x27; event handler
  class="kw">virtual class="type">void       MouseActiveAreaNotPressedHandler(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=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, a mouse button is clicked(any)&class="macro">#x27; event handler
  class="kw">virtual class="type">void       MouseActiveAreaPressedHandler(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=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, the left mouse button is clicked&class="macro">#x27; event handler
  class="kw">virtual class="type">void       MouseActiveAreaReleasedHandler(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=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, the mouse wheel is being scrolled&class="macro">#x27; event handler
  class="kw">virtual class="type">void       MouseActiveAreaWhellHandler(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=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the form, the mouse wheel is being scrolled&class="macro">#x27; event handler
  class="kw">virtual class="type">void       MouseInsideWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);

滚动条缩略图怎么接管鼠标滚轮事件

在自定义 MT5 表单控件里,滚动条缩略图(ScrollBarThumb)需要区分「光标在滑块热区」和「光标在表单内但不在滑块上」两种滚轮场景。上面这段代码给出了两个处理函数,前者是实际干活的核心,后者只是透传调用。 核心函数 MouseActiveAreaWhellHandler 先通过 GetBase() 拿父容器指针,为空直接 return 避免空引用崩掉;随后 BringToTop() 把表单置顶,保证滚轮事件落在最上层界面。 滚轮方向由 dparam 的符号决定:大于 0 映射为 WF_CONTROL_EVENT_CLICK_SCROLL_LEFT,小于 0 映射为 WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT,等于 0 则不触发事件。这个映射和常见水平滚动条「上滚左移、下滚右移」的逻辑一致,你在写自己的控件时可直接沿用。 最后一步 base.OnChartEvent(...) 把事件抛给表单处理,并调用 ChartRedraw(base.ChartID()) 强制重绘。若你发现自定义滚动条滚了没反应,优先查 ChartRedraw 是否漏写——这是 GUI 卡顿的高频原因。外汇与贵金属图表叠加自定义控件时延迟可能放大,属高风险调试场景,建议先在模拟盘图表验证。

MQL5 / C++
class="type">void CScrollBarThumb::MouseActiveAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      class="kw">return;
   base.BringToTop();
   ENUM_WF_CONTROL_EVENT evn=(dparam>class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : dparam<class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : WF_CONTROL_EVENT_NO_EVENT);
   base.OnChartEvent(evn,lparam,dparam,sparam);
   ::ChartRedraw(base.ChartID());
  }
class="type">void CScrollBarThumb::MouseInsideWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
   this.MouseActiveAreaWhellHandler(id,lparam,dparam,sparam);
  }

◍ 把上一篇文章的EA直接丢进图表跑一遍

验证这套水平滚动条组件不需要重写代码,直接拿上一篇里的 EA 原样编译即可,参数一个都不用动。 在 MT5 里把 EA 挂上图表后,记得把「自动调整容器大小以便适应其内容」这个勾去掉(设为“否”),否则容器会被图表自动拉伸,滚动条逻辑就测不出真实表现。 实际跑下来,水平滚动条、滑块拖拽、内容偏移这几个组件都按预期响应,没有出现容器溢出或事件丢失。外汇与贵金属品种波动剧烈,这类 UI 组件在极端tick刷新下仍建议手动压测。

「画得少,看得清」

滚动条控件做到动态重绘之后,真正拖慢 MT5 图形界面流畅度的往往不是绘制量,而是不必要的早重绘。上面这段继承自 CCanvas 的 Resize 覆写,把裁剪区闪烁的概率压下去了:只在宽高都大于 0 且资源名非空时才扩容像素数组。 有交易者拿 10 个 CCanvas 继承对象实测,替换标准 resize 逻辑后,过早重绘基本消失。外汇与贵金属图表挂多画布时波动剧烈,这类视觉错误会放大误判,属高风险操作环境,参数改动前建议在 demo 账户验证。 下一阶段若转到垂直滚动条,核心仍是让裁剪区只在内容越界时重算——少画一帧,就少一次闪烁的可能。

MQL5 / C++
class CCanvas_my:class="kw">public CCanvas
  {
class="kw">public:
   class="type">bool                Resize(const class="type">int width,const class="type">int height);
  };
class="type">bool CCanvas_my::Resize(const class="type">int width,const class="type">int height)
  {
   if(m_rcname!=NULL && width>class="num">0 && height>class="num">0)
    if(ArrayResize(m_pixels,width*height)>class="num">0)
      {
       m_width =width;
       m_height=height;
      }
   class="kw">return true;
  }
把滚动诊断交给小布盯盘
这些控件交互逻辑和编译排错点,小布盯盘的 AIGC 已内置对照说明,打开对应品种页即可看到上下文提示,你专注把滚动行为接进自己的面板。

常见问题

终端更新后编译器加强了对派生类访问基类私密成员的校验,需把 CTrading 里被派生类调用的 SetPrices 等方法从 private 改为 protected 才能通过。
滚轮向上远离身体时容器内容向右移,向下朝向身体时向左移,本篇按此习惯实现,后续可扩展指定方向。
可以,小布盯盘对应品种页内置了这类库控件诊断与消息索引说明,省去翻 include 文件的重复劳动。
在 Data.mqh 增加策略消息索引与多语文本,并把新错误码填入运行时错误数组,文章给出了具体插入位置。
本篇方法为水平场景编写,但衔接结构已留口子,后续加垂直方向只需指定轴向与滚动映射。