在 Windows 任务栏上排序窗口或将它们最小化到系统托盘






4.78/5 (13投票s)
TaskbarSorterXP 是一款小巧的实用程序,允许用户对 Windows 任务栏上的窗口进行排序。此外,窗口还可以最小化到系统托盘。
目录
- 引言
- 本项目将演示的内容
- 摘要
- 背景
- 使用代码
- 检索所有相关窗口
- 隐藏和显示窗口
- 对窗口进行排序
- 设置窗口的透明度
- 从可执行文件中检索图标
- 将窗口最小化到系统托盘
- 使窗口始终保持在最前面
- 关注点
- 历史
引言
本项目将演示的内容
- 调用非托管代码(P/Invoke,API 调用)
- 使用非托管代码显示/隐藏/激活应用程序窗口
- 使用非托管代码设置窗口透明度
- 使用
NotifyIcon
将窗口最小化到系统托盘 - 检索可执行文件中的应用程序图标
- 使用
Delegate
实现回调函数 - 在
ListView
中移动ListViewItems
- 通过 P/Invoke 提升
System.Windows.Forms.Panel
- 保存/检索用户设置
- 绑定到
ListView
的ContextMenuStrip
摘要
TaskbarSorterXP
是一款小巧的实用程序,允许用户对 Windows 任务栏上的窗口进行排序。
Windows XP 不允许像 Windows 7 那样在任务栏上对窗口进行排序。我个人总是按相同的顺序打开应用程序,以便在任务栏上“排序”(例如,Outlook,然后是 Explorer,然后是 Browser,...)。但有时您不得不关闭并重新打开它们 - 因此任务栏不可避免地会混乱。
然而,Windows 7 支持重新排序窗口。但我当时太懒了,不愿意手动操作。
我一直在寻找的是一种一键式解决方案,以我偏好的顺序对任务栏上的窗口进行排序。这就是开始这个项目的原因。
对任务栏上的窗口进行排序的解决方案很简单:
- 隐藏所有窗口
- 按排序顺序显示窗口
起初,我一直在摆弄
System.Diagnostics.Process.GetProcesses()
但 Process
没有公开设置窗口可见状态的属性。这显然是合乎逻辑的,因为 Process
并不总是代表一个窗体/窗口。
幸运的是,我之前已经通过 VBA 和 Win32 API 调用处理过隐藏/显示窗口窗体。这就是为什么这个解决方案需要与非托管代码进行互操作,例如
// <summary>
// Used to show, hide, minimize or maximize a window
// </summary>
// <param name="hWnd">handle of window to manipulate</param>
// <param name="nCmdShow">see consts below</param>
// <returns></returns>
[DllImport("user32.dll", EntryPoint="ShowWindowAsync")]
public static extern Boolean ApiShowWindowAsync(IntPtr hWnd, int nCmdShow);
对窗口进行排序是通过一个简单的 GUI 完成的。启动应用程序会显示一个窗体,其中包含一个 ListView
,列出了所有当前可见的窗口。用户有两种方式可以对窗口进行排序:
- 通过上下文菜单或
Button
对ListViewItem
进行排序 - 应用用户设置中的预定义排序顺序
您需要指定的是可执行文件的名称。
背景
为什么需要对任务栏上的窗口进行排序?好吧,我喜欢高效地完成计算机工作。那么为什么还要浪费时间在任务栏上搜索 Windows Explorer?或者浏览器窗口?当然,只需一秒钟就可以查看任务栏 - 但您需要点击任务栏上的窗口多少次来激活它?
在 Windows Vista/7 时代,为什么还需要对任务栏上的窗口进行排序?好吧,我的个人笔记本仍然运行 Windows XP。快速、稳定、可靠。
随着 v1.1.0 的发布,我所有的需求都得到了满足。但是,再次,我(对我来说)觉得每次都从程序开始菜单运行 TaskBarSorterXP
太痛苦了。所以我添加了一个 NofifyIcon
,现在我能够从系统托盘运行 TaskBarSorterXP
。但为什么只将 TaskBarSorterXP
最小化到系统托盘呢?例如,播放的 Media Player 是一个不需要停留在任务栏上的应用程序。邮件客户端也是如此。这就是为什么我在 v1.2.0 中添加了**最小化任何窗口**到系统托盘的可能性。
Using the Code
这个解决方案由六个小类组成:
TaskBarSorterGUI |
主 GUI。处理用户交互。 |
TaskBarSorterHelpers |
GUI 的一些辅助函数,保存/加载设置等。 |
TaskBarSorterPreferences |
用于定义应用程序窗口首选排序顺序的 GUI。 |
未托管 |
非托管 API 调用的声明。 |
WindowItem |
此类代表一个窗口 |
WindowsList |
此类包含所有(可见)窗口(句柄)。 |
ListViewHelpers |
用于 ListView 相关功能的辅助函数。 |
检索所有相关窗口
为了检索(所有)窗口,我使用了托管代码(参见 Unmanaged
)。
// <summary>
// Unmanaged function to enumerate all top-level windows on the screen.
// Returns the window of every opened window to the callback function
// </summary>
// <param name="lpEnumFunc">Pointer (callback) to a function which is
// called for every opened window</param>
// <param name="lParam">application defined. In this context always 0</param>
// <returns></returns>
[DllImport("user32.Dll", EntryPoint = "EnumWindows")]
public static extern int ApiEnumWindows(WindowsList.WinCallBack lpEnumFunc, int lParam);
第一个参数需要一个指向回调函数的指针。此函数会针对每个打开的窗口进行调用,并将窗口句柄传递给回调函数。 .NET 中的回调函数是通过 Delegate
实现的。因此,下一步是实现一个 Delegate
(参见 WindowList
)...
// <summary>
// Delegate for the Unmanaged.ApiEnumWindows() function.
// In .NET callback functions can be realized by delegates.
// </summary>
// <param name="hwnd">window handle passed by unmanaged function call</param>
// <param name="lParam">unused, but needed to match callback function signature</param>
// <returns></returns>
public delegate Boolean WinCallBack(int hwnd, int lParam);
...以及相关的函数调用(参见 WindowList.init()
)...
// retrieve the windows(-handles)
// fill property by call back function
Unmanaged.ApiEnumWindows(new WinCallBack(EnumWindowCallBack), 0);
此表达式为每个打开的窗口调用一个名为 EnumWindowCallBack
的函数,并传递窗口句柄。现在我们需要实现这个接收窗口句柄的回调函数。之后我们如何处理这个窗口句柄取决于我们。还有许多其他有用的非托管函数需要窗口句柄(参见 Unmanaged)。
// <summary>
// called by delegate function (s. above).
// Callback function for Unmanaged.ApiEnumWindows() which is called for
// every opened window.
// </summary>
// <param name="hwnd">window handle passed by unmanaged function call</param>
// <param name="lParam">unused, but needed to match callback function signature</param>
// <returns></returns>
// <remarks>
// v1.1.0 : use of WINDOWPLACEMENT
// </remarks>
private bool EnumWindowCallBack(int hwnd, int lParam) {
IntPtr windowHandle = (IntPtr)hwnd;
StringBuilder sbWindowTitle = new StringBuilder(1024);
// get window title text
Unmanaged.ApiGetWindowText((int)windowHandle, sbWindowTitle,
sbWindowTitle.Capacity);
// handle only processes with a title
if (sbWindowTitle.Length > 0) {
// get the process class (don't handle 'Progman'
StringBuilder sbProcessClass = new StringBuilder(256);
Unmanaged.ApiGetClassName
(hwnd, sbProcessClass, sbProcessClass.Capacity);
String processClass = sbProcessClass.ToString();
// is the window visible?
Boolean isVisible = Unmanaged.ApiIsWindowVisible(windowHandle);
// only relevant windows?
Boolean isRelevant = false;
if (this.ReturnOnlyRelevantWindows) {
isRelevant = (isVisible && !processClass.Equals
("Progman", StringComparison.CurrentCultureIgnoreCase));
} else {
isRelevant = true;
}
if (isRelevant) {
// determine window size and position (just because)
Unmanaged.RECT r = new Unmanaged.RECT();
Unmanaged.ApiGetWindowRect(windowHandle, ref r);
// determine window's appearance
Unmanaged.WINDOWPLACEMENT windowPlacement =
new Unmanaged.WINDOWPLACEMENT();
windowPlacement.length =
System.Runtime.InteropServices.Marshal.SizeOf(windowPlacement);
Unmanaged.ApiGetWindowPlacement(hwnd, ref windowPlacement);
// create new WindowItem
WindowItem wi = new WindowItem(sbWindowTitle.ToString(),
windowHandle,
processClass,
isVisible,
new Unmanaged.POINT(r.Left, r.Top),
new Unmanaged.POINT(r.Right - r.Left, r.Bottom - r.Top)
);
// set additional values
wi.WindowPlacement = windowPlacement;
wi.WindowRect = r;
// add to collection
this.Windows.Add(wi);
} else {
// window is not relevant
}
} else {
// empty window titles are not of any interest (i believe ...)
}
return true;
}
我的回调函数所做的事情很简单:
- 我(通过非托管函数调用)获取窗口标题文本,因为只有带有窗口文本的窗口才相关(我认为...)
- 我(通过非托管函数调用)获取类名,因为 '
Progman
' 不相关(我认为...) - 我(通过非托管函数调用)检查窗口是否可见。
- 我检查窗口是否相关。一个相关的窗口是:
- 可见,并且
- 进程类不是 '
Progman
'。
- 如果窗口相关:
- 获取窗口的位置和大小(仅出于此目的)通过非托管函数调用。
- 获取窗口的外观(最大化、位置等)。有了这些信息,就可以正确地恢复窗口。
- 创建一个新的
WindowItem
。 - 将
WindowItem
添加到集合中。
最后,我们得到一个“相关”WindowItem
的集合。
我第一次运行项目时没有进行“相关”窗口的测试。所以,我只是**隐藏了所有句柄**,然后**重新显示了所有句柄**。嗯,这很有趣,因为有一些句柄不属于窗口。我不得不重启我的笔记本,因为我的桌面上出现了成千上万的物体……
隐藏和显示窗口
隐藏和/或显示窗口再次通过非托管函数调用完成(参见 Unmanaged
)。
// <summary>
// Used to show, hide, minimize or maximize a window
// </summary>
// <param name="hWnd">handle of window to manipulate</param>
// <param name="nCmdShow">see consts below</param>
// <returns></returns>
[DllImport("user32.dll", EntryPoint="ShowWindowAsync")]
public static extern Boolean ApiShowWindowAsync(IntPtr hWnd, int nCmdShow);
public const int SW_HIDE = 0;
public const int SW_SHOWNORMAL = 1;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
public const int SW_SHOWNOACTIVATE = 4;
public const int SW_RESTORE = 9;
public const int SW_SHOWDEFAULT = 10;
现在我们有了一个窗口句柄列表(在我们的 WindowItem
集合中)和一个显示或隐藏窗口的函数。因此,我们已准备好实现我们的排序逻辑(先隐藏所有,然后按排序顺序显示它们)。
对窗口进行排序
为了对窗口进行排序,我们只需要一个函数,它接受一个窗口句柄集合,隐藏它们,然后再次显示它们。因为我们还需要之前的窗口布局,所以很明显我们的函数将处理一个 WindowItem
集合(参见 WindowsList.SortWindowsByWindowItemList()
)。
// <summary>
// Sorts the windows on the Taskbar.
//
// In order to restore the windows on the previous place,
// WindowItem has a property WindowPlacement which is set in EnumWindowCallBack()
// </summary>
// <param name="hwndOrdered"></param>
// <see cref="WindowItem">
// <see cref="EnumWindowCallBack">
public static void SortWindowsByWindowItemList(List<windowitem> hwndOrdered) {
// STEP 1: hide all
foreach (WindowItem wItem in hwndOrdered) {
// hide
Unmanaged.ApiShowWindowAsync(wItem.WindowHandle, Unmanaged.SW_HIDE);
}
System.Threading.Thread.Sleep(200);
// STEP 2: show all windows one after another
foreach (WindowItem wItem in hwndOrdered) {
Unmanaged.ApiSetWindowPlacement(wItem.WindowHandle.ToInt32(),
wItem.WindowPlacement);
if (wItem.WindowPlacement.showCmd == Unmanaged.SW_SHOWNORMAL) {
Unmanaged.ApiShowWindowAsync
(wItem.WindowHandle, Unmanaged.SW_SHOWNORMAL);
} else if (wItem.WindowPlacement.showCmd ==
Unmanaged.SW_SHOWMINIMIZED) {
Unmanaged.ApiShowWindowAsync
(wItem.WindowHandle, Unmanaged.SW_SHOWMINIMIZED);
} else if (wItem.WindowPlacement.showCmd ==
Unmanaged.SW_SHOWMAXIMIZED) {
Unmanaged.ApiShowWindowAsync(wItem.WindowHandle,
Unmanaged.SW_SHOWMAXIMIZED);
} else {
Unmanaged.ApiShowWindowAsync
(wItem.WindowHandle, Unmanaged.SW_SHOWNORMAL);
}
// give some time to display the window
System.Threading.Thread.Sleep(200);
}
}
如果用户对 ListView
进行排序,则 WindowItem
会从 ListViewItem.Tag
中获取,并传递给 SortWindowsByWindowItemList()
。
// <summary>
// sorts the windows on the Windows Taskbar by ListView sort order
// </summary>
private void btnSortByListView_Click(object sender, EventArgs e) {
List<windowitem> windowsOrdered = new List<windowitem>();
// iterate through list view items and get attached WindowItems
foreach (ListViewItem lvItem in this.lv_Windows.Items) {
WindowItem wItem = (WindowItem)lvItem.Tag;
windowsOrdered.Add(wItem);
}
// reorder windows by a list of WindowItem
WindowsList.SortWindowsByWindowItemList(windowsOrdered);
}
设置窗口的透明度
ListView
的上下文菜单中有一个条目可以设置所选 WindowItem
的透明度。透明度的设置再次通过 P/Invoke 完成(参见 WindowItem.SetTransparency()
和 Unmanaged
)。
// <summary>
// Sets the window transparency
// </summary>
// <param name="Alpha">255 = opaque, 0 = transparent</param>
public void SetTransparency(byte Alpha) {
// Retrieve the extended window style.
int extStyle = Unmanaged.ApiGetWindowLong
(this.WindowHandle, Unmanaged.GWL_EXSTYLE);
// Change the attribute of the specified window
Unmanaged.ApiSetWindowLong
(this.WindowHandle, Unmanaged.GWL_EXSTYLE, extStyle | Unmanaged.WS_EX_LAYERED);
// Sets the opacity and transparency color key of a layered window.
Unmanaged.ApiSetLayeredWindowAttributes
(this.WindowHandle, 0, Alpha, Unmanaged.LWA_ALPHA);
}
从可执行文件中检索图标
在将窗口最小化到系统托盘之前,我们需要一个 Icon
来创建我们的 NotifyIcon
。系统托盘图标应与窗口图标相同。再次,有一些 API 调用可以从可执行文件中检索图标。
// get only 1 small and 1 large icon
[DllImport("shell32.dll", EntryPoint = "ExtractIconEx", CharSet=CharSet.Auto)]
public static extern int ApiExtractIconExSingle
(string stExeFileName, int nIconIndex, ref IntPtr phiconLarge,
ref IntPtr phiconSmall, int nIcons);
// get all small and all large icons
[DllImport("shell32.dll", EntryPoint= "ExtractIconEx", CharSet=CharSet.Auto)]
public static extern int ApiExtractIconExMulti
(string stExeFileName, int nIconIndex, IntPtr[] phiconLarge,
IntPtr[] phiconSmall, int nIcons);
System.Drawing.Icon
类有一个 public
方法 ExtractAssociatedIcon()
,它也允许提取一个图标 - 但只有一个。为了保持项目的简单性,我使用了此方法,但它并不总是返回与窗口相同的图标(例如,资源管理器图标)。(参见 TaskBarSorterHelpers.GetIconFromFile()
、TaskBarSorterGUI.refreshListView()
)。
// <summary>
// retrieves 1 icon from a file.
// if the file does not contain an icon or an error occurs
// the default icon will be used
// </summary>
internal static System.Drawing.Icon GetIconFromFile
(String fileName, System.Drawing.Icon defaultIcon) {
System.Drawing.Icon result = null;
try {
result = System.Drawing.Icon.ExtractAssociatedIcon(fileName);
} catch (Exception ex) {
result = defaultIcon;
MessageBox.Show(ex.Message + "\r\n" +
ex.StackTrace, "GetIconFromFile()");
}
if ((result.Height == 0) || (result.Width == 0)) {
// empty icon, use default icon
result = defaultIcon;
}
return result;
}
将窗口最小化到系统托盘
使用 NotifyIcon
可以轻松地将窗口最小化到系统托盘(参见 TaskBarSorterGUI.newNotifyIcon()
)。
// <summary>
// Minimizes the window to the system tray and
// adds a reference to a list so it can be restored on application exit
// </summary>
// <param name="wItem">WindowItem to minimize to System Tray</param>
private void newNotificationIcon(WindowItem wItem) {
// create a notification icon
NotifyIcon icon = new NotifyIcon();
// get the icon from associated executable
icon.Icon = TaskBarSorterHelpers.GetIconFromFile
(wItem.ModulePath, this._DefaultIcon);
// get window title for tool tip.
icon.Text = StringHelpers.Left(wItem.WindowTitle, 50);
icon.Visible = true;
// add WindowItem data to NotifyIcon (see showMinimizedWindow())
icon.Tag = wItem;
// all NotifyIcons use the same event handler
icon.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler
(this.showMinimizedWindow);
// add to minimized WindowItems collection
// see Form_Closing()
this._minimizedWindowItems.Add(wItem);
// hide window
wItem.Hide();
}
- 创建一个新的
NotifyIcon
。 - 将
WindowItem
添加到Tag
属性。此信息对于再次恢复窗口是必需的。 - 添加一个双击事件处理程序。
- 将
WindowItem
添加到最小化窗口的集合中。在应用程序关闭时,我们必须将此集合中的所有窗口恢复。 - 最后,我们隐藏窗口。
恢复 NotifyIcon
就像这样简单:
// <summary>
// Double click event handler for our minimized NotifyIcons
// </summary>
private void showMinimizedWindow(object sender, MouseEventArgs e) {
NotifyIcon icon = (NotifyIcon)sender;
WindowItem wItem = (WindowItem)icon.Tag;
// show the minimized window again
wItem.Show();
// remove icon from taskbar
icon.Dispose();
// remove from minimized WindowItems collection
this._minimizedWindowItems.Remove(wItem);
}
使窗口始终保持在最前面
为了使窗口始终保持在最前面,我再次通过 P/Invoke 实现(参见 Unmanaged
)。WindowItem.StayOnTop()
:
// used to set a window on top
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
public static extern bool ApiSetWindowPos
(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
为了保持本文简洁,我将不发布以下实现的函数体(您可以在线浏览代码,如上所示)。
在 ListView 中移动 ListViewItems
通过 ListView
的 Button
和/或 ContextMenu
,用户可以将选定的 ListViewItem
在 ListView
中向上或向下移动。
ListViewHelpers.SwapListViewItems()
ListViewHelpers.MoveSelectedListViewItemDown()
ListViewHelpers.MoveSelectedListViewItemUp()
通过 P/Invoke 提升 System.Windows.Forms.Panel
TaskBarSorterPreferences
中的 ListView
放置在一个提升的 Panel
中(仅此而已)。
TaskBarSorterPreferences.initGUI()
Unmanaged
,请参见 GUI 相关 API 部分。
保存/检索用户设置
用户设置的保存/检索通过以下方式完成:
TaskBarSorterHelpers.GetApplicationSortOrder()
TaskBarSorterHelpers.SetApplicationSortOrder()
绑定到 ListView 的 ContextMenuStrip
在 ContextMenuStrip Opening Event
中,可以根据需要启用/禁用 ToolStripMenuItem
。
TaskBarSorterGUI.lvContextMenu_Opening()
关注点
WindowsList
可以继承List<>
。- 没有窗口标题的窗口将被从排序过程中排除。
- Windows 7 上透明度无效。
历史
1.0.0 | 05.02.2011 | 初次发布 |
1.1.0 | 09.02.2011 | 变更
|
1.2.0 | 12.02.2011 | 变更
|