65.9K
CodeProject 正在变化。 阅读更多。
Home

WTL 工具栏助手

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (26投票s)

2006年12月11日

CPOL

9分钟阅读

viewsIcon

78440

downloadIcon

4157

向 WTL 工具栏添加文本、下拉菜单和组合框。

Sample Image - toolbar_helper.png

引言

这是一个特殊的工具栏助手模板类,您可以在基于 WTL 框架窗口的应用程序中使用它来实现以下功能:

  • 向工具栏按钮添加文本
  • 向工具栏按钮添加下拉菜单
  • 向工具栏添加组合框

这里提供的代码使得任何 WTL CFrameWnd 派生窗口都可以非常容易地显示专业的工具栏,我希望关于它如何工作的解释也能派上用场。

入门

首先,您需要包含头文件,最好是在您的 MainFrm.h

#include "ToolBarHelper.h"

接下来,从 CToolBarHelper 派生您的窗口类(可能是 CMainFrame

class CMainFrame :
 ...
 public CToolBarHelper<CMainFrame>

接下来,如果您想要工具栏下拉菜单/组合框,您需要在消息映射中添加一个 CHAIN_MSG_MAP 条目,以确保 CBN_SELCHANGETBN_DROPDOWN 消息得到正确处理

BEGIN_MSG_MAP(CMainFrame)
    ...
    CHAIN_MSG_MAP(CToolBarHelper<CMainFrame>)
END_MSG_MAP()

无论如何,这样做可能是一个好主意,以防您以后想添加下拉菜单/组合框。

CString,等等。

请注意,代码使用了 CStringCRectCSize 类。这意味着如果您在 Visual Studio 6 下编译,您将需要 #include <atlmisc.h>,而 Visual Studio 2005 用户可能希望使用新的 atlstr.hatltypes.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

很简单。

幕后

向工具栏按钮添加文本需要以下步骤:

  1. 确保工具栏设置了 TBSTYLE_EX_MIXEDBUTTONS 样式位。
  2. 调用 CToolBarCtrl::AddStrings 方法将按钮文本添加到工具栏的内部列表。
  3. 更改按钮信息,确保设置了 TBSTYLE_AUTOSIZEBTNS_SHOWTEXT 位。
  4. 删除然后重新插入工具栏按钮。

向工具栏按钮添加下拉菜单

为了向工具栏按钮添加下拉菜单,首先您需要使用 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..."));
 }
}

非常简单。

幕后

向工具栏按钮添加下拉菜单涉及以下步骤:

  1. 确保按钮本身分配了 TBSTYLE_EX_DRAWDDARROWS 样式。
  2. 处理 TBN_DROPDOWN 通知消息。
  3. 使用 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。组合框的宽度、高度和样式将分别默认为 1616CBS_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 发布以来,人们一直希望向工具栏添加控件,而标准做法大致如下:

  • 将占位符按钮更改为分隔符,因为由于设计上的一个怪癖,分隔符的宽度可以更改。
  • 将分隔符的宽度更改为与您要创建的控件的宽度匹配。
  • 使用分隔符按钮矩形作为大小创建控件,并将工具栏作为控件的父级。

多年来,这对于各种框架应用程序都运行良好,也是我撰写本文时选择的方法。然而,我的第一次尝试最终看起来像这样:

Sample screenshot

问题在于,如果您使用 Windows XP 视觉样式,工具栏分隔符不再像经典模式或运行 Win9x/Win2000 时那样是空白区域 - 相反,会绘制一条垂直线。从上面的图像中可以看出,这看起来有点奇怪。起初,我以为这将是一个噩梦般的解决方案,需要一个讨厌而复杂的 NM_CUSTOMDRAW hack。人们将占位符按钮更改为分隔符的原因是它们可以是任意宽度,而普通按钮的宽度是固定的......真的吗?当然不是 - 带有文本的按钮显然可以改变宽度以匹配文本,这为我提供了以下解决方案:

  • 将占位符按钮的样式更改为 BTNS_SHOWTEXT
  • 确保按钮状态被禁用。否则,将鼠标悬停在组合框下方会导致绘制一个幻影按钮。

另一个小问题是组合框的高度可能比工具栏本身小得多(特别是如果像我一样,您启用了大字体),所以我还添加了一些代码来居中它。

最后一点是组合框使用的字体。起初,我只使用了系统 GUI 库存字体,但这与用于显示工具栏按钮文本的字体略有不同。经过一番深入研究,似乎使用了菜单字体,所以我结合使用 SystemParametersInfoSPI_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 方法添加。

改进

如果您对如何改进此代码有任何想法或建议,请随时发布。我有一个想法是添加对向工具栏添加编辑控件的支持,这应该不会太难。

© . All rights reserved.