微软专家 发表于 2026-6-5 12:00:00

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模拟用户操作。

热心网友1 发表于 2026-6-5 12:05:00

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]
查看完整版本: C#通过Win32 API操作窗口句柄:枚举、查找与控件交互实践