Windows11でまとめてタブを開いたりするVBS

相変わらずブログが放置状態になっていますが、今だけ使える旬の内容なので久しぶりに更新することにしました。Windows11 の新機能のタブはそのうちより改良され、表題のようなことはしなくても良くなると思うので。

同じような VBS が Reddit にあって、またそれを紹介しているブログもあったりしますが、結構単純な感じであったため、私の重いと感じている環境では期待した動作にはなりませんでした。なので、なるべく SendKeys を使わない形で、より確度の高い動作が出来るように、久しぶりに VBScript を書きました。

「久しぶりに」と言っているように、ブログを更新していない間すっかり Python を覚え、今はそちらをメインで使っています。IE でブラウザの自動化をしてたりしましたが、こちらのサポートも終わってしまったので、Python + Selenium に移行しています。

掲載する VBS は二つで、最初に作った Reddit にあるのと同じような新規ウィンドウを立ち上げて指定のパスを開くパターン。もう一つは、既存のウィンドウ (エクスプローラー) に対し新規タブを開き指定のパスを開くパターンです。この記事を書いている間も大きく書き換えて記事も書き直したり、なかなか確定出来ませんでした(笑)

注意点や味噌など解説していきます。

新規ウィンドウでまとめてタブを開く VBS

主に起動直後などでまとめて指定のタブを開きたい場合、使えると思います。ただ最近エクスプローラーの設定を見てて気付きましたが、「ログオン時に以前のフォルダー ウィンドウを表示する」という項目を今更知ったので、この設定をオンにするなら必要性が薄れるかも(笑)

' ExplorerTabsInNewWin.vbs
Option Explicit

Dim sl, ws, LpLim, LpSlp, i, j, args, iniWinCnt
Set sl = CreateObject("Shell.Application")
Set ws = CreateObject("WScript.Shell")
Set args = WScript.Arguments
If args.Count = 0 Then WScript.Quit

Const LOC_NAME = "ホーム"
LpLim = 50: LpSlp = 200
iniWinCnt = sl.Windows.Count

ws.Run ("explorer.exe")
WaitNewWin

For i = 0 To args.Count - 1
	For j = sl.Windows.Count - 1 To 0 Step -1
		With sl.Windows(j)
			If InStr(TypeName(.document), "IShellFolderViewDual") And .LocationName = LOC_NAME And j > iniWinCnt - 1 Then
				.Navigate args(i)
				Exit For
			End If
		End With
	Next
	if i < args.Count - 1 Then
		ws.AppActivate "Explorer"
		WScript.Sleep 200
		ws.SendKeys "^t"
		WScript.Sleep 300
		WaitNewWin
	End If
Next

Sub WaitNewWin()
	Dim i, j
	For i = 1 To LpLim
		For j = sl.Windows.Count - 1 To 0 Step -1
			With sl.Windows(j)
				If InStr(TypeName(.document), "IShellFolderViewDual") And .Visible And .LocationName = LOC_NAME And j > iniWinCnt - 1 Then Exit Sub
			End With
		Next
		WScript.Sleep LpSlp
	Next
	WScript.Quit
End Sub

初めは Reddit のもののように、スクリプトの中に開きたいパスを書いていましたが、特に前述の既存ウィンドウの新規タブに対し指定のパスを開く VBS において、それを多くのパスで Win + R を使ってやりたかったことから、上記のスクリプトが書かれた VBS ファイルを多数コピーしてパスだけが違うという状態は非合理に思えたため、パスだけスクリプトに渡してあげる仕様にしました。ショートカット (.lnk) のプロパティのリンク先で、「C:\ExplorerTabsInNewWin.vbs C:\Users」のような感じで引数を与えます。開きたいタブ分、空白で区切って引数のパスを増やします。(Program Files などパス自体に空白がある場合は勿論ダブルコーテーションが必要です。)

