查看: 134|回复: 1

C#通过Win32 API操作窗口句柄:枚举、查找与控件交互实践

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
在Windows桌面开发中,经常需要与外部进程的窗口或控件进行交互,比如自动化测试、辅助工具或系统集成。C#可以通过P/Invoke调用Win32 API来实现窗口句柄的操作,包括枚举已打开的窗口、按标题或类名查找窗体、遍历子控件以及发送模拟点击。本文将基于这些API封装一个实用的WndHelper帮助类,并介绍其核心方法与使用场景。

首先需要添加System.Runtime.InteropServices引用,并定义必要的委托和API签名。核心的user32.dll函数包括:EnumWindows用于枚举所有顶层窗口;EnumChildWindows用于遍历指定父窗口的子窗口;FindWindow和FindWindowEx用于按类名和窗口名精确查找;GetParent、IsWindowVisible、GetWindowText、GetClassName和GetWindowRect用于获取窗口状态和属性。
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Runtime.InteropServices;
  5. using System.Text;
  6. public class WndHelper
  7. {
  8.     private delegate bool WndEnumProc(IntPtr hWnd, int lParam);
  9.     [DllImport("user32")]
  10.     private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);
  11.     [DllImport("user32.dll")]
  12.     [return: MarshalAs(UnmanagedType.Bool)]
  13.     private static extern bool EnumChildWindows(IntPtr hwndParent, WndEnumProc lpEnumFunc, int lParam);
  14.     [DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
  15.     private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
  16.     [DllImport("user32.dll", EntryPoint = "FindWindowEx", SetLastError = true)]
  17.     private static extern IntPtr FindWindowEx(IntPtr hwndParent, uint hwndChildAfter, string lpszClass, string lpszWindow);
  18.     [DllImport("user32")]
  19.     private static extern IntPtr GetParent(IntPtr hWnd);
  20.     [DllImport("user32")]
  21.     private static extern bool IsWindowVisible(IntPtr hWnd);
  22.     [DllImport("user32")]
  23.     private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount);
  24.     [DllImport("user32")]
  25.     private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
  26.     [DllImport("user32")]
  27.     private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect);
  28.     [StructLayout(LayoutKind.Sequential)]
  29.     private readonly struct LPRECT
  30.     {
  31.         public readonly int Left;
  32.         public readonly int Top;
  33.         public readonly int Right;
  34.         public readonly int Bottom;
  35.     }
  36. }
复制代码

为了方便存储窗口信息,定义WindowInfo结构体,包含句柄、类名、标题、可见性和边界矩形。通过计算Bounds可以判断窗口是否最小化(Left和Top均为-32000)。
  1. public readonly struct WindowInfo
  2. {
  3.     public WindowInfo(IntPtr hWnd, string className, string title, bool isVisible, Rectangle bounds)
  4.     {
  5.         Hwnd = hWnd;
  6.         ClassName = className;
  7.         Title = title;
  8.         IsVisible = isVisible;
  9.         Bounds = bounds;
  10.     }
  11.     public IntPtr Hwnd { get; }
  12.     public string ClassName { get; }
  13.     public string Title { get; }
  14.     public bool IsVisible { get; }
  15.     public Rectangle Bounds { get; }
  16.     public bool IsMinimized => Bounds.Left == -32000 && Bounds.Top == -32000;
  17. }
复制代码

枚举所有顶层窗口时,EnumWindows的回调函数会为每个窗口调用一次。在回调中,通过GetParent判断是否为顶层窗口(从Windows 8起EnumWindows只返回桌面应用顶层窗口,可省去该判断),然后获取类名、标题、可见性和矩形信息,构造成WindowInfo添加到列表中。
  1. private static List<WindowInfo> windowList;
  2. public static IReadOnlyList<WindowInfo> FindAllWindows(Predicate<WindowInfo> match = null)
  3. {
  4.     windowList = new List<WindowInfo>();
  5.     EnumWindows(OnWindowEnum, 0);
  6.     return windowList.FindAll(match ?? DefaultPredicate);
  7. }
  8. private static bool OnWindowEnum(IntPtr hWnd, int lparam)
  9. {
  10.     if (GetParent(hWnd) == IntPtr.Zero)
  11.     {
  12.         var classNameBuilder = new StringBuilder(512);
  13.         GetClassName(hWnd, classNameBuilder, classNameBuilder.Capacity);
  14.         var className = classNameBuilder.ToString();
  15.         var titleBuilder = new StringBuilder(512);
  16.         GetWindowText(hWnd, titleBuilder, titleBuilder.Capacity);
  17.         var title = titleBuilder.ToString().Trim();
  18.         var isVisible = IsWindowVisible(hWnd);
  19.         LPRECT rect = default;
  20.         GetWindowRect(hWnd, ref rect);
  21.         var bounds = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
  22.         windowList.Add(new WindowInfo(hWnd, className, title, isVisible, bounds));
  23.     }
  24.     return true;
  25. }
  26. private static readonly Predicate<WindowInfo> DefaultPredicate = x => x.IsVisible && !x.IsMinimized && x.Title.Length > 0;
