图形界面 X: 高级列表和表格管理。代码优化 (集成构建 7)·进阶篇
🧩

图形界面 X: 高级列表和表格管理。代码优化 (集成构建 7)·进阶篇

(2/3)· 控件还靠手动调属性?这篇把列表、滚动条和表格的可编程管理一次讲透

案例拆解新手友好 第 2/3 篇
不少人在 MQL5 界面里改个列表项、挪下滚动条还得重编译 EA,运行时动不了就卡在半路。控件属性写死在初始化里,后面想动态调整只能返工。这篇接着上篇把可编程控制补上,少走弯路。

◍ 用代码把滚动条滑块推到想要的位置

在自建 MT5 面板里,列表项动态增长时,若想让视图始终跟着最新数据走,就得在运行时操纵滚动条滑块。CScrollV 和 CScrollH 都提供了 MovingThumb() 方法,垂直与水平逻辑几乎一致,下面只拆垂直部分。 MovingThumb() 只有一个参数 pos,默认值是 WRONG_VALUE。不传位置直接调用,滑块会落到列表最末端——这对实时追加日志或报价行的自动滚屏很实用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 管理垂直滚动条的类                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CScrollV : class="kw">public CScroll
  {
class="kw">public:
   class=class="str">"cmt">//--- 移动滑块到指定位置
   class="type">void               MovingThumb( const class="type">int pos= WRONG_VALUE );
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 移动滑块到指定位置                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CScrollV::MovingThumb( const class="type">int pos= WRONG_VALUE )
  {
class=class="str">"cmt">//--- 如果不需要滚动条, 离开
   if (m_items_total<=m_visible_items_total)
       class="kw">return;
class=class="str">"cmt">// --- 检查滑块位置
   class="type">uint check_pos= class="num">0;
class=class="str">"cmt">//--- 若超出范围则调整位置
   if (pos< class="num">0 || pos>m_items_total-m_visible_items_total)
      check_pos=m_items_total-m_visible_items_total;
   else
      check_pos=pos;
class=class="str">"cmt">//--- 保存滑块位置
   CScroll::CurrentPos(check_pos);
class=class="str">"cmt">//--- 计算并设置滚动条滑块 Y 坐标
   CalculateThumbY();
  }
逐行看这段:开头类声明里 MovingThumb 的省缺参数就是 WRONG_VALUE,意味着“未指定”。函数体第一道闸是总项数不超过可见项数就直接 return,此时根本不画滚动条,调用无意义。 check_pos 先清零,再判断 pos 是否越界——下限小于 0 或上限超过“总项减可见项”时,强制夹到最大可滚位置;否则用传入值。随后 CurrentPos() 落库,CalculateThumbY() 按新位置算像素坐标并重绘。外汇与贵金属面板高频刷新,这种边界夹紧能避免滑块飞出控件区。 验证办法:在 EA 的 OnChartEvent 里监听按键,按 End 调一次 MovingThumb(),看列表是否瞬移到底部;故意传一个大于总数的 pos,确认它被夹到末位而非报错。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 管理垂直滚动条的类                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CScrollV : class="kw">public CScroll
  {
class="kw">public:
   class=class="str">"cmt">//--- 移动滑块到指定位置
   class="type">void               MovingThumb( const class="type">int pos= WRONG_VALUE );
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 移动滑块到指定位置                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CScrollV::MovingThumb( const class="type">int pos= WRONG_VALUE )
  {
class=class="str">"cmt">//--- 如果不需要滚动条, 离开
   if (m_items_total<=m_visible_items_total)
       class="kw">return;
class=class="str">"cmt">// --- 检查滑块位置
   class="type">uint check_pos= class="num">0;
class=class="str">"cmt">//--- 若超出范围则调整位置
   if (pos< class="num">0 || pos>m_items_total-m_visible_items_total)
      check_pos=m_items_total-m_visible_items_total;
   else
      check_pos=pos;
class=class="str">"cmt">//--- 保存滑块位置
   CScroll::CurrentPos(check_pos);
class=class="str">"cmt">//--- 计算并设置滚动条滑块 Y 坐标
   CalculateThumbY();
  }

「列表类的私有辅助与重构清除逻辑」

CListView 把重复出现两次以上的坐标与尺寸计算收进三个私有方法,每个只占用一行调用:CalculationItemY 按前一项底边减 1 算出当前项 Y 坐标,CalculationItemsWidth 在有纵向滚动条时扣掉滚动条宽度,CalculationYSize 用「单项高度×可见数-(可见数-1)+2」得到列表总高。

MQL5 / C++
class="type">int CListView::CalculationItemY( const class="type">int item_index= class="num">0 )
  { class="kw">return ((item_index> class="num">0 )?m_items[item_index- class="num">1 ].Y2()- class="num">1 : CElementBase::Y()+ class="num">1 ); }
class="type">int CListView::CalculationItemsWidth( class="type">void )
  { class="kw">return ((m_items_total>m_visible_items_total) ?CElementBase::XSize()-m_scrollv.ScrollWidth()- class="num">1 : CElementBase::XSize()- class="num">2 ); }
class="type">int CListView::CalculationYSize( class="type">void )
  { class="kw">return (m_item_y_size*m_visible_items_total-(m_visible_items_total- class="num">1 )+ class="num">2 ); }
Clear() 先循环 Delete 可见项对象,再 FreeObjectsArray 释放指针数组,把选中文本和索引归零、ListSize(0),隐藏并重置滚动条,最后必须把背景指针 m_area 重新 AddToArray——因为释放时它已被删掉,不补回后续绘制会丢背景。
MQL5 / C++
class="type">void CListView::Clear( class="type">void )
  {
  for ( class="type">int r= class="num">0 ; r<m_visible_items_total; r++) m_items[r].Delete();
  CElementBase::FreeObjectsArray();
  m_selected_item_text = "";
  m_selected_item_index = class="num">0 ;
  ListSize( class="num">0 );
  m_scrollv.Hide();
  m_scrollv.MovingThumb( class="num">0 );
  m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total);
  CElementBase::AddToArray(m_area);
  }
Rebuilding() 开头必调 Clear(),随后用传入的 items_total 和 visible_items_total 重设大小;当 CalculationYSize 算出的 y_size 与当前不符就同步 m_area 与基类高度,再调滚动条 ChangeThumbSize / ChangeYSize,跑 CreateList(),总数超可见数且控件可见时才 Show 滚动条。
MQL5 / C++
class="type">void CListView::Rebuilding( const class="type">int items_total, const class="type">int visible_items_total)
  {
  Clear();
  ListSize(items_total);
  VisibleListSize(visible_items_total);
  class="type">int y_size=CalculationYSize();
  if (y_size!=CElementBase::YSize())
    {
    m_area.YSize(y_size);
    m_area.Y_Size(y_size);
    CElementBase::YSize(y_size);
    }
  m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total);
  m_scrollv.ChangeYSize(y_size);
  CreateList();
  if (m_items_total>m_visible_items_total)
    {
    if (CElementBase::IsVisible())
      m_scrollv.Show();
    }
  }
