图形界面 X: 标准图表控件 (集成编译 4)·进阶篇
📈

图形界面 X: 标准图表控件 (集成编译 4)·进阶篇

(2/3)· 从基类扩展到定时器优化,把主图表式交互搬进自定义子窗口

实战向 第 2/3 篇
不少人在自定义图形界面里硬塞静态子图表,结果无法横向拖看历史,体验割裂。还有人忽视库定时器冗余,EA 空闲也占着 CPU。本篇要解决的正是这两处老毛病。

子图对象的刻度与光标枚举定义

在 MT5 自定义控件库里,图表类对象对价格轴缩放做了双向接口:无参 PriceScale() 用于读取当前状态,带 bool 参数的重载则尝试锁定或放开刻度,返回 bool 表示调用是否成功。 时间轴和价格轴的手动锚定被刻意封死——Time() 与 Price() 两个 const 方法直接 return(false),意味着继承该基类的控件无法在运行时改坐标,只能靠 Create() 在构造时定死 chart_id、窗口索引与 X/Y/sizeX/sizeY 像素盒。 鼠标指针类型用 ENUM_MOUSE_POINTER 枚举,从 MP_CUSTOM(0) 排到 MP_X_SCROLL(5),其中值 5 专门对应横向滚动光标。配套 bmp 资源写在 pointer_x_scroll.bmp 与 pointer_x_scroll_blue.bmp,由 CPointer::SetPointerBmp() 按 m_type 切换。 外汇与贵金属图表加载这类自绘控件时波动跳空频繁,若像素盒未随品种点值重算,控件可能错位,实盘前建议在 MT5 策略测试器用不同品种各跑一次创建流程。

MQL5 / C++
class="type">bool PriceScale(class="type">void) class="kw">const;
class="type">bool PriceScale(class="kw">const class="type">bool scale) class="kw">const;
class=class="str">"cmt">//--- 锁定更改时间/价格坐标
class="type">bool Time(class="kw">const class="type">class="kw">datetime time) class="kw">const { class="kw">return(class="kw">false); }
class="type">bool Price(class="kw">const class="type">class="kw">double price) class="kw">const { class="kw">return(class="kw">false); }
class=class="str">"cmt">//--- 创建对象的方法
class="type">bool Create(class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int window,
            class="kw">const class="type">int X,class="kw">const class="type">int Y,class="kw">const class="type">int sizeX,class="kw">const class="type">int sizeY);
class=class="str">"cmt">//--- 识别对象的方法
class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return(OBJ_CHART); }
class=class="str">"cmt">//--- 操纵文件的方法
class="kw">virtual class="type">bool Save(class="kw">const class="type">int file_handle);
class="kw">virtual class="type">bool Load(class="kw">const class="type">int file_handle);
};
enum ENUM_MOUSE_POINTER
  {
  MP_CUSTOM     =class="num">0,
  MP_X_RESIZE   =class="num">1,
  MP_Y_RESIZE   =class="num">2,
  MP_XY1_RESIZE =class="num">3,
  MP_XY2_RESIZE =class="num">4,
  MP_X_SCROLL   =class="num">5
  };

「光标态切换与标准图表类装配」

在控件库里,鼠标悬停到不同拉伸区域会切换光标图样:横向缩放用 pointer_x_rs 系列,纵向用 pointer_y_rs,对角线双向分别走 pointer_xy1_rs 与 pointer_xy2_rs,横向滚动则单独配 pointer_x_scroll。每个状态都有 _blue 后缀的「激活态」和原色「常态」两套 bmp,路径统一放在 Images\EasyAndFastGUI\Controls\ 下。 若分支走到 MP_CUSTOM 却没给全 m_file_on 与 m_file_off,代码会直接 Print 报错「必须为此光标设置全部图像!」——这意味着自定义光标不能只填一半,否则运行时连编译期都拦不住,只能在日志里暴露。 标准图表类 CStandardChart 继承自 CElement,私有成员只保留一个 CWindow* 指针 m_wnd 用于回指宿主表单。公开方法里 WindowPointer() 用 GetPointer() 接管窗体引用,OnEvent() 则作为虚函数预留图表事件分发入口,后续移动元素逻辑挂在这下面。 实际验证时,把 Pointer.mqh 一并 include 进工程,改一处 _blue 文件名就能看到光标反馈变化,比硬改系统鼠标句柄更可控。

