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

纯 C 语言中的停靠工具栏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (83投票s)

2005年4月28日

CPOL

41分钟阅读

viewsIcon

305594

downloadIcon

5613

如何在纯 C 语言(无 MFC、ATL、COM 或 WTL)中创建停靠的工具栏/窗口。

引言

本教程将向您展示如何仅使用标准的 Win32 图形和 Windows 函数(即,不使用 MFC、WTL、Automation 等)来创建停靠的工具窗口。停靠的工具窗口是指可以“附加”(即“停靠”)到另一个窗口的内边框,或者可以(由最终用户)“从”该边框“撕下”,自由浮动并由鼠标移动的窗口。允许停靠某些工具窗口的窗口称为所有者(或容器)窗口

一个浮动在其所有者窗口上方的工具窗口。

同一个工具窗口停靠在其所有者窗口的底部内边框。

首先,我们将讨论工具窗口的“浮动”方面——即如何让工具窗口保持浮动在所有其他窗口之上,如何正确处理窗口激活等。之后,我们将了解如何让这些浮动的工具窗口“停靠”到所有者窗口的一侧,并讨论各种窗口管理方法。

提供的源代码将最终构成一个名为 DockWnd.dll 的 Win32 DLL,其函数可由任何 Win32 程序使用,以轻松支持停靠的工具窗口。创建停靠窗口的方式有很多种。我估计互联网上大部分代码都使用了精心设计的 C++ 类来隐藏实现。然而,我仍然偏爱用 C 语言编码,因此此库的设计将采用非面向对象的方法,从而可以轻松地被任何语言编写的应用程序使用。

此代码/文章基于 James Brown 提供的原始免费代码。他的网站包含此代码的早期、不同版本(以及许多其他免费的 Win32 教程/示例)。

目录

DOCKINFO 结构

由于我们的停靠库需要维护一些关于它所管理的每个工具窗口的信息,因此我们需要一个结构来存储这些信息。我们将定义一个 DOCKINFO 结构(在 dockwnd.h 中),并为每个创建的工具窗口分配一个 DOCKINFO。应用程序将调用我们停靠库中的 DockingAlloc 函数来分配一个 DOCKINFO(初始化为默认值),然后将其传递给 DockingCreateFrame 函数,该函数创建与该 DOCKINFO 关联的实际工具窗口。此 struct 将保存信息,例如工具窗口的句柄 (HWND),工具窗口所有者窗口的句柄,工具窗口当前是停靠在所有者还是浮动,以及我们私有用于管理停靠工具窗口的其他信息。我们将工具窗口的句柄存储在 DOCKINFOhwnd 字段中。我们将所有者窗口的句柄存储在 DOCKINFOcontainer 字段中。DOCKINFOuDockedState 字段的值告诉我们工具窗口是浮动还是停靠。当工具窗口浮动时,此字段将与 DWS_FLOATING OR(因此为负值)。稍后我们将讨论其他 DOCKINFO 字段。

注意:应用程序负责创建和管理所有者窗口。我们的库仅处理创建/管理工具窗口。

我们为工具窗口注册了自己的窗口类(类名为“DockWnd32”)。此类窗口的过程(dockWndProc)位于我们的库中。我们使用工具窗口的 GWL_USERDATA 字段来存储指向该工具窗口的 DOCKINFO struct 的指针。这样,我们就可以仅通过特定工具窗口的句柄(HWND)轻松快速地获取相应的 DOCKINFO

我们不想限制应用程序只能有一个所有者窗口及其停靠窗口集。例如,一个应用程序可能有两个所有者窗口,每个窗口都有自己的停靠窗口集。我们也不想限制应用程序的工具窗口数量。因此,我们可能会被要求为许多工具窗口和所有者集创建 DOCKINFO。通过将工具窗口的 DOCKINFO 指针存储在工具窗口本身中,并将工具窗口及其所有者窗口的句柄存储在 DOCKINFO 中,我们可以轻松获取我们库所需的所有信息,而应用程序的工作量最少。

有时,我们的库需要能够枚举特定所有者窗口的所有工具窗口。我们稍后会详细介绍。在下面的某些示例代码中,我们将仅引用一个占位符函数 DockingNextToolWindow,您应假定该函数将获取特定所有者窗口的下一个工具窗口(实际上是该工具窗口的 DOCKINFO)。在库的实际源代码中,此占位符被更复杂的代码替换,我们将在后面进行检查。

DockingCreateFrame 如何创建一个浮动的工具窗口

浮动的工具窗口只是一个具有 WS_POPUP 样式的标准窗口。当使用所有者窗口创建弹出窗口时,弹出窗口的定位使其始终保持在该所有者窗口之上。这就是我们创建和显示浮动工具窗口的方式。

// Create a floating (popup) tool window
HWND hwnd = CreateWindowEx(
    WS_EX_TOOLWINDOW,
    "ToolWindowClass", "ToolWindow",
    WS_POPUP | WS_SYSMENU | WS_THICKFRAME | WS_CAPTION | WS_VISIBLE,
    200, 200, 400, 64,
    hwndOwner, NULL, GetModuleHandle(0), NULL
    );

注意:在上面的示例中,假定 hwndOwner 是应用程序创建的某个其他窗口的句柄,该窗口将成为我们工具窗口的所有者窗口。应用程序必须创建此窗口,然后将其句柄传递给 DockingCreateFrame。换句话说,在创建任何工具窗口之前,应用程序都必须创建所有者窗口。

WS_EX_TOOLWINDOW 扩展样式没有什么特别之处,除了使窗口具有较小的标题栏。它不会使窗口神奇地浮动——这是通过指定 WS_POPUP 样式和所有者窗口自动实现的。上面 CreateWindowEx 的外观可能如下所示。

一个浮动在其所有者窗口(“主窗口”)上方的工具窗口。

防止窗口失活

上图显示了所有者窗口(“主窗口”)的标题栏呈非活动状态。这是完全正常的,因为一次只有一个窗口可以获得输入焦点,并且操作系统通常只显示该一个窗口的标题栏呈活动状态。因此,当我们创建工具窗口时,操作系统会显示工具窗口为活动状态,而显示所有者窗口为非活动状态。

但是,工具窗口及其所有者窗口同时显示为活动状态是正常的做法。这样看起来更自然。因此,我们需要制定一个策略,使我们的工具窗口和所有者窗口都显示为活动状态,即使只有一个窗口实际上拥有输入焦点。

解决方案涉及 WM_NCACTIVATE 消息。当一个窗口的非客户区(标题栏和边框)需要激活或停用时,操作系统会发送此消息。与所有窗口消息一样,WM_NCACTIVATE 消息的发送带有两个参数 - wParamlParam。当一个窗口接收到 WM_NCACTIVATE 消息且 wParam=TRUE 时,表示标题栏和边框应显示为活动状态。当 wParam=FALSE 时,表示标题栏和边框应显示为非活动状态。

注意:MSDN 表明 WM_NCACTIVATElParam 始终为 0。但是,我观察到 lParam 指示正在停用的窗口的窗口句柄。这在 Win95、98 和 NT、2000、XP 下似乎是真的。我们的解决方案依赖于此未记录的功能。