AddItem() 只接一个文本参数(默认空串),方法内先给项目数组加一个元素;若总数未超可见数就直接建图形对象,超了才显示滚动条、调滑块尺寸并重算项宽。CreateItem() 被 CreateList 循环和 AddItem 共用,避免两份近似代码。 外汇与贵金属 GUI 自动化涉及高危操作,任何控件逻辑改动请在 MT5 策略测试器隔离验证,再上真实账户。

列表视图的增项与滚动实现

在 MT5 自定义控件里,CListView 承担纯文本列表的呈现。往列表塞数据靠 AddItem(),它先按 ItemsTotal() 取当前数量,把内部数组 m_item_total 扩 1,再用 ArrayResize 把 m_item_value 拉大,把新字符串写进末尾下标。 若总项数已超过可视上限 m_visible_items_total,就走滚动逻辑:调 m_scrollv.ChangeThumbSize 重算滑块比例,父可见时 Show() 出垂直滚动条;随后把可见项的宽度统一成列表宽减滚动条宽再减 1,避免文字压到滚动条上。 不足一屏时走另一分支:算坐标、CreateItem 建对象、HighlightSelectedItem 高亮,且当 array_size==1 时把首项错误文本存进 m_selected_item_text,这一细节容易在复用代码时漏掉。 Scrolling() 负责程序化滚动,唯一参数 pos 缺省 WRONG_VALUE,意为滚到末位。函数先判 m_items_total<=m_visible_items_total 就直接 return,无滚动条不干活;last_pos_index 固定为总数减可见数,pos<0 或越界都夹到末位,再 MovingThumb+UpdateList 完成位移。 外汇与贵金属 GUI ea 开发属高风险领域,这类控件逻辑错误只会造成显示异常,不会直接下单,但回测时若列表越界仍可能拖慢 tick 处理。CBheckBoxList 已内置类似滚动,可对照移植。

