图形界面 VIII: 日历控件 (第一章)·进阶篇
📘

图形界面 VIII: 日历控件 (第一章)·进阶篇

第 2/3 篇

◍ 日历控件的类骨架与配色字段

在 MT5 自定义图形界面里,CCalendar 直接继承 CElement,构造函数与析构函数均为空实现,说明对象生命周期交给外部表单管理。通过 WindowPointer 把所属 CWindow 的指针存进 m_wnd,后续所有坐标计算都依赖这个宿主窗口。 事件体系覆盖了 OnEvent(鼠标/键盘id分发)、OnEventTimer(周期刷新)和 Moving(整体位移),这意味着日历可以随窗口拖拽实时重绘,也可能在定时器里更新高亮日期。 私有段里堆了 14 个 color 变量:从 m_area_color 到 m_item_text_color_hover,把区域、日期格子、边框、分隔线在不同状态(正常/禁用/悬停/选中)下的颜色全部拆开存。做深色盘面板时,只要改这 14 个值就能整体换肤,不用碰绘制逻辑。 箭头按钮用 m_left_arrow_file_on / _off 两个字符串存位图路径,禁用态和可用态走不同资源,避免用代码画箭头带来的锯齿问题。

MQL5 / C++
CCalendar(class="type">void);
~CCalendar(class="type">void);
class=class="str">"cmt">//--- 保存表单指针
class="type">void        WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
class=class="str">"cmt">//---
class="kw">public:
class=class="str">"cmt">//--- 事件处理函数
class="kw">virtual class="type">void    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">//--- 计时器
class="kw">virtual class="type">void    OnEventTimer(class="type">void);
class=class="str">"cmt">//--- 移动控件
class="kw">virtual class="type">void    Moving(class="kw">const class="type">int x,class="kw">const class="type">int y);
class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除
class="kw">virtual class="type">void    Show(class="type">void);
class="kw">virtual class="type">void    Hide(class="type">void);
class="kw">virtual class="type">void    Reset(class="type">void);
class="kw">virtual class="type">void    Delete(class="type">void);
class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置鼠标左键点击优先级
class="kw">virtual class="type">void    SetZorders(class="type">void);
class="kw">virtual class="type">void    ResetZorders(class="type">void);
class=class="str">"cmt">//--- 重置颜色
class="kw">virtual class="type">void    ResetColors(class="type">void) {}
};
class CCalendar : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 区域颜色
  class="type">class="kw">color          m_area_color;
  class=class="str">"cmt">//--- 区域边框的颜色
  class="type">class="kw">color          m_area_border_color;
  class=class="str">"cmt">//--- 不同状态下日历项目(日期)的颜色
  class="type">class="kw">color          m_item_back_color;
  class="type">class="kw">color          m_item_back_color_off;
  class="type">class="kw">color          m_item_back_color_hover;
  class="type">class="kw">color          m_item_back_color_selected;
  class=class="str">"cmt">//--- 不同状态下项目边框的颜色
  class="type">class="kw">color          m_item_border_color;
  class="type">class="kw">color          m_item_border_color_hover;
  class="type">class="kw">color          m_item_border_color_selected;
  class=class="str">"cmt">//--- 不同状态下项目文字的颜色
  class="type">class="kw">color          m_item_text_color;
  class="type">class="kw">color          m_item_text_color_off;
  class="type">class="kw">color          m_item_text_color_hover;
  class=class="str">"cmt">//--- 分隔线的颜色
  class="type">class="kw">color          m_sepline_color;
  class=class="str">"cmt">//--- 转到之前/之后月份的按钮的标签(在活动/阻止状态下)
  class="type">class="kw">string         m_left_arrow_file_on;
  class="type">class="kw">string         m_left_arrow_file_off;

「给日历控件配色和箭头的接口」

