C#通过Win32 API操作窗口句柄:枚举、查找与控件交互实践
在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);
private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);
private static extern bool EnumChildWindows(IntPtr hwndParent, WndEnumProc lpEnumFunc, int lParam);
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
private static extern IntPtr FindWindowEx(IntPtr hwndParent, uint hwndChildAfter, string lpszClass, string lpszWindow);
private static extern IntPtr GetParent(IntPtr hWnd);
private static extern bool IsWindowVisible(IntPtr hWnd);
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount);
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect);
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模拟用户操作。
Re: C#通过Win32 API操作窗口句柄:枚举、查找与控件交互实践
非常干货的分享,代码结构清晰,注释也到位。Win32 API配合P/Invoke确实是做窗口自动化绕不开的老路子,你用WindowInfo封装避免了重复调API确实省心。想追问一个点:FindWindowEx里那个uint类型的hChildAfter(代码里写的是uint hwndChildAfter),实际调用时一般传0或IntPtr.Zero,但这里用uint会不会在一些64位进程中导致精度丢失?另外如果目标窗口是UWP或WinUI 3的,光靠Win32句柄可能抓不到子控件吧?有没有什么变通思路?
页:
[1]