MQL5 / C++
class CListView : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 添加项目至列表
   class="type">void              AddItem(const class="type">class="kw">string value="");
  };
class="type">void CListView::AddItem(const class="type">class="kw">string value="")
  {
   class="type">int array_size=ItemsTotal();
   m_items_total=array_size+class="num">1;
   ::ArrayResize(m_item_value,m_items_total);
   m_item_value[array_size]=value;
   if(m_items_total>m_visible_items_total)
     {
      m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total);
      if(CElementBase::IsVisible())
         m_scrollv.Show();
      if(m_visible_items_total<class="num">1)
         class="kw">return;
      class="type">int width=CElementBase::XSize()-m_scrollv.ScrollWidth()-class="num">1;
      if(width==m_items[class="num">0].XSize())
         class="kw">return;
      for(class="type">int i=class="num">0;i<m_items_total && i<m_visible_items_total;i++)
        {
         m_items[i].XSize(width);
         m_items[i].X_Size(width);
        }
      class="kw">return;
     }
   class="type">int x=CElementBase::X()+class="num">1;
   class="type">int y=CalculationItemY(array_size);
   class="type">int width=CalculationItemsWidth();
   CreateItem(array_size,x,y,width);
   HighlightSelectedItem();
   if(array_size==class="num">1)
      m_selected_item_text=m_item_value[class="num">0];
  }

class CListView : class="kw">public CElement
  {
class="kw">public:
   class="type">void              Scrolling(const class="type">int pos=WRONG_VALUE);
  };
class="type">void CListView::Scrolling(const class="type">int pos=WRONG_VALUE)
  {
   if(m_items_total<=m_visible_items_total)
       class="kw">return;
   class="type">int index=class="num">0;
   class="type">int last_pos_index=m_items_total-m_visible_items_total;
   if(pos<class="num">0)
      index=last_pos_index;
   else
      index=(pos>last_pos_index)?last_pos_index:pos;
   m_scrollv.MovingThumb(index);
   UpdateList(index);
  }

◍ 表格类私有方法精简与斑马纹一键化

CTable 这次重构把九类高频重复逻辑收进了私有方法:行数组缩放、单元格省缺初始化、X/Y 轴尺寸与坐标计算、列宽测算、Y 轴尺寸变更。调用关系清晰后,主流程可读性明显提升,且未改动对外行为。 列宽测算 CalculationColumnWidth() 只带一个布尔参,默认 false 算全部列总宽;传 true 时走递归算最后一列宽。之所以分开算,是因为末列右缘常因滚动条存在而不与表格右缘对齐——垂直滚动条出现条件为 m_rows_total > m_visible_rows_total,此时要扣掉 ScrollWidth()。 创建或改宽时触发 ColumnsXResize():先取列宽,再双层循环把每格的 X、X_Distance、XSize、X_Size、XGap 全部重设;若末列则改用 CalculationColumnWidth(true)。方法尾段处理排序箭头——固定表头时索引 +1,且箭头只在 m_is_sorted_column_index 落于可视列范围内才 ShiftSortArrow() 平移。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算列宽                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CTable::CalculationColumnWidth(const class="type">bool is_last_column=false)
  {
   class="type">int width=class="num">0;
   class=class="str">"cmt">//--- 检查垂直滚动条的存在
   class="type">bool is_scrollv=m_rows_total>m_visible_rows_total;
   class=class="str">"cmt">//---
   if(!is_last_column)
     {
      if(m_visible_columns_total==class="num">1)
        width=(is_scrollv)?m_x_size-m_scrollv.ScrollWidth() : width=m_x_size-class="num">2;
      else
       {
        if(is_scrollv)
           width=(m_x_size-m_scrollv.ScrollWidth())/class="type">int(m_visible_columns_total);
        else
           width=m_x_size/(class="type">int)m_visible_columns_total+class="num">1;
       }
     }
   else
     {
      width=CalculationColumnWidth();
      class="type">int last_column=(class="type">int)m_visible_columns_total-class="num">1;
      class="type">int w=m_x_size-(width*last_column-last_column);
      width=(is_scrollv)?w-m_scrollv.ScrollWidth()-class="num">1 : w-class="num">2;
     }
   class=class="str">"cmt">//---
   class="kw">return(width);
  }
