MFC界面美化实战:从360SafeDemo看传统软件现代化改造

MFC界面美化实战:从360SafeDemo看传统软件现代化改造 简介在Windows桌面应用开发领域图形设备接口GDI和GDI是构建用户界面的基础技术。GDI提供了底层的绘图能力而GDI在此基础上扩展了渐变、抗锯齿和Alpha混合等高级特性为界面美化提供了技术支撑。这些技术的核心价值在于它们允许开发者在保持应用稳定性的前提下对用户界面进行深度定制从而提升软件的整体视觉体验和用户满意度。在工业控制、医疗设备和仪器仪表等存量巨大的传统行业软件中许多系统仍基于MFC框架开发其界面往往停留在早期Windows风格。通过GDI/GDI技术对MFC界面进行现代化改造成为了一种经济高效的技术路径既能避免重写带来的高昂成本和风险又能显著提升产品的市场竞争力。本文以经典的360SafeDemo项目为样本深入解析了MFC界面美化的完整方案涵盖了自定义窗口框架、控件自绘、背景处理等核心环节并针对界面闪烁、内存泄漏等常见问题提供了实用的解决方案。1. 项目概述从“360SafeDemo”看MFC界面美化的核心价值看到“360SafeDemo.zip”这个文件名很多老C/MFC开发者可能会心一笑。这大概率是一个参考了早期360安全卫士界面风格的MFC演示项目。在WinForm、WPF乃至各种现代Web前端框架大行其道的今天为什么我们还要讨论一个看似“古老”的MFC对话框美化项目原因很简单存量巨大需求真实。大量工业控制、仪器仪表、医疗设备的上位机软件其核心框架依然是基于MFC开发的。这些软件功能稳定、逻辑复杂但界面往往停留在Windows 95/98的风格与现代化的用户体验格格不入。直接重写成本高昂、风险巨大因此在原有MFC框架上进行深度界面美化成为提升产品竞争力最经济、最可行的技术路径。这个“360SafeDemo”项目正是这一技术路径的一个典型样本它展示了如何通过一套相对完整的美化方案让一个传统的MFC对话框应用程序脱胎换骨拥有接近现代软件的视觉体验。本文将深入拆解这类项目的核心技术点、实现思路、实操细节以及那些官方文档不会告诉你的“坑”目标是让你不仅能复现一个漂亮的界面更能掌握一套适用于任何MFC项目的美化方法论。2. 项目整体设计与思路拆解2.1 核心需求解析我们到底要美化什么拿到一个MFC对话框项目进行美化不能上来就盲目画图。首先需要系统性地分析界面构成明确美化目标。一个典型的MFC对话框应用程序其视觉元素可以分解为以下几个层次窗口框架Frame包括对话框本身的标题栏、边框、最小化/最大化/关闭按钮。这是美化的第一印象目标通常是实现自定义标题栏无边框或自定义绘制边框、更换按钮样式。背景与底色整个对话框客户区的背景。传统MFC是灰色或系统色美化时需要替换为渐变色、纯色或纹理图片这是奠定整体视觉基调的关键。控件Controls这是美化的重头戏包括但不限于按钮CButton需要支持各种状态正常、悬浮、按下、禁用的图片切换或GDI绘制。静态文本CStatic改变字体、颜色、阴影可能还需要支持背景透明。**编辑框CEdit、组合框CComboBox、列表框CListCtrl**等需要自定义边框、背景色、文字颜色甚至完全自绘其下拉列表、滚动条等子部件。标签页CTabCtrl美化标签头可能改为圆角、彩色并自定义每个页面的背景。进度条CProgressCtrl用自定义位图或GDI绘制来替代系统默认的“槽”和“滑块”。布局与间距原始的MFC对话框编辑器生成的布局往往比较紧凑、呆板。美化时需要重新调整控件间距运用对齐、分组例如用Group Box或自绘分隔线等设计原则让界面更有呼吸感和层次感。“360SafeDemo”这类项目其核心价值在于它提供了一套相对完整的、可复用的解决方案来应对上述所有层次的美化需求而不是零散地解决一两个控件的问题。2.2 技术方案选型GDI、GDI还是第三方库明确了美化目标接下来就要选择技术武器。MFC界面美化主要有三条技术路径各有优劣纯GDIGraphics Device Interface这是MFC的“原配”。通过处理WM_PAINT消息在OnPaint()或OnDrawItem()等函数中使用CDC设备上下文对象进行绘图。你可以画线、矩形、椭圆贴位图BitBlt,StretchBlt。优点最底层性能高与MFC结合最紧密无任何外部依赖。缺点功能原始实现圆角、渐变、Alpha混合半透明非常困难甚至无法实现代码复杂度高。对于复杂的皮肤效果需要大量精细的位图资源。GDI微软在GDI之上提供的更高级的二维图形API。它提供了丰富的类来处理渐变画刷、路径、图像缩放、抗锯齿和Alpha通道。优点能轻松实现渐变、圆角、半透明、高质量图像缩放等现代UI效果。是平衡效果和复杂度的较好选择。缺点需要额外初始化库GdiplusStartup性能略低于纯GDI对于极复杂的动态效果如动画支持较弱。第三方界面库如BCGControlBar Pro、Xtreme Toolkit、Skin等商业库或开源的DuiLib、SOUI等DirectUI框架。优点功能极其强大提供海量现成的美化控件和主题开发效率最高效果也最炫。缺点商业库需要付费有许可风险第三方库会引入额外的依赖和复杂性可能增加软件体积与现有MFC代码的集成可能需要一定改造学习有成本。“360SafeDemo”项目的典型选择分析根据其名称和常见实现推断它很可能采用的是“GDI为主GDI为辅重度依赖位图资源”的方案。这是因为历史原因这类项目诞生时GDI可能还未普及或开发者对其不熟GDI是更稳妥的选择。性能考虑对于安全类软件界面响应速度是重要指标纯GDI在绘制大量标准控件时效率有保障。效果实现通过精心制作的状态位图正常、悬停、按下等用GDI的贴图函数可以实现非常精美的按钮、背景效果规避了GDI在绘制上的短板。对于简单的颜色和字体变化则直接使用GDI函数。注意如果你是新启动一个MFC美化项目我个人强烈建议以GDI作为核心技术。它足以应对90%的美化需求且代码比纯GDI位图方案更简洁、更易于维护和调整比如改个颜色只需改代码而不用重新做图。第三方库适合大型、对界面要求极高且预算充足的项目。3. 核心细节解析与实操要点3.1 自定义窗口框架实现无边框与自定义标题栏这是美化第一步也是基础。系统默认的标题栏风格无法自定义。实现原理移除对话框的原始标题栏和边框然后在客户区顶部自己绘制一个“假”的标题栏并处理相关的窗口消息。修改对话框属性在资源编辑器中将对话框的Border属性改为None或者通过代码在OnInitDialog()中调用ModifyStyle(WS_CAPTION, 0)来移除标题栏。实现窗口拖动没有了标题栏窗口就无法拖动了。我们需要在鼠标左键按下时模拟标题栏拖动。通常是在对话框的OnLButtonDown消息处理函数中判断点击位置是否在我们自绘的标题栏区域如果是则调用PostMessage(WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(point.x, point.y))。这个技巧是向系统发送一个“非客户区鼠标按下”的消息并指定为HTCAPTION系统就会接管后续的拖动操作。自绘标题栏在OnPaint()中绘制标题栏的背景、图标、标题文字。这通常使用GDI的Graphics对象绘制渐变矩形用DrawString绘制文字。自定义系统按钮最小化、最大化/还原、关闭按钮需要自己用CButton派生类或者自绘来实现。为每个按钮准备多状态位图正常、悬浮、按下并在相应的鼠标消息WM_MOUSEMOVE,WM_LBUTTONDOWN,WM_LBUTTONUP中更新状态并重绘。实操心得双缓冲绘图在自绘标题栏和背景时务必使用双缓冲技术。直接在窗口DC上绘图在频繁重绘时会出现严重闪烁。创建一个兼容的内存DC先在内存中画好所有内容再一次性BitBlt到窗口DC上。考虑DPI缩放如果你的软件需要支持高DPI显示器那么标题栏的高度、按钮的大小、字体大小都不能写死像素值。应该根据系统的DPI缩放因子进行动态计算。可以使用GetDeviceCaps(hDC, LOGPIXELSY)获取DPI然后进行比例换算。关闭按钮的特殊处理自定义关闭按钮时除了绘制其点击事件应调用OnCancel()或PostMessage(WM_CLOSE, 0, 0)来关闭窗口。如果需要拦截关闭进行确认应重写OnClose事件。3.2 控件美化从CButton派生的自绘按钮类控件美化是核心。以最常用的按钮为例创建一个自绘按钮类是通用做法。实现原理从MFC的CButton类派生一个新类例如CSkinButton。然后重写两个关键虚函数PreSubclassWindow(): 在子类化窗口之前调用这里可以设置按钮的Owner Draw属性ModifyStyle(0, BS_OWNERDRAW)告诉系统这个按钮由我们自己绘制。DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct): 这是自绘的核心。系统在需要绘制按钮时会调用此函数。参数lpDrawItemStruct包含了绘制的DC、按钮的状态是否按下、获得焦点、禁用等、以及绘制区域等信息。在DrawItem函数中我们的逻辑是void CSkinButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC* pDC CDC::FromHandle(lpDrawItemStruct-hDC); CRect rect lpDrawItemStruct-rcItem; UINT state lpDrawItemStruct-itemState; // 1. 根据状态state决定使用哪套图片或颜色 // 例如if (state ODS_DISABLED) { /* 禁用态 */ } // else if (state ODS_SELECTED) { /* 按下态 */ } // else if (m_bHover) { /* 悬浮态 */ } // m_bHover需在WM_MOUSEMOVE中设置 // else { /* 正常态 */ } // 2. 绘制按钮背景可以是贴图或GDI渐变 // 例如使用GDI绘制圆角矩形渐变背景 // 或者将预加载的位图(m_bmpNormal等)选入内存DC然后BitBlt到pDC // 3. 绘制按钮文字 // 根据状态设置文字颜色使用pDC-DrawText或GDI的DrawString绘制 // 注意计算文字位置使其居中 // 4. 如果按钮有焦点state ODS_FOCUS可以绘制一个虚线框或其他提示 }为了支持悬浮状态MouseHover你还需要在按钮类中处理WM_MOUSEMOVE和WM_MOUSELEAVE消息来设置和清除一个m_bHover标志并触发重绘。注意事项资源管理如果使用位图务必在类中妥善管理位图资源的加载LoadBitmap和释放DeleteObject避免资源泄漏。推荐在PreSubclassWindow中加载在析构函数中释放。状态同步按钮的禁用状态EnableWindow(FALSE)会通过ODS_DISABLED标志反映在DrawItem中。但像BN_CLICKED这样的通知消息在自绘按钮中依然会正常发送父对话框可以正常处理ON_BN_CLICKED事件。通用性设计一个好的CSkinButton类应该提供接口允许外部设置不同状态的图片句柄HBITMAP或颜色值而不是把资源路径写死在类内部。这样同一个类可以用于界面中不同风格的按钮。3.3 非客户区与背景美化处理WM_ERASEBKGND和WM_CTLCOLOR对话框背景和静态文本等控件的背景色、文字色通常通过处理WM_ERASEBKGND和WM_CTLCOLOR消息来实现。WM_ERASEBKGND当窗口背景需要擦除时发送。如果我们想绘制自定义背景如图片或渐变应该在这里进行。BOOL CMyDlg::OnEraseBkgnd(CDC* pDC) { CRect rect; GetClientRect(rect); // 使用GDI绘制渐变背景到pDC-m_hDC // 或者将背景位图拉伸绘制到rect区域 // ... return TRUE; // 返回TRUE表示我们已经处理了背景擦除系统不再进行默认处理 }返回TRUE是关键它阻止了系统用默认背景色通常是灰色去填充避免了闪烁。WM_CTLCOLOR当一个子控件如CStatic,CEdit,CListCtrl即将被绘制时其父窗口会收到此消息。我们可以在这里为特定的控件设置背景刷子、文字颜色。HBRUSH CMyDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); UINT nID pWnd-GetDlgCtrlID(); // 根据控件ID进行差异化处理 if (nID IDC_STATIC_TITLE) { pDC-SetTextColor(RGB(255, 255, 255)); // 白色文字 pDC-SetBkMode(TRANSPARENT); // 透明背景模式 // 返回一个空刷子(NULL_BRUSH)表示背景透明由OnEraseBkgnd绘制 return (HBRUSH)GetStockObject(NULL_BRUSH); } else if (nCTlColor CTLCOLOR_EDIT) { // 对所有编辑框设置背景色和文字色 pDC-SetTextColor(RGB(0, 0, 0)); pDC-SetBkColor(RGB(240, 240, 240)); // 创建一个灰色的背景刷需要在类成员变量中保存并最终删除 if (m_brEditBack.GetSafeHandle() NULL) m_brEditBack.CreateSolidBrush(RGB(240, 240, 240)); return (HBRUSH)m_brEditBack; } return hbr; }重要在OnCtlColor中返回的刷子句柄其生命周期必须足够长通常是对话框类的一个成员变量CBrush不能是局部变量否则会导致程序崩溃。4. 实操过程与核心环节实现4.1 搭建基础美化框架从零创建一个“360风格”对话框让我们一步步实现一个简化版的美化对话框核心是自定义标题栏和背景。步骤1创建MFC对话框项目使用Visual Studio创建一个基于对话框的MFC应用程序命名为“SkinDemo”。步骤2准备资源在资源文件(.rc)中导入或创建所需的位图资源IDB_BG_MAIN: 对话框主背景图或使用GDI绘制渐变。IDB_BTN_CLOSE_NORMAL,IDB_BTN_CLOSE_HOVER,IDB_BTN_CLOSE_DOWN: 关闭按钮的三态图。类似地准备最小化、最大化按钮的位图。准备一个图标IDI_MAIN作为标题栏图标。步骤3实现无边框与拖动在对话框类的头文件中声明消息映射和函数// SkinDemoDlg.h class CSkinDemoDlg : public CDialogEx { // ... protected: HICON m_hIcon; BOOL m_bMaximized; // 记录是否最大化 CPoint m_ptOldPos; // 用于拖动计算 CRect m_rcCaption; // 标题栏区域 CRect m_rcCloseBtn; // 关闭按钮区域 CRect m_rcMinBtn; // 最小化按钮区域 // ... afx_msg void OnPaint(); afx_msg HCURSOR OnQueryDragIcon(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnSize(UINT nType, int cx, int cy); // ... };在OnInitDialog()中移除边框并初始化标题栏区域BOOL CSkinDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // ... 其他初始化 // 移除对话框默认边框和标题栏 ModifyStyle(WS_CAPTION | WS_THICKFRAME, 0, SWP_FRAMECHANGED); // 也可以使用SetWindowLong(m_hWnd, GWL_STYLE, GetStyle() ~(WS_CAPTION | WS_THICKFRAME)); // 初始化标题栏区域假设标题栏高度为30像素 GetClientRect(m_rcCaption); m_rcCaption.bottom m_rcCaption.top 30; // 初始化按钮区域位于标题栏右侧 int btnWidth 24, btnHeight 24; int margin 5; m_rcCloseBtn.SetRect(m_rcCaption.right - margin - btnWidth, m_rcCaption.top margin, m_rcCaption.right - margin, m_rcCaption.top margin btnHeight); m_rcMinBtn.SetRect(m_rcCloseBtn.left - margin - btnWidth, m_rcCaption.top margin, m_rcCloseBtn.left - margin, m_rcCaption.top margin btnHeight); return TRUE; }实现拖动功能void CSkinDemoDlg::OnLButtonDown(UINT nFlags, CPoint point) { // 如果点击点在自定义标题栏区域且不在系统按钮上则触发拖动 if (m_rcCaption.PtInRect(point) !m_rcCloseBtn.PtInRect(point) !m_rcMinBtn.PtInRect(point)) { // 保存点击位置相对于窗口客户区 m_ptOldPos point; ClientToScreen(m_ptOldPos); // 转换为屏幕坐标 // 发送非客户区消息模拟标题栏拖动 PostMessage(WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(m_ptOldPos.x, m_ptOldPos.y)); } CDialogEx::OnLButtonDown(nFlags, point); }步骤4绘制标题栏和背景在OnPaint()中绘制void CSkinDemoDlg::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(rect); // 1. 双缓冲开始 CDC memDC; CBitmap memBitmap; memDC.CreateCompatibleDC(dc); memBitmap.CreateCompatibleBitmap(dc, rect.Width(), rect.Height()); CBitmap* pOldBitmap memDC.SelectObject(memBitmap); // 2. 绘制背景这里用GDI示例 Graphics graphics(memDC.GetSafeHdc()); LinearGradientBrush bgBrush(Point(0, 0), Point(0, rect.Height()), Color(255, 40, 90, 160), // 顶部颜色 Color(255, 10, 30, 80)); // 底部颜色 graphics.FillRectangle(bgBrush, 0, 0, rect.Width(), rect.Height()); // 3. 绘制标题栏背景一个深色矩形 SolidBrush titleBrush(Color(255, 25, 60, 120)); graphics.FillRectangle(titleBrush, m_rcCaption.left, m_rcCaption.top, m_rcCaption.Width(), m_rcCaption.Height()); // 4. 绘制标题文字 FontFamily fontFamily(L微软雅黑); Font font(fontFamily, 12, FontStyleBold, UnitPixel); SolidBrush textBrush(Color(255, 255, 255, 255)); CString strTitle; GetWindowText(strTitle); graphics.DrawString(strTitle, -1, font, PointF(10, 8), textBrush); // 5. 绘制系统按钮根据m_btnCloseState, m_btnMinState状态贴图 // 这里简化直接画两个矩形框代表按钮 Pen whitePen(Color(255, 255, 255, 255), 2); // 最小化按钮一个横杠 graphics.DrawLine(whitePen, m_rcMinBtn.left 6, m_rcMinBtn.top 12, m_rcMinBtn.right - 6, m_rcMinBtn.top 12); // 关闭按钮一个叉 graphics.DrawLine(whitePen, m_rcCloseBtn.left 6, m_rcCloseBtn.top 6, m_rcCloseBtn.right - 6, m_rcCloseBtn.bottom - 6); graphics.DrawLine(whitePen, m_rcCloseBtn.left 6, m_rcCloseBtn.bottom - 6, m_rcCloseBtn.right - 6, m_rcCloseBtn.top 6); // 6. 双缓冲结束将内存DC内容拷贝到屏幕DC dc.BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); }OnEraseBkgnd直接返回TRUE因为我们已经在OnPaint中绘制了全部背景BOOL CSkinDemoDlg::OnEraseBkgnd(CDC* pDC) { return TRUE; }步骤5处理按钮点击和悬浮效果这需要在OnMouseMove,OnLButtonUp中判断鼠标位置更新按钮状态正常、悬浮、按下并调用InvalidateRect来重绘特定区域以实现动态效果。代码较长其逻辑是记录每个按钮的当前状态在鼠标移动时检查是否进入按钮区域并更新状态在点击时执行相应操作如关闭窗口PostMessage(WM_CLOSE)。4.2 集成自绘控件将CSkinButton应用到对话框将CSkinButton类文件.h和.cpp加入项目。在资源编辑器中将需要美化的按钮的“Owner Draw”属性设置为True。或者在OnInitDialog中通过代码动态子类化// 假设IDC_BTN_TEST是对话框上的一个按钮ID m_btnTest.SubclassDlgItem(IDC_BTN_TEST, this);为CSkinButton对象设置属性。可以在对话框类中为CSkinButton类型的成员变量m_btnTest设置图片或颜色。BOOL CSkinDemoDlg::OnInitDialog() { // ... 其他初始化 m_btnTest.SubclassDlgItem(IDC_BTN_TEST, this); m_btnTest.SetColors(RGB(70, 130, 230), RGB(50, 110, 210), RGB(255, 255, 255)); // 设置正常、按下背景色和文字色 m_btnTest.SetRoundRect(5); // 设置圆角半径 // 或者使用位图m_btnTest.SetBitmaps(IDB_BTN_NORMAL, IDB_BTN_HOVER, IDB_BTN_DOWN, IDB_BTN_DISABLED); return TRUE; }处理按钮事件。自绘按钮的点击事件BN_CLICKED与普通按钮完全一样在消息映射中绑定即可。4.3 处理静态文本和编辑框背景透明对于静态文本(CStatic)使其背景透明以融入我们自定义的对话框背景是最常见的需求。在资源编辑器中将静态文本控件的“Transparent”属性勾选。但这有时不生效。更可靠的方法是在OnCtlColor中处理HBRUSH CSkinDemoDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); if (nCtlColor CTLCOLOR_STATIC) { // 使静态文本背景透明 pDC-SetBkMode(TRANSPARENT); // 设置文字颜色例如白色 pDC-SetTextColor(RGB(255, 255, 255)); // 返回空刷子表示背景透明 return (HBRUSH)GetStockObject(NULL_BRUSH); } // 对于编辑框可以设置特定的背景色 else if (nCtlColor CTLCOLOR_EDIT) { pDC-SetBkColor(RGB(255, 255, 255)); // 白色背景 pDC-SetTextColor(RGB(0, 0, 0)); // 黑色文字 if (m_brEdit.GetSafeHandle() NULL) m_brEdit.CreateSolidBrush(RGB(255, 255, 255)); return (HBRUSH)m_brEdit; } return hbr; }注意返回的HBRUSH用于填充控件的背景。对于静态文本返回NULL_BRUSH意味着不填充背景就是父窗口对话框的背景从而实现了透明效果。对于编辑框我们返回一个实心刷子来提供白色背景。5. 常见问题与排查技巧实录在MFC界面美化过程中你会遇到各种各样奇怪的问题。下面是我踩过的一些坑和解决方案。5.1 界面闪烁问题这是自绘界面最常见的问题。画面在更新时出现明显的闪烁或撕裂感。原因通常是因为在OnPaint中直接绘图到屏幕DC而Windows在WM_PAINT之前会先发送WM_ERASEBKGND用背景色擦除窗口如果背景色与绘制内容不同就会先看到背景色闪一下再看到绘制内容。解决方案双缓冲绘图如前文OnPaint示例所示所有绘制操作先在内存DC中进行最后一次性BitBlt到屏幕。这是解决闪烁的根本方法。处理WM_ERASEBKGND重写此消息处理函数直接返回TRUE。这告诉系统“背景我已经擦过了你别管了”。结合双缓冲效果最佳。使用CMemDC类MFC没有内置的双缓冲类但可以自己封装一个或者使用开源库如CodeProject上的CMemDC。它简化了双缓冲的创建和销毁过程。只重绘脏区域在OnPaint中通过CPaintDC dc(this)的dc.m_ps.rcPaint获取需要重绘的区域脏矩形只更新这个区域能极大提升性能减少闪烁。但对于复杂的、各部分关联的界面实现起来较麻烦。5.2 控件刷新不正常或残留例如拖动窗口后原来被对话框遮挡的桌面内容残留在客户区或者控件移动后原位置有图像残留。原因Windows的绘图消息处理机制问题。当窗口的一部分失效需要重画时系统会产生WM_PAINT消息。但如果我们的绘制逻辑没有覆盖整个失效区域或者控件自身没有正确重绘就会残留。解决方案确保OnPaint绘制整个客户区即使只更新部分区域在双缓冲中我们也应该先绘制整个背景再绘制其他内容。这样当BitBlt时整个区域都是新的内容。在移动或调整控件大小时手动触发重绘例如在OnSize消息处理函数中不要只调用Invalidate()这会使整个窗口重绘可能引起闪烁。更好的做法是调用InvalidateRect指定需要更新的具体区域。void CSkinDemoDlg::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); // 重新计算标题栏和按钮区域 RecalcLayout(); // 只重绘标题栏区域避免整个窗口闪烁 InvalidateRect(m_rcCaption, FALSE); }对于自绘控件确保DrawItem覆盖整个rcItem区域在自绘控件的DrawItem函数中给定的rect就是需要绘制的整个区域必须用背景色或背景图完全填充这个矩形不能只画一部分。5.3 内存泄漏与资源管理美化项目大量使用GDI对象画笔、画刷、字体、位图和GDI对象Graphics,Brush,Pen等。管理不当会导致GDI泄漏表现为程序运行一段时间后界面卡顿、闪烁甚至崩溃。GDI对象泄漏排查基本原则对于通过CreatePen,CreateSolidBrush,LoadBitmap等函数创建的GDI对象必须使用DeleteObject删除。对于通过GetStockObject获取的系统对象不要删除。MFC封装类尽量使用MFC的封装类如CPen,CBrush,CFont,CBitmap。它们在其析构函数中会自动调用DeleteObject。但要注意如果将GDI对象选入DC必须在用完后选出否则析构时可能会失败因为对象仍被DC使用。{ CPen newPen(PS_SOLID, 1, RGB(0,0,0)); CPen* pOldPen pDC-SelectObject(newPen); // 使用pDC画图... pDC-SelectObject(pOldPen); // 用完一定要选回旧对象 } // newPen离开作用域自动析构GDI对象管理GDI对象位于Gdiplus命名空间是C类遵循RAII原则。但GdiplusStartup和GdiplusShutdown必须成对调用。通常在整个应用程序初始化时调用Startup退出时调用Shutdown。一个常见的坑在OnCtlColor中返回自定义的画刷。这个画刷必须长期存在直到对话框销毁。因此应该在对话框类中声明一个成员变量CBrush m_brEdit;在OnInitDialog或首次需要时创建它并在对话框析构时由MFC自动清理。绝对不能在OnCtlColor函数内部创建局部CBrush并返回其句柄因为函数结束后局部对象被销毁返回的句柄就变成了野指针会导致不可预知的崩溃。5.4 与现有业务代码的兼容性问题美化一个已有的大型MFC项目时最大的挑战不是技术而是如何在不破坏原有功能逻辑的前提下平滑地集成美化代码。逐步替换分而治之不要试图一次性美化所有界面。从一个主对话框开始成功后再推广到其他对话框。为每个需要美化的对话框创建一个派生类如CMySkinDlg将美化代码放在这里而不是直接修改原对话框类。使用子类化Subclass对于标准控件的美化优先使用子类化技术SubclassWindow或SubclassDlgItem。这允许你创建一个新的控件类如CSkinButton来“接管”原有控件的窗口过程从而自定义其行为和外貌而无需修改创建该控件的原始代码。注意消息处理顺序当你重写了OnPaint,OnEraseBkgnd,OnCtlColor等消息后基类如CDialogEx的默认处理就被覆盖了。有时基类的默认处理完成了一些必要工作比如为某些控件设置默认颜色。一个安全的做法是在自定义处理完成后根据需要选择性地调用基类方法。例如在OnCtlColor中可以先调用基类然后修改其返回的HBRUSH或pDC的属性。测试测试再测试美化后务必对软件的所有功能进行回归测试。特别是键盘Tab键焦点切换、控件的禁用/启用状态、窗口的缩放最大化最小化、在高DPI下的显示等边界情况很容易在美化过程中被忽略。MFC界面美化是一项细致且需要耐心的工作它没有银弹。但一旦你掌握了这套从框架到控件、从原理到实操的方法论并积累了足够的“避坑”经验你就能让那些“老当益壮”的MFC程序焕发新的活力在用户体验上不输于任何现代框架开发的软件。这其中的价值尤其是在维护和升级现有大型项目时是难以估量的。本文还有配套的精品资源点击获取