图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)·进阶篇
🧩

图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)·进阶篇

(2/3)· 从列行重建到双击事件,搞懂 CCanvasTable 如何接管旧表格的六个盲区

偏理论 第 2/3 篇
很多交易者把表格当静态展示,排序一触发就错位、控件丢失属性。旧 CTable 在单元格塞复选框时根本撑不住,硬套只会越改越乱。CCanvasTable 从本篇起才完全兼容且在某些点反超,别再抱着老对象写补丁。

◍ 表格控件的列点击排序逻辑

在 MT5 自定义表格控件里,点表头就能触发排序,核心是先定位鼠标坐标落在哪一列。下面这段截取自 CCanvasTable 类的点击处理尾部,先比对 x 坐标区间拿到 column_index,再调 SortData 重排并广播自定义事件。 if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2) // 判断点击x是否在列c的左右边界内 { column_index=c; // 命中则记录列索引 break; // 跳出循环不再查其他列 } SortData(column_index); // 按命中列做数据排序 EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,EnumToString(DataType(column_index))); // 向图表发自定义事件通知排序列与类型 return(true); // 点击处理成功返回 类里用 m_is_sort_mode 控制是否允许排序,m_is_sorted_column_index 存当前排序列(WRONG_VALUE 表示未排序),m_last_sort_direction 记上次升/降方向。SortData 默认排第 0 列,内部走 QuickSort 快排,CheckSortCondition 按字符串值比大小决定交换,因此文本列和数字列混排时可能顺序不符合数值直觉。 实盘加载这类控件做报价面板时,外汇与贵金属波动快、点差跳变频繁,误触表头重排可能让你短暂看错价位层级,建议在 IsSortMode(false) 下锁定,或只在回测观察器里开排序。

MQL5 / C++
   class=class="str">"cmt">//--- 如果找到标题, 保存其索引
   if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2)
     {
      column_index=c;
      class="kw">break;
     }
   }
class=class="str">"cmt">//--- 根据指定的列排序数据
   SortData(column_index);
class=class="str">"cmt">//--- 发送有关消息
   ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index)));
   class="kw">return(true);
   }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 按照列排序数据的模式
  class="type">bool              m_is_sort_mode;
  class=class="str">"cmt">//--- 排序列的索引 (WRONG_VALUE – 表格未排序)
  class="type">int               m_is_sorted_column_index;
  class=class="str">"cmt">//--- 最后排序方向
  ENUM_SORT_MODE    m_last_sort_direction;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 数据排序模式
  class="type">void              IsSortMode(class="kw">const class="type">bool flag)                { m_is_sort_mode=flag;               }
  class=class="str">"cmt">//--- 获取/设置数据类型
  ENUM_DATATYPE     DataType(class="kw">const class="type">uint column_index);
  class="type">void              DataType(class="kw">const class="type">uint column_index,class="kw">const ENUM_DATATYPE type);
  class=class="str">"cmt">//--- 根据指定的列排序数据
  class="type">void              SortData(class="kw">const class="type">uint column_index=class="num">0);
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 处理在标题上单击
  class="type">bool              OnClickHeaders(class="kw">const class="type">class="kw">string clicked_object);
  class=class="str">"cmt">//--- 快速排序方法
  class="type">void              QuickSort(class="type">uint beg,class="type">uint end,class="type">uint column,class="kw">const ENUM_SORT_MODE mode=SORT_ASCEND);
  class=class="str">"cmt">//--- 检查排序条件
  class="type">bool              CheckSortCondition(class="type">uint column_index,class="type">uint row_index,class="kw">const class="type">class="kw">string check_value,class="kw">const class="type">bool direction);
  };

「在任意索引插列删列的实现思路」

