尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MFC桌面时钟开发:从消息机制到GDI绘制的完整实践

MFC桌面时钟开发:从消息机制到GDI绘制的完整实践 1. 项目概述与核心价值最近在整理一些老项目的代码翻出来一个多年前用MFC写的桌面时钟程序。虽然现在各种桌面美化工具、系统托盘时钟插件层出不穷但这个自己动手从零搭建的MFC时钟其背后的设计思路和实现细节对于理解Windows桌面程序开发、MFC框架的消息机制以及C的面向对象实践依然有着不小的价值。这个项目标题“基于MFC的C时钟程序设计与实现”听起来简单但它绝不仅仅是调用一个GetLocalTime然后显示出来那么简单。它涉及了MFC应用程序框架的初始化、窗口创建、消息映射、定时器管理、图形绘制GDI以及如何优雅地处理界面更新等一系列经典问题。对于正在学习或希望巩固Windows桌面开发、MFC乃至C面向对象GUI编程的朋友来说这是一个非常理想的练手和深入理解的项目。这个程序的核心功能是在桌面窗口上实时、动态地显示当前系统时间。它需要解决几个关键问题如何创建一个无边框或自定义样式的窗口如何以每秒一次的频率甚至更精细去获取并更新时间显示如何将时间数字或指针式表盘美观地绘制在窗口上以及如何让这个程序能够响应用户的一些简单交互比如拖拽移动、右键退出等通过实现这个时钟你将能亲手触摸到MFC这个经典框架的脉搏理解一个Windows程序从出生到运行的生命周期掌握利用C进行图形界面编程的基本方法。无论你是刚学完C语法想找个有成就感的项目练手还是已经有一定基础但想深入Windows编程内部这个项目都能给你带来扎实的收获。2. 整体设计与架构思路拆解在动手写代码之前我们先来拆解一下这个时钟程序的整体架构。一个典型的MFC单文档或多文档应用程序可能过于复杂对于时钟这种小型工具我们选择更轻量的基于对话框的应用程序作为起点。但为了获得更大的自定义界面自由度比如实现圆形表盘、无边框窗口我们最终采用了基于CFrameWnd的普通窗口应用程序结构。这是本项目第一个重要的设计决策舍弃AppWizard生成的对话框模板手动构建主窗口类从而完全掌控窗口的创建、样式和绘制过程。2.1 技术栈与工具选型开发环境Visual Studio 2019/2022。这是开发MFC应用程序最自然、支持最完善的环境。社区版Community完全免费且功能强大。高版本VS对C标准支持更好同时完美兼容MFC。项目类型创建新项目时选择“MFC应用程序”。在应用程序类型中关键的一步是选择**“基于对话框”还是“单个文档”。如前所述为了灵活性我们可以在创建后改造但更清晰的起点是选择“单个文档”然后在向导中取消选中“文档/视图架构支持”**这样生成的就是一个简单的基于CFrameWnd的应用程序它自带一个空的客户区供我们自由绘制。核心MFC类CWinApp应用程序类全局唯一负责程序初始化、消息循环。CFrameWnd我们的主窗口类继承自它来创建应用程序的主窗口。我们将在这里处理窗口消息、绘制时钟。CDC设备上下文与CGdiObject如CPen,CBrush,CFont用于在窗口上进行图形绘制是画出时钟表盘和指针的关键。CTimer实则是Windows定时器通过SetTimer和KillTimerAPI函数结合MFC的消息映射机制来实现。我们将使用它来驱动时间的每秒更新。2.2 程序运行逻辑流程图概念层面程序启动CWinApp派生类的InitInstance()函数被调用。在这里我们创建并显示主窗口CFrameWnd派生类对象。窗口创建主窗口在创建过程中会收到WM_CREATE消息。我们在这里进行初始化工作例如设置窗口样式去掉边框、标题栏、设置窗口初始位置和大小、启动一个定时器例如设置ID为1间隔1000毫秒。消息循环程序进入主消息循环等待并分发消息如鼠标、键盘、定时器消息。定时器触发每隔1000毫秒系统会向我们的窗口发送一个WM_TIMER消息。我们在窗口类中通过ON_WM_TIMER()宏映射这个消息到一个处理函数如OnTimer。时间更新与重绘在OnTimer函数中我们调用GetLocalTime或CTime::GetCurrentTime()获取最新的系统时间。然后最关键的一步调用Invalidate()或InvalidateRect()函数标记窗口的客户区为“无效”需要重画。这会导致Windows发送WM_PAINT消息。绘制时钟WM_PAINT消息被映射到OnPaint函数。在这里我们创建CPaintDC对象进行所有的绘制操作先绘制一个干净的背景可能是纯色也可能是加载的位图然后根据当前时间计算时针、分针、秒针的角度或者直接绘制格式化的数字时间字符串将其画到窗口上。交互处理我们还可以响应WM_LBUTTONDOWN和WM_MOUSEMOVE来实现窗口拖拽响应WM_RBUTTONUP弹出菜单退出程序等。程序退出用户关闭窗口时收到WM_DESTROY消息在OnDestroy中我们需要销毁定时器KillTimer然后调用PostQuitMessage结束消息循环。这个逻辑清晰地将“数据时间获取”、“事件驱动定时器”、“界面呈现绘制”分离开是MFC/Win32程序设计的典型模式。3. 核心实现步骤详解下面我们进入具体的实现环节。我会假设你已经使用VS创建了一个名为“MyClock”的MFC应用程序并选择了“单个文档”且取消了“文档/视图支持”。这样你会得到CMyClockApp应用类和CMainFrame主框架类。我们将主要改造CMainFrame。3.1 窗口样式与外观定制默认的框架窗口有标题栏、菜单栏、边框。对于时钟我们通常想要一个无边框、可拖拽的定制化窗口。这需要在窗口创建时修改样式。在CMainFrame的PreCreateWindow函数中修改CREATESTRUCT结构BOOL CMainFrame::PreCreateWindow(CREATESTRUCT cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; // 移除不需要的样式 cs.style WS_POPUP; // 弹出式窗口无边框 // cs.style WS_OVERLAPPED | WS_SYSMENU; // 如果还想要一个简单的系统菜单有关闭按钮可以用这个 cs.dwExStyle | WS_EX_LAYERED; // 支持分层窗口可用于实现半透明效果高级特性 // 设置窗口初始大小例如一个300x300的正方形窗口用于模拟表盘 cs.cx 300; cs.cy 300; // 如果你想窗口一开始就位于屏幕中央可以在这里计算位置 // 但更常见的做法是在OnCreate中调用CenterWindow() return TRUE; }在CMainFrame::OnCreate函数中进行进一步的初始化int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) -1) return -1; // 1. 设置窗口圆角可选需要Windows API HRGN hRgn CreateRoundRectRgn(0, 0, 300, 300, 20, 20); // 创建圆角区域 SetWindowRgn(hRgn, TRUE); DeleteObject(hRgn); // 2. 将窗口置于屏幕中央 CenterWindow(); // 3. 启动定时器ID为1间隔1000毫秒1秒 SetTimer(1, 1000, NULL); return 0; }注意WS_POPUP样式移除了所有边框和标题栏这意味着窗口将无法通过常规方式拖拽。我们需要自己处理WM_LBUTTONDOWN消息来模拟拖拽这会在后面交互部分实现。3.2 定时器设置与时间更新机制定时器是时钟程序的“心脏”。我们在OnCreate中启动了它。现在需要添加消息映射和处理函数。首先在CMainFrame类的头文件消息映射声明里添加afx_msg void OnTimer(UINT_PTR nIDEvent); afx_msg void OnPaint(); 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 OnRButtonUp(UINT nFlags, CPoint point);在对应的.cpp文件的消息映射表中添加条目BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() ON_WM_TIMER() ON_WM_PAINT() ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() ON_WM_LBUTTONUP() ON_WM_RBUTTONUP() ON_WM_DESTROY() END_MESSAGE_MAP()然后实现OnTimer函数void CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) // 确保是我们启动的那个定时器 { // 获取当前时间。这里使用CTime它比SYSTEMTIME结构更方便。 CTime currentTime CTime::GetCurrentTime(); // 你可以将时间存储为成员变量供OnPaint使用 m_currentTime currentTime; // 强制窗口重绘 Invalidate(); // 标记整个客户区为无效触发WM_PAINT UpdateWindow(); // 立即发送WM_PAINT消息不等待消息队列可选可使更新更即时 } CFrameWnd::OnTimer(nIDEvent); }这里m_currentTime是你在CMainFrame类中添加的一个CTime类型的成员变量。在OnPaint中你将使用这个变量来获取时、分、秒。3.3 图形绘制OnPaint的实现这是最体现“设计”的部分。我们将在OnPaint中绘制整个时钟界面。这里以绘制一个简单的模拟指针时钟为例。void CMainFrame::OnPaint() { CPaintDC dc(this); // 构造一个用于绘制的设备上下文必须且仅用于OnPaint中 CRect rectClient; GetClientRect(rectClient); // 获取客户区矩形 int clientWidth rectClient.Width(); int clientHeight rectClient.Height(); // 1. 绘制背景 dc.FillSolidRect(rectClient, RGB(240, 240, 245)); // 浅灰色背景 // 设置绘图模式为抗锯齿需要GDI这里先用GDI演示 dc.SetBkMode(TRANSPARENT); // 设置文字背景透明 // 2. 绘制表盘外圆 int centerX clientWidth / 2; int centerY clientHeight / 2; int radius min(centerX, centerY) - 10; // 半径留出边距 CPen penCircle(PS_SOLID, 3, RGB(50, 50, 50)); CPen* pOldPen dc.SelectObject(penCircle); CBrush brushFace(RGB(255, 255, 255)); // 白色表盘 CBrush* pOldBrush dc.SelectObject(brushFace); dc.Ellipse(centerX - radius, centerY - radius, centerX radius, centerY radius); // 3. 绘制刻度小时刻度和分钟刻度 for (int i 0; i 12; i) { double angle i * 30.0 * 3.14159 / 180.0; // 每小时30度转换为弧度 int innerRadius radius - 15; // 小时刻度内端点 int outerRadius radius - 5; // 小时刻度外端点 int x1 centerX innerRadius * sin(angle); int y1 centerY - innerRadius * cos(angle); // 注意屏幕坐标系Y轴向下所以用减号 int x2 centerX outerRadius * sin(angle); int y2 centerY - outerRadius * cos(angle); dc.MoveTo(x1, y1); dc.LineTo(x2, y2); } // 可以类似地绘制更细的分钟刻度60个这里省略。 // 4. 获取当前时间并绘制指针 int nHour m_currentTime.GetHour(); int nMinute m_currentTime.GetMinute(); int nSecond m_currentTime.GetSecond(); // 计算指针角度注意时针应该随分钟缓慢移动例如3:30时针应指向3和4中间 double hourAngle ((nHour % 12) nMinute / 60.0) * 30.0 * 3.14159 / 180.0; double minuteAngle (nMinute nSecond / 60.0) * 6.0 * 3.14159 / 180.0; // 每分钟6度 double secondAngle nSecond * 6.0 * 3.14159 / 180.0; // 每秒6度 // 绘制时针 CPen penHour(PS_SOLID, 6, RGB(0, 0, 0)); dc.SelectObject(penHour); int hourHandLength radius * 0.5; dc.MoveTo(centerX, centerY); dc.LineTo(centerX hourHandLength * sin(hourAngle), centerY - hourHandLength * cos(hourAngle)); // 绘制分针 CPen penMinute(PS_SOLID, 4, RGB(0, 0, 0)); dc.SelectObject(penMinute); int minuteHandLength radius * 0.7; dc.MoveTo(centerX, centerY); dc.LineTo(centerX minuteHandLength * sin(minuteAngle), centerY - minuteHandLength * cos(minuteAngle)); // 绘制秒针 CPen penSecond(PS_SOLID, 2, RGB(255, 0, 0)); dc.SelectObject(penSecond); int secondHandLength radius * 0.8; dc.MoveTo(centerX, centerY); dc.LineTo(centerX secondHandLength * sin(secondAngle), centerY - secondHandLength * cos(secondAngle)); // 5. 绘制中心点 CBrush brushCenter(RGB(0, 0, 0)); dc.SelectObject(brushCenter); dc.Ellipse(centerX - 5, centerY - 5, centerX 5, centerY 5); // 6. 恢复原来的GDI对象良好习惯 dc.SelectObject(pOldPen); dc.SelectObject(pOldBrush); // 7. 可选绘制数字时间文本 CString strTime m_currentTime.Format(_T(%H:%M:%S)); CFont font; font.CreatePointFont(120, _T(Arial)); // 12点字体 CFont* pOldFont dc.SelectObject(font); dc.SetTextColor(RGB(0, 100, 200)); dc.TextOutW(10, 10, strTime); dc.SelectObject(pOldFont); }这段代码绘制了一个带有时、分、秒指针的模拟时钟并在左上角显示了数字时间。它涵盖了GDI的基本操作选择画笔、画刷、绘制几何图形、计算坐标、输出文本。3.4 用户交互处理拖拽与右键菜单由于我们使用了无边框窗口(WS_POPUP)需要自己实现拖拽功能。原理是在鼠标左键按下时记录下鼠标在窗口客户区的初始位置在鼠标移动时根据鼠标当前位置与初始位置的偏移量来计算并设置窗口的新位置。首先在CMainFrame类中添加两个成员变量class CMainFrame : public CFrameWnd { // ... private: CPoint m_ptOldPos; // 记录拖拽起始点屏幕坐标 BOOL m_bDragging; // 是否正在拖拽 CTime m_currentTime; };然后实现鼠标消息处理函数void CMainFrame::OnLButtonDown(UINT nFlags, CPoint point) { // 将客户区坐标转换为屏幕坐标 ClientToScreen(point); m_ptOldPos point; // 记录按下时的屏幕坐标 m_bDragging TRUE; // 捕获鼠标确保即使鼠标移动到窗口外也能收到OnMouseMove消息 SetCapture(); CFrameWnd::OnLButtonDown(nFlags, point); } void CMainFrame::OnMouseMove(UINT nFlags, CPoint point) { if (m_bDragging) { // 获取当前鼠标的屏幕坐标 CPoint ptCurScreen(point); ClientToScreen(ptCurScreen); // 计算窗口应该移动到的位置 CRect rectWindow; GetWindowRect(rectWindow); rectWindow.OffsetRect(ptCurScreen.x - m_ptOldPos.x, ptCurScreen.y - m_ptOldPos.y); // 移动窗口 MoveWindow(rectWindow); // 更新旧的参考点为当前位置 m_ptOldPos ptCurScreen; } CFrameWnd::OnMouseMove(nFlags, point); } void CMainFrame::OnLButtonUp(UINT nFlags, CPoint point) { if (m_bDragging) { m_bDragging FALSE; // 释放鼠标捕获 ReleaseCapture(); } CFrameWnd::OnLButtonUp(nFlags, point); } void CMainFrame::OnRButtonUp(UINT nFlags, CPoint point) { // 弹出简单的右键菜单 CMenu menu; menu.CreatePopupMenu(); menu.AppendMenu(MF_STRING, 1001, _T(退出(X))); // 将客户区坐标转换为屏幕坐标 ClientToScreen(point); // 显示菜单并处理选择 int nCmd menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RETURNCMD, point.x, point.y, this); if (nCmd 1001) { PostMessage(WM_CLOSE); // 发送关闭消息 } CFrameWnd::OnRButtonUp(nFlags, point); }3.5 资源清理与退出别忘了在窗口销毁时清理定时器。void CMainFrame::OnDestroy() { CFrameWnd::OnDestroy(); // 销毁定时器 KillTimer(1); }4. 进阶优化与功能扩展一个基础时钟完成后我们可以考虑很多优化和扩展让它更实用、更美观。4.1 解决闪烁问题如果你发现时钟指针在更新时闪烁这是因为OnPaint中先填充背景再绘制图形中间有一个短暂的“空白”帧。解决方法双缓冲绘图这是最根本的解决方案。原理是在内存中创建一个与屏幕兼容的“画布”CDC和CBitmap先将所有图形绘制到这个内存画布上绘制完成后一次性将这个画布的内容“贴”到屏幕窗口上。这消除了中间过程的可见性。void CMainFrame::OnPaint() { CPaintDC dcScreen(this); CRect rectClient; GetClientRect(rectClient); // 创建内存DC和兼容位图 CDC dcMem; CBitmap bmpMem; dcMem.CreateCompatibleDC(dcScreen); bmpMem.CreateCompatibleBitmap(dcScreen, rectClient.Width(), rectClient.Height()); CBitmap* pOldBmp dcMem.SelectObject(bmpMem); // 1. 在内存DC上绘制背景 dcMem.FillSolidRect(rectClient, RGB(240, 240, 245)); // ... (所有原本在dcScreen上的绘图操作现在都在dcMem上进行) // 2. 绘制表盘、指针、文字... // 3. 将内存DC的内容一次性拷贝到屏幕DC dcScreen.BitBlt(0, 0, rectClient.Width(), rectClient.Height(), dcMem, 0, 0, SRCCOPY); // 4. 清理 dcMem.SelectObject(pOldBmp); }处理WM_ERASEBKGND消息默认情况下Windows在发送WM_PAINT前会发送WM_ERASEBKGND来擦除背景这可能导致闪烁。我们可以直接吞掉这个消息不让它擦除。BOOL CMainFrame::OnEraseBkgnd(CDC* pDC) { // 直接返回TRUE表示我们已经处理了背景擦除实际上没做。 // 结合双缓冲效果最好。 return TRUE; // return CFrameWnd::OnEraseBkgnd(pDC); // 如果不用双缓冲不要屏蔽默认处理 }4.2 实现更平滑的秒针动画高精度定时器1秒更新一次秒针是跳动的。要实现平滑扫秒石英钟效果需要更快的更新频率比如100毫秒更新一次。但WM_TIMER的精度有限约55ms且消息可能被阻塞。对于高精度平滑动画可以考虑多媒体定时器(timeSetEvent): 精度可达1ms但资源消耗较大。单独的工作线程Sleep在一个线程中循环获取时间并触发重绘用Sleep(50)控制频率。但需要注意所有UI操作必须在主线程窗口线程进行所以线程中不能直接调用Invalidate()需要通过PostMessage向主窗口发送自定义消息。使用SetTimer并设置更小的间隔例如SetTimer(1, 100, NULL)每100ms触发一次。这是最简单的折中方案对于时钟动画基本够用。在OnTimer中你需要获取更精确到毫秒的时间可以用GetTickCount或CTime的GetCurrentTime但后者精度只到秒需要结合其他方法计算毫秒级偏移。这里给出一个使用SetTimer进行100ms更新的简单思路// 在OnCreate中 SetTimer(1, 100, NULL); // 100毫秒 // 添加成员变量记录动画开始时间或上一帧时间 DWORD m_dwLastTick; int m_nLastSecond; // 上一秒的秒数 void CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { CTime now CTime::GetCurrentTime(); int nCurrentSecond now.GetSecond(); DWORD dwCurrentTick GetTickCount(); // 如果秒数发生了变化更新完整的时间 if (nCurrentSecond ! m_nLastSecond) { m_currentTime now; m_nLastSecond nCurrentSecond; m_dwLastTick dwCurrentTick; } else { // 秒数没变计算自这一秒开始过去了多少毫秒 DWORD dwElapsed dwCurrentTick - m_dwLastTick; // 可以将这个毫秒数存储起来在OnPaint中用于计算秒针的中间角度 m_millisecondOffset dwElapsed % 1000; // 假设存储在一个成员变量中 } Invalidate(FALSE); // FALSE参数表示不擦除背景配合双缓冲使用 } CFrameWnd::OnTimer(nIDEvent); }然后在OnPaint中计算秒针角度时将毫秒偏移量考虑进去double secondAngle (nSecond m_millisecondOffset / 1000.0) * 6.0 * 3.14159 / 180.0;4.3 添加个性化功能皮肤与主题将绘制的颜色、画笔粗细、背景图片等参数化。可以定义几个不同的主题结构体让用户切换。置顶显示在窗口创建后或通过菜单命令调用SetWindowPos(wndTopMost, ...)将窗口设为始终置顶。透明度设置使用分层窗口(WS_EX_LAYERED)通过SetLayeredWindowAttributesAPI设置整体透明度或使用UpdateLayeredWindow实现更复杂的每像素透明度需要GDI。系统托盘图标使用Shell_NotifyIconAPI将程序最小化到系统托盘而不是任务栏。这需要处理托盘消息自定义消息WM_USERxxx。闹钟或报时功能增加一个对话框设置闹钟时间在OnTimer中检查当前时间是否与设定时间匹配触发声音或提示。5. 常见问题与调试技巧在实现过程中你可能会遇到一些典型问题。这里记录几个我踩过的坑和解决方法。5.1 窗口不显示或立即退出检查InitInstance确保在CMyClockApp::InitInstance()中创建了主窗口对象new CMainFrame并且调用了pFrame-ShowWindow(SW_SHOW)和pFrame-UpdateWindow()。对于非文档/视图程序可能需要手动创建窗口并调用Create或LoadFrame。检查消息循环InitInstance最后应返回TRUE。MFC的Run函数会自动启动消息循环。如果程序窗口一闪而过可能是InitInstance提前返回了FALSE。5.2 定时器不触发或触发异常ID冲突确保SetTimer的ID在OnTimer中能正确匹配。一个窗口可以有多个定时器用ID区分。消息阻塞如果主线程在进行长时间操作如大量计算或Sleep会阻塞消息处理导致定时器消息无法被及时处理看起来就像定时器“停了”。确保耗时的操作放在工作线程中。精度问题WM_TIMER是低精度定时器消息被放在普通消息队列中。它的最小间隔受系统时钟滴答约15.6ms限制且不能保证准时。对于需要高精度的场景如前所述需考虑其他方案。5.3 绘制内容错乱或残留坐标计算错误屏幕坐标系Y轴向下为正这与数学坐标系相反。在计算三角函数坐标时sin和cos的用法要特别注意我上面的例子中y centerY - radius * cos(angle)就是做了这个转换。未恢复GDI对象每次SelectObject选择新的画笔、画刷、字体后最好保存旧的在绘制结束后再选回去。虽然设备上下文在CPaintDC析构时会清理但养成这个习惯能避免很多难以察觉的bug。未调用基类OnPaint在重写的OnPaint中不要调用基类CFrameWnd::OnPaint()。因为基类的OnPaint可能执行默认的绘制干扰我们的自定义绘制。CPaintDC的构造和析构会自动处理BeginPaint和EndPaint。5.4 发布程序无法在别人电脑上运行缺少MFC运行时库如果你的项目在Visual Studio中默认使用的是“在静态库中使用MFC”则生成的可执行文件会比较大但可以独立运行。如果使用的是“在共享DLL中使用MFC”则需要目标机器上安装相应版本的Microsoft Visual C Redistributable。发布时最好将安装包一起打包或者直接使用静态链接。在项目属性 - 配置属性 - 常规 - MFC的使用中可以修改此项。Unicode字符集问题确保你的字符串处理使用_T()宏或TCHAR相关类型以兼容多字符集设置。或者在项目属性中明确设置字符集为“使用Unicode字符集”或“使用多字节字符集”并与你的代码写法保持一致。5.5 调试技巧TRACE宏在Debug模式下使用TRACE(_T(“当前时间%d:%d:%d\n”), hour, minute, second);可以在VS的输出窗口打印调试信息非常方便。断言使用ASSERT宏检查关键假设例如指针非空、数组索引有效等。Graphics Debugging对于复杂的GDI绘制问题可以尝试使用Visual Studio的图形调试器仅限DirectX但对于传统GDI更实用的方法是在绘制不同部分时使用不同颜色临时标记帮助定位哪一步绘制出了问题。这个基于MFC的C时钟项目麻雀虽小五脏俱全。它串联起了Windows桌面程序开发的核心概念消息循环、资源管理、GDI绘图、定时器、用户交互。完成它之后你不仅得到了一个可以放在桌面上的小工具更重要的是你获得了一套理解和构建更复杂MFC应用程序的坚实框架。当你下次需要开发一个数据监控面板、一个自定义播放器界面或者任何其他桌面工具时这次的经验将会成为你可靠的起点。
返回列表