C#对Windows窗口或窗口句柄的操作,都是通过 Win32 API 实现的,通过DllImport
引入Windows API操作窗口(句柄),可以实现枚举已打开的窗口、向窗口或子窗口(窗口内的控件)发送文本、关闭、键盘按键等各种命令,实现窗口的基本操作。
新建Windows帮助类public class WndHelper{}
,提供窗口相关的操作,并添加引用using System.Runtime.InteropServices;
。
新建WindowHandle
项目,用于测试窗口句柄帮助类的使用。
枚举和查找windows窗口信息
EnumWindows枚举所有(顶层)窗口和获取窗口信息的API
EnumWindows
API 用来枚举所有的窗口,其第一个参数需要定义一个方法作为参数传入,用于处理枚举时的每一次结果(即C#中的委托方法,委托类型为WndEnumProc(IntPtr hWnd, int lparam)
)。
实现一个FindAllWindows
方法,获取所有的顶层窗口信息,可以指定查询条件(Predicate<T>
泛型委托),
WndEnumProc
枚举窗口时的处理方法中,需要判断顶层窗口、获取必需的窗口信息
GetParent
获取窗口的父窗口,用于判断找到的窗口是否是顶层窗口。IsWindowVisible
判断窗口是否可见GetWindowText
获取窗口标题GetClassName
获取窗口类名GetWindowRect
获取窗口位置和尺寸,需要定义一个结构体LPRECT
注:从Windows8开始,
EnumWindows
仅仅遍历桌面应用的顶层窗口。也就是说,Win8之后的使用可以不需要判断GetParent
是否为顶层窗口。
对应的win32 API如下:
/// <summary> /// 枚举窗口时的委托参数 /// </summary> /// <param name="hWnd"></param> /// <param name="lParam"></param> /// <returns></returns> private delegate bool WndEnumProc(IntPtr hWnd, int lParam); /// <summary> /// 枚举所有窗口 /// </summary> /// <param name="lpEnumFunc"></param> /// <param name="lParam"></param> /// <returns></returns> [DllImport("user32")] private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam); /// <summary> /// 获取窗口的父窗口句柄 /// </summary> /// <param name="hWnd"></param> /// <returns></returns> [DllImport("user32")] private static extern IntPtr GetParent(IntPtr hWnd); [DllImport("user32")] private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32")] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount); [DllImport("user32")] private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32")] private static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab); [DllImport("user32")] private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect); [StructLayout(LayoutKind.Sequential)] private readonly struct LPRECT { public readonly int Left; public readonly int Top; public readonly int Right; public readonly int Bottom; }
窗体信息结构体
WindowInfo
结构体用于存放必需的窗体信息,也可以直接指定为只读结构体(public readonly struct WindowInfo{}
,需要C#7.2版本支持)
获取的窗体信息包括窗口句柄、窗口标题、位置、大小尺寸、是否是最小化、可见性等。
/// <summary> /// 获取 Win32 窗口的一些基本信息。 /// </summary> public struct WindowInfo { public WindowInfo(IntPtr hWnd, string className, string title, bool isVisible, Rectangle bounds) : this() { Hwnd = hWnd; ClassName = className; Title = title; IsVisible = isVisible; Bounds = bounds; } /// <summary> /// 获取窗口句柄。 /// </summary> public IntPtr Hwnd { get; } /// <summary> /// 获取窗口类名。 /// </summary> public string ClassName { get; } /// <summary> /// 获取窗口标题。 /// </summary> public string Title { get; } /// <summary> /// 获取当前窗口是否可见。 /// </summary> public bool IsVisible { get; } /// <summary> /// 获取窗口当前的位置和尺寸。 /// </summary> public Rectangle Bounds { get; } /// <summary> /// 获取窗口当前是否是最小化的。 /// </summary> public bool IsMinimized => Bounds.Left == -32000 && Bounds.Top == -32000; }
获取窗口FindAllWindows
的实现
通过Predicate<WindowInfo>
设置获取的窗口满足的条件,默认仅查找可见且有标题栏的窗口。
/// <summary> /// 查找当前用户空间下所有符合条件的(顶层)窗口。如果不指定条件,将仅查找可见且有标题栏的窗口。 /// </summary> /// <param name="match">过滤窗口的条件。如果设置为 null,将仅查找可见和标题栏不为空的窗口。</param> /// <returns>找到的所有窗口信息</returns> public static IReadOnlyList<WindowInfo> FindAllWindows(Predicate<WindowInfo> match = null) { windowList = new List<WindowInfo>(); //遍历窗口并查找窗口相关WindowInfo信息 EnumWindows(OnWindowEnum, 0); return windowList.FindAll(match ?? DefaultPredicate); } /// <summary> /// 遍历窗体处理的函数 /// </summary> /// <param name="hWnd"></param> /// <param name="lparam"></param> /// <returns></returns> private static bool OnWindowEnum(IntPtr hWnd, int lparam) { // 仅查找顶层窗口。 if (GetParent(hWnd) == IntPtr.Zero) { // 获取窗口类名。 var lpString = new StringBuilder(512); GetClassName(hWnd, lpString, lpString.Capacity); var className = lpString.ToString(); // 获取窗口标题。 var lptrString = new StringBuilder(512); GetWindowText(hWnd, lptrString, lptrString.Capacity); var title = lptrString.ToString().Trim(); // 获取窗口可见性。 var isVisible = IsWindowVisible(hWnd); // 获取窗口位置和尺寸。 LPRECT rect = default; GetWindowRect(hWnd, ref rect); var bounds = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top); // 添加到已找到的窗口列表。 windowList.Add(new WindowInfo(hWnd, className, title, isVisible, bounds)); } return true; } /// <summary> /// 默认的查找窗口的过滤条件。可见 + 非最小化 + 包含窗口标题。 /// </summary> private static readonly Predicate<WindowInfo> DefaultPredicate = x => x.IsVisible && !x.IsMinimized && x.Title.Length > 0; /// <summary> /// 窗体列表 /// </summary> private static List<WindowInfo> windowList;
获取所有的可见窗体:
var windows = WndHelper.FindAllWindows(); for (int i = 0; i < windows.Count; i++) { var window = windows[i]; Console.WriteLine($@"{i.ToString().PadLeft(3, ' ')}. {window.Title} {window.Bounds.X}, {window.Bounds.Y}, {window.Bounds.Width}, {window.Bounds.Height}"); } Console.ReadLine();
查好包含指定Title的窗体信息:
var windows = WndHelper.FindAllWindows(x => x.Title.Contains("Test"));
不设置过滤,查好所有窗体信息:
var windows = WndHelper.FindAllWindows(x => true);
EnumChildWindows遍历子窗口
EnumChildWindows
用于遍历指定父窗口(可选)的子窗口。
BOOL EnumChildWindows( [in, optional] HWND hWndParent, [in] WNDENUMPROC lpEnumFunc, [in] LPARAM lParam );
/// <summary> /// 遍历子窗体(控件) /// </summary> /// <param name="hwndParent">父窗口句柄</param> /// <param name="lpEnumFunc">遍历的回调函数</param> /// <param name="lParam">传给遍历时回调函数的额外数据</param> /// <returns></returns> [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool EnumChildWindows(IntPtr hwndParent, WndEnumProc lpEnumFunc, int lParam); /// <summary> /// 枚举窗口时的委托参数 /// </summary> /// <param name="hWnd"></param> /// <param name="lParam"></param> /// <returns></returns> private delegate bool WndEnumProc(IntPtr hWnd, int lParam);
FindWindow/FindWindowEx查找窗体
FindWindow、FindWindowEx查找顶层窗体和子窗体
FindWindow
方法可以直接查找某顶层窗体句柄。
FindWindowEx
方法用于查找子窗体句柄。
/// <summary> /// 查找窗体 /// </summary> /// <param name="lpClassName">窗体的类名称,比如Form、Window。若不知道,指定为null即可</param> /// <param name="lpWindowName">窗体的标题/文字</param> /// <returns></returns> [DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); /// <summary> /// 查找子窗体(控件) /// </summary> /// <param name="hwndParent">父窗体句柄,不知道窗体时可指定IntPtr.Zero</param> /// <param name="hwndChildAfter">子窗体(控件),通常不知道子窗体(句柄),指定0即可</param> /// <param name="lpszClass">子窗体(控件)的类名,通常指定null,它是window class name,并不等同于C#中的列名Button、Image、PictureBox等,两者并不相同,可通过GetClassName获取正确的类型名</param> /// <param name="lpszWindow">子窗体的名字或控件的Title、Text,通常为显示的文字</param> /// <returns></returns> [DllImport("user32.dll", EntryPoint = "FindWindowEx", SetLastError = true)] private static extern IntPtr FindWindowEx(IntPtr hwndParent, uint hwndChildAfter, string lpszClass, string lpszWindow);
HWND FindWindowEx(HWND hwndParent,HWND hwndChildAfter,LPCTSTR lpszClass,LPCTSTR lpszWindow);
FindWindowEx的参数:
hwndParent:要查找子窗口的父窗口句柄。如果hwndParent为NULL,则函数以桌面窗口为父窗口,查找桌面窗口的所有子窗口。Windows NT5.0 and later:如果hwndParent是HWND_MESSAGE,函数仅查找所有消息窗口。
hwndChildAfter :子窗口句柄。查找从在Z序中的下一个子窗口开始。子窗口必须为hwndParent窗口的直接子窗口而非后代窗口。如果HwndChildAfter为NULL,查找从hwndParent的第一个子窗口开始。如果hwndParent 和 hwndChildAfter同时为NULL,则函数查找所有的顶层窗口及消息窗口。
lpszClass:指向一个指定了类名的空结束字符串,或一个标识类名字符串的成员的指针。它表示window class name,并不等同于C#中的类名,通常指定null即可,可通过GetClassName获取正确的类型名。
lpszWindow:指向一个指定了窗口名(窗口标题)的空结束字符串。如果该参数为 NULL,则为所有窗口全匹配。返回值:如果函数成功,返回值为具有指定类名和窗口名的窗口句柄。如果函数失败,返回值为NULL。
查找窗体或控件的使用
查找子窗体(控件)时,FindWindowEx
第三个参数windows类名指定null即可。不要使用C#中的Button等,将会查找不到。
var wndHandle = WndHelper.FindWindow(null, "Form测试窗体的标题栏"); if (wndHandle != IntPtr.Zero) { //找到Button IntPtr btnHandle = WndHelper.FindWindowEx(wndHandle, IntPtr.Zero, null, "点击测试"); IntPtr btnHandle2 = WndHelper.FindWindowEx(wndHandle, IntPtr.Zero, null, "Click"); //IntPtr btnHandle3 = WndHelper.FindWindowEx(msgHandle, IntPtr.Zero, "Control", "点击测试"); if (btnHandle != IntPtr.Zero) { WndHelper.SendClick(btnHandle); // 发送点击事件 } }
MessageBox显示的窗体也为顶层窗体
var wndHandle = WndHelper.FindWindow(null, "测试"); // 查找MessageBox窗体
绑定&快捷按键的控件查找
对于默认的MessageBox显示的窗体,如果是不同类型的按钮,会通知指定快捷键,比如Y表示“是”;N表示“否”。
快捷键的绑定是通过&
(包括自己手动实现的绑定快捷键),因此查找时也需要指定,比如"否(&N)"、"是(&Y)"
如下,查找一个标题为"测试"MessageBox弹窗的窗口句柄,并查找其下面的否(N)
按钮,实现点击。
var wndHandle = WndHelper.FindWindow(null, "测试"); // 查找MessageBox窗体 if (wndHandle != IntPtr.Zero) { IntPtr noBtnHandle = WndHelper.FindWindowEx(wndHandle, IntPtr.Zero, null, "否(&N)"); // 使用&对应快捷按键,查找MessageBox中的"否(N)"按钮 if (noBtnHandle != IntPtr.Zero) { WndHelper.SendClick(noBtnHandle); } }
查找&
快捷键的按钮控件:
FindWindow与FindWindowW、FindWindowA
在winuser.h
头的定义中,FindWindow
作为FindWindowW
或FindWindowA
的别名,它根据UNICODE
定义的预处理常量自动选择该函数的ANSI或Unicode版本。
注意,混合使用编码将可能导致编译或运行时错误。通常推荐直接FindWindow
,而不要直接使用FindWindowW
或FindWindowA
。
FindWindowEx
同样,为FindWindowExA
和FindWindowExW
的自动别名。
附:关于上面使用遍历窗口API查找窗体时的静态字段windowList和childWindowList
静态字段windowList
和childWindowList
用于循环窗口句柄时处理每个句柄,但是,由于是共用的静态字段,如果遇到多线程的情况下,肯定会出现问题或混乱。
因此,最好修改下代码,处理多线程使用时,这两个字段的竞争。
参考

