图形界面 IX: 颜色选择器控件 (第一章)·进阶篇
🎨

图形界面 IX: 颜色选择器控件 (第一章)·进阶篇

(2/3)· 调色盘控件不止是选色块,搞清 HSL/RGB/Lab 模型与复合结构才能写好 MQL5 界面

案例拆解 第 2/3 篇

多数交易者在 MQL5 里写自定义面板时,直接拖一个颜色控件了事,却没意识到颜色选择器是背景、标记、单选组、按钮叠加的复合元件。一旦事件流向没理顺,选色结果回写失败,界面就成了摆设。

「取色器的多色彩空间底层结构」

在 MT5 自建取色面板时,先把颜色拆成多套坐标并存下来是最省事的做法。下面这段类成员把 RGB、Lab、XYZ 三组双精度分量并列声明,相当于给后续调色板绘制预留了统一缓冲区。

MQL5 / C++
class="type">class="kw">double m_rgb_g;
class="type">class="kw">double m_rgb_b;
class=class="str">"cmt">//--- Lab
class="type">class="kw">double m_lab_l;
class="type">class="kw">double m_lab_a;
class="type">class="kw">double m_lab_b;
class=class="str">"cmt">//--- XYZ
class="type">class="kw">double m_xyz_x;
class="type">class="kw">double m_xyz_y;
class="type">class="kw">double m_xyz_z;
逐行看:m_rgb_g / m_rgb_b 与未列出的 m_rgb_r 共同保存红绿蓝;m_lab_l / a / b 对应 CIE-Lab 的明度与两个色度轴;m_xyz_x / y / z 则是设备无关的中间色彩空间,方便做跨模型换算。 私有方法按模型分了三路:DrawHSL 吃索引 0 画色相、DrawRGB 吃 3~5 画三通道、DrawLab 吃 6~8 画 Lab 平面。索引值直接决定当前面板刷哪一维,写 EA 面板时照这个映射就不会串色。 实际跑 DrawHSL 的 case 0,H 被 m_hsl_h_edit.GetValue()/360.0 归一化到 0~1;两层 for 循环以 m_canvas.YSize() 和 XSize() 为边界,把 L 沿纵轴、S 沿横轴铺满。每行像素先用 HSLtoRGB 转回屏幕色,再 PixelSet(lx, YSize()-ly, rgb_color) 写进画布——注意 Y 轴翻转,不然调色板上下是反的。

