图形界面 IX: 颜色选择器控件 (第一章)·进阶篇
(2/3)· 调色盘控件不止是选色块,搞清 HSL/RGB/Lab 模型与复合结构才能写好 MQL5 界面
多数交易者在 MQL5 里写自定义面板时,直接拖一个颜色控件了事,却没意识到颜色选择器是背景、标记、单选组、按钮叠加的复合元件。一旦事件流向没理顺,选色结果回写失败,界面就成了摆设。
「取色器的多色彩空间底层结构」
在 MT5 自建取色面板时,先把颜色拆成多套坐标并存下来是最省事的做法。下面这段类成员把 RGB、Lab、XYZ 三组双精度分量并列声明,相当于给后续调色板绘制预留了统一缓冲区。
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="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 就能直观看到渐变带。 外汇与贵金属图表叠加这类自定义色域工具属于高风险环境下的辅助视图,颜色映射本身不预测价格,仅帮助人工辨色。
}
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 下可能拖慢响应。
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 终端响应延迟。
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 面板报错。
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 对象上就能用。
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)); }
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)); }