老版 CTable 只能在表格末尾追加列和行,遇到要在开头插一列的需求就只能整体右移。新封装的 CCanvasTable 改成按索引操作:比如在索引 0 插新列时,列数组先扩容一个元素,所有单元格属性和数值整体向右挪一格,腾出来的首列全是空白单元,插行同理。 删除则反向处理——以删第一列为例,先把数据和属性向左移一个元素覆盖掉目标列,再把列数组大小减一。这样逻辑比旧版直白很多,代码量也降了。 初始化靠两个私有辅助方法顶上:ColumnInitialize() 给指定列填省缺值,CellInitialize() 给指定单元格填省缺值。从代码能看到,省缺列宽固定 100、对齐 ALIGN_CENTER、类型 TYPE_STRING;单元格默认无图像、文本色取 m_cell_text_color、类型 CELL_SIMPLE。 复制用 ColumnCopy(目的索引, 源索引) 搬整列属性,CellCopy() 搬单格,参数就是收发双方的列行索引。每次增删后必须调 RecalculateAndResizeTable(bool) 重算尺寸并重绘,true 为彻底重绘、false 为轻量刷新。 排序状态会跟着列走:删掉排序列时,记录排序列索引的字段直接置零。清表只需 TableSize() 设最小(留 1 行 1 列),同时把选中行、排序方向、排序列索引一并清零。外汇和贵金属图表上跑这类自定义表格控件属于高频重绘操作,实盘前请在策略测试器里验证内存占用与闪烁表现。

MQL5 / C++
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 使用省缺值初始化指定的列
   class="type">void                ColumnInitialize(class="kw">const class="type">uint column_index);
   class=class="str">"cmt">//--- 使用省缺值初始化指定的单元格
   class="type">void                CellInitialize(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用省缺值初始化指定的列                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::ColumnInitialize(class="kw">const class="type">uint column_index)
  {
class=class="str">"cmt">//--- 使用省缺值初始化指定列的属性
   m_columns[column_index].m_x           =class="num">0;
   m_columns[column_index].m_x2          =class="num">0;
   m_columns[column_index].m_width       =class="num">100;
   m_columns[column_index].m_type        =TYPE_STRING;
   m_columns[column_index].m_text_align  =ALIGN_CENTER;
   m_columns[column_index].m_text_x_offset =m_text_x_offset;
   m_columns[column_index].m_image_x_offset =m_image_x_offset;
   m_columns[column_index].m_image_y_offset =m_image_y_offset;
   m_columns[column_index].m_header_text ="";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用省缺值初始化指定的单元格                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::CellInitialize(class="kw">const class="type">uint column_index,class="kw">const class="type">uint row_index)
  {
   m_columns[column_index].m_rows[row_index].m_full_text     ="";
   m_columns[column_index].m_rows[row_index].m_short_text     ="";
   m_columns[column_index].m_rows[row_index].m_selected_image =class="num">0;
   m_columns[column_index].m_rows[row_index].m_text_color     =m_cell_text_color;
   m_columns[column_index].m_rows[row_index].m_digits         =class="num">0;
   m_columns[column_index].m_rows[row_index].m_type           =CELL_SIMPLE;
class=class="str">"cmt">//--- 省缺情况下, 单元格不包含图像
   ::ArrayFree(m_columns[column_index].m_rows[row_index].m_images);
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 将指定列 (源) 复制到新位置 (目的)。
   class="type">void                ColumnCopy(class="kw">const class="type">uint destination,class="kw">const class="type">uint source);
  };

自绘表格的列与单元格拷贝逻辑

在 MT5 的 CCanvasTable 自绘表格类里,列级别复制由 ColumnCopy 完成:传入目的列号和源列号,直接把表头文本、列宽、对齐方式、文本与图像偏移等 7 个成员变量逐字段覆盖。注意它只搬列属性,不碰具体单元格数据。 单元格级别复制由 CellCopy 承担,参数是「目的列/行 + 源列/行」四元组。除了类型、小数位、长短文本、文字色、选中图这 6 个字段,它还用 ArraySize 取源单元格图像数组长度,ArrayResize 给目的数组扩容,再循环调用 ImageCopy 逐图拷贝;若某图像数据长度小于 1 就 continue 跳过。 任何拷贝或增删后,都要靠 RecalculateAndResizeTable 收尾:先 CalculateTableSize 算尺寸,再 ChangeTableSize 改缓冲,最后 UpdateTable 重绘(redraw 默认 false)。外汇与贵金属图表叠加这类自绘控件时波动快,实盘误用可能触发重绘卡顿,属高风险操作。 [CODE]void CCanvasTable::ColumnCopy(const uint destination,const uint source) { m_columns[destination].m_header_text =m_columns[source].m_header_text; m_columns[destination].m_width =m_columns[source].m_width; m_columns[destination].m_type =m_columns[source].m_type; m_columns[destination].m_text_align =m_columns[source].m_text_align; m_columns[destination].m_text_x_offset =m_columns[source].m_text_x_offset; m_columns[destination].m_image_x_offset =m_columns[source].m_image_x_offset; m_columns[destination].m_image_y_offset =m_columns[source].m_image_y_offset; } class CCanvasTable : public CElement { private: void CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source); }; void CCanvasTable::CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source) { m_columns[column_dest].m_rows[row_dest].m_type =m_columns[column_source].m_rows[row_source].m_type; m_columns[column_dest].m_rows[row_dest].m_digits =m_columns[column_source].m_rows[row_source].m_digits; m_columns[column_dest].m_rows[row_dest].m_full_text =m_columns[column_source].m_rows[row_source].m_full_text; m_columns[column_dest].m_rows[row_dest].m_short_text =m_columns[column_source].m_rows[row_source].m_short_text; m_columns[column_dest].m_rows[row_dest].m_text_color =m_columns[column_source].m_rows[row_source].m_text_color; m_columns[column_dest].m_rows[row_dest].m_selected_image=m_columns[column_source].m_rows[row_source].m_selected_image; int images_total=::ArraySize(m_columns[column_source].m_rows[row_source].m_images); ::ArrayResize(m_columns[column_dest].m_rows[row_dest].m_images,images_total); for(int i=0; i<images_total; i++) { if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<1) continue; ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i); } } class CCanvasTable : public CElement { private: void RecalculateAndResizeTable(const bool redraw=false); }; void CCanvasTable::RecalculateAndResizeTable(const bool redraw=false) { CalculateTableSize(); ChangeTableSize(); UpdateTable(redraw); }[/CODE]

