WindowsTerminal をWinUI3のパネルに召喚

# WindowsTerminalのホスト
WindowsTerminalはすごく扱いやすいソフトだとおもいませんか？
そしてそれをそのままWinUI3のパネルに入れてGUIでも動かせるようなそんなアプリを作りたいとは思ったことはありませんか？
例えばパスの指定の際、わざわざエクスプローラーも同時に開いてるあなた、コンソールの横にディレクトリツリーが常時表示してあり、そのツリーをダブルクリックすることでそのパスがTerminalにコピーされるようなアプリが欲しいと思ったことはありませんか？
今回はそんな希望を叶えるためにいろいろチャレンジしてみたので、その備忘録を行います。

# 伝家の宝刀DLL IMPORT
WindowsTerminalなどのアプリを自分が作成するプロジェクトに取り込みたい場合、必ず通らないといけないのは`Win32API`です。
Windowsの様々な機能を取り扱っているAPIなのですが、アプリがどのアプリにホストされているかというのもこの`Win32API`で変更することができます。
そしてC#にはこの`Win32API`を簡単に扱えるようにしたものが標準で装備されています。
その名も`DILL IMPORT`
これは`Win32API`はもちろん他のC++などでネイティブコードとしてコンパイルされたdllをC#で簡単に呼び出してしまおうという優れものになります。
今回はこれを使用して、WinUI3アプリにWindowsTerminalを組み込んでみるところまでやってみたいと思います。

# MainWindow
今回は汎用性を考慮して`MainWindow`のUIで`TerminalPage`を呼び出し、`TerminalPage`の中でWindowsTerminalのホスト処理をしていく設計にしたいと思います。

まず`MainWindow.xaml.cs`の処理はこちらです。
```csharp
public sealed partial class MainWindow : Window
{
    // ホスト処理に重要になってくるこのアプリケーションのウィンドウハンドル
    IntPtr myHWnd;
    public MainWindow()
    {
        this.InitializeComponent();
        // このアプリケーション自体のウィンドウハンドルを取得する
        myWhnd = WinRT.Interop.WindowNative.GetWindowHandle(this);        
    }
}
```
やっていることは単純にこのアプリのウィンドウハンドルを取得しているだけです。
`WinRT.Interop.WindowNative.GetWindowHandle({Windowクラス})`で取得できるのです。
いろいろやってみた結果、この関数がうまく作動してウィンドウハンドルを取得できるのはWindowクラスだけになりますので、この`MainWindow`クラスで取得を行っています。

続いてUI部分です
```xml
<Window
    x:Class="TerminalHositing.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TerminalHositing"
    xmlns:view="using:TerminalHositing.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    
    <view:TermianlPage HWnd="{x:Bind myWhnd}"/>
</Window>
```
ホスト処理を行うための`TerminalPage`というクラスを作成して、それをUI側で呼び出している形になります。
後述する、`TerminalPage`に作成した依存関係プロパティに`MainWindow`のウィンドウハンドルをデータバインディングで渡しています。

# TerminalPage 依存関係プロパティ
WinUI3でXaml部で記述できるプロパティを任意に設定したいとき、依存関係プロパティを作成して対応します。
今回はウィンドウハンドルを受け取るプロパティを作りたいので以下のように記述しました。
```csharp
// これが依存関係プロパティ
// DependencyProperty.Register()にて設定を行う
public static readonly DependencyProperty hWndProperty =
  DependencyProperty.Register(
    nameof(HWnd),// 以下のプロパティラッパーの名前を設定
    typeof(long),// 取り扱うプロパティの型名(IntPtr型は依存関係プロパティ非対応なのでlong)
    typeof(TerminalPage),// ホストされるクラス名(定義されている現在のクラス名)
    null// メタデータ、基本nullでよし
  );

// これがプロパティラッパー
// 普段使用しているようなプロパティはこれで、処理はこちらのプロパティを使って行う
public IntPtr HWnd
{
    get { return new IntPtr((long)GetValue(hWndProperty)); }
    set { SetValue(hWndProperty, value.ToInt64()); }
}
```
上記が依存関係プロパティとプロパティラッパーになります。
UI側から情報が送られてきたものが依存関係プロパティ`hWndProperty`に入れられ、その情報がプロパティラッパー`HWnd`に送られます。
このプロパティラッパー`HWnd`を用いて様々な処理を行っていきます。
依存関係プロパティは`DependencyProperty.Register`で設定します。
1.やりとりする先のプロパティ
2.保持するプロパティの型名
3.ホストされるクラス名
4.メタデータ
で設定します。
2.の保持するプロパティは本当は`IntPtr`を使用したいのですが、非対応のため代わりに`long`で保持しています。
プロパティラッパーに渡されるとき`IntPtr`にキャストしてプロパティを保持する形となります。
3.は現在定義しているクラスになります。今回はTerminalPageに定義されているのでそれを指定しています。
これで`MainWindow`から与えられたウィンドウハンドルが取得できるようになります。

# WindowsTerminalをプログラムから起動
WindowsTerminalをそのまま入れ込むことに必要なのはプログラムから起動する処理が必要です。
```csharp
var process = new Process();
process.StartInfo.FileName = "wt.exe";
process.StartInfo.UseShellExecute = false;
process.Start();
```
これでWindowsTerminalがプログラムから起動できました。

