1
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 9

WindowsTerminalをWinUI3で操作する

Last updated at Posted at 2024-12-08

WindowsTerminalをパネルと追従させる

前回、WindowsTerminalをWinUIの画面上に召喚することができました。
しかし、普通のコントロールのように指定の場所を指定してそこに配置するような機能や、ウィンドウのサイズの変更に追従するような設計にはできていませんでした。
今回は、前回作ったTerminalPageをUIパネルとして配置やウィンドウサイズの変更に追従するような処理を追加していきます。

コントロールのサイズ取得とウィンドウのサイズ変更

パネルに追従させるためは、WindowsTerminalのウィンドウの位置を変更することが必要です。
そこでDll ImportSetWindowPos関数が必要になります。
以下のように追加します。

// DllImport宣言
[DllImport("user32.dll")]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

// ウィンドウのサイズを追従させるための処理
private void PanelSizeChange()
{
    // アプリの基準となるウィンドウを取得
    var xamlRoot = this.XamlRoot;
    if(xamlRoot != null)
    {
        // DPIスケール因子を取得
        double dpiRatio = xamlRoot.RasterizationScale;
        
        // 現在のコントロール(TerminalPage)の横幅と高さを取得する
        var width = (int) (this.ActualWidth * dpiRatio);
        var height = (int) (this.ActualHeight * dpiRatio);
        
        // アプリの基準となるウィンドウからこのコントロールまでのオフセットを取得
        Point position = this.TransformToVisual(xamlRoot.Content).TransformPoint(new Point(0, 0));
        var posX = (int) (position.X * dpiRatio);
        var posY = (int) (position.Y * dpiRatio);

        // Dll ImportしたSetWindowPosでWindowsTerminalのウィンドウの位置、幅を変える
        SetWindowPos(terminalHandle, IntPtr.Zero, posX, posY, width, height, 0);
    }            
}

以前から取得したWindowsTerminalのウィンドウハンドルに対して、ウィンドウの位置やサイズを変更しています。
DPIスケールを取得している理由として、モニターごとに設定している差異を吸収しないと表示がずれてしまったりするからです。
WinUI3ではこれをはじめから考慮して作られているのですが、追加したWindowTerminalはそうではないので、この値をかけることにより、正常な値を取得できるようになっています
TransfromToVisualはアプリの基準となるウィンドウからこのコントロールまでのオフセット(Transform)を取得していて、続くTransformPointでTransform型からPointにキャストしてます。

サイズ変更処理を呼び出す。

先ほどの処理を呼び出します。
呼び出すポイントとして、WindowsTerminalがこのコントロールに描画されたときと、このコントロールがサイズ変更を検知した場合になりますので、以下のようにします。

// 前回作成したWindowsTerminal埋め込み処理の最後に呼び出す
private void EmbedWindowsTerminal()
{
    if (terminalHandle != IntPtr.Zero)
    {
        uint style = (uint) GetWindowLong(terminalHandle, GWL_STYLE);
        style &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
        SetWindowLong(terminalHandle, GWL_STYLE, style);
        SetParent(terminalHandle, HWnd);
+       PanelSizeChange();
    }
}

// 初期化の際、下のイベントを登録
public TerminalPage()
{
    this.InitializeComponent();
+   this.SizeChanged += TermianlSizeChanged;
}
// サイズチェンジドイベントが発行されたとき呼び出す
+ private void TermianlSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+    if (sender is TerminalPage element)
+    {
+        PanelSizeChange();
+    }
+ }

これでWindowTerminalがWinUI3の標準コントロールのような振る舞いをすることが可能になりました。

UIからの書き込み

WinUI3のUIイベントなどから、WindowsTerminalに書き込む方法を紹介します。

例えば、ツリービューで選択したパスをそのままコピーしてTerminalに書き込むなどのことができるようになります。
いままでわざわざエクスプローラーを開いてーとかやっていましたがそんなことしなくてもよくなるのです。

WindowsTerminalに処理を書き込む場合、Win32APISendInputSetForegroundWindowを使います。
この2つを使う理由として、与えられた文字列をそのまま書き込む場合、WindowsTerminalウィンドウをアクティブにしてそのままCtrl+Vを使って書き込んだ方が簡単だからです。

[DllImport("user32.dll", SetLastError = true)]
static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);


// 以下入力処理に必要な構造体と定数
[StructLayout(LayoutKind.Sequential)]
struct INPUT
{
    public uint type;
    public InputUnion u;
    public static int Size => Marshal.SizeOf(typeof(INPUT));
}