在 MT5 自定义日历控件里,视觉状态全靠一组 setter 接管。区域背景、边框、分隔线各有一个独立入口,改一处不影响其他元素,方便做暗色/亮色主题切换。 日期单元格分四种状态:常态、禁用、悬停、选中,背景色和边框色分别由 ItemBackColor* 与 ItemBorderColor* 控制;文字色也拆出 Off / Hover 两套,避免灰掉的项还用亮白字。 翻月按钮的图标不走画布,而是直接吃外部图片路径,LeftArrowFileOn 接收激活态箭头文件地址写入 m_left_arrow_file_on。右键箭头同理,用字符串成员变量存路径,运行时加载。 下面这段是接口声明原样,逐行看就是「成员变量 + 公开 setter 直接赋值」的极简写法,没有校验和重绘触发,调用后通常要手动 Redraw。 string m_right_arrow_file_on; string m_right_arrow_file_off; //--- public: //--- 设置颜色 (1) 区域, (2) 区域边框, (3) 分隔线 void AreaBackColor(const color clr) { m_area_color=clr; } void AreaBorderColor(const color clr) { m_area_border_color=clr; } void SeparateLineColor(const color clr) { m_sepline_color=clr; } //--- 不同状态下日历项目(日期)的颜色 void ItemBackColor(const color clr) { m_item_back_color=clr; } void ItemBackColorOff(const color clr) { m_item_back_color_off=clr; } void ItemBackColorHover(const color clr) { m_item_back_color_hover=clr; } void ItemBackColorSelected(const color clr) { m_item_back_color_selected=clr; } //--- 不同状态下项目边框的颜色 void ItemBorderColor(const color clr) { m_item_border_color=clr; } void ItemBorderColorHover(const color clr) { m_item_border_color_hover=clr; } void ItemBorderColorSelected(const color clr) { m_item_border_color_selected=clr;} //--- 不同状态下项目文字的颜色 void ItemTextColor(const color clr) { m_item_text_color=clr; } void ItemTextColorOff(const color clr) { m_item_text_color_off=clr; } void ItemTextColorHover(const color clr) { m_item_text_color_hover=clr; } //--- 设置转到之前/之后月份按钮(在活动/阻止状态下)的标签 void LeftArrowFileOn(const string file_path) { m_left_arrow_file_on=file_path; }

MQL5 / C++
class="type">class="kw">string m_right_arrow_file_on;
class="type">class="kw">string m_right_arrow_file_off;
class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 设置颜色 (class="num">1) 区域, (class="num">2) 区域边框, (class="num">3) 分隔线
  class="type">void AreaBackColor(class="kw">const class="type">class="kw">color clr)              { m_area_color=clr;               }
  class="type">void AreaBorderColor(class="kw">const class="type">class="kw">color clr)             { m_area_border_color=clr;        }
  class="type">void SeparateLineColor(class="kw">const class="type">class="kw">color clr)           { m_sepline_color=clr;            }
  class=class="str">"cmt">//--- 不同状态下日历项目(日期)的颜色
  class="type">void ItemBackColor(class="kw">const class="type">class="kw">color clr)               { m_item_back_color=clr;          }
  class="type">void ItemBackColorOff(class="kw">const class="type">class="kw">color clr)            { m_item_back_color_off=clr;      }
  class="type">void ItemBackColorHover(class="kw">const class="type">class="kw">color clr)          { m_item_back_color_hover=clr;    }
  class="type">void ItemBackColorSelected(class="kw">const class="type">class="kw">color clr)       { m_item_back_color_selected=clr; }
  class=class="str">"cmt">//--- 不同状态下项目边框的颜色
  class="type">void ItemBorderColor(class="kw">const class="type">class="kw">color clr)             { m_item_border_color=clr;        }
  class="type">void ItemBorderColorHover(class="kw">const class="type">class="kw">color clr)         { m_item_border_color_hover=clr;  }
  class="type">void ItemBorderColorSelected(class="kw">const class="type">class="kw">color clr)      { m_item_border_color_selected=clr;}
  class=class="str">"cmt">//--- 不同状态下项目文字的颜色
  class="type">void ItemTextColor(class="kw">const class="type">class="kw">color clr)               { m_item_text_color=clr;          }
  class="type">void ItemTextColorOff(class="kw">const class="type">class="kw">color clr)            { m_item_text_color_off=clr;      }
  class="type">void ItemTextColorHover(class="kw">const class="type">class="kw">color clr)          { m_item_text_color_hover=clr;    }
  class=class="str">"cmt">//--- 设置转到之前/之后月份按钮(在活动/阻止状态下)的标签
  class="type">void LeftArrowFileOn(class="kw">const class="type">class="kw">string file_path)      { m_left_arrow_file_on=file_path; }

