【実務・中級編】【上級】DAO.Recordsetの「AbsolutePosition」を活用した、進捗バーの動的更新とユーザー体験の向上 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordsetの「AbsolutePosition」で実現する、秒速・極上のUXデザイン

開発現場でよく見かける光景がある。数万件のレコードを処理する巨大なバッチ処理、あるいは複雑な集計クエリのループ。画面がフリーズしたかのように固まり、ユーザーは「本当に動いているのか?」「落ちたのか?」と不安に駆られながら、タスクマネージャーを開く。

プロとして、これは絶対に許されないアンチパターンだ。

「Accessだから遅い」「VBAだからリッチなUIは作れない」。そう言い訳をする前に、オブジェクトモデルの挙動を深く理解し、適切なプロパティを叩いているか問い直してほしい。

今回は、DAO.Recordsetの隠れた名優`AbsolutePosition`を極限まで使い倒し、長時間処理のユーザー体験(UX)を劇的に向上させるための決定版を伝授する。

1. なぜ「件数カウント+ループ」の進捗表示は愚行なのか?

多くの初級〜中級プログラマが陥る罠がこれだ。

‘ 【最悪な実装例】やってはいけないアンチパターン
Dim rs As DAO.Recordset
Dim totalCount As Long
Dim currentCount As Long

Set rs = CurrentDb.OpenRecordset(“T_HeavyData”, dbOpenSnapshot)

‘ 1. 全件数を得るためにわざわざMoveLastを叩く(非効率)
rs.MoveLast
totalCount = rs.RecordCount
rs.MoveFirst

currentCount = 0
Do Until rs.EOF
‘ 重い処理
currentCount = currentCount + 1

‘ 2. 毎ループごとに進捗フォームを更新(画面描画のオーバーヘッドで爆死)
Forms!F_Progress!ProgressBar.Width = (currentCount / totalCount) 3000
Forms!F_Progress!TxtStatus.Caption = currentCount & ” / ” & totalCount & ” 処理中…”
DoEvents ‘ 非効率なイベント割り込み

rs.MoveNext
Loop
rs.Close

このコードの何がクソなのか。
1. `RecordCount`の誤解: DAOの`Snapshot`やダイナセットにおいて、`Open`直後の`RecordCount`は「実際にアクセスした件数」しか返さないことが多い。そのため、正確な総数を得るためにわざわざ`MoveLast`を強制される。
2. ループ内での手動カウンター: 毎ループごとに変数をインクリメントし、UIを更新する。これではVBAの実行スレッドがUI描画に阻害され、処理速度が体感で数倍〜数十倍に落ちる。
3. `DoEvents`の乱用: リソースを無駄に食いつぶし、最悪の場合はユーザーの余計なダブルクリックによる二重起動(競合エラー)を誘発する。

プロのエンジニアは、もっとエレガントに、かつハードウェアの限界を引き出す。

2. 決定解:`AbsolutePosition` と `PercentPosition` の真実

DAO.Recordsetには、現在自分がどこにいるのかを示す指標が用意されている。それが `AbsolutePosition` プロパティだ。

  • `AbsolutePosition`: 0から始まる現在のレコード位置を返す(例:先頭なら`0`、2件目なら`1`)。
  • `PercentPosition`: 現在位置の全体に対する割合(0〜100)を返す。

ここで重要なのは、「`AbsolutePosition`は、レコードセットが完全にポップレート(移送)されていなくても、エンジン内部のポインタ位置を高速に返してくれる」という点だ。

さらに、UIの更新頻度を制御(間引き)することで、描画負荷を極限まで下げつつ、滑らかな進捗バーを実現できる。

3. 【プロダクションコード】実務で使える堅牢な進捗バー制御クラス

ここでは、単なるプロシージャではなく、呼び出し元を汚さず、かつ安全にエラーハンドリングと画面制御を行う実用的なコードを提示する。

前提として、進捗状況を表示する専用の非モーダルフォーム(例: `F_Progress`)が存在し、その中に以下のコントロールがあるものとする。

  • 棒グラフ用のラベルまたは矩形コントロール: `Bar` (幅をピクセル単位で変化させる)
  • パーセンテージ表示用テキストボックス: `TxtPercent`
  • メッセージ表示用ラベル: `TxtMessage`

呼び出し側の実装例

Public Sub ExecuteHeavyBatchProcess()
Dim db As DAO.Database
Dim rs As DAO.Recordset

Set db = CurrentDb
‘ スナップショットで最速の読込専用アクセスを確保
Set rs = db.OpenRecordset(“SELECT FROM T_HugeData WHERE 処理フラグ = 0”, dbOpenSnapshot, dbReadOnly)

If rs.EOF Then
MsgBox “処理対象のデータが存在しません。”, vbInformation
Exit Sub
End If

‘ 総件数の正確な取得(インデックスが効いていればMoveLastのコストは最小限)
rs.MoveLast
Dim totalRows As Long
totalRows = rs.RecordCount
rs.MoveFirst

