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

C# 实现 Shell 功能,第三部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (221投票s)

2003年3月3日

Ms-PL

18分钟阅读

viewsIcon

683824

downloadIcon

17653

本文档介绍了应用程序桌面工具栏,这是一种可以像任务栏一样固定在屏幕边缘的应用程序。本文档将开发一个用于开发此类应用程序的基类。

Sample Image - csdoesshell3.jpg

引言

我知道在我上一篇文章中,我曾承诺开始解释如何扩展 shell,但我发现了一些非常有趣的信息,这些信息与 shell 有关,我必须在开始扩展 shell 之前写出来。

那么,这都是关于什么的?本文档是关于应用程序桌面工具栏,简称 Appbars。什么是 Appbars?Appbars 是可以对齐到屏幕边缘的应用程序。例如,ICQ 是一个 appbar,因为它可以在屏幕的右侧或左侧边缘对齐,当它被停靠时,如果您最大化其他窗口,它仍然可见,因为系统会告知其他窗口工作区域变小了。另一个著名的 appbar 示例是 `TaskBar`,是的,就是那个列出打开的应用程序的栏,我们都有。`taskbar` 可以对齐到所有边缘,但不能处于浮动状态。

在本文档中,我们将开发一个名为 `ApplicationDesktopToolbar` 的类。这个类继承自 `System.Windows.Forms.Form`。因此,当我们想让我们的应用程序表现得像一个 `appbar` 时,我们只需要继承 `ApplicationDesktopToolbar` 而不是 `System.Windows.Forms.Form`。

注意:理解本文档无需额外的阅读材料。但这里有一些链接,您可能想在阅读本文档后阅读它们。

MSDN:使用应用程序桌面工具栏

另一条注意:本文档添加的代码包含我们上一部分开始开发的 `ShellLib` 库的新版本。

第一部分:基础知识

现在我们知道了什么是应用程序桌面工具栏(我告诉过您,就像任务栏一样,但也可以浮动!),让我们开始吧。开发这样的应用程序有好消息也有坏消息。坏消息是,没有神奇的 API 可以为我们完成工作,我们需要自己动手。好消息是,操作系统会帮助我们知道何时使用坏消息部分中的函数。

让我们从好消息开始。操作系统提供了什么?操作系统提供有关可能影响我们窗口的窗口移动的信息,以便我们可以相应地采取行动。想象一下,您有一个 `appbar`,它对齐到屏幕底部,就在 `taskbar` 的位置,然后用户调整了 `taskbar` 的大小,在这种情况下,我们也应该调整我们 `appbar` 的大小。

操作系统提供的另一个信息是可用的工作空间。当我们想停靠我们的 `appbar` 时,我们需要知道要设置它的大小。同样,需要考虑系统中的其他 `appbar` 和 `taskbar`。

为了接收操作系统信息,我们所要做的就是注册到操作系统的一个特殊列表中。操作系统维护一个系统上所有 `appbar` 的列表,当发生可能影响 `appbar` 的事情时,操作系统会将消息发送到此列表中的 `appbar`,以便 `appbar` 可以响应(`移动`、`调整大小`、`消失` 等)。

您可能想知道如何完成此注册,或者操作系统如何通知我们更改。这将在下一部分中解释。

第二部分:与操作系统通信

操作系统和我们的程序如何通信?答案分为两部分。首先,操作系统如何与我们的程序通信?很简单,通过向我们的窗口过程发送窗口消息。但第二部分呢?我们如何与操作系统通信?我们借助一个名为 `SHAppBarMessage` 的 API 函数来与操作系统通信。此函数有两个参数,第一个是我们要发送给操作系统的消息,第二个参数是一个 `struct`,其中包含有关正在发送的消息的更多信息。

这是 C++ 中 `SHAppBarMessage` API 和 `APPBARDATA` `struct` 的原始声明。

UINT_PTR SHAppBarMessage( 
        DWORD dwMessage,        // Appbar message value to send.
        PAPPBARDATA pData);     // Address of an APPBARDATA structure.     
typedef struct _AppBarData {
    DWORD cbSize;
    HWND  hWnd;
    UINT  uCallbackMessage;
    UINT  uEdge;
    RECT  rc;
    LPARAM  lParam;
} APPBARDATA, *PAPPBARDATA;