因此,当我们创建工具窗口时,我们的所有者窗口会收到一个 WM_NCACTIVATE 消息,其中 wParam=FALSE,而我们的工具窗口会收到一个 WM_NCACTIVATE 消息,其中 wParam=TRUE

当此消息传递给 DefWindowProc() 时,操作系统会执行两项操作。首先,标题栏会根据 wParam 分别是 TRUE 还是 FALSE 来绘制为活动或非活动状态。其次,操作系统会为该窗口设置一个内部标志,该标志会记住该窗口是绘制为活动状态还是非活动状态。这使得 DefWindowProc() 能够处理后续的 WM_NCPAINT 消息,以绘制具有正确激活状态的标题栏。建议始终将 WM_NCACTIVATE 传递给 DefWindowProc(),以便设置此内部标志,即使您还自己处理 WM_NCACTIVATE

WM_NCACTIVATE 消息为我们提供了一种方法,使我们所有的工具窗口和所有者窗口都可以看起来处于活动状态,即使只有一个窗口实际上拥有焦点。为此,无论何时我们的工具窗口或所有者窗口接收到 WM_NCACTIVATE,当我们将 WM_NCACTIVATE 传递给 DefWindowProc() 时,我们都始终用 TRUE 替换 wParam。结果是操作系统始终将我们的工具窗口和所有者窗口的标题栏渲染为活动状态。

我们可以将以下代码添加到我们所有工具窗口和所有者窗口的窗口过程中,以同时显示它们都处于活动状态。

case WM_NCACTIVATE:
    return DefWindowProc(hwnd, msg, TRUE, lParam);

工具窗口及其所有者窗口都显示为活动状态。

顺便说一句,MDI 子窗口也使用此相同技术来保持其标题栏处于活动状态。唯一的区别是 MDI 窗口具有 WS_CHILD 样式,而不是 WS_POPUP

显示所有工具窗口为活动状态

上述方法似乎能达到我们的目的,但有一个问题。即使我们的应用程序不在前台,我们的所有者窗口和工具窗口也将始终显示为活动状态。例如,如果最终用户切换到某个其他应用程序的窗口,我们的工具窗口和所有者窗口仍然会显示为活动状态,这可能会让最终用户感到不安。

此外,每当我们显示消息框或普通对话框时,所有者窗口和工具窗口仍将显示为活动状态,而在此场景下,我们理想情况下希望它们显示为非活动状态。

这需要更仔细地研究窗口激活消息。以下是当一个窗口变为活动状态,另一个窗口变为非活动状态时发送的一系列窗口激活消息的描述:

  1. WM_MOUSEACTIVATE 消息会发送给即将变为活动状态的窗口,询问是否允许激活请求。您的窗口过程返回的值(即 MA_ACTIVATEMA_NOACTIVATE)会影响后续的激活消息。
  2. 如果属于不同应用程序的窗口即将变为活动状态(或非活动状态),则会发送 WM_ACTIVATEAPP。此消息会发送给当前处于活动状态的窗口(通知它即将变为非活动状态),以及即将变为活动状态的窗口。返回值应始终为零,并且从不影响后续消息的行为。
  3. 如上所述,当一个窗口的非客户区需要显示为活动或非活动状态时,会发送 WM_NCACTIVATE
  4. WM_ACTIVATE 是最后发送给即将变为活动状态的窗口的消息。当此消息传递给 DefWindowProc() 时,操作系统会将输入焦点设置到该窗口。

对于所有这些激活消息,实际上只涉及两个窗口——正在停用的窗口和正在激活的窗口。因此,即使我们有许多浮动的工具窗口,也不是所有它们都会收到这些消息。只有正在激活的窗口和正在停用的窗口会收到消息。但是,为了让事情看起来和感觉正确,我们希望每个工具窗口的显示状态(即,工具窗口的标题栏显示为活动还是非活动)与其他所有工具窗口的状态相同。因此,即使并非所有窗口都会收到上述消息,我们仍然需要所有窗口同步到同一状态。

每当我们需要禁用或启用我们的所有者窗口时,上述讨论同样适用。如果所有者窗口需要被禁用或启用,我们希望将所有工具窗口同步到相同的状态。(但是,窗口被禁用或启用时会发送不同的消息。)

因此,我们的停靠库需要做一些工作,才能使所有东西看起来和感觉都恰当。

  1. 当我们的应用程序被激活/停用时,我们需要同步所有工具窗口之间的活动/非活动显示。

    这也适用于我们应用程序内部的激活。例如,如果用户激活了我们创建的非工具窗口,那么我们希望所有工具窗口显示为非活动状态。如果用户从该窗口切换回工具窗口,我们希望所有工具窗口显示为活动状态。

  2. 当所有者窗口由于模态对话框或消息框的显示而被禁用时,我们必须禁用所有工具窗口(以及任何无模式对话框),以防止用户在模态对话框/消息框显示期间与之交互。

初步解决方案

我们的第一个解决方案将集中在 WM_ACTIVATE 消息上。每当窗口被激活或停用时,都会收到此消息。我们将采取的方向是判断接收此消息的窗口是处于活动状态还是非活动状态,并通过手动发送“模拟” WM_NCACTIVATE 消息来同步所有其他窗口到相同状态。此模拟消息将强制其他窗口更新其标题栏,使其与接收 WM_ACTIVATE 的窗口处于相同状态。

这是我们可以添加到我们的所有工具窗口或所有者窗口的窗口过程中的一个函数。每当其中一个窗口收到 WM_ACTIVATE 消息时,它将调用此函数来同步所有工具窗口的状态。

/*********************** DockingActivate() **********************
 * Sends WM_NCACTIVATE to all the owner's tool windows. A
 * tool or owner window calls this in response to receiving
 * a WM_ACTIVATE message.
 *
 * container =  Handle to owner window.
 * hwnd =       Handle to window which received WM_ACTIVATE (can
 *              be the owner, or one of its tool windows).
 * wParam =     WPARAM of the WM_ACTIVATE message.
 * lParam =     LPARAM of the WM_ACTIVATE message.
 */

LRESULT WINAPI DockingActivate(HWND container, 
               HWND hwnd, WPARAM wParam, LPARAM lParam)
{
   DOCKINFO * dwp;
   BOOL       fKeepActive;

   fKeepActive = (wParam != WA_INACTIVE);

   // Get the DOCKINFO of the next tool window for this owner window. when 0
   // is returned, there are no more tool windows for this owner.
   while ((dwp = DockingNextToolWindow(container)))
   {
      // Sync this tool window to the same state as the window that called
      // DockingActivate.
      SendMessage(dwp->hwnd, WM_NCACTIVATE, fKeepActive, 0);
   }

   // Allow the window that called DockingActivate to handle its WM_NCACTIVATE
   // as normally it would.
   return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
}

它在一定程度上有效。所有工具窗口都会正确激活和停用,并且同时进行。但这个解决方案不是最好的。

问题在于,每当活动窗口更改时,每个工具窗口的标题栏都会闪烁。这是因为操作系统发送 WM_ACTIVATE 消息的方式。此消息首先发送给正在停用的窗口。如果恰好是一个工具窗口或所有者窗口,它将调用 DockingActivate 来停用所有工具窗口。然后,WM_ACTIVATE 消息会发送给活动窗口。如果该窗口恰好也是一个工具窗口或所有者窗口,它将调用 DockingActivate 来(正确地)激活所有工具窗口。正是因为 DockingActivate 被快速调用了两次(一次停用工具窗口,然后又激活它们),导致所有窗口闪烁。

