実用的なスクリプトを書いていこう
前回はウインドウのサイズを変更するPowerShellスクリプトとして、次のスクリプトを紹介した。
今回からは、このスクリプトの中身を読み込みながら、もう少し汎用的に利用できるスクリプトを作っていく。
スクリプト内部の動作内容を整理して理解する
先ほどのスクリプトはそれほど長くないので、仕組みを把握するのは簡単だ。基本的に次のロジックで動作している。
- プロセス一覧から、目的とするウインドウのハンドラを取得する
- 対象ウィンドウの配置場所とサイズを取得する
- 新しい配置場所をサイズを計算する
- 計算した場所とサイズをウインドウへ反映させる
ウインドウの配置場所やサイズを取得/変更する機能は、PowerShellには用意されていない。それらは「Windows API」と呼ばれる、Windowsが提供する基本機能だ。user32.dllに含まれており、これを呼び出すことで処理を実現している。
今回は、プロセス一覧から目的とするプロセスを選択するところを深堀りしていく。
標的となるウインドウのプロセスを特定する
PowerShellではGet-Processコマンドレットを使うことでシステムで動作しているプロセスの一覧を取得することができる。次のようなイメージだ。
PS C:\Users\daichi\Documents\powershell> (Get-Process)[0..20]
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
24 19.63 34.20 0.39 13100 1 ApplicationFrameHost
18 54.03 53.71 55.31 7036 0 audiodg
50 39.83 7.02 2.41 11764 1 ColorPickerUI
7 6.10 4.54 0.00 4868 0 conhost
11 6.61 7.88 0.00 6584 0 conhost
28 2.31 4.56 0.00 868 0 csrss
34 4.68 5.35 0.00 988 1 csrss
30 18.02 58.32 12.81 7732 1 ctfmon
19 5.33 13.87 0.00 5088 0 dasHost
23 4.90 12.46 0.05 12312 1 dllhost
16 3.01 10.50 0.00 12764 0 dllhost
7 1.45 5.06 14.86 7000 1 dptf_helper
76 426.06 399.57 0.00 1560 1 dwm
7 1.91 5.62 0.00 4080 0 esif_uf
97 134.27 206.27 11.17 7912 1 explorer
6 1.91 2.86 0.00 1124 0 fontdrvhost
8 3.81 6.52 0.00 1484 1 fontdrvhost
48 82.27 1.53 0.98 7012 1 HxOutlook
37 15.86 1.80 1.78 5644 1 HxTsr
0 0.06 0.01 0.00 0 0 Idle
11 2.20 9.45 0.00 3016 0 igfxCUIServiceN
PS C:\Users\daichi\Documents\powershell>
1つのアプリケーションが1つのプロセスに対応するとわかりやすいのだが、最近のアプリケーションは複数のプロセスで動作していることも多い。特に、Webブラウザなどはその傾向が顕著だ。マルチコアが進むにつれて、こういったマルチプロセスで構成されたアプリケーションはさらに増えるだろう。
例えば、Microsoft Edgeは「msedge」というのがプロセス名であり、このプロセス名で動作しているプロセスを探すと次のように複数のプロセスが見つかる。
Microsoft Edgeを構成する複数のプロセス
PS C:\Users\daichi\Documents\powershell> Get-Process -Name msedge
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
17 22.69 62.42 0.62 512 1 msedge
16 15.21 44.51 0.08 800 1 msedge
13 8.02 19.02 0.55 1776 1 msedge
17 21.00 57.57 1.05 2712 1 msedge
17 7.02 21.29 3.84 3968 1 msedge
15 14.69 43.30 0.06 5176 1 msedge
18 27.10 69.38 0.62 7136 1 msedge
94 67.54 158.99 41.73 8076 1 msedge
24 122.97 194.22 19.06 9016 1 msedge
15 15.16 43.03 0.11 9172 1 msedge
16 15.48 44.97 0.12 10260 1 msedge
15 15.08 44.47 0.08 10320 1 msedge
38 401.62 413.95 458.94 10808 1 msedge
14 12.32 27.27 0.06 12200 1 msedge
148 22.19 46.27 12.42 12480 1 msedge
9 2.01 7.09 0.03 12512 1 msedge
16 17.20 45.48 0.11 14128 1 msedge
16 17.83 45.79 0.14 14952 1 msedge
19 63.54 104.03 7.12 15200 1 msedge
16 15.86 46.26 0.09 15220 1 msedge
16 16.89 44.88 0.17 15332 1 msedge
PS C:\Users\daichi\Documents\powershell>
ここからウインドウを構成するプロセスを特定し、そのウインドウを操作するためのハンドラを取得しなければならない。
どうやって絞り込むかと言うと、プロセスに関する情報のなかから、それがウインドウを司るプロセスかどうかを推測するのだ。Get-ProcessはProcessオブジェクトの集まりを返す。Processオブジェクトについては、次のページに情報がまとまっている。
Processオブジェクトで、プロセスの特定に使えそうなプロパティを抜き出すと、次のようになる。
プロパティ | 内容 |
---|---|
Handle | プロセスのネイティブハンドル |
Id | プロセスID |
MainModule | プロセスのメインモジュール |
MainWindowHandle | プロセスのメインウインドウで使われるウインドウハンドル |
MainWindowTitle | プロセスのメインウインドウキャプション |
Modules | プロセスに読み込まれたモジュール一覧 |
ProcessName | プロセス名 |
SessionId | プロセスのターミナルセッションID |
Threads | プロセスで実行されているスレッドセット |
まず、プロセス名、プロセスID、ウインドウタイトル辺りでプロセスの情報を表示させてみよう。
PS C:\Users\daichi\Documents\powershell> Get-Process -Name msedge | % { $_.Name + " " + $_.Id + "^I" + $_.MainWindowTitle }
msedge 1776
msedge 3968
msedge 4444
msedge 6684
msedge 7552
msedge 8076 【連載】PowerShell Core入門 - 基本コマンドの使い方 [156] Windows 11開発版配信、Windows Terminalの同梱を確認|サーバ/ストレージ|IT製品の事例・解説記事 - 個人 - Microsoft Edge
msedge 8776
msedge 9016
msedge 9340
msedge 10808
msedge 12480
msedge 12512
msedge 13060
msedge 15200
PS C:\Users\daichi\Documents\powershell>
なお、上記結果を得たときは、次のようにMicrosoft Edgeでマイナビのページを表示させていた。
つまり、MainWindowTitleプロパティで実際にウインドウのタイトルが取得できていることがわかる。これで絞り込みができそうだ。
ちなみに、上記Microsoft Edgeではなく、別ウインドウを開いて「新規タブ」のページにしておき、そちらにフォーカスを当てた後で先ほどと同じ処理を実行すると、今度は次のような出力が得られる。
PS C:\Users\daichi\Documents\powershell> Get-Process -Name msedge | % { $_.Name + " " + $_.Id + "^I" + $_.MainWindowTitle }
msedge 1776
msedge 3968
msedge 4444
msedge 6684
msedge 7552
msedge 8076 新しいタブ - 個人 - Microsoft Edge
msedge 8776
msedge 9016
msedge 9340
msedge 10808
msedge 12480
msedge 12512
msedge 13060
msedge 15200
PS C:\Users\daichi\Documents\powershell>
Microsoft Edgeの場合、直前までアクティブだったウインドウ(タブ)のタイトルがMainWindowTitleプロパティで取得できるようだ。
プロセスを特定する識別子がプロセスIDだが、ウインドウについてはプロセスIDではなくウインドウハンドラという別のオブジェクトを経由して動作する必要がある。ウインドウ用のユニークIDのようなものだ。これも出力するように書き換えて実行してみると、次のようになる。
PS C:\Users\daichi\Documents\powershell> Get-Process -Name msedge | % { $_.Name + " " + $_.Id + "^I" + $_.MainWindowHandle + " " + $_.MainWindowTitle }
msedge 1776 0
msedge 3968 0
msedge 6684 0
msedge 7552 0
msedge 7924 0
msedge 8076 526020 新しいタブ - 個人 - Microsoft Edge
msedge 8776 0
msedge 9016 0
msedge 9340 0
msedge 10808 0
msedge 12480 0
msedge 12512 0
msedge 13060 0
msedge 14916 0
msedge 15200 0
PS C:\Users\daichi\Documents\powershell>
ウインドウタイトルが存在していないプロセスは、ウインドウハンドラの値が0になっている。このハンドラの値を使えば、ウインドウに関連してないプロセスとして排除することができそうだ。
それでは、Get-Processで取得したプロセス全体に対して、MainWindowHandleが0以外のものに関して情報を取得するような処理を実行してみよう。次のような結果が得られる。これは「ウインドウが存在すると見られるプロセスの一覧」ということになる。
PS C:\Users\daichi\Documents\powershell> Get-Process | ? { $_.MainWindowHandle -ne 0 } | % { $_.Name + "^I" + $_.Id
+ "^I" + $_.MainWindowHandle + "^I" + $_.MainWindowTitle }
ApplicationFrameHost 13100 198140 受信トレイ - Hotmail - メール
explorer 7912 131344
HxOutlook 7012 132626 メール
msedge 8076 328554 【連載】PowerShell Core入門 - 基本コマンドの使い方 [156] Windows 11開発版配信、Windows Terminalの同梱を確認|サーバ/ストレージ|IT製品の事例・解説記事 - 個人 - Microsoft Edge
PowerToys.Settings 13080 132788 PowerToys の設定
svchost 7808 1442696
TextInputHost 13676 131814 Microsoft Text Input Application
WindowsTerminal 2208 524840 PowerShell
WindowsTerminal 3788 394856 PowerShell
WindowsTerminal 4196 788048 PowerShell
WindowsTerminal 15116 131784 PowerShell
WinStore.App 10592 197900 Microsoft Store
PS C:\Users\daichi\Documents\powershell>
これまでの出力を整理すると、次の順番で絞り込みを行えば、このスクリプトが欲するプロセスを特定することができそうだということがわかる。
- プロセス名
- MainWindowHandleプロパティ値が0以外
- MainWindowTitleが目的とする名前になっている
なお、先ほどから何気なく使っている「% {処理}」という表記だが、これは「ForEach-Object -Process {処理}」と同じだ。「ForEach-Object」と書くと煩雑なので「%」で書いている。同じように「? {条件}」は「Where-Object {条件}」と同じだ。「Where-Object」と書くと煩雑なので「?」で記述している。
スクリプトへ落とし込む
今回調べた内容をシェルスクリプトへ落とし込んでみよう。ウインドウサイズを変更したいプロセスを特定して、その情報を出力するという内容を実装したのが次の「resize.ps1 ver 1」だ。
#!/usr/bin/env pwsh
#====================================================================
# アプリケーション
#====================================================================
$processName = "プロセス名"
$windowTitle = "ウインドウタイトル"
#====================================================================
# Main
#====================================================================
Get-Process -Name $processName |
? { $_.MainWindowHandle -ne 0 } |
? { $_.MainWindowTitle -match $windowTitle } |
% {
$_.Name + "^I" + $_.Id + "^I" + $_.MainWindowHandle + "^I" + $_.MainWindowTitle
}
このスクリプトを実際に使えるものにちょっと書き換えてみよう。Microsoft Edgeに対して適用するなら次のような感じになる(resize-msedge.ps1)。
#!/usr/bin/env pwsh
#====================================================================
# アプリケーション
#====================================================================
$processName = "msedge"
$windowTitle = "^.* Edge$"
#====================================================================
# Main
#====================================================================
Get-Process -Name $processName |
? { $_.MainWindowHandle -ne 0 } |
? { $_.MainWindowTitle -match $windowTitle } |
% {
$_.Name + " " + $_.Id + " " + $_.MainWindowHandle + " " + $_.MainWindowTitle
}
設定アプリケーションなら次のようになる(resize-settings.ps1)。
#!/usr/bin/env pwsh
#====================================================================
# アプリケーション
#====================================================================
$processName = "ApplicationFrameHost"
$windowTitle = "設定"
#====================================================================
# Main
#====================================================================
Get-Process -Name $processName |
? { $_.MainWindowHandle -ne 0 } |
? { $_.MainWindowTitle -match $windowTitle } |
% {
$_.Name + " " + $_.Id + " " + $_.MainWindowHandle + " " + $_.MainWindowTitle
}
特筆するところはないのだが、MainWindowTitleとの比較を行う段階で正規表現による比較をしている辺りがちょっとしたポイントだろうか。ここを正規表現で比較するようにしておくと、結構応用範囲が広くなり、実用的なスクリプトになりやすいのだ。
実際にこのスクリプトを実行すると次のようになる。
◆resize-msedge.ps1の実行サンプル
PS C:\Users\daichi\Documents\powershell\20210715\sources> .\resize-msedge.ps1
msedge 8076 328554 【連載】PowerShell Core入門 - 基本コマンドの使い方 [156] Windows 11開発版配信、Windows Terminalの同梱を確認|サーバ/ストレージ|IT製品の事例・解説記事 - 個人 - Microsoft Edge
PS C:\Users\daichi\Documents\powershell\20210715\sources>
◆resize-settings.ps1の実行サンプル
PS C:\Users\daichi\Documents\powershell\20210715\sources> .\resize-settings.ps1
ApplicationFrameHost 13100 5572350 設定
PS C:\Users\daichi\Documents\powershell\20210715\sources>
出発点としてはこんなところだろう。
もっと整理しておきたいなら、resize.ps1で使うプロセス名とウインドウタイトルを引数で指定できるようにして、resize-msedge.ps1やresize-settings.ps1からはresize.ps1を実行するように書き換えることになる。どちらが良いかは難しいが、スニペット的にそれ単体でコピーして使いたいなら、1ファイルで完結してくれているほうが扱いやすい。だが、似たようなスクリプトが何十個もできると後からまとめて書き換えるのが面倒になるので、汎用的な1つのコマンドに集約しておきたいというのもわかる。この辺りは、好みの問題かもしれない。