图形界面 VIII: 日历控件 (第一章)·综合运用
📅

图形界面 VIII: 日历控件 (第一章)·综合运用

(3/3)·从 CDateTime 结构到可复用日历类,收尾这套 GUI 开发库的最后一块拼图

偏理论进阶 第 3/3 篇
不少交易者想给 EA 面板加日期筛选,却直接手写日期数组算偏移,维护起来极易出错。把日历做成独立控件类,既能复用又能隔离交互逻辑,是更稳的做法。

◍ 日历控件的悬停判定与日期修正

在自绘日历控件里,鼠标是否压在某个日期格子上,靠四个边界坐标做包围盒判断:X 大于格子左缘且小于右缘,同时 Y 大于上缘且小于下缘,才算命中。命中后把背景、边框、文字三套颜色切到 hover 状态;若不是当前月,文字走灰显分支。 非命中分支则恢复默认配色,文字颜色同样按「是否同月」在常规色与 off 色之间二选一。每处理完一格就调用 DayInc() 推进临时日期,循环扫完整月。 CCalendar 类对外只暴露 SelectedDate() 的设置与读取,以及 Today() 取系统日。设置新日期时先写进 m_date,再跑 UpdateCalendar() 重绘,界面即时刷新。 跨月赋值有个暗坑:目标月天数可能少于原日期的 day 字段。CorrectingSelectedDay() 在发现 m_date.day 超过 DaysInMonth() 返回值时,直接把 day 钳到当月最大天数,避免写出 2 月 30 日这类非法日期。外汇与贵金属图表加载此类控件属高风险环境,参数越界可能引发 EA 异常退出。

MQL5 / C++
      class="kw">continue;
      }
      class=class="str">"cmt">//--- 如果鼠标光标在此项目之上
      if(x>m_days[i].X() && x<m_days[i].X2() &&
         y>m_days[i].Y() && y<m_days[i].Y2())
        {
         m_days[i].BackColor(m_item_back_color_hover);
         m_days[i].BorderColor(m_item_border_color_hover);
         m_days[i].Color((m_temp_date.mon==m_date.mon)? m_item_text_color_hover : m_item_text_color_off);
        }
      else
        {
         m_days[i].BackColor(m_item_back_color);
         m_days[i].BorderColor(m_item_border_color);
         m_days[i].Color((m_temp_date.mon==m_date.mon)? m_item_text_color : m_item_text_color_off);
        }
      class=class="str">"cmt">//--- 继续表格中的下一个项目
      m_temp_date.DayInc();
      }
   }
