图形界面 X: 文本编辑框, 图片滑块和简单控件 (构建 5)·综合运用
📘

图形界面 X: 文本编辑框, 图片滑块和简单控件 (构建 5)·综合运用

第 3/3 篇

◍ 列表控件悬停高亮与双态图标按钮的实现细节

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() 可编程选行,指定索引即选中并取消其他选择,适合做外部信号触发表格定位,外汇或贵金属面板高频刷新时需注意重绘开销偏高。

MQL5 / C++
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 取第一列对应行文本。

MQL5 / C++
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();
  }
逐行拆解:index 行做边界收敛;is_selected 标记原状态;m_selected_item 写索引或清空;m_selected_item_text 同步文本或置空;cell_params 拼出用于后续事件定位的「列_行_内容」字符串;m_prev_item_index_focus 清焦点防残影;UpdateTable 重绘;HighlightSelectedItem 高亮。开 MT5 把这段塞进你的 CTable 派生类,点行就能看到选中态互斥切换。 另一处修的是 CIconTabs 控件:当用图标选项卡打开表单并最大化窗口时,选定选项卡的显示元素会错位或丢失。该问题在普通尺寸下不触发,仅在最大化时显现,修复后选项卡高亮元素能正确跟随窗口尺寸重算坐标。外汇与贵金属面板开发属高风险定制,未充分回归测试勿直接上实盘账户。

MQL5 / C++
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() 编程选行。第四个选卡是日历、下拉日历和双态图标按钮,按压和释放显示不同图。外汇和贵金属图表上挂这类界面属于高频交互,误触或重绘卡顿都可能引发行情误判,实盘前先在模拟环境跑熟。

MQL5 / C++
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 试长度截断,比看十篇说明都快。工具就是工具,验证完就关掉它去盯盘。

常见问题

在控件 OnMouseMove 里记录光标所在行索引,重绘时给该行换底色即可,注意移出区域要清状态避免残留。
常见坑是没在切选项卡时重置选中行缓存,导致旧行号映射到新表;切页先清零选中索引再重绘。
可以,把控件代码或截图发给小布,它能对照常见坑点指出悬停高亮与选中状态管理的遗漏。
用同一图标资源按状态裁切或换图,别每次重建对象;状态变量驱动重绘,避免重复释放设备资源。
直接用文章里的测试程序挂载新控件,逐个触发输入、拖动、悬停,看控制台有无越界或空指针报错。