Windowsアプリを最前面表示にして起動するPowerShellスクリプトをささっと作る

<font color=gray>普通の人は PowerToys の Always On Top を使いましょう（小声）</font>

# 0. はじめに

以前，HTA（HTML Application）でWindowsのデスクトップに表示するデジタル時計を作ったのだが，他のウィンドウに隠れてしまって時計が見えなくなるという弱点があった。

[Windowsのデスクトップに表示するデジタル時計をHTAでささっと作る - Qiita](https://qiita.com/TETSURO1999/items/5f8342f69c670b29aaa1)

ちなみにこんな時計だ。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490655/77e679e0-2a15-bd68-a044-5b76e3dd44be.png">

ウィンドウを最前面表示にするためには HTA の機能内では実現困難であり，Win32 API の SetWindowPos を呼び出す必要がある。下記の HTA はウィンドウの最前面表示を実現しているが，結局のところ COM オブジェクト経由で Excel を用いて Win32API をコールしているのだ。

[HTA で Memo アプリを作りました。- github](https://github.com/Neos21/hta-memo)

筆者は Microsoft Office の永続ライセンスを持っているので Excel 必須でも問題ないが，なんとなく Windows にデフォルトでインストールされている機能以外を使ったら負け！な気がして採用を躊躇していた。

だがしかし，PowerShell ならば Win32 API を呼び出せることに気づいた。PowerShell は C# のコードを実行可能であり，C# 経由で Win32 API を呼び出すというアクロバティックなテクニックだが，先人がいて助かった。というか，下記の記事でサンプルとして例示している Win32 API から判断して，ほぼ本記事と同じことにチャレンジしていたのではないかと推察される。

[powershellでWin32 APIを利用する（powershell 2.0以上）- Qiita](https://qiita.com/y-takano/items/cb752ad6a10e550ec92f)

# 1. 実装コード

当初，HTA の中から自分自身を最前面表示にする PowerShell スクリプトを呼び出すことを考えたが，汎用性を考えて逆に PowerShell スクリプト側から HTA を起動して最前面表示にするようにした。こうすると HTA 以外のあらゆる Windows アプリを最前面表示可能になる。そういう訳で本記事も当初は「HTA を最前面表示にして・・・」というタイトルだったのだが，内容に合わせて現在のタイトル「Windows アプリを最前面表示にして・・・」に変更した。

```powershell:TopMostLauncher.ps1
#-------------------------------------------------------------------------------
# 定数設定
#-------------------------------------------------------------------------------
$timeout  = 3000	# [ms] ウィンドウハンドルを確認するタイムアウト期間
$interval = 100		# [ms] ウィンドウハンドルを確認するインターバル時間
#-------------------------------------------------------------------------------
# Win32 API の定義
# Special thanks to: https://qiita.com/y-takano/items/cb752ad6a10e550ec92f
#-------------------------------------------------------------------------------
$Win32 = &{
$cscode = @"
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(
    string lpClassName,
    string lpWindowName
);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(
    IntPtr hWnd,
    IntPtr hWndInsertAfter,
    int    x,
    int    y,
    int    cx,
    int    cy,
    uint   uFlags
);
public static IntPtr FindWindowByName( string lpWindowName ) {
    return FindWindow( null, lpWindowName );
}
"@
    return (Add-Type -MemberDefinition $cscode -Name "Win32ApiFunctions" -PassThru)
}
#-------------------------------------------------------------------------------
# ヘルプメッセージ
#-------------------------------------------------------------------------------
if( $Args.Length -lt 2 ) {
    Write-Host "アプリを最前面にして起動します。"
    Write-Host ""
    Write-Host "TopMostLauncher(.ps1) [タイトル文字列] [コマンド名] ..."
    Exit -1
}
#-------------------------------------------------------------------------------
# コマンドの起動
#-------------------------------------------------------------------------------
$title = $Args[0]
$command = $Args[1]
if( $Args.Length -ge 3 ) {
    Start-Process -FilePath $command -ArgumentList $Args[2..-1]
} else {
    Start-Process -FilePath $command
}
#-------------------------------------------------------------------------------
# 指定したタイトル文字列を持つアプリケーションを検索して最前面表示にする
# タイムアウト期間 $timeout 内に見つからなかった場合はエラーとする
#-------------------------------------------------------------------------------
for( $t = 0; $t -le $timeout; $t += $interval ) {
    Start-Sleep -MilliSeconds $interval
    $hwnd = $Win32::FindWindowByName( $title )
    if( $hwnd -ne 0 ) {
        if( $Win32::SetWindowPos( $hwnd, -1, 0, 0, 0, 0, 3 ) ) {
            Exit 0
        } else {
            Write-Host "コマンド $command の最前面表示に失敗しました"
            Exit -1
        }
   }
}
Write-Host "タイトル文字列 $title のアプリケーションが見つかりません"
Exit -1
```

# 2. 呼び出し例

通常の Windows アプリ，たとえばメモ帳を起動する場合は下記のようになる。

```:呼び出し例その１（正常動作時）
.\TopMostLauncher.ps1 "無題 - メモ帳" notepad.exe
```

HTA を呼び出す場合は下記のようになる。

```:呼び出し例その２（正常動作時）
.\TopMostLauncher.ps1 CLOCK mshta.exe C:\Tools\hta\DigitalClock.HTA
```

タイトル文字列を間違えた場合，下記のようにエラーメッセージが表示される。ただし，アプリケーション自体は立ち上がってしまう。

```:呼び出し例その３（タイトル文字列を誤った場合）
.\TopMostLauncher.ps1 rock mshta.exe C:\tools\hta\DigitalClock.HTA
タイトル文字列 rock のアプリケーションが見つかりません
```

# 3. 今後の課題

- アプリケーションが PowerShell 経由で呼び出されるので，起動時に一瞬だけ PowerShell のコンソールウィンドウが開いて閉じるのが少しカッコ悪い。コンソールウィンドウを隠すこと自体は技術的にさほど難しくはないが，その場合，エラーメッセージをコンソールに出力しても見えないので MessageBox 等で表示する必要がある。

- タイトル文字列の指定を省略できないだろうか？そもそも起動するプロセス ID は分かっているのだから，プロセス ID からウィンドウハンドルを直接取得できないだろうか？
  ⇒おそらく困難。一つのプロセスで複数のウィンドウを持つことができるので。

- 最前面表示で起動した後，その設定を解除することができない。
  ⇒最前面表示を頻繁にオン／オフしたい人は本記事の最初の行を読んで欲しい。

- PowerToys の Always On Top のように最前面表示したアプリケーションのウィンドウ枠を太くして目立たせたいが，ちょっと大変そうだ。ちなみに PowerToys の Always On Top では DWM（Desktop Window Manager）の機能を使っている。

# 4. 参考文献

- [powershellでWin32 APIを利用する（powershell 2.0以上）- Qiita](https://qiita.com/y-takano/items/cb752ad6a10e550ec92f)
  MessageBox を除き，掲載コードをほぼそのまま流用させて頂いた。
- [HTA で Memo アプリを作りました。- github](https://github.com/Neos21/hta-memo)
  Execute.hta のソースコードを見る限り，[Yahoo!知恵袋](http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q12151482357) の回答を参考にしているようだ。もっともその回答も Vector の[TopMostWindow.HTA](https://www.vector.co.jp/soft/winnt/util/se429282.html) から引用しているように思える。

- [microsoft/PowerToys - github](https://github.com/microsoft/PowerToys)
  src\modules\alwaysontop\AlwaysOnTop\AlwaysOnTop.cpp あたりのソースコードが参考になるだろう。