日历控件的类骨架与指针暴露

在 MT5 自定义 GUI 里做日历,核心是先立一个继承自 CElement 的 CCalendar 类。私有区塞了 7 个星期标题编辑框和 42 个日期格编辑框——42 不是拍脑袋,是 6 行 × 7 列把跨月余数都兜住的最小网格。 对外只放一个 CreateCalendar() 入口,其余 CreateArea / CreateMonthLeftArrow / CreateMonthsList 等九个子创建函数全锁在 private,调用方不必关心箭头位图标签和分隔线矩形怎么拼。 真正方便调试的是后半段直接把内部控件指针吐出来:GetComboBoxPointer 用 ::GetPointer(m_months) 拿组合框,GetListViewPointer 和 GetScrollVPointer 则借 m_months 的内嵌方法取列表与竖滚动条。开 MT5 把这段塞进 EA,就能在 OnInit 里打印指针地址验证对象树是否建全。 外汇与贵金属图表挂这类控件属高风险操作,控件句柄泄漏可能拖慢报价刷新,建议先在模拟盘图表试跑。

MQL5 / C++
class="type">void              LeftArrowFileOff(class="kw">const class="type">class="kw">string file_path)   { m_left_arrow_file_off=file_path;    }
class="type">void              RightArrowFileOn(class="kw">const class="type">class="kw">string file_path)   { m_right_arrow_file_on=file_path;    }
class="type">void              RightArrowFileOff(class="kw">const class="type">class="kw">string file_path)  { m_right_arrow_file_off=file_path;   }
};
class CCalendar : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 用于创建日历的对象和控件
  CRectLabel       m_area;
  CBmpLabel        m_month_dec;
  CBmpLabel        m_month_inc;
  CComboBox        m_months;
  CSpinEdit        m_years;
  CEdit            m_days_week[class="num">7];
  CRectLabel       m_sep_line;
  CEdit            m_days[class="num">42];
  CIconButton      m_button_today;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 用于创建日历的方法
  class="type">bool             CreateCalendar(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y);
  class=class="str">"cmt">//---
class="kw">private:
  class="type">bool             CreateArea(class="type">void);
  class="type">bool             CreateMonthLeftArrow(class="type">void);
  class="type">bool             CreateMonthRightArrow(class="type">void);
  class="type">bool             CreateMonthsList(class="type">void);
  class="type">bool             CreateYearsSpinEdit(class="type">void);
  class="type">bool             CreateDaysWeek(class="type">void);
  class="type">bool             CreateSeparateLine(class="type">void);
  class="type">bool             CreateDaysMonth(class="type">void);
  class="type">bool             CreateButtonToday(class="type">void);
  };
class CCalendar : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- (class="num">1) 取得组合框指针 
  class=class="str">"cmt">//    (class="num">2) 取得列表指针, (class="num">3) 取得列表滚动条指针, 
  class=class="str">"cmt">//    (class="num">4) 取得输入栏位指针, (class="num">5) 取得按钮指针
  CComboBox       *GetComboBoxPointer(class="type">void)                class="kw">const { class="kw">return(::GetPointer(m_months));      }
  CListView       *GetListViewPointer(class="type">void)                      { class="kw">return(m_months.GetListViewPointer()); }
  CScrollV        *GetScrollVPointer(class="type">void)                        { class="kw">return(m_months.GetScrollVPointer());  }

◍ 日历控件的指针暴露与构造配色

在自定义日历类里,先给内部子控件留了取指针的口子:自旋编辑框和图标按钮分别通过 GetSpinEditPointer 与 GetIconButtonPointer 返回,外面要改年份或触发“今天”按钮时不用破封装。 CCalendar 私有区放了三个 CDateTime 实例:m_date 记用户选中日,m_today 取系统当前日,m_temp_date 专供区间计算和越界检查,三者分离能避免重绘时把临时状态写进正式选择。 构造函数用初始化列表一口气铺了 18 个颜色常量,例如悬停底色 C'235,245,255'、选中边框 C'85,170,255',这些 RGB 直接决定 MT5 图形日历的视觉反馈。把 m_zorder 设 0、m_area_zorder 设 1、m_button_zorder 设 2,保证按钮永远压在面板和底色之上,鼠标命中测试才不会穿帮。 开 MT5 新建 EA 把这段抄进自定义面板工程,改 C'193,218,255' 这一个值就能立刻看到选中日高亮变色,验证层级与配色是否如预期生效。