MQL5 / C++
class="type">void CCanvasTable::ColumnCopy(class="kw">const class="type">uint destination,class="kw">const class="type">uint source)
  {
   m_columns[destination].m_header_text     =m_columns[source].m_header_text;
   m_columns[destination].m_width           =m_columns[source].m_width;
   m_columns[destination].m_type            =m_columns[source].m_type;
   m_columns[destination].m_text_align      =m_columns[source].m_text_align;
   m_columns[destination].m_text_x_offset   =m_columns[source].m_text_x_offset;
   m_columns[destination].m_image_x_offset  =m_columns[source].m_image_x_offset;
   m_columns[destination].m_image_y_offset  =m_columns[source].m_image_y_offset;
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class="type">void               CellCopy(class="kw">const class="type">uint column_dest,class="kw">const class="type">uint row_dest,class="kw">const class="type">uint column_source,class="kw">const class="type">uint row_source);
  };
class="type">void CCanvasTable::CellCopy(class="kw">const class="type">uint column_dest,class="kw">const class="type">uint row_dest,class="kw">const class="type">uint column_source,class="kw">const class="type">uint row_source)
  {
   m_columns[column_dest].m_rows[row_dest].m_type         =m_columns[column_source].m_rows[row_source].m_type;
   m_columns[column_dest].m_rows[row_dest].m_digits       =m_columns[column_source].m_rows[row_source].m_digits;
   m_columns[column_dest].m_rows[row_dest].m_full_text    =m_columns[column_source].m_rows[row_source].m_full_text;
   m_columns[column_dest].m_rows[row_dest].m_short_text   =m_columns[column_source].m_rows[row_source].m_short_text;
   m_columns[column_dest].m_rows[row_dest].m_text_color   =m_columns[column_source].m_rows[row_source].m_text_color;
   m_columns[column_dest].m_rows[row_dest].m_selected_image=m_columns[column_source].m_rows[row_source].m_selected_image;
   class="type">int images_total=::ArraySize(m_columns[column_source].m_rows[row_source].m_images);
   ::ArrayResize(m_columns[column_dest].m_rows[row_dest].m_images,images_total);
   for(class="type">int i=class="num">0; i<images_total; i++)
     {
      if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<class="num">1)
        class="kw">continue;
      ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i);
     }
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class="type">void               RecalculateAndResizeTable(class="kw">const class="type">bool redraw=class="kw">false);
  };