[StructLayout(LayoutKind.Explicit)]
struct InputUnion
{
    [FieldOffset(0)] public MOUSEINPUT mi;
    [FieldOffset(0)] public KEYBDINPUT ki;
    [FieldOffset(0)] public HARDWAREINPUT hi;
}

[StructLayout(LayoutKind.Sequential)]
struct MOUSEINPUT
{
    public int dx;
    public int dy;
    public uint mouseData;
    public uint dwFlags;
    public uint time;
    public IntPtr dwExtraInfo;
}

[StructLayout(LayoutKind.Sequential)]
struct KEYBDINPUT
{
    public ushort wVk;
    public ushort wScan;
    public uint dwFlags;
    public uint time;
    public IntPtr dwExtraInfo;
}

[StructLayout(LayoutKind.Sequential)]
struct HARDWAREINPUT
{
    public uint uMsg;
    public ushort wParamL;
    public ushort wParamH;
}

const uint INPUT_KEYBOARD = 1;
const uint KEYEVENTF_KEYUP = 0x0002;

入力のための構造体が少し長いですがこれがないと入力処理ができないので、必ず書いてください

それでは処理を書いていきます。

// なにかボタンをクリックしたときこの処理を呼び出す
// 引数には書き込む文字列を与える
 public void SetText(string pastString)
 {
     // 最初にWindowsTerminalウィンドウをアクティブにする
     SetForegroundWindow(terminalHandle);
     // 与えられた文字列を一度クリップボードにコピー
     var pack = new DataPackage();
     pack.SetText(pastString);
     Clipboard.SetContent(pack);
     //Ctrl+Vを行い、クリップボードの情報をそのままWindowsTerminalウィンドウに貼り付ける
     PasteFromClipboard();
 }

 static void PasteFromClipboard()
 {
     // 以下のように設定することでCtrl+VがWindowTerminalウィンドウ上で実行され、文字列が貼り付けられる
     INPUT[] inputs = new INPUT[4];

     // Ctrlキーの押下
     inputs[0].type = INPUT_KEYBOARD;
     inputs[0].u.ki.wVk = (ushort) VirtualKey.Control;
     inputs[0].u.ki.dwFlags = 0;

     // Vキーの押下
     inputs[1].type = INPUT_KEYBOARD;
     inputs[1].u.ki.wVk = (ushort) VirtualKey.V;
     inputs[1].u.ki.dwFlags = 0;

     // Vキーの解放
     inputs[2].type = INPUT_KEYBOARD;
     inputs[2].u.ki.wVk = (ushort) VirtualKey.V;
     inputs[2].u.ki.dwFlags = KEYEVENTF_KEYUP;

     // Ctrlキーの解放
     inputs[3].type = INPUT_KEYBOARD;
     inputs[3].u.ki.wVk = (ushort) VirtualKey.Control;
     inputs[3].u.ki.dwFlags = KEYEVENTF_KEYUP;

     // 現在アクティブなウィンドウ(WindowsTerminalウィンドウ)に入力情報を送る
     SendInput((uint) inputs.Length, inputs, INPUT.Size);

     Thread.Sleep(100);  // 適度な遅延を挿入(貼り付けの時間を確保)
 }

これでWindowsTerminalウィンドウに情報が書き込まれるようになります。

それではMainWindowでこの処理を呼び出していきます。

<?xml version="1.0" encoding="utf-8"?>
<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">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="3*"/>
        </Grid.ColumnDefinitions>
        <StackPanel Grid.Row="0">
            <TextBox x:Name="myArea" Text=""/>
            <Button Content="SetPosition" Click="Set_Text"/>
        </StackPanel>
        <Border Grid.Column="1">
            <view:TerminalPage x:Name="terminalPanel" HWnd="{x:Bind myWhnd}"/>            
        </Border>
    </Grid>    
</Window>

左半分にテキストエリアとボタンを配置し、右側にWindowsTerminalを配置しています。
テキストエリアに書かれた文字をボタンを押して書き込むという設計です。
ボタンクリックで以下の処理が呼び出されるように設定してます。

private void Set_Text(object sender, RoutedEventArgs e)
{
    terminalPanel.SetText(myArea.Text);
}

x:Nameで指定されたterminalPanelSetText()を呼び出し、引数にはmyAreaのテキスト情報を与えています。

これでボタンを押したら、テキストエリアに書き込まれた内容が、ターミナルに書き込まれるようになります。

1
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
1
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?