部分解决方案是在停用工具窗口之前执行检查。我们知道,如果一个窗口正在被停用,lParam 会标识即将被激活的(另一个)窗口。如果这个另一个窗口是我们的工具窗口之一,我们可以跳过停用工具窗口,因为我们知道另一个(工具)窗口之后会自行激活它们。

if (fKeepActive == FALSE)
{
   while ((dwp = DockingNextToolWindow(container)))
   {
      if (dwp->hwnd == (HWND)lParam)
         return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
   }
}

这可以防止每个工具窗口短暂地停用然后再次激活。仍然存在一个问题,尽管是小问题。问题是,那个正在被停用的单个工具窗口仍然会短暂闪烁,然后再次激活。这是因为该窗口已经收到了它的 WM_NCACTIVATE 消息,这导致窗口被绘制为非活动状态。最终窗口会获得激活的外观,但这种短暂的闪烁仍然可见。

同步所有工具窗口的激活状态

我们需要退一步,从稍微不同的方向来解决问题。与其处理 WM_ACTIVATE(在窗口标题栏重绘之后调用),不如直接解决问题核心,重写 DockingActivate,使其在窗口接收到 WM_NCACTIVATE 消息时调用。这将确保不会发生不必要的激活或停用。

下面提供的函数代表调用 DockingActivate 的工具(或所有者)窗口执行多项任务:

  1. 搜索列表以查找另一个正在激活/停用的窗口(由 lParam 指定,而不是接收 WM_NCACTIVATE 的窗口)。如果这个另一个窗口是一个工具窗口,那么我们就强制所有工具窗口处于激活状态。
  2. 将所有当前工具窗口同步到我们的(可能的新)状态。
  3. 根据我们的新状态,激活/停用调用 DockingActivate 的窗口。

代码如下:

LRESULT WINAPI DockingActivate(HWND container, 
            HWND hwnd, WPARAM wParam, LPARAM lParam)
{
   DOCKINFO * dwp;
   BOOL       fKeepActive;
   BOOL       fSyncOthers;

   // If this is a spoof'ed message we sent, then handle it
   // normally (but reset LPARAM to 0).
   if (lParam == -1)
      return DefWindowProc(hwnd, WM_NCACTIVATE, wParam, 0);

   fKeepActive = wParam;
   fSyncOthers = TRUE;

   while ((dwp = DockingNextToolWindow(container)))
   {
      // UNDOCUMENTED FEATURE:
      // If the other window being activated/deactivated (i.e. not the one that
      // called here) is one of our tool windows, then go (or stay) active.
      if ((HWND)lParam == dwp->hwnd)
      {
         fKeepActive = TRUE;
         fSyncOthers = FALSE;
         break;
      }
   }

   if (fSyncOthers == TRUE)
   {
      // Sync all other tool windows to the same state.
      while ((dwp = DockingNextToolWindow(container)))
      {
         // Send a spoof'ed WM_NCACTIVATE message to this tool window,
         // but not if it is the same window that called here. Note that
         // we substitute a -1 for LPARAM to indicate that this is a
         // spoof'ed message we sent. The operating system would never
         // send a WM_NCACTIVATE with LPARAM = -1.
         if (dwp->hwnd != hwnd && hwnd != (HWND)lParam)
            SendMessage(dwp->hwnd, WM_NCACTIVATE, fKeepActive, -1);
      }
   }

   return DefWindowProc(hwnd, WM_NCACTIVATE, fKeepActive, lParam);
}

上面的代码使用了 WM_NCACTIVATE 消息的一个未记录功能,这是我在试验这些激活消息时观察到的。MSDN 文档指出 lParam 未使用(可能为零),但在 Windows 95、98、ME、NT、2000、XP 下并非如此。

相反,lParam 是正在代替我们激活/停用的另一个窗口的句柄(即,如果我们正在被停用,lParam 将是正在激活的窗口的句柄)。这并不总是这样,特别是当另一个正在激活/停用的窗口属于另一个进程时。在这种情况下,lParam 将为零。

同步所有工具窗口的启用状态

现在,我们需要处理另一个问题。当我们的所有者窗口被禁用时(也许是因为模态对话框或消息框弹出),我们也需要禁用所有工具窗口。此功能可防止用户在模态对话框或消息框显示期间单击和激活主窗口以及任何工具窗口。

解决方案类似于我们解决激活问题的方式,只是这次我们编写了一个函数,工具窗口或所有者窗口在接收到 WM_ENABLE 消息时调用该函数。DockingEnable 仅将所有工具窗口启用/禁用到与所有者窗口相同的状态。

/*********************** DockingEnable() **********************
 * Sends WM_ENABLE to all the owner's tool windows.
 * A window calls this in response to receiving a
 * WM_ENABLE message.
 *
 * container =  Handle to owner window.
 * hwnd =       Handle to window which received WM_ENABLE (can
 *              be the owner, or one of its tool windows).
 * wParam =     WPARAM of the WM_ENABLE message.
 * lParam =     LPARAM of the WM_ENABLE message.
 */

LRESULT WINAPI DockingEnable(HWND container, 
               HWND hwnd, WPARAM wParam, >LPARAM lParam)
{
   DOCKINFO * dwp;

   while ((dwp = DockingNextToolWindow(container)))
   {
      // Sync this tool window to the same state as the window that called
      // DockingEnable (but not if it IS the window that called here).
      if (dwp->hwnd != hwnd) EnableWindow(dwp->hwnd, wParam);
   }

   // Allow the window that called DockingEnable to handle its WM_ENABLE
   // as normally it would.
   return DefWindowProc(hwnd, WM_ENABLE, wParam, lParam);
}

停靠一个工具窗口

之前的讨论带您了解了创建浮动工具窗口所需步骤。现在,我们将讨论让这些浮动窗口与所有者窗口“停靠”所需的技术。我不会在此教程中重现库的所有源代码,因为涉及的内容很多。我将概述所采用的方法,您可以研究注释详尽的源代码以获取详细信息。

首先,我们需要定义“停靠”和“未停靠”这两个术语。当工具窗口浮动时,它就是未停靠的。正如我们已经知道的那样,为了实现这一点,工具窗口必须具有 WS_POPUP 样式。

另一方面,当工具窗口完全包含在其所有者窗口内部,并沿所有者的一个边框时,它就是停靠的。为了实现这一点,我们必须使用 WS_CHILD(而不是 WS_POPUP)样式创建工具窗口(或将样式从 WS_POPUP 更改为 WS_CHILD),并使其所有者窗口也成为其父窗口。当工具窗口具有 WS_CHILD 样式时,操作系统会将其限制在其父窗口的内部区域,并且工具窗口在图形上“锚定”到其父窗口(即,当最终用户移动父窗口时,子窗口会自动随之移动)。

但请注意,当父窗口调整大小时,父窗口需要移动/调整停靠的工具窗口的大小,以便工具窗口保持“附加”到边框。(当然,我们的库中有函数可以供所有者窗口调用,使这尽可能容易。)