class="type">void CCanvasTable::RecalculateAndResizeTable(class="kw">const class="type">bool redraw=class="kw">false)
  {
   CalculateTableSize();
   ChangeTableSize();
   UpdateTable(redraw);
  }

◍ 表格列的动态增删逻辑

在 MT5 的 CCanvasTable 自定义绘图表里,列并非固定结构,而是用动态数组承载。AddColumn 在指定索引插入一列,DeleteColumn 则移除指定列,两者都通过 ArrayResize 重排底层存储,并在末尾调用 RecalculateAndResizeTable 重算整表尺寸。 AddColumn 先把总列数加 1,将 m_columns 数组扩容,再把新列的行数组大小对齐到 m_rows_total。若传入的 column_index 超出范围,会被钳制为末列索引(m_columns_total-1),随后从末尾向前循环,把旧列数据整体后移,空出目标位置并由 ColumnInitialize、CellInitialize 填默认值。 一个容易踩坑的细节:若当前表处于按某列排序的状态(m_is_sorted_column_index != WRONG_VALUE),插入列时若该索引被向后平移,排序标记必须同步自增,否则后续排序会错位到错误列。删除列时逻辑相反,从目标索引向前覆盖,同样要处理排序索引的偏移。 外汇与贵金属图表上用这类自定义表做盘口快照时,频繁增删列会带来重绘开销,建议在 tick 批量更新阶段关闭 redraw 参数,一次性重绘,降低 CPU 占用概率。

MQL5 / C++
class CCanvasTable : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 在表格的指定索引处添加一列
  class="type">void              AddColumn(class="kw">const class="type">int column_index,class="kw">const class="type">bool redraw=class="kw">false);
  class=class="str">"cmt">//--- 从表格的指定索引处删除一列
  class="type">void              DeleteColumn(class="kw">const class="type">int column_index,class="kw">const class="type">bool redraw=class="kw">false);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在表格的指定索引处添加一列                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::AddColumn(class="kw">const class="type">int column_index,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 将数组大小增加一个元素
  class="type">int array_size=(class="type">int)ColumnsTotal();
  m_columns_total=array_size+class="num">1;
  ::ArrayResize(m_columns,m_columns_total);
class=class="str">"cmt">//--- 设置行数组的大小
  ::ArrayResize(m_columns[array_size].m_rows,m_rows_total);
class=class="str">"cmt">//--- 在超出范围的情况下调整索引
  class="type">int checked_column_index=(column_index>=(class="type">int)m_columns_total)? (class="type">int)m_columns_total-class="num">1 : column_index;
class=class="str">"cmt">//--- 平移其它列 (从数组的末尾开始添加列的索引)
  for(class="type">int c=array_size; c>=checked_column_index; c--)
    {
    class=class="str">"cmt">//--- 平移排序数组的符号
    if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE)
      m_is_sorted_column_index++;
    class=class="str">"cmt">//--- 前一列的索引
    class="type">int prev_c=c-class="num">1;
    class=class="str">"cmt">//--- 使用省缺值初始化新列
    if(c==checked_column_index)
      ColumnInitialize(c);
    class=class="str">"cmt">//--- 将数据从前一列移动到当前列
    else
      ColumnCopy(c,prev_c);
    class=class="str">"cmt">//---
    for(class="type">uint r=class="num">0; r<m_rows_total; r++)
      {
      class=class="str">"cmt">//--- 使用省缺值初始化新的列单元
      if(c==checked_column_index)
        {
        CellInitialize(c,r);
        class="kw">continue;
        }
      class=class="str">"cmt">//--- 将数据从前一列单元动到当前列单元
      CellCopy(c,r,prev_c,r);
      }
    }
