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

一个用于排列任务栏窗口按钮的工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (53投票s)

2005年5月27日

CPOL

8分钟阅读

viewsIcon

270225

downloadIcon

7144

将您的窗口按钮移动到首选顺序。仅限 WinXP 或更高版本。

注意: 此工具仅在 Windows XP 及更高版本上运行!

引言

此工具允许您更改任务栏中窗口按钮的顺序。

背景

我通常会打开大约 20 个窗口,如果它们按我的“正常”顺序排列,切换起来会快得多。例如,我总是喜欢 Outlook 成为任务栏中的第一个按钮,这样我就可以知道在收到邮件时点击哪里。问题是,当 Outlook 挂起时,我必须终止它的进程。然后当我重新启动它时,它会出现在列表的末尾,很快就会被我的其他窗口隐藏起来。使用此工具,我可以轻松地将 Outlook 移回它所属的开头,而无需关闭和重新打开我所有其他窗口。

这个问题是我的一个怪癖——如果窗口的顺序不困扰你,那么你就不需要这个工具 :)

工作原理

此工具使用大量 PInvoke 调用——如果您想查看详细信息,请在源代码的“Common/Kernel32.cs”和“Common/User32.cs”文件中查看。它还使用了一个 unsafe 方法(我的第一个),这让我感觉有点不干净 :)。我开始用 C++ 编写它,但在 C# 工作了四年之后,这实在太痛苦了。

基本思路是获取任务栏中的窗口列表,显示它们并允许重新排序,然后应用新的排序。听起来很简单 :)

获取窗口列表

事实证明这是最难的部分。幸运的是,我找到了一种方法,但它只适用于 Windows XP 及更高版本。Windows 2000 使用不同的窗口层次结构,我不知道(也不关心 :) 早期操作系统的情况。

获取按钮窗口句柄

使用 Spy++,我发现任务栏中的按钮窗口实际上是一个 ToolbarWindow32——这是常用的 Windows 控件之一。这是运气,因为 Microsoft 很容易使用自定义窗口类。就目前而言,我们对 ToolbarWindow32 相当了解。首先要做的就是获取它的窗口句柄。我们可以使用 GetDesktopWindowFindWindowEx 来做到这一点。

IntPtr hDesktop = User32.GetDesktopWindow();
IntPtr hTray    = User32.FindWindowEx( hDesktop , 0, "Shell_TrayWnd"   , null );
IntPtr hReBar   = User32.FindWindowEx( hTray    , 0, "ReBarWindow32"   , null );
IntPtr hTask    = User32.FindWindowEx( hReBar   , 0, "MSTaskSwWClass"  , null );
IntPtr hToolbar = User32.FindWindowEx( hTask    , 0, "ToolbarWindow32" , null );

这只是沿着从桌面窗口开始的窗口层次结构向下遍历。现在我们有了 ToolbarWindow32 的句柄,就可以开始有趣的事情了 :)

获取按钮数量

这很容易,因为我们知道 TB_BUTTONCOUNT 消息。我们所要做的就是使用我们的窗口句柄发送此消息,它将返回计数。

UInt32 count = User32.SendMessage( _ToolbarWindowHandle, TB.BUTTONCOUNT, 0, 0 );

请注意,窗口计数比我们预期的要多。有关解释,请参阅 Groups

获取每个按钮的信息

这应该也很容易,因为我们知道 TB_GETBUTTON 消息。它有点复杂,因为我们正在访问来自不同进程的 ToolbarWindow32,所以我们必须进行一些内存管理。有关详细信息,请参阅 Cross-process memory access。它还使用了一个 unsafe 块,因为我们需要将结构体的地址作为指针传递。我现在将重点放在算法上。

TBBUTTON tbButton;
TBBUTTON* ipRemoteBuffer = & tbButton; // unsafe
for ( int i = 0 ; i < count ; i++ )
{
    User32.SendMessage(
        hToolbar,
        TB.GETBUTTON,
        ( IntPtr ) i,
        ipRemoteBuffer );
}

请注意,这是伪代码——它不能编译,它只是给你一个算法的思路。

