DoEasy. 控件 (第 32 部分): 水平滚动条,鼠标轮滚动·综合运用
(3/3)· 从编译报错修复到滚轮操控容器,一篇把 WinForms 滚动条最后缺口补上
很多人在拉取旧版 DoEasy 示例时一编译就报私密方法不可访问,却不知道是终端更新后编译器开始严格检查继承权限。还有人以为水平滚动条只能靠箭头按钮,从没试过用鼠标拖滑块或滚轮直接平移容器内容。
◍ ONNX 推理报错码的中文对照
在 MT5 里用 MQL5 跑 ONNX 模型时,运行时错误码落在 5800–5808 区间,系统会把俄文与英文双语消息存进二维数组 messages_runtime_onnx。 错误 5800 对应 ONNX 内部标准错误,5801 是 ONNX Runtime API 初始化失败,5802 表示属性或取值不被 MQL5 支持,5803 为 API 执行 run 时出错。
- 专指传给 OnnxRun 的参数个数不对,5805 则是某个参数值本身非法;碰到这类返回,优先检查模型输入输出节点数量和 EA 里传的数组维度是否一致。
外汇与贵金属行情受杠杆与跳空影响,模型推理失败可能直接导致信号延迟或错单,实盘前务必在策略测试器用历史数据复现一次报错路径。
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 段历史读取异常,回测和实盘信号可能偏差,建议逐项打印验证。
{"Некорректный тип параметра","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 起),大概率和保证金或成交拒绝有关,需结合日志区分。
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][语言数] 申好。外汇与贵金属脚本跑这套映射时波动触发频繁,高风险,建议先在策略测试器离线跑错误注入。
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 像素。做自定义面板时若滑块太窄点不动,先核对这两个值再改布局。
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 挂个自定义面板拖一下就能验证钳制是否生效。
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 变化就能验证边界判断是否生效。
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 里每秒重算可能偏卡,倾向只在滚动事件触发时调。
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 占用风险。
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 重算滑块宽度和坐标,滑块比例随内容溢出自动变化。
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 里加节流,避免快速滚动手势触发过量重绘。
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 卡顿的高频原因。外汇与贵金属图表叠加自定义控件时延迟可能放大,属高风险调试场景,建议先在模拟盘图表验证。
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 账户验证。 下一阶段若转到垂直滚动条,核心仍是让裁剪区只在内容越界时重算——少画一帧,就少一次闪烁的可能。
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; }