大まかな流れは、新規ウィンドウ・タブが開くまで待機し、開いたことが確認出来たら指定のパスを開く、といった感じの繰り返しです。最後のウィンドウハンドルを取得してそれを元に新規ウィンドウの有無を判定していたりしましたが、結局問題がある場合があってウィンドウを開く前のウィンドウ数と比較することにしました。

以下は詳細や細かな注意点などです。

WaitNewWin

新規ウィンドウが生成されるまで待機します。判断する条件は、開いた際のウィンドウタイトルと初期ウィンドウ数より上であるか、です。例えばスクリプト実行前に開いてるウィンドウに「ホーム」があっても、それは除外されることになります。

For Each ではなく For 文の逆回転を使ってます。本当は For Each を使いたかったのですが、VBScript では For Each の逆回転が出来ないので致し方なく For にしました。なので一応注意なのですが、場合によって「オブジェクトがない」というエラーが出る可能性はあります。「sl.Windows.Count - 1」と現在のウィンドウ数の MAX から 0 に向けて繰り返してるので、構文上はエラーが起きるはずはないのですが、VBA のウォッチウィンドウで色々検証していた時、何故かこの sl.Windows.Count に入ってる数と その sl.Windows にぶら下がってる Item の数が一致しない場合があり、その時はエラーが出ました。(例えば .Count に 10 という数字が入ってるが実際ぶら下がってる Item が何故か 7 つしかない場合に i に 10 が入った時、当然エラーとなる) ただエラーが出たのはその時だけで、実用していて今のところ同じエラーはありません。

逆回転している理由は、新規ウィンドウは配列の後方にある訳ですので、大きい数字から開始した方が効率的に思えたからです。前方から始めると余分なループが発生します。

永久ループになってしまうのを避けるため、外側の For でリミットを設けています (LpLim)。リミットに達したら VBScript 自体そのまま終了させるようにしています。

その他

Reddit のものと違って、パスは SendKeys で送って Enter で叩くのではなく、Navigate でナビゲートさせます。

SendKeys で開くのは Ctrl + T のみです。この部分も新規タブを SendKeys を使わず出来ないかと検証しましたが、後述しますがウォッチウィンドウで見る限り新しいタブ機能に関しては別の仕組みになっているように思え、VBScript 側からはどうにも制御出来なさそうでした。

既存ウィンドウでタブを開く時に、指定のパスを開く VBS

私は主に指定のパスを開きたい時、前述のように Win + R からよく開いてます。ただこの際エクスプローラーのショートカットを叩いても、新規ウィンドウで開いてしまう訳です。これが何とかならないかとこのバージョンも作りました。

何れこれも将来の Windows アップデートで、ショートカットに何らかの引数を付けたら既存ウィンドウにタブを追加して開く、など機能が拡張するのだろうと個人的には予想しています。

主な目的は既存ウィンドウに対しての実行ですが、既存ウィンドウが無かった時は新規ウィンドウを起動して実行します。(つまりその場合一つ目のスクリプトとほぼ同じ動作) 謂わば統合した形になります。

デメリットは、前述のスクリプトも含め、重く感じます…。もし Windows 標準の機能ならもっさり感はないと思うのだけど。ただ重さは環境によると思います。Ryzen 3700X で仮想 OS を常時起動してる環境では重いですが、3900X ではそこまで重いと感じません。

' ExplorerTabsInExistWin.vbs
Option Explicit

Dim sl, ws, LpLim, LpSlp, i, j, args, w, flgNew, iniWinCnt
Set sl = CreateObject("Shell.Application")
Set ws = CreateObject("WScript.Shell")
Set args = WScript.Arguments
If args.Count = 0 Then WScript.Quit

Const LOC_NAME = "ホーム"
LpLim = 50: LpSlp = 200
flgNew = True
iniWinCnt = sl.Windows.Count

For Each w In sl.Windows
	If InStr(TypeName(w.document), "IShellFolderViewDual") And w.Visible Then flgNew = False: Exit For
Next

