trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

最小化状態のウィンドウのハンドルを取得して操作しようとしたら失敗した話

起こったこと

Windows10、C#で、最小化した電卓に対しFindWindow(null, "電卓")でハンドルを取得、ShowWindow(hWnd, nCmdShow)でウィンドウの状態を操作しようとしたが反応しなかった*1。最小化状態でハンドルを取得後、手動で非最小化状態にしてから操作しても期待とは異なる動きをした。

原因

非最小化状態と最小化状態では取得したウィンドウハンドルが異なるため。理由はしらぬい。

f:id:trsing:20191026171137p:plain
非最小化状態
f:id:trsing:20191026171200p:plain
最小化状態
非最小化状態ではApplicationFrameWindowのウィンドウハンドルを取得、最小化状態だとWindows.UI.Core.CoreWindowのウィンドウハンドルを取得していた。

対処療法

次の処理をしてからFindWindowを実行。

  • EnumWindowsウィンドウを列挙、タイトルが目的のものと同じかつ最小化状態であれば最小化状態を解除。

他、FindWindowの一つ目の引数(lpClassName)でApplicationFrameWindowを指定する、EnumWindowsGetClassNameでクラスネームも見てApplicationFrameWindowのものを取得する、でもいけるかもいけないかも *2 ?(未確認)

背景

UIAutomationであそぼーとやってるうちにウィンドウの最大化、最小化とかもしたいなーとなって適当にやっているうちに、最小化状態で開始すると、最大化などができないことに気づきました。ごちゃごちゃやってるうちにどうも取得しているハンドルが違うっぽい?っぽい!Spy++で確認、なんやこれ。なんかEnumWindowsだと全部取得できるらしいですよ奥さん、めんどうだから見つけ次第最小化解除してみたらええんちゃうか、いけたからまあええかみたいな感じ。

f:id:trsing:20191026195710p:plain
最小化状態で取得→手動で非最小化→最小化。これで勘付いた

ソース

static void Main(string[] args)
{
    Console.WriteLine("AppName");
    var name = Console.ReadLine();

    var automationHelper = new AutomationHelper(name);
    while (true)
    {
        Console.WriteLine("Command or AutomationId:");
        var autoid = Console.ReadLine();
        switch (autoid)
        {
            case "Maximize":
                automationHelper.MaximizeWindow();
                break;
            case "Minimize":
                automationHelper.MinimizeWindow();
                break;
            case "Normalize":
                automationHelper.NormalizeWindow();
                break;
            case "Click":
                Console.WriteLine(" x y:");
                var points = Console.ReadLine().Split().Select(x => int.Parse(x)).ToArray();
                automationHelper.Click(points[0], points[1]);
                break;
            default:
                var btnClear = automationHelper.FindInvokePatternById(autoid);
                btnClear.Invoke();
                break;
        }
    }
}
class AutomationHelper
{
    const int MOUSEEVENTF_LEFTDOWN = 0x0002;
    const int MOUSEEVENTF_LEFTUP = 0x0004;

    const int SW_MAXIMIZE = 3;
    const int SW_MINIMIZE = 6;
    const int SW_RESTORE = 9;

    AutomationElement rootElement;
    IntPtr hWnd;
    string title;

