C# による Windows ウィンドウハンドル操作クラスの設計と実装

概要

Windows 環境において、外部アプリケーションのウィンドウをプログラムから制御する場合、WinAPI を経由したハンドル操作が有効な手段となります。.NET Framework 環境では P/Invoke を利用してこれらの機能 wrappers を作成し、オブジェクト指向的に扱うことで、保守性と再利用性を高めることができます。本稿では、カーソル位置からのハンドル取得、テキスト操作、およびウィンドウ階層構造に基づく識別子の永続化手法について解説します。

カーソル位置からのウィンドウ識別

まず、現在のマウスカーソルが位置するウィンドウのハンドルを取得する機能を実装します。これには GetCursorPosWindowFromPoint の 2 つの API を組み合わせます。以下のラッパーメソッドは、座標取得とハンドル解決を一元管理します。

using System;
using System.Drawing;
using System.Runtime.InteropServices;

public static class NativeInterop
{
    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool GetCursorPos(out Point lpPoint);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr WindowFromPoint(Point Point);

    /// <summary>
    /// 現在のカーソル位置にあるウィンドウのハンドルを取得します
    /// </summary>
    public static IntPtr ResolveHandleFromCursor()
    {
        Point currentPosition = new Point();
        if (GetCursorPos(out currentPosition))
        {
            return WindowFromPoint(currentPosition);
        }
        return IntPtr.Zero;
    }
}

ウィンドウテキストの操作

特定のコントロール(例えば入力ボックス)のテキスト値を変更するには、SendMessage API を使用して WM_SETTEXT メッセージを送信します。ハンドルさえ判明していれば、プロセスを跨いで値を書き込むことが可能です。

public static class NativeInterop
{
    // ... 前述のコード ...

    private const int WM_SET_TEXT_VALUE = 0x000C;

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, string lParam);

    /// <summary>
    /// 指定されたウィンドウのテキスト内容を変更します
    /// </summary>
    public static void UpdateWindowText(IntPtr handle, string content)
    {
        SendMessage(handle, WM_SET_TEXT_VALUE, IntPtr.Zero, content);
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

    /// <summary>
    /// 指定されたウィンドウのテキスト内容を取得します
    /// </summary>
    public static string RetrieveWindowText(IntPtr handle)
    {
        StringBuilder buffer = new StringBuilder(256);
        GetWindowText(handle, buffer, buffer.Capacity);
        return buffer.ToString();
    }
}

ハンドルの永続化と階層構造の追跡

ウィンドウハンドルはアプリケーションの再起動などにより変化するため、単純なハンドル値の保存は信頼性に欠けます。より安定した識別子として、「クラス名」と「親ウィンドウ内での出現順序(インデックス)」の組み合わせを用います。これにより、ウィンドウツリーを辿って特定のコントロールを再現的に特定できます。

必要な API には、クラス名取得用の GetClassName、親ウィンドウ取得用の GetParent、子ウィンドウ検索用の FindWindowEx などがあります。

public static class NativeInterop
{
    // ... 前述のコード ...

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetParent(IntPtr hWnd);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    /// <summary>
    /// 親ウィンドウ配下にある特定のクラス名を持つ子ハンドルを列挙します
    /// </summary>
    public static List<IntPtr> EnumerateChildHandles(IntPtr parentHandle, string className)
    {
        List<IntPtr> handles = new List<IntPtr>();
        IntPtr current = IntPtr.Zero;
        
        while (true)
        {
            current = FindWindowEx(parentHandle, current, className, null);
            if (current == IntPtr.Zero) break;
            handles.Add(current);
        }
        return handles;
    }

    /// <summary>
    /// ウィンドウのクラス名を取得します
    /// </summary>
    public static string RetrieveClassName(IntPtr handle)
    {
        StringBuilder buffer = new StringBuilder(128);
        if (GetClassName(handle, buffer, buffer.Capacity) == 0)
        {
            throw new InvalidOperationException("Handle class name retrieval failed.");
        }
        return buffer.ToString();
    }
}

WindowElement クラスの実装

上記の API をラップし、ウィンドウの階層情報を保持する WindowElement クラスを定義します。このクラスは自身を識別するための署名(Signature)を生成し、また署名から実際のハンドルを解決する機能を持ちます。署名形式は「クラス名:インデックス」をセミコロンで連結した文字列とします。

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;

