#74　【C#】他ウィンドウを操作する

## 概要

[前回](https://www.nxted.co.jp/blog/blog_detail?id=71)、C#の他ウィンドウ操作の事前準備として、エクスプローラーのクラス名とタブ追加時のメッセージパラメータを調査しました。

今回はその情報を使って実際にウィンドウ操作をやってみます。

3パターン作成していますが、ウィンドウ操作の仕組みを見るなら実装1のみで大丈夫です。
実装2, 3は応用編です。
C# .NETフレームワーク のコンソールアプリとして作成しています。

## 実装1: タブ追加実行

FindWindow関数でクラス名からウィンドウハンドルを取得し、
タブ追加時と同様のメッセージをPostMessage関数で送信することで、
タブ追加を実行することができます

```cs
// Win32 APIをインポート
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
private static extern IntPtr SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

const uint WM_COMMAND = 0x0111;

[STAThread]
static void Main(string[] args)
{
    // エクスプローラのウィンドウ取得
    IntPtr hwnd = FindWindow("CabinetWClass", null);

    if (hwnd != IntPtr.Zero)
    {
        // ウィンドウ表示
        SetForegroundWindow(hwnd);

        // 新規タブ追加
        PostMessage(hwnd, WM_COMMAND, (IntPtr)0x0000A21B, (IntPtr)0x00000000);
    }
    else
    {
        Console.WriteLine("ウィンドウが見つかりませんでした。");
        Console.ReadLine();
    }
}
```

## 実装2: アドレス移動

もちろん、PostMessageで実行することができない操作もあります。
そういった場合も、キーボードやマウスのイベントを起こすことで実行できることがあります。

例として、エクスプローラーのアドレス移動をやってみます。
この操作は、PostMessageで実行できない(可能かもしれませんが私はちょうどいいメッセージを見つけられませんでした)のですが、以下のコードでは、
アドレスバーにフォーカス⇒クリップボードにアドレスを設定⇒ペースト⇒Enterキーイベント送信
という流れを実施することで`C:\Windows\System32`へのアドレス移動を実現しています。
ごり押し感が否めなく、順番の担保もsleepで無理やり行っているので微妙ですが一応こういうこともできますという紹介です。

```cs
// Win32 APIをインポート
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
private static extern IntPtr SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

const uint WM_COMMAND = 0x0111;

[STAThread]
static void Main(string[] args)
{
    // エクスプローラのウィンドウ取得
    IntPtr hwnd = FindWindow("CabinetWClass", null);

    if (hwnd != IntPtr.Zero)
    {
        // ウィンドウ表示
        SetForegroundWindow(hwnd);

        // アドレスバーフォーカス
        PostMessage(hwnd, WM_COMMAND, (IntPtr)0x00001006, (IntPtr)0x00000000);
        Thread.Sleep(500);
        // アドレス入力
        Clipboard.SetText("C:\\Windows\\System32");
        SendKeys.SendWait("^v");
        Thread.Sleep(1000);
        // 確定
        SendKeys.SendWait("{ENTER}");
    }
    else
    {
        Console.WriteLine("ウィンドウが見つかりませんでした。");
        Console.ReadLine();
    }
}
```

## 実装3: アドレス取得

実装1,2ではウィンドウに対して情報を送信したので、今度は逆にウィンドウから情報を取得してみます。

エクスプローラのオプションから、「タイトルバーに完全なパスを表示する」をONにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843084/517f7019-493c-7f37-b632-8b55781e7c7e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843084/48cd226b-7f62-030b-97db-5a1099fef8b5.png)
このオプションをつけると、エクスプローラのタブ名に開いているフォルダのフルパスが表示されるようになります。
では、エクスプローラで開かれているタブのパスをまとめて取得してみます
```cs
// Win32 APIをインポート
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
private static extern IntPtr SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumChildWindows(IntPtr handle, EnumWindowsDelegate enumProc, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowTextLength(IntPtr hWnd);

const uint WM_COMMAND = 0x0111;
const int MAX_LENGTH = 500;

private delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lParam);

private static int cnt = 0;

[STAThread]
static void Main(string[] args)
{
    // エクスプローラのウィンドウ取得
    IntPtr hwnd = FindWindow("CabinetWClass", null);

    if (hwnd != IntPtr.Zero)
    {
        // 子ウィンドウ探索
        EnumChildWindows(hwnd, EnumChildWindowCallBack, IntPtr.Zero);
        if (cnt == 0)
        {
            Console.WriteLine("タブウィンドウが見つかりませんでした。");
        }
    }
    else
    {
        Console.WriteLine("ウィンドウが見つかりませんでした。");
    }
    Console.ReadLine();
}

private static bool EnumChildWindowCallBack(IntPtr hWnd, IntPtr lparam)
{
    //ウィンドウのクラス名を取得する
    StringBuilder className = new StringBuilder(MAX_LENGTH);
    GetClassName(hWnd, className, className.Capacity);
    // エクスプローラのタブウィンドウのとき処理する
    if (className.ToString() == "ShellTabWindowClass")
    {
        cnt++;
        //ウィンドウのタイトルを取得する
        int textLen = GetWindowTextLength(hWnd);
        StringBuilder title = new StringBuilder(textLen + 1);
        GetWindowText(hWnd, title, title.Capacity);
        Console.WriteLine(title);
    }
    return true;
}
```
実装1, 2登場していなかった関数もいくつか登場しています。
EnumChildWindows によってエクスプローラの子ウィンドウを列挙し、GetClassName を使用してその中からタブウィンドウだけを抽出し、GetWindowText によってタイトルバー(今回はタブのフルパス)を取得しています

エクスプローラでタブを開いた状態でこのプログラムを実行すると、このように各タブのパスが表示されます
```
C:\Users
C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools
C:\Windows\System32
```

## まとめ

今回は、.NETフレームワークから他ウィンドウの操作を行ってみました。
実装1,2,3を組み合わせれば、Chromeのように、Ctrl + Shift + T でエクスプローラで1度削除したタブを復活させるツールなども作成可能かと思います。
今回は例としてエクスプローラを使用しましたが、それ以外のウィンドウについてもメッセージで動かせるものであれば同様に実装可能です。
参考になれば幸いです。
最後まで読んでいただきありがとうございます！