一个良好的停靠库必须允许最终用户通过用鼠标抓住工具窗口并将其拖动到可停靠或不可停靠的区域来停靠和取消停靠任何工具窗口。停靠窗口的实现方式有很多种。这是因为 Windows 中没有标准的、内置的停靠窗口支持。应用程序开发人员必须实现自己的停靠窗口,或者依赖第三方库来完成工作(例如 MFC)。

有两种常见的停靠窗口实现类型。最常见的(也是我个人认为最直观的)类型是您用鼠标抓住工具窗口(通过“抓手条”或标题栏),然后在屏幕上拖动它。当您拖动工具窗口时,窗口本身不会移动,而是在屏幕上 XOR 绘制一个拖动矩形,显示释放鼠标时窗口将移动到的位置的轮廓——就像关闭全窗口拖动时 Windows 的工作方式一样。使用此方法,当窗口拖动到/从窗口时,反馈矩形会发生可见变化,以指示窗口可以放置。这就是我们的停靠库使用的停靠实现。

正在拖动的工具窗口。您可以看到拖动矩形。

第二种停靠实现可以在一些较新风格的应用程序(例如 Microsoft Outlook)中找到。与反馈矩形不同,窗口可以即时“撕下”或“吸附”到所有者窗口上——即,一旦您操作它们,它们就会吸附到位。我个人不喜欢这种用户界面,而我们的停靠库也不使用它。

我们的工具窗口将具有以下特征:

  • 停靠的工具窗口的左侧将有一个“抓手条”,允许用户抓住它并取消停靠。
  • 工具窗口在屏幕上移动时将使用反馈(拖动)矩形——即使启用了“全窗口拖动”系统设置。这在上面的图片中已显示。
  • 当拖动矩形在屏幕上拖动时,在某个时候它会与所有者窗口的边框之一相交。发生这种情况时,拖动矩形需要发生可见变化,以反映工具窗口现在位于停靠“区域”内。正常约定是使用一个宽(例如三像素)的阴影矩形表示浮动位置,而使用一个单像素矩形表示停靠位置。
  • 当拖动工具窗口后释放鼠标时,必须进行测试以查看窗口是否应停靠或浮动。(即,拖动矩形最终是否移动到了这些停靠“区域”之一,或者它是否在任何此类区域之外,因此工具窗口是浮动的?)
  • 由最终用户决定,即使拖动矩形释放到可停靠区域上,也可以强制工具窗口浮动。这通常通过最终用户按住<Control> 键来实现。
  • 浮动时,工具窗口可以像任何普通窗口一样调整大小。在这种情况下,不需要特殊处理——可以使用标准的 Windows 调整大小行为。
  • 停靠时,工具窗口只能沿一个方向调整大小——垂直或水平(但不能同时)。这意味着我们只需要记住其宽度高度,而不是两者。如果工具窗口停靠在所有者的顶部或底部边框,那么我们记住其高度。如果工具窗口停靠在所有者的左侧或右侧边框,那么我们记住其宽度。无论我们记住哪个值,我们都将其存储在 DOCKINFOnDockedSize 字段中。至于其位置,它已存储在 DOCKINFOuDockedState 字段中。
  • 当用户双击浮动工具窗口的标题栏或停靠窗口的抓手条时,工具窗口将在浮动和停靠之间切换,反之亦然。

我们的停靠库会跟踪工具窗口是停靠还是浮动。如果已停靠,我们需要知道该工具窗口停靠在所有者的哪个边框上。DOCKINFOuDockedState 字段用于存储此状态。如前所述,如果此字段与 DWS_FLOATING OR,则工具窗口是浮动的。如果不与 DWS_FLOATING OR,则工具窗口已停靠,并且该字段的其余位是 DWS_DOCKED_LEFTDWS_DOCKED_RIGHTDWS_DOCKED_TOP DWS_DOCKED_BOTTOM,具体取决于工具窗口停靠在哪个边框上。

我们需要能够将工具窗口在子窗口(停靠)和弹出窗口(浮动)之间切换。这基本上是通过下面的代码实现的。

   // Assume "dwp" is a pointer to the tool window's DOCKINFO.

   DWORD dwStyle = GetWindowLong(dwp->hwnd, GWL_STYLE);
 
   // Is the window currently floating?
   if (dwp->uDockedState & DWS_FLOATING)
   {
       // Toggle from WS_POPUP to WS_CHILD. We do this by altering
       // the window's style flags to remove WS_POPUP, and add
       // WS_CHILD. Then, we set the owner window as the parent.
       SetWindowLong(dwp->hwnd, GWL_STYLE, (dwStyle & ~WS_POPUP) | WS_CHILD);
       SetParent(dwp->hwnd, dwp->container);
   }
   else
   {
       // Toggle from WS_CHILD to WS_POPUP. We do this by altering
       // the window's style flags to remove WS_CHILD, and add
       // WS_POPUP. Then, we make sure it has no parent.
       SetWindowLong(dwp->hwnd, GWL_STYLE, (dwStyle & ~WS_CHILD) | WS_POPUP);
       SetParent(dwp->hwnd, NULL);
   }

查看上面代码中的第二个 SetParent API 调用。将子窗口(停靠)变为弹出窗口(浮动)的唯一方法是将其父窗口设置为零(NULL)。由于工具窗口不再有父窗口,它就不会被限制在另一个窗口内部。它可以自由地在桌面上浮动。但由于它仍然有所有者窗口,操作系统会使其浮动在该所有者窗口之上。换句话说,当窗口停靠时,其所有者窗口也是其父窗口。当窗口浮动时,其所有者窗口也不再是其父窗口。

浮动与停靠尺寸

如前所述,工具窗口可以处于两种状态之一:停靠或浮动(未停靠)。我们将记住工具窗口在浮动状态和停靠状态下的尺寸,并将此信息存储在 DOCKINFO 中。这样,最终用户就可以为工具窗口的两种状态设置不同的尺寸。因为我们也允许最终用户通过双击抓手/标题栏在两种状态之间快速切换,所以我们需要记住工具窗口在两种状态下的最后位置。

当工具窗口浮动时,它可以像普通窗口一样调整大小。这意味着我们需要在 DOCKINFO 中存储宽度和高度。当然,为了记住它的位置,我们需要存储其 X 和 Y 位置(以屏幕坐标表示)。这些值分别存储在 DOCKINFOcxFloatingcyFloatingxposypos 字段中。

注意cxFloatingcyFloating 实际上设置为浮动工具窗口客户端(内部)区域的尺寸,而不是工具窗口本身的物理尺寸(包括其标题栏和边框)。这是因为我们希望客户端区域始终保持相同尺寸,即使系统设置发生变化(例如,通过控制面板修改标题栏高度)。

当工具窗口停靠时,它只能沿一个方向调整大小——垂直或水平。这意味着我们只需要记住其宽度高度,而不是两者。如果工具窗口停靠在所有者的顶部或底部边框,那么我们记住其高度。如果工具窗口停靠在所有者的左侧或右侧边框,那么我们记住其宽度。无论我们记住哪个值,我们都将其存储在 DOCKINFOnDockedSize 字段中。至于其位置,它已存储在 DOCKINFOuDockedState 字段中。

使用拖动矩形移动窗口