复制代码

调用示例:获取所有可见窗口并打印信息,或按标题过滤。
  1. var windows = WndHelper.FindAllWindows();
  2. foreach (var w in windows)
  3.     Console.WriteLine($"{w.Title} - {w.Bounds.X},{w.Bounds.Y}");
  4. // 按标题包含“Test”过滤
  5. var testWindows = WndHelper.FindAllWindows(x => x.Title.Contains("Test"));
复制代码

查找特定窗口句柄时,优先使用FindWindow(按类名和窗口名查找顶层窗口)和FindWindowEx(按父窗口、类名和窗口名查找子窗口)。注意第三个参数lpszClass是Windows类名,不是C#的类名,如果不知道可传null,但可能会匹配多个;最好通过GetClassName确认。另外,如果按钮或控件绑定了快捷键(如“否(&N)”),查找时也必须传入完整的带&的字符串。
  1. // 查找标题为“测试”的顶层窗口
  2. IntPtr hWnd = WndHelper.FindWindow(null, "测试");
  3. if (hWnd != IntPtr.Zero)
  4. {
  5.     // 查找子按钮“点击测试”(假设按钮文本为此)
  6.     IntPtr btnHandle = WndHelper.FindWindowEx(hWnd, IntPtr.Zero, null, "点击测试");
  7.     if (btnHandle != IntPtr.Zero)
  8.     {
  9.         // 发送点击消息(假设WndHelper中有SendClick方法)
  10.         WndHelper.SendClick(btnHandle);
  11.     }
  12. }
  13. // 查找MessageBox弹窗中的“否(&N)”按钮
  14. IntPtr msgBoxHwnd = WndHelper.FindWindow(null, "测试");
  15. if (msgBoxHwnd != IntPtr.Zero)
  16. {
  17.     IntPtr noBtn = WndHelper.FindWindowEx(msgBoxHwnd, IntPtr.Zero, null, "否(&N)");
  18.     if (noBtn != IntPtr.Zero)
  19.         WndHelper.SendClick(noBtn);
  20. }
复制代码

注意:FindWindow和FindWindowEx在winuser.h中定义为FindWindowW(Unicode)或FindWindowA(ANSI),直接使用不带后缀的版本即可,编译器会根据项目设置自动选择。

多线程使用时,上述实现中的静态字段windowList可能产生竞争条件。建议改为在每次调用时创建局部列表,并通过回调参数传递,或者使用Concurrent集合。

以上方法适用于Windows桌面应用的窗口自动化、测试工具开发或系统监控场景。通过组合EnumWindows、FindWindow和FindWindowEx,可以灵活地定位目标窗口及其子控件,然后利用SendMessage或PostMessage模拟用户操作。
回复

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: C#通过Win32 API操作窗口句柄:枚举、查找与控件交互实践

非常干货的分享,代码结构清晰,注释也到位。Win32 API配合P/Invoke确实是做窗口自动化绕不开的老路子,你用WindowInfo封装避免了重复调API确实省心。想追问一个点:FindWindowEx里那个uint类型的hChildAfter(代码里写的是uint hwndChildAfter),实际调用时一般传0或IntPtr.Zero,但这里用uint会不会在一些64位进程中导致精度丢失?另外如果目标窗口是UWP或WinUI 3的,光靠Win32句柄可能抓不到子控件吧?有没有什么变通思路?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-5 15:29 , Processed in 0.029219 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部