以及 C# 等效项。

// Sends an appbar message to the system. 
[DllImport("shell32.dll")]
public static extern UInt32 SHAppBarMessage(
    UInt32 dwMessage,         // Appbar message value to send.
    ref APPBARDATA pData);    // Address of an APPBARDATA structure. The 
                              // content of the structure depends on the 
                              // value set in the dwMessage parameter. 
 
[StructLayout(LayoutKind.Sequential)]
public struct APPBARDATA
{
    public UInt32 cbSize;
    public IntPtr hWnd;
    public UInt32 uCallbackMessage;
    public UInt32 uEdge;
    public RECT rc;
    public Int32 lParam;
}

让我们来解释一下。`SHAppBarMessage` API 的第一个参数是 `dwMessage`,这个参数是要发送的消息。它可以是以下 `enum` 值之一:

public enum AppBarMessages
{
    // Registers a new appbar and specifies the message identifier
    // that the system should use to send notification messages to 
    // the appbar. 
    New                       = 0x00000000,    
    // Unregisters an appbar, removing the bar from the system's 
    // internal list.
    Remove                    = 0x00000001,
    // Requests a size and screen position for an appbar.
    QueryPos                  = 0x00000002,    
    // Sets the size and screen position of an appbar. 
    SetPos                    = 0x00000003,    
    // Retrieves the autohide and always-on-top states of the 
    // Microsoft® Windows® taskbar. 
    GetState                  = 0x00000004,    
    // Retrieves the bounding rectangle of the Windows taskbar. 
    GetTaskBarPos             = 0x00000005,    
    // Notifies the system that an appbar has been activated. 
    Activate                  = 0x00000006,    
    // Retrieves the handle to the autohide appbar associated with
    // a particular edge of the screen. 
    GetAutoHideBar            = 0x00000007,    
    // Registers or unregisters an autohide appbar for an edge of 
    // the screen. 
    SetAutoHideBar            = 0x00000008,    
    // Notifies the system when an appbar's position has changed. 
    WindowPosChanged          = 0x00000009,    
    // Sets the state of the appbar's autohide and always-on-top 
    // attributes.
    SetState                  = 0x0000000a    
}

消息将在后面解释。第二个参数 `pData` 包含我们发送消息时需要提供的额外信息。请注意,并非 `struct` 的所有字段都应填写。这取决于消息。

第三部分:注册我们的 AppBar

让我们的程序成为 `appbar` 的第一步是在操作系统的 `appbar` 列表中注册它。注册是通过发送 `AppBarMessages.New` 消息完成的。发送此消息时,应设置 `APPBARDATA` `struct` 的以下成员。`hWnd` 应设置为我们的窗口句柄,`uCallbackMessage` 应设置为操作系统将用于与我们的程序通信的唯一消息 ID。

让我们再回顾一遍,因为我知道这有点模糊。我们想将我们的窗口注册为 `appbar`,因此我们使用 `SHAppBarMessage` 来向操作系统发送 `AppBarMessages.New` 消息,其中包含我们的窗口句柄和一个唯一的消息 ID。当操作系统想通知我的窗口一些我应该知道的事情时,它会向我的窗口过程发送消息,操作系统发送的消息就是我在注册过程中提供的唯一消息 ID。操作系统可能会执行类似的操作:

SendMessage([window handle],[unique message id],[notify code],
            [notify extra info]);

因此,在我的窗口过程中,我需要响应唯一的 message ID,并且根据 `wParam`,我将知道操作系统发送给我的通知消息是什么。

为什么 message ID 必须是唯一的?因为我需要在我的窗口过程中响应它,我不能只是给一个随机值,因为它可能是一个有效 Message ID。那么,如何获得一个不适合任何窗口消息的数字呢?为此,我们使用 API 函数 `RegisterWindowMessage`,该函数接收一个 `string` 并返回附加到该 `string` 的 Message ID。如果 `string` 不存在,它会创建一个新的唯一 Message ID 并返回它。此 API 通常用于同一台计算机上的两个应用程序之间的通信(每个应用程序都使用相同的 `string` 调用此 API,它们都获得相同的 Message ID,现在它们可以通过发送此 Message ID 来通信)。因此,我们需要声明此函数才能使用它,这是 C++ 声明。