我们遇到的第一个障碍是让 Windows 在最终用户移动浮动窗口时显示反馈矩形。从 Windows 95 开始,引入了一项新的用户界面功能。此功能通常称为“拖动时显示窗口内容”。启用后,窗口不再使用标准的反馈矩形进行移动和大小调整。

不幸的是,无法为特定窗口关闭此功能。SystemParametersInfo API 调用(使用 SPI_GETDRAGFULLWINDOWS 设置)可以打开和关闭此功能,但这适用于整个系统,并不十分理想。当然,我们可以设计一种方法,在窗口移动期间暂时关闭拖动窗口系统设置(实际上,这会非常简单)。关键是,这有点像个 hack,我更喜欢为这类问题提供正确的解决方案。

唯一的解决方案是覆盖标准的 Windows 行为并手动提供反馈矩形。这意味着需要处理一些鼠标消息。现在,我不想展示任何代码——再次强调,源代码清楚地展示了如何实现这一点(在工具窗口的窗口过程中,dockWndProc)。我将提供所需的处理的基本大纲。

最重要的任务是阻止用户用鼠标拖动窗口。我知道这听起来适得其反,但我们需要完全接管标准的窗口移动逻辑。这实际上很简单——我们的停靠窗口过程只需要处理 WM_NCLBUTTONDOWN,并在鼠标点击标题栏区域时返回 0。通过阻止默认窗口过程处理此消息,窗口拖动将被完全禁用。

为了模拟窗口的移动,我们需要处理一些鼠标消息。只需要处理三个:

  1. WM_NCLBUTTONDOWN - 当最终用户单击工具窗口时会收到此消息。除了返回 0 以阻止操作系统执行正常的窗口拖动外,我们还在其初始位置绘制拖动矩形,并使用 SetCapture API 调用设置鼠标捕获。我们还安装了一个键盘钩子,以便我们可以检查最终用户是否按下了 CTRL 键(强制工具窗口浮动)或 ESC 键(中止操作)。
  2. WM_MOUSEMOVE - 鼠标移动时会收到此消息。我们的响应是(擦除旧位置并在新位置绘制)在新位置重绘拖动矩形。此外,我们需要根据最终用户是否已将矩形移动到可停靠区域来决定绘制哪种类型的矩形。
  3. WM_LBUTTONUP - 释放鼠标时会收到此消息。我们从屏幕上移除拖动矩形,释放鼠标捕获,然后采取适当的操作来实际重新定位工具窗口。这可能意味着停靠/取消停靠,或者如果窗口已经浮动,则仅移动窗口。

如您所见,这需要一些工作,但并不特别复杂。使用此方法的一个大优点是,当窗口停靠或浮动时,可以使用相同的鼠标代码。这使代码简短简洁。

绘制拖动矩形

拖动矩形基本上只是一个简单的矩形。理想情况下,这个矩形需要使用 XOR 位块传输逻辑来绘制,这样我们就可以在它在屏幕上移动时轻松地绘制/擦除它。

正在拖动的工具窗口。您可以看到拖动矩形。

下面的代码绘制一个具有指定坐标的阴影矩形。源代码中的等效函数比下面的代码做得更多(它绘制了两种类型的拖动矩形),但我将其简化以保持简单。

void DrawXorFrame(int x, int y, int width, int height)
{
    // Raw bits for bitmap - enough for an 8x8 monochrome image
    static WORD _dotPatternBmp1[] = 
    {
        0x00aa, 0x0055, 0x00aa, 0x0055, 0x00aa, 0x0055, 0x00aa, 0x0055
    };

    HBITMAP hbm;
    HBRUSH  hbr;
    HANDLE  hbrushOld;
    WORD    *bitmap;

    int border = 3;

    HDC hdc = GetDC(0);

    // Create a patterned bitmap to draw the borders
    hbm = CreateBitmap(8, 8, 1, 1, _dotPatternBmp1);
    hbr = CreatePatternBrush(hbm);

    hbrushOld = SelectObject(hdc, hbr);

    // Draw the rectangle in four stages - top, right, bottom, left
    PatBlt(hdc, x+border, y, width-border,  border, PATINVERT);
    PatBlt(hdc, x+width-border, y+border, border, height-border, PATINVERT);
    PatBlt(hdc, x, y+height-border, width-border, border, PATINVERT);
    PatBlt(hdc, x, y, border, height-border, PATINVERT);

    // Clean up
    SelectObject(hdc, hbrushOld);
    DeleteObject(hbr);
    DeleteObject(hbm);

    ReleaseDC(0, hdc);
}

如您所见,我们的矩形位图数据作为全局数据存在于我们的停靠库中。我们只需调用一些图形函数将其(以矩形形状)位块传输到屏幕上,在最终用户当前已将鼠标移动到的屏幕位置。

重绘停靠的窗口

当工具窗口的状态从停靠变为浮动,或反之亦然时,这意味着需要重绘所有者窗口的布局。例如,如果一个工具窗口之前是浮动的,然后停靠到所有者窗口,那么其他已经停靠的工具窗口可能需要调整大小/重新定位,以容纳新停靠的工具窗口。

如果一个工具窗口停靠在所有者窗口上,但被撕下并保持浮动,这意味着其他剩余的停靠窗口可能需要调整大小/重新定位,以填补先前停靠窗口留下的“空位”。

每当工具窗口的状态在状态之间切换时,我们的停靠库都有一个名为 updateLayout 的函数,该函数在被调用时向所有者窗口发送一个模拟的 WM_SIZE 消息,以通知它需要重绘自身。然后,所有者窗口应重绘其内容并调用名为 DockingArrangeWindows 的停靠库函数。DockingArrangeWindows 执行所有工作,以重新定位和重绘停靠的工具窗口。

枚举工具窗口

在上面的代码片段中,我们有一个占位符函数 DockingNextToolWindow,用于枚举给定所有者窗口的工具窗口。我们的停靠库中实际上没有这样的函数。让我们来看看我们的停靠库是如何实际枚举工具窗口的。

不幸的是,Windows 操作系统没有一个函数可以枚举由特定窗口拥有的所有窗口。如果有,我们可以直接将所有者窗口传递给该函数。操作系统确实有一个名为 EnumChildWindows 的函数。它枚举给定父窗口的所有子窗口。由于停靠的工具窗口也以其所有者窗口作为父窗口,因此 EnumChildWindows 将枚举给定所有者的所有停靠的工具窗口。但是 EnumChildWindows 不会枚举任何浮动的工具窗口,因为所有者窗口不是浮动工具窗口的父窗口。

还有另一个操作系统函数称为 EnumWindows。它枚举桌面上的所有顶级(即弹出)窗口。由于我们的浮动工具窗口是 WS_POPUP 样式,因此它适用于枚举它们。(但是,它除了我们的工具窗口之外,还会枚举桌面上的所有窗口,所以我们需要额外的工作来隔离所需的工具窗口)。EnumWindows 不枚举那些顶级窗口的任何子窗口(WS_CHILD 窗口)。因此,EnumWindows 不会枚举任何停靠的工具窗口。

因此,枚举所有工具窗口将是一个两步过程。首先,我们将调用 EnumChildWindows 来枚举给定父窗口(也恰好是所有者窗口)的停靠窗口。然后,我们将调用 EnumWindows 来枚举给定所有者的浮动工具窗口,并进行一些额外的处理,以确保我们隔离的窗口是针对所需所有者窗口的。

