C#からdllImportでWin32 APIのEnumWindows関数を使う方法

.NET Frameworkに大抵のものはあらかた揃っていますが、それでもないものもあります。そのため、.NET以外の他所の関数を読み込んで使用する方法があります。いろいろ調べたのでメモ。

やりたかったこ

あるプログラムP1からexeキックしたプログラムP2のタイトルとプロセスIDをC#で取得したい。

経緯の履歴
同じプロセス空間にあるため(インスタンスを生成したような感じらしい)、P2はP1とプロセス名が同じになってしまい、P2のMainWindowTitleが取得できなかった。P1もP2もタスクバーに表示されているからProcess.GetProcessesメソッド(プロセス一覧取得)で取れそうなのに…

「タスクバー タイトル 取得 c#」とかでぐぐったが、直接的な方法をうまく検索できず、断念…

c# 子ウィンドウ 取得」あたりで検索した。
EnumChildWindows関数というものがあった。使ってみたが、ウィンドウというのがイメージと違ってコントロールのことのようで、画面の上に乗っかっているテキストフィールドやラベルなど、細々したコントロールまで含めて列挙するようで、ちょっと違うと思い、保留。

最終的に、EnumWindowsを使って全列挙中から必要なものだけ取得する方法で、なんとか目的を達成できた。

画面上のすべてのウィンドウとそのタイトルを列挙する DOBON.NET
https://dobon.net/vb/dotnet/process/enumwindows.html
を参考にした。
上記ページの説明
「プロセスのメインウィンドウしか探せませんので、同じプロセスが複数のウィンドウを表示している場合は、その内1つしか取得できません。」
の通り、Process.GetProcessesメソッドでは目的の画面のMainWindowTitleが取得できなかった。

dll読み込みの記述

Win32 API(WindowsシステムのAPI)のEnumWindows関数を使用します。トップレベルウィンドウを列挙する関数です。(トップレベルウィンドウとは、親を持たないウィンドウのこと)
↓EnumWindows 関数(MSDN)
https://msdn.microsoft.com/ja-jp/library/cc410851.aspx

BOOL EnumWindows(
  WNDENUMPROC lpEnumFunc,  // コールバック関数
  LPARAM lParam            // アプリケーション定義の値
);

公式リファレンスの「対応情報」の「インポートライブラリ:User32.lib を使用」より、「user32.dll」を読み込めばいいことがわかります。
基本的に.lib→.dllに変換すればOKです。
f:id:mocotanus:20171012054323j:plain

コールバック関数は、falseを返すか、全てを列挙し終わるまで、何度も呼ばれ続けます。

引数のlpEnumFuncにはコールバック関数へのポインタを指定します。デリゲートを渡せばOKです。デリゲートは、タイプセーフな関数ポインタまたはコールバック関数と等価です。

コールバック メソッドとしてのデリゲートのマーシャ リング https://msdn.microsoft.com/ja-jp/library/5zwkzwf4(v=vs.110).aspx