' 新規の場合
If flgNew Then
	ws.Run ("explorer.exe")
	WaitNewWin
' 既存のウィンドウで開く場合
Else
	OpenTab
End If

For i = 0 To args.Count - 1
	For j = sl.Windows.Count - 1 To 0 Step -1
		With sl.Windows(j)
			If InStr(TypeName(.document), "IShellFolderViewDual") And .LocationName = LOC_NAME And j > iniWinCnt - 1 Then
				.Navigate args(i)
				Exit For
			End If
		End With
	Next
	If i < args.Count - 1 Then OpenTab
Next

Sub OpenTab()
	If flgNew Then
		ws.AppActivate "Explorer"
	Else
		For Each w In sl.Windows
			If InStr(TypeName(w.document), "IShellFolderViewDual") And w.Visible Then ws.AppActivate w.LocationName
		Next
	End If
	WScript.Sleep 200
	If Not flgNew Then
		ws.SendKeys "{F4}"
		WScript.Sleep 100
	End If
	ws.SendKeys "^t"
	WScript.Sleep 300
	WaitNewWin
End Sub

Sub WaitNewWin()
	Dim i, j
	For i = 1 To LpLim
		For j = sl.Windows.Count - 1 To 0 Step -1
			With sl.Windows(j)
				If InStr(TypeName(.document), "IShellFolderViewDual") And .Visible And .LocationName = LOC_NAME And j > iniWinCnt - 1 Then Exit Sub
			End With
		Next
		WScript.Sleep LpSlp
	Next
	WScript.Quit
End Sub
Open Tab

新規ウィンドウに対し行う時は、ws.AppActivate "Explorer" で最前面化します。しかし既存のウィンドウは最後に開いたものに対して実行したいため、具体的に指定します。

味噌の一つは、具体的に指定と言っても、For で抜けることなく最後まで回していることです。これには理由があり、当初は最後に開いたウィンドウ (タブ) の LocationName を指定して最前面化していました。しかしそれが上手く動作しない場合があり、原因を探ってみると、最後のタブ以外に自分が手動でフォーカスを移していた場合に起こることが分りました。

例えば A, B, C というタブがあり、最後に開いたタブとしては C だけど、自分で作業上 B のタブへフォーカスしていた場合、C を最前面化させたくても、出来ません。それでは B にフォーカスがあることを取得し、B の LocationName を指定して最前面化すれば良いのではとも思いましたが、前述のウォッチウィンドウの通り「どのタブにフォーカスがあるか」などのプロパティはない模様です。タブ関係のプロパティは全く無さそうです。

そこで、一つに指定せず通常回転で全部回せば、どこかで B にヒットしウィンドウを最前面化出来るなと思い浮かびました。二つ目の味噌は、他の部分では逆回転が主な回し方ですが、逆にここでは通常回転であることです。例えば B と同じパスだけどウィンドウが違い、且つそっちの方が古いウィンドウの場合、順番的に逆回転だとその古いウィンドウの方が最終的に最前面されますが、通常回転だと新しい方が最前面化されます。

F4 の送信に関して、これははっきり言って F4 の機能そのものを目的としている訳ではなく、本来なら不要です。ですが、どうもタブを開く Ctrl + T が効かない場合があるようで、その状態の場合、どうにか出来ないかと色々検証した結果、唯一 F4 のショートカットだけは効きました。これを実行した後なら、Ctrl + T でタブを開くことが出来ます。

詳しい原因は分かりませんが、For で回して ws.AppActivate w.LocationName するとなる模様です。致し方ないので既存ウィンドウに対して開く場合は、F4 を送信することにしました。

あと気を付ける点として、LocationName を使って最前面化しているので、もしエクスプローラーの設定が「タイトルバーに完全なパスを表示する」をオンにしていると、フォルダ名ではなくパスが入ってしまうので最前面化されません。その場合パスが格納されてるプロパティがありますので、そちらを使えば動作すると思われます。