让我们看看一个计算给定所有者窗口拥有的总工具窗口数量(包括浮动和停靠)的函数。

typedef struct {
   UINT    count;
   HWND    container;
} DOCKCOUNTPARAMS;

/***************** DockingCountFrames() *****************
 * Counts the number of tool windows for the specified
 * owner window.
 *
 * container =   Handle to owner window.
 */

UINT WINAPI DockingCountFrames(HWND container)
{
   DOCKCOUNTPARAMS   dockCount;

   // Initialize count to 0, and store the desired owner window
   dockCount.count = 0;
   dockCount.container = container;

   // Enumerate/count the floating tool windows
   EnumWindows(countProc, (LPARAM)&dockCount);

   // Enumerate/count the docked tool windows
   EnumChildWindows(container, countProc, (LPARAM)&dockCount);

   // Return the total count
   return dockCount.count;
}

/******************* countProc() ********************
 * This is called by EnumChildWindows or EnumWindows
 * for each window.
 *
 * hwnd =       Handle of a window.
 * lParam =     The LPARAM arg we passed to EnumChildWindows
 *              or EnumWindows. That would be our DOCKCOUNTPARAMS.
 */

static BOOL CALLBACK countProc(HWND hwnd, LPARAM lParam)
{
   DOCKINFO *  dwp;
    
   // Is this one of the tool windows for the desired owner window?
   if (GetClassWord(hwnd, GCW_ATOM) == DockingFrameAtom &&
      (dwp = (DOCKINFO *)GetWindowLong(hwnd, GWL_USERDATA)) &&
      dwp->container == ((DOCKCOUNTPARAMS *)lParam)->container)
   {
      // Yes it is. Increment count.
      ((DOCKCOUNTPARAMS *)lParam)->count += 1;
   }

   // Tell operating system to continue.
   return TRUE;
}

请注意,我们将 countProc() 用作 EnumWindowsEnumChildWindows 的回调。并且我们将我们自己初始化的 DOCKCOUNTPARAMS 结构传递给我们的回调。首先,我们调用 EnumWindows 来枚举浮动窗口。然后,我们调用 EnumChildWindows 来枚举我们所需所有者的停靠窗口。所以,让我们看一下 countProc()。使此工作正常工作的关键在于获取并检查窗口的类 ATOM。如果它与我们在注册我们自己的停靠窗口类时获得的 ATOM(由 RegisterClassEx 返回)匹配,那么我们就知道这是我们的一个工具窗口。如果是我们的一个工具窗口,我们就知道它的 GWL_USERDATA 字段应该包含它的 DOCKINFO。请注意,所有者窗口句柄已存储在 DOCKINFOcontainer 字段中。因此,我们只需要将此句柄与传递给 DockingCountFrames 的所有者句柄进行比较,以确定它是否是所需所有者窗口的工具窗口。

库的各种其他功能

上面的讨论详细介绍了我们停靠库功能的所有最重要方面。但是,还有一些其他附带功能是可选的。您可以通过简单地将适当的值设置到 DOCKINFOdwStyle 字段中,来为给定的工具窗口启用任何这些功能。例如,您可以强制工具窗口始终保持停靠或浮动。您可以强制它保持其原始尺寸。您可以限制工具窗口可以停靠到所有者的哪些边框。

DockingAlloc 创建一个 DOCKINFO 时,这些额外的功能都不会被启用。


一个应用程序

到目前为止,我们只讨论了停靠库中的代码。由于库的整个目的是供应用程序使用,现在我们将注意力转向一个示例应用程序。有一个名为 DockTest 的示例 C 应用程序包含在该库中。此示例创建了一个所有者窗口。所有者窗口有一个 View -> Tool Window 菜单项,您可以选择该菜单项来创建工具窗口。然后,您可以移动工具窗口,进行停靠和取消停靠,以感受实现的工作方式。每次选择此菜单项时,都会创建一个新的工具窗口,因此您可以查看多个工具窗口如何浮动和停靠。

我们创建的所有者窗口是一个 MDI 窗口,其窗口过程是 frameWndProc。您可以使用 File -> New 菜单项打开文档窗口,并查看停靠的工具窗口如何与文档窗口交互。(但是,正如我们稍后将看到的,应用程序需要做一些工作来管理这种交互。)

创建一个工具窗口

让我们看看应用程序如何创建一个工具窗口。这发生在 View -> Tool Window 菜单项被选中时,因此创建工具窗口的位置是在 frameWndProc 处理菜单 ID IDM_VIEW_TOOLWINDOWWM_COMMAND 时。下面是创建工具窗口所需的简化版本:

void createToolWindow(HWND owner)
{
   DOCKINFO    *dw;
   HWND        frame;

   // Allocate a DOCKINFO structure.
   if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
   {
      // Create a Docking Frame window (ie, the tool window).
      if ((frame = DockingCreateFrame(dw, owner, "My title")))
      {
         // Create the child window that will be hosted inside of the Docking
         // Frame window (ie, the contents of the tool window's client area)
         // and save it in the DOCKINFO's focusWindow field. We'll create an
         // EDIT control to be the contents, but you can utilize any standard
         // control, or a child window of your own class.
         if((dw->focusWindow = CreateWindow("EDIT", 0, 
              ES_MULTILINE|WS_VSCROLL|WS_CHILD|WS_VISIBLE,
              0,0,0,0,
              frame,
              (HMENU)IDC_MYEDIT, GetModuleHandle(0), 0)))
         {
            // Show the Docking Frame.
            DockingShowFrame(dw);

            // Success!
            return;
         }

         // Destroy the tool window if we can't create its contents.
         // NOTE: The docking library will free the above DOCKINFO.
         DestroyWindow(frame);
      }
      MessageBox(0, "Can't create tool window", "ERROR", MB_OK);
   }
   else
      MessageBox(0, "No memory for a DOCKINFO", "ERROR", MB_OK);
}

首先,我们调用 DockingAlloc 来获取一个 DOCKINFO 结构。我们传递所需的初始状态,该状态将是 DWS_FLOATINGDWS_DOCKED_LEFTDWS_DOCKED_RIGHTDWS_DOCKED_TOPDWS_DOCKED_BOTTOM 之一,具体取决于我们是希望工具窗口最初创建为浮动状态,还是停靠在四个边框之一上。停靠库创建一个 DOCKINFO 并将其初始化为默认值,然后返回一个指针。

此时,我们可以修改 DOCKINFO,如果我们想要默认功能以外的东西。在上面的代码中,我们只是使用了默认值。

接下来,我们调用 DockingCreateFrame 来创建实际的工具窗口。我们传递刚刚获得的 DOCKINFO、我们所有者窗口的句柄以及工具窗口的所需标题(仅在工具窗口浮动时显示)。DockingCreateFrame 将创建工具窗口并返回其句柄。工具窗口不会创建为可见状态,因此屏幕上尚未显示任何内容。

