图形界面 X: 时间控件, 复选框列表控件和表格排序 (集成编译 6)·综合运用
◍ 列头点击触发排序与箭头跟随
CTable 里处理列头点击的是 OnClickTableHeaders()。排序模式未开、点到的不是表格单元、控件 ID 不匹配、或者点的不是顶行(RowIndexFromObjectName 返回值大于 0),方法会直接 return false,不做任何排序。 通过对象名反解列索引时有个细节:若开启了首列固定(m_fix_first_column),循环起点 l 置 1,水平滚动条滑块位置 h 要加上这个偏移;否则 l=0、h 从 0 起算。找到被点中的列后调用 SortData(column_index),随后用 EventChartCustom 抛出 ON_SORT_DATA 事件,附带控件 ID、排序列索引和该列数据类型三个参数。 列数超过可视列数时,拖水平滚动条会让排序箭头错位。ShiftSortArrow() 负责把 m_sort_arrow 的 X 坐标重算到当前列头右缘减去 m_sort_arrow_x_gap 的位置,并根据锚定边设置 XGap;箭头对象在元素可见时才置为 OBJ_ALL_PERIODS。 UpdateTable() 在平移标题的循环里埋了检测:一旦发现 h 等于 m_is_sorted_column_index 就置 is_shift_sort_arrow 并调用 ShiftSortArrow;循环结束若该标志仍假且排序列合法,说明排序列已滑出可视区——索引大于 0 或非固定首列时箭头直接 OBJ_NO_PERIODS 隐藏,唯固定首列例外,仍对其调 ShiftSortArrow(0)。 外汇与贵金属图表控件开发属高风险定制,建议在 MT5 策略测试器外另建指标工程,逐行改 m_fix_first_column 与 m_visible_columns_total 验证箭头行为。
else m_last_sort_direction=SORT_DESCEND; class=class="str">"cmt">//--- 保存最后排序的数据列的索引 m_is_sorted_column_index=( class="type">int )column_index; class=class="str">"cmt">//--- 排序 QuickSort(first_index,last_index,column_index,m_last_sort_direction); class=class="str">"cmt">//--- 刷新表格 UpdateTable(); class=class="str">"cmt">//--- 根据排序方向设置图标 m_sort_arrow.State((m_last_sort_direction==SORT_ASCEND)? true : false ); } class="type">bool CTable::OnClickTableHeaders( class="kw">const class="type">class="kw">string clicked_object) { class=class="str">"cmt">//--- 如果禁用排序模式, 离开 if (!m_is_sort_mode) class="kw">return ( false ); class=class="str">"cmt">//--- 如果未有表格单元点击, 离开 if (:: StringFind(clicked_object,CElement::ProgramName()+ "_table_edit_" , class="num">0 )< class="num">0 ) class="kw">return ( false ); class=class="str">"cmt">//--- 从对象名中获取标识符 class="type">int id=CElement::IdFromObjectName(clicked_object); class=class="str">"cmt">//--- 如果标识符不匹配, 离开 if (id!=CElement::Id()) class="kw">return ( false ); class=class="str">"cmt">//--- 如果不是表格标题, 离开 if (RowIndexFromObjectName(clicked_object)> class="num">0 ) class="kw">return ( false ); class=class="str">"cmt">//--- 用于确定列索引 class="type">uint column_index= class="num">0 ; class=class="str">"cmt">//--- 如果启用了固定标题模式, 平移一个索引 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=class="str">"cmt">//--- 列 for ( class="type">uint c=l; c<m_visible_columns_total; c++) { class=class="str">"cmt">//--- 如果并非在此单元按下 if (m_columns[c].m_rows[ class="num">0 ].Name()==clicked_object) { class=class="str">"cmt">//--- 获取列索引 column_index=(m_fix_first_column && c== class="num">0 ) ? class="num">0 : h; class="kw">break ; } class=class="str">"cmt">//--- h++; } class=class="str">"cmt">//--- 根据指定列进行数据排序 SortData(column_index); class=class="str">"cmt">//--- 发送有关消息 :: EventChartCustom(m_chart_id,ON_SORT_DATA,CElement::Id(),m_is_sorted_column_index,:: EnumToString(DataType(column_index))); class="kw">return ( true ); } class="type">void CTable::ShiftSortArrow( class="kw">const class="type">uint column) { class=class="str">"cmt">//--- 如果元素未隐藏, 则显示对象 if (CElement::IsVisible()) m_sort_arrow.Timeframes( OBJ_ALL_PERIODS ); class=class="str">"cmt">//--- 计算并设置坐标 class="type">int x=m_columns[column].m_rows[ class="num">0 ].X2()-m_sort_arrow_x_gap; m_sort_arrow.X(x); m_sort_arrow.X_Distance(x); class=class="str">"cmt">//--- 距边缘空余 m_sort_arrow.XGap((m_anchor_right_window_side)?m_wnd.X2()-x : x-m_wnd.X()); } class="type">void CTable::UpdateTable( class="type">void ) { class=class="str">"cmt">//... class=class="str">"cmt">//--- 在顶行中平移标题 if (m_fix_first_row) { class=class="str">"cmt">//--- 用于确定排序图标的移位 class="type">bool is_shift_sort_arrow= false ; class=class="str">"cmt">//--- 列 for ( class="type">uint c=l; c<m_visible_columns_total; c++) { class=class="str">"cmt">//--- 如果未超出数组范围 if (h>=l && h<m_columns_total) { class=class="str">"cmt">//--- 如果发现已排序列 if (!is_shift_sort_arrow && m_is_sort_mode && h==m_is_sorted_column_index) { is_shift_sort_arrow= true ; class=class="str">"cmt">//--- 调整排序图标 class="type">uint column=h-(h-c); if (column>=l && column<m_visible_columns_total) ShiftSortArrow(column); } class=class="str">"cmt">//--- 调整 (class="num">1) 数值, (class="num">2) 背景颜色, (class="num">3) 文本颜色和 (class="num">4) 文本在单元内对齐 SetCellParameters(c, class="num">0 ,m_vcolumns[h].m_vrows[ class="num">0 ],m_headers_color,m_headers_text_color,m_vcolumns[h].m_text_align[ class="num">0 ]); } class=class="str">"cmt">//--- h++; } class=class="str">"cmt">//--- 如果存在已排序表格, 但未发现 if (!is_shift_sort_arrow && m_is_sort_mode && m_is_sorted_column_index!= WRONG_VALUE ) { class=class="str">"cmt">//--- 隐藏, 如果索引大于零 if (m_is_sorted_column_index> class="num">0 || !m_fix_first_column) m_sort_arrow.Timeframes( OBJ_NO_PERIODS ); class=class="str">"cmt">//--- 设置首列的标题 else ShiftSortArrow( class="num">0 ); } } class=class="str">"cmt">//... }
控件字体与边距的独立控制
这一版函数库把字体控制下沉到了 CElement 基类。之前所有控件共用一套字体属性,现在每个控件都能单独指定字体名和字号,默认仍是 Calibri 8 点。基类里新增了 m_font 和 m_font_size 两个受保护字段,以及 Font()/FontSize() 的读写方法对。 [CODE] class CElement { protected : //--- 字体 string m_font; int m_font_size; //--- public : //--- (1) 字体和 (2) 字号 void Font( const string font) { m_font=font; } string Font( void ) const { return (m_font); } void FontSize( const int font_size) { m_font_size=font_size; } int FontSize( void ) const { return (m_font_size); } }; //+------------------------------------------------------------------+
| // | 构造器 |
|---|
//+------------------------------------------------------------------+ CElement::CElement( void ) : m_font( "Calibri" ) , m_font_size( 8 ) { } [/CODE] 上面这段可以逐行看:m_font 存字体名,m_font_size 存字号;Font(const string) 是写方法,把传入字体名赋给 m_font;Font(void) const 是读方法,返回当前字体;FontSize 同理。构造器里直接用初始化列表把默认值锁成 Calibri 和 8,意味着不显式改就不会脱离省缺。 派生类如 CCheckBox 在 CreateLabel 里直接调 CElement::Font() 和 FontSize() 把值透传给标签对象,函数库所有控件类都做了同样改造。想验证的话,开 MT5 新建一个带复选框的面板,在 CElement 构造后改 Font("Consolas")、FontSize(10),编译运行就能看到标签字体变了。 边距逻辑也改了:控件距表单边缘的空余不再内部隐式算,而是直接由创建方法传入 x_gap、y_gap。以 CProgram::CreateDropCalendar 为例,调用方传坐标间隙,方法内转交给 m_drop_calendar.CreateDropCalendar 处理,自动完成相对定位。做 GUI ea 时,这套改动能减少硬编码偏移,调参更直观。
class CElement { class="kw">protected : class=class="str">"cmt">//--- 字体 class="type">class="kw">string m_font; class="type">int m_font_size; class=class="str">"cmt">//--- class="kw">public : class=class="str">"cmt">//--- (class="num">1) 字体和 (class="num">2) 字号 class="type">void Font( class="kw">const class="type">class="kw">string font) { m_font=font; } class="type">class="kw">string Font( class="type">void ) class="kw">const { class="kw">return (m_font); } class="type">void FontSize( class="kw">const class="type">int font_size) { m_font_size=font_size; } class="type">int FontSize( class="type">void ) class="kw">const { class="kw">return (m_font_size); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CElement::CElement( class="type">void ) : m_font( "Calibri" ) , m_font_size( class="num">8 ) { } class="type">bool CCheckBox::CreateLabel( class="type">void ) { class=class="str">"cmt">//--- 形成对象名 class="type">class="kw">string name=CElement::ProgramName()+ "_checkbox_lable_" +( class="type">class="kw">string )CElement::Id(); class=class="str">"cmt">//--- 坐标 class="type">int x =(m_anchor_right_window_side)?m_x-m_label_x_gap : m_x+m_label_x_gap; class="type">int y =(m_anchor_bottom_window_side)?m_y-m_label_y_gap : m_y+m_label_y_gap; class=class="str">"cmt">//--- 依据状态的文本颜色 class="type">color label_color=(m_check_button_state) ?m_label_color : m_label_color_off; class=class="str">"cmt">//--- 设置对象 if (!m_label.Create(m_chart_id,name,m_subwin,x,y)) class="kw">return ( false ); class=class="str">"cmt">//--- 设置属性 m_label.Description(m_label_text); m_label.Font(CElement::Font()); m_label.FontSize(CElement::FontSize()); m_label.Color(label_color); m_label.Corner(m_corner); m_label.Anchor(m_anchor); m_label.Selectable( false ); m_label.Z_Order(m_zorder); m_label.Tooltip( "\n" ); class=class="str">"cmt">//--- 距边缘的空余 m_label.XGap((m_anchor_right_window_side)?x : x-m_wnd.X()); m_label.YGap((m_anchor_bottom_window_side)?y : y-m_wnd.Y()); class=class="str">"cmt">//--- 初始化梯度数组 CElement::InitColorArray(label_color,m_label_color_hover,m_label_color_array); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_label); class="kw">return ( true ); } class="type">bool CProgram::CreateDropCalendar( class="kw">const class="type">int x_gap , class="kw">const class="type">int y_gap , class="kw">const class="type">class="kw">string text) { class=class="str">"cmt">//--- 传递对象至面板 m_drop_calendar.WindowPointer(m_window); class=class="str">"cmt">//--- 附加第二个选项卡 m_tabs.AddToElementsArray( class="num">1 ,m_drop_calendar); class=class="str">"cmt">//--- 在创建之前设置属性 m_drop_calendar.XSize( class="num">140 ); m_drop_calendar.YSize( class="num">20 ); m_drop_calendar.AreaBackColor( clrWhite ); class=class="str">"cmt">//--- 创建控件 if (!m_drop_calendar.CreateDropCalendar(m_chart_id,m_subwin,text, x_gap , y_gap )) class="kw">return ( false ); class=class="str">"cmt">//--- 将控件指针加入基类 CWndContainer::AddToElementsArray( class="num">0 ,m_drop_calendar); class="kw">return ( true ); }
「搭一个能跑控件的 MQL5 测试壳」
想验证自己写的自定义控件到底稳不稳,最快的办法不是挂进实盘 EA,而是先做一个纯测试用的 MQL 程序。这个壳子只需要把控件铺出来、能交互就行,不涉及任何交易逻辑。 具体布局可以这样:顶部放一个 CMenuBar 主菜单,带下拉子菜单;底部接一个状态条;中间用两个选项卡切分。第一个选项卡里塞一个 CTable 表格,并打开排序模式——前三列分别指定为 TYPE_DATETIME、TYPE_DOUBLE、TYPE_LONG,其余列默认 TYPE_STRING。实测时按第二列(double 型)升序排,能看到行记录随数值重排,证明排序回调已生效。 第二个选项卡集中放四个独立控件:CDropCalendar 下拉日历、CTimeEdit 时间编辑框、CCheckBoxList 复选框列表、CListView 列表视图。把它们摆在同一画布上,能分别点开、输入、勾选,就说明各自消息响应没冲突。 这类测试程序的源码通常随文附在结尾,直接拷进 MT5 的 MQL5/Scripts 目录编译,即可在终端脚本里点开看界面。外汇与贵金属测试虽不下单,但 MT5 环境配置错误仍可能导致界面卡死,建议先用模拟终端跑。
◍ 画得少,看得清
这套 GUI 函数库到当前阶段已迭代出明确分层:社区用户 Miguel 在 2020 年 5 月按文章把构建版本归为 5 组,其中 Build7 至 Build13 属于「新结构旧库」过渡带,而 Build16 才真正走到新库新结构。你在 MT5 里直接拉 easyandfastgui_mql5.zip(536.54 KB)解压,能看见第 4 节提到的库结构示意图对应的源文件,但第 2 组里 Build5、Build6 与更早的 10-2 文存在无法互相编译的坑,别指望无缝替换。 真正落地时,先把界面控件精简到只留下单面板和状态标签,比堆满 Tab 更利于盯盘。外汇与贵金属波动剧烈、杠杆风险高,任何界面优化都只是降低操作摩擦,不改动策略概率。 作者留了个开放口子:动态调整窗口大小这类特性早有用户提,但控件还停在中间阶段。你若真在 MT5 里改出可缩放版本,直接发出来比等官方更实际。