图形界面 VII: 表格控件 (第一章)·进阶篇
📊

图形界面 VII: 表格控件 (第一章)·进阶篇

(2/3)· 文本标签、编辑框与绘制型表格三套类如何各司其职,多数自定义面板卡在滚动与锁定表头

进阶 第 2/3 篇
想用 MQL5 把交易信号、持仓明细铺成可滚动面板,却总被滚动条和表头锁定绕晕?本篇把三类表格控件的类结构一次拆清,省去你翻标准库的试错成本。

◍ 表格控件的偏移与冻结接口

在 MT5 自定义表格控件里,先要把绘制偏移和首行首列冻结这两个基础能力摸透。XOffset 与 ColumnXOffset 分别接管整体横向偏移和列区域横向偏移,直接写私有成员 m_x_offset、m_column_x_offset,调用一次即生效。 首行首列锁定用重载方法实现读写分离:FixFirstRow() 无参版本返回 m_fix_first_row 布尔状态,带 bool 参数的版本改写该标志。FixFirstColumn 同理,两套接口让面板在滚动时可以选择钉住表头或首列。 CLabelsTable 继承 CElement 后,用 LTLabels 结构以 m_rows[] 嵌套存可见单元格对象,LTOptions 则单独存 m_vrows[] 文本与 m_colors[] 颜色,二者按下标对应。 行数列数有四档查询:RowsTotal / ColumnsTotal 拿逻辑总规模,VisibleRowsTotal / VisibleColumnsTotal 只返回当前可视区数量。实盘面板若只渲染可视区,这两组值差可能达到数十倍,务必分开用。 别把偏移当摆设 把 XOffset 设成图表宽度的 1/10(例如 1920 宽屏设 192),再开 FixFirstColumn(true),左列品种名在横向滚动报价表时就不会被挤出视野。外汇与贵金属波动剧烈,这类界面辅助仅降低盯盘失误率,不预示任何方向。

MQL5 / C++
class="type">void      XOffset(const class="type">int x_offset)              { m_x_offset=x_offset;             }
class="type">void      ColumnXOffset(const class="type">int x_offset)          { m_column_x_offset=x_offset;     }
class=class="str">"cmt">//--- (class="num">1) 读取 和 (class="num">2) 设置第一行的锁定模式
class="type">bool      FixFirstRow(class="type">void)                          const { class="kw">return(m_fix_first_row);       }
class="type">void      FixFirstRow(const class="type">bool flag)                     { m_fix_first_row=flag;           }
class=class="str">"cmt">//--- (class="num">1) 读取 和 (class="num">2) 设置第一列的锁定模式
class="type">bool      FixFirstColumn(class="type">void)                       const { class="kw">return(m_fix_first_column);    }
class="type">void      FixFirstColumn(const class="type">bool flag)                  { m_fix_first_column=flag;        }
};
class CLabelsTable : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 表格可见部分的对象数组
  class="kw">struct LTLabels
    {
      CLabel          m_rows[];
    };
  LTLabels          m_columns[];
  class=class="str">"cmt">//--- 表格数值和属性的数组
  class="kw">struct LTOptions
    {
      class="type">class="kw">string          m_vrows[];
      class="type">color           m_colors[];
    };
  LTOptions         m_vcolumns[];
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 返回 (class="num">1) 行 和(class="num">2) 列 的总数
  class="type">int       RowsTotal(class="type">void)                          const { class="kw">return(m_rows_total);          }
  class="type">int       ColumnsTotal(class="type">void)                       const { class="kw">return(m_columns_total);       }
  class=class="str">"cmt">//--- 返回表格中可见部份的 (class="num">1) 行 和 (class="num">2) 列 的总数
  class="type">int       VisibleRowsTotal(class="type">void)                   const { class="kw">return(m_visible_rows_total);  }
  class="type">int       VisibleColumnsTotal(class="type">void)                const { class="kw">return(m_visible_columns_total); }
  class=class="str">"cmt">//--- 设置 (class="num">1) 表格的大小 和 (class="num">2) 可见部份的大小