‘ 進捗フォームのオープン
DoCmd.OpenForm “F_Progress”
Forms!F_Progress!TxtMessage.Caption = “データを処理しています…”

Dim lngCurrent As Long
Dim lngStepInterval As Long
‘ 10分割以上、またはデータ量に応じた描画インターバルを動的算出(過剰なUI描画を抑制)
lngStepInterval = IIf(totalRows > 1000, totalRows \ 100, 1) ‘ 1%刻みで更新

‘ — メインループ —
Do Until rs.EOF

‘ —————————————————-
‘ ここに実際の重い処理を記述
‘ 例: Call ProcessItem(rs!ID)
‘ —————————————————-

‘ 現在位置の取得(AbsolutePositionは0基底なので +1 する)
lngCurrent = rs.AbsolutePosition + 1

‘ 描画のオーバーヘッドを防ぐため、一定間隔または最終行のみUIを更新
If (lngCurrent Mod lngStepInterval = 0) Or (lngCurrent = totalRows) Then
Call UpdateProgressBar(lngCurrent, totalRows)
End If

rs.MoveNext
Loop

‘ クリーンアップ
rs.Close
Set rs = Nothing
Set db = Nothing

DoCmd.Close acForm, “F_Progress”
MsgBox “すべての処理が正常に完了しました。”, vbInformation
End Sub

進捗更新用ヘルパープロシージャ

Private Sub UpdateProgressBar(ByVal current As Long, ByVal total As Long)
On Error GoTo ErrorHandler

Const MAX_WIDTH As Long = 4000 ‘ 進捗バーが100%の時のピクセル幅
Dim currentWidth As Long
Dim percentage As Double

If total <= 0 Then Exit Sub percentage = (CDbl(current) / CDbl(total)) currentWidth = CLng(MAX_WIDTH percentage) ' フォームのコントロール制御 With Forms!F_Progress .Bar.Width = currentWidth .TxtPercent.Caption = Format(percentage, "0% ") & "(" & current & " / " & total & " 件)" .Repaint ' 強制再描画(間引いているため、ここにコストをかけても全体のスループットは落ちない) End With DoEvents ' OSに制御を返し、フリーズを防ぐ Exit Sub ErrorHandler: ' ユーザーが途中で進捗フォームを閉じた場合のエラーなどを安全に無視 Resume Next End Sub ---

4. アーキテクトからの実践的アドバイス:データベース連携の罠

このコードをマルチユーザー環境や、バックエンドがSQL ServerなどのODBCリンクテーブルである環境で動かす場合、いくつかの「地雷」を踏む可能性がある。

1. `dbOpenSnapshot` の強制
ダイナセット (`dbOpenDynaset`) やテーブルタイプ (`dbOpenTable`) でこれをやると、レコードセットの構築やロックの維持に余計なリソースを消費する。読込専用のバッチ処理であれば、必ず `dbOpenSnapshot` を指定せよ。これにより、Jet/ACEエンジンはローカルキャッシュをフル活用し、`AbsolutePosition` の計算をも高速化する。

2. リンクテーブルにおける `RecordCount` と `AbsolutePosition` の限界
バックエンドがSQL Serverの場合、`MoveLast` を実行した瞬間に全レコードがクライアント側にフェッチ(転送)される可能性がある。数百万件規模のテーブルに対してこれをやると、ネットワーク帯域を圧迫し、Access側がメモリ不足(Out of Memory)でクラッシュする。

  • 対策: 10万件を超えるような巨大なデータソースの場合は、`AbsolutePosition` による単純な進捗率ではなく、PK(主キー)の範囲分割(チャンク処理)や、あらかじめ発行した `SELECT COUNT()` の結果を分母として使う設計に切り替えるべきだ。

3. `DoEvents` の魔力に溺れるな
`DoEvents` を挟むことでUIのフリーズを防げるが、ユーザーが「処理中に何度もボタンを連打する」「進捗フォームの閉じるボタン(×)を押してしまう」というリスクを生む。
処理の開始時に `Application.Echo False` で画面描画を完全にロックするか、進捗フォーム側でコントロールの有効無効を制御する「Mandelbrot(マンデルブロ)的防御」を必ず実装すること。

5. 結び:コードの美しさは、UXの美しさに直結する

「動けばいい」で作られたVBAは、例外なく現場で嫌われ、やがてマクロのスパゲッティと化して消えていく。

しかし、オブジェクトのライフサイクルを理解し、適切なプロパティ(`AbsolutePosition`)を選び、描画のボトルネックを計算し尽くしたコードは、まるで高級なデスクトップアプリのような滑らかな挙動をAccessにもたらす。

プロの仕事とは、ただロジックを組むことではない。「ユーザーに待ち時間のストレスを感じさせないこと」だ。
今日のコードをあなたのソリューションに組み込み、その圧倒的なパフォーマンスの違いを肌で体感してほしい。

タイトルとURLをコピーしました