尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CToolBarEx实战:MFC工具栏自绘、状态控制与动态布局全攻略

CToolBarEx实战:MFC工具栏自绘、状态控制与动态布局全攻略 简介这是一份面向 Visual C / MFC 开发者的 CToolBarEx 扩展工具栏学习资源主要解决标准 CToolBar 在自定义按钮、动态菜单、图标与文字混排、按钮状态管理等方面的灵活性不足问题适合希望提升 Windows 界面交互体验的中级开发者。资源共 30 个文件压缩包仅 43KB内容以 7 个 h 头文件、6 个 cpp 源码文件为主完整覆盖示例程序框架工程文件dsp/dsw/rc便于直接编译运行ico/bmp 及资源文件展示不同按钮状态下的图标素材txt 文件则包含说明与来源注释。已有 93 人学习/下载。通过研读这套示例读者可掌握 CToolBarEx 的创建、AddButton 添加按钮、消息映射绑定、SetButtonInfo 动态设置状态以及下拉菜单与按钮联动的实现思路具有很强的上手参考价值。1. 别再手动拖按钮坐标了CToolBarEx 在 Visual C 里帮你省掉的三件脏活打开 TB.rar 这类老资源包的人多半是接手了一个 MFC 项目想在窗口顶部放一条能换肤、能悬浮、按钮状态不乱的工具栏。默认的 CToolBar 不是不能做而是做起来让人想把键盘砸了每个按钮的图标坐标要按像素调工具栏背景图要么不显示、要么把按钮盖住窗口拉伸后按钮还停在原地。CToolBarEx 就是 Visual C 社区里流传多年的一套扩展类它把工具条的自绘、状态控制和动态增删封装好让你的工具条长得像现代软件而不是 Win95 时代的灰格子。这篇笔记适合正在维护 MFC 老项目、或者想在纯 Win32 对话框上快速搭一条能用的工具栏的开发者跟着复现一个小时就能把 CToolBarEx 接到你自己的窗口上。2. 从 CToolBar 到 CToolBarEx默认工具条不够用三个切入点改哪了2.1 默认 CToolBar 让开发者反复调坐标的四个死穴MFC 的 CToolBar 本质是对 Win32 工具条控件Toolbar Window Class的薄封装。它默认支持位图按钮和文本但你只要碰过真实项目就会立刻撞上四个死穴。第一个死穴是没有自绘钩子。CToolBar 自己绘制按钮但绘制逻辑写死在 MFC 源码里。你想把鼠标悬浮到按钮上时让背景变蓝、按钮边缘带个圆角只能改写系统绘制流程。Win32 工具条虽然支持 TB_SETDRAWTEXTFLAGS 之类的消息但 MFC 封装没有把自绘消息完整暴露出来。第二个死穴是背景图处理异常痛苦。CToolBar 的背景是父窗口背景擦除后留下的你想放一张 1024x48 的渐变背景条系统会先擦成灰色再画按钮最后背景图透不出来。第三个死穴是按钮状态管理靠 SetButtonStyle 和 GetButtonStyle可用的状态只有 TBSTATE_CHECKED、TBSTATE_ENABLED、TBSTATE_HIDDEN 等几个位而且按下状态和悬浮状态完全混在一起无法区分。第四个死穴是按钮布局不会随窗口宽度变化自动换行或压缩窗口拉宽了右边空一大块拉窄了按钮直接挤出可视区域。CToolBarEx 做的事情就是堵住这四个洞。它通过自绘Owner Draw接管每个按钮的绘制通过重写 OnEraseBkgnd 处理背景图通过一个内部状态数组维护每个按钮的自定义状态并且在 WM_SIZE 里重排按钮坐标。这类类在 2000 年代很流行当时叫扩展工具栏今天看仍然是 MFC 里做自定义界面最高性价比的方案。2.2 扩展点选在哪重绘、按钮状态与布局管理理解 CToolBarEx 的代码先要抓住三个扩展切入点。第一个切入点是重绘。Win32 工具条想自绘需要给按钮设置 TBSTYLE_LIST 之外的 owner-draw 风格并在处理 WM_DRAWITEM 消息时自己画按钮。CToolBarEx 一般在构造函数里把自己的风格改成自绘然后重写 DrawButton 或 OnDrawButton。第二个切入点是按钮状态。默认 CToolBar 用一个 TBBUTTON 结构存按钮信息但结构里能用的状态位太有限。扩展类通常自定义一个结构比如struct TBBUTTONEX { int nID; // 命令 ID int nImage; // 图标索引 BYTE fsState; // 兼容系统的状态 BYTE nCustomState; // 自定义状态悬浮 / 按下 / 禁用 CRect rcBound; // 按钮实际矩形 };第三个切入点是布局管理。CToolBar 的布局算法在 CControlBar::CalcInsideRect 和 Layout 里扩展类会在按钮增删或窗口尺寸变化时重新计算每个按钮的 rcBound并考虑分隔条、下拉箭头和文本宽度。很多 CToolBarEx 实现干脆放弃 MFC 默认 Layout在 OnSize 里手动遍历按钮数组用需要显示的位置整数累加算出每个按钮的矩形。这样做代码直白但要注意 DPI 缩放否则高分屏下按钮会挤成一团。这三个切入点对应你在 TB.rar 里会看到的三类文件从 CToolBar 派生的主类头文件、一个自绘按钮类有时直接复用主类、以及若干消息处理函数。看懂这三个点后面无论拿到谁的 CToolBarEx 源码都能很快定位到关键代码。3. 手写一个够用的 CToolBarEx从基类选择到图标按钮加载的最小代码3.1 继承 CControlBar 还是包装 CToolBar动手写 CToolBarEx 前先决定基类。这条路我走过两遍第一遍从 CControlBar 直接派生第二遍老老实实从 CToolBar 派生。结论是如果你要的是“一条能用的工具栏”从 CToolBar 派生最稳因为 CToolBar 已经帮你把位图、按钮添加、命令路由和停靠逻辑都搭好了。从 CControlBar 派生需要对 Win32 Toolbar 控件做大量封装复杂度和维护成本直接翻倍。以 Visual C 6.0 和后续 VS 系列通用的做法为例类的骨架长这样// CToolBarEx.h #pragma once #include afxext.h class CToolBarEx : public CToolBar { public: CToolBarEx(); virtual ~CToolBarEx(); BOOL LoadToolBarEx(UINT nIDToolbar, UINT nIDBitmap, int nButtonWidth 0, int nButtonHeight 0); BOOL SetButtonImage(int nIDCommand, int nImageIndex); void SetToolBarBkColor(COLORREF clrBk); protected: COLORREF m_clrBk; // 背景色 CImageList m_ImageList; // 图标列表 CMapint, int, int, int m_mapCmdToIndex; // 命令 ID 到图标索引 afx_msg void OnEraseBkgnd(CDC* pDC); afx_msg void OnDrawButton(CDC* pDC, LPRECT lpRect, UINT nState, int nID); afx_msg BOOL OnToolTipText(UINT nID, NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() };这里我只保留了最核心的三个成员背景色、图标列表和命令到图标的映射。为什么要 CMap因为后续 SetButtonImage 要按命令 ID 改图标而工具栏内部用的是索引不映射的话每次都要遍历查找。图标列表用 CImageList 而不是直接 LoadBitmap因为 CImageList 天然支持透明色和多种尺寸后面换色也简单。对应的消息映射和关键实现如下// CToolBarEx.cpp BEGIN_MESSAGE_MAP(CToolBarEx, CToolBar) ON_WM_ERASEBKGND() ON_WM_DRAWITEM() ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipText) END_MESSAGE_MAP() BOOL CToolBarEx::LoadToolBarEx(UINT nIDToolbar, UINT nIDBitmap, int nButtonWidth, int nButtonHeight) { if (!CToolBar::CreateEx(this, TBSTYLE_TOOLTIPS | TBSTYLE_FLAT) || !LoadToolBar(nIDToolbar)) return FALSE; // 从位图资源创建图标列表 CBitmap bmp; if (!bmp.LoadBitmap(nIDBitmap)) return FALSE; BITMAP bmpInfo; bmp.GetBitmap(bmpInfo); int nCount bmpInfo.bmWidth / 16; // 按 16 像素宽切图标 m_ImageList.Create(16, 16, ILC_COLOR32 | ILC_MASK, nCount, 0); m_ImageList.Add(bmp, RGB(255, 0, 255)); // 用洋红做透明色 SetImageList(m_ImageList); SetButtonSize(nButtonWidth ? nButtonWidth : 24, nButtonHeight ? nButtonHeight : 22); return TRUE; }逻辑说明LoadToolBarEx 先调用默认 LoadToolBar 加载按钮定义这时工具栏已经有按钮 ID 和分隔条但图标还是空白。然后用 nIDBitmap 位图创建图标列表ico 的切割按位图宽度除以 16 个像素。注意 SetImageList 会覆盖 CToolBar 内部默认的图像列表所以后续按钮的图标都从这里取。参数方面nButtonWidth 和 nButtonHeight 是按钮点击区域尺寸一般比图标尺寸大 4 到 8 个像素太小手指很难点中太大会让工具栏看起来松散。我这里透明色用洋红 RGB(255,0,255)这是从 VB 时代流传下来的约定你也可以换成纯黑但记得不要在图标里真的用到这种颜色否则那一块会变透明。背景擦除处理是 CToolBarEx 与普通 CToolBar 的明显区别BOOL CToolBarEx::OnEraseBkgnd(CDC* pDC) { if (m_clrBk ! RGB(255, 255, 255)) { CRect rc; GetClientRect(rc); pDC-FillSolidRect(rc, m_clrBk); return TRUE; } return CToolBar::OnEraseBkgnd(pDC); }这段代码先判断背景色是否不是白色如果不是就直接用背景色填充整个客户区并返回 TRUE 表示已经擦除。这样做的好处是程序启动时不会有白闪而且你后续想贴背景图只用在这个函数里画位图再返回 TRUE 即可。注意必须返回 TRUE不然系统还会用父窗口背景再擦一遍你画的颜色会被覆盖。3.2 给按钮加上悬浮高亮重写 DrawItem 的完整思路光有图标按钮还是系统默认的凸起样式CToolBarEx 的重头戏在于自绘按钮。在消息映射里我们加了 ON_WM_DRAWITEM对应的处理函数需要自己画出按钮的四种状态正常、悬浮、按下、禁用。这里给出一种最简实现void CToolBarEx::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC* pDC CDC::FromHandle(lpDrawItemStruct-hDC); CRect rc(lpDrawItemStruct-rcItem); UINT nState lpDrawItemStruct-itemState; int nID lpDrawItemStruct-CtlID; // 绘制背景 if (nState ODS_SELECTED) pDC-FillSolidRect(rc, RGB(255, 230, 200)); else if (nState ODS_HOTLIGHT) pDC-FillSolidRect(rc, RGB(220, 240, 255)); else pDC-FillSolidRect(rc, m_clrBk); // 画边框制造“按下去”的错觉 if (nState ODS_SELECTED) pDC-Draw3dRect(rc, RGB(120, 120, 120), RGB(255, 255, 255)); else if (nState ODS_HOTLIGHT) pDC-Draw3dRect(rc, RGB(200, 200, 200), RGB(255, 255, 255)); // 取图标并绘制到居中位置 int nIndex -1; m_mapCmdToIndex.Lookup(nID, nIndex); if (nIndex 0) { CSize szIcon(16, 16); m_ImageList.Draw(pDC, nIndex, rc.left (rc.Width() - szIcon.cx) / 2, rc.top (rc.Height() - szIcon.cy) / 2, ILD_TRANSPARENT); } }逻辑说明自绘消息会告诉你按钮处于什么状态ODS_SELECTED 表示鼠标按下ODS_HOTLIGHT 表示鼠标悬浮这两个标志来自工具条控件自己在运行时检测鼠标位置。我们只要根据状态用不同颜色填充按钮矩形再画一条凹陷或凸起的边就能模拟立体感。这里用 FillSolidRect 而不是画位图是为了让代码最短可读。实际产品里你可以换成渐变填充或者用一个背景位图按九宫格拉伸。重要的一步是查映射表得到图标索引然后调用 CImageList::Draw 把图标画在按钮正中央。需要说明自绘按钮的 rcItem 矩形是由工具栏控件自己分配的你不要在函数里修改 lpDrawItemStruct-rcItem否则下次重绘会对不上。另外如果你发现自绘状态下按钮的 Tooltip 不显示多半是忽略了 TTN_NEEDTEXT 通知下面第 4 章会讲。4. 让按钮会说话图标状态、下拉箭头与动态增删怎么落地4.1 用状态位区分正常、悬浮、按下、禁用别再只靠 TBBUTTON 的 fsStateCToolBar 原生的按钮状态只有通过 TB_SETSTATE 设置能区分可用和选中但对悬浮和按下无能为力。在 CToolBarEx 里我们既要用系统状态又要自己维护一层“扩展状态”。常见的做法是给每个按钮保存一个字节的 m_nCustomState用位标记#define TBE_NORMAL 0x00 #define TBE_HOVER 0x01 #define TBE_PRESSED 0x02 #define TBE_DISABLED 0x04 #define TBE_CHECKED 0x08在刷新按钮状态时可以这样写void CToolBarEx::RefreshButtonState(int nID) { UINT nState GetButtonState(nID); // 系统状态 int nCustom m_nCustomStates[nID]; if (nState TBSTATE_ENABLED) nCustom ~TBE_DISABLED; else nCustom | TBE_DISABLED; if (nState TBSTATE_CHECKED) nCustom | TBE_CHECKED; m_nCustomStates[nID] nCustom; // 强制重绘这个按钮 CRect rcBtn; GetItemRect(CommandToIndex(nID), rcBtn); InvalidateRect(rcBtn); }逻辑说明系统状态在用户点击按钮时会自动变化比如按下时 TBSTATE_PRESSED 变成 1。我们把它读出来后合成到自定义状态里这样在 OnDrawItem 里只需要查 m_nCustomStates[nID] 这一个变量不用反复调用 GetButtonState。刷新完成后用 GetItemRect 拿到按钮矩形并强制重绘这是很关键的一步否则 UI 会停留在旧状态。要注意命令 ID 和索引的转换是 CToolBar 系列的经典坑。CommandToIndex 返回按钮在工具条内的索引但是遇到分隔条索引照样占位。所以 GetItemRect 的参数必须是 CommandToIndex 的返回值而不能直接用命令 ID。如果你在自绘里用命令 ID 当索引去 GetItemRect拿到的矩形会错得一塌糊涂。4.2 动态增删按钮与下拉箭头InsertButton 之后必须做的事很多场景下工具栏不是启动时就定死的比如最近文件列表要动态出现在工具条上。CToolBar 提供 InsertButton 和 DeleteButton但你直接调用就会发现图标不显示、位置错乱。原因是 CToolBar 的按钮数组和图标索引是两套逻辑插入按钮时它不会自动帮你把新按钮的图标索引设置好。正确姿势是插入后立刻用 SetButtonInfo 指定图标索引代码如下int CToolBarEx::AddButtonEx(int nID, int nImage, UINT nStyle, int nWidth) { int nIndex CommandToIndex(nID); if (nIndex 0) return nIndex; // 已存在避免重复插入 // 按钮结构 TBBUTTON tbb; memset(tbb, 0, sizeof(tbb)); tbb.idCommand nID; tbb.iBitmap nImage; tbb.fsState TBSTATE_ENABLED; tbb.fsStyle nStyle; // TBSTYLE_BUTTON / TBSTYLE_SEP 等 tbb.dwData 0; tbb.iString 0; nIndex InsertButton(tbb, -1); // 追加到末尾 // 插入按钮后必须重新设置按钮信息否则图标不刷新 SetButtonInfo(nIndex, nID, nStyle, nImage); // 重新布局 SizeToolBarButtons(); return nIndex; }逻辑说明InsertButton 返回的是工具栏数组中的索引注意它可能不是按钮总数减一因为中间有分隔条也要占索引。SetButtonInfo 是 CToolBar 提供的接口专门用来设置按钮的样式和图像索引调用后按钮图标才真正绑定到 nImage。最后 SizeToolBarButtons 是 CToolBarEx 内部的一个方法它遍历所有按钮根据按钮宽度累加出每个按钮的位置并调用 SetButtonSize 设置到最后计算出来的总宽度。这里你会注意到我没有直接改 CToolBar 的原始按钮数组因为 MFC 不保证那些内部结构在版本升级后不变。下拉箭头是工具栏交互里很容易做错的地方。要给按钮加下拉箭头不能只用 TBSTYLE_DROPDOWN因为你还要自己处理 TBN_DROPDOWN 通知。常见的实现就是在按钮后面额外插入一个空白按钮专门画箭头void CToolBarEx::EnableDropDown(int nID, BOOL bEnable) { int nIndex CommandToIndex(nID); if (nIndex 0) return; if (bEnable) { SetButtonStyle(nIndex, GetButtonStyle(nIndex) | TBSTYLE_DROPDOWN); } else { SetButtonStyle(nIndex, GetButtonStyle(nIndex) ~TBSTYLE_DROPDOWN); } Invalidate(); }然后在消息映射里添加 TBN_DROPDOWN 的处理注意必须用 ON_NOTIFY_EX 链掉不然父窗口收不到通知。TBSTYLE_DROPDOWN 加了之后系统会自动在按钮右侧画一个小的下拉箭头你不需要自己画。但前提是按钮所在工具栏必须支持新样式所以 AddButtonEx 里创建的按钮样式要带上 TBSTYLE_DROPDOWN如果你在后期才开启那自绘代码里的 rcItem 宽度不会包含箭头部分图形会显示不全。这个坑比较隐蔽建议一开始就决定哪些按钮需要下拉启用后再重画。5. CToolBarEx 避坑排查这几类翻车现场90% 的人第一周都会遇到5.1 坑多个工具条共用一个按钮 ID命令响应被抢现象窗口里有主工具栏和格式工具栏两个工具栏上都有“保存”按钮命令 ID 都定义成 ID_FILE_SAVE。点击只响应其中一个另一个按钮虽然高亮但没有触达命令处理函数。原因MFC 的命令路由 ON_COMMAND 是通过消息反射自动查找的处理函数挂在 CFrameWnd 的派生类上。两个按钮 ID 相同路由时被命中的那个先接收另一个被吃掉。更糟糕的是当两个按钮都按顺序排列时第二个按钮可能根本收不到鼠标消息。解决给每个工具栏的按钮分配唯一 ID或者让多个按钮映射到同一个命令处理函数但这要小心参数。常用做法是用 ON_COMMAND_RANGE 处理一系列 ID并在处理函数里用 GetCurrentMessage-wParam 分辨来源。我在项目里干脆给每个工具栏保留一段专用 ID 区间比如主工具栏从 0xE000 开始格式工具栏从 0xF000 开始彻底杜绝冲突。5.2 坑图片透明色变成黑底按钮像贴了块膏药现象加载带透明背景的位图后图标周围出现大块黑或者洋红色方块悬浮时更明显。原因CImageList::Create 的最后一个参数 nFlags 决定遮罩如何生成。很多人直接传 ILC_COLOR24漏了 ILC_MASK这样透明色根本不被处理。再一个原因是位图中透明色和有效图标的颜色过度不干净或者位图本身不是 16 位/32 位切割时的像素比对出错。解决创建图像列表时必须使用 ILC_MASK并且透明色要选择图标里绝对不会出现的颜色。我一般用 RGB(0, 248, 0) 这种纯绿色而不是洋红因为某些图形工具导出透明的默认键是洋红会误伤。另外在 LoadToolBarEx 里加一处校验COLORREF clrMask RGB(0, 248, 0); m_ImageList.Create(16, 16, ILC_COLOR32 | ILC_MASK, nCount, 0); m_ImageList.Add(bmp, clrMask);如果你看到图标边缘有残余的绿边说明位图画师的透明处理不干净可以用 SetBkColor 设置图像列表的背景色或者用 ImageReady 重新导出 u 带 alpha 的 PNG再转成 BMP。5.3 坑工具条悬浮后拖动就崩溃断点在 CToolBar::FloatControlBar现象用户将工具栏拖离停靠位置变成悬浮条或者拖动悬浮条调整位置时程序突然崩溃调试器停在 FloatControlBar 或 CMFCToolBar 相关代码。原因多半是工具栏窗口没有正确创建或者漏掉了 CBRS_FLOAT_MULTI 之类的标志。CToolBarEx 如果自己处理了 WM_NCCALCSIZE 或者重写了 OnEraseBkgnd在悬浮模式下系统会改变窗口尺寸你的 OnEraseBkgnd 可能在设备上下文还没有正确状态时被调用。另一个常见原因是按钮矩形数组 m_nCustomStates 因为动态增删越界悬浮条重绘时访问非法内存。解决给工具栏创建时加上 CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC并调用 EnableDocking 和 SetWindowPos 确保父窗口准备好再显示。关键点在于 CToolBarEx 的 m_nCustomStates 不要在 OnDrawItem 外直接按索引访问必须用命令 ID 先做 CommandToIndex 再取状态。我排查这类崩溃时习惯在 DrawItem 入口处加一行索引边界检查if (nIndex 0 || nIndex m_nCustomStates.GetSize()) { TRACE(_T(CToolBarEx: invalid index %d\n), nIndex); return; }这样至少崩溃时能留下日志。还有一个隐藏雷区如果你在 CToolBarEx 里处理 WM_WINDOWPOSCHANGED 来重排按钮请注意悬浮窗口的 WM_WINDOWPOSCHANGED 触发频率极高不要在里面对工具栏做 InsertButton 这类操作否则状态机错乱拖几次就释放了非法内存。5.4 坑自绘按钮在 DPI 缩放下图标和热区错位现象Win10 系统把显示缩放调成 125% 或 150% 后按钮图标变小、点击区域和图标对不上悬浮高亮出现在错误位置。原因MFC 传统代码不感知 DPICToolBarEx 里按 16 像素切图标但系统把窗口逻辑坐标换算成物理像素后按钮矩形被放大了但绘图区域没有同步缩放。解决在程序初始位置调用 SetProcessDpiAwareness 或者在资源文件中声明 dpiAware。CToolBarEx 内部应该使用当前 DPI 折算后的按钮尺寸比如 24 逻辑像素在 150% 下就要设按钮尺寸为 36 物理像素。最稳的办法是在 LoadToolBarEx 中获取当前 DC 的 LOGPIXELSXint nLogPixelsX GetDeviceCaps(pDC-m_hDC, LOGPIXELSX); int nScale nLogPixelsX / 96; SetButtonSize(24 * nScale, 22 * nScale);同时图标列表也要用更高分辨率的图源否则放大会糊。6. 最后一招把 CToolBarEx 变成你项目里的通用控件并验证命令链没被破坏把代码写完只是第一步经验最多的那部分是让它安静地待在你的类库里几年不用改。我习惯在项目里做两件事。第一件是把 CToolBarEx 的头文件和源文件收进 MFC 类向导可以识别的目录然后在任何对话框或框架窗口里直接用 DDX 控件绑定。具体做法在对话框编辑器里拖一个 Custom Control类名填 CToolBarEx样式随便填然后在 OnInitDialog 里调用 SubclassDlgItem 把窗口接管过来。这样工具条就和其他控件一样参与 Tab 键切换还能用 GetDlgItem 动态访问。第二件是验证命令路由没有被覆盖。每次改完自绘逻辑我会在 Debug 下打开 MFC 的消息跟踪重载 _AfxTraceEnable然后手动点击每个按钮确认 WM_COMMAND 都发送到了目标处理函数。如果按一个按钮触发了两个消息或者根本没消息优先检查 GetButtonInfo 返回的 nID 是否和资源文件里的 ID 一致。我还有一条血泪经验不要在 ON_UPDATE_COMMAND_UI 中直接操作 CToolBarEx 的按钮状态因为 CMenu 和 CToolBar 共用这套机制状态更新时机不同会导致按钮外观闪烁。把状态刷新放到单独定时器里用上面的 RefreshButtonState 统一刷新比在消息泵里散布 return TRUE 要干净得多。最后建议你在 CToolBarEx 里保留一组观察变量按钮总数、当前宽度、最后一个按钮的矩形。程序运行后用 Spy 对比这些值和窗口实际显示的像素能快速发现布局问题。我把这条维护经验写进来希望你接手别人留下的 TB.rar 扩展类时不要急着改功能先把状态刷新和布局验证补齐不然后面每加一个按钮都可能是一场和坐标的苦战。希望这些踩坑记录能帮到你让你的工具条从 CToolBar 的灰色时代真正迈入自绘时代。本文还有配套的精品资源点击获取
返回列表