现在我们有了一个 TBBUTTON 结构体,其中填充了每个按钮的信息。如果我们查看 fsState 字段,我们会发现一些按钮被隐藏了,并且有预期的可见按钮数量。又一次幸运 :) 请注意,TBBUTTON 结构体有一个 32 位 dwData 字段用于用户数据。这以后会派上用场……

获取按钮文本

TBBUTTON 结构体有一个 String 字段,但它并不那么容易使用。使用 TB_GETBUTTONTEXT 消息让控件完成工作更容易。

int chars = ( int ) User32.SendMessage(
    hToolbar,
    TB.GETBUTTONTEXTW,
    ( IntPtr ) tbButton.idCommand,
    ipRemoteBuffer );

请注意,此消息需要一个 CommandId,我们从 TBBUTTON 结构体中获取了它。

获取窗口句柄

这真的是幸运的部分。我心想:“我会把窗口句柄放在哪里?”。他们必须把地方放好,以便在选择按钮时能够激活正确的窗口。最显而易见的地方就是每个按钮的结构体,而存储指向此结构体的指针最明显的地方就是每个 TBBUTTONdwData 字段。

所以我看了一下 dwData 字段,它们似乎是指针。到目前为止还可以。然后我看了一下它们指向的内存,它们就在那里:第一个字段存储着窗口句柄 :))) Microsoft 的开发人员毕竟也不是那么不同 :)

使用窗口句柄

剩下的就很简单了。我使用了 Thomas Caudal [^] 的 TreeListView [^],它使 UI 变得简单。一些胶水代码用于处理项目的移动,然后是“应用”按钮。

为了应用新的顺序,我们只需要使用 ShowWindow 隐藏所有窗口,然后按所需顺序逐个显示它们。轻而易举 :)

关注点

有几件事需要更详细地解释。

Groups

在 XP 及更高版本中,您可以选择将相似的任务栏按钮组合在一起。此工具在打开或关闭此功能时均可正常工作。无论哪种情况,Explorer 都会在每个组的开头添加一个隐藏的虚拟按钮。这就是为什么 TB_BUTTONCOUNT 返回的按钮比可见按钮多。这些额外的按钮很容易识别,因为它们没有窗口句柄。

出于兴趣,该行为受几个注册表项控制。

  • "HKCU\Software\Microsoft\Windows\CurrentVersion \Explorer\Advanced\TaskbarGlomming" 的值为 0 表示分组关闭,值为 1 表示分组打开。
  • "HKCU\Software\Microsoft\Windows\CurrentVersion \Explorer\Advanced\TaskbarGroupSize" 是在窗口折叠成一个按钮之前的最小窗口数。我将其设置为 99 以有效地将其关闭。您可以使用 XP PowerToys 中的 TweakUI 设置此值(这可能更安全)。

跨进程内存访问

这是一个棘手的问题。一些必需的 Win32 函数使用结构体来移动信息。如果您和您的目标在同一个进程空间中,这可以很好地工作,但在我们的情况下就失败了。朴素的实现不起作用。如果您声明一个结构体,并获取指向它的指针以传递给您的函数,那么指针将指向您虚拟内存空间中的某个地址。函数无法知道这一点,并认为它指向其虚拟内存中的一个块。充其量,这只会导致通用保护性故障。

解决方案是在 Explorer 的虚拟内存中为您的结构体分配空间,调用函数,然后将数据复制回您的虚拟内存空间,以便您可以使用它。这在没有提升权限的情况下也可以工作,因为 Explorer 在本地用户帐户下运行。由于我们一个接一个地使用几个函数,我只是分配了一个内存页面,并将其用于所有结构体。

基本算法如下:

  • 使用 GetWindowThreadProcessId 从窗口句柄获取进程 ID。
  • 使用 OpenProcess 将进程 ID 转换为进程句柄。
  • 使用 VirtualAllocEx 在 Explorer 的虚拟内存空间中分配一些内存。
  • 调用所需的 API 函数来填充缓冲区。
  • 使用 ReadProcessMemory 将缓冲区复制到我们的虚拟内存空间。
  • 不要忘记使用 VirtualFreeEx 释放缓冲区,并使用 CloseHandle 来完成。