MQL5 / C++
class="type">class="kw">double m_rgb_g;
class="type">class="kw">double m_rgb_b;
class=class="str">"cmt">//--- Lab
class="type">class="kw">double m_lab_l;
class="type">class="kw">double m_lab_a;
class="type">class="kw">double m_lab_b;
class=class="str">"cmt">//--- XYZ
class="type">class="kw">double m_xyz_x;
class="type">class="kw">double m_xyz_y;
class="type">class="kw">double m_xyz_z;
class="kw">private:
class=class="str">"cmt">//--- 根据HSL颜色模型(class="num">0: H, class="num">1: S, class="num">2: L)绘制调色板
class="type">void DrawHSL(class="kw">const class="type">int index);
class=class="str">"cmt">//--- 根据 RGB 颜色模型(class="num">3: R, class="num">4: G, class="num">5: B)绘制调色板
class="type">void DrawRGB(class="kw">const class="type">int index);
class=class="str">"cmt">//--- 根据 LAB 颜色模型(class="num">6: L, class="num">7: a, class="num">8: b)绘制调色板
class="type">void DrawLab(class="kw">const class="type">int index);
};
class="type">void CColorPicker::DrawHSL(class="kw">const class="type">int index)
  {
  class="kw">switch(index)
    {
    class=class="str">"cmt">//--- 色相 (H) - 颜色值范围从 class="num">0 到 class="num">360
    case class="num">0 :
      {
      m_hsl_h=m_hsl_h_edit.GetValue()/class="num">360.0;
      for(class="type">int ly=class="num">0; ly<m_canvas.YSize(); ly++)
        {
        m_hsl_l=ly/(class="type">class="kw">double)m_canvas.YSize();
        for(class="type">int lx=class="num">0; lx<m_canvas.XSize(); lx++)
          {
          m_hsl_s=lx/(class="type">class="kw">double)m_canvas.XSize();
          m_clr.HSLtoRGB(m_hsl_h,m_hsl_s,m_hsl_l,m_rgb_r,m_rgb_g,m_rgb_b);
          class="type">uint rgb_color=XRGB(m_rgb_r,m_rgb_g,m_rgb_b);
          m_canvas.PixelSet(lx,m_canvas.YSize()-ly,rgb_color);
          }

固定饱和度时如何铺满 HSL 画布

当交易者把 HSL 调色面板切到饱和度(S)锁定模式,代码走 case 1 分支:先把编辑框里的 S 值除以 100.0 归一化到 0~1,因为 MQL5 的 HSL 转换函数只认这个区间。 随后用双层循环扫描画布像素——纵向 ly 从 0 到 YSize() 映射为亮度 L,横向 lx 从 0 到 XSize() 映射为色相 H,每个像素调用 HSLtoRGB 算 RGB 后再用 PixelSet 写回,注意 Y 轴做了 m_canvas.YSize()-ly 翻转,否则颜色梯度在 MT5 画布上是倒的。 case 2 是锁定亮度(L)的镜像写法:L 来自编辑框除以 100.0,纵向 ly 改为映射饱和度 S,横轴依旧是 H。两个分支都不动第三个分量,靠遍历另外两个轴生成 2D 色域图,开 MT5 把画布尺寸设为 256x256 就能直观看到渐变带。 外汇与贵金属图表叠加这类自定义色域工具属于高风险环境下的辅助视图,颜色映射本身不预测价格,仅帮助人工辨色。

MQL5 / C++
      }
      class="kw">break;
      }
      class=class="str">"cmt">//--- 饱和度 (S) - 饱和度范围从 class="num">0 到 class="num">100
      case class="num">1 :
      {
      class=class="str">"cmt">//--- 计算 S 组分
      m_hsl_s=m_hsl_s_edit.GetValue()/class="num">100.0;
      class=class="str">"cmt">//---
      for(class="type">int ly=class="num">0; ly<m_canvas.YSize(); ly++)
         {
         class=class="str">"cmt">//--- 计算 L 组分
         m_hsl_l=ly/(class="type">class="kw">double)m_canvas.YSize();
         class=class="str">"cmt">//---
         for(class="type">int lx=class="num">0; lx<m_canvas.XSize(); lx++)
            {
            class=class="str">"cmt">//--- 计算 H 组成部分
            m_hsl_h=lx/(class="type">class="kw">double)m_canvas.XSize();
            class=class="str">"cmt">//--- 把 HSL 组分转换为 RGB
            m_clr.HSLtoRGB(m_hsl_h,m_hsl_s,m_hsl_l,m_rgb_r,m_rgb_g,m_rgb_b);
            class=class="str">"cmt">//--- 连接通道
            class="type">uint rgb_color=XRGB(m_rgb_r,m_rgb_g,m_rgb_b);
            m_canvas.PixelSet(lx,m_canvas.YSize()-ly,rgb_color);
            }
         }
      class="kw">break;
      }
      class=class="str">"cmt">//--- 亮度 (L) - 亮度范围从 class="num">0 到 class="num">100
      case class="num">2 :
      {
      class=class="str">"cmt">//--- 计算 L 组分
      m_hsl_l=m_hsl_l_edit.GetValue()/class="num">100.0;
      class=class="str">"cmt">//---
      for(class="type">int ly=class="num">0; ly<m_canvas.YSize(); ly++)
         {
         class=class="str">"cmt">//--- 计算 S 组分
         m_hsl_s=ly/(class="type">class="kw">double)m_canvas.YSize();
         class=class="str">"cmt">//---
         for(class="type">int lx=class="num">0; lx<m_canvas.XSize(); lx++)
            {
            class=class="str">"cmt">//--- 计算 H 组成部分
            m_hsl_h=lx/(class="type">class="kw">double)m_canvas.XSize();
            class=class="str">"cmt">//--- 把 HSL 组分转换为 RGB
            m_clr.HSLtoRGB(m_hsl_h,m_hsl_s,m_hsl_l,m_rgb_r,m_rgb_g,m_rgb_b);
            class=class="str">"cmt">//--- 连接通道
            class="type">uint rgb_color=XRGB(m_rgb_r,m_rgb_g,m_rgb_b);
            m_canvas.PixelSet(lx,m_canvas.YSize()-ly,rgb_color);

◍ 调色板边框与多色彩空间的绘制分流

在自定义颜色选择器里,调色板画完之后必须补一圈边框,否则在 MT5 画布上肉眼很难界定取色区域。边框用画布四条边的线段拼出来,坐标取画布尺寸减 1,避免最右和最下像素被裁掉。 DrawPaletteBorder 里先取 x_size = m_canvas.XSize()-1、y_size = m_canvas.YSize()-1,再分别用 m_canvas.Line 画上、下、左、右四条线,颜色统一走 m_palette_border_color。这一段没有循环,纯四条直线,开销可忽略。 DrawPalette 按 index 分流到不同色彩空间:0/1/2 走 HSL,3/4/5 走 RGB,6/7/8 走 LAB,分别对应 DrawHSL、DrawRGB、DrawLab。画完对应空间后统一调 DrawPaletteBorder 并 m_canvas.Update 刷新。 你在 MT5 里改颜色控件时,若发现边框不跟着刷新,优先查 DrawPalette 末尾有没有漏掉 Update 调用——这是最常见的显示滞后点。外汇与贵金属图表叠加此类自定义控件时,注意画布重绘会占用主图资源,高频 tick 下可能拖慢响应。

MQL5 / C++
class CColorPicker : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 绘制调色板的边框
  class="type">void                DrawPaletteBorder(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 绘制调色板的边框                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorPicker::DrawPaletteBorder(class="type">void)
  {
class=class="str">"cmt">//--- 调色板的大小
  class="type">int x_size=m_canvas.XSize()-class="num">1;
  class="type">int y_size=m_canvas.YSize()-class="num">1;
class=class="str">"cmt">//--- 绘制边框
  m_canvas.Line(class="num">0,class="num">0,x_size,class="num">0,m_palette_border_color);
  m_canvas.Line(class="num">0,y_size,x_size,y_size,m_palette_border_color);
  m_canvas.Line(class="num">0,class="num">0,class="num">0,y_size,m_palette_border_color);
  m_canvas.Line(x_size,class="num">0,x_size,y_size,m_palette_border_color);
  }
class CColorPicker : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 绘制调色板
  class="type">void                DrawPalette(class="kw">const class="type">int index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 绘制调色板                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorPicker::DrawPalette(class="kw">const class="type">int index)
  {
  class="kw">switch(index)
    {
    class=class="str">"cmt">//--- HSL(class="num">0: H, class="num">1: S, class="num">2: L)
    case class="num">0 : case class="num">1 : case class="num">2 :
      {
      DrawHSL(index);
      class="kw">break;
      }
    class=class="str">"cmt">//--- RGB(class="num">3: R, class="num">4: G, class="num">5: B)
    case class="num">3 : case class="num">4 : case class="num">5 :
      {
      DrawRGB(index);
      class="kw">break;
      }
    class=class="str">"cmt">//--- LAB(class="num">6: L, class="num">7: a, class="num">8: b)
    case class="num">6 : case class="num">7 : case class="num">8 :
      {
      DrawLab(index);
      class="kw">break;
      }
    }
class=class="str">"cmt">//--- 绘制调色板的边框
  DrawPaletteBorder();
class=class="str">"cmt">//--- 更新调色板
  m_canvas.Update();
  }

「取色器里 RGB 与 HSL 的边界钳制」

做 MT5 自定义面板时,颜色拾取器最容易被忽略的是组分越界。CColorPicker 里 AdjustmentComponentRGB 用 fmin/fmax 把 R/G/B 三个成员死死钳在 0–255,任何算法算出的负值或溢出值都会被截掉,不会污染后续 HSL/Lab 转换。 HSL 的修正逻辑不一样:m_hsl_h 乘 360、m_hsl_s 与 m_hsl_l 乘 100,说明内部存储的是归一化小数(0–1),显示前才放大到常规角度与百分比。若你抄这段代码去接自己的滑块,记得输入也得先归一,否则乘完直接爆表。 SetControls 这个方法暴露了编辑框联动的细节:当 fix_selected 为真且 index 指向某个单选按钮时,只跳过对应那一个编辑框不回写,其余 8 个(HSL 3个+RGB 3个+Lab 3个里除自身外)全部 ChangeValue 刷新;fix_selected 为假则 9 个框全量同步。实际调试时若发现切模型后某框不更新,先查 index 传参而不是怀疑 ChangeValue 失效。 外汇与贵金属图表上挂这类 UI 属于低风险辅助,但自动化改色若误触发指标重绘,可能短暂占用主线程,高频刷新场景下需自测 MT5 终端响应延迟。

MQL5 / C++
class="type">void CColorPicker::AdjustmentComponentRGB(class="type">void)
  {
  m_rgb_r=::fmin(::fmax(m_rgb_r,class="num">0),class="num">255);
  m_rgb_g=::fmin(::fmax(m_rgb_g,class="num">0),class="num">255);
  m_rgb_b=::fmin(::fmax(m_rgb_b,class="num">0),class="num">255);
  }
class="type">void CColorPicker::AdjustmentComponentHSL(class="type">void)
  {
  m_hsl_h*=class="num">360;
  m_hsl_s*=class="num">100;
  m_hsl_l*=class="num">100;
  }
class CColorPicker : class="kw">public CElement
  {
class="kw">private:
  class="type">void        SetControls(class="kw">const class="type">int index,class="kw">const class="type">bool fix_selected);
  };
class="type">void CColorPicker::SetControls(class="kw">const class="type">int index,class="kw">const class="type">bool fix_selected)
  {
  if(fix_selected)
    {
    if(index!=class="num">0)
      m_hsl_h_edit.ChangeValue(m_hsl_h);
    if(index!=class="num">1)
      m_hsl_s_edit.ChangeValue(m_hsl_s);
    if(index!=class="num">2)
      m_hsl_l_edit.ChangeValue(m_hsl_l);
    if(index!=class="num">3)
      m_rgb_r_edit.ChangeValue(m_rgb_r);
    if(index!=class="num">4)
      m_rgb_g_edit.ChangeValue(m_rgb_g);
    if(index!=class="num">5)
      m_rgb_b_edit.ChangeValue(m_rgb_b);
    if(index!=class="num">6)
      m_lab_l_edit.ChangeValue(m_lab_l);
    if(index!=class="num">7)
      m_lab_a_edit.ChangeValue(m_lab_a);
    if(index!=class="num">8)
      m_lab_b_edit.ChangeValue(m_lab_b);
    class="kw">return;
    }
  m_hsl_h_edit.ChangeValue(m_hsl_h);
  m_hsl_s_edit.ChangeValue(m_hsl_s);
  m_hsl_l_edit.ChangeValue(m_hsl_l);
  m_rgb_r_edit.ChangeValue(m_rgb_r);
  m_rgb_g_edit.ChangeValue(m_rgb_g);
  m_rgb_b_edit.ChangeValue(m_rgb_b);
  m_lab_l_edit.ChangeValue(m_lab_l);
  m_lab_a_edit.ChangeValue(m_lab_a);
  m_lab_b_edit.ChangeValue(m_lab_b);
  }
class CColorPicker : class="kw">public CElement
  {
class="kw">private:
  class="type">void        SetHSL(class="type">void);

取色面板的 RGB 与多空间联动

在 MT5 自定义取色器里,RGB 不是孤立存在的。SetRGB() 先通过三个编辑框 GetValue() 拿到 r/g/b 分量,紧接着调用 RGBtoHSL() 与 RGBtoXYZ()+XYZtoCIELab(),一次性把同一颜色映射到 HSL 和 Lab 空间,方便不同模型下做视觉对比。 SetComponents() 是统一入口:当 fix_selected=true 时,它从 m_picked_color 反解 RGB,再走一遍 HSL/Lab 转换并刷新控件;否则按 index 分流——0~2 走 SetHSL,3~5 走 SetRGB,6~8 走 SetLab,最后用 DrawPalette() 按当前单选按钮重绘调色板。 实测这类转换在 0~255 整型分量下无精度损失,但 HSL 的 l 分量经 AdjustmentComponentHSL() 修正后,可能与原始 RGB 存在 ±1 的舍入偏差,介意的话可在编辑框里直接比对数值。外汇与贵金属图表配色属高风险界面的低优先级环节,改面板不影响订单逻辑,但误改控件绑定可能让 EA 面板报错。

MQL5 / C++
class="type">void CColorPicker::SetRGB(class="type">void)
  {
class=class="str">"cmt">//--- 取得当前的 RGB 组分值
   m_rgb_r=m_rgb_r_edit.GetValue();
   m_rgb_g=m_rgb_g_edit.GetValue();
   m_rgb_b=m_rgb_b_edit.GetValue();
class=class="str">"cmt">//--- 把 RGB 组分转换为 HSL 组分
   m_clr.RGBtoHSL(m_rgb_r,m_rgb_g,m_rgb_b,m_hsl_h,m_hsl_s,m_hsl_l);
class=class="str">"cmt">//--- 修正 HSL 组分
   AdjustmentComponentHSL();
class=class="str">"cmt">//--- 把 RGB 组分转换为 Lab 组分
   m_clr.RGBtoXYZ(m_rgb_r,m_rgb_g,m_rgb_b,m_xyz_x,m_xyz_y,m_xyz_z);
   m_clr.XYZtoCIELab(m_xyz_x,m_xyz_y,m_xyz_z,m_lab_l,m_lab_a,m_lab_b);
class=class="str">"cmt">//--- 在编辑框中设置当前的数值
   SetControls(class="num">0,false);
  }

class="type">void CColorPicker::SetComponents(class="kw">const class="type">int index=class="num">0,class="kw">const class="type">bool fix_selected=true)
  {
class=class="str">"cmt">//--- 如果需要根据所选单选按钮对应的组分修正颜色
   if(fix_selected)
     {
     class=class="str">"cmt">//--- 把所选的颜色解开到RGB组分
     m_rgb_r=m_clr.GetR(m_picked_color);
     m_rgb_g=m_clr.GetG(m_picked_color);
     m_rgb_b=m_clr.GetB(m_picked_color);
     class=class="str">"cmt">//--- 把 RGB 组分转换为 HSL 组分
     m_clr.RGBtoHSL(m_rgb_r,m_rgb_g,m_rgb_b,m_hsl_h,m_hsl_s,m_hsl_l);
     class=class="str">"cmt">//--- 修正 HSL 组分
     AdjustmentComponentHSL();
     class=class="str">"cmt">//--- 把 RGB 组分转换到 LAB 组分
     m_clr.RGBtoXYZ(m_rgb_r,m_rgb_g,m_rgb_b,m_xyz_x,m_xyz_y,m_xyz_z);
     m_clr.XYZtoCIELab(m_xyz_x,m_xyz_y,m_xyz_z,m_lab_l,m_lab_a,m_lab_b);
     class=class="str">"cmt">//--- 在编辑框中设置颜色
     SetControls(m_radio_buttons.SelectedButtonIndex(),true);
     class="kw">return;
     }
class=class="str">"cmt">//--- 设置颜色模型的参数
   class="kw">switch(index)
     {
     case class="num">0 : case class="num">1 : case class="num">2 :
        SetHSL();
        class="kw">break;
     case class="num">3 : case class="num">4 : case class="num">5 :
        SetRGB();
        class="kw">break;
     case class="num">6 : case class="num">7 : case class="num">8 :
        SetLab();
        class="kw">break;
     }
class=class="str">"cmt">//--- 根据所选的单选按钮绘制调色板
   DrawPalette(m_radio_buttons.SelectedButtonIndex());
  }

◍ 把调色板选中的颜色写进三个控件

在 MT5 自定义指标或 EA 的面板里,用户从调色板点选一个颜色后,需要同步刷新悬停块、已选块和当前块三处显示。下面这段 CColorPicker 类的 CurrentColor 方法就是干这个的,直接挂到你的 GUI 对象上就能用。

MQL5 / C++
class="kw">public:
  class=class="str">"cmt">//--- 设置用户从调色板中选择的颜色
  class="type">void                 CurrentColor(class="kw">const class="type">color clr);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置当前颜色                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorPicker::CurrentColor(class="kw">const class="type">color clr)
  {
  m_hover_color=clr;
  m_hover.Color(clr);
  m_hover.BackColor(clr);
  m_hover.Tooltip(::ColorToString(clr));
class=class="str">"cmt">//---
  m_picked_color=clr;
  m_picked.Color(clr);
  m_picked.BackColor(clr);
  m_picked.Tooltip(::ColorToString(clr));
class=class="str">"cmt">//---
  m_current_color=clr;
  m_current.BackColor(clr);
  m_current.Tooltip(::ColorToString(clr));
  }
逐行看:public 下的 CurrentColor 声明只接一个 color 类型参数 clr,由调色板回调传入。函数体里先改 m_hover_color 成员变量,再把悬停控件 m_hover 的前景色、背景色都设成 clr,并用 ColorToString 把色值转成字符串挂到 Tooltip,鼠标移上去就能看到具体色号。 接着同样的逻辑复制到 m_picked 已选控件和 m_current 当前控件,区别是 m_current 没调 Color 只改了 BackColor,说明当前块大概率只显示底色块不画边框文字。三处同步后,面板颜色状态就一致了,不会出现悬停色和已选色脱节。 开 MT5 新建一个继承自 CColorPicker 的面板类,把 m_hover、m_picked、m_current 三个 CWnd 派生对象在 Create 阶段实例化,再绑定调色板按钮的点击事件到 CurrentColor,就能在实盘图表上验证这套刷新逻辑是否跟手。外汇与贵金属图表控件改造属于高风险定制,参数错乱可能导致面板重绘卡顿,建议先在策略测试器里跑。

MQL5 / C++
class="kw">public:
  class=class="str">"cmt">//--- 设置用户从调色板中选择的颜色
  class="type">void                 CurrentColor(class="kw">const class="type">color clr);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置当前颜色                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorPicker::CurrentColor(class="kw">const class="type">color clr)
  {
  m_hover_color=clr;
  m_hover.Color(clr);
  m_hover.BackColor(clr);
  m_hover.Tooltip(::ColorToString(clr));
class=class="str">"cmt">//---
  m_picked_color=clr;
  m_picked.Color(clr);
  m_picked.BackColor(clr);
  m_picked.Tooltip(::ColorToString(clr));
class=class="str">"cmt">//---
  m_current_color=clr;
  m_current.BackColor(clr);
  m_current.Tooltip(::ColorToString(clr));
  }
把控件状态诊断交给小布
这些颜色控件的事件与状态映射,小布盯盘已内置可视化诊断,打开对应品种页即可看控件交互链路,你只需专注业务逻辑。

常见问题

包含背景、调色盘显示区、选中与悬停标记、三组单选按钮加编辑框、取消与确认按钮,是多层对象的复合控件。
可以,小布盯盘内置控件交互诊断,能呈现颜色选择器的事件流向与状态,减少手动打印日志的重复劳动。
HSL 用色相饱和度亮度,数值非负;Lab 的 a/b 坐标带正负,覆盖绿粉蓝黄通道,适合精确色彩空间转换。
颜色按钮负责呈现与触发,选择器负责取色,分离后复用度更高,也避免单个类承担过多职责导致维护困难。