图形界面 X: 文本编辑框, 图片滑块和简单控件 (构建 5)·综合运用
◍ 列表控件悬停高亮与双态图标按钮的实现细节
MT5 自定义控件里,列表视图(CListView)的悬停高亮不是靠系统事件直接画,而是在 ChangeItemsColor() 里先判断 m_lights_hover 开关和滚动条状态,再决定要不要进 CheckItemFocus() 做逐行坐标命中。 CheckItemFocus() 用滚动条当前位置 v 做偏移计数,循环可见项时用 m_mouse.X()/Y() 落在 m_items[i] 的 X~X2、Y~Y2 矩形内来认定光标所在行,命中即改背景与文字色并 break,逻辑简单但依赖鼠标对象实时坐标。 OnEvent() 中只在 CHARTEVENT_MOUSE_MOVE 且 MouseFocus() 为真时调 ChangeItemsColor();一旦光标离开控件区域,走 ResetColors() 并把 m_prev_item_index_focus 置为 WRONG_VALUE,避免残留高亮。相同框架也复用在 CTable、CCalendar、CTreeView,只是各控件边界判断略有差异。 双态图标按钮 CIconButton 靠 IconFilePressedOn() / IconFilePressedOff() 设置按下态图片,两方法开头都强制检查 m_two_state,非双态直接 return,说明单态按钮调了也没效果。 CTable 后来补了 SelectRow() 可编程选行,指定索引即选中并取消其他选择,适合做外部信号触发表格定位,外汇或贵金属面板高频刷新时需注意重绘开销偏高。
class="type">void CListView::CheckItemFocus(class="type">void) { class="type">int v=m_scrollv.CurrentPos(); class=class="str">"cmt">// 取垂直滚动条滑块当前位置 for(class="type">int i=class="num">0;i<m_visible_items_total;i++) class=class="str">"cmt">// 遍历所有可见项 { if(v>=class="num">0 && v<m_items_total) class=class="str">"cmt">// 若滚动位置在列表范围内 v++; class=class="str">"cmt">// 计数器随可见项下移 if(m_selected_item_index==v-class="num">1) class=class="str">"cmt">// 若是已选项目 { m_items[i].BackColor(m_item_color_selected); class=class="str">"cmt">// 已选项背景色 m_items[i].Color(m_item_text_color_selected); class=class="str">"cmt">// 已选项文字色 class="kw">continue; class=class="str">"cmt">// 跳过后续高亮判断 } if(m_mouse.X()>m_items[i].X() && m_mouse.X()<m_items[i].X2() && class=class="str">"cmt">// 鼠标X在项宽内 m_mouse.Y()>m_items[i].Y() && m_mouse.Y()<m_items[i].Y2()) class=class="str">"cmt">// 鼠标Y在项高内 { m_items[i].BackColor(m_item_color_hover); class=class="str">"cmt">// 悬停背景色 m_items[i].Color(m_item_text_color_hover); class=class="str">"cmt">// 悬停文字色 m_prev_item_index_focus=i; class=class="str">"cmt">// 记录悬停项索引 class="kw">break; class=class="str">"cmt">// 命中即退出循环 } } } class="type">void CListView::ChangeItemsColor(class="type">void) { if(!m_lights_hover || m_scrollv.ScrollState()) class=class="str">"cmt">// 悬停禁用或滚动中 class="kw">return; class=class="str">"cmt">// 直接离开 if(!CElement::IsDropdown() && m_wnd.IsLocked()) class=class="str">"cmt">// 非下拉且窗口锁住 class="kw">return; class=class="str">"cmt">// 直接离开 if(m_prev_item_index_focus==WRONG_VALUE) class=class="str">"cmt">// 之前无悬停项 { CheckItemFocus(); class=class="str">"cmt">// 检查当前焦点 } else { class="type">int i=m_prev_item_index_focus; class=class="str">"cmt">// 取前次悬停项 class="type">bool condition=m_mouse.X()>m_items[i].X() && m_mouse.X()<m_items[i].X2() && m_mouse.Y()>m_items[i].Y() && m_mouse.Y()<m_items[i].Y2(); class=class="str">"cmt">// 是否仍在该项内 if(!condition) class=class="str">"cmt">// 已移到别处 { m_items[i].BackColor(m_item_color); class=class="str">"cmt">// 复原背景 m_items[i].Color(m_item_text_color); class=class="str">"cmt">// 复原文字 m_prev_item_index_focus=WRONG_VALUE; class=class="str">"cmt">// 清空记录 CheckItemFocus(); class=class="str">"cmt">// 重查焦点 } } } class="type">void CListView::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { if(id==CHARTEVENT_MOUSE_MOVE) class=class="str">"cmt">// 鼠标移动事件 { if(!CElement::MouseFocus()) class=class="str">"cmt">// 光标不在控件区域 { if(m_prev_item_index_focus!=WRONG_VALUE) class=class="str">"cmt">// 原悬停项还在 { ResetColors(); class=class="str">"cmt">// 重置列表颜色 m_prev_item_index_focus=WRONG_VALUE; class=class="str">"cmt">// 清索引 } class="kw">return; class=class="str">"cmt">// 离开处理 } ChangeItemsColor(); class=class="str">"cmt">// 在区域内才改色 class="kw">return; } } class CIconButton : class="kw">public CElement { class="kw">private: class="type">class="kw">string m_icon_file_on; class="type">class="kw">string m_icon_file_off; class="type">class="kw">string m_icon_file_pressed_on; class="type">class="kw">string m_icon_file_pressed_off; class="kw">public: class="type">void IconFileOn(class="kw">const class="type">class="kw">string file_path); class="type">void IconFileOff(class="kw">const class="type">class="kw">string file_path); class="type">void IconFilePressedOn(class="kw">const class="type">class="kw">string file_path); class="type">void IconFilePressedOff(class="kw">const class="type">class="kw">string file_path); }; class="type">void CIconButton::IconFilePressedOn(class="kw">const class="type">class="kw">string file_path) { if(!m_two_state) class=class="str">"cmt">// 非双态模式 class="kw">return; class=class="str">"cmt">// 离开 m_icon_file_pressed_on=file_path; class=class="str">"cmt">// 存路径 if(m_button.State()) class=class="str">"cmt">// 按钮当前按下 m_icon.BmpFileOn("::"+file_path); class=class="str">"cmt">// 立即换图 } class="type">void CIconButton::IconFilePressedOff(class="kw">const class="type">class="kw">string file_path) { if(!m_two_state) class=class="str">"cmt">// 非双态模式 class="kw">return; class=class="str">"cmt">// 离开 m_icon_file_pressed_off=file_path; class=class="str">"cmt">// 存路径 if(m_button.State()) class=class="str">"cmt">// 按钮当前按下 m_icon.BmpFileOff("::"+file_path); class=class="str">"cmt">// 立即换图 }
「表格行选中与选项卡渲染的两个坑」
MQL5 自定义面板里,CTable 的 SelectRow 方法负责行选中切换。核心逻辑是先做越界保护:传入行号大于等于总行数时,强制落到最后一行(m_rows_total-1),避免数组越界崩面板。 随后用 is_selected 判断当前行是否已被选中,若是则把 m_selected_item 置为 WRONG_VALUE 实现取消选中,否则写入新索引并同步 m_selected_item_text 取第一列对应行文本。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建编辑框表单的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTable : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 选择指定的表单行 class="type">void SelectRow(class="kw">const class="type">uint row_index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 选择指定的表单行 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTable::SelectRow(class="kw">const class="type">uint row_index) { class=class="str">"cmt">//--- 在超出范围的情况下进行调整 class="type">uint index=(row_index>=(class="type">uint)m_rows_total)?m_rows_total-class="num">1:row_index; class=class="str">"cmt">//--- 如果已经选择此行, 则取消选择 class="type">bool is_selected=(index==m_selected_item); class=class="str">"cmt">//--- 保存行索引 m_selected_item=(is_selected)?WRONG_VALUE:(class="type">int)index; class=class="str">"cmt">//--- 保存单元行 m_selected_item_text=(is_selected)?"":m_vcolumns[class="num">0].m_vrows[index]; class=class="str">"cmt">//--- 生成单元参数的字符串 class="type">class="kw">string cell_params=class="type">class="kw">string(class="num">0)+"_"+class="type">class="kw">string(index)+"_"+m_vcolumns[class="num">0].m_vrows[index]; class=class="str">"cmt">//--- 重置焦点 m_prev_item_index_focus=WRONG_VALUE; class=class="str">"cmt">//--- 更新表格 UpdateTable(); class=class="str">"cmt">//--- 高亮已选行 HighlightSelectedItem(); }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建编辑框表单的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTable : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 选择指定的表单行 class="type">void SelectRow(class="kw">const class="type">uint row_index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 选择指定的表单行 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTable::SelectRow(class="kw">const class="type">uint row_index) { class=class="str">"cmt">//--- 在超出范围的情况下进行调整 class="type">uint index=(row_index>=(class="type">uint)m_rows_total)?m_rows_total-class="num">1:row_index; class=class="str">"cmt">//--- 如果已经选择此行, 则取消选择 class="type">bool is_selected=(index==m_selected_item); class=class="str">"cmt">//--- 保存行索引 m_selected_item=(is_selected)?WRONG_VALUE:(class="type">int)index; class=class="str">"cmt">//--- 保存单元行 m_selected_item_text=(is_selected)?"":m_vcolumns[class="num">0].m_vrows[index]; class=class="str">"cmt">//--- 生成单元参数的字符串 class="type">class="kw">string cell_params=class="type">class="kw">string(class="num">0)+"_"+class="type">class="kw">string(index)+"_"+m_vcolumns[class="num">0].m_vrows[index]; class=class="str">"cmt">//--- 重置焦点 m_prev_item_index_focus=WRONG_VALUE; class=class="str">"cmt">//--- 更新表格 UpdateTable(); class=class="str">"cmt">//--- 高亮已选行 HighlightSelectedItem(); }
用测试程序亲手跑一遍新控件
想验证这一批图形控件到底好不好用,最快的办法不是读文档,而是直接开一个 MT5 测试程序把 CTabs 里的四个选卡挨个戳一遍。第一个选卡塞了进度条、文本编辑框、带下拉的复合框、轮转数值框、调色按钮和文本标签,除了标签以外全挂了图标;进度条和编辑框的描述会按固定间隔自己变,用来演示动态刷新。 复合框里列的是 CFonts 类的字体名,选一个字体,文本标签立刻跟着变;轮转框改数字,标签字号同步变;点调色按钮拉起选择器,颜色也实时反映到标签上。下面这段事件处理就是绑这三件事的,注意 ON_CLICK_COMBOBOX_ITEM、ON_CLICK_INC/DEC 和 ON_CHANGE_COLOR 三个自定义事件分开拦截,lparam 拿控件 ID 做匹配: 第二个选卡只放了一个 CPicturesSlider,默认塞三张同尺寸图就能滑,想用代码切图就调 SelectPicture()。第三个选卡的 CTable 用 SelectRow() 编程选行。第四个选卡是日历、下拉日历和双态图标按钮,按压和释放显示不同图。外汇和贵金属图表上挂这类界面属于高频交互,误触或重绘卡顿都可能引发行情误判,实盘前先在模拟环境跑熟。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 图表事件处理器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 从复选框列表中进行选择的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM) { class=class="str">"cmt">//--- 如果控件标识符匹配 if(lparam==m_combobox1.Id()) { class=class="str">"cmt">//--- 改变字体 m_text_label1.LabelFont(m_combobox1.ButtonText()); } class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//--- 点击轮转编辑框按钮的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_INC || id==CHARTEVENT_CUSTOM+ON_CLICK_DEC) { class=class="str">"cmt">//--- 如果控件标识符匹配 if(lparam==m_spin_edit1.Id()) { class=class="str">"cmt">//--- 改变字号 m_text_label1.LabelFontSize(class="type">int(m_spin_edit1.GetValue())); } class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//--- 使用择色器变更颜色的事件 if(id==CHARTEVENT_CUSTOM+ON_END_EDIT) { class=class="str">"cmt">//--- 如果控件标识符匹配 if(lparam==m_spin_edit1.Id()) { class=class="str">"cmt">//--- 改变字号 m_text_label1.LabelFontSize(class="type">int(m_spin_edit1.GetValue())); } class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//--- 使用择色器变更颜色的事件 if(id==CHARTEVENT_CUSTOM+ON_CHANGE_COLOR) { class=class="str">"cmt">//--- 如果控件标识符匹配 if(lparam==m_color_picker.Id()) { class=class="str">"cmt">//--- 如果从第一个按钮得到响应 if(sparam==m_color_button1.LabelText()) { class=class="str">"cmt">//--- 改变对象颜色 m_text_label1.LabelColor(m_color_button1.CurrentColor()); class="kw">return; } } class="kw">return; } class=class="str">"cmt">//--- 按钮按下的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { class=class="str">"cmt">//--- 如果调用颜色选择器的第一个按钮按下 if(sparam==m_color_button1.LabelText()) { class=class="str">"cmt">//--- 传递按钮指针, 使用颜色选择器自动打开窗口 m_color_picker.ColorButtonPointer(m_color_button1); class="kw">return; } class=class="str">"cmt">//--- class="kw">return; } }
◍ 把工具请下神坛
这套 GUI 函数库眼下已能覆盖基础控件,压缩包 easyandfastgui_mql5.zip 体积约 509.73 KB,直接丢进 MT5 的 MQL5/Include 就能调。结构图里能看到它把面板、标签、输入框拆成了独立类,下一步作者计划把全部元素改为 OBJ_BITMAP_LABEL 绘制,绕开终端图形对象 63 字符的硬限制。 有读者在评论区实测过:CTextLabel 超出 63 字符就不显示,换成位图标签才解决,这说明现版库仍受平台对象约束。外汇和贵金属图表上加这类自定义面板,能少开几层属性框,但策略失灵、点错按钮都是高频风险,别把它当信号源供着。 真要上手,先下 ZIP 跑通例程里的 CTextEdit,再自己改 LabelText 试长度截断,比看十篇说明都快。工具就是工具,验证完就关掉它去盯盘。