C# Windowsアプリ開発小ネタ集

C#でWindowsアプリを開発するときの小ネタ集。
## ウィンドウハンドルを取得する方法
Win32 APIでウィンドウを操作したりするときに必要になるのが、ウィンドウハンドルです。
自分のウィンドウなら、`Control.Handle`プロパティがあるので簡単です。
```csharp
    IntPtr hWnd = this.Handle;
```
自分で起動したプロセスのメインウィンドウは、`Process.MainWindowHandle`で取得できます。ただし、すぐにウィンドウが開くわけではないので、ウィンドウが開くまで待つ必要があります。Microsoftのドキュメントには`Process.WaitForInputIdle()`を使えとありますが、アプリの構造によってはうまく動作しません。
```csharp
Process proc = Process.Start( "hoge.exe" );
// proc.WaitForInputIdle(); // うまくいかない場合がある
while ( proc.MainWindowHandle == 0 ) { // こっちのほうが確実
    await Task.Delay( 10 );
}
IntPtr hWnd = proc.MainWindowHandle;
```
## TextBoxのキャレットを隠す方法(WinForms)
TextBoxをdisableにするとキャレットが消せるけど、文字が灰色になってしまいます。`HideCaret()`というWin32 APIを使えば、disableにせずともキャレットを消せますが、１回だけだとフォームがアクティブになったときまた復活するので、アクティブになるたびに消してやればOK。
```csharp
[LibraryImport( "user32.dll" )]
[return: MarshalAs( UnmanagedType.Bool )]
private static partial bool HideCaret( IntPtr hWnd );

public Form1() {
    InitializeComponent();
    this.Activated += Form1_Activated;
}

private void Form1_Activated( object? sender, EventArgs e ) {
    HideCaret( textBox1.Handle );
}
```
## マウスカーソルを消す方法(WinForms)
`Cursor.Hide()`と`Cursor.Show()`を使えば簡単なのですが、実はこれ、HideとShowの回数が合っていなければならない、すなわち、２回Hideしたら２回Showしなければならないという、厄介な仕様があります。この仕様が便利なこともあるのかもしれませんが、たいていは厄介なだけなので、一枚かませて簡略化しましょう。
```csharp
public class CursorControl {
    private static bool show = true;

    public static void Show() {
        if ( !show ) {
            Cursor.Show();
            show = true;
        }
    }

    public static void Hide() {
        if ( show ) {
            Cursor.Hide();
            show = false;
        }
    }
}
```
## DataGridViewのコンボボックスをワンクリックで開く方法(WinForms)
DataGridViewのコンボボックスは、２回クリックしないとドロップダウンが開かないという不自然な仕様になっています。これをワンクリックで開くようにするには、セルが選択されたときにドロップダウンを開くキー操作を送ってやります。
```csharp
dataGridView1.CellEnter += DataGridView1_CellEnter;

private void DataGridView1_CellEnter( object? sender, DataGridViewCellEventArgs e ) {
    if ( sender == null ) {
        return;
    }
    DataGridView dgv = (DataGridView)sender;
    if ( dgv.Columns[ e.ColumnIndex ].CellTemplate is DataGridViewComboBoxCell ) {
        SendKeys.Send( "{F4}" );
    }
}
```
この例は、セルテンプレートを用いる場合の方法です。対象のコンボボックスかどうか判定する条件(２個目のif文)は、DataGridViewの設定に依存します。
## コンソールアプリと対話する方法
ProcessStartInfoにSTDINとSTDOUTのリダイレクトを設定すれば簡単じゃね？と思うのですが、なぜか、StreamReaderの挙動がおかしくなります。具体的に言うと、STDOUTに何も出力されていない時、`StreamReader.Read()`や`StreamReader.Peek()`が永遠に戻ってこなくなります。それを回避するため、`StreamReader.ReadAsync()`で非同期に読み取って、StringBuilder上に文字列として構築するようにします。
STDIN(StreamWriter)のほうは普通に扱って問題ありません。
```csharp
private Process? proc;
private StreamReader? stdout;
private StreamWriter? stdin;

void launch() {
    var psi = new ProcessStartInfo( "hoge.exe" );
    psi.UseShellExecute = false;
    psi.RedirectStandardOutput = true;
    psi.RedirectStandardInput = true;
    proc = Process.Start( psi );
    stdout = proc?.StandardOutput;
    stdin = proc?.StandardInput;
}

private char[] stdoutBuffer = new char[ 1000 ];
private StringBuilder sbStdout = new StringBuilder();

private async Task readStdout() {
    if ( stdout != null ) {
        int count = await stdout.ReadAsync( stdoutBuffer, 0, stdoutBuffer.Length );
        sbStdout.Append( stdoutBuffer, 0, count );
    }
}
```
## モニターのリフレッシュレートを取得する方法
指定したウィンドウがあるモニターのリフレッシュレートを取得します。
まず、`MonitorFromWindow()`でウィンドウが存在するモニターを取得して、`GetMonitorInfoW()`でモニターのデバイス名を取得して、最後に`EnumDisplaySettingsW()`で現在の設定を取得します。
やることは簡単なのですが、Win32 API関係の定義が多くてとても長くなります。サンプルはgistに入れました。
https://gist.github.com/yamamaya/4a4c3051fa03a2425549f10ef11dafd5
## アプリにキー操作を送る方法
自分以外の特定のアプリにキー操作を送るには、`SendKeys.Send()`を使います。キー操作の記述方法は以下のページを参照してください。

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.sendkeys?view=windowsdesktop-7.0