「アプリケーション定義の値」については調べたけど結局よくわかっておらず…IntPtr.Zeroを指定するサンプルばかりだったのでそうしています(´・ω・`)

C#から呼び出すコードの書き方

using System.Runtime.InteropServices;// DllImport属性用

(略)

        // トップレベルウィンドウを列挙する
        [DllImport("user32.dll")]
        private static extern bool EnumWindows(EnumWindowsDelegate lpEnumFunc, IntPtr lParam);

        // EnumWindowsから呼び出されるコールバック関数のデリゲート
        private delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lParam);

using System.Runtime.InteropServices;
 DllImport 属性(DllImportAttribute)の定義場所の名前空間。usingで省略すると便利。
DllImport 属性
 呼び出したい関数を含むDLLを文字列で指定する。
 Win32 API以外でも同様の記述になる。
static修飾子
 必須
extern修飾子
 必須
 関数がC#コードの外部で実装されていることを宣言する。
 一般に、アンマネージ コードを呼び出すときにDllImport 属性と共に使用される。

・publicやprivateは自由

フォームアプリケーションのフォームのコンストラクタから呼んでみる

        private static int cnt = 0;

        // コンストラクタ
        public Form1()
        {
            InitializeComponent();

            // ウィンドウの列挙を開始
            EnumWindows(EnumerateWindows, IntPtr.Zero);

            Console.WriteLine(cnt.ToString());// 2840とかすごい数だった
        } 

        // ウィンドウを列挙するコールバックメソッド
        private static bool EnumerateWindows(IntPtr hWnd, IntPtr lParam)
        {
            cnt++;

            // 何か処理

            // 途中で列挙をやめるときは、return false;となる

            // ウィンドウの列挙を継続する
            return true;            
        }

コード全体のサンプル

Windowsフォームアプリケーションを新規に作成して試しに動かしてみました。タスクバーに表示されているプログラム一覧と非常に近い一覧が取得できました。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Diagnostics;// プロセス用
using System.Runtime.InteropServices;// DllImport用

namespace Sample03
{
    public partial class Form1 : Form
    {
        #region DllImport
        // トップレベルウィンドウを列挙する
        [DllImport("user32.dll")]
        private static extern bool EnumWindows(EnumWindowsDelegate lpEnumFunc, IntPtr lParam);

        // ウィンドウの表示状態を調べる(WS_VISIBLEスタイルを持つかを調べる)
        [DllImport("user32.dll")]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        //ウィンドウのタイトルの長さを取得する
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetWindowTextLength(IntPtr hWnd);

        // ウィンドウのタイトルバーのテキストを取得
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

        // ウィンドウを作成したプロセスIDを取得
        //[DllImport("user32")]// 「.dll」なくても動いてた
        [DllImport("user32.dll")]
        private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); 
        #endregion

        // EnumWindowsから呼び出されるコールバック関数のデリゲート
        private delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lParam);

        #region 【クラス】WindowProp:ウィンドウの情報
        /// <summary>
        /// ウィンドウの情報
        /// </summary>
        public class WindowProp
        {
            public int ProcessId { get; set; }
            public string ProcessName { get; set; }
            public string Title { get; set; }

            public WindowProp()
            {
                this.ProcessId = 0;
                this.ProcessName = "";
                this.Title = "";
            }
        }
        #endregion

        // staticなメソッド内で使用したいプロパティにも、staticが必要
        // 「静的でないフィールド、メソッド、またはプロパティ'~(略)'  で、オブジェクト参照が必要です」なエラーが出る
        /// <summary>ウィンドウ情報のリスト</summary>
        private static List<WindowProp> WindowPropList { get; set; }

        private static int cnt = 0;// 2840とかすごい数だった
        private static int cntIsWindowVisible = 0;// 23
        private static int cntGetWindowTextLength = 0;// 12(IsWindowVisibleで除外しない場合、218)

        // コンストラクタ
        public Form1()
        {
            InitializeComponent();

            WindowPropList = new List<WindowProp>();
            // ウィンドウの列挙を開始
            // ここで渡したIntPtr.Zeroは、EnumerateWindowsの引数lParamに入ってくる
            EnumWindows(EnumerateWindows, IntPtr.Zero);

            foreach (var p in WindowPropList)
            {
                Console.WriteLine(p.ProcessName + " : " + p.Title);
            }
        }

        // ウィンドウを列挙するコールバックメソッド
        private static bool EnumerateWindows(IntPtr hWnd, IntPtr lParam)
        {
            cnt++;
            // すごい大量に列挙するので、条件つけてたくさんはじくといいようです

            // ウィンドウが可視かどうか調べて、表示してないのものを除外する
            if (IsWindowVisible(hWnd) == false) return true;
            cntIsWindowVisible++;

            //ウィンドウのタイトルの長さを取得する
            int textLen = GetWindowTextLength(hWnd);
            if (textLen == 0) return true;
            cntGetWindowTextLength++;

            //ウィンドウのタイトルを取得する
            var title = new StringBuilder(textLen + 1);
            GetWindowText(hWnd, title, title.Capacity);
            // ウィンドウハンドルからプロセスIDを取得
            int processId;
            GetWindowThreadProcessId(hWnd, out processId);
            // プロセスIDからProcessクラスのインスタンスを取得
            Process p = Process.GetProcessById(processId);

            Form1.WindowPropList.Add(new WindowProp()
            {
                ProcessId = processId,
                ProcessName = p.ProcessName,
                Title = title.ToString(),
            });

            // 途中で列挙をやめるときは、return false;にする

            // ウィンドウの列挙を継続する
            return true;
        }
    }
}

Console.WriteLineメソッドでコンソールに出力した文字列は、VisualStudioの「出力」ウィンドウにも表示されます。