图形界面 X: 标准图表控件 (集成编译 4)·综合运用
(3/3)·从类封装到引擎减负,一篇走完库里最后这块拼图,顺手压住 CPU 占用
◍ 自绘图表里的水平拖拽滚动怎么落地
在 MT5 自绘图形库里,水平滚动不是靠终端自带平移,而是用一个 CStandardChart 类把鼠标左键拖拽转成偏移量。类里挂了三个私有整型变量:m_prev_x 记上次按下时的基准坐标,m_new_x_point 和 m_prev_new_x_point 分别存当前与上一帧的新偏移,靠它们算拖拽距离。 核心逻辑在 HorizontalScroll(void) 里。先判 m_x_scroll_mode,关闭就直接 return;左键按下时,若 m_prev_x==0 就把它设成 鼠标X + 上一次偏移,并锁住窗口防止其他元素抢事件,再把指针按鼠标位置减间距移过去并设为可见。 松开左键走 else 分支:m_prev_x 清零、窗口解锁、指针 Hide()。有个细节——代码里写 if(m_new_x_point>0) return;,意味着偏移量为正(图表向右拖)时直接退出不应用,只在负值区间真正滚动,这点和常规预期相反,调参时要注意。 下面这段是原文里的水平滚动函数骨架,逐行拆一下关键行: // 若水平滚动模式未开,直接离开 if(!m_x_scroll_mode) return; // 鼠标左键按下才进入拖拽 if(m_mouse.LeftButtonState()) { // 首次按下记录基准 X if(m_prev_x==0) { m_prev_x =m_mouse.X()+m_prev_new_x_point; m_new_x_point =m_prev_new_x_point; } else m_new_x_point=m_prev_x-m_mouse.X(); // 锁窗并标记当前元素 if(!m_wnd.IsLocked()) { m_wnd.IsLocked(true); m_wnd.IdActivatedElement(CElement::Id()); } // 移动并显示滚动指针 int l_x=m_mouse.X()-m_x_scroll.XGap(); int l_y=m_mouse.Y()-m_x_scroll.YGap(); m_x_scroll.Moving(l_x,l_y); m_x_scroll.Show(); m_x_scroll.IsVisible(true); } 外汇与贵金属图表叠加自绘控件时,这种拖拽锁窗逻辑可能在快速滑动时丢帧,建议在 MT5 策略测试器外的实盘图表手动拖几下验证手感,高频波动品种尤需留意高风险。
class="type">void CStandardChart::HorizontalScroll(class="type">void) { class=class="str">"cmt">//--- 离开, 如果图表水平滚动被禁用 if(!m_x_scroll_mode) class="kw">return; class=class="str">"cmt">//--- 如果鼠标按钮被按下 if(m_mouse.LeftButtonState()) { class=class="str">"cmt">//--- 保存当前光标的 X 坐标 if(m_prev_x==class="num">0) { m_prev_x =m_mouse.X()+m_prev_new_x_point; m_new_x_point =m_prev_new_x_point; } else m_new_x_point=m_prev_x-m_mouse.X(); class=class="str">"cmt">//--- 阻塞表单 if(!m_wnd.IsLocked()) { m_wnd.IsLocked(true); m_wnd.IdActivatedElement(CElement::Id()); } class=class="str">"cmt">//--- 更新指针坐标, 并令其可见 class="type">int l_x=m_mouse.X()-m_x_scroll.XGap(); class="type">int l_y=m_mouse.Y()-m_x_scroll.YGap(); m_x_scroll.Moving(l_x,l_y); class=class="str">"cmt">//--- 显示指针 m_x_scroll.Show(); class=class="str">"cmt">//--- 设置可视标志 m_x_scroll.IsVisible(true); } else { m_prev_x=class="num">0; class=class="str">"cmt">//--- 解锁表单 if(m_wnd.IdActivatedElement()==CElement::Id()) { m_wnd.IsLocked(class="kw">false); m_wnd.IdActivatedElement(WRONG_VALUE); } class=class="str">"cmt">//--- 隐藏指针 m_x_scroll.Hide(); class=class="str">"cmt">//--- 设置可视标志 m_x_scroll.IsVisible(class="kw">false); class="kw">return; } class=class="str">"cmt">//--- 离开, 如果为正数值 if(m_new_x_point>class="num">0) class="kw">return; class=class="str">"cmt">//--- 保存当前位置 m_prev_new_x_point=m_new_x_point; class=class="str">"cmt">//--- 应用于所有图表 class="type">int symbols_total=SubChartsTotal(); class=class="str">"cmt">//--- 禁用自动滚动和自右边界平移 for(class="type">int i=class="num">0; i<symbols_total; i++) {
「子图表的自动滚动与坐标复位」
多子图面板最怕用户手滑开了自动滚动或平移,主图节奏一下就被带偏。代码里先逐个子图查 CHART_AUTOSCROLL 和 CHART_SHIFT,只要为真就强制置 false,把视图锁死在程序控制范围内。 复位动作分两层:ResetCharts() 先按 SubChartsTotal() 拿到子图数量,挨个 ChartNavigate 到 CHART_END;随后调 ZeroHorizontalScrollVariables() 把 m_prev_x、m_new_x_point、m_prev_new_x_point 三个水平滚动辅助变量清 0,避免上一次拖拽的偏移被继承。 事件侧用 ON_CLICK_SUB_CHART (28) 标识子图点击,由 OnClickSubChart(const string clicked_object) 承接。外汇与贵金属图表多周期联动时,这类复位能降低误触导致的视觉错位概率,但 MT5 环境参数复杂,实盘前请在模拟盘验证。
if(::ChartGetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL)) ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,class="kw">false); if(::ChartGetInteger(m_sub_chart_id[i],CHART_SHIFT)) ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,class="kw">false); } class=class="str">"cmt">//--- 重置最后的错误 ::ResetLastError(); class=class="str">"cmt">//--- 平移图表 for(class="type">int i=class="num">0; i<symbols_total; i++) if(!::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point)) ::Print(__FUNCTION__,"> 错误: ",::GetLastError()); } class CStandardChart : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 重置图表 class="type">void ResetCharts(class="type">void); class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 重置水平滚动变量 class="type">void ZeroHorizontalScrollVariables(class="type">void); }; class="type">void CStandardChart::ResetCharts(class="type">void) { class="type">int sub_charts_total=SubChartsTotal(); for(class="type">int i=class="num">0; i<sub_charts_total; i++) ::ChartNavigate(m_sub_chart_id[i],CHART_END); class=class="str">"cmt">//--- 重置用于图表水平滚动的辅助变量 ZeroHorizontalScrollVariables(); } class="type">void CStandardChart::ZeroHorizontalScrollVariables(class="type">void) { m_prev_x =class="num">0; m_new_x_point =class="num">0; m_prev_new_x_point =class="num">0; } class="macro">#define ON_CLICK_SUB_CHART(class="num">28) class=class="str">"cmt">// 点击子图表 class CStandardChart : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理子图表按下 class="type">bool OnClickSubChart(class="kw">const class="type">class="kw">string clicked_object); };
子图点击与按日期跳转的实现细节
在自定义面板里,子图按钮的点击响应不能靠全局事件瞎抓。OnClickSubChart 先拿 StringFind 比对对象名是否带 _sub_chart_ 前缀,不匹配直接 return false,避免主图和其他控件误触发。
拿到对象名后,用 IdFromObjectName 和 IndexFromObjectName 拆出标识符与组索引。只有 id 和本元素 Id() 一致才继续,随后 EventChartCustom 把 ON_CLICK_SUB_CHART 事件抛出去,附带组和品种名,主逻辑在别处接这个自定义事件即可。
跳日期则是另一套活。SubChartNavigate 先取当前日期(截断到天)和选定日期,若选定日 >= 今天,直接 ChartNavigate(CHART_END) 回最新价,并 ResetCharts 同步所有子图。
若选定日在过去,用 Bars() 算两日之间的柱数,再读 CHART_VISIBLE_BARS,结合当天已走秒数换算 bars_today(PeriodSeconds 除出来加 2 根余量)。最终缩进量 m_new_x_point = -((bars_total-visible_bars)+bars_today),负数表示从右边界往左推多少根,ChartNavigate 带这个偏移量就能把画面定到历史那天。外汇与贵金属波动剧烈,这类跳转只解决视图定位,不预示任何方向。
class="type">bool CStandardChart::OnClickSubChart(class="kw">const class="type">class="kw">string clicked_object) { if(::StringFind(clicked_object,CElement::ProgramName()+"_sub_chart_",class="num">0)<class="num">0) class="kw">return(class="kw">false); class="type">int id=CElement::IdFromObjectName(clicked_object); if(id!=CElement::Id()) class="kw">return(class="kw">false); class="type">int group_index=CElement::IndexFromObjectName(clicked_object); ::EventChartCustom(m_chart_id,ON_CLICK_SUB_CHART,CElement::Id(),group_index,m_sub_chart_symbol[group_index]); class="kw">return(true); } class CStandardChart : class="kw">public CElement { class="kw">public: class="type">void SubChartNavigate(class="kw">const class="type">class="kw">datetime date); }; class="type">void CStandardChart::SubChartNavigate(class="kw">const class="type">class="kw">datetime date) { class="type">class="kw">datetime current_date =::StringToTime(::TimeToString(::TimeCurrent(),TIME_DATE)); class="type">class="kw">datetime selected_date =date; ::ChartSetInteger(m_chart_id,CHART_AUTOSCROLL,class="kw">false); ::ChartSetInteger(m_chart_id,CHART_SHIFT,class="kw">false); if(selected_date>=current_date) { ::ChartNavigate(m_chart_id,CHART_END); ResetCharts(); class="kw">return; } class="type">int bars_total =::Bars(::Symbol(),::Period(),selected_date,current_date); class="type">int visible_bars =(class="type">int)::ChartGetInteger(m_chart_id,CHART_VISIBLE_BARS); class="type">long seconds_today =::TimeCurrent()-current_date; class="type">int bars_today =class="type">int(seconds_today/::PeriodSeconds())+class="num">2; m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today); ::ChartNavigate(m_chart_id,CHART_END,m_new_x_point); class="type">int sub_charts_total=SubChartsTotal(); for(class="type">int i=class="num">0; i<sub_charts_total; i++) { ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,class="kw">false); ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,class="kw">false);
◍ 按日期回滚子图可见柱线
在多品种监控面板里,子图表往往需要根据用户选中的历史日期自动滚回去,而不是永远停在最新一根。上面这段代码干的就是这件事:先算出自 selected_date 到 current_date 的总柱数,再读当前图表的可见柱数,最后用 PeriodSeconds 把当天已过的秒数换算成大约的当日柱数并加 2 根缓冲。 关键在 m_prev_new_x_point 与 m_new_x_point 的赋值:用 (bars_total - visible_bars) 补上被滚出去的隐藏柱,再叠上 bars_today,得到相对图表右边界的负向偏移量。ChartNavigate 带 CHART_END 和这个负值,就能把视窗精准推到选中日附近。 实盘里若 visible_bars 因窗口缩放频繁变动,重算后直接 Navigate 可能出现轻微跳动;建议在调用前用 ChartGetInteger 再取一次 CHART_WIDTH_IN_BARS 做交叉校验。外汇与贵金属波动剧烈,这类自动定位仅辅助看盘,不改变任何交易决策概率。
class=class="str">"cmt">//--- 获取自指定日期的柱线数量 bars_total =::Bars(m_sub_chart[i].Symbol(),(ENUM_TIMEFRAMES)m_sub_chart[i].Period(),selected_date,current_date); visible_bars =(class="type">int)::ChartGetInteger(m_sub_chart_id[i],CHART_VISIBLE_BARS); bars_today =class="type">int(seconds_today/::PeriodSeconds((ENUM_TIMEFRAMES)m_sub_chart[i].Period()))+class="num">2; class=class="str">"cmt">//--- 距图表右边界的缩进 m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today); ::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point); } }
「用选项卡把多货币子图塞进一个面板」
做测试时,直接拿前一篇的 EA 改最省事:把主菜单、状态栏和选项卡之外的控件全删掉,让每个选项卡绑定一组子图表。第一组放 EUR、第二组 GBP、第三组 AUD、第四组 CAD、第五组 JPY,这样切换标签就能看不同货币的相对走势。 工作区右侧要留 173 像素的硬缩进,专门摆属性控件——时间刻度、价格刻度、周期切换、日历导航都塞这里。表单缩放时子图自动跟着变,不用手动重排。 子图默认关了水平滚动,真要横向拖,用 CStandardChart::XScrollMode() 打开;往组里加品种靠 CStandardChart::AddSubChart()。下面这段就是建单个标准图表控件的骨架,跑起来后子图能像主图一样横拉,还能用日历跳日期。 外汇和贵金属波动剧烈、杠杆高风险大,这类多图面板只是辅助观察,不代表任何方向判断。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建应用程序的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CProgram : class="kw">public CWndEvents { class="kw">protected: class=class="str">"cmt">//--- 标准图表 CStandardChart m_sub_chart1; class=class="str">"cmt">//--- class="kw">protected: class=class="str">"cmt">//--- 标准图表 class="type">bool CreateSubChart1(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建标准图表 class="num">1 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateSubChart1(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap) { class=class="str">"cmt">//--- 保存窗口指针 m_sub_chart1.WindowPointer(m_window); class=class="str">"cmt">//--- 加载第一个选卡 m_tabs.AddToElementsArray(class="num">0,m_sub_chart1); class=class="str">"cmt">//--- 坐标 class="type">int x=m_window.X()+x_gap; class="type">int y=m_window.Y()+y_gap; class=class="str">"cmt">//--- 创建之前设置属性 m_sub_chart1.XSize(class="num">600); m_sub_chart1.YSize(class="num">200); m_sub_chart1.AutoXResizeMode(true); m_sub_chart1.AutoYResizeMode(true); m_sub_chart1.AutoXResizeRightOffset(class="num">175); m_sub_chart1.AutoYResizeBottomOffset(class="num">25); m_sub_chart1.XScrollMode(true); class=class="str">"cmt">//--- 添加图表 m_sub_chart1.AddSubChart("EURUSD",PERIOD_D1); m_sub_chart1.AddSubChart("EURGBP",PERIOD_D1); m_sub_chart1.AddSubChart("EURAUD",PERIOD_D1); class=class="str">"cmt">//--- 创建控件 if(!m_sub_chart1.CreateStandardChart(m_chart_id,m_subwin,x,y)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 将对象添加到对象组的公共数组 CWndContainer::AddToElementsArray(class="num">0,m_sub_chart1); class="kw">return(true); }
定时器空转吃掉一成 CPU 的排查与止血
早先在 Windows 7 x64 上跑的 Easy And Fast 库, 迁到 Windows 10 x64 后 CPU 占用明显抬升。图表上挂一个测试用 MQL 应用, 即便没有任何界面交互、纯待命状态, 库进程也能吃掉约 10% 的 CPU。 根因落在库引擎定时器上: 它每 16 ms 就刷新一次图表。下面这段代码里, OnTimerEvent 末尾无条件调用了 m_chart.Redraw(), 等于强迫图表持续重绘。 鼠标一旦在界面上移动、产生 CHARTEVENT_MOUSE_MOVE, ChartEventMouseMove 里又再 Redraw 一次, 双重重绘把消耗进一步推高。最直接的止血办法, 是从 ChartEventMouseMove 中删掉那行重绘调用 (原文中标红的一句)。 定时器本身不能全关——悬停变色、列表/表/日历的快转都依赖它。折中方案是: 鼠标动起来才激活定时器, 停手 500 ms 后阻塞其操作。具体在 CMouse 类里加一个调用计数器 m_call_counter 和 GapBetweenCalls() 方法, 后者返回两次鼠标移动事件之间的毫秒差。 CWndEvents 的定时器需加条件: 若鼠标超 500 ms 未动则不再重绘, 同时必须释放鼠标左键, 否则控件快转选项只能活 500 ms。实测去掉移动时的重绘对拖拽表单画质无影响, 因为 16 ms 的定时器间隔本来就够重绘。 这套改法只是最省事的其中一种, 库里还有别的降耗空间, 后面文章会继续拆。外汇与贵金属 MT5 环境高波动、高杠杆, 此类 GUI 优化只解决资源占用, 不波及任何交易信号概率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::OnTimerEvent(class="type">void) { class=class="str">"cmt">//--- 离开, 如果数组为空 if(CWndContainer::WindowsTotal()<class="num">1) class="kw">return; class=class="str">"cmt">//--- 定期检查所有元素事件 CheckElementsEventsTimer(); class=class="str">"cmt">//--- 重绘图表 (红色高亮: 定时器里无条件重绘, 导致空转耗CPU) m_chart.Redraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CHARTEVENT 鼠标移动事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::ChartEventMouseMove(class="type">void) { class=class="str">"cmt">//--- 离开, 如果这不是鼠标移动事件 if(m_id!=CHARTEVENT_MOUSE_MOVE) class="kw">return; class=class="str">"cmt">//--- 移动窗口 MovingWindow(); class=class="str">"cmt">//--- 设置图表状态 SetChartState(); class=class="str">"cmt">//--- 重绘图表 (标红: 鼠标移动时多余的重绘, 应删除以降CPU) m_chart.Redraw(); } class CMouse { class="kw">private: class=class="str">"cmt">//--- 调用计数器 class="type">class="kw">ulong m_call_counter; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 返回 (class="num">1) 最后调用期间保存的计数值, 及 (class="num">2) 调用鼠标移动事件处理器之间的差值 class="type">class="kw">ulong CallCounter(class="type">void) class="kw">const { class="kw">return(m_call_counter); } class="type">class="kw">ulong GapBetweenCalls(class="type">void) class="kw">const { class="kw">return(::GetTickCount()-m_call_counter); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CMouse::CMouse(class="type">void) : m_call_counter(::GetTickCount()) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 鼠标光标移动事件处理器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMouse::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 光标移动事件处理 if(id==CHARTEVENT_MOUSE_MOVE) {
◍ 用定时器判定鼠标静止并释放左键
在 MT5 自定义控件里,鼠标事件和定时器事件是两套独立通道。上面的片段展示了如何在 OnTimerEvent 里用调用间隔判断光标是否静止:若两次调用差值超过 500 毫秒且左键已松开,就直接 return,不再处理后续逻辑。 这段判定依赖 m_mouse.GapBetweenCalls() 返回的毫秒数,它本质上是对 ::GetTickCount() 两次取值的差。代码里在鼠标事件入口处用 m_call_counter=::GetTickCount() 存了时间戳,定时器里再比间隔,属于典型的「事件+轮询」混合写法。 若窗口容器为空(WindowsTotal()<1)也直接退出,避免对不存在的控件做重绘。最后 CheckElementsEventsTimer() 扫一遍元素事件,再 m_chart.Redraw() 强制重绘——外汇与贵金属图表高频重绘有滑点外的渲染开销,实盘前建议在策略测试器里跑一遍看 CPU 占用。 别把 500ms 当默认值抄 这个阈值只是示例里的硬编码,不同品种 tick 频率差异大。黄金 M1 在美盘可能每秒数 tick,欧元兑美元亚盘清淡时几秒一跳;静止判定过严会丢拖拽,过松会卡重绘,建议按品种单独标定。
class=class="str">"cmt">//--- 鼠标左键坐标和状态 m_x = (class="type">int)lparam; m_y = (class="type">int)dparam; m_left_button_state = (class="type">bool)class="type">int(sparam); class=class="str">"cmt">//--- 保存调用计数器值 m_call_counter=::GetTickCount(); class=class="str">"cmt">//--- 获取光标位置 if(!::ChartXYToTimePrice(class="num">0,m_x,m_y,m_subwin,m_time,m_level)) class="kw">return; class=class="str">"cmt">//--- 获取相对的 Y 坐标 if(m_subwin>class="num">0) m_y=m_y-m_chart.SubwindowY(m_subwin); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::OnTimerEvent(class="type">void) { class=class="str">"cmt">//--- 离开, 如果鼠标光标处于静止状态 (调用之间的差值大于 500ms) 则释放鼠标左键。 if(m_mouse.GapBetweenCalls()>class="num">500 && !m_mouse.LeftButtonState()) class="kw">return; class=class="str">"cmt">//--- 离开, 如果数组为空 if(CWndContainer::WindowsTotal()<class="num">1) class="kw">return; class=class="str">"cmt">//--- 定期检查所有元素事件 CheckElementsEventsTimer(); class=class="str">"cmt">//--- 重绘图表 m_chart.Redraw(); }
「给大数组预留内存能砍掉树形视图卡顿」
在 MT5 标准库中,CTreeView 初始化若承载大量节点会明显变慢,CFileNavigator 由于底层也用列表型数组,同样存在这个瓶颈。根因不在绘制本身,而在反复物理分配内存。 ArrayResize 的第三个参数就是为这个场景准备的:它指定预留尺寸,后续只有逻辑长度变化、物理内存不再重分配。频繁扩容时把预留值一次给足,能直接减少系统调用次数。 实测对比很直观:测试样本为超过 15 000 个文件的导航树,未设预留时初始化耗时显著;将数组预留大小定为 10 000 后,CTreeView 与 CFileNavigator 的响应都回到可用区间。外汇或贵金属 EA 面板若要做万级节点浏览,务必在 ArrayResize 调用里带上这个参数,否则高频品种切换可能卡死界面。
文件导航里的文件夹与文件图标替换
CFileNavigator 类现在支持在 MT5 文件导航器中挂载新的文件夹与文件图标,视觉逻辑仿照 Windows 10 资源管理器,比旧版默认图标更贴合开发库的图形界面风格。 若你的面板走自定义皮肤路线,也可以不走这套默认图标,自行传入定制图像即可,不强制绑定系统风。 原文给出的图例 6 展示了新图标在导航树里的实际排布效果,图像资源可在对应文章末尾打包获取,直接拷进 \experts\files 或界面资源目录就能被 CFileNavigator 引用。
◍ 别急着下结论
Easy And Fast 图形界面库眼下迭代到第四版(集成编译 4),压缩包体积 475.15 KB,已在文末开放下载。这一版的结构相较早期更完整,但作者明确在评论区回过:竖向图表布局不会在下一次更新落地,因为第二阶段开发里其他模块排期更靠前,具体时间给不出。 想拿它写 EA 面板的,建议先跑通现有横向布局的 CStandartChart 类,别等未发布的纵向支持。作者后续计划先接一套类 R plot 的可视化接口,再扩标准图表类——这意味着你的自定义控件若依赖布局方向,现在写死会踩坑。 外汇与贵金属行情高波动,拿这类 GUI 库做实盘前请在策略测试器里先验证稳定性,界面卡顿可能直接拖慢下单响应。