2011年11月18日金曜日

.net framework : マウスカーソルの位置にあるウィンドウの情報を得る

2018/11/08 追記) WindowFromPointが動かないのは引数の間違いでした。 補足情報を投稿しておきます。

以降、過去の投稿に関しては書き換えていません。


前の投稿「wpf : マウスキャプチャとマウスカーソルの変更」の続きです。 コードは前の投稿のMainWindow.xaml.csを見てください。 ここでは「マウスキャプチャ中にマウスが指しているウィンドウの情報(タイトル、ウィンドウクラス、実行ファイル名)を得る」という部分について書きます。 前回書いたとおり躓きました。 「win32sdkの時代の定石コードを探してきてDllImportしてアレコレ」で済むかと思ってたんですが、64bitの壁に阻まれました。

サンプルコードを試した環境はwindows7 64bit版です。 で、その64bit環境で試したら、アプリケーションをx86(32bit)でビルドしたのとx64(64bit)でビルドしたのとで挙動が違ってました。

この部分のコードは次のような手順になっています。

  1. WindowFromPointでマウスが指しているウィンドウを探す。
  2. GetWindowTextでウィンドウのタイトルを得る。
  3. GetClassNameでウィンドウクラスを得る。
  4. プロセス操作で実行ファイル名を得る。

躓いたのは「1:WindowFromPoint」と「4:プロセス操作」です。 構成をx86(32bit)にして動作させると「4:プロセス操作」で64bitプロセスの実行ファイル名が得られませんでした。 構成をx64(64bit)にして動作させると「1:WindowFromPoint」が動いてくれませんでした。

まずは「1:WindowFromPoint」について、「WindowFromPoint 64bit」というキーワードで検索してみたら、「動かない」「バグなのか?」と書かれたサイトが大量にヒットします。 どうやら32bit構成にするしかなさそうですね。

一応、64bit版でのWindowFromPointを実現するのにこんな方法も空想できます。

  • 64bit環境でも動くWindowFromPointを自作する。
  • 32bitの別プロセスでWindowFromPointをさせてプロセス間通信でウィンドウハンドルを得る。

ただこれは、やりたいことと手間のバランスを考えるとあんまり良くなさそうです。 実際、WindowFromPoint自作の方に手を出したら、レイヤードウィンドウのヒットテストのやり方が分からなくて詰みました。 ということでアプリケーションは32bitにすることに決定。

「4:プロセス操作で実行ファイル名を得る」で躓いたところについて。 この部分では、当初次のような手順で実行ファイル名を探していました。

  1. GetWindowThreadProcessIdでプロセスIDを得る。
  2. OpenProcessでプロセスハンドルを得る。
  3. EnumProcessModulesでモジュールハンドルを得る。
  4. GetModuleFileNameExにプロセスハンドルとモジュールハンドルを渡して実行ファイル名を得る。

win32sdkが全盛だったころの定石ですね。 しかし、それでは不完全でした。 ↑で書いたとおり、WindowFromPointが64bit未対応のせいでアプリケーションを32bitにするしかないわけですが、それでは64bitアプリケーションの実行ファイル名が得られません。 関係ありそうな情報を検索したところ、こんなページを発見。

どうやら、EnumProcessModulesを使ったのが良くなかったようですね。 32bitアプリケーションでEnumProcessModulesをすると64bitのモジュールが列挙できない模様。 QueryFullProcessImageNameというAPIがあり、そちらを使えば32bitアプリケーションからも64bitプロセスの実行ファイル名を得られました。 手順はこのようになります。

  1. GetWindowThreadProcessIdでプロセスIDを得る。
  2. OpenProcessでプロセスハンドルを得る。
  3. QueryFullProcessImageNameで実行ファイル名を得る。

ただ、QueryFullProcessImageNameはVista以降でしか使えないようです。

  • win32apiのDllImportを使ってコードを書く。
  • x86構成でビルド。

という条件の下で今回のコードを多くの環境で動かしたかったら、

  1. 32bit環境では↑で最初に書いたEnumProcessModulesを使う。
  2. 64bit環境(Vista以降)ではQueryFullProcessImageNameを使う。
  3. 64bitのXPは非対応、ではあんまりなので不完全な動作となるが32bit環境と同様に処理する。

とすることになりそうですね。 しょっぱい。 おとなしく.net frameworkのSystem.Diagnostics.Processを使った方が良かったかな? DllImportでなんとかすることに気を取られて、.netの方に考えが向かなかったんですよね。 反省。