上面代码逐行拆:第4行 width=0 初始化返回宽度;第6行判断总行是否超可视行决定有无垂直滚动条;第8–15行非末列分支,单列时扣滚动条宽或减2,多列时均分并+1防舍入丢失;第17–22行末列分支先递归取普通列宽,再用总宽减前n-1列占用得末列宽,滚动条存在再减1。 斑马纹以前要逐格 CellColor() 涂色,现在控件创建前调一次 IsZebraFormatRows(第二种颜色) 即可。ZebraFormatRows() 里若 m_is_zebra_format_rows==clrNONE 直接退出;固定首行时 r==0 跳过,偶数行用斑马色奇数行用省缺色,否则反转。外汇与贵金属图表上挂此类自定义表格控件属高风险操作,参数误设可能导致面板绘制异常。
MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 表格格式化为斑马样式                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTable::ZebraFormatRows(class="type">void)
  {
   class=class="str">"cmt">//--- 如果此禁用模式, 离开
   if(m_is_zebra_format_rows==clrNONE)
      class="kw">return;
   class=class="str">"cmt">//--- 省缺颜色
   class="type">class="kw">color clr=m_cell_color;
   class=class="str">"cmt">//---
   for(class="type">uint c=class="num">0;c<m_columns_total;c++)
     {
      for(class="type">uint r=class="num">0;r<m_rows_total;r++)
        {
         if(m_fix_first_row)
           {
            if(r==class="num">0)
              class="kw">continue;
            class=class="str">"cmt">//---
            clr=(r%class="num">2==class="num">0)?m_is_zebra_format_rows : m_cell_color;
           }
         else
           clr=(r%class="num">2==class="num">0)?m_cell_color : m_is_zebra_format_rows;
         class=class="str">"cmt">//--- 在公共数组里保存单元背景色
         m_vcolumns[c].m_cell_color[r]=clr;
        }
     }
  }
这段逐行看:第4行关掉斑马模式就 return;第6行取省缺单元格色;双层循环遍历列与行,固定首行时首行不染、偶数行着斑马色,不固定首行则偶数行省缺奇数行斑马色;末行把算好的色写进 m_vcolumns[c].m_cell_color[r] 供绘制读取。剩余几个私有方法逻辑平铺,无递归或跨类依赖,开 MT5 按头列表对照源码即可验证。

MQL5 / C++
class CTable : class="kw">public CElement
  {
 class="kw">private:
   class=class="str">"cmt">//--- 调整行数组大小
   class="type">void               RowResize(const class="type">uint column_index,const class="type">uint new_size);
   class=class="str">"cmt">//--- 使用省缺值初始化单元格
   class="type">void               CellInitialize(const class="type">uint column_index,const class="type">int row_index=WRONG_VALUE);
   class=class="str">"cmt">//--- 计算沿 X 轴的表格大小
   class="type">int                CalculationXSize(class="type">void);
   class=class="str">"cmt">//--- 计算沿 Y 轴的表格大小
   class="type">int                CalculationYSize(class="type">void);
   class=class="str">"cmt">//--- 计算单元格的 X 坐标
   class="type">int                CalculationCellX(const class="type">int column_index=class="num">0);
   class=class="str">"cmt">//--- 计算单元格的 Y 坐标
   class="type">int                CalculationCellY(const class="type">int row_index=class="num">0);
   class=class="str">"cmt">//--- 计算列宽
   class="type">int                CalculationColumnWidth(const class="type">bool is_last_column=false);
   class=class="str">"cmt">//--- 计算列宽
   class="type">void               ColumnsXResize(class="type">void);
   class=class="str">"cmt">//--- 更改沿 Y 轴的表格大小
   class="type">void               YResize(class="type">void);
  };

