在Windows桌面开发中,经常需要与外部进程的窗口或控件进行交互,比如自动化测试、辅助工具或系统集成。C#可以通过P/Invoke调用Win32 API来实现窗口句柄的操作,包括枚举已打开的窗口、按标题或类名查找窗体、遍历子控件以及发送模拟点击。本文将基于这些API封装一个实用的WndHelper帮助类,并介绍其核心方法与使用场景。
首先需要添加System.Runtime.InteropServices引用,并定义必要的委托和API签名。核心的user32.dll函数包括:EnumWindows用于枚举所有顶层窗口;EnumChildWindows用于遍历指定父窗口的子窗口;FindWindow和FindWindowEx用于按类名和窗口名精确查找;GetParent、IsWindowVisible、GetWindowText、GetClassName和GetWindowRect用于获取窗口状态和属性。
- using System;
- using System.Collections.Generic;
- using System.Drawing;
- using System.Runtime.InteropServices;
- using System.Text;
- public class WndHelper
- {
- private delegate bool WndEnumProc(IntPtr hWnd, int lParam);
- [DllImport("user32")]
- private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static extern bool EnumChildWindows(IntPtr hwndParent, WndEnumProc lpEnumFunc, int lParam);
- [DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
- private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
- [DllImport("user32.dll", EntryPoint = "FindWindowEx", SetLastError = true)]
- private static extern IntPtr FindWindowEx(IntPtr hwndParent, uint hwndChildAfter, string lpszClass, string lpszWindow);
- [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 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结构体,包含句柄、类名、标题、可见性和边界矩形。通过计算Bounds可以判断窗口是否最小化(Left和Top均为-32000)。
- public readonly struct WindowInfo
- {
- public WindowInfo(IntPtr hWnd, string className, string title, bool isVisible, Rectangle bounds)
- {
- Hwnd = hWnd;
- ClassName = className;
- Title = title;
- IsVisible = isVisible;
- Bounds = bounds;
- }
- public IntPtr Hwnd { get; }
- public string ClassName { get; }
- public string Title { get; }
- public bool IsVisible { get; }
- public Rectangle Bounds { get; }
- public bool IsMinimized => Bounds.Left == -32000 && Bounds.Top == -32000;
- }
复制代码
枚举所有顶层窗口时,EnumWindows的回调函数会为每个窗口调用一次。在回调中,通过GetParent判断是否为顶层窗口(从Windows 8起EnumWindows只返回桌面应用顶层窗口,可省去该判断),然后获取类名、标题、可见性和矩形信息,构造成WindowInfo添加到列表中。
- private static List<WindowInfo> windowList;
- public static IReadOnlyList<WindowInfo> FindAllWindows(Predicate<WindowInfo> match = null)
- {
- windowList = new List<WindowInfo>();
- EnumWindows(OnWindowEnum, 0);
- return windowList.FindAll(match ?? DefaultPredicate);
- }
- private static bool OnWindowEnum(IntPtr hWnd, int lparam)
- {
- if (GetParent(hWnd) == IntPtr.Zero)
- {
- var classNameBuilder = new StringBuilder(512);
- GetClassName(hWnd, classNameBuilder, classNameBuilder.Capacity);
- var className = classNameBuilder.ToString();
- var titleBuilder = new StringBuilder(512);
- GetWindowText(hWnd, titleBuilder, titleBuilder.Capacity);
- var title = titleBuilder.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;
- }
- private static readonly Predicate<WindowInfo> DefaultPredicate = x => x.IsVisible && !x.IsMinimized && x.Title.Length > 0;
复制代码
调用示例:获取所有可见窗口并打印信息,或按标题过滤。
- var windows = WndHelper.FindAllWindows();
- foreach (var w in windows)
- Console.WriteLine($"{w.Title} - {w.Bounds.X},{w.Bounds.Y}");
- // 按标题包含“Test”过滤
- var testWindows = WndHelper.FindAllWindows(x => x.Title.Contains("Test"));
复制代码
查找特定窗口句柄时,优先使用FindWindow(按类名和窗口名查找顶层窗口)和FindWindowEx(按父窗口、类名和窗口名查找子窗口)。注意第三个参数lpszClass是Windows类名,不是C#的类名,如果不知道可传null,但可能会匹配多个;最好通过GetClassName确认。另外,如果按钮或控件绑定了快捷键(如“否(&N)”),查找时也必须传入完整的带&的字符串。
- // 查找标题为“测试”的顶层窗口
- IntPtr hWnd = WndHelper.FindWindow(null, "测试");
- if (hWnd != IntPtr.Zero)
- {
- // 查找子按钮“点击测试”(假设按钮文本为此)
- IntPtr btnHandle = WndHelper.FindWindowEx(hWnd, IntPtr.Zero, null, "点击测试");
- if (btnHandle != IntPtr.Zero)
- {
- // 发送点击消息(假设WndHelper中有SendClick方法)
- WndHelper.SendClick(btnHandle);
- }
- }
- // 查找MessageBox弹窗中的“否(&N)”按钮
- IntPtr msgBoxHwnd = WndHelper.FindWindow(null, "测试");
- if (msgBoxHwnd != IntPtr.Zero)
- {
- IntPtr noBtn = WndHelper.FindWindowEx(msgBoxHwnd, IntPtr.Zero, null, "否(&N)");
- if (noBtn != IntPtr.Zero)
- WndHelper.SendClick(noBtn);
- }
复制代码
注意:FindWindow和FindWindowEx在winuser.h中定义为FindWindowW(Unicode)或FindWindowA(ANSI),直接使用不带后缀的版本即可,编译器会根据项目设置自动选择。
多线程使用时,上述实现中的静态字段windowList可能产生竞争条件。建议改为在每次调用时创建局部列表,并通过回调参数传递,或者使用Concurrent集合。
以上方法适用于Windows桌面应用的窗口自动化、测试工具开发或系统监控场景。通过组合EnumWindows、FindWindow和FindWindowEx,可以灵活地定位目标窗口及其子控件,然后利用SendMessage或PostMessage模拟用户操作。 |