图形界面 X: 高级列表和表格管理。代码优化 (集成构建 7)·综合运用
(3/3)·从滚动条到 CTable 的可编程管理,把界面函数库优化成易读易扩的实战体系
CTable 可编程控件的几个核心方法
在这次函数库更新里,只有 CTable 这一种表格类型拿到了可编程控制能力。对外暴露的公共方法覆盖了重构表格、增列、增行、清表,以及水平和垂直滚动,相当于把表格的骨架和视口都交给了代码去摆弄。 重建表格必须走 CTable::Rebuilding(),四个参数分别是列总数、可见列数、行总数、可见行数。方法一进来先 Clear() 清掉旧结构,再按参数重设数组大小和滚动条缩略比例;当 rows_total 大于 visible_rows_total 时垂直滚动条才会出现,columns_total 大于 visible_columns_total 时水平滚动条才会出现,这个阈值判断直接决定了视口里要不要留出滚动条宽度。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 重构表格 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTable::Rebuilding(const class="type">int columns_total, const class="type">int visible_columns_total, const class="type">int rows_total, const class="type">int visible_rows_total) { class=class="str">"cmt">//--- 清除表格 Clear(); class=class="str">"cmt">//--- 设置表格和其可见部分大小 TableSize(columns_total,rows_total); VisibleTableSize(visible_columns_total,visible_rows_total); class=class="str">"cmt">//--- 调整滚动条大小 m_scrollv.ChangeThumbSize(rows_total,visible_rows_total); m_scrollh.ChangeThumbSize(columns_total,visible_columns_total); class=class="str">"cmt">//---检查垂直滚动条是否存在 class="type">bool is_scrollv=m_rows_total>m_visible_rows_total; class=class="str">"cmt">//--- 检查水平滚动条是否存在 class="type">bool is_scrollh=m_columns_total>m_visible_columns_total; class=class="str">"cmt">//--- 计算沿 Y 轴的表格大小 class="type">int y_size=CalculationYSize(); class=class="str">"cmt">//--- 改变垂直滚动条大小 m_scrollv.ChangeYSize(y_size); class=class="str">"cmt">//--- 改变表格大小 m_y_size=(is_scrollh)?y_size+m_scrollh.ScrollWidth()-class="num">1 : y_size; m_area.YSize(m_y_size); m_area.Y_Size(m_y_size); class=class="str">"cmt">//--- 调整水平滚动条沿 Y 轴的位置 m_scrollh.YDistance(CElementBase::Y2()-m_scrollh.ScrollWidth()); class=class="str">"cmt">//--- 如果需要水平滚动条 if(is_scrollh) { class=class="str">"cmt">//--- 根据垂直滚动条的存在设置大小 if(!is_scrollv) m_scrollh.ChangeXSize(m_x_size); else { class=class="str">"cmt">//--- 计算和更改水平滚动条的宽度 class="type">int x_size=m_area.XSize()-m_scrollh.ScrollWidth()+class="num">1; m_scrollh.ChangeXSize(x_size); } } class=class="str">"cmt">//--- 创建表格单元 CreateCells(); class=class="str">"cmt">//--- 若有必要, 显示滚动条 if(rows_total>visible_rows_total) { if(CElementBase::IsVisible()) m_scrollv.Show(); } if(columns_total>visible_columns_total) { if(CElementBase::IsVisible()) m_scrollh.Show(); } }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在表格里添加一列 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTable::AddColumn(class="type">void) { class=class="str">"cmt">//--- 数组大小递增一个元素 class="type">uint array_size=ColumnsTotal(); m_columns_total=array_size+class="num">1; ::ArrayResize(m_vcolumns,m_columns_total); class=class="str">"cmt">//--- 设置行数组大小 RowResize(array_size,m_rows_total); class=class="str">"cmt">//--- 以省缺值初始化数组 CellInitialize(array_size); class=class="str">"cmt">//--- 如果列总数超过可见额度 if(m_columns_total>m_visible_columns_total) { class=class="str">"cmt">//--- 调整表格沿 Y 轴的大小 YResize(); class=class="str">"cmt">//--- 如果没有垂直滚动条, 令水平滚动条占据表格的全宽 if(m_rows_total<=m_visible_rows_total) m_scrollh.ChangeXSize(m_x_size); class=class="str">"cmt">//--- 调整滑块大小并显示滚动条 m_scrollh.ChangeThumbSize(m_columns_total,m_visible_columns_total); class=class="str">"cmt">//--- 显示滚动条 if(CElementBase::IsVisible()) m_scrollh.Show(); class=class="str">"cmt">//--- 将行格式化为斑马样式 ZebraFormatRows(); class=class="str">"cmt">//--- 刷新表格 UpdateTable(); class="kw">return; } class=class="str">"cmt">//--- 计算列宽 class="type">int width=CalculationColumnWidth(); class=class="str">"cmt">//--- 调整最后一列宽度 if(m_columns_total>=m_visible_columns_total) width=CalculationColumnWidth(true); class=class="str">"cmt">//--- 计算 X 坐标 class="type">int x=CalculationCellX(array_size); class=class="str">"cmt">//--- for(class="type">uint r=class="num">0; r<m_rows_total && r<m_visible_rows_total; r++) { class=class="str">"cmt">//--- 计算 Y 坐标 class="type">int y=CalculationCellY(r); class=class="str">"cmt">//--- 创建对象 CreateCell(array_size,r,x,y,width); class=class="str">"cmt">//--- 为表头设置相应的颜色 if(m_fix_first_row && r==class="num">0) m_columns[array_size].m_rows[r].BackColor(m_headers_color); } class=class="str">"cmt">//--- 将行格式化为斑马样式 ZebraFormatRows(); class=class="str">"cmt">//--- 刷新表格 UpdateTable(); }
class CTable : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 重构表格 class="type">void Rebuilding(const class="type">int columns_total, const class="type">int visible_columns_total, const class="type">int rows_total, const class="type">int visible_rows_total); class=class="str">"cmt">//--- 在表格里添加一列 class="type">void AddColumn(class="type">void); class=class="str">"cmt">//--- 在表格里添加一行 class="type">void AddRow(class="type">void); class=class="str">"cmt">//--- 清除表格 (删除所有行和列) class="type">void Clear(class="type">void); class=class="str">"cmt">//--- 表格滚动: (class="num">1) 垂直以及 (class="num">2) 水平 class="type">void VerticalScrolling(const class="type">int pos=WRONG_VALUE); class="type">void HorizontalScrolling(const class="type">int pos=WRONG_VALUE); };
「用定时器驱动表格与列表的渐进填充」
做一个验证用的 MQL 应用,图形界面开两个选项卡:第一个放 CTable 表格加六个控件(清除/重构按钮,行总数、列总数、可视行数、可视列数编辑框),第二个放列表视图和复选框列表,同样用清除/重构按钮加项目总数、可见项目数编辑框控制,附带日历和时间下拉控件。这样能在 MT5 里直接看到控件类方法怎么跑。 顺带提一个对界面开发很有用的 CTimeCounter 类,它只用三个字段(步长 m_step、间隔 m_pause、计数器 m_time_counter)和两个方法管重绘频率。构造时默认 m_step=16 毫秒、m_pause=1000 毫秒,SetParameters() 可改步长和间隔,CheckTimeCounter() 每被调用一次若未到间隔就累加步长返回 false,到了就清零返回 true。 库文件路径在这版变了:控件类在 Include\EasyAndFastGUI\Controls,其余移到 Include\EasyAndFastGUI 根目录。自定义文件里要写 #include <EasyAndFastGUI\WndEvents.mqh> 和 #include <EasyAndFastGUI\TimeCounter.mqh> 才能用。示例里建了两个计数器,m_counter1 设 16/500 管状态条,m_counter2 设 16/150 管列表表格刷新。 定时器事件里用 m_counter2.CheckTimeCounter() 卡节奏:每过 150 毫秒,若表格行数少于编辑框值就 AddRow(),列少就 AddColumn();列表视图和复选框列表项数不够就 AddItem("SYMBOL "+数量) 并 Scrolling() 把滑块推到末尾,最后 m_chart.Redraw()。外汇与贵金属 GUI 自动化测试涉及实盘逻辑,高风险,参数仅作界面验证用。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TimeCounter.mqh | class=class="str">"cmt">//| 版权所有 class="num">2016, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定时计数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTimeCounter { class="kw">private: class=class="str">"cmt">//--- 计数器步长 class="type">uint m_step; class=class="str">"cmt">//--- 定时间隔 class="type">uint m_pause; class=class="str">"cmt">//--- 定时计数器 class="type">uint m_time_counter; class=class="str">"cmt">//--- class="kw">public: CTimeCounter(class="type">void); ~CTimeCounter(class="type">void); class=class="str">"cmt">//--- 设置步长和定时间隔 class="type">void SetParameters(const class="type">uint step, const class="type">uint pause); class=class="str">"cmt">//--- 检查指定的时间间隔是否已过 class="type">bool CheckTimeCounter(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTimeCounter::CTimeCounter(class="type">void) : m_step(class="num">16), m_pause(class="num">1000), m_time_counter(class="num">0) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 析构器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTimeCounter::~CTimeCounter(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置步长和定时间隔 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTimeCounter::SetParameters(const class="type">uint step, const class="type">uint pause) { m_step =step; m_pause =pause; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 检查是否指定的时间间隔已过 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTimeCounter::CheckTimeCounter(class="type">void) { class=class="str">"cmt">//--- 如果指定时间间隔未过, 则增加计数器 if(m_time_counter<m_pause) { m_time_counter+=m_step; class="kw">return(false); } class=class="str">"cmt">//--- 计数器清零 m_time_counter=class="num">0; class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Program.mqh | class=class="str">"cmt">//| 版权所有 class="num">2016, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <EasyAndFastGUI\WndEvents.mqh> class="macro">#include <EasyAndFastGUI\TimeCounter.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建应用程序类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CProgram : class="kw">public CWndEvents { class="kw">protected: class=class="str">"cmt">//--- 定时计数器 CTimeCounter m_counter1; class=class="str">"cmt">// 用于更新状态条 CTimeCounter m_counter2; class=class="str">"cmt">// 用于更新列表和表格 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CProgram::CProgram(class="type">void) { class=class="str">"cmt">//--- 设置定时计数器参数 m_counter1.SetParameters(class="num">16,class="num">500); m_counter2.SetParameters(class="num">16,class="num">150); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnTimerEvent(class="type">void) { CWndEvents::OnTimerEvent(); ... class=class="str">"cmt">//--- 在控件刷新之间的暂停 if(m_counter2.CheckTimeCounter()) { class=class="str">"cmt">//--- 如果总数小于指定, 在表格里添加一行 if(m_table.RowsTotal()<m_spin_edit1.GetValue()) m_table.AddRow(); class=class="str">"cmt">//--- 如果总数小于指定, 在表格里添加一列 if(m_table.ColumnsTotal()<m_spin_edit2.GetValue()) m_table.AddColumn(); class=class="str">"cmt">//--- 如果总数小于指定, 在列表视图里添加一项 if(m_listview.ItemsTotal()<m_spin_edit5.GetValue()) { m_listview.AddItem("SYMBOL " + class="type">class="kw">string(m_listview.ItemsTotal())); class=class="str">"cmt">//--- 将滚动条滑块移到列表末尾 m_listview.Scrolling(); } class=class="str">"cmt">//--- 如果总数小于指定, 在复选框列表里添加一项 if(m_checkbox_list.ItemsTotal()<m_spin_edit5.GetValue()) { m_checkbox_list.AddItem("Checkbox " + class="type">class="kw">string(m_checkbox_list.ItemsTotal())); class=class="str">"cmt">//--- 将滚动条滑块移到列表末尾 m_checkbox_list.Scrolling(); } class=class="str">"cmt">//--- 图表重绘 m_chart.Redraw(); } }
◍ 用图表事件驱动表格与列表重建
在 MT5 自定义指标或 EA 里,想让界面控件随用户点击实时变化,核心就是重写 OnEvent 并拦截 CHARTEVENT_CUSTOM 偏移量。上面这段处理器把四个按钮的 lparam 与控件 Id() 比对,分别走清空或重建逻辑,读者可直接拷进自己的 CProgram 类里改条件验证。 第二个按钮触发时,表格会按四个 SpinEdit 的当前值重构:m_table.Rebuilding 接收行、列及两个偏移参数,随后 InitializeTable 与 UpdateTable 必须顺序调用,否则界面不会刷新。漏掉 UpdateTable 是新手最常卡住的点,图表上看起来像没反应。 第四个按钮则同时重建 CheckboxList 和 ListView,并硬性选中第 8 项(索引 7)。循环里用 i%2!=0 决定复选框勾选状态,也就是奇数行勾上、偶数行空着——这种规律填充在批量生成品种监视列表时很实用,贵金属与外汇符号都可套用。 外汇与贵金属界面工具开发属高风险环境,参数误写可能导致图表卡顿或客户端无响应,建议在策略测试器外的独立图表先跑通再上实盘。
class="type">void CProgram::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 按钮按下事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { Print(__FUNCTION__," > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam); class=class="str">"cmt">//--- 第一个按钮事件 if(lparam==m_simple_button1.Id()) { class=class="str">"cmt">//--- 清除表格 m_table.Clear(); class="kw">return; } class=class="str">"cmt">//--- 第二个按钮事件 if(lparam==m_simple_button2.Id()) { class=class="str">"cmt">//--- 重构表格 m_table.Rebuilding((class="type">int)m_spin_edit3.GetValue(),(class="type">int)m_spin_edit4.GetValue(), (class="type">int)m_spin_edit1.GetValue(),(class="type">int)m_spin_edit2.GetValue()); class=class="str">"cmt">//--- 表格初始化 InitializeTable(); class=class="str">"cmt">//--- 刷新表格以便显示变化 m_table.UpdateTable(); class="kw">return; } class=class="str">"cmt">//--- 第三个按钮事件 if(lparam==m_simple_button3.Id()) { class=class="str">"cmt">//--- 清除列表 m_listview.Clear(); m_checkbox_list.Clear(); class="kw">return; } class=class="str">"cmt">//--- 第二个按钮事件 if(lparam==m_simple_button4.Id()) { class=class="str">"cmt">//--- 重构列表 m_checkbox_list.Rebuilding((class="type">int)m_spin_edit5.GetValue(),(class="type">int)m_spin_edit6.GetValue()); m_listview.Rebuilding((class="type">int)m_spin_edit5.GetValue(),(class="type">int)m_spin_edit6.GetValue()); class=class="str">"cmt">//--- 在列表视图中选择第八个项目 m_listview.SelectItem(class="num">7); class=class="str">"cmt">//--- 用数据填充列表视图 class="type">int items_total=m_listview.ItemsTotal(); for(class="type">int i=class="num">0;i<items_total;i++) m_listview.SetItemValue(i,"SYMBOL "+class="type">class="kw">string(i)); class=class="str">"cmt">//--- 用数据填充复选框列表, 每隔一秒勾选复选框 items_total=m_checkbox_list.ItemsTotal(); for(class="type">int i=class="num">0;i<items_total;i++) { m_checkbox_list.SetItemValue(i,"Checkbox "+class="type">class="kw">string(i)); m_checkbox_list.SetItemState(i,(i%class="num">2!=class="num">0)?true:false); } class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//--- class="kw">return; } }
把工具请下神坛
CTable 类在最近一次更新里补齐了列与行的动态管理:DeleteColumn() 和 DeleteRow() 重新回到接口,配合 IsSortedColumnIndex() 可以确认当前排序列,避免手动维护顺序时踩坑。下面这段代码截取自更新后的类声明,逐行看就是控件该有的样子。 //--- (1) 当前排序方向,(2) 已排序数组索引 int IsSortDirection(void) const { return(m_last_sort_direction); } // 返回上一次排序方向(升/降),外部据此判断表状态 int IsSortedColumnIndex(void) const { return(m_is_sorted_column_index); } // 返回正在排序的列索引,未排序时返回-1一类初始值 //--- 表重建 void Rebuilding(const int columns_total,const int rows_total,const bool redraw=false); // 按新行列数重建表,调用后需重设标题与列宽 //--- 在指定索引处向表添加一列 void AddColumn(const int column_index,const bool redraw=false); // 在 column_index 位置插一列,redraw 控制是否立即重绘 //--- 删除表格中指定索引处的一列 void DeleteColumn(const int column_index,const bool redraw=false); // 删列,之前讨论里有人误以为此方法被移除,实际是找错工程目录 //--- 在指定索引处向表添加一行 void AddRow(const int row_index,const bool redraw=false); // 插一行,用于动态日志或报价面板 //--- 按指定索引删除表中的一行 void DeleteRow(const int row_index,const bool redraw=false); // 删行,和 DeleteColumn 对称 //--- 删除所有行 void DeleteAllRows(const bool redraw=false); // 清数据留骨架 //--- 清除表格。只保留一列和一行。 void Clear(const bool redraw=false); // 重置到最小表结构 实盘里把 EasyAndFast 库拖进 EA 做面板,本质只是省掉自己写 CCanvas 的时间。外汇和贵金属波动受杠杆与消息牵引,高风险始终在,界面再顺手也不能替你过滤爆仓概率;下载 zip(约 540 KB)后在 MetaEditor 里编译跑一遍,比看任何说明都来得直接。
class=class="str">"cmt">//--- (class="num">1) 当前排序方向,(class="num">2) 已排序数组索引 class="type">int IsSortDirection(class="type">void) const { class="kw">return(m_last_sort_direction); } class="type">int IsSortedColumnIndex(class="type">void) const { class="kw">return(m_is_sorted_column_index); } class=class="str">"cmt">//--- 表重建 class="type">void Rebuilding(const class="type">int columns_total,const class="type">int rows_total,const class="type">bool redraw=false); class=class="str">"cmt">//--- 在指定索引处向表添加一列 class="type">void AddColumn(const class="type">int column_index,const class="type">bool redraw=false); class=class="str">"cmt">//--- 删除表格中指定索引处的一列 class="type">void DeleteColumn(const class="type">int column_index,const class="type">bool redraw=false); class=class="str">"cmt">//--- 在指定索引处向表添加一行 class="type">void AddRow(const class="type">int row_index,const class="type">bool redraw=false); class=class="str">"cmt">//--- 按指定索引删除表中的一行 class="type">void DeleteRow(const class="type">int row_index,const class="type">bool redraw=false); class=class="str">"cmt">//--- 删除所有行 class="type">void DeleteAllRows(const class="type">bool redraw=false); class=class="str">"cmt">//--- 清除表格。只保留一列和一行。 class="type">void Clear(const class="type">bool redraw=false);