class=class="str">"cmt">//--- 计算并调整到表格大小
  RecalculateAndResizeTable(redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从表格指定索引处删除一列                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::DeleteColumn(class="kw">const class="type">int column_index,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 获取列数组的大小
  class="type">int array_size=(class="type">int)ColumnsTotal();
class=class="str">"cmt">//--- 在超出范围的情况下调整索引
  class="type">int checked_column_index=(column_index>=array_size)? array_size-class="num">1 : column_index;
class=class="str">"cmt">//--- 平移其它列 (从指定索引开始到最后一列)
  for(class="type">int c=checked_column_index; c<array_size-class="num">1; c++)
    {

「删列与重建时的索引自修正」

在 CCanvasTable 里手动删掉某一列时,最容易踩的坑是排序状态索引没跟着变。上面这段逻辑做的事很直接:若被删的列不是当前排序列,就检查排序列索引是否大于被删列,是的话把 m_is_sorted_column_index 减 1,保证后续排序仍指向同一语义列;若删的正是排序列,则把该索引置为 WRONG_VALUE,等价于“已无排序”。 紧接着代码把下一列数据通过 ColumnCopy 和 CellCopy 前移补齐,并把 m_columns_total 设为 array_size-1 后调用 ArrayResize 收缩底层数组,最后走 RecalculateAndResizeTable 重算坐标。实测中若漏掉 m_is_sorted_column_index-- 这一步,删前列后再点表头排序,有概率让画面按错误列重排。 Rebuilding 与 Clear 是更上层的封装:前者用 TableSize(columns_total,rows_total) 直接改维并重算,后者强制 TableSize(1,1) 并把选中文本、选中项、排序方向等复位成默认。做自定义面板时,别在外部直接改数组长度,统一走这两个方法能少很多幽灵 bug。

MQL5 / C++
  if(c!=checked_column_index)
      {
       if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE)
          m_is_sorted_column_index--;
      }
   class=class="str">"cmt">//--- 如果排序的列已被删除, 则为零
   else
       m_is_sorted_column_index=WRONG_VALUE;
   class=class="str">"cmt">//--- 下一列的索引
   class="type">int next_c=c+class="num">1;
   class=class="str">"cmt">//--- 将数据从下一列移动到当前列
   ColumnCopy(c,next_c);
   class=class="str">"cmt">//--- 将下一个列单元中的数据移动到当前列单元
   for(class="type">uint r=class="num">0; r<m_rows_total; r++)
      CellCopy(c,r,next_c,r);
   }
class=class="str">"cmt">//--- 将列数组减少一个元素
   m_columns_total=array_size-class="num">1;
   ::ArrayResize(m_columns,m_columns_total);
class=class="str">"cmt">//--- 计算并调整到表格大小
   RecalculateAndResizeTable(redraw);
   }
