相変わらずブログが放置状態になっていますが、今だけ使える旬の内容なので久しぶりに更新することにしました。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 自体そのまま終了させるようにしています。
追記
上記の数の不一致の問題、恐らくエクスプローラーが強制終了した時に起こることが分かりました。上記でエラーが出たら、タスクマネージャーを開いてエクスプローラーのプロセスをすべて強制終了し、エクスプローラーを再起動すると問題なく動作します。
既存ウィンドウでタブを開く時に、指定のパスを開く 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 を使って最前面化しているので、もしエクスプローラーの設定が「タイトルバーに完全なパスを表示する」をオンにしていると、フォルダ名ではなくパスが入ってしまうので最前面化されません。その場合パスが格納されてるプロパティがありますので、そちらを使えば動作すると思われます。