ちなみに、OSが64bit版かどうかはEnvironment.Is64BitOperatingSystemで分かります。 ついでに、プロセスが64bit版かどうかはEnvironment.Is64BitProcessで分かります。 プロセスの方は if(IntPtr.Size == 8) というコードでもチェック可能。 OSのバージョンはEnvironment.OSVersionで分かります。 バージョンの数字についてはこのページに詳しく載ってました。

と、こんなもんかな? 3つのウィンドウ情報を調べるだけで妙な深さのノウハウを調べることになってしまいました。 wpfネタのハズだったんですけどねぇ。

最後に、前の投稿に載せてなかったDllImportのコードを載せときます。

// Win32dll.cs
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace MouseCaptureTest
{
    internal class Win32dll
    {
        public const int STRING_BUFFER_LENGTH = 1024;

        [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern IntPtr WindowFromPoint(int x, int y);

        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        private static extern int GetWindowText(IntPtr hWnd, string lpString, int cch);

        public static string GetWindowText(IntPtr hWnd)
        {
            string text = new string((char)0, STRING_BUFFER_LENGTH);
            int len = GetWindowText(hWnd, text, text.Length);
            if (len == 0)
                return null;
            else
                return text.Substring(0, len);
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        private static extern int GetClassName(IntPtr hWnd, string lpString, int cch);

        public static string GetClassName(IntPtr hWnd)
        {
            string text = new string((char)0, STRING_BUFFER_LENGTH);
            int len = GetClassName(hWnd, text, text.Length);
            if (len == 0)
                return null;
            else
                return text.Substring(0, len);
        }

        public enum GetAncestorFlags : uint
        {
            GA_PARENT = 1,
            GA_ROOT = 2,
            GA_ROOTOWNER = 3
        }
        [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern IntPtr GetAncestor(IntPtr hWnd, GetAncestorFlags gaFlags);

        [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint ProcessId);

        public enum ProcessAccessFlags : uint
        {
            Terminate = 0x00000001,
            CreateThread = 0x00000002,
            VMOperation = 0x00000008,
            VMRead = 0x00000010,
            VMWrite = 0x00000020,
            DuplicateHandle = 0x00000040,
            CreateProcess = 0x00000080,
            SetQuota = 0x00000100,
            SetInformation = 0x00000200,
            QueryInformation = 0x00000400,
            SuspendResume = 0x00000800,
            QueryLimitedInformation = 0x00001000,
            Synchronize = 0x00100000
        }
        [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

        [DllImport("psapi.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern bool EnumProcessModules(IntPtr hProcess, [MarshalAs(UnmanagedType.LPArray)] [In][Out] IntPtr[] lphModule, uint cb, out uint lpcbNeeded);

        [DllImport("psapi.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern int GetModuleFileNameEx(IntPtr hProcess, IntPtr hModule, string lpBaseName, int nSize);

        public static string GetModuleFileNameEx(IntPtr hProcess, IntPtr hModule)
        {
            string text = new string((char)0, STRING_BUFFER_LENGTH);
            int len = GetModuleFileNameEx(hProcess, hModule, text, text.Length);
            if (len == 0)
                return null;
            else
                return text.Substring(0, len);
        }

        [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern bool CloseHandle(IntPtr handle);

        [DllImport("psapi.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        private static extern int GetProcessImageFileName(IntPtr hProcess, string lpString, int cch);

        public static string GetProcessImageFileName(IntPtr hProcess)
        {
            string text = new string((char)0, STRING_BUFFER_LENGTH);
            int len = GetProcessImageFileName(hProcess, text, text.Length);
            if (len == 0)
                return null;
            else
                return text.Substring(0, len);
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern bool QueryFullProcessImageName(IntPtr hProcess, uint dwFlags, string lpExeName, ref int lpdwSize);

        public static string QueryFullProcessImageName(IntPtr hProcess, bool native)
        {
            uint dwFlags = (native ? (uint)0x00000001 : (uint)0);

            string text = new string((char)0, STRING_BUFFER_LENGTH);
            int len = text.Length;
            if (QueryFullProcessImageName(hProcess, dwFlags, text, ref len))
                return text.Substring(0, len);
            else
                return null;
        }

        public const uint WS_EX_TRANSPARENT = 0x00000020;
        public const int GWL_EXSTYLE = -20;
        [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern uint GetWindowLong(IntPtr hWnd, int index);

        [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern uint SetWindowLong(IntPtr hWnd, int index, uint unValue);
    }
}