UINT RegisterWindowMessage(
LPCTSTR lpString);   // message string

以及 C# 等效项。

// The RegisterWindowMessage function defines a new window message that is 
// guaranteed to be unique throughout the system. The message value can be 
// used when sending or posting messages. 
[DllImport("user32.dll")]
public static extern UInt32 RegisterWindowMessage(
    [MarshalAs(UnmanagedType.LPTStr)]
    String lpString);    // Pointer to a null-terminated string that 
                         // specifies the message to be registered. 

现在我们知道了如何完成 `appbar` 的注册,让我们看一些执行此操作的代码。首先,这是我们如何获取新的唯一 Message ID 以供以后使用,此函数在构造函数中调用。

private UInt32 RegisterCallbackMessage()
{
    String uniqueMessageString = Guid.NewGuid().ToString();
    return ShellApi.RegisterWindowMessage(uniqueMessageString);
}

没什么好解释的,首先我通过生成一个 `GUID` 来获取唯一的 `string`,然后我使用这个 `string` 来获取 Message ID。

这是一个完成注册的函数:

private Boolean AppbarNew()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uCallbackMessage = RegisterCallbackMessage();

    // install new appbar
    UInt32 retVal = ShellApi.SHAppBarMessage((UInt32)AppBarMessages.New, 
                                              ref msgData);
        
    return (retVal!=0);
}

在这里,我创建了一个新的 `APPBARDATA` `struct`,设置了它的尺寸、我的窗口句柄和一个唯一的消息 ID,然后使用 `AppBarMessages.New` 消息调用 `SHAppBarMessage`。代码不完整,因为我需要在窗口过程中存储唯一的消息 ID 以供以后使用。

第四部分:设置 AppBar 的大小

下一步需要关注的是当我们想设置 `appbar` 的大小和位置时会发生什么。首先要记住的是,当我们设置 `appbar` 的位置时,我们需要注意不要干扰任何其他 `appbar`,包括 `taskbar`。因此,为了更改 `appbar` 的位置,我们需要遵循以下步骤:

  1. 我们的应用程序应向操作系统提议我们要将 `appbar` 定位在哪里。此提议应包括我们要停靠的边缘以及我们窗口的大小(在 `RECT` `struct` 中)。我们通过向操作系统发送 `AppBarMessages.QueryPos` 消息来完成此操作。发送此消息后,操作系统将检查提议的位置是否存在问题,如果存在问题,它将修复我们的矩形,使其不会干扰任何其他 `appbar`。请注意,通过发送消息,我们还没有更改任何内容,我们只是检查所需位置是否有效,并返回一个有效的位置,还没有进行任何移动。
  2. 在修复返回的矩形后,我们需要向操作系统发送 `AppBarMessages.SetPos` 消息。此消息更改了操作系统内部 `appbar` 列表中我们 `appbar` 的位置。在此步骤中,有两个重要事项需要理解。
    1. 操作系统将首先重新检查我们请求的位置,以查看我们是否没有干扰任何其他 `appbar`,检查后,它将再次修复矩形,使其不会干扰任何 `appbar`,然后,它会将矩形信息设置在 `appbar` 的内部列表中。
    2. 第二件要注意的事情是,此消息实际上并没有移动窗口!它只更改了内部 `appbar` 列表中的矩形。
  3. 在 `AppBarMessages.SetPos` 消息返回后,我们需要实际将我们的窗口移动到内部列表中注册的位置。这可以通过调用 `MoveWindow` API 来完成,甚至更简单,只需设置窗体的 `Size` 和 `Location` 属性。有一点需要注意,我们需要将窗口移动到 `AppBarMessages.SetPos` 消息返回的矩形,因为操作系统可能已更改它。

为了在我们的程序中执行这些步骤,我们将定义两个辅助函数来处理消息的发送。

private void AppbarQueryPos(ref ShellApi.RECT appRect)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uEdge = (UInt32)m_Edge;
    msgData.rc = appRect;

    // query position for the appbar
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.QueryPos, ref msgData);
    appRect    = msgData.rc;
}

private void AppbarSetPos(ref ShellApi.RECT appRect)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uEdge = (UInt32)m_Edge;
    msgData.rc = appRect;

    // set postion for the appbar
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.SetPos, ref msgData);
    appRect    = msgData.rc;
}

