图形界面 VIII: 日历控件 (第一章)·综合运用
(3/3)·从 CDateTime 结构到可复用日历类,收尾这套 GUI 开发库的最后一块拼图
◍ 日历控件的悬停判定与日期修正
在自绘日历控件里,鼠标是否压在某个日期格子上,靠四个边界坐标做包围盒判断:X 大于格子左缘且小于右缘,同时 Y 大于上缘且小于下缘,才算命中。命中后把背景、边框、文字三套颜色切到 hover 状态;若不是当前月,文字走灰显分支。 非命中分支则恢复默认配色,文字颜色同样按「是否同月」在常规色与 off 色之间二选一。每处理完一格就调用 DayInc() 推进临时日期,循环扫完整月。 CCalendar 类对外只暴露 SelectedDate() 的设置与读取,以及 Today() 取系统日。设置新日期时先写进 m_date,再跑 UpdateCalendar() 重绘,界面即时刷新。 跨月赋值有个暗坑:目标月天数可能少于原日期的 day 字段。CorrectingSelectedDay() 在发现 m_date.day 超过 DaysInMonth() 返回值时,直接把 day 钳到当月最大天数,避免写出 2 月 30 日这类非法日期。外汇与贵金属图表加载此类控件属高风险环境,参数越界可能引发 EA 异常退出。
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 实例上就能观察到日期消息触发频率。外汇与贵金属图表加载此类自定义控件存在平台兼容性高风险,建议先用模拟账户窗体试跑。
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 逻辑紊乱,建议在策略测试器先验证。
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() 确认时分秒已归零。外汇与贵金属图表加载此类控件属高风险定制,建议先在模拟账户跑通事件链路。
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——这段边界处理在回测历史面板时容易漏看。
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 里实机测一下焦点重算的耗时。
}
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 图表点几下,就能验证月份切换和年份跳转是否互不打架。
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 左右,翻年手感会明显跟手。外汇与贵金属图表挂这类控件属高风险环境,实盘前务必在策略测试器里点满边界用例。
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 月会稳定触发高亮且不再自增,可作为边界逻辑的验证点。
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 占用和刷新灵敏度间取得平衡;外汇与贵金属行情受跨日跳空影响大,这类本地日历控件仅作展示辅助,实盘仍属高风险。
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 的帧率。
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 手动点一遍验证。
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 策略测试器先用模拟品种跑通坐标逻辑再上真仓。
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 循环遍历,是面板批量管理的标准入口。
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 占用,但高频环境仍属高风险操作,参数需实盘前在策略测试器里压测。
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 { <span class="keyword">private</span>: <span class="comment">//--- 下拉日历</span> CDropCalendar m_drop_calendar1; CDropCalendar m_drop_calendar2; <span class="comment">//---</span> <span class="keyword">private</span>: <span class="comment">//--- 下拉日历</span> <span class="preprocessor">#define </span>DROPCALENDAR1_GAP_X (<span class="number">7</span>) <span class="preprocessor">#define </span>DROPCALENDAR1_GAP_Y (<span class="number">207</span>) <span class="keyword">bool</span> CreateDropCalendar1(<span class="keyword">const</span> <span class="keyword">string</span> text); <span class="preprocessor">#define </span>DROPCALENDAR2_GAP_X (<span class="number">170</span>) <span class="preprocessor">#define </span>DROPCALENDAR2_GAP_Y (<span class="number">207</span>) <span class="keyword">bool</span> &nb
「下拉日历之后还能怎么玩」
这一章把集成式的日历图形控件和下拉日历跑通了,对有时间轴设置的图表来说,直接嵌进面板比手动填 datetime 输入框更不容易填错。源码包 easyandfastgui_mql5.zip 体积 364.68 KB,比 MQL4 版的 364.54 KB 略大,说明 MQL5 封装层多了点东西,下载后丢进 MT5 的 MQL5/Include 就能编译验证。 作者留了个活口:日历还不是终版,后续会加更顺手的交互,也会持续放新控件。下一篇要拆的是树形视图和等级列表,做品种切换面板或账户结构展示时大概率用得上。 想自己改的人,建议先只动日历的默认宽度和字体参数,看渲染会不会错位;社区里有人提过直接移植第三方 C++ GUI 库,但无注释、无内部文档,排错成本远高于基于这套已有说明的框架迭代。 外汇和贵金属品种波动剧烈、杠杆风险高,拿这类界面做挂单或止损定时,先在策略测试器里用历史数据跑几遍再上真实账户。