public class WindowElement
{
    public IntPtr Handle { get; private set; }
    public string WindowClass { get; private set; }
    public WindowElement ParentElement { get; private set; }
    public int SiblingIndex { get; private set; }

    private Rectangle _cachedRect;

    private WindowElement() { }

    public WindowElement(IntPtr hWnd)
    {
        if (hWnd == IntPtr.Zero) throw new ArgumentException("Invalid handle");
        
        this.Handle = hWnd;
        this.WindowClass = NativeInterop.RetrieveClassName(hWnd);
        this.ParentElement = ResolveParent();
        this.SiblingIndex = CalculateSiblingIndex();
        this._cachedRect = NativeInterop.GetWindowRect(hWnd);
    }

    private WindowElement ResolveParent()
    {
        IntPtr parentPtr = NativeInterop.GetParent(this.Handle);
        if (parentPtr == IntPtr.Zero) return null;
        return new WindowElement(parentPtr);
    }

    private int CalculateSiblingIndex()
    {
        IntPtr parentPtr = this.ParentElement?.Handle ?? IntPtr.Zero;
        var siblings = NativeInterop.EnumerateChildHandles(parentPtr, this.WindowClass);
        return siblings.IndexOf(this.Handle);
    }

    public string Text
    {
        get { return NativeInterop.RetrieveWindowText(this.Handle); }
        set { NativeInterop.UpdateWindowText(this.Handle, value); }
    }

    /// <summary>
    /// 現在のウィンドウ階層を文字列署名として生成します
    /// </summary>
    public override string ToString()
    {
        StringBuilder path = new StringBuilder();
        WindowElement current = this;
        
        while (current != null)
        {
            string escapedClass = EscapeString(current.WindowClass);
            path.Insert(0, $"{escapedClass}:{current.SiblingIndex};");
            current = current.ParentElement;
        }
        
        return path.ToString().TrimEnd(';');
    }

    /// <summary>
    /// 署名文字列からウィンドウ要素を復元します
    /// </summary>
    public static WindowElement ResolveFromSignature(string signaturePath)
    {
        if (string.IsNullOrEmpty(signaturePath)) return null;

        string[] parts = signaturePath.Split(';');
        string[] rootPart = parts[parts.Length - 1].Split(':');
        string rootClass = UnescapeString(rootPart[0]);
        
        // デスクトップを親としてルート候補を検索
        var rootHandles = NativeInterop.EnumerateChildHandles(IntPtr.Zero, rootClass);
        string[] childPath = new string[parts.Length - 1];
        Array.Copy(parts, childPath, parts.Length - 1);

        foreach (IntPtr rootHandle in rootHandles)
        {
            IntPtr currentHandle = rootHandle;
            bool match = true;

            for (int i = childPath.Length - 1; i >= 0; i--)
            {
                string[] segment = childPath[i].Split(':');
                string className = UnescapeString(segment[0]);
                int index = int.Parse(segment[1]);

                var children = NativeInterop.EnumerateChildHandles(currentHandle, className);
                if (index < 0 || index >= children.Count)
                {
                    match = false;
                    break;
                }
                currentHandle = children[index];
            }

            if (match) return new WindowElement(currentHandle);
        }

        return null;
    }

    public static WindowElement CaptureFromCursor()
    {
        return new WindowElement(NativeInterop.ResolveHandleFromCursor());
    }

    private static string EscapeString(string input)
    {
        return input.Replace(":", "\\:").Replace(";", "\\;");
    }

    private static string UnescapeString(string input)
    {
        return input.Replace("\\:", ":").Replace("\\;", ";");
    }
}

利用事例

このクラスを利用することで、例えば特定のアプリケーション起動後に、以前記録した署名パスを用いて入力フォームを自動埋めするといった処理が可能になります。ハンドルが変化しても、ウィンドウツリー構造とクラス名が維持されていれば正確にターゲットを特定できます。

// 現在のカーソル下のウィンドウ情報を取得
var target = WindowElement.CaptureFromCursor();
string signature = target.ToString();

// 後ほど署名から復元して操作
var restored = WindowElement.ResolveFromSignature(signature);
if (restored != null)
{
    restored.Text = "自動入力された値";
}

タグ: C# WinAPI Interop UIAutomation DesktopDevelopment

5月25日 17:27 投稿