现在,一个没有任何内容的工具窗口将没有太大用处。因此,我们需要在工具窗口内部创建对最终用户有用的东西。我们可以说工具窗口需要一些“内容”。具体来说,我们需要创建一个 WS_CHILD 窗口,该窗口以工具窗口为父窗口。这可以是任何标准控件,例如编辑框、列表框、树状视图控件等。或者它可以是我们自己的类的窗口。在上面的代码中,我们只是创建了一个多行编辑控件。请注意,我们将工具窗口设置为该控件的父窗口,并指定了 WS_CHILD 样式。这将导致编辑控件在图形上嵌入到工具窗口内部,并随工具窗口自动移动。编辑控件的大小和位置现在并不重要,因为它们将在工具窗口最终可见之前进行大小调整和定位。我们将此控件的句柄存入 DOCKINFOfocusWindow 字段。停靠库会自动调整此控件的大小以填充工具窗口的客户区,并在用户激活该工具窗口时自动将焦点赋予该控件。

最后,我们调用 DockingShowFrame。这首先向我们的所有者窗口发送一个 WM_SIZE 消息(我们将在此处完成工具窗口及其内容窗口的最终大小调整/定位),然后使工具窗口可见。

这就是创建工具窗口所需的所有内容。此时,停靠库将管理此窗口的停靠和取消停靠。

所有者中处理 WM_SIZE 消息

停靠库会透明地处理工具窗口的大部分方面。但有几次它需要应用程序的帮助。其中一次是所有者窗口调整大小时。给定所有者窗口的新尺寸,有理由认为任何停靠的工具窗口可能也需要调整大小和重新定位,以使其保持停靠在所有者的所需一侧。因此,所有者窗口在接收到 WM_SIZE 时必须执行以下操作:

   case WM_SIZE:
   {
      HDWP   hdwp;
      RECT   rect;

      // Do the default handling of this message.
      DefFrameProc(hwnd, MainWindow, msg, wParam, lParam);

      // Set the area where tool windows are allowed.
      // (Take into account any status bar, toolbar etc).
      rect.left = rect.top = 0;
      rect.right = LOWORD(lParam);
      rect.bottom = HIWORD(lParam);

      // Allocate enough space for all tool windows which are docked.
      hdwp = BeginDeferWindowPos(DockingCountFrames(hwnd, 
                         1) + 1); // + 1 for the MDI client

      // Position the docked tool windows for this owner
      // window. rect will be modified to contain the "inner" client
      // rectangle, where we can position an MDI client.
      DockingArrangeWindows(hwnd, hdwp, &rect);

      // Here we resize our MDI client window so that it fits into the area
      // described by "rect". Do not let it extend outside of this
      // area or it (and the client windows inside of it) will be obscured
      // by docked tool windows (or vice versa).
      DeferWindowPos(hdwp, MainWindow, 0, rect.left, rect.top, 
               rect.right - rect.left, rect.bottom - rect.top, 
               SWP_NOACTIVATE|SWP_NOZORDER);

      EndDeferWindowPos(hdwp);

      return 0;
   }

首先,我们将 WM_SIZE 传递给 DefFrameProc,让操作系统处理所有者窗口的默认大小调整。我们必须先这样做,以便在继续调整停靠工具窗口的大小/重新定位之前,所有者窗口的大小已经确定。

停靠库有一个名为 DockingArrangeWindows 的函数,该函数会重绘给定所有者的所有停靠窗口。因此,要完全重绘其停靠的窗口,所有者只需调用这一个函数即可。但有几个先决条件。首先,所有者必须用其客户区尺寸填充一个 RECT,并将其传递给 DockingArrangeWindows。其次,所有者窗口必须调用 Windows API BeginDeferWindowPos 来为所有停靠的窗口预留足够的空间。(停靠库有一个名为 DockingCountFrames 的函数,可以调用该函数来获取所有者中停靠工具窗口的总数。)我们使用 BeginDeferWindowPos,以便在有许多工具窗口时,我们将推迟最终绘制,直到所有窗口都调整好大小和定位为止。这样效率更高,并且不会给最终用户造成任何难看的视觉瑕疵。

一个非常重要的方面是,在 DockingArrangeWindows 调整了所有停靠的工具窗口的大小和位置后,它会更新 RECT,使其仅包含未被工具窗口占用的所有者客户区。换句话说,RECT 是剩余的空白客户区。我们将这个剩余区域用于调整/重新定位我们的 MDI 子窗口,使其仅填充这个剩余区域。这样,我们的文档窗口就不会被停靠的工具窗口遮挡,反之亦然。

所有者中处理 WM_NCACTIVATE 消息

另一次我们的停靠库需要应用程序帮助的情况是所有者窗口接收到 WM_NCACTIVATE 消息时。还记得我们之前讨论过如何使所有工具窗口的标题栏激活与所有者窗口同步。现在,我们需要所有者窗口在接收到 WM_NCACTIVATE 消息时通知停靠库。所有者窗口需要执行以下操作:

   case WM_NCACTIVATE:
   {
      DOCKPARAMS   dockParams;

      dockParams.container = dockParams.hwnd = hwnd;
      dockParams.wParam = wParam;
      dockParams.lParam = lParam;
      return(DockingActivate(&dockParams));
   }

我们只需用从 WM_ACTIVATE 消息接收到的值填充一个 DOCKPARAMS struct(在 DockWnd.h 中定义),并填充所有者窗口句柄(在 DOCKPARAMScontainer 字段中)和接收 WM_ACTIVATE 的窗口句柄(此处为所有者窗口)。然后我们调用 DockingActivate,它负责同步所有工具窗口的标题栏激活。

所有者中处理 WM_ENABLE 消息

另一次我们的停靠库需要应用程序帮助的情况是所有者窗口接收到 WM_ENABLE 消息时。还记得我们之前讨论过如何使所有工具窗口的启用状态与所有者窗口同步。现在,我们需要所有者窗口在接收到 WM_ENABLE 消息时通知停靠库。所有者窗口需要执行以下操作:

   case WM_ENABLE:
   {
      DOCKPARAMS   dockParams;

      dockParams.container = dockParams.hwnd = hwnd;
      dockParams.wParam = wParam;
      dockParams.lParam = lParam;
      return(DockingEnable(&dockParams));
   }

这与 WM_NCACTIVATE 处理几乎相同,但它与 WM_ENABLE 消息有关,并且我们调用了一个名为 DockingEnable 的函数。DockingEnable 负责将所有工具窗口的启用状态同步到所有者窗口。

处理发送到标准控件的消息

您会注意到我们使用了一个标准的编辑控件作为我们工具窗口的内容。如您所知,编辑窗口会在某些操作时向其父窗口发送消息。例如,当最终用户修改编辑控件的内容时,会发送一个 WM_COMMAND 消息,通知代码为 EN_CHANGE

但是请记住,父窗口是工具窗口,而我们的工具窗口过程(dockWndProc)位于停靠库中。那么应用程序如何获取该消息呢?