我在这里只展示一个调用,其他的类似。

private unsafe bool GetTBButton(
    IntPtr hToolbar,
    int i, // button id
    ref TBBUTTON tbButton,
    ... )
{
    // One page
    const int BUFFER_SIZE = 0x1000;

    byte[] localBuffer = new byte[ BUFFER_SIZE ];

    UInt32 processId = 0;
    UInt32 threadId =
        User32.GetWindowThreadProcessId(
            hToolbar,
            out processId );

    IntPtr hProcess =
        Kernel32.OpenProcess(
            ProcessRights.ALL_ACCESS,
            false,
            processId );

    if ( hProcess == IntPtr.Zero ) return false;

    IntPtr ipRemoteBuffer = Kernel32.VirtualAllocEx(
        hProcess,
        IntPtr.Zero,
        new UIntPtr( BUFFER_SIZE ),
        MemAllocationType.COMMIT,
        MemoryProtection.PAGE_READWRITE );

    if ( ipRemoteBuffer == IntPtr.Zero ) return false;

    // TBButton
    fixed ( TBBUTTON* pTBButton = & tbButton )
    {
        IntPtr ipTBButton = new IntPtr( pTBButton );

        int b = ( int ) User32.SendMessage(
            hToolbar,
            TB.GETBUTTON,
            ( IntPtr ) i,
            ipRemoteBuffer );

        if ( b == 0 ) { Debug.Assert( false ); return false; }

        Int32 dwBytesRead = 0;
        IntPtr ipBytesRead = new IntPtr( & dwBytesRead );

        bool b2 = Kernel32.ReadProcessMemory(
            hProcess,
            ipRemoteBuffer,
            ipTBButton,
            new UIntPtr( ( uint ) sizeof( TBBUTTON ) ),
            ipBytesRead );

        if ( ! b2 ) { Debug.Assert( false ); return false; }
    }
        
    ...

    Kernel32.VirtualFreeEx(
        hProcess,
        ipRemoteBuffer,
        UIntPtr.Zero,
        MemAllocationType.RELEASE );

    Kernel32.CloseHandle( hProcess );

    return true;
}

unsafe 关键字允许我们使用指针,而 fixed 关键字将对象锁定在内存中,这样 GC 就不会在我们将数据复制回我们的虚拟内存空间时移动它。除此之外,它只是 P/Invoke 的一些东西。如果您有兴趣,请查看源代码中的“Common\Kernel32.cs”和“Common\User32.cs”文件。

图标

这只是些装饰。TreeListView 没有 ImageList 就无法正常工作,所以我添加了一个。然后我想,最好显示默认的窗口图标来区分项目。我的第一次尝试并不太成功。我使用 GetClassLongGCL_HICONSM 参数来获取与窗口类关联的小图标。这对大多数应用程序都有效,但对我的 .NET 应用程序无效。框架似乎每次应用程序运行时都会生成一个新的窗口类,但它没有设置图标句柄——不错 :) 然而,这些应用程序在收到 WM_GETICON 消息时正确返回图标句柄,所以最终我就是这样做的。我还通过覆盖主窗体的 OnHandleCreated 来设置该工具的窗口类图标句柄,但这并非必需。

免费宣传

我在这个项目中使用了我之前几篇文章中的代码。我使用了我的 OSVersion [^] 类来检查我们是否在 XP 或更高版本上运行。我还使用我的 TreeCollection [^] 作为后备存储。我还使用了 GlobalMemoryStatusEx API 在“OS Version”对话框中显示内存使用情况——只是为了好玩 :)

结论

好了,大概就是这样了。如果您只想使用这个工具,那没关系——希望您觉得它有用。如果您想深入研究,可以查看代码,了解 P/Invoke 和跨进程内存访问的示例。

我只想提一下 pinvoke.net [^] wiki 站点——非常方便,而且大部分是准确的 :)

历史

  • 2005 年 8 月 8 日 - 版本 2。
    • 修复了 hIcon 错误。
    • 更改了图标。
  • 2005 年 5 月 27 日 - 版本 1。
© . All rights reserved.