这两个函数非常简单。它们都接受提议的矩形,创建一个 `APPBARDATA` `struct` 并用所需信息填充它(边缘来自类的 `private` 变量)。然后,使用 API `SHAppBarMessage` 向操作系统发送消息。关于此代码的一个注意事项是,在发送消息后,我们将 `appRect` 变量设置为消息返回的矩形,因为它可能已更改。

现在我将在此处介绍调整我们类大小的代码。代码遵循 3 个步骤。关于此代码的一个注意事项是,`m_PrevSize` 是一个 `private` 变量,用于存储窗口在浮动模式下的位置和大小。该函数的想法是,如果窗口的宽度为 90,高度为 40,您将窗口停靠在顶部,那么高度将保持为 40,宽度将是整个屏幕的宽度。这是代码。

private void SizeAppBar() 
{
    // prepare the proposed rectangle
    ShellApi.RECT rt = new ShellApi.RECT();

    if ((m_Edge == AppBarEdges.Left) || 
        (m_Edge == AppBarEdges.Right)) 
    {
        rt.top = 0;
        rt.bottom = SystemInformation.PrimaryMonitorSize.Height;
        if (m_Edge == AppBarEdges.Left) 
        {
            rt.right = m_PrevSize.Width;
        }
        else 
        {
            rt.right = SystemInformation.PrimaryMonitorSize.Width;
            rt.left = rt.right - m_PrevSize.Width;
        }
    }
    else 
    {
        rt.left = 0;
        rt.right = SystemInformation.PrimaryMonitorSize.Width;
        if (m_Edge == AppBarEdges.Top) 
        {
            rt.bottom = m_PrevSize.Height;
        }
        else 
        {
            rt.bottom = SystemInformation.PrimaryMonitorSize.Height;
            rt.top = rt.bottom - m_PrevSize.Height;
        }
    }

    // Step 1: check the proposed rectangle using QueryPos
    AppbarQueryPos(ref rt);
    
    switch (m_Edge) 
    { 
        case AppBarEdges.Left: 
            rt.right = rt.left + m_PrevSize.Width;
            break; 
        case AppBarEdges.Right: 
            rt.left= rt.right - m_PrevSize.Width;
            break; 
        case AppBarEdges.Top: 
            rt.bottom = rt.top + m_PrevSize.Height;
            break; 
        case AppBarEdges.Bottom: 
            rt.top = rt.bottom - m_PrevSize.Height;
            break; 
    }

    // Step 2: after fixing the rectangle, set the proposed rectangle using 
    //         SetPos

    AppbarSetPos(ref rt);

    // Step 3: Do the actual moving of the window
    Location = new Point(rt.left,rt.top);
    Size = new Size(rt.right - rt.left,rt.bottom - rt.top);
}

首先,我们构建第一个矩形提案。然后,我们发送 `AppBarMessages.QueryPos` 消息,然后我们修复提议的矩形并发送 `AppBarMessages.SetPos` 消息,最后我们执行实际的窗口移动。就这么简单。

第五部分:其他消息

到目前为止,我们已经看到了 `SHAppBarMessage` API 的几种用法。我们在函数 `AppbarNew`、`AppbarQueryPos` 和 `AppbarSetPos` 中使用了它。在本节中,我们将快速说明所有其余可发送的消息及其包装函数。

我们将从 `AppBarMessages.Remove` 消息开始。发送此消息是为了将我们的窗口从操作系统内部 `appbar` 列表中删除。发送此消息后,我们将停止接收操作系统的通知,我们的窗口将被视为普通窗口。我们只需要指定要删除的窗口句柄。这是一个执行此操作的漂亮包装函数。

private Boolean AppbarRemove()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
            
    // remove appbar
    UInt32 retVal = ShellApi.SHAppBarMessage((UInt32)AppBarMessages.Remove,
                                              ref msgData);
    
    // set previous size and location    
    Size = m_PrevSize;
    Location = m_PrevLocation;

    return (retVal!=0);
}

该函数创建一个 `APPBARDATA` `struct`,设置 `hWnd` 成员并发送 `AppBarMessages.Remove` 消息,发送消息后,该函数会恢复窗口的位置。