では次にこの起動したWindowsTerminalのウィンドウハンドルを取得します。
この時FindWindowという指定したアプリのWindowハンドルを取得するWin32APIを使っています。
```csharp
// DllImport定義(Win32APIのFindWindowを使用するという定義)
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

// ページにWindowsTerminalを埋め込む処理
private void EmbedWindowsTerminal()
{
    // 上記のWindowsTerminal起動処理
    var process = new Process();
    process.StartInfo.FileName = "wt.exe";
    process.StartInfo.UseShellExecute = false;
    process.Start();

    // アプリケーション側のウィンドウハンドルが取得できていなかったときの処理
    if(HWnd == IntPtr.Zero)
    {
        Console.WriteLine("hWnd=null");
        return;
    }

    // 起動してるアプリのウィンドウハンドルが取得できないことがあるので、取得できていないときは取得処理を繰り返す。
    while (terminalHandle == IntPtr.Zero)
    {
        // 立ち上げたWindowsTerminalのウィンドウハンドルを取得してる
        terminalHandle = FindWindow("CASCADIA_HOSTING_WINDOW_CLASS", null);
    }
}
```

WindowsTerminalをPageに埋め込む場合はホスト元となる親ウィンドウハンドルとWindowsTerminalのウィンドウハンドル2つが必要なためこの処理で取得しています。

この2つのウィンドウハンドルを使い`Win32API`の`SetParent()`,`SetWindowLong`,`GetWindowLong`にてWinUI3に埋め込んでいきます。
```csharp
// 埋め込み
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

// WindowsStyleの変更
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
```
`SetParent`で埋め込みはできるのですが、それだけだと閉じるボタンなどが表示されたものごと埋め込まれてしまうので、上記のWindowStyleを変更できる`Win32API`によってそれらを非表示にしていく形とのなります。

以下がその処理になります。
今まで記述したものもまとめております。
```diff_c_sharp
public sealed partial class TerminalPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
    }
    // Dll Import
    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

+   [DllImport("user32.dll", SetLastError = true)]
+   private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

    // WindowsStyleの変更
+   [DllImport("user32.dll", SetLastError = true)]
+   static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
+   [DllImport("user32.dll", SetLastError = true)]
+   static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);

    // WindowsTerminalのウィンドウハンドルのフィールド
    private IntPtr terminalHandle;

    // WindowStyleはポインタで決められているのでその定義
+   const int GWL_STYLE = -16;
+   const uint WS_SYSMENU = 0x00080000;
+   const uint WS_CAPTION = 0x00C00000;
+   const uint WS_MINIMIZEBOX = 0x00020000;
+   const uint WS_MAXIMIZEBOX = 0x00010000;

    // 依存関係プロパティ
    public static readonly DependencyProperty hWndProperty =
      DependencyProperty.Register(
        nameof(HWnd),
        typeof(long),
        typeof(TerminalPage),
        null
      );
    // プロパティラッパー
    public IntPtr HWnd
    {
        get { return new IntPtr((long)GetValue(hWndProperty)); }
        set { SetValue(hWndProperty, value.ToInt64()); }
    }

    private void EmbedWindowsTerminal()
    {
        // WindowsTerminalの起動
        var process = new Process();
        process.StartInfo.FileName = "wt.exe";
        process.StartInfo.UseShellExecute = false;
        process.Start();

        if(HWnd == IntPtr.Zero)
        {
            Console.WriteLine("hWnd=null");
            return;
        }
        // WindowTerminal側のウィンドウハンドルの取得
        while (terminalHandle == IntPtr.Zero)
        {
            terminalHandle = FindWindow("CASCADIA_HOSTING_WINDOW_CLASS", null);
        }

+        if (terminalHandle != IntPtr.Zero)
+        {
+            // 現在のWindowStyleを取得
+            uint style = (uint) GetWindowLong(terminalHandle, GWL_STYLE);
+            // 取得したWindowStyleに手を加える(拡大縮小ボタンや閉じるボタンを無効化するなど)
+            style &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
+            // 手を加えたStyleを入れなおす
+            SetWindowLong(terminalHandle, GWL_STYLE, style);
+            // アプリケーションウィンドウにWindowTerminalを埋め込む
+            SetParent(terminalHandle, HWnd);
+        }
    }
    // UIのロードイベントから埋め込み処理を呼び出す
    private void Page_Loaded(object sender, RoutedEventArgs e)
    {
+        EmbedWindowsTerminal();
    }
}

```
`Loadedイベント`をxamlのほうで設定するとこのページがロードされたら埋め込み処理が走るようになります。
```diff_xml
<Page
    x:Class="TerminalHositing.Views.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TerminalHositing.Views"
    xmlns:vm="using:TerminalHositing.ViewModels"
    xmlns:m="using:TerminalHositing.Models"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
+   Loaded="Page_Loaded">
```

<font color=red>※重大なことですが、WindowsTerminalでタイトルバーを非表示として設定していると`SetWindowLong()`が動作しません。かならず使用する前にWindowsTerminalの設定で「タイトルバーの非表示をしない」ようにしてください。<font>

これでWindowTerminalがWinUI3のパネルに埋め込まれました。
しかし、これでは好みの位置に配置をしたりできないので、その方法は後日解説したいと思います。