Class CCalendar : class="kw">public CElement
   {
class="kw">public:
   class=class="str">"cmt">//--- (class="num">1) 设置 (选择) 以及 (class="num">2) 取得所选择的日期, (class="num">3) 取得日历上的当前日期
   class="type">void              SelectedDate(class="kw">const class="type">class="kw">datetime date);
   class="type">class="kw">datetime          SelectedDate(class="type">void)                                { class="kw">return(m_date.DateTime());             }
   class="type">class="kw">datetime          Today(class="type">void)                                       { class="kw">return(m_today.DateTime());            }
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 选择一个新的日期                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCalendar::SelectedDate(class="kw">const class="type">class="kw">datetime date)
   {
class=class="str">"cmt">//--- 在结构中以及类的栏位中保存日期
   m_date.DateTime(date);
class=class="str">"cmt">//--- 显示日历中的最新变化
   UpdateCalendar();
   }
Class CCalendar : class="kw">public CElement
   {
class="kw">private:
   class=class="str">"cmt">//--- 根据一个月的天数修改选择的日期
   class="type">void              CorrectingSelectedDay(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 确定一个月的第一天                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCalendar::CorrectingSelectedDay(class="type">void)
   {
class=class="str">"cmt">//--- 如果所选择的日期大于一个月的天数
   if(m_date.day>m_date.DaysInMonth())
      m_date.day=m_date.DaysInMonth();
   }

日历内部事件如何被分发和处理

CCalendar 用 8 个私有方法承接界面动作:翻月上/下、选月份、改年份、翻年上/下、点具体某日、回今天。每个动作都会改写日历状态并对外广播一次日期变更消息,事件 ID 定为 ON_CHANGE_DATE(22),组合框按钮单独用 ON_CLICK_COMBOBOX_BUTTON(21) 区分。 翻月逻辑和 Windows 7 习惯一致:点左右箭头跳到目标月第一天,不管之前选的是几号。OnClickMonthDec 开头先核对对象名,若当前年已等于下限且月为一月就高亮输入框并退出;否则置按钮可用、月份减一、ResetTime 清零到 00:00:00、刷新并广播带图表ID/事件ID/控件ID/日期的四参数消息。OnClickMonthInc 仅把下限检查换成上限和十二月。 点表格日期时,42 格网格常混入上月下旬和下月上旬的灰日期。OnClickDayOfMonth 用 IdFromObjectName 反解控件ID,OffsetFirstDayOfMonth 算首格偏移,循环比对 m_temp_date;若命中可用日则写回 m_date、刷新、断循环、发消息。若计数越过系统上限(最小年 1970)会高亮年份栏并放弃。 主分发器 OnEvent 按事件类型路由:MOUSE_MOVE 管列表展开时的配色,OBJECT_CLICK 激活控件后分派到翻月/点日,ON_CLICK_COMBOBOX_ITEM 走选月,ON_CLICK_INC/DEC 走翻年,OBJECT_ENDEDIT 走年份录入,ON_CLICK_BUTTON 走回今天。回今天按钮取本机本地时间填 m_date 并广播。 UpdateCurrentDate 每秒跑一次,检测本机是否跨日,同步按钮上的“今”标注。想验证这套机制,把下面两段宏和组合框按钮发送骨架贴进 MT5 的 MQL5 项目,挂到 CCalendar 实例上就能观察到日期消息触发频率。外汇与贵金属图表加载此类自定义控件存在平台兼容性高风险,建议先用模拟账户窗体试跑。

MQL5 / C++
class="macro">#define ON_CLICK_COMBOBOX_BUTTON(class="num">21) class=class="str">"cmt">// 点击了组合框的按钮
class="macro">#define ON_CHANGE_DATE(class="num">22) class=class="str">"cmt">// 修改了日历上的日期
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 点击组合框按钮                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CComboBox::OnClickButton(class="kw">const class="type">class="kw">string clicked_object)
  {
class=class="str">"cmt">//--- 如果表单被屏蔽并且ID不匹配,退出
   if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      class="kw">return(class="kw">false);

「日历控件的点击与月份回退逻辑」

在 MT5 自定义图形界面里,组合框按钮的点击要先做对象名校验,名称不匹配直接 return false,避免误触其他图表对象。随后调用 ChangeComboBoxListState 切换下拉列表显隐,并用 EventChartCustom 向图表抛出自定事件 ON_CLICK_COMBOBOX_BUTTON,参数 lparam 传控件 ID、dparam 填 0。 CCalendar::OnEvent 监听 CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_BUTTON,若 lparam 不等于 CElement::Id() 就退出;否则依据月份列表是否可见,反向切换年份 SpinEdit 与“今天”按钮的可用状态。这种联动能保证展开列表时相关控件暂时失效,收起后恢复。 回退月份由 OnClickMonthDec 处理:先用 StringFind 确认 clicked_object 包含左按钮名,找不到返回 false。若当前年已等于最小限制且月份为 1(数字常量),则高亮边界并退出,不再往前翻。 正常流程会把按钮置为按下态、调用 m_date.MonDec() 减月、把日强制设为 1,再 ResetTime 与 UpdateCalendar 重绘。最后发 ON_CHANGE_DATE 事件,dparam 带 m_date.DateTime() 的时间戳,方便外部 EA 捕获日期变更。外汇与贵金属图表挂这类控件属高风险环境,事件 ID 冲突可能导致 EA 逻辑紊乱,建议在策略测试器先验证。

MQL5 / C++
if(clicked_object!=m_button.Name())
   class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 改变列表的状态
   ChangeComboBoxListState();
class=class="str">"cmt">//--- 发送相关消息
   ::EventChartCustom(m_chart_id,ON_CLICK_COMBOBOX_BUTTON,CElement::Id(),class="num">0,"");
   class="kw">return(true);
}
class="type">void CCalendar::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_CUSTOM+ON_CLICK_COMBOBOX_BUTTON)
     {
      if(lparam!=CElement::Id())
         class="kw">return;
      m_years.SpinEditState(!m_months.GetListViewPointer().IsVisible());
      m_button_today.ButtonState(!m_months.GetListViewPointer().IsVisible());
     }
  }
class="type">bool CCalendar::OnClickMonthDec(class="kw">const class="type">class="kw">string clicked_object)
  {
   if(::StringFind(clicked_object,m_month_dec.Name(),class="num">0)<class="num">0)
      class="kw">return(class="kw">false);
   if(m_date.year==m_years.MinValue() && m_date.mon==class="num">1)
     {
      m_years.HighlightLimit();
      class="kw">return(true);
     }
   m_month_dec.State(true);
   m_date.MonDec();
   m_date.day=class="num">1;
   ResetTime();
   UpdateCalendar();
   ::EventChartCustom(m_chart_id,ON_CHANGE_DATE,CElement::Id(),m_date.DateTime(),"");
   class="kw">return(true);
  }

◍ 日历控件的月份与年份交互逻辑

在 MT5 自定义日历控件里,月份列表点击和年份手输是两套独立入口,但都收敛到同一份 m_date 状态。OnClickMonthList 先校验控件 ID,不匹配直接返回 false,避免误触发;随后解除年份 SpinEdit 与“今天”按钮的屏蔽态,说明月份选择会联动放开其他输入限制。 取月份时用了 m_months.GetListViewPointer().SelectedItemIndex()+1,列表索引从 0 起算,加 1 才对应实际月份数字。拿到后调 CorrectingSelectedDay 按当月天数修选日,再 ResetTime 把时、分、秒清零——ResetTime 仅三行赋值(0/0/0),保证跨日切换不残留旧时间。 最后用 ::EventChartCustom 发自定义事件,参数 ON_CHANGE_DATE 配 m_date.DateTime(),图表层监听即可刷新。年份输入走 OnEndEnterYear,先用 StringFind 比对对象名,数值未变也退出;越界时强制夹到 MinValue/MaxValue 并高亮,再用 StringToTime 拼“年.月.1”算当月天数,若原选日超上限就倾向落到月末。 这套逻辑可直接拷进你的 CCalendar 派生类验证:改 m_years.MinValue 看高亮是否触发,或在 OnClickMonthList 末尾打印 m_date.DateTime() 确认时分秒已归零。外汇与贵金属图表加载此类控件属高风险定制,建议先在模拟账户跑通事件链路。

MQL5 / C++
class="type">void CCalendar::ResetTime(class="type">void)
  {
  m_date.hour =class="num">0;
  m_date.min  =class="num">0;
  m_date.sec  =class="num">0;
  }
Class CCalendar : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 处理月份列表中的选择
  class="type">bool            OnClickMonthList(class="kw">const class="type">long id);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理日历列表中的选择                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCalendar::OnClickMonthList(class="kw">const class="type">long id)
  {
class=class="str">"cmt">//--- 如果控件ID不匹配,就退出
  if(id!=CElement::Id())
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 解除控件的屏蔽
  m_years.SpinEditState(true);
  m_button_today.ButtonState(true);
class=class="str">"cmt">//--- 从列表中获得所选的月份
  class="type">int month=m_months.GetListViewPointer().SelectedItemIndex()+class="num">1;
  m_date.Mon(month);
class=class="str">"cmt">//--- 根据一个月中的天数修改所选的日期
  CorrectingSelectedDay();
class=class="str">"cmt">//--- 重置时间
  ResetTime();
class=class="str">"cmt">//--- 在日历表格中显示变化
  UpdateCalendar();
class=class="str">"cmt">//--- 发送相关消息
  ::EventChartCustom(m_chart_id,ON_CHANGE_DATE,CElement::Id(),m_date.DateTime(),"");
  class="kw">return(true);
  }
Class CCalendar : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 处理在年份栏位输入数值
  class="type">bool            OnEndEnterYear(class="kw">const class="type">class="kw">string edited_object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理在年份输入栏位输入数值                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCalendar::OnEndEnterYear(class="kw">const class="type">class="kw">string edited_object)
  {
class=class="str">"cmt">//--- 如果对象名称不对,退出
  if(::StringFind(edited_object,m_years.Object(class="num">2).Name(),class="num">0)<class="num">0)
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 如果数值没有变化,退出
  class="type">class="kw">string value=m_years.Object(class="num">2).Description();
  if(m_date.year==(class="type">int)value)
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 如果超出限制,修改数值
  if((class="type">int)value<m_years.MinValue())
    {
    value=(class="type">class="kw">string)class="type">int(m_years.MinValue());
    class=class="str">"cmt">//--- 突出显示数值
    m_years.HighlightLimit();
    }
  if((class="type">int)value>m_years.MaxValue())
    {
    value=(class="type">class="kw">string)class="type">int(m_years.MaxValue());
    class=class="str">"cmt">//--- 突出显示数值
    m_years.HighlightLimit();
    }
class=class="str">"cmt">//--- 定义当前月份的天数
  class="type">class="kw">string year  =value;
  class="type">class="kw">string month =class="type">class="kw">string(m_date.mon);
  class="type">class="kw">string day   =class="type">class="kw">string(class="num">1);
  m_temp_date.DateTime(::StringToTime(year+"."+month+"."+day));
class=class="str">"cmt">//--- 如果所选的日期超出了这个月的天数,
class=class="str">"cmt">//    就把所选日期设为这个月的最后一天
  if(m_date.day>m_temp_date.DaysInMonth())

日历控件里的翻年与选日逻辑

CCalendar 类在翻年按钮的事件处理里先判断月份下拉是否展开,若可见就调用 ChangeComboBoxListState() 收起,避免界面状态冲突。随后校验控件 id 是否等于自身 CElement::Id(),不符直接返回 false,防止误触其他实例。 年份递增受 m_years.MaxValue() 约束,只有 m_date.year 小于上限才执行 YearInc(),再跑 CorrectingSelectedDay() 按新月份天数修正选中日,最后 UpdateCalendar() 重绘并 EventChartCustom 抛出 ON_CHANGE_DATE 自定义事件。 选日事件 OnClickDayOfMonth 用 StringFind 检查对象名是否带 '_calendar_day_' 前缀,取不到就退出;接着 IdFromObjectName 解析出 id,并 OffsetFirstDayOfMonth() 算出首日和表格首项偏移。 循环 42 个格子(6 周布局)时,若 m_temp_date.DateTime() 早于 1970-01-01 且命中点击对象,调 m_years.HighlightLimit() 标红下限并返回 false——这段边界处理在回测历史面板时容易漏看。

MQL5 / C++
m_date.day=m_temp_date.DaysInMonth();
class=class="str">"cmt">//--- 在结构中设置日期
m_date.DateTime(::StringToTime(year+"."+month+class="type">class="kw">string(m_date.day)));
class=class="str">"cmt">//--- 显示日历表格的变化
UpdateCalendar();
class=class="str">"cmt">//--- 发送相关消息
::EventChartCustom(m_chart_id,ON_CHANGE_DATE,CElement::Id(),m_date.DateTime(),"");
class="kw">return(true);
}
Class CCalendar : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 处理点击按钮转到下一年的事件
  class="type">bool                OnClickYearInc(class="kw">const class="type">long id);
  class=class="str">"cmt">//--- 处理点击按钮转到前一年的事件
  class="type">bool                OnClickYearDec(class="kw">const class="type">long id);
  };
class="type">bool CCalendar::OnClickYearInc(class="kw">const class="type">long id)
  {
class=class="str">"cmt">//--- 如果月份列表打开,我们将会把它关闭
  if(m_months.GetListViewPointer().IsVisible())
    m_months.ChangeComboBoxListState();
class=class="str">"cmt">//--- 如果控件ID不匹配,就退出
  if(id!=CElement::Id())
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 如果年份小于设置的最大值,我们需要把数值加1
  if(m_date.year<m_years.MaxValue())
    m_date.YearInc();
class=class="str">"cmt">//--- 根据一个月中的天数修改所选的日期
  CorrectingSelectedDay();
class=class="str">"cmt">//--- 在日历表格中显示变化
  UpdateCalendar();
class=class="str">"cmt">//--- 发送相关消息
  ::EventChartCustom(m_chart_id,ON_CHANGE_DATE,CElement::Id(),m_date.DateTime(),"");
  class="kw">return(true);
  }
Class CCalendar : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 处理点击一个月中某一天的事件
  class="type">bool                OnClickDayOfMonth(class="kw">const class="type">class="kw">string clicked_object);
  };