    [StructLayout(LayoutKind.Sequential)]
    struct INPUT
    {
        public int type;
        public MOUSEINPUT mi;
    }
    [StructLayout(LayoutKind.Sequential)]
    struct MOUSEINPUT
    {
        public int dx;
        public int dy;
        public int mouseData;
        public int dwFlags;
        public int time;
        public IntPtr dwExtraInfo;
    }

    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    static extern bool ClientToScreen(IntPtr hwnd, out System.Drawing.Point lpPoint);
    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    static extern uint SendInput(uint nInt, INPUT[] pInputs, int cbSize);
    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    extern static int ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    static extern int GetWindowTextLength(IntPtr hWnd);
    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    static extern bool IsIconic(IntPtr hWnd);
    delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lparam);
    [DllImport("user32.dll ", CharSet = CharSet.Auto)]
    static extern bool EnumWindows(EnumWindowsDelegate lpEnumFunc, IntPtr lparam);
    bool EnumWindowCallBack(IntPtr hWnd, IntPtr lparam)
    {
        int textLen = GetWindowTextLength(hWnd);
        if (0 < textLen)
        {
            var tsb = new StringBuilder(textLen + 1);
            GetWindowText(hWnd, tsb, tsb.Capacity);
            //該当のものの非最小化する
            if (tsb.ToString() == title && IsIconic(hWnd))
                ShowWindow(hWnd, SW_RESTORE);
        }
        return true;
    }

    public AutomationHelper(string title)
    {
        this.title = title;
        EnumWindows(new EnumWindowsDelegate(EnumWindowCallBack), IntPtr.Zero);
        hWnd = FindWindow(null, title);
        rootElement = AutomationElement.FromHandle(hWnd);
    }

    //指定座標(クライアント座標基準)をクリック
    public void Click(int x, int y)
    {
        var pt = new System.Drawing.Point();
        ClientToScreen(hWnd, out pt);
        var ptt = new System.Drawing.Point(pt.X + x, pt.Y + y);
        System.Windows.Forms.Cursor.Position = ptt;
        INPUT[] input = new INPUT[2];
        input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
        input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
        SendInput(2, input, Marshal.SizeOf(input[0]));
    }
    //AutomationIdから取得
    public AutomationElement FindElementById(string automationId)
    {
        var cnd = new PropertyCondition(AutomationElement.AutomationIdProperty, automationId);
        return rootElement.FindFirst(TreeScope.Element | TreeScope.Descendants, cnd);
    }
    public InvokePattern FindInvokePatternById(string automationId)
    {
        return FindElementById(automationId).GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
    }
    //Nameから取得
    public IEnumerable<AutomationElement> FindElementsByName(string name)
    {
        var cnd = new PropertyCondition(AutomationElement.NameProperty, name);
        return rootElement.FindAll(TreeScope.Element | TreeScope.Descendants, cnd).Cast<AutomationElement>();
    }
    public InvokePattern FindInvokePatternByName(string name)
    {
        return FindElementsByName(name).FirstOrDefault().GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
    }
    public IEnumerable<AutomationElement> FindButtonByName(string name)
    {
        const string BUTTON_CLASS_NAME = " Button ";
        return FindElementsByName(name).Where(x => x.Current.ClassName == BUTTON_CLASS_NAME);
    }
    public InvokePattern FindButtonInvokePatternByName(string name)
    {
        return FindButtonByName(name).FirstOrDefault().GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
    }

    public void MaximizeWindow() => ControlWindow(SW_MAXIMIZE);
    public void MinimizeWindow() => ControlWindow(SW_MINIMIZE);
    public void NormalizeWindow() => ControlWindow(SW_RESTORE);
    public int ControlWindow(int nCmdShow) => ShowWindow(hWnd, nCmdShow);
}

参考

UIAutomationとかいろいろ

C#にてマウスとキーボードを操りし者 - Qiita

RPA九人衆による「アカネチャンカワイイヤッタ」の自動化 - Qiita

UIAutomationで.Net製デスクトップアプリのGUIコンポーネントの自動制御を試みるまでのハートフルストーリー - たーせる日記

C# - c#でcalc.exeを最小化するには|teratail

起動中のMicrosoft EdgeからタイトルとURLを取得するC#コード(DOM編) | 初心者備忘録

画面上のすべてのウィンドウとそのタイトルを列挙する - .NET Tips (VB.NET,C#...)

*1:Windows8.1では発生しなかった。OSが原因というより電卓のせいぽい?

*2:ApplicationFrameWindowで決め打ちして問題ないかわかんないしなー