`SendKeys.Send()`には、送り先を指定する方法がなく、その時点でアクティブなウィンドウに送られます。そのため、キー操作を送る前に、送り先のウィンドウをWin32 APIの`SetForegroundWindow()`でアクティブにてやります。
```csharp
[LibraryImport( "user32.dll" )]
[return: MarshalAs( UnmanagedType.Bool )]
private static partial bool SetForegroundWindow( IntPtr hWnd );

SetForegroundWindow( hWnd );
SendKeys.Send( "%({ENTER})" ); // Alt+Enter
```
`SetForegroundWindow()`と`SendKey.Send()`の間でユーザーが手動でウィンドウを切り替えたりした場合の動作は保証できません。Hook等を駆使して、ユーザーによる操作を全面的に禁止する必要があります。
## 操作を全面的に禁止する方法
Win32 APIのキーボードフックとマウスフックという仕掛けを使います。なにしろ、PCのすべての操作を奪ってしまうので、開発やデバッグがそれなりに面倒です。興味のある方はググってみてください。後で追記するかも？
## 実行中のファイルの情報を取得する方法
`Assembly.Get****Assembly()`というメソッドが何種類かあるけど、違いがよく判らなくなってます。昔は起動用のEXEファイルとか、アセンブリを格納したDLLとか区別されていたけど、今はDLLしか返さないような？実行中のファイル名やアセンブリのバージョンが取得できます。
```csharp
string? filename = Assembly.GetExecutingAssembly()?.Location;
Version? version = Assembly.GetExecutingAssembly()?.GetName().Version;
```
## Windowsアカウントの権限を取得する方法
```csharp
Thread.GetDomain().SetPrincipalPolicy( PrincipalPolicy.WindowsPrincipal );
var pri = (WindowsPrincipal)Thread.CurrentPrincipal;
bool isAdmin = pri.IsInRole( WindowsBuiltInRole.Administrator );
bool isGuest = pri.IsInRole( WindowsBuiltInRole.Guest );
```
## 管理者権限になる方法
`ProcessStartInfo.Verb`に`RunAs`を設定して自分自身を起動しなおします。もちろん他の実行ファイルにしてもOK。なお、通常、画面が暗転してUACのダイアログが出ます。
```csharp
static void Main( string[] args ) {
	if ( 力が欲しいか？ ) {
		var psi = new ProcessStartInfo();
		psi.WorkingDirectory = Environment.CurrentDirectory;
		psi.FileName = Assembly.GetEntryAssembly().Location;
		psi.Verb = "RunAs";
		psi.Arguments = string.Join( " ", args );
	    Process.Start( psi );
        return;
	}
}
```