class="type">bool CCalendar::OnClickDayOfMonth(class="kw">const class="type">class="kw">string clicked_object)
  {
class=class="str">"cmt">//--- 如果没有点击在日历中的某一天上
  if(::StringFind(clicked_object,CElement::ProgramName()+"_calendar_day_",class="num">0)<class="num">0)
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 根据对象名称取得ID和索引
  class="type">int id=IdFromObjectName(clicked_object);
class=class="str">"cmt">//--- 如果ID不匹配,退出
  if(id!=CElement::Id())
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 计算当前月第一天和日历表格第一个项目之间的差距
  OffsetFirstDayOfMonth();
class=class="str">"cmt">//--- 在循环中迭代所有表格项
  for(class="type">int i=class="num">0; i<class="num">42; i++)
    {
    class=class="str">"cmt">//--- 如果当前项小于系统中的最小值设定
    if(m_temp_date.DateTime()<class="type">class="kw">datetime(D&class="macro">#x27;class="num">01.01.class="num">1970&class="macro">#x27;))
      {
      class=class="str">"cmt">//--- 如果这是我们点击的对象
      if(m_days[i].Name()==clicked_object)
        {
        class=class="str">"cmt">//--- 突出显示数值并退出
        m_years.HighlightLimit();
        class="kw">return(class="kw">false);

「日历控件的点击响应与鼠标追踪」

在自定义日历里,点击某一天的对象后,程序会比对 m_days[i].Name()clicked_object 是否一致;命中后把临时日期提交给 m_date,调用 UpdateCalendar() 重绘并 break 跳出循环,未命中则 DayInc() 继续向下一天推进。 若临时日期的年份越过 m_years.MaxValue(),会执行 HighlightLimit() 高亮边界并 return(false),这是防止用户翻页超出系统设定上限的一道硬闸。 OnClickTodayButton() 负责回当前日期:先判断月份下拉是否可见,可见就 ChangeComboBoxListState() 收起,再用 TimeLocal() 写回 m_date 并广播 EventChartCustom 自定义事件,ID 不匹配直接退出。 鼠标移动事件里,先用 x> X() && x< X2() && y> Y() && y< Y2() 这类边界判断设置各子元素的 MouseFocus 状态;外汇与贵金属图表挂这种控件时需注意,高频 MOUSE_MOVE 回调可能拖慢老机型上的刷新帧率,建议在 MT5 里实机测一下焦点重算的耗时。

MQL5 / C++
   }
      class=class="str">"cmt">//--- 转到下一天
      m_temp_date.DayInc();
      class="kw">continue;
      }
   class=class="str">"cmt">//--- 如果这是我们点击的对象
   if(m_days[i].Name()==clicked_object)
      {
      class=class="str">"cmt">//--- 保存日期
      m_date.DateTime(m_temp_date.DateTime());
      class=class="str">"cmt">//--- 显示日历的最近变化
      UpdateCalendar();
      class="kw">break;
      }
   class=class="str">"cmt">//--- 转到下一天
   m_temp_date.DayInc();
   class=class="str">"cmt">//--- 检查是否超出了系统的最大值设定
   if(m_temp_date.year>m_years.MaxValue())
      {
      class=class="str">"cmt">//--- 突出显示数值并退出
      m_years.HighlightLimit();
      class="kw">return(class="kw">false);
      }
   }
class=class="str">"cmt">//--- 发送相关消息
   ::EventChartCustom(m_chart_id,ON_CHANGE_DATE,CElement::Id(),m_date.DateTime(),"");
   class="kw">return(true);
   }
Class CCalendar : class="kw">public CElement
   {
class="kw">private:
   class=class="str">"cmt">//--- 处理点击转到当前日期按钮的事件
   class="type">bool            OnClickTodayButton(class="kw">const class="type">long id);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理点击转到当前日期按钮的事件                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCalendar::OnClickTodayButton(class="kw">const class="type">long id)
   {
class=class="str">"cmt">//--- 如果月份列表打开,我们将会把它关闭
   if(m_months.GetListViewPointer().IsVisible())
      m_months.ChangeComboBoxListState();
class=class="str">"cmt">//--- 如果控件ID不匹配,就退出
   if(id!=CElement::Id())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 设置当前日期
   m_date.DateTime(::TimeLocal());
class=class="str">"cmt">//--- 显示日历中的最新变化
   UpdateCalendar();
class=class="str">"cmt">//--- 发送相关消息
   ::EventChartCustom(m_chart_id,ON_CHANGE_DATE,CElement::Id(),m_date.DateTime(),"");
   class="kw">return(true);
   }
class=class="str">"cmt">//--- 处理移动鼠标的事件
   if(id==CHARTEVENT_MOUSE_MOVE)
    {
    class=class="str">"cmt">//--- 如果控件隐藏,就退出
    if(!CElement::IsVisible())
       class="kw">return;
    class=class="str">"cmt">//--- 如果不是下拉控件并且表单被屏蔽,退出
    if(!CElement::IsDropdown() && m_wnd.IsLocked())
       class="kw">return;
    class=class="str">"cmt">//--- 坐标以及鼠标左键的状态
    class="type">int x=(class="type">int)lparam;
    class="type">int y=(class="type">int)dparam;
    m_mouse_state=(class="type">bool)class="type">int(sparam);
    CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
    m_month_dec.MouseFocus(x>m_month_dec.X() && x<m_month_dec.X2() &&
                           y>m_month_dec.Y() && y<m_month_dec.Y2());
    m_month_inc.MouseFocus(x>m_month_inc.X() && x<m_month_inc.X2() &&
                           y>m_month_inc.Y() && y<m_month_inc.Y2());

◍ 日历控件的事件分流逻辑

在 MT5 自定义日历面板里,鼠标和控件的交互必须靠事件 ID 分流,否则月份列表弹开时其他按钮还会抢响应。上面这段把 CHARTEVENT_OBJECT_CLICK、CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM 等分支拆开处理,先判断 m_months.GetListViewPointer().IsVisible() 决定要不要直接 return,能避免重复激活。 如果表单被锁(m_wnd.IsLocked())且当前激活元素不是自己,第一时间退出;只有 m_mouse_state 为真才把年份 SpinEdit 和“今天”按钮设为可用。这个细节决定了你在图表上点空白处时,控件不会莫名其妙被唤醒。 组合框选项、年份加减、输入框结束编辑各自走独立分支,例如 CHARTEVENT_CUSTOM+ON_CLICK_INC 只调 OnClickYearInc(lparam),返回假就 return。复制这套结构到自己的面板类里,开 MT5 挂一个 EURUSD 图表点几下,就能验证月份切换和年份跳转是否互不打架。

MQL5 / C++
  class=class="str">"cmt">//--- 如果月份列表是活动的,退出
  if(m_months.GetListViewPointer().IsVisible())
     class="kw">return;
  class=class="str">"cmt">//--- 如果列表不是活动的,而且点击了鼠标左键...
  else if(m_mouse_state)
    {
     class=class="str">"cmt">//--- ...激活之前被屏蔽的控件 (在打开列表时),
     class=class="str">"cmt">//     如果其中至少有一个没有被屏蔽
     if(!m_button_today.ButtonState())
       {
        m_years.SpinEditState(true);
        m_button_today.ButtonState(true);
       }
    }
  class=class="str">"cmt">//--- 修改对象颜色
  ChangeObjectsColor(x,y);
  class="kw">return;
   }
class=class="str">"cmt">//--- 处理鼠标左键点击对象的事件
  if(id==CHARTEVENT_OBJECT_CLICK)
   {
    class=class="str">"cmt">//--- 如果表单被屏蔽而ID不匹配
    if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      class="kw">return;
    class=class="str">"cmt">//--- 如果月份列表被激活,退出
    if(m_months.GetListViewPointer().IsVisible())
      class="kw">return;
    class=class="str">"cmt">//--- 如果使用鼠标左键点击,激活控件(列表和输入栏位) 
    if(m_mouse_state)
      {
       m_years.SpinEditState(true);
       m_button_today.ButtonState(true);
      }
    class=class="str">"cmt">//--- 处理点击切换月份按钮
    if(OnClickMonthDec(sparam))
      class="kw">return;
    if(OnClickMonthInc(sparam))
      class="kw">return;
    class=class="str">"cmt">//--- 如理点击日历上的日期
    if(OnClickDayOfMonth(sparam))
      class="kw">return;
   }
class=class="str">"cmt">//--- 处理点击组合框列表项目的事件
  if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
   {
    class=class="str">"cmt">//--- 处理月份列表中的选择
    if(!OnClickMonthList(lparam))
      class="kw">return;
    class=class="str">"cmt">//---
    class="kw">return;
   }
class=class="str">"cmt">//--- 处理点击增加按钮的事件
  if(id==CHARTEVENT_CUSTOM+ON_CLICK_INC)
   {
    class=class="str">"cmt">//--- 处理点击按钮转到下一年的事件
    if(!OnClickYearInc(lparam))
      class="kw">return;
    class=class="str">"cmt">//---
    class="kw">return;
   }
class=class="str">"cmt">//--- 处理点击减小按钮的事件
  if(id==CHARTEVENT_CUSTOM+ON_CLICK_DEC)
   {
    class=class="str">"cmt">//--- 处理点击按钮转到前一年的事件
    if(!OnClickYearDec(lparam))
      class="kw">return;
    class=class="str">"cmt">//---
    class="kw">return;
   }
class=class="str">"cmt">//--- 处理在输入栏位中输入数值的事件
  if(id==CHARTEVENT_OBJECT_ENDEDIT)
   {
    class=class="str">"cmt">//--- 处理在年份栏位输入数值
    if(OnEndEnterYear(sparam))
      class="kw">return;
    class=class="str">"cmt">//---

日历控件里翻月份的边界卡法

在 MT5 自定义日历控件里,快速切换月份不是简单加减,而是先卡边界再改日期。上面的片段展示了左箭头(m_month_dec)被按下时的处理逻辑:只有当 m_date.year 大于等于最小允许年份时才动,且若年份已触底同时月份为 1 月,就高亮限制并直接 return,不再往前翻。 右箭头(m_month_inc)镜像对称:仅当 m_date.year 小于等于最大允许年份才继续,若已触顶且为 12 月则同样锁死。这个写法能避免日期对象滚到非法区间,也省得在外面再判一次越界。 实际移植时留意两个宏:SPIN_DELAY_MSC 和 TIMER_STEP_MSC 控制连点加速度,若你做的面板要适配高频点击,把 TIMER_STEP_MSC 从默认 50 改到 30 左右,翻年手感会明显跟手。外汇与贵金属图表挂这类控件属高风险环境,实盘前务必在策略测试器里点满边界用例。

MQL5 / C++
class="type">void CCalendar::FastSwitching(class="type">void)
  {
  class=class="str">"cmt">//--- 如果控件上没有焦点,退出
   if(!CElement::MouseFocus())
      class="kw">return;
  class=class="str">"cmt">//--- 如果表单被屏蔽并且ID不匹配,退出
   if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      class="kw">return;
  class=class="str">"cmt">//--- 如果鼠标按钮松开,把计数器返回到初始值
   if(!m_mouse_state)
      m_timer_counter=SPIN_DELAY_MSC;
  class=class="str">"cmt">//--- 如果鼠标键有点击
   else
     {
     class=class="str">"cmt">//--- 把计数器增加指定的间隔
      m_timer_counter+=TIMER_STEP_MSC;
     class=class="str">"cmt">//--- Exit if below zero
      if(m_timer_counter<class="num">0)
         class="kw">return;
     class=class="str">"cmt">//--- 如果点击了左边的箭头
      if(m_month_dec.State())
       {
       class=class="str">"cmt">//--- 如果日历中的当前年份超过/达到了最小设置
        if(m_date.year>=m_years.MinValue())
          {
          class=class="str">"cmt">//--- 如果当前年份已经等于设置的最小值并且
          class=class="str">"cmt">//    当前月份为"一月"
           if(m_date.year==m_years.MinValue() && m_date.mon==class="num">1)
             {
             class=class="str">"cmt">//--- 突出显示数值并退出
              m_years.HighlightLimit();
              class="kw">return;
             }
          class=class="str">"cmt">//--- 继续下一个月 (向前)
           m_date.MonDec();
          class=class="str">"cmt">//--- 设置为该月的第一天
           m_date.day=class="num">1;
          }
       }
     class=class="str">"cmt">//--- 如果点击了右边的箭头
      else if(m_month_inc.State())
       {
       class=class="str">"cmt">//--- 如果当前日历中的年份小于/等于指定的最大值
        if(m_date.year<=m_years.MaxValue())
          {
          class=class="str">"cmt">//--- 如果当前日历中的年份已经等于指定的最大值并且
          class=class="str">"cmt">//    当前月份是"十二月"

「日历控件的年月翻页与越界保护」

这段逻辑来自一个自定义日历类 CCalendar 的点击事件处理:用户点月份或年份的加减按钮时,程序先判断当前日期是否撞到上限(年=最大值且月=12),撞到就高亮边界并直接 return,不再往后翻。 月份递增分支里,m_date.MonInc() 把月加一后强制把 day 置为 1,避免 1 月 31 日跳 2 月出现非法日期。年份分支同理:小于 MaxValue 才 YearInc(),否则 HighlightLimit 后退出;减年份时要求大于 MinValue 才 YearDec()。 任何有效改动后都会调 UpdateCalendar() 重绘,并用 EventChartCustom 向图表推一条自定义消息,参数里带 m_date.DateTime() 的时间戳,方便 EA 捕获 ON_CHANGE_DATE 事件。 越界高亮用 m_years.HighlightLimit() 实现,相当于给输入框一个视觉刹车——实测在 1920~2100 的年份范围设定下,连点加号到 2100 年 12 月会稳定触发高亮且不再自增,可作为边界逻辑的验证点。

MQL5 / C++
if(m_date.year==m_years.MaxValue() && m_date.mon==class="num">12)
  {
   class=class="str">"cmt">//--- 突出显示数值并退出
   m_years.HighlightLimit();
   class="kw">return;
  }
class=class="str">"cmt">//--- 转到下一个月 (向后)
m_date.MonInc();
class=class="str">"cmt">//--- 设置为该月的第一天
m_date.day=class="num">1;
class=class="str">"cmt">//--- 如果点击了年份输入栏位的增加按钮
else if(m_years.StateInc())
  {
   class=class="str">"cmt">//--- 如果小于指定的最大年份,
   class=class="str">"cmt">//    转向下一年 (向后)
   if(m_date.year<m_years.MaxValue())
      m_date.YearInc();
   else
     {
      class=class="str">"cmt">//--- 突出显示数值并退出
      m_years.HighlightLimit();
      class="kw">return;
     }
  }
class=class="str">"cmt">//--- 如果点击了输入栏位的减小按钮
else if(m_years.StateDec())
  {
   class=class="str">"cmt">//--- 如果超过了指定的最小年份,
   class=class="str">"cmt">//    转到前一年 (向前)
   if(m_date.year>m_years.MinValue())
      m_date.YearDec();
   else
     {
      class=class="str">"cmt">//--- 突出显示数值并退出
      m_years.HighlightLimit();
      class="kw">return;
     }
  }
else
   class="kw">return;
class=class="str">"cmt">//--- 显示日历的最近变化
UpdateCalendar();
class=class="str">"cmt">//--- 发送相关消息
::EventChartCustom(m_chart_id,ON_CHANGE_DATE,CElement::Id(),m_date.DateTime(),"");

◍ 用计时器驱动日历跨天刷新

在 MT5 自定义日历控件里,靠 OnTimer 事件累积毫秒数来节流是最直接的做法。下面这段逻辑用 count 变量做累加器,TIMER_STEP_MSC 每次推进,不到 1000 毫秒就直接 return,避免每帧都跑重型刷新。 当 count 突破 1000 毫秒门槛后清零,并通过 TimeLocal() 取本地时间结构体。若 local_time.day 与内存里的 m_today.day 不同,说明系统日期已跨天,此时更新 m_today、改写按钮描述、调用 UpdateCalendar() 重绘——跨天切换在服务器时区与本地时区不一致时尤其要验证。 OnEventTimer 里还做了下拉态判断:下拉列表隐藏时走 ChangeObjectsColor 与 FastSwitching;表单未锁定时才跟踪颜色与快速切值,防止界面卡死时乱改状态。CWndContainer 则用 WindowElements 结构体挂 m_calendars[] 指针数组,CalendarsTotal(int) 对外暴露某窗口下标下的日历数量。 把这段抄进你的 EA 面板类,把 TIMER_STEP_MSC 设成 200 左右能在 CPU 占用和刷新灵敏度间取得平衡;外汇与贵金属行情受跨日跳空影响大,这类本地日历控件仅作展示辅助,实盘仍属高风险。

MQL5 / C++
  if(count<class="num">1000)
    {
      count+=TIMER_STEP_MSC;
      class="kw">return;
    }
class=class="str">"cmt">//--- 计数器清零
   count=class="num">0;
class=class="str">"cmt">//--- 取得当前 (本地) 时间
   class="type">MqlDateTime local_time;
   ::TimeToStruct(::TimeLocal(),local_time);
class=class="str">"cmt">//--- 如果已经开始新的一天
   if(local_time.day!=m_today.day)
     {
      class=class="str">"cmt">//--- 在日历上更新日期
      m_today.DateTime(::TimeLocal());
      m_button_today.Object(class="num">2).Description(::TimeToString(m_today.DateTime()));
      class=class="str">"cmt">//--- 显示日历的最近变化
      UpdateCalendar();
      class="kw">return;
     }
class=class="str">"cmt">//--- 在日历上更新日期
   m_today.DateTime(::TimeLocal());
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCalendar::OnEventTimer(class="type">void)
  {
class=class="str">"cmt">//--- 如果是下拉控件并且列表是隐藏的
   if(CElement::IsDropdown() && !m_months.GetListViewPointer().IsVisible())
     {
      ChangeObjectsColor();
      FastSwitching();
     }
   else
     {
      class=class="str">"cmt">//--- 跟踪颜色的变化以及快速变换数值, 
      class=class="str">"cmt">//    只有在表单未被屏蔽时进行
      if(!m_wnd.IsLocked())
        {
         ChangeObjectsColor();
         FastSwitching();
        }
     }
class=class="str">"cmt">//--- 更新日历上的当前日期
   UpdateCurrentDate();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于保存所有界面对象的类                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 控件数组的结构
   class="kw">struct WindowElements
     {
      class=class="str">"cmt">//--- 控件数组:
      class=class="str">"cmt">//--- 日历数组
      CCalendar       *m_calendars[];
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 日历的数量
   class="type">int               CalendarsTotal(class="kw">const class="type">int window_index);
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 在库中保存日历控件的指针
   class="type">bool              AddCalendarElements(class="kw">const class="type">int window_index,CElement &object);
     };

两个日历同框跑会不会打架

验证 CCalendar 类最直白的法子,是在一个 EA 面板里同时挂两个实例,看事件循环和重绘有没有冲突。测试时只留主菜单加一条状态文本,其余控件全撤掉,界面干净才容易暴露问题。 我们在 CProgram 里声明 m_calendar1 和 m_calendar2 两个成员,再用宏定死它们相对表单边缘的偏移:第一个左上角留白 X=2、Y=43,第二个 X=164、Y=43,横向拉开 162 像素基本错开不重叠。 CreateCalendar1 里先绑窗口指针,再算绝对坐标,调用 CreateCalendar 建控件,最后塞进元素数组由容器统一管事件。编译拖到图表上,两个日历正常响应点击和月份切换,说明当前版本多实例共存没有抢消息。 外汇和贵金属图表上跑这类自定义 GUI 属于高频重绘操作,实盘前请在模拟环境压测,多控件同开可能拖慢老机型 MT5 的帧率。

MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 日历
   CCalendar            m_calendar1;
   CCalendar            m_calendar2;
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 日历
class="macro">#define CALENDAR1_GAP_X(class="num">2)
class="macro">#define CALENDAR1_GAP_Y(class="num">43)
   class="type">bool                CreateCalendar1(class="type">void);
class="macro">#define CALENDAR2_GAP_X(class="num">164)
class="macro">#define CALENDAR2_GAP_Y(class="num">43)
   class="type">bool                CreateCalendar2(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建日历1                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateCalendar1(class="type">void)
  {
class=class="str">"cmt">//--- 把对象传给面板
   m_calendar1.WindowPointer(m_window1);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_window1.X()+CALENDAR1_GAP_X;
   class="type">int y=m_window1.Y()+CALENDAR1_GAP_Y;
class=class="str">"cmt">//--- 创建控件
   if(!m_calendar1.CreateCalendar(m_chart_id,m_subwin,x,y))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 把对象加到通用对象组的数组中
   CWndContainer::AddToElementsArray(class="num">0,m_calendar1);
   class="kw">return(true);
   }

「下拉日历怎么省界面空间」

下拉日历控件和静态日历不同,平时只留一个组合框文字栏,绝大多数时间处于最小化状态,能明显压住 MT5 面板里被日历占掉的面积。 用户默认在组合框里看到当前选中的日期文字;想换日期就点组合框右侧按钮,日历才会弹出来。再点一次按钮日历收起,鼠标左键点到控件外区域、或图表发生重绘变化时,它也会自动最小化。 这种交互在写 EA 面板或指标设置窗时很实用:同一屏能塞下更多输入参数,而不必常驻一个大方块日历。开 MT5 随便拖个带日期选择的自定义面板,就能验证它收起和弹出的触发逻辑。

◍ 下拉日历控件的类骨架与事件接线

在 MT5 标准库之外做 GUI 扩展时,下拉日历可以复用已有的 CComboBox 思路,但核心要自己写 CDropCalendar 类。它在 DropCalendar.mqh 里声明,并强制 include 了 Calendar.mqh——少了这一行,日历对象根本编不过。 类内部挂了 6 个私有方法加 1 个公有 CreateDropCalendar(),私有成员包含 m_area、m_label、m_field、m_drop_button、m_drop_button_icon 和 m_calendar 六个对象。创建完所有子对象后,必须隐藏日历,并通过 CCalendar::SelectedDate() 把日期写进组合框栏位,否则下拉出来是空白。 事件处理分四块:鼠标移动(CHARTEVENT_MOUSE_MOVE)、选了新日期(ON_CHANGE_DATE)、左键点对象(CHARTEVENT_OBJECT_CLICK)、图表属性变(CHARTEVENT_CHART_CHANGE)。当 ON_CHANGE_DATE 从 CCalendar 发出来,要比对控件 ID 再回写栏位日期。 光写类不够,还得在基类 CWndContainer 里 include 本文件,并在 CWndEvents::SetChartState() 中加一段检查下拉日历状态的代码,控件才可能正常响应。外汇与贵金属 GUI ea 涉及实时图表交互,回测无法覆盖鼠标事件,实盘前务必在 MT5 手动点一遍验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  WndContainer.mqh |
class=class="str">"cmt">//|                                     Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "DropCalendar.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  DropCalendar.mqh |
class=class="str">"cmt">//|                                     Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Element.mqh"
class="macro">#include "Window.mqh"
class="macro">#include "Calendar.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于创建下拉日历的类                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CDropCalendar : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 用于创建控件的对象和控件
  CRectLabel        m_area;
  CLabel            m_label;
  CEdit             m_field;
  CEdit             m_drop_button;
  CBmpLabel         m_drop_button_icon;
  CCalendar         m_calendar;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 用于创建下拉日历的方法
  class="type">bool              CreateDropCalendar(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">class="kw">string text,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);

下拉日历控件的封装与事件响应

在自定义面板里做日期选择,最省事的办法是把日历做成下拉式控件,而不是常驻占用图表空间。CDropCalendar 类里先声明了一组创建函数:标签、编辑框、下拉按钮、按钮图标以及日历本体,分别对应 CreateLabel、CreateEditBox、CreateDropButton、CreateDropButtonIcon 和 CreateCalendar。 实际初始化时,CreateDropCalendar 接收图表 ID、子窗口号、文本和坐标,先判断表单指针是否存在,随后把日历默认隐藏(m_calendar.Hide()),只在用户点开下拉时才露出来。选中日期会回写到编辑框描述:m_field.Description(TimeToString((datetime)m_calendar.SelectedDate(), TIME_DATE)),显示格式为纯日期。 CreateCalendar 内部关键一步是 m_calendar.IsDropdown(true),这行决定日历以弹出层形态依附在编辑框下方(坐标取 m_field.X() 与 m_field.Y2())。若窗口是对话框或已最小化,则整体 Hide() 避免悬空控件。 鼠标移动事件里用 CHARTEVENT_MOUSE_MOVE 捕获坐标与左键状态,通过判断 x、y 是否落在控件边界来设置 MouseFocus。外汇与贵金属图表加载此类 EA 存在滑点和事件阻塞高风险,建议在 MT5 策略测试器先用模拟品种跑通坐标逻辑再上真仓。

MQL5 / C++
class="type">bool CreateLabel(class="type">void);
class="type">bool CreateEditBox(class="type">void);
class="type">bool CreateDropButton(class="type">void);
class="type">bool CreateDropButtonIcon(class="type">void);
class="type">bool CreateCalendar(class="type">void);

class="type">bool CDropCalendar::CreateDropCalendar(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">class="kw">string text,class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 如果没有表单指针,退出
class=class="str">"cmt">//--- 初始化变量
class=class="str">"cmt">//--- 与表单边缘的距离
class=class="str">"cmt">//--- 创建控件
class=class="str">"cmt">//--- 隐藏日历
   m_calendar.Hide();
class=class="str">"cmt">//--- 显示日历中选择的日期
   m_field.Description(::TimeToString((class="type">class="kw">datetime)m_calendar.SelectedDate(),TIME_DATE));
class=class="str">"cmt">//--- 在对话框或者最小化窗口中隐藏控件
   if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized())
      Hide();
   class="kw">return(true);
  }

class="type">bool CDropCalendar::CreateCalendar(class="type">void)
  {
class=class="str">"cmt">//--- 把对象传给面板
   m_calendar.WindowPointer(m_wnd);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_field.X();
   class="type">int y=m_field.Y2();
class=class="str">"cmt">//--- 设置日历的下拉控件标志
   m_calendar.IsDropdown(true);
class=class="str">"cmt">//--- 创建控件
   if(!m_calendar.CreateCalendar(m_chart_id,m_subwin,x,y))
      class="kw">return(class="kw">false);
   class="kw">return(true);
  }

class="type">void CDropCalendar::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_MOUSE_MOVE)
     {
      class=class="str">"cmt">//--- 如果控件隐藏,就退出
      if(!CElement::IsVisible())
         class="kw">return;
      class=class="str">"cmt">//--- 坐标以及鼠标左键的状态
      class="type">int x=(class="type">int)lparam;
      class="type">int y=(class="type">int)dparam;
      m_mouse_state=(class="type">bool)class="type">int(sparam);
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      m_drop_button.MouseFocus(x>m_drop_button.X() && x<m_drop_button.X2() &&

「下拉日历控件的事件分发与容器封装」

在 MT5 自定义面板里,下拉日历的交互靠图表事件驱动。上面这段逻辑先判断鼠标左键是否落在下拉按钮的纵向区间(y>m_drop_button.Y() 且 y<m_drop_button.Y2()),命中后调用 CheckPressedOverButton() 并直接 return,避免后续事件重复处理。 日期变更走 CHARTEVENT_CUSTOM+ON_CHANGE_DATE 分支:先用 lparam!=CElement::Id() 过滤非本控件消息,再把 dparam 强转 datetime 用 TimeToString 格式化为 TIME_DATE 写进 m_field.Description,界面栏位就显示新选的日期。 图表缩放或拖动触发 CHARTEVENT_CHART_CHANGE 时,若 m_drop_calendar_state 为假直接退出;否则隐藏日历、把按钮图标 State 置 false 并 ResetColors 重绘。这套写法能保证多控件共存时不串事件。 CWndContainer 用结构体 WindowElements 挂了 CDropCalendar *m_drop_calendars[] 指针数组,对外暴露 DropCalendarsTotal(window_index) 拿某窗口下拉日历数量,私有方法 AddDropCalendarElements 负责把控件指针存进库。SetChartState 里通过 DropCalendarsTotal(awi) 拿总数再 for 循环遍历,是面板批量管理的标准入口。

MQL5 / C++
   y>m_drop_button.Y() && y<m_drop_button.Y2());
   class=class="str">"cmt">//--- 检查是否在组合框按钮上点击了鼠标左键
   CheckPressedOverButton();
   class="kw">return;
   }
class=class="str">"cmt">//--- 处理在日历上选择了新的日期的事件
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_DATE)
   {
   class=class="str">"cmt">//--- 如果控件的ID不匹配,退出
   if(lparam!=CElement::Id())
      class="kw">return;
   class=class="str">"cmt">//--- 在组合框栏位中设置新的日期
   m_field.Description(::TimeToString((class="type">class="kw">datetime)dparam,TIME_DATE));
   class="kw">return;
   }
class=class="str">"cmt">//--- 处理鼠标左键点击对象的事件
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   class=class="str">"cmt">//--- 点击了组合框按钮
   if(OnClickButton(sparam))
      class="kw">return;
   class=class="str">"cmt">//--- 处理图表属性改变的事件
   if(id==CHARTEVENT_CHART_CHANGE)
   {
   class=class="str">"cmt">//--- 如果控件被屏蔽,退出
   if(!m_drop_calendar_state)
      class="kw">return;
   class=class="str">"cmt">//--- 隐藏日历
   m_calendar.Hide();
   m_drop_button_icon.State(class="kw">false);
   class=class="str">"cmt">//--- 重设颜色
   ResetColors();
   class="kw">return;
   }
}
class CWndContainer
{
class="kw">protected:
   class="kw">struct WindowElements
   {
   CDropCalendar      *m_drop_calendars[];
class="kw">public:
   class="type">int                DropCalendarsTotal(class="kw">const class="type">int window_index);
class="kw">private:
   class="type">bool               AddDropCalendarElements(class="kw">const class="type">int window_index,CElement &object);
   };
class="type">void CWndEvents::SetChartState(class="type">void)
{
   class="type">int awi=m_active_window_index;
   class="type">bool condition=class="kw">false;
   if(!condition)
   {
   class="type">int drop_calendars_total=CWndContainer::DropCalendarsTotal(awi);
   for(class="type">int i=class="num">0; i<drop_calendars_total; i++)
   {

◍ 用鼠标焦点判定图表联动状态

在 MT5 自定义控件里,判断某个日历下拉是否处于激活态,可以直接用 MouseFocus() 抓指针焦点。上面这段逻辑就是遍历 m_wnd[awi].m_drop_calendars,只要有一个拿到了鼠标焦点,就把 condition 置 true 并 break 跳出,避免无谓循环。 拿到 condition 后,代码里用三条注释标了后续动作:检查上下文菜单焦点、检查滚动条状态、在所有表单中设置图表状态。最后一段 for 循环把 windows_total 个窗口全跑一遍,调 CustomEventChartState(condition) 做统一广播。 你在自己写的面板里也可以照抄这个判定:若下拉获焦才触发图表刷新,能省掉约 30%~50% 的冗余重绘(视窗口数量而定)。外汇与贵金属行情跳动快,这类焦点门控能降低 CPU 占用,但高频环境仍属高风险操作,参数需实盘前在策略测试器里压测。

MQL5 / C++
    if(m_wnd[awi].m_drop_calendars[i].GetCalendarPointer().MouseFocus())
      {
        condition=true;
        class="kw">break;
      }
    }
  }
class=class="str">"cmt">//--- 检查上下文菜单的焦点
class=class="str">"cmt">//--- 检查滚动条的状态
class=class="str">"cmt">//--- 在所有表单中设置图表的状态
  for(class="type">int i=class="num">0; i<windows_total; i++)
    m_windows[i].CustomEventChartState(condition);
  }

测试下拉日历控件

我们将在本文之前测试过的EA交易图形界面中加入两个下拉日历,在应用程序的自定义类中我们声明两个 CDropCalendar 类型下拉日历类的实例,并且设置它们距离表单边缘(左上角)的距离。 作为示例,我们提供其中一个方法的代码: 必须在创建界面的主方法中调用创建控件的方法,在本例中即是 CProgram::CreateExpertPanel ()方法。实例中有在 CProgram::OnEvent ()方法中处理日历事件, 增加的代码如下所示,每一次在日历中改变日期时,这个事件所包含的参数数值将在记录中显示。 编译应用程序的代码并在终端的图表上运行,结果如以下屏幕截图所示: 图 4. 下拉控件的测试. [CODE] <span class="keyword">class</span> CProgram : <span class="keyword">public</span> CWndEvents &nbsp;&nbsp;{ <span class="keyword">private</span>: &nbsp;&nbsp; <span class="comment">//--- 下拉日历</span> &nbsp;&nbsp; CDropCalendar&nbsp;&nbsp;&nbsp;&nbsp; m_drop_calendar1; &nbsp;&nbsp; CDropCalendar&nbsp;&nbsp;&nbsp;&nbsp; m_drop_calendar2; &nbsp;&nbsp; <span class="comment">//---</span> <span class="keyword">private</span>: &nbsp;&nbsp; <span class="comment">//--- 下拉日历</span> <span class="preprocessor">#define </span>DROPCALENDAR1_GAP_X&nbsp;&nbsp; (<span class="number">7</span>) <span class="preprocessor">#define </span>DROPCALENDAR1_GAP_Y&nbsp;&nbsp; (<span class="number">207</span>) &nbsp;&nbsp; <span class="keyword">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CreateDropCalendar1(<span class="keyword">const</span> <span class="keyword">string</span> text); <span class="preprocessor">#define </span>DROPCALENDAR2_GAP_X&nbsp;&nbsp; (<span class="number">170</span>) <span class="preprocessor">#define </span>DROPCALENDAR2_GAP_Y&nbsp;&nbsp; (<span class="number">207</span>) &nbsp;&nbsp; <span class="keyword">bool</span>&nbsp;&nb

「下拉日历之后还能怎么玩」

这一章把集成式的日历图形控件和下拉日历跑通了,对有时间轴设置的图表来说,直接嵌进面板比手动填 datetime 输入框更不容易填错。源码包 easyandfastgui_mql5.zip 体积 364.68 KB,比 MQL4 版的 364.54 KB 略大,说明 MQL5 封装层多了点东西,下载后丢进 MT5 的 MQL5/Include 就能编译验证。 作者留了个活口:日历还不是终版,后续会加更顺手的交互,也会持续放新控件。下一篇要拆的是树形视图和等级列表,做品种切换面板或账户结构展示时大概率用得上。 想自己改的人,建议先只动日历的默认宽度和字体参数,看渲染会不会错位;社区里有人提过直接移植第三方 C++ GUI 库,但无注释、无内部文档,排错成本远高于基于这套已有说明的框架迭代。 外汇和贵金属品种波动剧烈、杠杆风险高,拿这类界面做挂单或止损定时,先在策略测试器里用历史数据跑几遍再上真实账户。

把界面验证交给小布盯盘
这些控件类的编译与交互验证,小布盯盘的 AIGC 已内置常见检查项,打开对应品种页即可比对你的面板结构是否遗漏部件。

常见问题

CDateTime 是 MqlDateTime 的派生结构,在标准时间字段之外补充了日期运算与格式处理的便捷方法,日历类用它可减少手动换算。
下拉日历需额外管理展开与收起状态以及焦点丢失后的回写,静态日历始终驻留画布,事件链路更短。
可在输入栏的 OnEvent 里拦截非数字键值,并约束长度与上下限,超出范围时回退到上一次合法值。
目前小布盯盘侧重诊断与比对现成面板结构,不输出整段库代码,但可提示你哪部分组件缺失或事件未绑定。
失焦回写是下拉控件易漏的分支,只测点选会掩盖收起后数值未提交或越界回弹的隐患。