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つのウィンドウハンドルを使いWin32API
のSetParent()
,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のパネルに埋め込まれました。
しかし、これでは好みの位置に配置をしたりできないので、その方法は後日解説したいと思います。