実用的なスクリプトを書いていこう

前回はウインドウのサイズを変更するPowerShellスクリプトとして、次のスクリプトを紹介した。


今回からは、このスクリプトの中身を読み込みながら、もう少し汎用的に利用できるスクリプトを作っていく。

スクリプト内部の動作内容を整理して理解する

先ほどのスクリプトはそれほど長くないので、仕組みを把握するのは簡単だ。基本的に次のロジックで動作している。

  1. プロセス一覧から、目的とするウインドウのハンドラを取得する
  2. 対象ウィンドウの配置場所とサイズを取得する
  3. 新しい配置場所をサイズを計算する
  4. 計算した場所とサイズをウインドウへ反映させる


ウインドウの配置場所やサイズを取得/変更する機能は、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でマイナビのページを表示させていた。

Get-Process実行時に開いておいた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>

これまでの出力を整理すると、次の順番で絞り込みを行えば、このスクリプトが欲するプロセスを特定することができそうだということがわかる。

  1. プロセス名
  2. MainWindowHandleプロパティ値が0以外
  3. 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つのコマンドに集約しておきたいというのもわかる。この辺りは、好みの問題かもしれない。