图形界面 X: 时间控件, 复选框列表控件和表格排序 (集成编译 6)·进阶篇
◍ 用复选框列表做多选项筛选
在 MT5 自定义面板里,CListView 只能单选,遇到要同时勾选多个交易品种或时间帧的场景就不够用了。比如用户只想盯 EURUSD、XAUUSD 和 M1、H1,就需要能多选的控件。 复选框列表(CCheckBoxList)由几层结构叠出来:最底层是控件通用背景,上面放垂直滚动条,再往上是复选框组,组内每个单元含背景、图标和文本标签。这种分层和 CListView 的差异在于——后者整行选中,前者在行内独立切换勾选状态。 外汇与贵金属波动剧烈、杠杆风险高,做这类多选界面时建议默认不预勾任何品种,避免用户误开过多持仓监控。
「复选框列表控件的类结构拆解」
在 MT5 自定义控件库里,复选框列表和普通的 CListView 最大区别是:每一项不是单个对象,而是由三个图形对象拼出来的——背景区、复选位图标签、文字标签,再加一条垂直滚动条。CCheckBoxList 类私有成员里就能看到 m_area(CRectLabel)、m_items[](CEdit)、m_checks[](CBmpLabel)、m_labels[](CLabel)和 m_scrollv(CScrollV)这五类对象。 除了文字内容,复选框还需要独立的开关状态数组 m_item_state[]。如果不维护这个布尔数组,界面上的勾选和底层数据会对不上,后续读取策略状态就会出错。 设置和读取状态的两个方法值得逐行看。SetItemState 先取数组大小,空数组直接 Print 报错;索引越界时自动夹到末位,再把 state 写进 m_item_state,最后调 ShiftList() 按滚动位置重绘。GetItemState 逻辑对称,返回对应索引的布尔值。 开 MT5 把这段类声明和俩方法丢进 CheckBoxList.mqh,编译后挂到 EA 面板上,就能用索引直接读写每个复选框——外汇和贵金属界面交互高风险,参数误触可能下发错误下单指令,验证时先用模拟账户跑。
class CCheckBoxList : class="kw">public CElement { class="kw">private : class=class="str">"cmt">//--- 创建列表对象 CRectLabel m_area; CEdit m_items[]; CBmpLabel m_checks[]; CLabel m_labels[]; CScrollV m_scrollv; class=class="str">"cmt">//--- class="kw">public : class=class="str">"cmt">//--- 创建控件方法 class="type">bool CreateCheckBoxList( class="kw">const class="type">long chart_id, class="kw">const class="type">int subwin, class="kw">const class="type">int x_gap, class="kw">const class="type">int y_gap); class=class="str">"cmt">//--- class="kw">private : class="type">bool CreateArea( class="type">void ); class="type">bool CreateList( class="type">void ); class="type">bool CreateItems( class="type">void ); class="type">bool CreateChecks( class="type">void ); class="type">bool CreateLabels( class="type">void ); class="type">bool CreateScrollV( class="type">void ); }; class CCheckBoxList : class="kw">public CElement { class="kw">private : class=class="str">"cmt">//--- 列表中复选框数值和状态的数组 class="type">class="kw">string m_item_value[]; class="type">bool m_item_state[]; class=class="str">"cmt">//--- class="kw">public : class=class="str">"cmt">//--- 返回/保存 (class="num">1) 状态和 (class="num">2) 指定索引处的列表项目文本 class="type">void SetItemState( class="kw">const class="type">uint item_index, class="kw">const class="type">bool state); class="type">void SetItemValue( class="kw">const class="type">uint item_index, class="kw">const class="type">class="kw">string value); class="type">bool GetItemState( class="kw">const class="type">uint item_index); class="type">class="kw">string GetItemValue( class="kw">const class="type">uint item_index); }; class="type">void CCheckBoxList::SetItemState( class="kw">const class="type">uint item_index, class="kw">const class="type">bool state) { class="type">uint array_size=:: ArraySize(m_item_state); if (array_size< class="num">1 ) :: Print( __FUNCTION__ , " > 如果列表内有至少一个项目, 则此方法被调用!" ); class="type">uint check_index=(item_index>=array_size)?array_size- class="num">1 : item_index; m_item_state[check_index]=state; ShiftList(); } class="type">bool CCheckBoxList::GetItemState( class="kw">const class="type">uint item_index) { class="type">uint array_size=:: ArraySize(m_item_state); if (array_size< class="num">1 ) :: Print( __FUNCTION__ , " > 如果列表内有至少一个项目, 则此方法被调用!" ); class="type">uint check_index=(item_index>=array_size)?array_size- class="num">1 : item_index; class="kw">return (m_item_state[check_index]); }
给表格加可点击排序列
做 MT5 面板时如果用了带数据的 CTable,常需要按列排序:点一下列标题升序(小到大),再点一下降序(大到小),和 MetaEditor 工具箱窗口那种按日期列倒排的逻辑一致。默认排序是关的,得先调 CTable::IsSortMode(true) 才激活交互。 悬浮和按下标题要换颜色,得往类里补三个背景色字段与方法,并在事件处理里调 HeaderColorByHover()。这段代码会遍历可见列,判断鼠标是否落在第 0 行标题区:没按左键用 hover 色,按了用 pressed 色,否则回到默认色。 排序箭头图标用 CBmpLabel 承载,CreateSignSortedData() 里若没设自定义 bmp,就退回 EasyAndFastGUI 的 SpinInc/SpinDec 两张图,建完立刻用 OBJ_NO_PERIODS 隐藏,等真正排序再显示。 表格单元格要支持小数位和类型区分。TOptions 结构里挂了 m_type 和 m_digits[],TableSize() 初始化时所有列默认 TYPE_STRING、小数位 0;想排实数得提前用 DataType() 设 ENUM_DATATYPE,否则按字符串比大小会排错。
class CTable : class="kw">public CElement { class="kw">private : class=class="str">"cmt">//--- 标题背景色 class="type">color m_headers_color; class="type">color m_headers_color_hover; class="type">color m_headers_color_pressed; class=class="str">"cmt">//--- class="kw">public : class=class="str">"cmt">//--- 标题背景色 class="type">void HeadersColor( class="kw">const class="type">color clr) { m_headers_color=clr; } class="type">void HeadersColorHover( class="kw">const class="type">color clr) { m_headers_color_hover=clr; } class="type">void HeadersColorPressed( class="kw">const class="type">color clr) { m_headers_color_pressed=clr; } }; class CTable : class="kw">public CElement { class="kw">private : class=class="str">"cmt">//--- 数据排序模式依据列 class="type">bool m_is_sort_mode; 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 CTable : class="kw">public CElement { class="kw">private : class=class="str">"cmt">//--- 当鼠标光标悬浮时更改表个标题颜色 class="type">void HeaderColorByHover( class="type">void ); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 当鼠标光标悬浮时更改表个标题颜色 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTable::HeaderColorByHover( class="type">void ) { class=class="str">"cmt">//--- 如果禁用列数据排序模式, 离开 if (!m_is_sort_mode || !m_fix_first_row) class="kw">return ; class=class="str">"cmt">//--- for ( class="type">uint c= class="num">0 ; c<m_visible_columns_total; c++) { class=class="str">"cmt">//--- 检查当前聚焦的标题 class="type">bool condition=m_mouse.X()>m_columns[c].m_rows[ class="num">0 ].X() && m_mouse.X()<m_columns[c].m_rows[ class="num">0 ].X2() && m_mouse.Y()>m_columns[c].m_rows[ class="num">0 ].Y() && m_mouse.Y()<m_columns[c].m_rows[ class="num">0 ].Y2(); class=class="str">"cmt">//--- if (!condition) m_columns[c].m_rows[ class="num">0 ].BackColor(m_headers_color); else { if (!m_mouse.LeftButtonState()) m_columns[c].m_rows[ class="num">0 ].BackColor(m_headers_color_hover); else m_columns[c].m_rows[ class="num">0 ].BackColor(m_headers_color_pressed); } } } class CTable : class="kw">public CElement { class="kw">private : class=class="str">"cmt">//--- 创建表格对象 CBmpLabel m_sort_arrow; class=class="str">"cmt">//--- class="kw">private : class=class="str">"cmt">//--- 创建表格方法 class="type">bool CreateSignSortedData( class="type">void ); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建一个箭头图标作为数据排序的标志 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTable::CreateSignSortedData( class="type">void ) { class=class="str">"cmt">//--- 如果禁用排序模式, 离开 if (!m_is_sort_mode) class="kw">return ( true ); class=class="str">"cmt">//--- 形成对象名 class="type">class="kw">string name=CElement::ProgramName()+ "_table_sort_array_" +( class="type">class="kw">string )CElement::Id(); class=class="str">"cmt">//--- 坐标 class="type">int x =(m_anchor_right_window_side)?m_columns[ class="num">0 ].m_rows[ class="num">0 ].X()-m_columns[ class="num">0 ].m_rows[ class="num">0 ].XSize()+m_sort_arrow_x_gap : m_columns[ class="num">0 ].m_rows[ class="num">0 ].X2()-m_sort_arrow_x_gap; class="type">int y =(m_anchor_bottom_window_side)?CElement::Y()-m_sort_arrow_y_gap : CElement::Y()+m_sort_arrow_y_gap; class=class="str">"cmt">//--- 如果未指定箭头的图标, 请设置默认值 if (m_sort_arrow_file_on== "" ) m_sort_arrow_file_on= "Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp" ; if (m_sort_arrow_file_off== "" ) m_sort_arrow_file_off= "Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp" ; class=class="str">"cmt">//--- 设置对象 if (!m_sort_arrow.Create(m_chart_id,name,m_subwin,x,y)) class="kw">return ( false ); class=class="str">"cmt">//--- 设置属性 m_sort_arrow.BmpFileOn( "::" +m_sort_arrow_file_on); m_sort_arrow.BmpFileOff( "::" +m_sort_arrow_file_off); m_sort_arrow.Corner(m_corner); m_sort_arrow.GetInteger( OBJPROP_ANCHOR ,m_anchor); m_sort_arrow.Selectable( false ); m_sort_arrow.Z_Order(m_zorder); m_sort_arrow.Tooltip( "\n" ); class=class="str">"cmt">//--- 保存坐标 m_sort_arrow.X(x); m_sort_arrow.Y(y); class=class="str">"cmt">//--- 保存大小 (对象) m_sort_arrow.XSize(m_sort_arrow.X_Size()); m_sort_arrow.YSize(m_sort_arrow.Y_Size()); class=class="str">"cmt">//--- 距边缘空余 m_sort_arrow.XGap((m_anchor_right_window_side)?x : x-m_wnd.X()); m_sort_arrow.YGap((m_anchor_bottom_window_side)?y : y-m_wnd.Y()); class=class="str">"cmt">//--- 隐藏对象 m_sort_arrow.Timeframes( OBJ_NO_PERIODS ); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_sort_arrow); class="kw">return ( true ); } class CTable : class="kw">public CElement { class="kw">private : class=class="str">"cmt">//--- 表格数值和属性的数组 class="kw">struct TOptions { ENUM_DATATYPE m_type; class="type">class="kw">string m_vrows[]; class="type">uint m_digits[]; ENUM_ALIGN_MODE m_text_align[]; class="type">color m_text_color[]; class="type">color m_cell_color[]; }; TOptions m_vcolumns[]; }; class CTable : class="kw">public CElement { class="kw">public : class=class="str">"cmt">//--- 设置指定表格单元的数值 class="type">void SetValue( class="kw">const class="type">uint column_index, class="kw">const class="type">uint row_index, class="kw">const class="type">class="kw">string value = "" , class="kw">const class="type">uint digits= class="num">0 ); }; class CTable : class="kw">public CElement { class="kw">public : class=class="str">"cmt">//--- 获取/设置数据类型 ENUM_DATATYPE DataType( class="kw">const class="type">uint column_index) { class="kw">return (m_vcolumns[column_index].m_type); } class="type">void DataType( class="kw">const class="type">uint column_index, class="kw">const ENUM_DATATYPE type) { m_vcolumns[column_index].m_type=type; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置表格大小 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTable::TableSize( class="kw">const class="type">uint columns_total, class="kw">const class="type">uint 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">uint i= class="num">0 ; i<m_columns_total; i++) { :: ArrayResize(m_vcolumns[i].m_vrows,m_rows_total); :: ArrayResize(m_vcolumns[i].m_digits,m_rows_total); :: ArrayResize(m_vcolumns[i].m_text_align,m_rows_total); :: ArrayResize(m_vcolumns[i].m_text_color,m_rows_total); :: ArrayResize(m_vcolumns[i].m_cell_color,m_rows_total);
◍ 表格排序为什么选快排而非 ArraySort
给 CTable 做行排序时,光交换单元格文本不够,连每列的文本颜色数组也要同步换位。私有方法 Swap() 用一层 for 循环遍历所有列,把 r1、r2 两行的 m_vrows 与 m_text_color 逐一暂存互换,避免排完序后颜色错位。 比较环节交给 CheckSortCondition(),它按列声明的 m_type 把字符串转成对应类型再比大小:TYPE_STRING 直接比、TYPE_DOUBLE 用 StringToDouble、TYPE_DATETIME 走 StringToTime,其余回落到 long。direction 参数控制是大于还是小于,从而支撑双向排序。 作者实测过十种算法,数组规模先放到 100,000 个随机元素:快速排序比标准 ArraySort() 还快,耗时压进 1 秒以内;当数组放大到 1,000,000 时,ArraySort() 反超但快排仅略差。最终没选 ArraySort(),原因很硬:它不支持双向、不能处理结构数组里的附属属性(颜色等)、也无法只挪数值不动其他字段。 实际用的 QuickSort() 是标准库版本的改造,递归里调用前面那两个私有方法。公有入口 SortData() 默认排第 0 列,若上次是降序或换列就切回升序,排完刷新表格并打箭头标记。你在 MT5 里改 m_last_sort_direction 的初始逻辑,就能把默认排序方向反过来。
class="type">void CTable::Swap(class="type">uint c, class="type">uint r1, class="type">uint r2) { class=class="str">"cmt">//--- 在循环里迭代所有列 for(class="type">uint i=class="num">0; i<m_columns_total; i++) { class=class="str">"cmt">//--- 交换文本 class="type">class="kw">string temp_text =m_vcolumns[i].m_vrows[r1]; m_vcolumns[i].m_vrows[r1] =m_vcolumns[i].m_vrows[r2]; m_vcolumns[i].m_vrows[r2] =temp_text; class=class="str">"cmt">//--- 交换文本颜色 class="type">color temp_text_color =m_vcolumns[i].m_text_color[r1]; m_vcolumns[i].m_text_color[r1] =m_vcolumns[i].m_text_color[r2]; m_vcolumns[i].m_text_color[r2] =temp_text_color; } } class="type">bool CTable::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) { class="type">bool condition=false; class="kw">switch(m_vcolumns[column_index].m_type) { case TYPE_STRING: {class="type">class="kw">string v1=m_vcolumns[column_index].m_vrows[row_index]; class="type">class="kw">string v2=check_value; condition=(direction)?v1>v2 : v1<v2; class="kw">break;} case TYPE_DOUBLE: {class="type">class="kw">double v1=class="type">class="kw">double(m_vcolumns[column_index].m_vrows[row_index]); class="type">class="kw">double v2=class="type">class="kw">double(check_value); condition=(direction)?v1>v2 : v1<v2; class="kw">break;} case TYPE_DATETIME: {class="type">class="kw">datetime v1=::StringToTime(m_vcolumns[column_index].m_vrows[row_index]); class="type">class="kw">datetime v2=::StringToTime(check_value); condition=(direction)?v1>v2 : v1<v2; class="kw">break;} class="kw">default: {class="type">long v1=(class="type">long)m_vcolumns[column_index].m_vrows[row_index]; class="type">long v2=(class="type">long)check_value; condition=(direction)?v1>v2 : v1<v2; class="kw">break;} } class="kw">return(condition); } class="type">void CTable::QuickSort(class="type">uint beg, class="type">uint end, class="type">uint column, class="kw">const ENUM_SORT_MODE mode=SORT_ASCEND) { class="type">uint r1=beg; class="type">uint r2=end; class="type">uint c=column; class="type">class="kw">string value=NULL; class="type">uint data_total=m_rows_total-class="num">1; class="kw">while(r1<end) { value=m_vcolumns[c].m_vrows[(beg+end)>>class="num">1]; class="kw">while(r1<r2) { class="kw">while(CheckSortCondition(c,r1,value,(mode==SORT_ASCEND)?false:true)) {if(r1==data_total) class="kw">break; r1++;} class="kw">while(CheckSortCondition(c,r2,value,(mode==SORT_ASCEND)?true:false)) {if(r2==class="num">0) class="kw">break; r2--;} if(r1<=r2) {Swap(c,r1,r2); if(r2==class="num">0){r1++; class="kw">break;} r1++; r2--;} } if(beg<r2) QuickSort(beg,r2,c,mode); beg=r1; r2=end; } } class="type">void CTable::SortData(class="kw">const class="type">uint column_index=class="num">0) { class="type">uint first_index=(m_fix_first_row)?class="num">1:class="num">0; class="type">uint last_index=m_rows_total-class="num">1; if(m_is_sorted_column_index==WRONG_VALUE || column_index!=m_is_sorted_column_index || m_last_sort_direction==SORT_DESCEND) m_last_sort_direction=SORT_ASCEND;