MFC时间控件完全自绘:从GDI绘图到交互逻辑的深度定制实践

发布时间:2026/8/9 12:03:02
MFC时间控件完全自绘:从GDI绘图到交互逻辑的深度定制实践 1. 项目概述为什么要在MFC里折腾时间控件的完全自绘做MFC桌面开发的朋友尤其是那些维护着历史遗留项目或者对界面有苛刻定制需求的肯定都跟系统标准控件“斗智斗勇”过。系统提供的日期时间选择控件CDateTimeCtrl功能是挺全但那个界面风格怎么说呢一股浓浓的“Windows经典”味儿跟现在流行的扁平化、现代化设计格格不入。更头疼的是你想改个颜色、换个字体、调整一下内部布局对不起系统控件提供的自绘接口NM_CUSTOMDRAW非常有限基本上只能改改背景色和文本色想动它的内部结构比如把年月日时分秒的显示区域重新排布或者加上自定义的图标、背景图那简直是痴人说梦。所以“完全自绘”这个概念就应运而生了。它不是简单的换肤而是彻底抛弃系统控件的原生绘制逻辑从零开始用GDI/GDI一笔一划地把整个控件的外观画出来。这意味着你对控件的每一个像素都有绝对的控制权。我最近就在一个医疗设备的上位机软件里遇到了这个需求客户要求时间控件必须符合他们全新的UI设计规范包括特定的渐变色背景、圆角边框、高亮选中状态甚至要在时间旁边显示一个自定义的状态图标。标准控件和常规的自绘根本做不到逼得我只能走上完全自绘这条路。这条路走通了收益是巨大的。你得到的不仅仅是一个独一无二的时间控件更是一套可复用的、高自由度的自绘框架。以后遇到任何需要深度定制界面的控件比如列表、树形、按钮组你都可以用类似的思路去攻克。当然挑战也不小你需要深入理解Windows消息机制、MFC的文档/视图架构、GDI绘图以及如何优雅地处理用户交互鼠标点击、键盘输入。接下来我就把自己踩过的坑和最终实现的方案掰开揉碎了分享给大家。2. 核心思路与架构设计从继承到重绘完全自绘不是蛮干得有清晰的架构。最核心的思路是继承并重写CDateTimeCtrl类然后拦截并处理关键的绘制和输入消息用自己的绘图代码完全覆盖父类的行为。2.1 类设计创建自定义时间控件类第一步创建一个新的MFC类比如叫CCustomDateTimeCtrl让它继承自CDateTimeCtrl。这一步在VS的类向导里就能完成。// CustomDateTimeCtrl.h #pragma once #include afxdtctl.h // 包含 MFC 日期时间控件头文件 class CCustomDateTimeCtrl : public CDateTimeCtrl { DECLARE_DYNAMIC(CCustomDateTimeCtrl) public: CCustomDateTimeCtrl(); virtual ~CCustomDateTimeCtrl(); // 重写虚函数 virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); virtual BOOL OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult); // 自定义绘制函数 void DrawBackground(CDC* pDC, CRect rect); void DrawText(CDC* pDC, CRect rect); void DrawFocusRect(CDC* pDC, CRect rect); void DrawDropDownButton(CDC* pDC, CRect rect); // 自定义属性 void SetTextColor(COLORREF clr); void SetBkColor(COLORREF clr); void SetHighlightColor(COLORREF clr); protected: DECLARE_MESSAGE_MAP() afx_msg void OnPaint(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); private: COLORREF m_clrText; // 文本颜色 COLORREF m_clrBk; // 背景颜色 COLORREF m_clrHighlight; // 高亮颜色 // 其他状态变量如当前焦点区域等 };这里的关键点在于我们声明了DrawItem这个虚函数。当控件的样式包含DTS_APPCANPARSE或我们处理自绘消息时框架会调用它。但仅仅这样还不够因为CDateTimeCtrl默认并不完全支持自绘。2.2 消息映射与绘制入口为了让控件进入完全自绘模式我们需要在创建控件时加上DTS_APPCANPARSE样式。这个样式原本是允许应用程序解析用户输入的但它有一个副作用会让控件发送WM_DRAWITEM消息从而触发我们的DrawItem函数。在对话框的OnInitDialog中创建或子类化控件时要修改样式BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // ... if (m_wndDateTimeCtrl.SubclassDlgItem(IDC_DATETIMEPICKER1, this)) { // 添加自绘相关样式 DWORD dwStyle m_wndDateTimeCtrl.GetStyle(); dwStyle | DTS_APPCANPARSE; m_wndDateTimeCtrl.ModifyStyle(0, dwStyle); // 设置自定义颜色等属性 m_wndDateTimeCtrl.SetTextColor(RGB(50, 50, 50)); m_wndDateTimeCtrl.SetBkColor(RGB(240, 245, 250)); } // ... return TRUE; }然而在实际操作中我发现仅靠DTS_APPCANPARSE和DrawItem有时并不能捕获所有的绘制请求特别是控件失去焦点、获得焦点、鼠标悬停等动态效果。更可靠的方法是直接重写OnPaint函数并禁止控件的默认擦除背景行为OnEraseBkgnd返回TRUE并什么也不做这样我们就能获得完整的绘制控制权。BEGIN_MESSAGE_MAP(CCustomDateTimeCtrl, CDateTimeCtrl) ON_WM_PAINT() ON_WM_ERASEBKGND() ON_WM_LBUTTONDOWN() ON_WM_KEYDOWN() // 可以添加更多消息如鼠标移动、键盘输入等 END_MESSAGE_MAP() void CCustomDateTimeCtrl::OnPaint() { CPaintDC dc(this); // 设备上下文用于绘制 CRect rectClient; GetClientRect(rectClient); // 1. 绘制背景 DrawBackground(dc, rectClient); // 2. 获取并格式化当前时间文本 CString strTime; SYSTEMTIME sysTime; if (GetTime(sysTime) GDT_VALID) { // 根据控件格式长日期、短日期、时间等格式化字符串 DWORD dwStyle GetStyle(); if (dwStyle DTS_TIMEFORMAT) strTime.Format(_T(%02d:%02d:%02d), sysTime.wHour, sysTime.wMinute, sysTime.wSecond); else if (dwStyle DTS_SHORTDATEFORMAT) strTime.Format(_T(%d/%d/%d), sysTime.wMonth, sysTime.wDay, sysTime.wYear); // ... 其他格式处理 } else { strTime _T(); // 或显示默认提示 } // 3. 绘制时间文本 // 这里需要精细计算文本位置模拟原控件的布局 CRect rectText rectClient; rectText.DeflateRect(5, 2); // 内边距 DrawText(dc, rectText, strTime); // 4. 如果控件有下拉按钮样式绘制下拉按钮 DWORD dwStyle GetStyle(); if (dwStyle DTS_UPDOWN) { // 绘制上下微调按钮 } else { // 绘制右侧的下拉日历按钮 DrawDropDownButton(dc, rectClient); } // 5. 如果控件获得焦点绘制焦点虚线框 if (GetFocus() this) { DrawFocusRect(dc, rectClient); } } BOOL CCustomDateTimeCtrl::OnEraseBkgnd(CDC* pDC) { // 返回TRUE表示我们已经处理了背景擦除防止闪烁 return TRUE; }注意在OnPaint中直接进行所有绘制是“完全自绘”最彻底的方式。它避免了系统默认绘制和自绘消息之间的冲突但也意味着所有交互状态的绘制如按下、禁用都需要你自己编码实现。2.3 交互逻辑的重构完全自绘后控件的点击、键盘响应逻辑也需要我们接管。例如点击下拉按钮区域应该弹出月份日历点击文本区域应该进入编辑模式或切换焦点部分。我们需要在OnLButtonDown中判断点击位置void CCustomDateTimeCtrl::OnLButtonDown(UINT nFlags, CPoint point) { // 1. 判断点击的是否是下拉按钮区域 CRect rectButton; GetClientRect(rectButton); rectButton.left rectButton.right - 20; // 按钮假设宽度20像素 if (rectButton.PtInRect(point)) { // 模拟原控件的下拉行为发送 DTN_DROPDOWN 通知消息 NMHDR nmhdr { m_hWnd, GetDlgCtrlID(), DTN_DROPDOWN }; GetParent()-SendMessage(WM_NOTIFY, GetDlgCtrlID(), (LPARAM)nmhdr); // 注意这里只是发送通知实际弹出日历需要父窗口处理。 // 更复杂的实现是自己创建并显示一个自定义的月份日历控件。 return; // 拦截不调用基类 } // 2. 判断点击的是文本的哪一部分年、月、日等用于切换编辑焦点 // 这需要根据当前格式和文本绘制区域来计算逻辑较复杂。 // 如果不处理则调用基类但基类行为可能不符合自绘预期 // CDateTimeCtrl::OnLButtonDown(nFlags, point); // 通常在完全自绘下我们选择不调用基类而是自己设置焦点等。 SetFocus(); }键盘处理OnKeyDown则更复杂需要处理左右箭头切换字段、上下箭头增减值、直接输入数字等。这本质上是在重新实现一个微型的时间编辑器。对于许多项目如果不需要复杂的键盘导航可以先实现点击下拉选择键盘逻辑暂时屏蔽或简化。3. 核心绘制技术细节与GDI实战完全自绘的核心就是绘图。MFC下主要使用GDI如果需要更复杂的图形如圆角、渐变、Alpha混合可以引入GDI。3.1 双缓冲绘图解决闪烁的终极方案在OnPaint中直接向屏幕DC绘制如果图形复杂很容易出现闪烁。双缓冲技术是解决这个问题的标准做法。void CCustomDateTimeCtrl::OnPaint() { CPaintDC dcScreen(this); // 最终绘制到屏幕的DC CRect rectClient; GetClientRect(rectClient); // 创建内存DC和位图 CDC dcMem; CBitmap bmpMem; dcMem.CreateCompatibleDC(dcScreen); bmpMem.CreateCompatibleBitmap(dcScreen, rectClient.Width(), rectClient.Height()); CBitmap* pOldBmp dcMem.SelectObject(bmpMem); // --- 所有绘制操作在内存DC上进行 --- // 1. 绘制背景到内存DC DrawBackground(dcMem, rectClient); // 2. 绘制文本到内存DC // ... // 3. 绘制按钮到内存DC // ... // --- 将内存DC内容一次性拷贝到屏幕DC --- dcScreen.BitBlt(0, 0, rectClient.Width(), rectClient.Height(), dcMem, 0, 0, SRCCOPY); // 清理资源 dcMem.SelectObject(pOldBmp); bmpMem.DeleteObject(); dcMem.DeleteDC(); }实操心得对于像时间控件这样大小固定、重绘频繁的控件我通常会在类中缓存内存DC和位图而不是每次OnPaint都创建销毁。只需在控件大小改变OnSize时重建位图即可能提升一些性能。3.2 使用GDI实现现代化外观GDI在绘制平滑渐变和圆角方面比较吃力而GDI则简单很多。首先需要在项目中引入GDI#include gdiplus.h链接gdiplus.lib。一个绘制圆角渐变背景的例子void CCustomDateTimeCtrl::DrawBackground(CDC* pDC, CRect rect) { using namespace Gdiplus; Graphics graphics(pDC-GetSafeHdc()); graphics.SetSmoothingMode(SmoothingModeAntiAlias); // 抗锯齿 // 定义渐变画刷 LinearGradientBrush brush( Point(rect.left, rect.top), Point(rect.left, rect.bottom), Color(255, 240, 245, 250), // 起始颜色 ARGB Color(255, 220, 230, 240) // 结束颜色 ); // 定义圆角矩形路径 int cornerRadius 5; GraphicsPath path; path.AddLine(rect.left cornerRadius, rect.top, rect.right - cornerRadius, rect.top); path.AddArc(rect.right - 2*cornerRadius, rect.top, 2*cornerRadius, 2*cornerRadius, 270, 90); path.AddLine(rect.right, rect.top cornerRadius, rect.right, rect.bottom - cornerRadius); path.AddArc(rect.right - 2*cornerRadius, rect.bottom - 2*cornerRadius, 2*cornerRadius, 2*cornerRadius, 0, 90); path.AddLine(rect.right - cornerRadius, rect.bottom, rect.left cornerRadius, rect.bottom); path.AddArc(rect.left, rect.bottom - 2*cornerRadius, 2*cornerRadius, 2*cornerRadius, 90, 90); path.AddLine(rect.left, rect.bottom - cornerRadius, rect.left, rect.top cornerRadius); path.AddArc(rect.left, rect.top, 2*cornerRadius, 2*cornerRadius, 180, 90); path.CloseFigure(); // 填充路径 graphics.FillPath(brush, path); // 绘制边框 Pen pen(Color(255, 180, 190, 210), 1); // 灰色边框 graphics.DrawPath(pen, path); }3.3 精确文本绘制与布局时间控件文本的布局是关键要模拟原生控件那种字段分隔如“2024/05/27”并支持焦点切换的效果。void CCustomDateTimeCtrl::DrawText(CDC* pDC, CRect rect, const CString strText) { // 1. 设置字体 CFont* pOldFont pDC-SelectObject(m_font); // m_font 是预先创建好的字体 // 2. 计算文本尺寸 CSize textSize pDC-GetTextExtent(strText); // 3. 计算垂直居中位置 int yPos rect.top (rect.Height() - textSize.cy) / 2; // 4. 设置文本颜色和背景模式 pDC-SetTextColor(m_clrText); pDC-SetBkMode(TRANSPARENT); // 透明背景这样我们绘制的渐变背景才能透出来 // 5. 绘制文本 pDC-TextOut(rect.left 5, yPos, strText); // 左对齐左边距5像素 // 6. 如果需要高亮当前编辑字段例如“月”字段被选中 if (m_nFocusField 0) // m_nFocusField 表示当前焦点字段索引 { // 计算该字段子串的起始和结束位置这需要解析strText和格式 int nStartChar, nEndChar; // ... 计算逻辑复杂需要根据格式字符串如“yyyy/MM/dd”来解析 CString strField strText.Mid(nStartChar, nEndChar - nStartChar 1); CSize fieldSize pDC-GetTextExtent(strField); // 计算该字段的矩形区域 CRect rectField(rect.left 5 pDC-GetTextExtent(strText.Left(nStartChar)).cx, yPos, rect.left 5 pDC-GetTextExtent(strText.Left(nEndChar 1)).cx, yPos fieldSize.cy); // 绘制高亮背景例如一个半透明的色块 pDC-FillSolidRect(rectField, RGB(255, 255, 200)); // 浅黄色高亮 // 重新绘制该字段文本确保在高亮背景之上 pDC-SetTextColor(RGB(0, 0, 0)); pDC-TextOut(rectField.left, yPos, strField); } pDC-SelectObject(pOldFont); }注意事项字段高亮的计算是整个文本绘制中最繁琐的部分。你需要根据控件的当前格式通过GetFormat获取如“yyyy年MM月dd日”来解析出年、月、日等字段的占位符位置并映射到实际显示的字符串上。这涉及到字符串解析和文本度量务必仔细处理否则高亮区域会错位。4. 交互与状态管理让控件“活”起来一个只有静态外观的控件是没用的。我们必须处理焦点、鼠标悬停、按下、禁用等状态。4.1 状态变量与鼠标跟踪在类中添加状态变量class CCustomDateTimeCtrl : public CDateTimeCtrl { // ... private: BOOL m_bMouseOver; // 鼠标是否在控件上 BOOL m_bButtonPressed; // 下拉按钮是否被按下 int m_nFocusField; // 当前焦点字段索引 (0年,1月...) CRect m_rectDropDown; // 下拉按钮的屏幕区域 // ... };通过WM_MOUSEMOVE,WM_MOUSELEAVE消息来实现悬停效果BEGIN_MESSAGE_MAP(CCustomDateTimeCtrl, CDateTimeCtrl) ON_WM_MOUSEMOVE() ON_WM_MOUSELEAVE() END_MESSAGE_MAP() void CCustomDateTimeCtrl::OnMouseMove(UINT nFlags, CPoint point) { if (!m_bMouseOver) { m_bMouseOver TRUE; TRACKMOUSEEVENT tme { sizeof(TRACKMOUSEEVENT), TME_LEAVE, m_hWnd, 0 }; ::TrackMouseEvent(tme); // 请求接收鼠标离开消息 Invalidate(); // 重绘可能显示悬停状态 } // 判断鼠标是否在下拉按钮上更新m_bButtonPressed状态等 CRect rectButton; GetClientRect(rectButton); rectButton.left rectButton.right - 20; BOOL bOverButton rectButton.PtInRect(point); // ... 根据bOverButton更新状态并重绘相应区域 CDateTimeCtrl::OnMouseMove(nFlags, point); } void CCustomDateTimeCtrl::OnMouseLeave() { m_bMouseOver FALSE; m_bButtonPressed FALSE; // 鼠标离开按钮按下状态也应重置 Invalidate(); CDateTimeCtrl::OnMouseLeave(); }在DrawDropDownButton函数中根据m_bMouseOver和m_bButtonPressed状态来绘制按钮的不同外观正常、悬停、按下。4.2 键盘导航与字段编辑实现键盘导航是提升用户体验的关键。我们需要在OnKeyDown中处理方向键和数字键。void CCustomDateTimeCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { SYSTEMTIME sysTime; if (GetTime(sysTime) ! GDT_VALID) { GetLocalTime(sysTime); // 获取当前时间作为默认值 } BOOL bModified FALSE; switch (nChar) { case VK_LEFT: // 焦点左移一个字段 m_nFocusField (m_nFocusField - 1 3) % 3; // 假设3个字段年、月、日 Invalidate(); break; case VK_RIGHT: // 焦点右移一个字段 m_nFocusField (m_nFocusField 1) % 3; Invalidate(); break; case VK_UP: // 当前字段值增加 bModified TRUE; switch (m_nFocusField) { case 0: sysTime.wYear; break; case 1: sysTime.wMonth (sysTime.wMonth % 12) 1; break; case 2: // 日期的增减需要考虑月份天数这里简化处理 sysTime.wDay min(sysTime.wDay 1, 31); break; } break; case VK_DOWN: // 当前字段值减少 bModified TRUE; switch (m_nFocusField) { case 0: sysTime.wYear max(sysTime.wYear - 1, 1601); break; // 最小年份 case 1: sysTime.wMonth (sysTime.wMonth - 2 12) % 12 1; break; case 2: sysTime.wDay max(sysTime.wDay - 1, 1); break; } break; case VK_F4: // 模拟按下下拉按钮 // ... 发送 DTN_DROPDOWN 通知 break; default: // 处理数字键直接输入更复杂需要缓存输入字符并组合成字段值 if (nChar 0 nChar 9) { // 进入数字输入模式更新对应字段的值 // 例如焦点在“月”上按‘5’则月份变为5 // 这通常需要一个输入缓冲区和一个定时器用于处理连续输入如‘12’表示12月 } break; } if (bModified) { // 验证并设置新的时间 // 注意需要检查日期有效性如2月30日 if (IsValidSystemTime(sysTime)) { SetTime(sysTime); Invalidate(); // 重绘显示新时间 // 发送时间改变的通知消息给父窗口 NMHDR nmhdr { m_hWnd, GetDlgCtrlID(), DTN_DATETIMECHANGE }; GetParent()-SendMessage(WM_NOTIFY, GetDlgCtrlID(), (LPARAM)nmhdr); } } else { CDateTimeCtrl::OnKeyDown(nChar, nRepCnt, nFlags); } }踩坑记录键盘直接输入数字是最难处理的部分。原生控件会有一个“字段编辑”模式比如焦点在月份上你按‘1’它显示‘1’紧接着按‘2’它会把‘1’和‘2’组合成‘12’作为月份。自己实现这个逻辑需要维护一个输入缓冲区和一个定时器用于判断两次按键的间隔以确定是输入新数字还是组合。对于大多数自绘控件如果对键盘输入要求不高可以暂时不实现这个复杂逻辑或者只响应上下箭头和弹出下拉列表。5. 通知消息与父窗口通信即使完全自绘控件也需要与父窗口通信比如时间改变、下拉请求等。MFC的日期时间控件通过WM_NOTIFY消息发送通知。我们需要在适当的时候发送这些通知以保持与原控件行为兼容。时间改变 (DTN_DATETIMECHANGE): 当用户通过键盘、下拉日历选择了新时间后调用SetTime并发送此通知。下拉请求 (DTN_DROPDOWN): 当用户点击下拉按钮时发送。父窗口可以在此消息处理中弹出自己的自定义日历控件。关闭下拉 (DTN_CLOSEUP): 当下拉日历关闭时发送。发送通知的示例// 在点击下拉按钮的区域时 void CCustomDateTimeCtrl::OnLButtonDown(UINT nFlags, CPoint point) { CRect rectButton; GetClientRect(rectButton); rectButton.left rectButton.right - 20; if (rectButton.PtInRect(point)) { m_bButtonPressed TRUE; InvalidateRect(rectButton); // 重绘按钮为按下状态 // 发送下拉通知 NMHDR nmhdr; nmhdr.hwndFrom m_hWnd; nmhdr.idFrom GetDlgCtrlID(); nmhdr.code DTN_DROPDOWN; GetParent()-SendMessage(WM_NOTIFY, (WPARAM)nmhdr.idFrom, (LPARAM)nmhdr); // 注意这里不会自动弹出日历需要父窗口响应此通知并显示。 return; } // ... 处理其他区域点击 }在父窗口通常是对话框中你需要处理这些通知BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx) ON_NOTIFY(DTN_DATETIMECHANGE, IDC_DATETIMEPICKER1, CMyDialog::OnDtnDatetimechangeDatetimepicker1) ON_NOTIFY(DTN_DROPDOWN, IDC_DATETIMEPICKER1, CMyDialog::OnDtnDropdownDatetimepicker1) END_MESSAGE_MAP() void CMyDialog::OnDtnDropdownDatetimepicker1(NMHDR *pNMHDR, LRESULT *pResult) { LPNMDATETIMECHANGE pDTChange reinterpret_castLPNMDATETIMECHANGE(pNMHDR); // 在这里创建并显示你自己的自定义月份日历控件CMyCalendarCtrl // 定位到时间控件附近弹出 // 当日历控件选择日期后设置回时间控件并发送 DTN_DATETIMECHANGE *pResult 0; }6. 常见问题与调试技巧实录在实现完全自绘时间控件的过程中我遇到了不少典型问题这里列出来供大家参考。6.1 控件不显示或显示异常问题控件创建后一片空白或者只显示一部分。排查检查OnPaint是否被调用在OnPaint开始处设置断点或输出调试信息。如果没调用检查消息映射ON_WM_PAINT()是否正确添加以及控件窗口是否可见。检查OnEraseBkgnd确保OnEraseBkgnd返回TRUE否则系统会先擦除背景可能导致闪烁或绘制内容被清除。检查双缓冲位图尺寸确保创建的内存位图大小与控件客户区 (rectClient) 一致。如果控件大小可变需要在OnSize消息中重建位图。检查GDI资源泄漏每次OnPaint中创建的CPen,CBrush,CFont,CBitmap等GDI对象必须在函数返回前还原并删除。使用SelectObject保存旧对象最后选回。对于GDI的Graphics对象它会在析构时自动释放资源但Brush和Pen也需要及时销毁。6.2 鼠标和键盘消息无响应问题点击控件没反应键盘操作无效。排查检查消息映射确认ON_WM_LBUTTONDOWN(),ON_WM_KEYDOWN()等已添加到消息映射表中。检查控件焦点控件必须获得焦点才能接收键盘消息。在OnLButtonDown中调用SetFocus()。确保父窗口或兄弟控件没有抢走焦点。消息被拦截如果你重写了父窗口的PreTranslateMessage确保没有过滤掉发送给该控件的消息。自定义的点击判断逻辑错误PtInRect判断的区域 (m_rectDropDown) 计算错误导致点击了却没进入处理分支。用调试器输出点击坐标和区域坐标进行对比。6.3 下拉日历与控件集成问题问题点击下拉按钮自定义日历弹出位置不对或者选择日期后无法回传。解决方案精确定位使用GetWindowRect获取时间控件在屏幕上的矩形然后计算下拉日历应该弹出的位置通常在控件左下方。CRect rectCtrl; GetWindowRect(rectCtrl); rectCtrl.top rectCtrl.bottom; // 日历显示在控件下方 rectCtrl.bottom rectCtrl.top 200; // 日历高度模态与非模态简单起见可以将自定义日历控件做成一个弹出式窗口 (WS_POPUP)并在显示时设置为模态循环虽然不严格模态但能捕获焦点。更优雅的方式是做成非模态但要处理好焦点切换和消息路由。数据回传在自定义日历控件的选择事件中获取选中的日期调用时间控件的SetTime方法并发送DTN_DATETIMECHANGE通知然后销毁或隐藏日历窗口。6.4 性能问题与闪烁问题控件重绘时感觉卡顿或有明显闪烁。优化双缓冲是必须的如前所述这是消除闪烁最基本有效的方法。避免无效重绘在OnPaint开始时获取需要重绘的区域 (dc.GetClipBox())只绘制该区域内的内容。对于复杂控件这能显著提升性能。缓存静态资源例如圆角路径、渐变画刷GDI、字体等如果每次重绘都创建开销很大。可以在类的构造函数或OnCreate中创建一次在析构函数中销毁。减少复杂的GDI操作GDI虽然强大但比GDI慢。对于简单的颜色填充和文本用GDI的FillSolidRect和TextOut更快。只在需要平滑渐变、圆角、图像混合时使用GDI。6.5 与原生控件功能对齐的挑战问题自绘控件的行为和原生控件有细微差别比如右键菜单、辅助功能屏幕阅读器、拖放等。建议明确需求边界完全自绘意味着你要负责所有交互。在项目初期就要和产品经理明确是否需要支持所有这些边缘功能。很多时候对于内部工具或特定场景的软件不需要完全对等。渐进式实现先实现核心的显示、点击下拉、键盘增减功能。确保稳定后再根据反馈逐步添加右键菜单、工具提示等。测试覆盖对自定义控件进行充分的测试包括不同Windows主题下、高DPI缩放下的显示以及各种键盘和鼠标操作组合。实现一个完全自绘的MFC时间控件是一项细致且富有挑战性的工作它要求开发者对Windows GUI编程有比较深的理解。但一旦完成你将获得一个外观完全可控、能与任何现代化UI设计无缝融合的强力组件。更重要的是这套自绘的框架和经验可以迁移到任何其他需要深度定制化的MFC控件开发中价值远超一个时间控件本身。

相关新闻