添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
+关注继续查看

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 用于循环窗口句柄时处理每个句柄,但是,由于是共用的静态字段,如果遇到多线程的情况下,肯定会出现问题或混乱。

因此,最好修改下代码,处理多线程使用时,这两个字段的竞争。

参考

C#实现操作Windows窗口句柄:常用窗口句柄相关API、Winform中句柄属性和Process的MainWindowHandle问题【窗口句柄总结之三】
本篇主要介绍一些与窗口句柄相关的一些API,比如设置窗口状态、当前激活的窗口、窗口客户区的大小、鼠标位置、禁用控件等,以及介绍Winform中的句柄属性,便于直接获取控件或窗体句柄,以及不推荐...
C#实现操作Windows窗口句柄:SendMessage/PostMessage发送系统消息、事件和数据【窗口句柄总结之二】
SendMessage/PostMessage API 可以实现发送系统消息,这些消息可以定义为常见的鼠标或键盘事件、数据的发送等各种系统操作......
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 尝试进行瞬移操作 | 尝试查找飞天漏洞 )
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 尝试进行瞬移操作 | 尝试查找飞天漏洞 )
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 遍历查找后坐力数据 | 尝试修改后坐力数据 )
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 遍历查找后坐力数据 | 尝试修改后坐力数据 )
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 从内存结构中根据寻址路径查找子弹数据的内存地址 )(三)
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 从内存结构中根据寻址路径查找子弹数据的内存地址 )(三)
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 从内存结构中根据寻址路径查找子弹数据的内存地址 )(二)
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 从内存结构中根据寻址路径查找子弹数据的内存地址 )(二)
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 从内存结构中根据寻址路径查找子弹数据的内存地址 )(一)
【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 从内存结构中根据寻址路径查找子弹数据的内存地址 )(一)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(四)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(四)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(三)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(三)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(二)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(二)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(一)
【Windows 逆向】使用 CE 分析内存地址 ( 运行游戏 | 使用 CE 工具分析游戏内子弹数量对应的内存地址 | 内存地址初步查找 | 使用二分法定位最终的内存地址 )(一)
Windows 技术篇-通过注册表查找vc运行库所在位置实战演示,通过ProductCode查看vc++运行库安装位置
Windows 技术篇-通过注册表查找vc运行库所在位置实战演示,通过ProductCode查看vc++运行库安装位置
成功解决(Win32): 已加载“C:\Windows\SysWOW64\ntdll.dll”。无法查找或打开 PDB 文件。
成功解决(Win32): 已加载“C:\Windows\SysWOW64\ntdll.dll”。无法查找或打开 PDB 文件。