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






4.91/5 (53投票s)
将您的窗口按钮移动到首选顺序。仅限 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
相当了解。首先要做的就是获取它的窗口句柄。我们可以使用 GetDesktopWindow
和 FindWindowEx
来做到这一点。
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
结构体中获取了它。
获取窗口句柄
这真的是幸运的部分。我心想:“我会把窗口句柄放在哪里?”。他们必须把地方放好,以便在选择按钮时能够激活正确的窗口。最显而易见的地方就是每个按钮的结构体,而存储指向此结构体的指针最明显的地方就是每个 TBBUTTON
的 dwData
字段。
所以我看了一下 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
就无法正常工作,所以我添加了一个。然后我想,最好显示默认的窗口图标来区分项目。我的第一次尝试并不太成功。我使用 GetClassLong
和 GCL_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。