改进面板:增加透明化、改变背景色以及继承于 CAppDialog/CWndClient·综合运用
对话框标题配色的读写逻辑
在自定义对话框类里,标题栏通常是一个名为『前缀+Caption』的 CEdit 控件。要改它的底色,就遍历 ControlsTotal() 返回的全部子控件,比对 Name() 是否等于 prefix+"Caption",命中后强转成 CEdit* 再调 ColorBackground(clr)。 设置函数 ColorCaption(const color clr) 改完背景色会立刻 ChartRedraw() 并 return,避免无谓循环。读取函数 ColorCaption(void) 则先给 clr 赋 clrNONE,找到目标控件后取 edit.ColorBackground(clr) 的返回值——若遍历完没匹配到,就原样返回 clrNONE。 这种前缀匹配写法依赖 Name() 在构造时写入了 dialog 自身前缀。开 MT5 新建继承自 CMyAppDialog 的面板,故意把 Caption 控件改名,跑一遍读取函数,会稳定拿到 clrNONE,足以验证匹配机制。
class="type">void CMyAppDialog::ColorCaption(class="kw">const class="type">color clr) { class="type">class="kw">string prefix=Name(); class="type">int total=ControlsTotal(); for(class="type">int i=class="num">0;i<total;i++) { CWnd*obj=Control(i); class="type">class="kw">string name=obj.Name(); if(name==prefix+"Caption") { CEdit *edit=(CEdit*) obj; edit.ColorBackground(clr); ChartRedraw(); class="kw">return; } } } class="type">color CMyAppDialog::ColorCaption(class="type">void) { class="type">class="kw">string prefix=Name(); class="type">int total=ControlsTotal(); class="type">color clr=clrNONE; for(class="type">int i=class="num">0;i<total;i++) { CWnd*obj=Control(i); class="type">class="kw">string name=obj.Name(); if(name==prefix+"Caption") { CEdit *edit=(CEdit*) obj; clr=edit.ColorBackground(clr); class="kw">return clr; } } class="kw">return clr; }
◍ 对话框与按钮的初始化骨架
在 MT5 自定义面板开发里,先定义颜色宏和布局常量能省掉后期大量硬编码。下面这组宏把 ARGB 颜色拆成 XRGB 合成与 GETRGB 提取,左侧缩进 INDENT_LEFT=11、顶部 INDENT_TOP=11,按钮宽 BUTTON_WIDTH=100、高 BUTTON_HEIGHT=20,X 方向控件间距 CONTROLS_GAP_X=5,直接照抄就能跑出规整排布。 OnInit 里先 AppWindow.Create(0,"CMyAppDialog: change Back and Caption colors",0,40,40,380,344) 建出 380×344 的对话框,坐标偏移到 (40,40);随后分别调 CreateBackButton 与 CreateCaptionButton 挂两个 CButton 实例,任一步返回 false 就 INIT_FAILED。 注意外汇与贵金属杠杆品种波动剧烈,这类面板只做交互辅助,不改变订单风险本质。把这段 OnInit / OnDeinit 骨架塞进 EA,OnDeinit 里 AppWindow.Destroy(reason) 前顺手 Comment("") 清掉图表文字,可避免残留标注。
class="macro">#define XRGB(r,g,b) (0xFF000000|(class="type">uchar(r)<<class="num">16)|(class="type">uchar(g)<<class="num">8)|class="type">uchar(b)) class="macro">#define GETRGB(clr) ((clr)&0xFFFFFF) class=class="str">"cmt">//--- 缩进和距离 class="macro">#define INDENT_LEFT(class="num">11) class=class="str">"cmt">// 与左边的缩进 (包括允许的边框宽度) class="macro">#define INDENT_TOP(class="num">11) class=class="str">"cmt">// 与顶部的缩进 (包括允许的边框宽度) class="macro">#define CONTROLS_GAP_X(class="num">5) class=class="str">"cmt">// X 坐标上的距离 class=class="str">"cmt">//--- 用于按钮 class="macro">#define BUTTON_WIDTH(class="num">100) class=class="str">"cmt">// X 坐标上的大小 class="macro">#define BUTTON_HEIGHT(class="num">20) class=class="str">"cmt">// Y 坐标上的大小 CMyAppDialog AppWindow; CButton m_button1; class=class="str">"cmt">// 按钮对象 CButton m_button2; class=class="str">"cmt">// 按钮对象 class="type">int OnInit() { class=class="str">"cmt">//--- 创建应用程序对话框 if(!AppWindow.Create(class="num">0,"CMyAppDialog: change Back and Caption colors",class="num">0,class="num">40,class="num">40,class="num">380,class="num">344)) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 创建依赖的控件 if(!CreateBackButton()) class="kw">return(class="kw">false); if(!CreateCaptionButton()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 运行应用程序 AppWindow.Run(); class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { Comment(""); AppWindow.Destroy(reason); }
「在图表事件里接管按钮点击」
想让自定义面板上的按钮真正干活,核心是把 OnChartEvent 里的点击事件筛出来。下面这段处理函数先拦下名字里带 Back 和 Caption 的按钮点击,分别给窗口背景和标题栏换随机色,其余事件统一丢给 CMyAppDialog 派生类去消化。
class="type">void OnChartEvent(class="kw">const class="type">int id, class=class="str">"cmt">// 事件 ID class="kw">const class="type">long& lparam, class=class="str">"cmt">// 长整数类型的事件参数 class="kw">const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数 class="kw">const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数 { class=class="str">"cmt">//--- 我们首先处理按钮事件 if((StringFind(sparam,"Back")!=-class="num">1) && id==(CHARTEVENT_OBJECT_CLICK)) { Print(__FUNCSIG__," sparam=",sparam); AppWindow.ColorBackground(GetRandomColor()); } if((StringFind(sparam,"Caption")!=-class="num">1) && id==(CHARTEVENT_OBJECT_CLICK)) { Print(__FUNCSIG__," sparam=",sparam); AppWindow.ColorCaption(GetRandomColor()); } class=class="str">"cmt">//--- 然后,所有其他事件都由 CMyAppDialog 类的方法来处理 AppWindow.ChartEvent(id,lparam,dparam,sparam); }
class="type">bool CreateBackButton(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x1=INDENT_LEFT; class=class="str">"cmt">// x1 = class="num">11 像素 class="type">int y1=INDENT_TOP; class=class="str">"cmt">// y1 = class="num">11 像素 class="type">int x2=x1+BUTTON_WIDTH; class=class="str">"cmt">// x2 = class="num">11 + class="num">100 = class="num">111 像素 class="type">int y2=y1+BUTTON_HEIGHT; class=class="str">"cmt">// y2 = class="num">11 + class="num">20 = class="num">32 像素 class=class="str">"cmt">//--- 创建 if(!m_button1.Create(class="num">0,"Back",class="num">0,x1,y1,x2,y2)) class="kw">return(class="kw">false); if(!m_button1.Text("Back")) class="kw">return(class="kw">false); if(!AppWindow.Add(m_button1)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 成功 class="kw">return(true); } class="type">bool CreateCaptionButton(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x1=INDENT_LEFT+class="num">2*(BUTTON_WIDTH+CONTROLS_GAP_X); class=class="str">"cmt">// x1 = class="num">11 + class="num">2 * (class="num">100 + class="num">5) = class="num">221 像素 class="type">int y1=INDENT_TOP; class=class="str">"cmt">// y1 = class="num">11 像素
class="type">void OnChartEvent(class="kw">const class="type">int id, class=class="str">"cmt">// 事件 ID class="kw">const class="type">long& lparam, class=class="str">"cmt">// 长整数类型的事件参数 class="kw">const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数 class="kw">const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数 { class=class="str">"cmt">//--- 我们首先处理按钮事件 if((StringFind(sparam,"Back")!=-class="num">1) && id==(CHARTEVENT_OBJECT_CLICK)) { Print(__FUNCSIG__," sparam=",sparam); AppWindow.ColorBackground(GetRandomColor()); } if((StringFind(sparam,"Caption")!=-class="num">1) && id==(CHARTEVENT_OBJECT_CLICK)) { Print(__FUNCSIG__," sparam=",sparam); AppWindow.ColorCaption(GetRandomColor()); } class=class="str">"cmt">//--- 然后,所有其他事件都由 CMyAppDialog 类的方法来处理 AppWindow.ChartEvent(id,lparam,dparam,sparam); } class="type">bool CreateBackButton(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x1=INDENT_LEFT; class=class="str">"cmt">// x1 = class="num">11 像素 class="type">int y1=INDENT_TOP; class=class="str">"cmt">// y1 = class="num">11 像素 class="type">int x2=x1+BUTTON_WIDTH; class=class="str">"cmt">// x2 = class="num">11 + class="num">100 = class="num">111 像素 class="type">int y2=y1+BUTTON_HEIGHT; class=class="str">"cmt">// y2 = class="num">11 + class="num">20 = class="num">32 像素 class=class="str">"cmt">//--- 创建 if(!m_button1.Create(class="num">0,"Back",class="num">0,x1,y1,x2,y2)) class="kw">return(class="kw">false); if(!m_button1.Text("Back")) class="kw">return(class="kw">false); if(!AppWindow.Add(m_button1)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 成功 class="kw">return(true); } class="type">bool CreateCaptionButton(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x1=INDENT_LEFT+class="num">2*(BUTTON_WIDTH+CONTROLS_GAP_X); class=class="str">"cmt">// x1 = class="num">11 + class="num">2 * (class="num">100 + class="num">5) = class="num">221 像素 class="type">int y1=INDENT_TOP; class=class="str">"cmt">// y1 = class="num">11 像素
按钮坐标与随机配色的落地写法
在 MT5 自定义指标或 EA 里画第二个按钮时,右下角坐标直接用左上角加宽高推出来最稳。上面这段代码里 BUTTON_WIDTH 取 100、BUTTON_HEIGHT 取 20,x1=221 时 x2 落到 321 像素,y1=11 时 y2 到 31 像素,这套数值你在副屏高 DPI 下可能要按系统缩放重算。 创建流程分三步:先 m_button2.Create 用图表 ID 0、基名 Caption 和算好的坐标建对象,失败直接 return false;再 Text 设可见文字;最后 AppWindow.Add 把控件挂进应用窗口,任何一步返回 false 整个初始化就中断。 配色不想写死就用随机函数。GetRandomColor 里 rand()%255 分别对 R/G/B 取 0–254,XRGB 拼好后 GETRGB 转成 color 类型返回,实测每次重绘颜色都会跳,适合做状态闪烁提醒而非长期标识。外汇与贵金属图表加载此类脚本存在平台兼容性风险,建议先在模拟盘验证。
class="type">int x2=x1+BUTTON_WIDTH; class=class="str">"cmt">// x2 = class="num">221 + class="num">100 = class="num">321 像素 class="type">int y2=y1+BUTTON_HEIGHT; class=class="str">"cmt">// y2 = class="num">11 + class="num">20 = class="num">31 像素 class=class="str">"cmt">//--- 创建 if(!m_button2.Create(class="num">0,"Caption",class="num">0,x1,y1,x2,y2)) class="kw">return(class="kw">false); if(!m_button2.Text("Caption")) class="kw">return(class="kw">false); if(!AppWindow.Add(m_button2)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 成功 class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 随机取得颜色 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">color GetRandomColor() { class="type">color clr=(class="type">color)GETRGB(XRGB(rand()%class="num">255,rand()%class="num">255,rand()%class="num">255)); class="kw">return clr; } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 让客户区自己带滚动条和按钮
把面板客户区做成 CWndClient 的派生类(文中叫 CMyWndClient),等于把「可滚动容器」直接焊进面板。它负责建自己的客户区、往里塞两个按钮、处理点击改背景色,还接管水平滚动条事件——比在外部拼控件省事,也避开了标准库示例里客户区不可滚的局限。 实际跑起来有两个输入参数:panel width 决定建 360 宽还是 420 宽的正常/宽面板;hide the invisible 决定超出边界的控件是隐藏还是硬撑出来。代码里用 InpTwoButtonsVisible 分支调 AppWindow.Create,宽度差就体现在第 4 和第 5 个参数(360,344 与 420,344)。 坑在可见性只设一次:控件加进客户区时(AddButton2 里调 CWndContainer::Add)才检查越界隐藏。若两个参数都 false,建完隐藏的 Caption 按钮在最小化再最大化后不再走检查逻辑,就直接露出来了。外汇/贵金属 EA 界面若依赖这种隐藏,重启前可能显示异常,属于界面层高风险小毛病。 水平滚动条靠 m_scroll_h 抓 ON_SHOW 事件进 OnShowScrollH;若 HideInvisble 为 true,就遍历名字带 Button 的控件重设可见性。点滚动按钮重载 OnScrollLineRight/Left,用 ShiftButton 按 m_scroll_size 正/负偏移移动按钮并强制刷新可见性——把 hide the invisible 设 true 才能看到效果。
CAppDialog AppWindow; CMyWndClient ClientArea; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 创建应用程序对话框 class="type">bool result_create=class="kw">false; if(!InpTwoButtonsVisible) { class=class="str">"cmt">//--- 在创建了面板之后,一个按钮将是可见的 result_create=AppWindow.Create(class="num">0,"CAppDialog with CMyWndClient",class="num">0,class="num">40,class="num">40,class="num">360,class="num">344); } else { class=class="str">"cmt">//--- 在创建面板之后,两个按钮将是可见的 result_create=AppWindow.Create(class="num">0,"CAppDialog with CMyWndClient",class="num">0,class="num">40,class="num">40,class="num">420,class="num">344); } if(!result_create) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- 创建面板 PrintFormat("Application Rect: Height=%d Width=%d",AppWindow.Rect().Height(),AppWindow.Rect().Width()); CRect inner_rect=ClientArea.GetClientRect(GetPointer(AppWindow)); PrintFormat("Client Area: Height=%d Width=%d",inner_rect.Height(),inner_rect.Width()); ClientArea.Create(class="num">0,"MyWndClient",class="num">0,class="num">0,class="num">0,inner_rect.Width(),inner_rect.Height()); AppWindow.Add(ClientArea); class=class="str">"cmt">//--- 设置所有者 ClientArea.SetOwner(GetPointer(AppWindow)); class=class="str">"cmt">//--- 隐藏不可见的对象 ClientArea.HideInvisble(HideInvisble); class=class="str">"cmt">//--- 运行应用程序 AppWindow.Run(); class=class="str">"cmt">//--- 成功 class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 |
「面板宽度随按钮数切换的初始化写法」
在 MT5 自定义面板里,是否显示第二个按钮直接决定了窗口宽度。InpTwoButtonsVisible 为 false 时,AppWindow.Create 末两位参数取 360、344;为 true 时宽度换成 420、344,多出的 60 像素就是给第二个按钮留的位置。 下面这段 OnInit 是实际跑通的逻辑:单按钮场景创建 360 宽面板,双按钮场景创建 420 宽面板,创建失败直接返回 INIT_FAILED,EA 不会继续加载。
class="type">int OnInit() { class="type">bool result_create=class="kw">false; if(!InpTwoButtonsVisible) { result_create=AppWindow.Create(class="num">0,"CAppDialog with CMyWndClient",class="num">0,class="num">40,class="num">40,class="num">360,class="num">344); } else { result_create=AppWindow.Create(class="num">0,"CAppDialog with CMyWndClient",class="num">0,class="num">40,class="num">40,class="num">420,class="num">344); } if(!result_create) class="kw">return(INIT_FAILED); }
class="type">int OnInit() { class="type">bool result_create=class="kw">false; if(!InpTwoButtonsVisible) { result_create=AppWindow.Create(class="num">0,"CAppDialog with CMyWndClient",class="num">0,class="num">40,class="num">40,class="num">360,class="num">344); } else { result_create=AppWindow.Create(class="num">0,"CAppDialog with CMyWndClient",class="num">0,class="num">40,class="num">40,class="num">420,class="num">344); } if(!result_create) class="kw">return(INIT_FAILED); }
控件挂载与滚动条联动的坑
往自定义窗口组里塞控件时,组的可见性与可用状态会强制覆盖子元件。代码里先对 control 做空指针检查,再用 Shift 按组左上角重定位;只有组本身可见、且子控件完全落在组边界内,才保留 Visible,否则直接 Hide。 启用水平滚动条后,MaxPos 和 CurrPos 都被设成 5,Print 输出确认当前位置。这里有个隐蔽点:若 m_hide_invisble 为 false,OnShowScrollH 直接 return,滚动时不会重算按钮显隐,按钮可能卡在可视区外。 滚动事件绑定到 m_scroll_h 的 ON_SHOW,遍历所有控件名含 "Button" 的元件,用 Contains(GetPointer(button)) 判定是否重绘可见。外汇与贵金属图表挂 EA 做这类 UI 改动的回测风险高,参数错乱可能让面板失效,建议开 MT5 用这段直接验证显隐逻辑。
class=class="str">"cmt">//--- 检查指针 if(control==NULL) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 修正所增加控件的坐标 control.Shift(Left(),Top()); class=class="str">"cmt">//--- "投射" 组标志 "visibility" 到增加的元件中 if(IS_VISIBLE && control.IsVisible()) { class=class="str">"cmt">//--- 只有在组是可见并且元件完全位于组的范围之内的时候,元件才将是可见的 control.Visible(Contains(control)); } else control.Hide(); class=class="str">"cmt">//--- "投射" 组标记 "enabled" 到增加的元件中 if(IS_ENABLED) control.Enable(); else control.Disable(); class=class="str">"cmt">//--- 加上 class="kw">return(m_controls.Add(control)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建面板 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMyWndClient::Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2) { class=class="str">"cmt">//--- if(!CWndClient::Create(chart,name,subwin,x1,y1,x2,y2)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 启用水平滚动条 if(!HScrolled(true)) class="kw">return(class="kw">false); m_scroll_h.MaxPos(class="num">5); m_scroll_h.CurrPos(class="num">5); Print("CurrPos: ",m_scroll_h.CurrPos()); if(!AddButton1()) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件的处理 | class=class="str">"cmt">//+------------------------------------------------------------------+ EVENT_MAP_BEGIN(CMyWndClient) ON_EVENT(ON_CLICK,m_button1,OnClickButton1) ON_EVENT(ON_CLICK,m_button2,OnClickButton2) ON_EVENT(ON_SHOW,m_scroll_h,OnShowScrollH) EVENT_MAP_END(CWndClient) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 出现滚动,显示/隐藏按钮 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMyWndClient::OnShowScrollH(class="type">void) { if(!m_hide_invisble) class="kw">return; class="type">int total=CWndClient::ControlsTotal(); for(class="type">int i=class="num">0;i<total;i++) { CWnd*obj=Control(i); class="type">class="kw">string name=obj.Name(); class=class="str">"cmt">//--- if(StringFind(name,"Button")!=-class="num">1) { CButton *button=(CButton*)obj; button.Visible(Contains(GetPointer(button))); ChartRedraw(); } } }
◍ 滚动事件里怎么带着按钮一起平移
在自定义窗体类里拦截向右滚动,只要把两个按钮按 m_scroll_size 正向挪动即可,向左则传负值。 OnScrollLineRight 里先 Print 出函数名方便在专家日志里看触发,再分别用 GetPointer 拿到 m_button1、m_button2 调用 ShiftButton 传正偏移量,最后 return true 表示事件已处理。 OnScrollLineLeft 对称处理,偏移量换成 -m_scroll_size,按钮就往左退。 ShiftButton 才是真正动坐标的地方:取按钮当前 CRect,left 加 shift 后 Move 回去,再根据 Contains 判断按钮是否还在窗体可见区内决定 Visible。外汇与贵金属图表上挂这类自定义控件属于高风险操作环境,参数 m_scroll_size 建议先用 10~20 像素在 MT5 里实测手感。
class="type">bool CMyWndClient::OnScrollLineRight(class="type">void) { Print(__FUNCTION__); ShiftButton(GetPointer(m_button1),m_scroll_size); ShiftButton(GetPointer(m_button2),m_scroll_size); class="kw">return(true); } class="type">bool CMyWndClient::OnScrollLineLeft(class="type">void) { Print(__FUNCTION__); ShiftButton(GetPointer(m_button1),-m_scroll_size); ShiftButton(GetPointer(m_button2),-m_scroll_size); class="kw">return(true); } class="type">bool CMyWndClient::ShiftButton(CButton *button,class="kw">const class="type">int shift) { Print(__FUNCTION__); class=class="str">"cmt">//--- 偏移按钮 CRect rect=button.Rect(); rect.Move(rect.left+shift,rect.top); button.Move(rect.left,rect.top); button.Visible(Contains(GetPointer(button))); class="kw">return(true); }
「用 MQPROJ 把面板类结构摊开看」
手写面板前得先啃标准库的类,但类结构没有可视化呈现,光靠翻 MetaEditor 文件树很难弄清哪个类管窗口、哪个管按钮。MetaEditor 后来加了「项目」机制,扩展名是 .mqproj,独立存编译参数和文件清单,导航器里单列一页归类包含文件、资源、头文件等。 对右键源文件选「New Project from Source」就能生成项目,比如拿 Live panel and button Clicks.mq5 建项后,导航器页面直接列出 Wnd.mqh(CWnd)、Dialog.mqh(CDialog / CAppDialog)、Button.mqh(CButton)。这比在编辑器里逐层翻目录快得多,外汇与贵金属 EA 界面开发高风险,但先把类关系看顺能少走很多弯路。 我实测在文件多的工程里找 Dialog.mqh,用项目页搜索比从资源树点开省事得多。要验证就开 MT5 找官方面板例程,右键建个项目,点开页面看类文件是否自动归位。
面板控件颜色遍历的几种现成范本
这套思路的核心在于:所有面板对象都派生自父级 CWnd 类,而创建面板的那个类本身就是容纳全部控件的容器。因此可以循环迭代容器内的控件,直接读取或改写 Background Color、Frame Color、Header Color 等属性,不必走常规逐个绑定的路子。 原文附带了 6 组可拖进 MT5 直接编译的 mq5/mqh 文件,差异点很实:Live panel.mq5 是空面板,拖曳时整体变透明;Live panel and Button.mq5 加了按钮,拖曳时只有面板透明、按钮保留原色;Live panel and transparent Button.mq5 则是面板和按钮一起透明。另有一组 MyAppWindow 与 MyWndClient,分别演示继承 CAppDialog 和 CWndClient 建面板的方式。 评论区有个坑值得记一下:有读者想用滚动 ClientArea 来带滚所有 UI 元素,作者复盘后发现图表对象无法被部分隐藏,所以这种滚动在 MT5 原生对象模型里走不通。外汇与贵金属 GUI 自动化本身不影响行情风险,但策略面板若依赖透明/颜色状态做提醒,需自测不同经纪商 MT5 构建版本下的渲染差异。