CsWin32でC#からWin32APIを使って、ウインドウを列挙し、そいつに好きなWindowsメッセージを送る

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

好きなウインドウに、好きなWindowsメッセージを送りたいということがあった。
(WM_CLOSEとか、そういうやつ)

以前こちらの記事で、好きなウインドウにWM_CLOSEを送るということをしたので、 それの派生で、好きなMsgを送れるようにする、プラス、なんとなくWinUI3でやってみることにした。

tera1707.com

C#でwin32APIを使うためにCsWin32を使った

Microsoft.Windows.CsWin32というMS製ライブラリをつかった。

こちらは、ソースジェネレーターという仕組みで、プロジェクトに手動で追加した「NativeMethods.txt」というファイルに書いたWin32APIに対応するP/Invokeのためのコードを自動で書いてくれるというパッケージ。

それを使ってやってみる。

コード置き場

https://github.com/tera1707/WindowsMessageSenderWinui

コード中身

現時点のコード

<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WindowMessageSender.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WindowMessageSender"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>

            <Button Grid.Column="0" x:Name="myButton" Grid.Row="0" Click="myButton_Click" HorizontalAlignment="Stretch" Margin="5" Background="Blue">ウインドウを探す</Button>
            <ToggleSwitch Grid.Column="1" x:Name="VisibleOnly" IsOn="False" OffContent="可視ウインドウのみ表示" OnContent="不可視ウインドウも表示"/>
        </Grid>        

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>

            <HyperlinkButton Content="WMメッセージ番号を調べる" NavigateUri="https://blog.goo.ne.jp/masaki_goo_2006/e/65fe9047e5f97bde1830566766b4829e"/>
            <TextBlock Grid.Column="1" Text="0x" VerticalAlignment="Center" FontSize="25"/>
            <TextBox Grid.Column="2" x:Name="tbMsgNumber" TextAlignment="Left" Text="0010" FontSize="30"/>
            <Button Grid.Column="3" Content="メッセージ送信" Click="Button_Click" Background="DarkCyan" HorizontalAlignment="Stretch"/>
        </Grid>

        <ListView x:Name="WindowList" Grid.Row="2" HorizontalAlignment="Stretch" SelectionMode="Single" ShowsScrollingPlaceholders="True" BorderThickness="1" BorderBrush="{ThemeResource ControlStrongStrokeColorDefaultBrush}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid BorderBrush="CadetBlue" BorderThickness="1" Margin="1" Padding="7,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>

                        <Grid.ContextFlyout>
                            <MenuFlyout>
                                <MenuFlyoutItem Text="送信する" Click="MenuFlyoutItem_Click"/>
                            </MenuFlyout>
                        </Grid.ContextFlyout>

                        <TextBlock Grid.Row="0" Text="{Binding Title}"/>
                        <TextBlock Grid.Row="1" Text="{Binding ClassName}"/>

                    </Grid>
                </DataTemplate>

            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>
using Microsoft.UI.Xaml;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.Win32;
using Windows.Win32.Foundation;

namespace WindowMessageSender
{
    public sealed partial class MainWindow : Window, INotifyPropertyChanged
    {

        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string propertyName)
            => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        internal ObservableCollection<WindowData> WindowTitle
        {
            get { return _windowTitle; }
            set { _windowTitle = value; OnPropertyChanged(nameof(WindowTitle)); }
        }
        private ObservableCollection<WindowData> _windowTitle = new();