表格尺寸与可见区的数组初始化逻辑

在 MT5 里用 C++ 类封装报价面板时,表格的物理尺寸和可见尺寸要分开管。CLabelsTable 提供了两个 void 方法:TableSize 管后台全量行列数组,VisibleTableSize 管前台实际绘制的行列数,两者下限都被硬性锁死——列最少 1、行最少 2。 TableSize 先把传入值做三元判断兜底,再对 m_vcolumns 做 ArrayResize,然后按列数循环给每列的 m_vrows、m_colors 扩到对应行数,并用 ArrayInitialize 把颜色数组刷成默认文字色。VisibleTableSize 逻辑类似,但只扩 m_columns[i].m_rows,不碰颜色数组。 构造函数里顺手给了几个易忽略的默认参数:行高 m_row_y_size=18、左边距 m_x_offset=30、列间距 m_column_x_offset=60,背景色默认 clrWhiteSmoke。外汇与贵金属行情跳动快,把这些值直接写死可能在高分屏上挤压报价,建议开 MT5 把 18 和 60 改成变量传参验证显示效果。

MQL5 / C++
class="type">void                TableSize(const class="type">int columns_total,const class="type">int rows_total);
class="type">void                VisibleTableSize(const class="type">int visible_columns_total,const class="type">int visible_rows_total);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置表格的大小                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabelsTable::TableSize(const class="type">int columns_total,const class="type">int rows_total)
  {
class=class="str">"cmt">//--- 必须至少有一列
   m_columns_total=(columns_total<class="num">1) ? class="num">1 : columns_total;
class=class="str">"cmt">//--- 必须至少有两行
   m_rows_total=(rows_total<class="num">2) ? class="num">2 : rows_total;
class=class="str">"cmt">//--- 设置列数组的大小
   ::ArrayResize(m_vcolumns,m_columns_total);
class=class="str">"cmt">//--- 设置行数组的大小
   for(class="type">int i=class="num">0; i<m_columns_total; i++)
     {
      ::ArrayResize(m_vcolumns[i].m_vrows,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_colors,m_rows_total);
      class=class="str">"cmt">//--- 使用默认值初始化文字颜色数组
      ::ArrayInitialize(m_vcolumns[i].m_colors,m_text_color);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置表格可见部份的大小                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabelsTable::VisibleTableSize(const class="type">int visible_columns_total,const class="type">int visible_rows_total)
  {
class=class="str">"cmt">//--- 必须至少有一列
   m_visible_columns_total=(visible_columns_total<class="num">1) ? class="num">1 : visible_columns_total;
class=class="str">"cmt">//--- 必须至少有两行
   m_visible_rows_total=(visible_rows_total<class="num">2) ? class="num">2 : visible_rows_total;
class=class="str">"cmt">//--- 设置列数组的大小
   ::ArrayResize(m_columns,m_visible_columns_total);
class=class="str">"cmt">//--- 设置行数组的大小
   for(class="type">int i=class="num">0; i<m_visible_columns_total; i++)
      ::ArrayResize(m_columns[i].m_rows,m_visible_rows_total);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLabelsTable::CLabelsTable(class="type">void) : m_fix_first_row(class="kw">false),
                                 m_fix_first_column(class="kw">false),
                                 m_row_y_size(class="num">18),
                                 m_x_offset(class="num">30),
                                 m_column_x_offset(class="num">60),
                                 m_area_color(clrWhiteSmoke),

「标签表格类的私有结构与构造初始化」

在 MT5 自定义控件库里,CLabelsTable 继承自 CElement,用私有成员把表格拆成背景区、纵向/横向滚动条和可见标签数组三块。m_area 是 CRectLabel 背景,m_scrollv / m_scrollh 分别管垂直和水平滚动,m_columns[] 里套了 LTLabels 结构,每个结构挂一个 CLabel 类型的 m_rows[] 动态数组,这样就能按列→行二级索引填文字。 构造函数里先把文本色锁成 clrBlack,总行/列和可见行/列都设成 2 和 1(m_rows_total(2)、m_columns_total(1)、m_visible_rows_total(2)、m_visible_columns_total(1)),意味着这个实例默认就是 1 列 2 行的窄表。随后调 CElement::ClassName 存类标识,m_zorder=0 和 m_area_zorder=1 把背景压在鼠标响应层下面,再用 TableSize 与 VisibleTableSize 把逻辑尺寸和可视尺寸落地。 CreateLabels 方法从 CElement::X() 取基准 x,y 初始为 0,offset 按是否首列在 m_x_offset 与 m_column_x_offset 之间切。循环只跑可见列数(m_visible_columns_total),c>0 时才换用列间偏移,说明首列左缘和后续列左缘是分开算的——改这两个偏移量能直接调表格左边留白和列间距。外汇与贵金属图表挂这类自定义控件时波动跳变频繁,实盘前应在 MT5 策略测试器用历史 tick 验证重绘不会错位。

MQL5 / C++
m_text_color(clrBlack),
 m_rows_total(class="num">2),
 m_columns_total(class="num">1),
 m_visible_rows_total(class="num">2),
 m_visible_columns_total(class="num">1)
{
class=class="str">"cmt">//--- 在基类中保存元件类的名称
 CElement::ClassName(CLASS_NAME);
class=class="str">"cmt">//--- 设置鼠标左键点击的优先级
 m_zorder      =class="num">0;
 m_area_zorder =class="num">1;
class=class="str">"cmt">//--- 设置表格和它可见部分的大小
 TableSize(m_columns_total,m_rows_total);
 VisibleTableSize(m_visible_columns_total,m_visible_rows_total);
}
class CLabelsTable : class="kw">public CElement
 {
class="kw">private:
 class=class="str">"cmt">//--- 创建表格的对象
 CRectLabel        m_area;
 CScrollV          m_scrollv;
 CScrollH          m_scrollh;
 class=class="str">"cmt">//--- 表格可见部分的对象数组
 class="kw">struct LTLabels
   {
     CLabel              m_rows[];
   };
 LTLabels          m_columns[];
 class=class="str">"cmt">//---
class="kw">public:
 class=class="str">"cmt">//--- 返回滚动条的指针
 CScrollV         *GetScrollVPointer(class="type">void)            const { class="kw">return(::GetPointer(m_scrollv)); }
 CScrollH         *GetScrollHPointer(class="type">void)            const { class="kw">return(::GetPointer(m_scrollh)); }
 class=class="str">"cmt">//--- 用于创建表格的方法
 class="type">bool             CreateLabelsTable(const class="type">long chart_id,const class="type">int subwin,const class="type">int x,const class="type">int y);
 class=class="str">"cmt">//---
class="kw">private:
 class="type">bool             CreateArea(class="type">void);
 class="type">bool             CreateLabels(class="type">void);
 class="type">bool             CreateScrollV(class="type">void);
 class="type">bool             CreateScrollH(class="type">void);
 };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建文字标签的数组                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLabelsTable::CreateLabels(class="type">void)
 {
class=class="str">"cmt">//--- 坐标和偏移
 class="type">int x     =CElement::X();
 class="type">int y     =class="num">0;
 class="type">int offset =class="num">0;
class=class="str">"cmt">//--- 列
 for(class="type">int c=class="num">0; c<m_visible_columns_total; c++)
   {
   class=class="str">"cmt">//--- 计算表格偏移
    offset=(c>class="num">0) ? m_column_x_offset : m_x_offset;

◍ 逐行铺标签的坐标与属性写入

在 MT5 面板里画一个多列表格,核心就是给每个单元格算好 X/Y 然后建对象。下面这段循环先把列偏移累加进 x,再按可见行数 m_visible_rows_total 逐个处理;r=0 时 Y 从表单顶向下 10 像素起,之后每行叠 m_row_y_size-1,避免行距过大。 对象名用程序名+列号 c+行号 r+元素 ID 拼出来,保证同图表多实例不重名。Create 失败直接 return(false),否则继续写 Description、字体、字号、颜色、锚点居中、不可选中、Z 序和空 tooltip。XGap/YGap 存的是相对表单左上角的偏移,方便后续整体移动时同步。

MQL5 / C++
class=class="str">"cmt">//--- 计算X坐标
   x=x+offset;
   class=class="str">"cmt">//--- 行
   for(class="type">int r=class="num">0;r<m_visible_rows_total; r++)
     {
      class=class="str">"cmt">//--- 构建对象名称
      class="type">class="kw">string name=CElement::ProgramName()+"_labelstable_label_"+(class="type">class="kw">string)c+"_"+(class="type">class="kw">string)r+"__"+(class="type">class="kw">string)CElement::Id();
      class=class="str">"cmt">//--- 计算 Y 坐标
      y=(r>class="num">0) ? y+m_row_y_size-class="num">1 : CElement::Y()+class="num">10;
      class=class="str">"cmt">//--- 创建对象
      if(!m_columns[c].m_rows[r].Create(m_chart_id,name,m_subwin,x,y))
         class="kw">return(class="kw">false);
      class=class="str">"cmt">//--- 设置属性
      m_columns[c].m_rows[r].Description(m_vcolumns[c].m_vrows[r]);
      m_columns[c].m_rows[r].Font(FONT);
      m_columns[c].m_rows[r].FontSize(FONT_SIZE);
      m_columns[c].m_rows[r].Color(m_text_color);
      m_columns[c].m_rows[r].Corner(m_corner);
      m_columns[c].m_rows[r].Anchor(ANCHOR_CENTER);
      m_columns[c].m_rows[r].Selectable(class="kw">false);
      m_columns[c].m_rows[r].Z_Order(m_zorder);
      m_columns[c].m_rows[r].Tooltip("\n");
      class=class="str">"cmt">//--- 与表单边缘的距离
      m_columns[c].m_rows[r].XGap(x-m_wnd.X());
      m_columns[c].m_rows[r].YGap(y-m_wnd.Y());
      class=class="str">"cmt">//--- 坐标
      m_columns[c].m_rows[r].X(x);
      m_columns[c].m_rows[r].Y(y);
      class=class="str">"cmt">//--- 保存对象指针
      CElement::AddToArray(m_columns[c].m_rows[r]);
      }
   }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class CLabelsTable : class="kw">public CElement
   {
class="kw">public:
   class=class="str">"cmt">//--- 设置指定表格单元的值
   class="type">void              SetValue(const class="type">int column_index,const class="type">int row_index,const class="type">class="kw">string value);
   class=class="str">"cmt">//--- 取得指定表格单元的值
   class="type">class="kw">string            GetValue(const class="type">int column_index,const class="type">int row_index);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置指定索引的值                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabelsTable::SetValue(const class="type">int column_index,const class="type">int row_index,const class="type">class="kw">string value)
   {
class=class="str">"cmt">//--- 检查是否超出了列的范围
   class="type">int csize=::ArraySize(m_vcolumns);
   if(csize<class="num">1 || column_index<class="num">0 || column_index>=csize)
      class="kw">return;
class=class="str">"cmt">//--- 检查是否超出了行的范围
   class="type">int rsize=::ArraySize(m_vcolumns[column_index].m_vrows);
   if(rsize<class="num">1 || row_index<class="num">0 || row_index>=rsize)
SetValue 的防护很实在:先取列数组大小 csize,小于 1 或索引越界就直接 return,不抛错也不写空指针。行也是同理用 ArraySize(m_vcolumns[column_index].m_vrows) 卡一遍,所以你在外层乱传 (-1, 0) 或超界索引时,面板不会崩,只是该格不更新。 开 MT5 把这段塞进你的 CElement 派生类,故意传 column_index=99 看是否静默跳过;外汇和贵金属图表加载此类面板属高频刷新场景,对象数可能破百,注意高杠杆品种跳空时重绘延迟可能变大。

MQL5 / C++
class=class="str">"cmt">//--- 计算X坐标
   x=x+offset;
   class=class="str">"cmt">//--- 行
   for(class="type">int r=class="num">0;r<m_visible_rows_total; r++)
     {
      class=class="str">"cmt">//--- 构建对象名称
      class="type">class="kw">string name=CElement::ProgramName()+"_labelstable_label_"+(class="type">class="kw">string)c+"_"+(class="type">class="kw">string)r+"__"+(class="type">class="kw">string)CElement::Id();
      class=class="str">"cmt">//--- 计算 Y 坐标
      y=(r>class="num">0) ? y+m_row_y_size-class="num">1 : CElement::Y()+class="num">10;
      class=class="str">"cmt">//--- 创建对象
      if(!m_columns[c].m_rows[r].Create(m_chart_id,name,m_subwin,x,y))
         class="kw">return(class="kw">false);
      class=class="str">"cmt">//--- 设置属性
      m_columns[c].m_rows[r].Description(m_vcolumns[c].m_vrows[r]);
      m_columns[c].m_rows[r].Font(FONT);
      m_columns[c].m_rows[r].FontSize(FONT_SIZE);
      m_columns[c].m_rows[r].Color(m_text_color);
      m_columns[c].m_rows[r].Corner(m_corner);
      m_columns[c].m_rows[r].Anchor(ANCHOR_CENTER);
      m_columns[c].m_rows[r].Selectable(class="kw">false);
      m_columns[c].m_rows[r].Z_Order(m_zorder);
      m_columns[c].m_rows[r].Tooltip("\n");
      class=class="str">"cmt">//--- 与表单边缘的距离
      m_columns[c].m_rows[r].XGap(x-m_wnd.X());
      m_columns[c].m_rows[r].YGap(y-m_wnd.Y());
      class=class="str">"cmt">//--- 坐标
      m_columns[c].m_rows[r].X(x);
      m_columns[c].m_rows[r].Y(y);
      class=class="str">"cmt">//--- 保存对象指针
      CElement::AddToArray(m_columns[c].m_rows[r]);
      }
   }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class CLabelsTable : class="kw">public CElement
   {
class="kw">public:
   class=class="str">"cmt">//--- 设置指定表格单元的值
   class="type">void              SetValue(const class="type">int column_index,const class="type">int row_index,const class="type">class="kw">string value);
   class=class="str">"cmt">//--- 取得指定表格单元的值
   class="type">class="kw">string            GetValue(const class="type">int column_index,const class="type">int row_index);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置指定索引的值                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabelsTable::SetValue(const class="type">int column_index,const class="type">int row_index,const class="type">class="kw">string value)
   {
class=class="str">"cmt">//--- 检查是否超出了列的范围
   class="type">int csize=::ArraySize(m_vcolumns);
   if(csize<class="num">1 || column_index<class="num">0 || column_index>=csize)
      class="kw">return;
class=class="str">"cmt">//--- 检查是否超出了行的范围
   class="type">int rsize=::ArraySize(m_vcolumns[column_index].m_vrows);
   if(rsize<class="num">1 || row_index<class="num">0 || row_index>=rsize)

表格单元的读取与刷新逻辑

在 CLabelsTable 里,GetValue 先做边界检查再返回值:用 ArraySize 拿到列数与行数,若 column_index 或 row_index 越界直接返回空串。这套防御写法能避免 MT5 面板在数据源抖动时抛数组越界错误。 TextColor 同理,先核列再核行,通过才写 m_colors[row_index]。想让某行报价变红提示,就在这里传 clrRed,不用重绘整个表格。 UpdateTable 是刷新入口:固定表头时 t、l 偏移 1,再取双向滚动条 CurrentPos 算可视起点。下例里 h、v 就是横向、纵向实际读的单元索引,m_fix_first_column 为真时还会单独同步第 0 列第 0 格的 Description。 外汇与贵金属标的波动快,面板刷新频率高,这段逻辑若越界保护漏写,可能在跳空时让 EA 界面卡死,属高风险操作环境,参数需实盘前在策略测试器验证。

MQL5 / C++
class="type">class="kw">string CLabelsTable::GetValue(const class="type">int column_index,const class="type">int row_index)
  {
class=class="str">"cmt">//--- 检查是否超出了列的范围
   class="type">int csize=::ArraySize(m_vcolumns);
   if(csize<class="num">1 || column_index<class="num">0 || column_index>=csize)
      class="kw">return("");
class=class="str">"cmt">//--- 检查是否超出了行的范围
   class="type">int rsize=::ArraySize(m_vcolumns[column_index].m_vrows);
   if(rsize<class="num">1 || row_index<class="num">0 || row_index>=rsize)
      class="kw">return("");
class=class="str">"cmt">//--- 返回值
   class="kw">return(m_vcolumns[column_index].m_vrows[row_index]);
  }
class="type">void CLabelsTable::TextColor(const class="type">int column_index,const class="type">int row_index,const class="type">color clr)
  {
class=class="str">"cmt">//--- 检查是否超出了列的范围
   class="type">int csize=::ArraySize(m_vcolumns);
   if(csize<class="num">1 || column_index<class="num">0 || column_index>=csize)
      class="kw">return;
class=class="str">"cmt">//--- 检查是否超出了行的范围
   class="type">int rsize=::ArraySize(m_vcolumns[column_index].m_vrows);
   if(rsize<class="num">1 || row_index<class="num">0 || row_index>=rsize)
      class="kw">return;
class=class="str">"cmt">//--- 设置颜色
   m_vcolumns[column_index].m_colors[row_index]=clr;
  }
class="type">void CLabelsTable::UpdateTable(class="type">void)
  {
class=class="str">"cmt">//--- 如果启用了固定表头模式,偏移一个索引
   class="type">int t=(m_fix_first_row) ? class="num">1 : class="num">0;
   class="type">int l=(m_fix_first_column) ? class="num">1 : class="num">0;
class=class="str">"cmt">//--- 取得垂直和水平滚动条滑块的当前位置
   class="type">int h=m_scrollh.CurrentPos()+l;
   class="type">int v=m_scrollv.CurrentPos()+t;
class=class="str">"cmt">//--- 在左侧列中移动表头
   if(m_fix_first_column)
     {
      m_columns[class="num">0].m_rows[class="num">0].Description(m_vcolumns[class="num">0].m_vrows[class="num">0]);
      class=class="str">"cmt">//--- 行
      for(class="type">int r=t; r<m_visible_rows_total; r++)

「滚动偏移下的单元格重绘逻辑」

表格在响应鼠标移动事件时,先通过 CHARTEVENT_MOUSE_MOVE 拿到光标的 x、y 以及左键状态,再交给 ScrollBarControl 判断是否触动了垂直或水平滑轨;任一滑轨生效就调用 UpdateTable() 重算视口。 重绘的核心是把滚动条滑块位置换算成虚拟行列下标:水平方向 h = m_scrollh.CurrentPos() + l,垂直方向 v = m_scrollv.CurrentPos() + t。这两个偏移量直接决定了可视区里每一个 m_columns[c].m_rows[r] 该绑定哪一条 m_vcolumns[h].m_vrows[v] 数据。 如果开启了首行固定(m_fix_first_row),会在双重循环之前单独把第 0 行表头按当前水平偏移 h 重写一次,避免滚动时列标题错乱。外层循环跑列、内层循环跑行,当 v 与 h 都落在有效范围内才执行颜色与描述的同步,否则该格保持空白。 在 MT5 里把这段接进自己的 CLabelsTable 派生类,拖动滚动条时应能看到数据随滑块位置无缝平移;若发现首行表头被覆盖,优先检查 m_fix_first_row 分支里的 h++ 是否漏了边界判断。

MQL5 / C++
   {
      if(r>=t && r<m_rows_total)
         m_columns[class="num">0].m_rows[r].Description(m_vcolumns[class="num">0].m_vrows[v]);
      class=class="str">"cmt">//---
      v++;
      }
   }
class=class="str">"cmt">//--- 在顶部行中移动表头
   if(m_fix_first_row)
   {
      m_columns[class="num">0].m_rows[class="num">0].Description(m_vcolumns[class="num">0].m_vrows[class="num">0]);
      class=class="str">"cmt">//--- 列
      for(class="type">int c=l; c<m_visible_columns_total; c++)
      {
         if(h>=l && h<m_columns_total)
            m_columns[c].m_rows[class="num">0].Description(m_vcolumns[h].m_vrows[class="num">0]);
         class=class="str">"cmt">//---
         h++;
      }
   }
class=class="str">"cmt">//--- 取得水平滚动条滑块的当前位置
   h=m_scrollh.CurrentPos()+l;
class=class="str">"cmt">//--- 列
   for(class="type">int c=l; c<m_visible_columns_total; c++)
   {
      class=class="str">"cmt">//--- 取得垂直滚动条滑块的当前位置
      v=m_scrollv.CurrentPos()+t;
      class=class="str">"cmt">//--- 行
      for(class="type">int r=t; r<m_visible_rows_total; r++)
      {
         class=class="str">"cmt">//--- 移动表格数据
         if(v>=t && v<m_rows_total && h>=l && h<m_columns_total)
           {
            class=class="str">"cmt">//--- 颜色的调整
            m_columns[c].m_rows[r].Color(m_vcolumns[h].m_colors[v]);
            class=class="str">"cmt">//--- 值的调整
            m_columns[c].m_rows[r].Description(m_vcolumns[h].m_vrows[v]);
            v++;
           }
      }
      class=class="str">"cmt">//---
      h++;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabelsTable::OnEvent(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">//--- 处理鼠标光标移动事件
   if(id==CHARTEVENT_MOUSE_MOVE)
   {
      class=class="str">"cmt">//--- 如果元件是隐藏的就退出
      if(!CElement::IsVisible())
         class="kw">return;
      class=class="str">"cmt">//--- 鼠标左键的坐标和状态
      class="type">int x=(class="type">int)lparam;
      class="type">int y=(class="type">int)dparam;
      m_mouse_state=(class="type">bool)class="type">int(sparam);
      class=class="str">"cmt">//--- 检查表格的焦点
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      class=class="str">"cmt">//--- 如果滑轨管理被启用,移动列表
      if(m_scrollv.ScrollBarControl(x,y,m_mouse_state) || m_scrollh.ScrollBarControl(x,y,m_mouse_state))
         UpdateTable();
      class=class="str">"cmt">//---
      class="kw">return;
   }
class=class="str">"cmt">//--- 处理对象的点击
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到表格类控件的渲染状态,你只需专注策略逻辑而非界面基建。

常见问题

可以。锁定模式支持仅锁首行、仅锁首列或同时锁定,由独立属性控制,滚动时表头区域保持静止。
前者单元格可交互输入,适合参数录入;后者只读展示,渲染更轻,超大数据集更稳。
小布盯盘的 AIGC 层已封装常见网格视图,你用策略输出二维数据即可,不必从 CLabelsTable 手写滚动逻辑。
当需要单元格内画走势缩略图或色块矩阵时,用 Canvas 直接绘比堆控件性能更好,也更容易做高密度视觉。
可见区行列数若大于总数,滚动条不出现;若小于总数但坐标算错,会出现空白格或重叠标签。