制御不能な「沈黙」を排除せよ:HTAとVBScriptによる高精度進捗表示アーキテクチャ
業務自動化の現場において、最もエンジニアの信頼を損なう行為は何か。それは「実行中のスクリプトが、生きているのか死んでいるのかをユーザーに悟らせないこと」である。
数千、数万のファイル操作や、レガシーなデータベースへの重いクエリ。これらを単なる `.vbs` ファイルとして実行させ、コンソールに何も表示させないのは、プロの仕事ではない。ユーザーは、フリーズしたのか、あるいは内部で致命的なエラーが発生して無限ループに陥ったのかを判断できず、最終的にプロセスを強制終了させる。その結果、ファイルシステムの整合性が破壊されるリスクを招くのだ。
本稿では、VBScriptのロジックと、MSHTA (HTML Application) によるリッチなUIを融合させ、「残り時間の動的推定」を含むプロ仕様のプログレスバーを実装する極限の知見を伝授する。
1. アーキテクチャの設計思想:UIスレッドの解放
VBScriptはシングルスレッドの言語である。HTA内で重いループ処理を記述すれば、UIスレッドは即座にロックされ、プログレスバーは更新を停止し、ウィンドウは「応答なし」となる。これは、技術的な敗北を意味する。
真のアーキテクトは、以下の2つのアプローチのいずれかを選択する。
1. Time-Slicing (時間分割法): 処理を小さな単位に分割し、`setTimeout` を用いて制御を一度UIスレッドに戻す。
2. Decoupled Process (プロセス分離型): ロジックを別プロセス(WSH)として実行し、HTAからファイルやレジストリを介して状態を監視する。
本稿では、実装の簡便性と配布の容易さを考慮し、HTA単体で完結させつつ、UIをフリーズさせない「再帰的タスク分割モデル」を採用する。
2. 実装:動的進捗推定エンジン
以下のコードは、単なるプログレスバーではない。経過時間から総所要時間を算出し、残りの時間をリアルタイムに推定する計算エンジンを組み込んだ実戦的なHTA実装である。
<!--#include "<mshtml>"-->
<html>
<head>
<title>Advanced Task Monitor</title>
<hta:application
id="oTaskApp"
applicationname="TaskMonitor"
border="thin"
singleinstance="yes"
sysmenu="yes"
windowstate="normal"
>
<style>
body { font-family: "Segoe UI", sans-serif; background: #f0f0f0; font-size: 12px; margin: 20px; }
.container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.progress-container { width: 100%; background: #e0e0e0; border-radius: 4px; height: 20px; margin: 15px 0; overflow: hidden; }
#progressBar { width: 0%; height: 100%; background: linear-gradient(90deg, #2196F3, #00BCD4); transition: width 0.2s ease; }
.status-text { font-weight: bold; color: #333; }
.info-text { color: #666; font-size: 11px; }
.btn-start { padding: 8px 16px; cursor: pointer; background: #4CAF50; color: white; border: none; border-radius: 4px; }
.btn-start:disabled { background: #ccc; }
</style>
<script language="vbscript">
' --- Global Variables ---
Dim startTime, totalItems, currentItem, elapsedSeconds
Dim remainingSeconds, totalEstimatedSeconds
' オブジェクトの明示的な解放を徹底するための定数
Const WScriptShell = "WScript.Shell"
Const FileSystem = "Scripting.FileSystemObject"
Sub Window_OnLoad
window.resizeTo 450, 300
End Sub
' ---------------------------------------------------------
' メイン・エグゼキューション・ループ
' ---------------------------------------------------------
Sub StartTask()
' 初期化
totalItems = 100 ' 今回はデモとして100ステップ
currentItem = 0
startTime = Timer
' UIの初期化
document.getElementById("btnStart").disabled = True
document.getElementById("status").innerText = "Initializing..."
' 非同期的なタスク実行の開始(Time-Slicingの開始)
ExecuteStep
End Sub
Sub ExecuteStep()
On Error Resume Next
' --- [Business Logic Section] ---
' ここに本来の重い処理を記述する。
' 本来はFileSystemObject等を用いてファイル操作を行う。
If currentItem < totalItems Then
' 重い処理のシミュレーション
Do While Timer < startTime + 0.05 ' 50ms程度の負荷
' CPU負荷を模倣
Loop
currentItem = currentItem + 1
' 進捗計算
UpdateUI
' 【極めて重要】
' 次のステップを setTimeout でスケジュールすることで、
' UIスレッドに制御を戻し、ブラウザの描画プロセスを確保する。
' これを行わないと、プログレスバーは動かない。
window.setTimeout "ExecuteStep", 10
Else
' 全工程完了
FinalizeTask
End If
If Err.Number <> 0 Then
MsgBox "Critical Error: " & Err.Description, 16, "Error"
FinalizeTask
End If
End Sub
' ---------------------------------------------------------
' 進捗・残り時間計算エンジン
' ---------------------------------------------------------
Sub UpdateUI()
Dim progressPercent, now, elapsed
now = Timer
elapsed = now - startTime
' 進捗率の算出
progressPercent = (currentItem / totalItems) * 100
document.getElementById("progressBar").style.width = progressPercent & "%"
' 残り時間の推定アルゴリズム
' 公式: (経過時間 / 現在の進捗) = 1ステップあたりの時間
' 残り時間 = 1ステップあたりの時間 * 残りステップ数
If currentItem > 0 Then
Dim timePerItem
timePerItem = elapsed / currentItem
remainingSeconds = timePerItem * (totalItems - currentItem)
' 表示用フォーマット
document.getElementById("status").innerText = "Processing: " & currentItem & " / " & totalItems
document.getElementById("timeLeft").innerText = "Estimated Remaining: " & FormatTime(remainingSeconds)
End If
End Sub
' 時間を HH:mm:ss 形式に整形する
Function FormatTime(totalSecs)
Dim h, m, s
h = Int(totalSecs / 3600)
m = Int((totalSecs Mod 3600) / 60)
s = Int(totalSecs Mod 60)
FormatTime = Right("0" & h, 2) & ":" & Right("0" & m, 2) & ":" & Right("0" & s, 2)
End Function
Sub FinalizeTask()
document.getElementById("btnStart").disabled = False
document.getElementById("status").innerText = "Task Completed Successfully."
document.getElementById("timeLeft").innerText = ""
' オブジェクトが残っている場合はここで明示的に破棄
' Set obj = Nothing
End Sub
</script>
</head>
<body>
<div class="container">
<div class="status-text" id="status">Ready to start</div>
<div class="progress-container">
<div id="progressBar"></div>
</div>
<div class="info-text" id="timeLeft">Estimated Remaining: --:--:--</div>
<br>
<input type="button" id="btnStart" class="btn-start" value="Start Heavy Process" onclick="StartTask">
</div>
</body>
</html>
3. アーキテクトの視点:極限の最適化と保守性
この実装には、シニアエンジニアが意識すべき「重み」が込められている。
3.1. 非同期性の確保(Time-Slicing)
コード内の `window.setTimeout “ExecuteStep”, 10` は、単なる遅延ではない。これは、MSHTAのイベントループに「隙間」を作るための命令である。この隙間があることで、Windows OSは描画メッセージを処理し、ユーザーはプログレスバーの動きを目視でき、ウィンドウのドラッグも可能になる。
3.2. 残り時間推定の数学的妥当性
残り時間の計算において、単純な平均値を用いる手法は、処理の負荷が一定であることを前提としている。実務においては、ファイルサイズやネットワーク帯域によって負荷は変動する。
より高度な実装を行うならば、「移動平均(Moving Average)」を導入すべきである。直近5ステップの経過時間を取得し、その平均から次ステップを予測することで、推定時間の「跳ね(Jitter)」を抑制し、ユーザーに安定した安心感を与えることができる。
3.3. メモリ管理とオブジェクトのライフサイクル
VBScriptにおける最大の懸念は、COMオブジェクトの残留である。
`CreateObject(“Scripting.FileSystemObject”)` 等で生成したオブジェクトは、スコープを抜けただけではメモリ上に居座り続ける場合がある。特にループ内でオブジェクトを生成・破棄するような設計にする場合は、必ず `Set obj = Nothing` を明示的に実行せよ。HTAは長時間稼働することが多いため、微細なメモリリークが最終的にプロセスのクラッシュを招く。
3.4. レガシー環境への配慮
HTAは内部的にInternet Explorerのレンダリングエンジンを使用している。Edgeへの移行が進む現代においても、WSH/HTAはWindowsのコアコンポーネントとして残り続けているが、CSSの最新機能(FlexboxやGridなど)は利用できない。デザインを構築する際は、常にレガシーなレンダリング能力を前提とした堅牢なHTML/CSSを選択することが、システム管理者の責務である。
結論
プロフェッショナルな自動化ツールとは、単に「結果を出す」だけのものではない。「実行プロセスを可視化し、ユーザーの心理的コストを最小化する」ものだ。
今回提示したHTAによる実装パターンは、VBScriptという制約の多い言語において、最大限のユーザー体験(UX)を引き出すための、一つの到達点である。この知見を、貴殿のレガシー・システム保守、および自動化アーキテクチャの構築に役立ててほしい。