DOCKINFO 中有一个 DockMsg 字段。在该字段中,我们将指向我们应用程序中某个函数的指针填入。我们在 DockAlloc DOCKINFO 之后执行此操作,如下所示:

   // Allocate a DOCKINFO structure.
   if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
   {
      // Set our own function for the docking library to call.
      dw->DockMsg = myMessages;

      ...

每当 dockWndProc 接收到它不处理的消息时,例如 WM_COMMANDWM_NOTIFY,它就会调用我们的应用程序函数,并将工具窗口的 DOCKINFO 以及消息、WPARAMLPARAM 参数传递进去。

以下是我们为了处理来自 IDC_MYEDIT 编辑控件的 EN_CHANGE 而可以添加的函数示例:

LRESULT WINAPI myMessages(DOCKINFO * dwp, UINT message, WPARAM wParam, LPARAM lParam)
{
   switch (message)
   {
      case WM_COMMAND:
      {
         if (LOWORD(wParam) == IDC_MYEDIT)
         {
            if (HIWORD(wParam) == EN_CHANGE)
            {
               // Here we would handle the EN_CHANGE, and then return
               // 0 to tell the docking library we handled it.
               return 0;
           }
         }
      }
   }

   // Return -1 if we want the docking library to do default handling.
   return -1;
}

注意:每个 DOCKINFO 都可以有自己的 DockMsg 函数,因此您无需担心工具窗口之间的控件 ID 冲突。

工具窗口内的多个子窗口

上面,我们使用了一个编辑控件来填充工具窗口的客户区。但是,如果我们希望在工具窗口中有多个控件,例如,一个编辑控件和一个标有“Clear”的按钮,该按钮会清除编辑控件中的文本呢?

这是完全可能的,但有几个先决条件。首先,当我们创建编辑控件和按钮控件时,我们必须使它们都成为工具窗口的子窗口。其次,我们必须提供一个函数来调整控件的大小和重新定位它们,并在我们 DockAlloc DOCKINFO 之后将指向该函数的指针填入 DOCKINFODockResize 字段。

   // Allocate a DOCKINFO structure.
   if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
   {
      // Set our own functions for the docking library to call.
      dw->DockMsg = myMessages;
      dw->DockResize = myResize;

      ...

当停靠库调用我们的函数时,它会传递工具窗口的 DOCKINFO,以及一个包含我们需要填充的区域的 RECT。以下是我们为了调整编辑控件和按钮控件的大小和重新定位而可以添加的函数示例,以便按钮保持在工具窗口底部边框附近,而编辑控件填充其余区域:

void WINAPI< myResize(DOCKINFO * dwp, RECT * area)
{
   HWND   child;

   // Position the button above the bottom border
   child = GetDlgItem(dw->hwnd, IDC_MYBUTTON);
   SetWindowPos(child, 0, rect->left + 10, 
     rect->bottom - 20, 50, 18, SWP_NOZORDER|SWP_NOACTIVATE);

   // Let the edit fill the remaining area
   child = GetDlgItem(dw->hwnd, IDC_MYEDIT);
   SetWindowPos(child, 0, rect->left, rect->top, 
     rect->right - rect->left, (rect->bottom - rect->top) - 22, 

SWP_NOZORDER|SWP_NOACTIVATE);
}

关闭一个工具窗口

当所有者窗口被销毁时,它的所有工具窗口也会自动销毁(除非您使用 DWS_FREEFLOAT 样式。在这种情况下,您的所有者窗口应该处理 WM_DESTROY 并调用 DockingDestroyFreeFloat)。停靠库通常会释放每个被销毁工具窗口的 DOCKINFO

如果您希望手动关闭工具窗口,只需调用 Windows API DestroyWindow,并将要关闭的工具窗口的句柄传递进去。同样,停靠库通常会释放 DOCKINFO

如果您希望覆盖停靠库释放 DOCKINFO 的默认行为,那么您必须编写自己的函数,并将一个指针填入 DOCKINFODockDestroy 字段。停靠库将在工具窗口被销毁时调用此函数(并将 DOCKINFO 传递给它)。您有责任最终通过将其传递给 DockingFree 来释放 DOCKINFO。的一种用途是使 DOCKINFO 在您的应用程序的整个生命周期内保持分配状态(针对给定的工具窗口)。每次最终用户重新打开该工具窗口时,您都将使用相同的 DOCKINFODockingCreateFrame。因为 DOCKINFO 存储了工具窗口的最后尺寸和位置,这意味着工具窗口将重新出现在它上次被销毁之前的位置。您将在应用程序准备终止时才释放 DOCKINFO

保存/恢复工具窗口的尺寸/位置

每次运行应用程序时,您通常希望恢复用户上次运行应用程序时设置的工具窗口的相同尺寸和位置。停靠库提供了两个函数来帮助保存和恢复工具窗口的位置。数据被保存在 Windows 注册表中,位于您选择的某个键下。每个工具窗口的尺寸/位置作为该键下的值分别保存。

在释放工具窗口的 DOCKINFO 之前,您应该先创建/打开您选择的注册表项,以保存该工具窗口的设置。然后,您将 DOCKINFO 和指向该打开项的句柄传递给 DockingSavePlacement。停靠库将保存该工具窗口的设置。以下是我们如何将工具窗口的设置保存在注册表项“HKEY_CURRENT_USER\Software\MyKey\MyToolWindow”下的示例:

   HKEY   hKey;
   DWORD  temp;

   // Open/Create the "Software\MyKey\MyToolWindow" key under CURRENT_USER.
   if (!RegCreateKeyEx(HKEY_CURRENT_USER, "Software\\MyKey\\MyToolWindow",
      0, 0, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, 0, &hKey, &temp))
   {
      // Let the docking library save this tool window's settings.
      DockingSavePlacement(dw, hKey);

      // Close registry key.
      RegCloseKey(hKey);
   }

每当您的程序运行时,都应在 DockAlloc 该工具窗口的 DOCKINFO 之后立即调用 DockingLoadPlacement 来恢复这些设置。以下是恢复先前保存的设置的示例:

   // Allocate a DOCKINFO structure.
   if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
   {
      HKEY   hKey;

      // Open the "Software\\MyKey\\MyToolWindow" key under CURRENT_USER.
      if (!RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\MyKey\\MyToolWindow",
         o, KEY_ALL_ACCESS, &hKey))
      {
         // Let the docking library restore this tool window's settings.
         DockingLoadPlacement(dw, hKey);

         // Close registry key.
         RegCloseKey(hKey);
      }

      ...

注意:如果该工具窗口没有先前保存的设置,则不会更改任何 DOCKINFO 字段。

杂项说明

您的源代码需要 #include 文件 DockWnd.h。此外,如果您想与停靠库进行静态链接,则必须将文件 DockWnd.lib 提供给您的链接器。Visual C++ 项目文件已进行了这些设置。

为了更好地查看源代码,请将您的编辑器 TAB 宽度设置为 3。

在停靠库源代码中,所有以大写字母开头的函数都是应用程序可以调用的函数。所有以小写字母开头的函数仅在内部调用。所有以大写字母开头的变量都是全局变量。所有以小写字母开头的变量都是局部变量(在栈上传递或声明)。

在库和示例源代码中都包含了 Microsoft Visual C++(4.0 或更高版本)项目文件,以便您快速上手。

结论

源代码下载提供了一个完整的停靠窗口库,以及一个小型示例 C 应用程序,向您展示如何使用它。希望您能够将此库用于您自己的应用程序,并获得即时停靠窗口!

历史

  • 首次发布于 2005 年 4 月 27 日。
  • 于 2005 年 5 月 9 日更新,以修复调整停靠窗口大小时的错误。
© . All rights reserved.