C#对Windows窗口或窗口句柄的操作,都是通过 P/Invoke 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
用于循环窗口句柄时处理每个句柄,但是,由于是共用的静态字段,如果遇到多线程的情况下,肯定会出现问题或混乱。
因此,最好修改下代码,处理多线程使用时,这两个字段的竞争。