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

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

第 3/3 篇

◍ 列头点击触发排序与箭头跟随

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 验证箭头行为。

MQL5 / C++
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 时,这套改动能减少硬编码偏移,调参更直观。

MQL5 / C++
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 里改出可缩放版本,直接发出来比等官方更实际。

常见问题

给列头绑定点击事件,在回调里切换排序字段与升/降序状态,并重绘表头箭头;箭头用文本符号或自绘三角跟随当前排序列即可。
控件应暴露独立的字体句柄与内边距参数,不要复用全局样式;分别设置 FontSize、Padding 后再重绘,边距就能脱离系统默认。
可以,把控件代码丢给小布,它能按你的品种页生成可交互预览并标出点击列头后的排序异常,省去手动挂测试壳。
一个带 OnChartEvent 转发的图表脚本、控件基类的 Create/OnEvent/Redraw 三方法,以及定时重绘钩子;有这三样就能在图表上跑起来。
只渲染当前视口行、隐藏次要列、用颜色区分状态而非堆文字;控件层做好虚拟滚动,视觉负担会降一大截。