        public MainWindow()
        {
            this.InitializeComponent();

            // WinUI3のウインドウのハンドルを取得
            var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            // そのウインドウのIDを取得
            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);
            // そこからAppWindowを取得する
            Microsoft.UI.Windowing.AppWindow appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);

            appWindow.Resize(new SizeInt32(700, 900));
        }

        private void myButton_Click(object sender, RoutedEventArgs e)
        {
            WindowTitle.Clear();

            string windowName;
            string windoClass;

            PInvoke.EnumWindows(((hWnd, param) =>
            {
                int bufferSize = PInvoke.GetWindowTextLength(hWnd) + 1;
                unsafe
                {
                    var isVisible = PInvoke.IsWindowVisible(hWnd);

                    fixed (char* windowNameChars = new char[bufferSize])
                    {
                        if (PInvoke.GetWindowText(hWnd, windowNameChars, bufferSize) == 0)
                        {
                            int errorCode = Marshal.GetLastWin32Error();
                            if (errorCode != 0)
                                throw new Win32Exception(errorCode);

                            return true;
                        }

                        windowName = new string(windowNameChars);
                    }

                    fixed (char* windoClassChars = new char[256])
                    {
                        if (PInvoke.GetClassName(hWnd, windoClassChars, 256) == 0)
                        {
                            int errorCode = Marshal.GetLastWin32Error();
                            if (errorCode != 0)
                                throw new Win32Exception(errorCode);

                            return true;
                        }

                        windoClass = new string(windoClassChars);
                    }

                    if (VisibleOnly.IsOn)
                    {
                        WindowTitle.Add(new WindowData(hWnd, windowName, windoClass, isVisible));
                    }
                    else if (isVisible == true)
                    {
                        WindowTitle.Add(new WindowData(hWnd, windowName, windoClass, isVisible));
                    }

                    return true;
                }
            }), (LPARAM)0);

            WindowList.ItemsSource = WindowTitle;
        }

        private void MenuFlyoutItem_Click(object sender, RoutedEventArgs e)
        {
            Button_Click(null, null);
        }

        // Msg送信ボタン押下
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (WindowList.SelectedItem is WindowData wd)
            {
                var msg = UInt32.Parse(tbMsgNumber.Text, System.Globalization.NumberStyles.HexNumber);
                PInvoke.PostMessage(wd.hWnd, msg, (WPARAM)0, (LPARAM)0);
            }
        }
    }

    internal class WindowData
    {
        public HWND hWnd {  get; set; }
        public string Title { get; set; }
        public string ClassName { get; set; }
        public bool IsVisible { get; set; }

        public WindowData(HWND hwnd, string title, string className, bool isVisible)
        {
            hWnd = hwnd;
            Title = title;
            ClassName = className;
            IsVisible = isVisible;
        }
    }
}

コードと同じ階層に作っておいておくNativeMethods.txt

EnumWindows
GetWindowText
GetWindowTextLength
GetClassName
IsWindowVisible
PostMessage

PWSTRを使いたくない

こちらのページを参考にさせて頂いてやってみたのだが、 そのページに書かれている通り、

[DllImport("USER32.dll", ExactSpelling = true, EntryPoint = "GetWindowTextW", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[SupportedOSPlatform("windows5.0")]
internal static extern int GetWindowText(winmdroot.Foundation.HWND hWnd, winmdroot.Foundation.PWSTR lpString, int nMaxCount);

という感じで、PWSTRという、ポインタを含む構造体が出てくる。 これを受けるためには、

 fixed (char* windowNameChars = new char[bufferSize])
 {
     PInvoke.GetWindowText(hWnd, windowNameChars, bufferSize);
     windowName = new string(windowNameChars);
 }

みたいな、charの配列へのポインタを含むコードをかかないといけなくて 、非常にうっとうしい。

ので、上のページに、spanを使うやり方が書かれていた。

それを参考に、コードを書きなおしてみる。

spanで書き直す

書き直し中。。。。

参考

microsoft/CsWin32 リポジトリ

https://github.com/microsoft/CsWin32

CsWin32 で Win32 API や COM を使ったアプリケーション開発を効率化する →Win32から文字列をC#でうけるときのうまいやり方が書かれている。

https://blog.shibayan.jp/entry/20220501/1651339430

[C#] CsWin32でWin32APIのプラットフォーム呼び出し(P/Invoke)コードを自動生成

https://qiita.com/radian-jp/items/a4509f9a44101fb2f30e

Getting the Value from PWSTR
PWSTR を使って文字列を取る方法

https://github.com/microsoft/CsWin32/discussions/181#discussioncomment-456365