MQL5 / C++
  CSpinEdit       *GetSpinEditPointer(class="type">void)         class="kw">const { class="kw">return(::GetPointer(m_years));       }
  CIconButton     *GetIconButtonPointer(class="type">void)        class="kw">const { class="kw">return(::GetPointer(m_button_today)); }
};
class CCalendar : class="kw">public CElement
{
class="kw">private:
  class=class="str">"cmt">//--- 用于操作日期和时间的结构的实例:
  CDateTime        m_date;       class=class="str">"cmt">// 用户所选择的日期
  CDateTime        m_today;      class=class="str">"cmt">// 用户电脑的当前/系统日期
  CDateTime        m_temp_date;  class=class="str">"cmt">// 用于计算和检查的实例
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CCalendar::CCalendar(class="type">void) : m_area_color(clrWhite),
                             m_area_border_color(clrSilver),
                             m_sepline_color(clrBlack),
                             m_item_back_color(clrWhite),
                             m_item_back_color_off(clrWhite),
                             m_item_back_color_hover(C&class="macro">#x27;class="num">235,class="num">245,class="num">255&class="macro">#x27;),
                             m_item_back_color_selected(C&class="macro">#x27;class="num">193,class="num">218,class="num">255&class="macro">#x27;),
                             m_item_border_color(clrWhite),
                             m_item_border_color_hover(C&class="macro">#x27;class="num">160,class="num">220,class="num">255&class="macro">#x27;),
                             m_item_border_color_selected(C&class="macro">#x27;class="num">85,class="num">170,class="num">255&class="macro">#x27;),
                             m_item_text_color(clrBlack),
                             m_item_text_color_off(C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27;),
                             m_item_text_color_hover(C&class="macro">#x27;class="num">0,class="num">102,class="num">204&class="macro">#x27;),
                             m_left_arrow_file_on(""),
                             m_left_arrow_file_off(""),
                             m_right_arrow_file_on(""),
                             m_right_arrow_file_off("")
  {
class=class="str">"cmt">//--- 在基类中保存控件类的名称
   CElement::ClassName(CLASS_NAME);
class=class="str">"cmt">//--- 设置鼠标左键点击优先级
   m_zorder      =class="num">0;
   m_area_zorder =class="num">1;
   m_button_zorder=class="num">2;
class=class="str">"cmt">//--- 初始化时间结构

「日历控件里差几天才对齐月初」

在 MT5 自定义面板里画月历,第一格往往不是 1 号。OffsetFirstDayOfMonth 干的事,就是算当前月 1 号距离表格首项的偏移量:把 '年.月.1' 转成时间结构,取 day_of_week 减 1,若为负则回绕成 6(对应周日为表首、差一整行的情况)。 代码里写死循环 42 项(6 行 × 7 列),足够覆盖任意月份加前后补位。SetCalendar 先拿 diff 把 m_temp_date 退到表首日期,再每天 DayInc 填格,描述文本只写 day 不写月份,跨月灰区靠颜色区分。 HighlightDate 重算偏移后逐格比对 mon 与 day,命中就把文字色、背景色、边框色全换成选中态。外汇和贵金属图表挂这种控件,注意本地时区用 TimeLocal,跨服务器时间可能错位,属高风险显示层细节。

MQL5 / C++
class="type">int CCalendar::OffsetFirstDayOfMonth(class="type">void)
  {
  class="type">class="kw">string date=class="type">class="kw">string(m_date.year)+"."+class="type">class="kw">string(m_date.mon)+"."+class="type">class="kw">string(class="num">1);
  m_temp_date.DateTime(::StringToTime(date));
  class="type">int diff=(m_temp_date.day_of_week-class="num">1>=class="num">0) ? m_temp_date.day_of_week-class="num">1 : class="num">6;
  m_temp_date.DayDec(diff);
  class="kw">return(diff);
  }

class="type">void CCalendar::SetCalendar(class="type">void)
  {
  class="type">int diff=OffsetFirstDayOfMonth();
  for(class="type">int i=class="num">0; i<class="num">42; i++)
    {
    m_days[i].Description(class="type">class="kw">string(m_temp_date.day));
    m_temp_date.DayInc();
    }
  }

class="type">void CCalendar::HighlightDate(class="type">void)
  {
  OffsetFirstDayOfMonth();
  for(class="type">int i=class="num">0; i<class="num">42; i++)
    {
    if(m_temp_date.mon==m_date.mon &&
       m_temp_date.day==m_date.day)
      {
      m_days[i].Color(m_item_text_color);
      m_days[i].BackColor(m_item_back_color_selected);
      m_days[i].BorderColor(m_item_border_color_selected);
      m_temp_date.DayInc();
      }
    }
  }

日历控件的刷新与悬停着色逻辑

CCalendar 的 UpdateCalendar() 把日期状态同步到界面:先 SetCalendar() 重绘表格,再 HighlightDate() 标出今天与用户选定日,最后把年份写进 m_years、把月份按索引 m_date.mon-1 选入 m_months 组合框。 ChangeObjectsColor(x,y) 负责光标掠过时的重着色。它先 OffsetFirstDayOfMonth() 算出当月首日相对表格首格的偏移,再对 m_days 数组(长度由 ArraySize 取得)逐格判断:若格内日期等于选定日或今天,则 DayInc() 跳到下一天并 continue,不改动颜色。 其余格子按是否属当前月切换文字色(m_item_text_color 或 m_item_text_color_off),边框与底色用统一变量控制。这套分支在 MT5 自定义日历面板里可直接复用,改 m_item_text_color_hover 就能调高亮反馈。 外汇与贵金属图表挂这类控件时需注意:GUI 重绘开销随格子数线性增长,在低频行情品种上可能无明显卡顿,但在秒级刷新的黄金 tick 上概率出现延迟,建议只在 D1 周期面板挂载。

MQL5 / C++
class="type">void CCalendar::UpdateCalendar(class="type">void)
  {
class=class="str">"cmt">//--- 在日历表格中显示变化
   SetCalendar();
class=class="str">"cmt">//--- 突出显示当前日期和用户选择的日期
   HighlightDate();
class=class="str">"cmt">//--- 在输入栏位中设置年份
   m_years.ChangeValue(m_date.year);
class=class="str">"cmt">//--- 在组合框列表中设置月份
   m_months.SelectedItemByIndex(m_date.mon-class="num">1);
  }
class="type">void CCalendar::ChangeObjectsColor(class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 计算当前月第一天和日历表格第一个项目之间的差距
   OffsetFirstDayOfMonth();
class=class="str">"cmt">//--- 在循环中迭代所有表格项
   class="type">int items_total=::ArraySize(m_days);
   for(class="type">int i=class="num">0; i<items_total; i++)
     {
     class=class="str">"cmt">//--- 如果表格项的月份与当前月匹配,并且
     class=class="str">"cmt">//   项目的日期与选择的日期匹配
     if(m_temp_date.mon==m_date.mon &&
         m_temp_date.day==m_date.day)
       {
       class=class="str">"cmt">//--- 继续表格中的下一个项目
       m_temp_date.DayInc();
       class="kw">continue;
       }
     class=class="str">"cmt">//--- 如果项目的 年/月/日 与当前日期(今天)的年/月/日 相匹配
     if(m_temp_date.year==m_today.year &&
         m_temp_date.mon==m_today.mon &&
         m_temp_date.day==m_today.day)
       {
       class=class="str">"cmt">//--- 继续表格中的下一个项目
       m_temp_date.DayInc();

常见问题

至少要预留背景色、周末色、选中色和左右翻月箭头两个指针字段,否则后期改样式只能重写类。
先算当月1号是星期几,再在绘制时左移对应格数;多数错位是因为没把周首日偏移算进循环起点。
可以,把控件类交给小布,它能按你的配色字段生成预览并标出悬停着色的判断分支,省去手动挂盘测试。
仅在翻月、选中和鼠标移动进出格时触发局部重绘,别在每次主循环都全量刷新。
用带判空的保护型getter返回指针,并在析构里置空,避免外部持有野指针导致闪退。