0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりで完走_C# is GODAdvent Calendar 2024

Day 8

WindowsTerminal をWinUI3のパネルに召喚

Last updated at Posted at 2024-12-07

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の処理はこちらです。

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部分です

<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部で記述できるプロパティを任意に設定したいとき、依存関係プロパティを作成して対応します。
今回はウィンドウハンドルを受け取るプロパティを作りたいので以下のように記述しました。

// これが依存関係プロパティ
// 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をそのまま入れ込むことに必要なのはプログラムから起動する処理が必要です。

var process = new Process();
process.StartInfo.FileName = "wt.exe";
process.StartInfo.UseShellExecute = false;
process.Start();

これでWindowsTerminalがプログラムから起動できました。

では次にこの起動したWindowsTerminalのウィンドウハンドルを取得します。
この時FindWindowという指定したアプリのWindowハンドルを取得するWin32APIを使っています。

// 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つのウィンドウハンドルを使いWin32APISetParent(),SetWindowLong,GetWindowLongにてWinUI3に埋め込んでいきます。

// 埋め込み
[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によってそれらを非表示にしていく形とのなります。

以下がその処理になります。
今まで記述したものもまとめております。

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のほうで設定するとこのページがロードされたら埋め込み処理が走るようになります。

<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">

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?