继续下一条消息。`Appbar` 的规则规定,当 `appbar` 应用程序收到 `WM_ACTIVATE` 消息时,它应该向操作系统发送 `AppBarMessages.Activate` 消息。所以这是 `AppbarActivate` 函数代码。

private void AppbarActivate()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    
    // send activate to the system
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.Activate, ref msgData);
}

无需解释。同样,根据规则,当我们的应用程序收到 `WM_WINDOWPOSCHANGED` 消息时,它应该向操作系统发送 `AppBarMessages.WindowPosChanged` 消息。这是代码。

private void AppbarWindowPosChanged()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    
    // send windowposchanged to the system 
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.WindowPosChanged, 
                              ref msgData);
}

操作系统允许我们设置 `appbar` 的一个属性,称为 `autohide`,通常意味着 `appbar` 在失去焦点时会消失,并在需要时重新出现。一个限制是每个边缘只能设置一个 `autohide appbar`。因此,如果 `taskbar` 设置了 `autohide`,您就不能将您的 `appbar` 设置在 `taskbar` 的同一边缘并将其设置为 `autohide`。两条帮助处理 `autohide` 操作的消息是 `AppBarMessages.GetAutoHideBar` 和 `AppBarMessages.SetAutoHideBar`。第一个消息获取一个边缘并返回在该边缘设置了 `autohide` 的窗口的句柄,如果没有窗口拥有该属性,则返回 `null`。第二个消息获取一个边缘、一个状态和一个窗口句柄,并将其设置为指定边缘的 `autohide`。请注意,第二个消息可能会返回失败,如果该边缘已经有一个 `autohide appbar`。一个重要的注意事项是,这些消息不负责窗口的隐藏和显示。这完全取决于您自己编写,它们仅在内部管理状态,这样您就不会在同一边缘设置两个 `autohide appbar`。使用这些消息的代码如下。

private Boolean AppbarSetAutoHideBar(Boolean hideValue)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uEdge = (UInt32)m_Edge;
    msgData.lParam = (hideValue) ? 1 : 0;
            
    // set auto hide
    UInt32 retVal = ShellApi.SHAppBarMessage(
        (UInt32)AppBarMessages.SetAutoHideBar,
        ref msgData);
    return (retVal!=0);
}

private IntPtr AppbarGetAutoHideBar(AppBarEdges edge)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.uEdge = (UInt32)edge;
            
    // get auto hide
    IntPtr retVal = (IntPtr)ShellApi.SHAppBarMessage(
        (UInt32)AppBarMessages.GetAutoHideBar,
        ref msgData);
    return retVal;
}

最后,我们还有三个简单的消息可能会有所帮助,这些消息在处理任务栏时使用。前两条:`AppBarMessages.GetState` 和 `AppBarMessages.SetState` 允许我们获取或设置 `taskbar` 的状态。当我说 `state` 时,我的意思是它是否是 `AlwaysOnTop` 以及它是否是 `AutoHide`。例如,如果您想设置 `AlwaysOnTop`,就像 `taskbar` 的状态一样,这可能很有用。最后一条消息是 `AppBarMessages.GetTaskBarPos`,它返回 `taskbar` 的矩形。以下是包装函数。

private AppBarStates AppbarGetTaskbarState()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
            
    // get taskbar state
    UInt32 retVal = ShellApi.SHAppBarMessage(
        (UInt32)AppBarMessages.GetState, 
        ref msgData);
    return (AppBarStates)retVal;
}

private void AppbarSetTaskbarState(AppBarStates state)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.lParam = (Int32)state;
            
    // set taskbar state
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.SetState, ref msgData);
}

private void AppbarGetTaskbarPos(out ShellApi.RECT taskRect)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
            
    // get taskbar position
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.GetTaskBarPos, 
                              ref msgData);
    taskRect = msgData.rc;
}

相当简单。所以,这些是我们能发送给操作系统的所有函数。在下一节中,我们将看到操作系统可能会向我们发送哪些通知。

第六部分:操作系统通知

还记得我们发送了 `AppBarMessages.New` 消息吗?我们必须向操作系统提供一个唯一的消息 ID。所以,正如我之前提到的,当操作系统想向我们发送通知时,它会使用我们提供的唯一消息 ID,并将 `wParam` 作为通知代码发送到我们的窗口过程。幸运的是,我们可能只会收到四种通知,这些通知在 `enum AppBarNotifications` 中得到了很好的声明。

