WTL 工具栏助手






4.89/5 (26投票s)
向 WTL 工具栏添加文本、下拉菜单和组合框。
引言
这是一个特殊的工具栏助手模板类,您可以在基于 WTL 框架窗口的应用程序中使用它来实现以下功能:
- 向工具栏按钮添加文本
- 向工具栏按钮添加下拉菜单
- 向工具栏添加组合框
这里提供的代码使得任何 WTL CFrameWnd
派生窗口都可以非常容易地显示专业的工具栏,我希望关于它如何工作的解释也能派上用场。
入门
首先,您需要包含头文件,最好是在您的 MainFrm.h 中
#include "ToolBarHelper.h"
接下来,从 CToolBarHelper
派生您的窗口类(可能是 CMainFrame
)
class CMainFrame : ... public CToolBarHelper<CMainFrame>
接下来,如果您想要工具栏下拉菜单/组合框,您需要在消息映射中添加一个 CHAIN_MSG_MAP
条目,以确保 CBN_SELCHANGE
和 TBN_DROPDOWN
消息得到正确处理
BEGIN_MSG_MAP(CMainFrame) ... CHAIN_MSG_MAP(CToolBarHelper<CMainFrame>) END_MSG_MAP()
无论如何,这样做可能是一个好主意,以防您以后想添加下拉菜单/组合框。
CString,等等。
请注意,代码使用了 CString
、CRect
和 CSize
类。这意味着如果您在 Visual Studio 6 下编译,您将需要 #include <atlmisc.h>
,而 Visual Studio 2005 用户可能希望使用新的 atlstr.h
和 atltypes.h 头文件。atlctrls.h 文件也很重要,但 WTL ClassWizard 默认会将其添加到 stdafx.h 中。
接下来,在您的 CMainFrame::OnCreate
函数中添加相关的“魔法”来美化您的工具栏。
向工具栏按钮添加文本
要向工具栏按钮添加文本,您首先需要确保您的工具栏是使用 ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX
而不是 ATL_SIMPLE_TOOLBAR_PANE_STYLE
创建的。如果您忘记了这一步,那么文本将不会出现在工具栏按钮上。只需替换这一行
HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, IDR_MAINFRAME, FALSE, ATL_SIMPLE_TOOLBAR_PANE_STYLE);
用这个:
HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, IDR_MAINFRAME, FALSE, ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX);
请注意,ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX
只是 ATL_SIMPLE_TOOLBAR_PANE_STYLE
和“魔法”工具栏 TBSTYLE_LIST
样式(确保按钮上显示文本)的组合。
现在,您只需使用以下方法之一将文本添加到相关按钮:
AddToolbarButtonText(HWND hWndToolBar, UINT nID, LPCTSTR lpsz)
使用此方法直接从字符串设置工具栏按钮的文本,例如
AddToolbarButtonText(hWndToolBar, ID_APP_ABOUT, _T("About"));
只需传递工具栏窗口句柄(由 CreateSimpleToolBarCtrl
返回的句柄)、您要更改的按钮 ID 以及您要添加的文本。
AddToolbarButtonText(HWND hWndToolBar, UINT nID, UINT nStringID)
使用此方法从指定的字符串资源添加文本。例如
AddToolbarButtonText(hWndToolBar, ID_EDIT_PASTE, IDS_TOOLBAR_TEXT);
这将加载 IDS_TOOLBAR_TEXT
字符串并将文本分配给指定的工具栏按钮(在本例中为 ID_EDIT_PASTE
)。
最后,您可以使用以下方法将工具栏按钮设置为已分配给按钮的工具提示文本:
AddToolbarButtonText(HWND hWndToolBar, UINT nID)
此方法将尝试加载与按钮具有相同 ID 的字符串,查找工具提示文本,并将其分配给按钮。例如
AddToolbarButtonText(hWndToolBar, ID_FILE_SAVE);
这将加载 ID 为 ID_FILE_SAVE
的字符串,它看起来像这样
Save the active document\nSave (Ctrl+S)
工具提示文本可以在 \n 之后立即找到。现在,正如您所看到的,加速键已硬编码到工具提示文本中,因此 AddToolbarButtonText
将查找 \n 之后的第一个开放括号,并仅使用括号之间的文本。因此,在此示例中,ID 为 ID_FILE_SAVE
的工具栏按钮将设置为文本 Save。
很简单。
幕后
向工具栏按钮添加文本需要以下步骤:
- 确保工具栏设置了
TBSTYLE_EX_MIXEDBUTTONS
样式位。 - 调用
CToolBarCtrl::AddStrings
方法将按钮文本添加到工具栏的内部列表。 - 更改按钮信息,确保设置了
TBSTYLE_AUTOSIZE
和BTNS_SHOWTEXT
位。 - 删除然后重新插入工具栏按钮。
向工具栏按钮添加下拉菜单
为了向工具栏按钮添加下拉菜单,首先您需要使用 Visual Studio 资源编辑器创建菜单。然后,只需在 CMainFrame::OnCreate
中添加对以下方法的调用:
AddToolBarDropDownMenu(HWND hWndToolBar, UINT nButtonID, UINT nMenuID)
指定工具栏窗口句柄(同样,由 CreateSimpleToolBarCtrl
返回的句柄)、将分配下拉按钮的按钮 ID 以及单击按钮时要显示的菜单 ID。很简单。菜单命令将正常路由到主框架窗口,如果您分配了按钮图像,它们将出现(请参阅示例程序进行演示)。
如果您想在显示菜单之前立即修改菜单(例如,添加新项目),则覆盖以下虚函数:
virtual void PrepareToolBarMenu(UINT nMenuID, HMENU hMenu);
此函数在 CToolBarHelper
显示菜单之前调用,并传递即将显示的菜单 ID 和菜单句柄,这将允许您修改菜单内容。
例如,示例程序演示了向附加到“文件|新建”工具栏按钮的 IDR_NEW
菜单添加分隔符和两个新菜单项:
void CMainFrame::PrepareToolBarMenu(UINT nMenuID, HMENU hMenu) { if (nMenuID == IDR_NEW) { CMenuHandle menu(hMenu); menu.AppendMenu(MF_SEPARATOR); menu.AppendMenu(MF_STRING, ID_NEW_DOCUMENT, _T("Document...")); menu.AppendMenu(MF_STRING, ID_NEW_TEMPLATE, _T("Template...")); } }
非常简单。
幕后
向工具栏按钮添加下拉菜单涉及以下步骤:
- 确保按钮本身分配了
TBSTYLE_EX_DRAWDDARROWS
样式。 - 处理
TBN_DROPDOWN
通知消息。 - 使用
CMainFrame::m_CmdBar.TrackPopupMenu
显示菜单,以确保菜单在每个项目旁边显示酷炫的小按钮(如果已分配)。
请注意,CSimpleMap
用于将工具栏按钮 ID 映射到菜单 ID。这个非常简单的映射类已经成为 ATL 的一部分很多年了,应该不会给您带来任何问题。我是一个 STL 的忠实用户,所以通常会使用 std::map
,但对于这个非常简单的目的,CSimpleMap
完全足够。
向工具栏添加组合框
要向工具栏添加组合框,首先您需要向工具栏添加一个用作占位符的按钮。只需使用 Visual Studio 工具栏编辑器添加一个新按钮,并确保它有一个 ID - 不需要图像。接下来,要创建组合框,请从 CMainFrame::OnCreate
调用以下方法:
CreateToolbarComboBox(HWND hWndToolBar, UINT nID, UINT nWidth, UINT nHeight, DWORD dwComboStyle)
您需要提供的最小参数是工具栏窗口句柄(同样,由 CreateSimpleToolBarCtrl
返回的句柄)以及您之前添加到工具栏中的特殊占位符按钮的 ID。组合框的宽度、高度和样式将分别默认为 16
、16
和 CBS_DROPDOWNLIST
(请注意,宽度/高度以字符而不是像素指定,并且应仅用作指导,因为不同版本的 Windows/视觉样式可能会导致组合框自行决定其高度)。
CreateToolbarComboBox
返回新创建的组合框的 HWND
,您可以使用它来添加项目。例如,要添加一个包含三个项目的组合框:
CComboBox combo = CreateToolbarComboBox(hWndToolBar, ID_COMBO_PLACEHOLDER); combo.AddString(_T("Item 1")); combo.AddString(_T("Item 2")); combo.AddString(_T("Item 3"));
当选择组合框项目时,CToolBarHelper
将调用您必须添加到 CMainFrame
类中的以下函数:
void OnToolBarCombo(HWND hWndCombo, UINT nID, int nSel, LPCTSTR lpszText, DWORD dwItemData);
此函数将传递组合框窗口句柄、按钮 ID(例如,ID_COMBO_PLACEHOLDER
)、新选择的组合框项目的索引、项目文本和项目数据。示例程序演示了其运行情况。
组合框 UI 更新
如果您希望工具栏组合框根据某些程序状态更新项目选择,那么您应该执行以下操作:
- 向
CMainFrame
类添加一个CComboBox
成员。 - 将
CreateToolbarComboBox
调用返回的HWND
分配给CComboBox
。 - 在您的
CMainFrame::OnIdle
方法中,相应地更改组合框选择。
例如,在您的 MainFrm.h 中
CComboBox m_wndCombo;
然后,在 CMainFrame::OnCreate
中
m_wndCombo = CreateToolbarComboBox(hWndToolBar, ID_COMBO_PLACEHOLDER); m_wndCombo.AddString(...);
在 CMainFrame::OnIdle
中
if (GetFocus() != m_wndCombo) { // Check combo selection is accurate }
请注意,更新组合框的 OnIdle
代码首先检查组合框是否没有焦点,这避免了一些潜在的讨厌的闪烁。
示例程序演示了其运行情况。
幕后
自从 Win95 发布以来,人们一直希望向工具栏添加控件,而标准做法大致如下:
- 将占位符按钮更改为分隔符,因为由于设计上的一个怪癖,分隔符的宽度可以更改。
- 将分隔符的宽度更改为与您要创建的控件的宽度匹配。
- 使用分隔符按钮矩形作为大小创建控件,并将工具栏作为控件的父级。
多年来,这对于各种框架应用程序都运行良好,也是我撰写本文时选择的方法。然而,我的第一次尝试最终看起来像这样:
问题在于,如果您使用 Windows XP 视觉样式,工具栏分隔符不再像经典模式或运行 Win9x/Win2000 时那样是空白区域 - 相反,会绘制一条垂直线。从上面的图像中可以看出,这看起来有点奇怪。起初,我以为这将是一个噩梦般的解决方案,需要一个讨厌而复杂的 NM_CUSTOMDRAW
hack。人们将占位符按钮更改为分隔符的原因是它们可以是任意宽度,而普通按钮的宽度是固定的......真的吗?当然不是 - 带有文本的按钮显然可以改变宽度以匹配文本,这为我提供了以下解决方案:
- 将占位符按钮的样式更改为
BTNS_SHOWTEXT
。 - 确保按钮状态被禁用。否则,将鼠标悬停在组合框下方会导致绘制一个幻影按钮。
另一个小问题是组合框的高度可能比工具栏本身小得多(特别是如果像我一样,您启用了大字体),所以我还添加了一些代码来居中它。
最后一点是组合框使用的字体。起初,我只使用了系统 GUI 库存字体,但这与用于显示工具栏按钮文本的字体略有不同。经过一番深入研究,似乎使用了菜单字体,所以我结合使用 SystemParametersInfo
和 SPI_GETNONCLIENTMETRICS
来获取 LOGFONT
,这样我就可以创建一份副本以用于组合框。
ToolBarHelper.h
这是完整的 CToolBarHelper
#pragma once // Define various toolbar button styles in case they are missing #ifndef TBSTYLE_EX_MIXEDBUTTONS #define TBSTYLE_EX_MIXEDBUTTONS 0x00000008 #endif #ifndef BTNS_SHOWTEXT #define BTNS_SHOWTEXT 0x0040 #endif #define ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX (ATL_SIMPLE_TOOLBAR_PANE_STYLE|TBSTYLE_LIST) /// Class used to expost useful toolbar /// functionality to a WTL CFrameWnd-derived class template <class T> class CToolBarHelper { private: /// Wrapper class for the Win32 TBBUTTONINFO structure. class CTBButtonInfo : public TBBUTTONINFO { public: /// Constructor CTBButtonInfo(DWORD dwInitialMask = 0) { memset(this, 0, sizeof(TBBUTTONINFO)); cbSize = sizeof(TBBUTTONINFO); dwMask = dwInitialMask; } }; /// Wrapper class for the Win32 TBBUTTON structure. class CTBButton : public TBBUTTON { public: /// Constructor CTBButton() { memset(this, 0, sizeof(TBBUTTON)); } }; private: CFont m_fontCombo; ///< Font to use for comboboxes CSimpleMap<UINT, UINT> m_mapMenu; ///< Map of command IDs -> menu IDs public: /// Message map BEGIN_MSG_MAP(CToolbarHelper<T>) COMMAND_CODE_HANDLER(CBN_SELCHANGE, OnSelChangeToolBarCombo) NOTIFY_CODE_HANDLER(TBN_DROPDOWN, OnToolbarDropDown) END_MSG_MAP() /// Modify a toolbar button to have a drop-down button void AddToolBarDropDownMenu(HWND hWndToolBar, UINT nButtonID, UINT nMenuID) { ATLASSERT(hWndToolBar != NULL); ATLASSERT(nButtonID > 0); // Use built-in WTL toolbar wrapper class CToolBarCtrl toolbar(hWndToolBar); // Add the necessary style bit (TBSTYLE_EX_DRAWDDARROWS) if // not already present if ((toolbar.GetExtendedStyle() & TBSTYLE_EX_DRAWDDARROWS) != TBSTYLE_EX_DRAWDDARROWS) toolbar.SetExtendedStyle(toolbar.GetExtendedStyle() | TBSTYLE_EX_DRAWDDARROWS); // Get existing button style CTBButtonInfo tbi(TBIF_STYLE); if (toolbar.GetButtonInfo(nButtonID, &tbi) != -1) { // Modify the button tbi.fsStyle |= TBSTYLE_DROPDOWN; toolbar.SetButtonInfo(nButtonID, &tbi); // We need to remember that this menu // ID is associated with the button ID // so use a basic map for this. m_mapMenu.Add(nButtonID, nMenuID); } } LRESULT OnToolbarDropDown(int /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/) { // Get the toolbar data NMTOOLBAR* ptb = reinterpret_cast<NMTOOLBAR*>(pnmh); // See if the button ID has an asscociated menu ID UINT nMenuID = m_mapMenu.Lookup(ptb->iItem); if (nMenuID) { // Get the toolbar control CToolBarCtrl toolbar(pnmh->hwndFrom); // Get the button rect CRect rect; toolbar.GetItemRect(toolbar.CommandToIndex(ptb->iItem), &rect); // Create a point CPoint pt(rect.left, rect.bottom); // Map the points toolbar.MapWindowPoints(HWND_DESKTOP, &pt, 1); // Load the menu CMenu menu; if (menu.LoadMenu(nMenuID)) { CMenuHandle menuPopup = menu.GetSubMenu(0); ATLASSERT(menuPopup != NULL); T* pT = static_cast<T*>(this); // Allow the menu items to be initialised (for example, // new items could be added here for example) pT->PrepareToolBarMenu(nMenuID, menuPopup); // Display the menu // Using command bar TrackPopupMenu method means menu icons are displayed pT->m_CmdBar.TrackPopupMenu(menuPopup, TPM_RIGHTBUTTON|TPM_VERTICAL, pt.x, pt.y); } } return 0; } /// Override this Allow the menu items to be enabled/checked/etc. virtual void PrepareToolBarMenu(UINT /*nMenuID*/, HMENU /*hMenu*/) { } /// Add text to a toolbar button void AddToolbarButtonText(HWND hWndToolBar, UINT nID, LPCTSTR lpsz) { // Use built-in WTL toolbar wrapper class CToolBarCtrl toolbar(hWndToolBar); // Set extended style if ((toolbar.GetExtendedStyle() & TBSTYLE_EX_MIXEDBUTTONS) != TBSTYLE_EX_MIXEDBUTTONS) toolbar.SetExtendedStyle(toolbar.GetExtendedStyle() | TBSTYLE_EX_MIXEDBUTTONS); // Get the button index int nIndex = toolbar.CommandToIndex(nID); CTBButton tb; toolbar.GetButton(nIndex, &tb); int nStringID = toolbar.AddStrings(lpsz); // Alter the button style tb.iString = nStringID; tb.fsStyle |= TBSTYLE_AUTOSIZE|BTNS_SHOWTEXT; // Delete and re-insert the button toolbar.DeleteButton(nIndex); toolbar.InsertButton(nIndex, &tb); } /// Add resource string to a toolbar button void AddToolbarButtonText(HWND hWndToolBar, UINT nID, UINT nStringID) { CString str; if (str.LoadString(nStringID)) AddToolbarButtonText(hWndToolBar, nID, str); } /// Add text to a toolbar button (using tooltip text) void AddToolbarButtonText(HWND hWndToolBar, UINT nID) { TCHAR sz[256]; if (AtlLoadString(nID, sz, 256) > 0) { // Add the text following the '\n' TCHAR* psz = _tcsrchr(sz, '\n'); if (psz != NULL) { // Skip to first character of the tooltip psz++; // The tooltip text may include the accelerator, i.e. // Open (Ctrl+O) // So look for an open brace TCHAR* pBrace = _tcschr(psz, '('); if (pBrace != NULL) *(pBrace - 1) = '\0'; AddToolbarButtonText(hWndToolBar, nID, psz); } } } /// Create a combobox on a toolbar HWND CreateToolbarComboBox(HWND hWndToolBar, UINT nID, UINT nWidth = 16, UINT nHeight = 16, DWORD dwComboStyle = CBS_DROPDOWNLIST) { T* pT = static_cast<T*>(this); // Use built-in WTL toolbar wrapper class CToolBarCtrl toolbar(hWndToolBar); // Get the size of the combobox font CreateComboFont(); CSize sizeFont = GetComboFontSize(); // Compute the width and height UINT cx = (nWidth + 8) * sizeFont.cx; UINT cy = nHeight * sizeFont.cy; // Set the button width CTBButtonInfo tbi(TBIF_SIZE|TBIF_STATE|TBIF_STYLE); // Make sure the underlying button is disabled tbi.fsState = 0; // BTNS_SHOWTEXT will allow the button size to be altered tbi.fsStyle = BTNS_SHOWTEXT; tbi.cx = static_cast<WORD>(cx); toolbar.SetButtonInfo(nID, &tbi); // Get the index of the toolbar button int nIndex = toolbar.CommandToIndex(nID); // Get the button rect CRect rc; toolbar.GetItemRect(nIndex, rc); rc.bottom = cy; // Create the combobox DWORD dwStyle = WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_TABSTOP|dwComboStyle; CComboBox combo; combo.Create(pT->m_hWnd, rc, NULL, dwStyle, 0, nID); combo.SetFont(m_fontCombo); combo.SetParent(toolbar); // The combobox might not be centred vertically, and we won't know the // height until it has been created. Get the size now and see if it // needs to be moved. CRect rectToolBar; CRect rectCombo; toolbar.GetClientRect(&rectToolBar); combo.GetWindowRect(rectCombo); // Get the different between the heights of the toolbar and // the combobox int nDiff = rectToolBar.Height() - rectCombo.Height(); // If there is a difference, then move the combobox if (nDiff > 1) { toolbar.ScreenToClient(&rectCombo); combo.MoveWindow(rectCombo.left, rc.top + (nDiff / 2), rectCombo.Width(), rectCombo.Height()); } return combo; } /// Create the font to use for comboboxes void CreateComboFont() { if (m_fontCombo == NULL) { NONCLIENTMETRICS ncm; ncm.cbSize = sizeof(NONCLIENTMETRICS); ::SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0); // Create menu font m_fontCombo.CreateFontIndirect(&ncm.lfMenuFont); ATLASSERT(m_fontCombo != NULL); } } /// Get the size of the default GUI font CSize GetComboFontSize() { ATLASSERT(m_fontCombo != NULL); // We need a temporary DC const T* pT = static_cast<const T*>(this); CClientDC dc(pT->m_hWnd); // Select in the menu font CFontHandle fontOld = dc.SelectFont(m_fontCombo); // Get the font size TEXTMETRIC tm; dc.GetTextMetrics(&tm); // Done with the font dc.SelectFont(fontOld); // Return the width and height return CSize(tm.tmAveCharWidth, tm.tmHeight + tm.tmExternalLeading); } LRESULT OnSelChangeToolBarCombo(WORD /*wNotifyCode*/, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/) { T* pT = static_cast<T*>(this); // Get the newly selected item index CComboBox combo(hWndCtl); int nSel = combo.GetCurSel(); // Get the item text CString strItemText; combo.GetLBText(nSel, strItemText); // Get the item data DWORD dwItemData = combo.GetItemData(nSel); // Call special function to handle the selection change pT->OnToolBarCombo(combo, wID, nSel, strItemText, dwItemData); // Set focus to the main window pT->SetFocus(); return TRUE; } };
示例应用
提供了两个示例应用程序 - 一个用于 Visual Studio 6,一个用于 Visual Studio 2005。我使用的是 WTL 7.5,但我认为它应该适用于早期版本。请注意,代码符合 Unicode 标准,Visual Studio 2005 示例包括 Unicode 发布和调试版本。
示例应用程序演示了所有可用的 CToolBarHelper
方法:
- 单击“新建”按钮下拉菜单以显示动态修改的菜单。
- 使用第一个组合框选择视图颜色。
- 单击“颜色”按钮以循环切换颜色。
- 单击“颜色”按钮下拉菜单以显示颜色菜单,包括按钮图标。请注意,更改颜色将导致第一个组合框更新。
- 第二个组合框只是在
OnCreate
中添加,从未通过OnIdle
更新。 - “保存”、“粘贴”和“关于”按钮文本已使用各种
AddToolbarButtonText
方法添加。
改进
如果您对如何改进此代码有任何想法或建议,请随时发布。我有一个想法是添加对向工具栏添加编辑控件的支持,这应该不会太难。