class CCanvasTable : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 重建表格
   class="type">void               Rebuilding(class="kw">const class="type">int columns_total,class="kw">const class="type">int rows_total,class="kw">const class="type">bool redraw=class="kw">false);
   class=class="str">"cmt">//--- 清除表格只留下一列和一行。
   class="type">void               Clear(class="kw">const class="type">bool redraw=class="kw">false);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 重建表格                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::Rebuilding(class="kw">const class="type">int columns_total,class="kw">const class="type">int rows_total,class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 设置新的大小
   TableSize(columns_total,rows_total);
class=class="str">"cmt">//--- 计算并调整到表格大小
   RecalculateAndResizeTable(redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 清除表格。仅留下一列一行。                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::Clear(class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 设置最小大小 1x1
   TableSize(class="num">1,class="num">1);
class=class="str">"cmt">//--- 设置省缺值
   m_selected_item_text     ="";
   m_selected_item          =WRONG_VALUE;
   m_last_sort_direction    =SORT_ASCEND;
   m_is_sorted_column_index =WRONG_VALUE;
class=class="str">"cmt">//--- 计算并调整到表格大小
   RecalculateAndResizeTable(redraw);
  }

用按下-释放-按下捕获左键双击

想在 MT5 图表上响应鼠标左键双击,不能依赖系统级的双击消息。实测发现,CHARTEVENT_MOUSE_MOVE 带 sparam 到来时,并不总能立刻跟上左键按下的轨迹,所以更稳的做法是在 CHARTEVENT_CLICK 事件里自己判断“按下-释放-按下”的节奏。 默认两次点击间隔阈值是 300 毫秒,由 CMouse 类的 m_pause_between_clicks 字段控制。CheckDoubleClick() 每次被调用时用两个 static uint 变量存上一次和本次的 GetTickCount() 毫秒数,差值小于阈值就向外抛 ON_DOUBLE_CLICK 事件(标识符编号 34)。 这套逻辑的好处是:无论光标底下有没有图形对象,图表任意位置都能捕获双击。外汇与贵金属图表挂 EA 时需注意,这类自定义事件处理仍受终端主线程调度影响,极端行情下点击响应可能有几帧延迟,属正常概率现象。 下面这段是核心声明的精简版,重点看高亮部分如何落地阈值与检测调用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Defines.mqh |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define ON_DOUBLE_CLICK(class="num">34) class=class="str">"cmt">// 鼠标左键双击事件

class CMouse
  {
class="kw">private:
  class=class="str">"cmt">//--- 暂停按钮鼠标左键点击 (用于判断双击)
  class="type">uint                m_pause_between_clicks;
class="kw">private:
  class=class="str">"cmt">//--- 检查鼠标左键双击
  class="type">bool                CheckDoubleClick(class="type">void);
  };

CMouse::CMouse(class="type">void) : m_pause_between_clicks(class="num">300)
  {
  }

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)
  {
  if(id==CHARTEVENT_CLICK)
    {
    CheckDoubleClick();
    class="kw">return;
    }
  }

class="type">void CMouse::CheckDoubleClick(class="type">void)
  {
  class="kw">static class="type">uint prev_depressed =class="num">0;
  class="kw">static class="type">uint curr_depressed =::GetTickCount();
  prev_depressed =curr_depressed;
  curr_depressed =::GetTickCount();
  }
把表格诊断交给小布盯盘
这些表格状态与小布盯盘的 AIGC 已内置,打开对应品种页即可看到排序与控件渲染是否异常,你只管判断信号。

常见问题

用 SortArrowXGap 和 SortArrowYGap 两个方法控制,图标索引 0 为升序、-1 为降序,仅在排序模式开启且数据已排后才绘制。
可接管单元格进入编辑或弹出上下文操作,本篇将左键双击作为图形界面用户管理的扩展入口单独处理。
可以,小布盯盘的品种页已内置表格渲染诊断,控件丢失或排序错位会直接标出,省去你翻代码。
因为不仅要搬文本和图像,颜色、对齐、绑定控件等属性都要随单元格走,ImageCopy 辅助方法就是为消重复代码而加的。
本篇起 CCanvasTable 完全兼容其它表格类型,建议直接转类型,旧对象在单元格加按钮或复选框时能力不够。