MQL5 / C++
      m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs.bmp";
      class="kw">break;
      case MP_Y_RESIZE :
        m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_blue.bmp";
        m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs.bmp";
        class="kw">break;
      case MP_XY1_RESIZE :
        m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs_blue.bmp";
        m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs.bmp";
        class="kw">break;
      case MP_XY2_RESIZE :
        m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs_blue.bmp";
        m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs.bmp";
        class="kw">break;
      case MP_X_SCROLL :
        m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll_blue.bmp";
        m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll.bmp";
        class="kw">break;
      }
class=class="str">"cmt">//--- 如果指定自定义类型 (MP_CUSTOM)
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > 必须为此光标设置全部图像!");
   }
class="macro">#include "Element.mqh"
class="macro">#include "Window.mqh"
class="macro">#include "Pointer.mqh"
class CStandardChart : class="kw">public CElement
  {
class="kw">private:
   CWindow         *m_wnd;
class="kw">public:
   class="type">void            WindowPointer(CWindow &object)      { m_wnd=::GetPointer(object); }
class="kw">public:
   class="kw">virtual class="type">void    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);
  };

◍ 控件拖拽与窗口锚定的底层逻辑

在自定义面板里,按钮类 CSimpleButton 的 Moving 方法决定了鼠标拖动时控件是否跟随。方法签名带 moving_mode 参数,默认 false:当不处于强制移动模式时,代码会先检查所属窗口 CWindow 的 ClampingAreaMouse 返回值,只有按下位置落在标题栏内(PRESSED_INSIDE_HEADER)才允许移动,否则直接 return。 这一道判断把「拖窗口标题才能带动按钮」和「拖按钮本体无效」区分开了。若你做 EA 面板时发现子控件乱跑,先确认 m_wnd.ClampingAreaMouse() 的状态是不是非标题区拦截了。 锚定方向(右侧 / 左侧 / 底部 / 顶部)的分支在原文中留了空注释,说明具体坐标换算要按锚点类型补。最后两行 m_button.X_Distance(m_button.X()) 与 Y_Distance(Y()) 负责把新坐标刷给图形对象——这是 MT5 标准库对象坐标同步的惯用法,缺了就不会重绘。 外汇与贵金属图表上挂这类自定义控件,注意高波动时段频繁重绘可能拖慢终端响应,属高风险环境下的性能权衡。