public enum AppBarNotifications
{
    // Notifies an appbar that the taskbar's autohide or 
    // always-on-top state has changed—that is, the user has selected 
    // or cleared the "Always on top" or "Auto hide" check box on the
    // taskbar's property sheet. 
    StateChange            = 0x00000000,    
    // Notifies an appbar when an event has occurred that may affect 
    // the appbar's size and position. Events include changes in the
    // taskbar's size, position, and visibility state, as well as the
    // addition, removal, or resizing of another appbar on the same 
    // side of the screen.
    PosChanged             = 0x00000001,    
    // Notifies an appbar when a full-screen application is opening or
    // closing. This notification is sent in the form of an 
    // application-defined message that is set by the ABM_NEW message. 
    FullScreenApp          = 0x00000002,    
    // Notifies an appbar that the user has selected the Cascade, 
    // Tile Horizontally, or Tile Vertically command from the 
    // taskbar's shortcut menu.
    WindowArrange          = 0x00000003    
}

我们将从 `AppBarNoifications.PosChanged` 开始。此通知告诉我们发生了一些可能影响我们窗口位置或大小的事情,所以我们的响应将是通过调用 `AppBarMessages.QueryPos` 和 `AppBarMessages.SetPos` 消息来调整窗口大小。例如,当另一个 `appbar` 连接到我们的 `appbar` 边缘时,或者当一个 `appbar` 曾经在我们边缘上但其大小或位置发生变化时,可能会发送此通知。

接下来,我们可能会收到 `AppBarNotifications.StateChange` 通知。当 `taskbar` 的状态发生变化时,会发送此通知。再次,`state` 指的是 `AlwaysOnTop` 或 `AutoHide` 设置。对此通知的一个可能响应是使用 `AppBarMessages.GetState` 消息获取 `taskbar` 的状态并相应地更改我们的窗口。

我们可能收到的另一条通知是 `AppBarNotifications.FullScreenApp`。当一个新窗口进入全屏模式或最后一个全屏窗口关闭时,会发送此通知。根据 `lParam`,我们可以判断是哪种情况。对此通知的正常响应是,在新全屏窗口打开时更改我们的 z 顺序,并在最后一个全屏窗口关闭时恢复我们的 z 顺序。如果您想知道什么是全屏模式,请尝试在 Internet Explorer 中按 **F11**。

最后,最后的通知是 `AppBarNotifications.WindowArrange`。当用户命令 shell 重新排列打开的窗口(`平铺`、`层叠`)时,会发送此通知。此通知会发送两次。一次在排列之前,一次在排列之后。根据 `lParam`,您可以判断是发生在排列之前还是之后。对此消息的正常响应是在排列之前隐藏我们的窗口,在排列之后显示我们的窗口,这样我们的窗口就不会参与排列并保持其位置。

这些是我们可用的所有通知。我还解释了一些这些通知的可能用途,稍后,您将看到适合这些解释的代码。

第七部分:综合运用知识

所以,现在我们知道了操作系统提供了什么,并且所有这些都被很好地包装起来了,剩下的就是将它们结合起来创建我们的 `ApplicationDesktopToolbar` 类。

像所有类一样,我们将从它的构造函数开始。在构造函数中,我们希望类注册我们的唯一消息 ID。我们希望的另一件事是 `FormBorderStyle` 被设置为 `SizableToolWindow`。您可能会问为什么这样做。嗯,事实证明,当操作系统管理系统中的窗口时,它会确保所有出现在 `taskbar` 中的窗口都不能进入 `appbar` 所占用的区域,在我们的情况下,这将导致即使我们声明我们是一个 `appbar`,系统也不会让我们将窗口定位在我们请求的位置。要解决这个问题,我们需要将我们的窗口设置为 `toolbar`,这样它就会获得 `WS_EX_TOOLBAR` 样式,然后它应该可以工作。现在请仔细注意:这个答案是微软在 MSDN 上对此问题的回复。不幸的是,这并不准确。如果准确的话,我只需要设置 `ShowInTask` 属性就足够了。此外,将 `FormBorderStyle` 设置为 `SizableToolWindow` 仍然会在 `taskbar` 中显示我们的窗口!我对此没有好的答案。我假设它曾经是正确的,但今天现实已经发生了一些变化。最后,我知道只有当 `FormBorderStyle` 是 `SizableToolWindow` 或 `FixedToolWindow` 时才能工作,而与 `ShowInTask` 属性无关。