class="type">int CTable::CalculationColumnWidth(const class="type">bool is_last_column=false)
  {
   class="type">int width=class="num">0;
   class="type">bool is_scrollv=m_rows_total>m_visible_rows_total;
   if(!is_last_column)
     {
      if(m_visible_columns_total==class="num">1)
        width=(is_scrollv)?m_x_size-m_scrollv.ScrollWidth() : width=m_x_size-class="num">2;
      else
       {
        if(is_scrollv)
           width=(m_x_size-m_scrollv.ScrollWidth())/class="type">int(m_visible_columns_total);
        else
           width=m_x_size/(class="type">int)m_visible_columns_total+class="num">1;
       }
     }
   else
     {
      width=CalculationColumnWidth();
      class="type">int last_column=(class="type">int)m_visible_columns_total-class="num">1;
      class="type">int w=m_x_size-(width*last_column-last_column);
      width=(is_scrollv)?w-m_scrollv.ScrollWidth()-class="num">1 : w-class="num">2;
     }
   class="kw">return(width);
  }

class="type">void CTable::ColumnsXResize(class="type">void)
  {
   class="type">int width=CalculationColumnWidth();
   for(class="type">uint c=class="num">0;c<m_columns_total && c<m_visible_columns_total;c++)
     {
      class="type">int x=CalculationCellX(c);
      if(c+class="num">1>=m_visible_columns_total)
        width=CalculationColumnWidth(true);
      for(class="type">uint r=class="num">0;r<m_rows_total && r<m_visible_rows_total;r++)
       {
        m_columns[c].m_rows[r].X(x);
        m_columns[c].m_rows[r].X_Distance(x);
        m_columns[c].m_rows[r].XSize(width);
        m_columns[c].m_rows[r].X_Size(width);
        m_columns[c].m_rows[r].XGap(CalculateXGap(x));
       }
     }
   if(m_is_sorted_column_index==WRONG_VALUE)
      class="kw">return;
   class="type">int l=(m_fix_first_column)?class="num">1:class="num">0;
   class="type">int h=m_scrollh.CurrentPos()+l;
   if(m_is_sorted_column_index>=h && m_is_sorted_column_index<(class="type">int)m_visible_columns_total)
     {
      ShiftSortArrow(m_is_sorted_column_index);
     }
  }

class CTable : class="kw">public CElement
  {
 class="kw">private:
   class="type">class="kw">color              m_is_zebra_format_rows;
 class="kw">public:
   class="type">void               IsZebraFormatRows(const class="type">class="kw">color clr) { m_is_zebra_format_rows=clr; }
  };

class CTable : class="kw">public CElement
  {
 class="kw">private:
   class="type">void               ZebraFormatRows(class="type">void);
  };

class="type">void CTable::ZebraFormatRows(class="type">void)
  {
   if(m_is_zebra_format_rows==clrNONE)
      class="kw">return;
   class="type">class="kw">color clr=m_cell_color;
   for(class="type">uint c=class="num">0;c<m_columns_total;c++)
     {
      for(class="type">uint r=class="num">0;r<m_rows_total;r++)
       {
        if(m_fix_first_row)
          {
           if(r==class="num">0)
              class="kw">continue;
           clr=(r%class="num">2==class="num">0)?m_is_zebra_format_rows : m_cell_color;
          }
        else
           clr=(r%class="num">2==class="num">0)?m_cell_color : m_is_zebra_format_rows;
        m_vcolumns[c].m_cell_color[r]=clr;
       }
     }
  }
把控件诊断交给小布
这些可编程控件的属性变化和小布盯盘里的 AIGC 看盘辅助思路一致——把重复的状态读取和判断交给工具,你只管在策略里调用结果,打开对应品种页就能看到现成诊断。

常见问题

基本不用大改,新版 CElement 继承自 CElementBase 并补了公用方法,原派生类仍当基类用,只是重复代码被收进底层了。
概率上不会冲突,方法层做了状态同步,编程设值和手动拖动走同一套坐标计算,倾向以最后一次操作为准。
可以,小布盯盘的 AIGC 模块支持按品种页动态呈现诊断字段,不必你自己写表格刷新逻辑,把重复劳动交给它即可。
接口签名大多保留,少数内部方法名调整,迁移时对着头文件比对一下事件绑定处就行,外汇贵金属界面开发仍属高风险定制,建议先沙箱测。