图形界面 X: 时间控件, 复选框列表控件和表格排序 (集成编译 6)·进阶篇
📘

图形界面 X: 时间控件, 复选框列表控件和表格排序 (集成编译 6)·进阶篇

第 2/3 篇

◍ 用复选框列表做多选项筛选

在 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 面板上,就能用索引直接读写每个复选框——外汇和贵金属界面交互高风险,参数误触可能下发错误下单指令,验证时先用模拟账户跑。

MQL5 / C++
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,否则按字符串比大小会排错。

MQL5 / C++
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 的初始逻辑,就能把默认排序方向反过来。

MQL5 / C++
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;

常见问题

用复选框列表控件维护一个选中项数组,每次勾选变更时回调刷新筛选结果,避免下拉框只能单选的局限。
通常拆成容器层、单项行层和勾选状态层,容器管坐标,单项管绘制,状态层单独存布尔值,改皮肤只动绘制层。
可以,小布能读取你勾选的品种组合,自动在对应页面推送异动提醒,不用你手动逐个加预警。
给每列绑一个类型标签,点击列头时按标签走比较函数,字符串走字典序、数字走数值比较,别混用。
快排能顺带交换整行关联数据且可控递归深度,现成函数多只排单数组,行错位是常见坑。