构造函数看起来是这样的:

public ApplicationDesktopToolbar()
{
    FormBorderStyle = FormBorderStyle.SizableToolWindow;
    
    // Register a unique message as our callback message
    CallbackMessageID = RegisterCallbackMessage();
    if (CallbackMessageID == 0)
        throw new Exception("RegisterCallbackMessage failed");
}

此外,我们还需要一些 `private` 变量来存储一些信息。

// saves the current edge
private AppBarEdges m_Edge = AppBarEdges.Float;

// saves the callback message id
private UInt32 CallbackMessageID = 0;

// are we in dock mode?
private Boolean IsAppbarMode = false;

// save the floating size and location
private Size m_PrevSize;
private Point m_PrevLocation;

在我们将讨论窗口过程之前,这里有一些我应该解释的小函数。

在窗体加载时,我需要保存窗体的先前大小和位置。

protected override void OnLoad(EventArgs e)
{
    m_PrevSize = Size;
    m_PrevLocation = Location;
    base.OnLoad(e);
}

在窗体关闭时,我们应该调用 `AppbarRemove` 来从内部列表中删除窗口。

protected override void OnClosing(CancelEventArgs e)
{
    AppbarRemove();
    base.OnClosing(e);
}

如果我们在停靠到边缘时大小发生变化,我只想保存一些大小信息,因为如果我们的窗口对齐到顶部或底部,我不需要保存宽度信息,因为它的宽度已设置为屏幕宽度。

protected override void OnSizeChanged(EventArgs e)
{
    if (IsAppbarMode)
    {
        if (m_Edge == AppBarEdges.Top || m_Edge == AppBarEdges.Bottom)
            m_PrevSize.Height = Size.Height;
        else
            m_PrevSize.Width = Size.Width;

        SizeAppBar();
    }
    
    base.OnSizeChanged(e);
}

在讨论消息处理之前,这是最后一个代码,`public` 属性 `Edge`。当它被设置时,它应该调用 `AppBarNew` 或 `AppBarRemove`,具体取决于它是否更改为浮动模式或屏幕边缘。

public AppBarEdges Edge 
{
    get 
    {
        return m_Edge;
    }
    set 
    {
        m_Edge = value;
        if (value == AppBarEdges.Float)
            AppbarRemove();
        else
            AppbarNew();

        if (IsAppbarMode)
            SizeAppBar();
    }
}

第八部分:窗口过程

终于到了窗口过程,一切都整合在一起的地方。在我们的窗口过程中,我们将处理四条消息。第一条消息是我们收到通知时接收到的消息。第二条消息是 `WM_ACTIVATE`,在此消息中,我们需要调用 `AppBarActivate`。第三条消息是 `WM_WINDOWPOSCHANGED`,在这里,我们需要调用 `AppBarWindowPosChanged`。最后一条消息是 `WM_NCHITTEST`。当然,我会解释为什么我要响应这条最后的消息。首先,您需要记住窗口的大小可以调整,但我希望当窗口停靠时,只能在一个方向上调整大小,而不是正常的 4 个方向。我的意思是,如果我们的窗口停靠在顶部边缘,那么我只想允许调整底部边框的大小!所以我们所做的是根据当前的停靠边缘,只允许一个边框调整大小。很快您就会看到实现这一点的代码。

所以这是窗口过程代码。

protected override void WndProc(ref Message msg)
{
    if (IsAppbarMode)
    {
        if (msg.Msg == CallbackMessageID)
        {
            OnAppbarNotification(ref msg);
        }
        else if (msg.Msg == (int)WM.ACTIVATE)
        {
            AppbarActivate();
        }
        else if (msg.Msg == (int)WM.WINDOWPOSCHANGED)
        {
            AppbarWindowPosChanged();
        }
        else if (msg.Msg == (int)WM.NCHITTEST)
        {
            OnNcHitTest(ref msg);
            return;
        }
    }
        
    base.WndProc(ref msg);
}

