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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (13投票s)

2011 年 2 月 6 日

CPOL

8分钟阅读

viewsIcon

49229

downloadIcon

1469

TaskbarSorterXP 是一款小巧的实用程序,允许用户对 Windows 任务栏上的窗口进行排序。此外,窗口还可以最小化到系统托盘。

目录

  1. 引言
    • 本项目将演示的内容
    • 摘要
  2. 背景
  3. 使用代码
    • 检索所有相关窗口
    • 隐藏和显示窗口
    • 对窗口进行排序
    • 设置窗口的透明度
    • 从可执行文件中检索图标
    • 将窗口最小化到系统托盘
  4. 使窗口始终保持在最前面
  5. 关注点
  6. 历史

引言

本项目将演示的内容

  • 调用非托管代码(P/Invoke,API 调用)
  • 使用非托管代码显示/隐藏/激活应用程序窗口
  • 使用非托管代码设置窗口透明度
  • 使用 NotifyIcon 将窗口最小化到系统托盘
  • 检索可执行文件中的应用程序图标
  • 使用 Delegate 实现回调函数
  • ListView 中移动 ListViewItems
  • 通过 P/Invoke 提升 System.Windows.Forms.Panel
  • 保存/检索用户设置
  • 绑定到 ListViewContextMenuStrip

摘要

TaskbarSorterXP 是一款小巧的实用程序,允许用户对 Windows 任务栏上的窗口进行排序。

Windows XP 不允许像 Windows 7 那样在任务栏上对窗口进行排序。我个人总是按相同的顺序打开应用程序,以便在任务栏上“排序”(例如,Outlook,然后是 Explorer,然后是 Browser,...)。但有时您不得不关闭并重新打开它们 - 因此任务栏不可避免地会混乱。

然而,Windows 7 支持重新排序窗口。但我当时太懒了,不愿意手动操作。

我一直在寻找的是一种一键式解决方案,以我偏好的顺序对任务栏上的窗口进行排序。这就是开始这个项目的原因。

对任务栏上的窗口进行排序的解决方案很简单:

  1. 隐藏所有窗口
  2. 按排序顺序显示窗口

起初,我一直在摆弄

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,列出了所有当前可见的窗口。用户有两种方式可以对窗口进行排序:

  1. 通过上下文菜单或 ButtonListViewItem 进行排序
  2. 应用用户设置中的预定义排序顺序

TaskbarSorterXP1.png

主 GUI

TaskbarSorterXP2.png

带有提升面板的首选项 GUI

您需要指定的是可执行文件的名称。

背景

为什么需要对任务栏上的窗口进行排序?好吧,我喜欢高效地完成计算机工作。那么为什么还要浪费时间在任务栏上搜索 Windows Explorer?或者浏览器窗口?当然,只需一秒钟就可以查看任务栏 - 但您需要点击任务栏上的窗口多少次来激活它?

在 Windows Vista/7 时代,为什么还需要对任务栏上的窗口进行排序?好吧,我的个人笔记本仍然运行 Windows XP。快速、稳定、可靠。

随着 v1.1.0 的发布,我所有的需求都得到了满足。但是,再次,我(对我来说)觉得每次都从程序开始菜单运行 TaskBarSorterXP 太痛苦了。所以我添加了一个 NofifyIcon,现在我能够从系统托盘运行 TaskBarSorterXP。但为什么只将 TaskBarSorterXP 最小化到系统托盘呢?例如,播放的 Media Player 是一个不需要停留在任务栏上的应用程序。邮件客户端也是如此。这就是为什么我在 v1.2.0 中添加了**最小化任何窗口**到系统托盘的可能性。

TaskbarSorterXP3.png

最小化到系统托盘的窗口

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;
  }

我的回调函数所做的事情很简单:

  1. 我(通过非托管函数调用)获取窗口标题文本,因为只有带有窗口文本的窗口才相关(我认为...)
  2. 我(通过非托管函数调用)获取类名,因为 'Progman' 不相关(我认为...)
  3. 我(通过非托管函数调用)检查窗口是否可见。
  4. 我检查窗口是否相关。一个相关的窗口是:
    • 可见,并且
    • 进程类不是 'Progman'。
  5. 如果窗口相关:
    1. 获取窗口的位置和大小(仅出于此目的)通过非托管函数调用。
    2. 获取窗口的外观(最大化、位置等)。有了这些信息,就可以正确地恢复窗口。
    3. 创建一个新的 WindowItem
    4. 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();
  }
  1. 创建一个新的 NotifyIcon
  2. WindowItem 添加到 Tag 属性。此信息对于再次恢复窗口是必需的。
  3. 添加一个双击事件处理程序。
  4. WindowItem 添加到最小化窗口的集合中。在应用程序关闭时,我们必须将此集合中的所有窗口恢复。
  5. 最后,我们隐藏窗口。

恢复 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

通过 ListViewButton 和/或 ContextMenu,用户可以将选定的 ListViewItemListView 中向上或向下移动。

  • 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 变更
  • 修复:窗口恢复到之前的状态。
  • 新增:用于编辑/保存应用程序排序偏好的 GUI。
  • 更改:使用用户设置代替 app.config
  • 更改:代码重构。
1.2.0 12.02.2011 变更
  • 新增:将 TaskBarSorter 最小化到系统托盘。
  • 新增:将列出的任何窗口最小化到系统托盘。
  • 新增:设置列出窗口的透明度。
  • 新增:使窗口保持在最前面。
  • 更改:WindowItem 类公开了一些新方法。
© . All rights reserved.