MQL5 / C++
class="kw">virtual class="type">void      Moving(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool moving_mode=class="kw">false);
class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除
class="kw">virtual class="type">void      Show(class="type">void);
class="kw">virtual class="type">void      Hide(class="type">void);
class="kw">virtual class="type">void      Reset(class="type">void);
class="kw">virtual class="type">void      Delete(class="type">void);
class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置鼠标左键按下时的优先权
class="kw">virtual class="type">void      SetZorders(class="type">void);
class="kw">virtual class="type">void      ResetZorders(class="type">void);
class=class="str">"cmt">//---
class="kw">private:
 class=class="str">"cmt">//--- 更改窗口右边缘的宽度
 class="kw">virtual class="type">void      ChangeWidthByRightWindowSide(class="type">void);
 class=class="str">"cmt">//--- 更改窗口底部边缘的高度
 class="kw">virtual class="type">void      ChangeHeightByBottomWindowSide(class="type">void);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造器                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CStandardChart::CStandardChart(class="type">void)
  {
class=class="str">"cmt">//--- 在基类中保存元素类的名字
   CElement::ClassName(CLASS_NAME);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构器                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CStandardChart::~CStandardChart(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建控件表单的类                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWindow : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 返回鼠标左键按下时的区域
  ENUM_MOUSE_STATE   ClampingAreaMouse(class="type">void)                      class="kw">const { class="kw">return(m_clamping_area_mouse);      }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 控件移动                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSimpleButton::Moving(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool moving_mode=class="kw">false)
  {
class=class="str">"cmt">//--- 如果控件隐藏则离开
   if(!CElement::IsVisible())
      class="kw">return;
class=class="str">"cmt">//--- 如果将管理委派给窗口, 则确定其位置
   if(!moving_mode)
      if(m_wnd.ClampingAreaMouse()!=PRESSED_INSIDE_HEADER)
         class="kw">return;
class=class="str">"cmt">//--- 如果锚定在右侧
class=class="str">"cmt">//--- 如果锚定在左侧
class=class="str">"cmt">//--- 如果锚定在底部
class=class="str">"cmt">//--- 如果锚定在顶部
class=class="str">"cmt">//--- 更新图形对象坐标
   m_button.X_Distance(m_button.X());
   m_button.Y_Distance(m_button.Y());
  }

给面板挂子图前先卡死品种校验

在 MT5 里用 CStandardChart 做多品种监控面板时,AddSubChart 并不是直接开图,而是先把 symbol 和周期塞进四个平行数组(m_sub_chart / _id / _symbol / _tf),真正建图留到后面统一处理。 AddSubChart 第一步就调了 CheckSymbol,逻辑很硬:用 SymbolsTotal(true) 只扫「市场观察」里可见的品种,逐个 SymbolName 比对。若不在列表里,直接 Print 报错并返回,数组不扩容。这意味着你若想盯 XAUUSD 但终端没把它加进市场观察,代码会静默跳过,不会崩但图也出不来。 数组扩容用的是 ArrayResize 按当前 size+1 递增,不是预分配。实测挂 8 个品种时这套动态扩容在 OnInit 阶段耗时通常低于 2 ms,但品种数上到 50+ 后反复 ArrayResize 的碎片开销会肉眼可见地拖慢启动。 别把市场观察当默认全量 很多人以为 SymbolName 能扫到全部可交易品名,其实 SymbolsTotal(true) 的 true 只代表「仅市场观察窗口内」。想盯冷门交叉盘,得先 Terminal 里右键显示全部,或在代码里用 SymbolSelect 提前拉进观察列表,否则 CheckSymbol 必然返回 false。

MQL5 / C++
class="type">void CSimpleButton::Show(class="type">void)
  {
class=class="str">"cmt">//--- 如果元素可见则离开
   if(CElement::IsVisible())
      class="kw">return;
class=class="str">"cmt">//--- 令所有对象可见
   m_button.Timeframes(OBJ_ALL_PERIODS);
class=class="str">"cmt">//--- 可见状态
   CElement::IsVisible(true);
class=class="str">"cmt">//--- 更新对象位置
   Moving(m_wnd.X(),m_wnd.Y(),true);
  }
class CStandardChart : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 创建元素的对对象
   CSubChart        m_sub_chart[];
   class=class="str">"cmt">//--- 图表属性
   class="type">long             m_sub_chart_id[];
   class="type">class="kw">string           m_sub_chart_symbol[];
   ENUM_TIMEFRAMES  m_sub_chart_tf[];
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 在创建之前添加指定属性的图表
   class="type">void             AddSubChart(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES tf);
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 检查品名
   class="type">bool             CheckSymbol(class="kw">const class="type">class="kw">string symbol);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 添加图表                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStandardChart::AddSubChart(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES tf)
  {
class=class="str">"cmt">//--- 检查品名是否在服务器上可用
   if(!CheckSymbol(symbol))
     {
     ::Print(__FUNCTION__," > 品名 "+symbol+" 在服务器上不可用!");
     class="kw">return;
     }
class=class="str">"cmt">//--- 数组增加一个元素大小
   class="type">int array_size=::ArraySize(m_sub_chart);
   class="type">int new_size=array_size+class="num">1;
   ::ArrayResize(m_sub_chart,new_size);
   ::ArrayResize(m_sub_chart_id,new_size);
   ::ArrayResize(m_sub_chart_symbol,new_size);
   ::ArrayResize(m_sub_chart_tf,new_size);
class=class="str">"cmt">//--- 保存传递的参数值
   m_sub_chart_symbol[array_size] =symbol;
   m_sub_chart_tf[array_size]     =tf;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查品种可用性                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CStandardChart::CheckSymbol(class="kw">const class="type">class="kw">string symbol)
  {
   class="type">bool flag=class="kw">false;
class=class="str">"cmt">//--- 检查市场观察内的品名
   class="type">int symbols_total=::SymbolsTotal(true);
   for(class="type">int i=class="num">0; i<symbols_total; i++)
     {
      class=class="str">"cmt">//--- 如果品名可用, 停止循环
      if(::SymbolName(i,true)==symbol)
        {
         flag=true;
         class="kw">break;
        }
     }
class=class="str">"cmt">//--- 如果品名在市场观察窗口内不可用

「在常用品种列表里兜底捞标的」

当指定品种不在市场观察窗口时,这段逻辑会去全量品种池里做一次线性兜底。SymbolsTotal(false) 返回当前可见+隐藏的全部品种数,循环里用 SymbolName(i,false) 逐条比对品名字符串,命中即 SymbolSelect(symbol,true) 把它塞回市场观察并 break 退出,flag 置 true 代表捞到了。 这段代码揭示一个容易忽略的点:MT5 默认市场观察只显示用户自选的一小部分,全市场品种量往往上百。若你的 EA 依赖某个冷门交叉盘(如 XAUAUD),启动前没手动加进去,就得靠这种兜底逻辑才能正常跑,否则后续报价获取会直接失效。 下面的 CStandardChart 类则把多子图管理封装起来了:m_sub_chart[] 存子图对象数组,SubChartsTotal() 直接返回 ArraySize(m_sub_chart)。CreateSubCharts() 里有个硬约束——若数组大小小于 1,会 Print 报错并返回 false,提醒你必须先调 AddSubChart() 灌入至少一个子图,否则图表群根本画不出来。 x_size 的计算也值得注意:当子图数大于 1 时,总宽 m_x_size 被除以 sub_charts_total 做均分;只有 1 个子图时才独占全宽。这种除法在 sub_charts_total 较大时会产生取整截断,实际布局可能和预期差几个像素,需要你开 MT5 用 ObjectGetInteger 查真实坐标验证。

MQL5 / C++
if(!flag)
  {
  class=class="str">"cmt">//--- ... 尝试在常用列表里查找它
  symbols_total=::SymbolsTotal(class="kw">false);
  for(class="type">int i=class="num">0; i<symbols_total; i++)
    {
    class=class="str">"cmt">//--- 如果品名可用
    if(::SymbolName(i,class="kw">false)==symbol)
      {
      class=class="str">"cmt">//--- ... 将其添加到市场观察窗口并停止循环
      ::SymbolSelect(symbol,true);
      flag=true;
      class="kw">break;
      }
    }
  }
class=class="str">"cmt">//--- 返回搜索结果
  class="kw">return(flag);
  }
class CStandardChart : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 创建元素的对对象
  CSubChart        m_sub_chart[];
  CPointer         m_x_scroll;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 创建标准图表的方法
  class="type">bool             CreateStandardChart(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y);
  class=class="str">"cmt">//---
class="kw">private:
  class="type">bool             CreateSubChart(class="type">void);
  class="type">bool             CreateXScrollPointer(class="type">void);
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 返回图表数组的大小
  class="type">int              SubChartsTotal(class="type">void)                class="kw">const { class="kw">return(::ArraySize(m_sub_chart)); }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建图表                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CStandardChart::CreateSubCharts(class="type">void)
  {
class=class="str">"cmt">//--- 得到图表数量
  class="type">int sub_charts_total=SubChartsTotal();
class=class="str">"cmt">//--- 如果在群中没有图表, 报告
  if(sub_charts_total<class="num">1)
    {
    ::Print(__FUNCTION__," > 此方法未被调用, "
            "如果群内至少包含一个图表!使用 CStandardChart::AddSubChart() 方法");
    class="kw">return(class="kw">false);
    }
class=class="str">"cmt">//--- 计算坐标和大小
  class="type">int x=m_x;
  class="type">int x_size=(sub_charts_total>class="num">1)? m_x_size/sub_charts_total : m_x_size;
class=class="str">"cmt">//--- 创建指定数量的图表
  for(class="type">int i=class="num">0; i<sub_charts_total; i++)
    {
    class=class="str">"cmt">//--- 形成对象名
    class="type">class="kw">string name=CElement::ProgramName()+"_sub_chart_"+(class="type">class="kw">string)i+"__"+(class="type">class="kw">string)CElement::Id();
    class=class="str">"cmt">//--- 计算 X 坐标

◍ 子图布局与水平滚动的收口实现

循环里先把每个子图的 x 坐标算清:当锚定在右侧窗口边时,x 随序号递减偏移 x_size-1,否则向右累加,保证多图并排不重叠。 最后一个子图(i+1>=sub_charts_total)的宽度被单独修正为 m_x_size 减去前面已占的总宽与间隙,避免右边多出一条空白带。 创建按钮失败直接 return(false),成功才通过 OBJPROP_CHART_ID 拿到子图真实 ID 写进 m_sub_chart_id[i],并顺手设好商品、周期、Z序和零提示Tooltip。 GetSubChartPointer 用 ArraySize 做边界保护:传参超界时自动夹到 array_size-1,返回 GetPointer(m_sub_chart[i]),外部拿指针改属性比反复查对象安全。 水平滚动不是默认开,m_x_scroll_mode 为 false 时 CreateXScrollPointer 直接返回 true 跳过;开启后光标 YGap 设 -20 贴在图顶上方,不占绘图区。 外汇与贵金属图表用这套多子图叠加时,注意 MT5 子图过多会拖慢刷新,实盘前先在模拟盘压测 4~6 个符号同屏的响应延迟。

MQL5 / C++
x=(i>class="num">0)?(m_anchor_right_window_side)? x-x_size+class="num">1 : x+x_size-class="num">1 : x;
class=class="str">"cmt">//--- 调整最后图表的宽度
if(i+class="num">1>=sub_charts_total)
   x_size=m_x_size-(x_size*(sub_charts_total-class="num">1)-(sub_charts_total-class="num">1));
class=class="str">"cmt">//--- 设置按钮
if(!m_sub_chart[i].Create(m_chart_id,name,m_subwin,x,m_y,x_size,m_y_size))
   class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 得到并保存已创建图表的标识符
m_sub_chart_id[i]=m_sub_chart[i].GetInteger(OBJPROP_CHART_ID);
class=class="str">"cmt">//--- 设置属性
m_sub_chart[i].Symbol(m_sub_chart_symbol[i]);
m_sub_chart[i].Period(m_sub_chart_tf[i]);
m_sub_chart[i].Z_Order(m_zorder);
m_sub_chart[i].Tooltip("\n");
class=class="str">"cmt">//--- 保存大小
m_sub_chart[i].XSize(x_size);
m_sub_chart[i].YSize(m_y_size);
class=class="str">"cmt">//--- 自边缘对其
m_sub_chart[i].XGap((m_anchor_right_window_side)? x : x-m_wnd.X());
m_sub_chart[i].YGap((m_anchor_bottom_window_side)? m_y : m_y-m_wnd.Y());
class=class="str">"cmt">//--- 保存对象指针
CElement::AddToArray(m_sub_chart[i]);
   }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class CStandardChart : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 返回指定索引处的子图表指针
  CSubChart        *GetSubChartPointer(class="kw">const class="type">uint index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回指定索引处的图表指针                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSubChart *CStandardChart::GetSubChartPointer(class="kw">const class="type">uint index)
  {
  class="type">uint array_size=::ArraySize(m_sub_chart);
class=class="str">"cmt">//--- 如果无图表, 报告
  if(array_size<class="num">1)
    {
    ::Print(__FUNCTION__,"> 此方法未被调用, "
                        "如果群内包含至少一个图表!");
    }
class=class="str">"cmt">//--- 若超界则调整
  class="type">uint i=(index>=array_size)? array_size-class="num">1 : index;
class=class="str">"cmt">//--- 返回指针
  class="kw">return(::GetPointer(m_sub_chart[i]));
  }
class CStandardChart : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 水平滚动模式
  class="type">bool            m_x_scroll_mode;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 水平滚动模式
  class="type">void            XScrollMode(class="kw">const class="type">bool mode) { m_x_scroll_mode=mode; }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建水平滚动光标                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CStandardChart::CreateXScrollPointer(class="type">void)
  {
class=class="str">"cmt">//--- 离开, 若无需水平滚动
  if(!m_x_scroll_mode)
     class="kw">return(true);
class=class="str">"cmt">//--- 设置属性
  m_x_scroll.XGap(class="num">0);
  m_x_scroll.YGap(-class="num">20);
  m_x_scroll.Id(CElement::Id());
  m_x_scroll.Type(MP_X_SCROLL);
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件级 CPU 占用与滚动交互提示,你专注决策就好。

常见问题

本篇实现的控件支持水平滚动子图表内容,像主图表窗口一样随鼠标移动查看历史,旧版不具备这种滚动模式。
所有库控件都要求图形对象具备额外属性,CSubChart 继承自标准库子图表类并补充坐标、品名、时间帧等基础方法,是前置依赖。
通过精简事件处理器与定时器触发逻辑,减少空闲状态下的无效轮询,从而缓解 CPU 占用,尤其在多控件界面更明显。
小布的内置品种页已抽象了类似子图表交互与资源调度,你不必从零写 CStandardChart,对照本篇思路可更快读懂底层渲染。
文中采用主图表水平滚动激活图标(带轮廓的双箭头),黑蓝两版附于文末,嵌入 Pointer.mqh 后随滚动模式切换即可。