`AppbarActivate` 和 `AppbarWindowPosChanged` 函数是熟悉的。我们现在将回顾 `OnNcHitTest` 的代码。

void OnNcHitTest(ref Message msg)
{
    DefWndProc(ref msg);
    if ((m_Edge == AppBarEdges.Top) && 
        ((int)msg.Result == (int)MousePositionCodes.HTBOTTOM))
        0.ToString();    
    else if ((m_Edge == AppBarEdges.Bottom) && 
        ((int)msg.Result == (int)MousePositionCodes.HTTOP))
        0.ToString();    
    else if ((m_Edge == AppBarEdges.Left) && 
        ((int)msg.Result == (int)MousePositionCodes.HTRIGHT))
        0.ToString();    
    else if ((m_Edge == AppBarEdges.Right) && 
        ((int)msg.Result == (int)MousePositionCodes.HTLEFT))
        0.ToString();    
    else if ((int)msg.Result == (int)MousePositionCodes.HTCLOSE)
        0.ToString();    
    else
    {
        msg.Result = (IntPtr)MousePositionCodes.HTCLIENT;
        return;
    }
    base.WndProc(ref msg);
}

好吧,实际上,这并不算太复杂。首先,我调用默认的窗口过程来获取鼠标位置。然后我检查边缘是否是顶部,并且鼠标位置是否在底部边框,那么我就不做任何事情。其余边缘也是如此。如果鼠标位置是关闭按钮,我也做任何事情,这样用户即使停靠也可以按下关闭按钮。最后,在所有其他情况下,我返回 `HTCLIENT`,这意味着鼠标位置在客户端区域。因此,例如,如果用户将鼠标放在窗口停靠在顶部边缘时的顶部边框上,窗口将认为用户指向客户端区域,而不会显示可调整大小的图标并让其调整大小。关于此代码的一个注意事项是,每当我需要什么都不做时,我都会执行 `0.ToString()`,因为我们在 C# 中不能放置空语句,而任何其他形成代码的方式都会导致代码可读性降低。所以不要感到困惑,`0.ToString()` 什么也不做。

最后,最后一个函数 `OnAppbarNotification`,这是我们处理从操作系统接收到的通知的地方。

void OnAppbarNotification(ref Message msg)
{
    AppBarStates state;
    AppBarNotifications msgType = (AppBarNotifications)(Int32)msg.WParam;
            
    switch (msgType)
    {
        case AppBarNotifications.PosChanged:
            SizeAppBar();
            break;
                
        case AppBarNotifications.StateChange:
            state = AppbarGetTaskbarState();
            if ((state & AppBarStates.AlwaysOnTop) !=0)
            {
                TopMost = true;
                BringToFront();
            }
            else
            {
                TopMost = false;
                SendToBack();
            }
            break;

        case AppBarNotifications.FullScreenApp:
            if ((int)msg.LParam !=0)
            {
                TopMost = false;
                SendToBack();
            }
            else
            {
                state = AppbarGetTaskbarState();
                if ((state & AppBarStates.AlwaysOnTop) !=0)
                {
                    TopMost = true;
                    BringToFront();
                }
                else
                {
                    TopMost = false;
                    SendToBack();
                }
            }
                    
            break;

        case AppBarNotifications.WindowArrange:
            if ((int)msg.LParam != 0)    // before
                Visible = false;
            else                         // after
                Visible = true;
                    
            break;
    }
}

我将根本不解释这个函数!如果您想要解释,请再次阅读第 6 部分:操作系统通知。

我们已经完成了 `ApplicationDesktopToolbar` 类的开发。

使用该类

这应该是您最喜欢的部分,使用此类非常简单。您只需要更改这一行:

public class frmMain : Form

改为以下行

public class frmMain : ShellLib.ApplicationDesktopToolbar

然后,当您想将应用程序停靠到某个边缘时,只需设置 `Edge` 属性,如下所示:

private void frmMain_Load(object sender, System.EventArgs e)
{
    this.Edge = AppBarEdges.Left;
}

我建议您尝试本文档的测试程序,这样您就可以亲眼看到结果了。

就是这样!

希望您喜欢。别忘了投票。

历史

  • 2003年3月3日:初始版本
© . All rights reserved.