图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)·进阶篇
(2/3)· 从列行重建到双击事件,搞懂 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) 下锁定,或只在回测观察器里开排序。
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 列),同时把选中行、排序方向、排序列索引一并清零。外汇和贵金属图表上跑这类自定义表格控件属于高频重绘操作,实盘前请在策略测试器里验证内存占用与闪烁表现。
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]
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 占用概率。
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。
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 时需注意,这类自定义事件处理仍受终端主线程调度影响,极端行情下点击响应可能有几帧延迟,属正常概率现象。 下面这段是核心声明的精简版,重点看高亮部分如何落地阈值与检测调用。
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(); }