C#实现操作Windows窗口句柄:遍历、查找窗体和控件【窗口句柄最全总结之一】

11,971 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情

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作为FindWindowWFindWindowA的别名,它根据UNICODE定义的预处理常量自动选择该函数的ANSI或Unicode版本。

注意,混合使用编码将可能导致编译或运行时错误。通常推荐直接FindWindow,而不要直接使用FindWindowWFindWindowA

FindWindowEx同样,为FindWindowExAFindWindowExW的自动别名。

附:关于上面使用遍历窗口API查找窗体时的静态字段windowList和childWindowList

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

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

参考