【実務・中級編】DAO.Recordsetの「AbsolutePosition」を活用した進捗率表示のカスタマイズ – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordsetの「AbsolutePosition」で進捗表示をカスタマイズする

Access VBAの世界で、あなたは日々、膨大なデータを相手に業務効率化の最前線に立っていることでしょう。データ処理の自動化はVBAの真骨頂ですが、大量のレコードを扱う際、ユーザーが最も不満を抱くのは「処理がどこまで進んでいるのか分からない」という不安です。画面がフリーズしたように見え、いつ終わるかも分からず、結果としてユーザー体験は最悪になります。

この記事では、単なるリファレンスの引き写しではない、真に実用的な進捗表示の実装方法を伝授します。特に、`DAO.Recordset`の`AbsolutePosition`プロパティを核とし、パフォーマンス、堅牢性、そして保守性を極限まで追求した設計思想を、具体的なコードと共に深く掘り下げていきます。

1. 進捗表示はなぜ「必須」なのか?安易な実装が招く破滅

「進捗表示なんて、なくても処理は動くから」――そう考える担当者は少なくありません。しかし、それは大きな間違いです。進捗表示は単なる「おまけ」ではなく、ユーザーとの対話を維持し、アプリケーションの信頼性を担保する必須の機能です。

想像してください。何千、何万件ものレコードを処理するVBAプロシージャを実行したとき、画面が数秒どころか数分間フリーズしてしまったら? ユーザーは「アプリケーションがクラッシュした」「処理が止まった」と判断し、強制終了させるでしょう。これはデータの破損、最悪の場合、業務停止につながる大惨事です。

安易な進捗表示の実装は、さらに深刻な問題を引き起こします。

  • `DoCmd.RunSQL`の連発: ループ内でSQLを直接実行し、その都度データベースにアクセスするような処理は、ネットワーク負荷とI/Oを極端に増大させ、パフォーマンスを著しく低下させます。
  • `RecordCount`の誤解: `Recordset.RecordCount`プロパティは、レコードセットを最後までスクロールしないと正確な値を返さないことがあります。処理開始時に`MoveLast`を実行して件数を取得するアプローチは一般的ですが、これがネットワーク経由のテーブルである場合、それ自体が非常に重い処理となり、進捗表示を始める前に長い待機時間が発生してしまいます。
  • `DoEvents`の乱用: UIのフリーズ回避に不可欠な`DoEvents`ですが、無計画に多用すると、コンテキストスイッチのオーバーヘッドで処理速度が大幅に低下します。また、イベントキューが意図しないイベントで溢れかえるリスクも伴います。

これらの落とし穴を避け、真にユーザーフレンドリーで効率的な進捗表示を実現するには、`DAO.Recordset`の特性を深く理解し、適切な設計原則に基づいて実装する必要があります。

2. DAO.Recordsetの真髄と「AbsolutePosition」の威力

Access VBAにおいて、Jet/ACEデータベース(.accdb, .mdb)を扱う上で、`DAO (Data Access Objects)`は最もネイティブで、最もパフォーマンスに優れたデータアクセス方法です。ADO (ActiveX Data Objects) も強力ですが、ローカルのAccessデータベースに対してはDAOが優位に立つ場面が多く、そのオブジェクトライフサイクルもAccessアプリケーションと密接に連携しています。

2.1. `Recordset.RecordCount`の特性を理解する

先述の通り、`Recordset.RecordCount`は注意が必要です。特に`dbOpenDynaset`や`dbOpenSnapshot`で開いたレコードセットの場合、初期状態では読み込まれたレコードの数、または取得できたレコードの概算値しか返さないことがあります。正確な件数を取得するには、一度レコードセットの終端に移動させる必要があります。

‘ 正確なRecordCountを取得するが、コストがかかる
With rs
If .RecordCount = 0 Then
‘ レコードが一件もない場合、MoveLastはエラーになる可能性があるので先にチェック
lngTotalRecords = 0
Else
.MoveLast ‘ レコードセットの終端に移動し、全件をフェッチさせる
lngTotalRecords = .RecordCount
.MoveFirst ‘ 処理開始位置に戻る
End If
End With

この`MoveLast`は、特にネットワーク経由のテーブルでは非常に重い処理になり得ます。進捗表示の「開始前」に時間がかかるのは本末転倒です。

2.2. `AbsolutePosition`の活用と注意点

ここで登場するのが`AbsolutePosition`です。`AbsolutePosition`は、レコードセットの現在のカーソル位置を、0から始まるインデックスで返します。つまり、先頭レコードが0、2番目のレコードが1、という具合です。

このプロパティの最大の利点は、`RecordCount`のように全件フェッチを強制せずとも、現在位置をリアルタイムに把握できる点にあります。`RecordCount`と組み合わせることで、私たちは現在位置と全体の進捗率を計算できます。

ただし、`AbsolutePosition`にはいくつかの注意点があります。

  • 0ベースインデックス: 先頭レコードは0です。パーセンテージ計算時には、1ベースとして扱うか、表示時に調整が必要です。
  • `EOF` (End Of File) と `BOF` (Beginning Of File): レコードセットの終端を過ぎると`-1`を返します。レコードセットが空の場合も`-1`を返すことがあります。処理ループの条件と組み合わせる際に注意が必要です。
  • 非ブックマーク可能レコードセット: `dbForwardOnly`で開いたレコードセットなど、一部のレコードセットタイプでは`AbsolutePosition`はサポートされません。通常、進捗表示を伴うループ処理では`dbOpenDynaset`や`dbOpenSnapshot`を使うため、問題になることは稀ですが、念のため理解しておきましょう。

3. 堅牢な進捗表示システムの設計思想

進捗表示システムは、単にバーが伸びるだけでは不十分です。以下の設計原則に基づき、堅牢かつ再利用性の高いモジュールを構築しましょう。

1. UIスレッドのフリーズ回避: `DoEvents`は必須ですが、乱用は厳禁です。更新頻度を適切に制御し、パフォーマンスと応答性のバランスを取ります。
2. エラーハンドリングとリソース解放: 処理中にエラーが発生しても、データベース接続やレコードセットが宙に浮かないよう、`On Error GoTo`と`Finally`セクションに相当するリソース解放処理を徹底します。
3. 汎用性と再利用性: 進捗表示のロジックを独立したクラスモジュールにカプセル化することで、様々な処理で再利用可能にします。これにより、コードの重複を避け、保守性を高めます。
4. 表示場所の選択: ステータスバー、または専用のプログレスバーフォームのどちらか、あるいは両方で表示できるよう柔軟な設計にします。
5. ファイル/DB連携時の考慮: 外部データベースとの連携では、ネットワーク遅延やトランザクション、排他制御も考慮に入れる必要があります。

4. プロダクションコード例:進捗表示クラス `CProgressIndicator`

ここでは、上記設計思想に基づいた汎用的な進捗表示クラス `CProgressIndicator` を作成します。このクラスは、ステータスバーへの表示と、カスタムフォームによるプログレスバー表示の両方に対応可能です。

4.1. 汎用進捗表示クラス `CProgressIndicator` の作成

新しいクラスモジュールを作成し、名前を `CProgressIndicator` とします。

‘ クラスモジュール名: CProgressIndicator
Option Compare Database
Option Explicit

‘=== プライベートメンバー ===
Private m_lngTotalSteps As Long ‘ 全体のステップ数 (レコード数など)
Private m_lngCurrentStep As Long ‘ 現在のステップ数
Private m_strTitle As String ‘ 処理のタイトル
Private m_frmProgress As Form ‘ プログレスバー表示用フォームオブジェクト
Private m_lngUpdateInterval As Long ‘ 進捗更新の間隔 (ステップ数)
Private m_lngLastUpdateStep As Long ‘ 最後に更新したステップ数
Private m_booUseStatusBar As Boolean ‘ ステータスバーを使用するか
Private m_booUseForm As Boolean ‘ フォームを使用するか
Private Const PROGRESS_FORM_NAME As String = “frmProgressIndicator” ‘ プログレスバーフォーム名

‘=== 初期化 ===
‘ 新しい進捗インジケータを初期化します。
‘ strTitle: 処理のタイトル (例: “データ更新中”)
‘ lngTotalSteps: 全体のステップ数 (例: レコード数)
‘ Optional lngUpdateInterval: 進捗表示を更新する頻度 (デフォルトは100ステップ)
‘ Optional booUseStatusBar: ステータスバーに表示するか (デフォルトはTrue)
‘ Optional booUseForm: カスタムフォームで表示するか (デフォルトはFalse)
Public Sub Initialize( _
ByVal strTitle As String, _
ByVal lngTotalSteps As Long, _
Optional ByVal lngUpdateInterval As Long = 100, _
Optional ByVal booUseStatusBar As Boolean = True, _
Optional ByVal booUseForm As Boolean = False _
)
‘ 引数チェック
If lngTotalSteps <= 0 Then ' 全ステップ数が0以下の場合はエラー、または警告とする ' ここではエラーとして処理停止を選択 Err.Raise vbObjectError + 1000, "CProgressIndicator.Initialize", "Total steps must be greater than 0." Exit Sub End If If lngUpdateInterval <= 0 Then ' 更新間隔が0以下の場合はデフォルト値を強制 lngUpdateInterval = 1 End If m_strTitle = strTitle m_lngTotalSteps = lngTotalSteps m_lngCurrentStep = 0 ' 初期化時は0から開始 m_lngUpdateInterval = lngUpdateInterval m_lngLastUpdateStep = 0 m_booUseStatusBar = booUseStatusBar m_booUseForm = booUseForm ' フォームを使用する場合の準備 If m_booUseForm Then On Error Resume Next ' フォームが開いていない場合はエラーになるので無視 DoCmd.Close acForm, PROGRESS_FORM_NAME, acSaveNo On Error GoTo 0 Set m_frmProgress = Forms.Open(PROGRESS_FORM_NAME, acFormDS) ' デザインビューで開く With m_frmProgress .Caption = m_strTitle !lblTitle.Caption = m_strTitle !prgBar.Max = 100 ' プログレスバーの最大値を100に設定 (パーセンテージ表示のため) !prgBar.Value = 0 !lblPercent.Caption = "0%" !lblStatus.Caption = "準備中..." .Visible = True ' フォームを表示 .Repaint ' 強制再描画 End With End If ' 初期のステータスバー表示 If m_booUseStatusBar Then Application.SysCmd acSysCmdSetStatus, m_strTitle & " (0%)" End If End Sub '=== 進捗の更新 === ' 現在のステップ数を指定して進捗を更新します。 ' lngCurrentStep: 現在の処理ステップ (AbsolutePositionなど) Public Sub UpdateProgress(ByVal lngCurrentStep As Long) m_lngCurrentStep = lngCurrentStep ' 更新間隔に達したか、または最後のステップに到達した場合のみ更新 If m_lngCurrentStep >= m_lngLastUpdateStep + m_lngUpdateInterval Or m_lngCurrentStep >= m_lngTotalSteps Then
Dim lngPercent As Long
Dim strStatus As String

‘ 進捗率の計算 (0-100%)
If m_lngTotalSteps > 0 Then
lngPercent = Int((m_lngCurrentStep / m_lngTotalSteps) 100)
If lngPercent > 100 Then lngPercent = 100 ‘ 念のため100%超えを防止
Else
lngPercent = 0
End If

‘ ステータス文字列の作成
strStatus = m_strTitle & ” (” & lngPercent & “% – ” & m_lngCurrentStep & ” / ” & m_lngTotalSteps & “)”

‘ ステータスバーの更新
If m_booUseStatusBar Then
Application.SysCmd acSysCmdSetStatus, strStatus
End If

‘ フォームの更新
If m_booUseForm And Not m_frmProgress Is Nothing Then
With m_frmProgress
!prgBar.Value = lngPercent
!lblPercent.Caption = lngPercent & “%”
!lblStatus.Caption = “処理中: ” & m_lngCurrentStep & ” / ” & m_lngTotalSteps
.Repaint ‘ 強制再描画
End With
End If

m_lngLastUpdateStep = m_lngCurrentStep ‘ 最終更新ステップを記録

‘ UIイベントを処理 (フリーズ防止)
DoEvents
End If
End Sub

‘=== 処理の完了 ===
‘ 進捗表示を終了し、リソースを解放します。
Public Sub Finish()
If m_booUseStatusBar Then
Application.SysCmd acSysCmdSetStatus, “処理が完了しました。”
End If

If m_booUseForm And Not m_frmProgress Is Nothing Then
m_frmProgress.lblStatus.Caption = “完了しました。”
m_frmProgress.prgBar.Value = 100
m_frmProgress.lblPercent.Caption = “100%”
m_frmProgress.Repaint
DoEvents
‘ 処理完了後、フォームを閉じるか残すかは運用次第
‘ ここでは少し待ってから閉じる例
Application.Wait Now + TimeValue(“00:00:02”) ‘ 2秒待機
DoCmd.Close acForm, PROGRESS_FORM_NAME, acSaveNo
End If

‘ オブジェクトの解放
Set m_frmProgress = Nothing
m_lngTotalSteps = 0
m_lngCurrentStep = 0
m_strTitle = “”
End Sub

‘=== エラーハンドリングとリソース解放のデストラクタ ===
Private Sub Class_Terminate()
‘ クラスが破棄される際に、未完了の進捗表示をクリーンアップ
If Not m_frmProgress Is Nothing Then
On Error Resume Next ‘ フォームが既に閉じられている可能性を考慮
DoCmd.Close acForm, PROGRESS_FORM_NAME, acSaveNo
On Error GoTo 0
Set m_frmProgress = Nothing
End If
If m_booUseStatusBar Then
Application.SysCmd acSysCmdSetStatus, “” ‘ ステータスバーをクリア
End If
End Sub

`CProgressIndicator`クラスのポイント:

  • カプセル化: 進捗表示に必要なすべてのロジック(計算、UI更新、フォーム操作)をこのクラス内に閉じ込めています。
  • 初期化メソッド `Initialize`: 処理のタイトル、総ステップ数、更新頻度、表示方法(ステータスバー/フォーム)を設定します。ここで`MoveLast`による`RecordCount`取得のコストは、このクラスを使用する呼び出し元で既に発生していることを前提とします。
  • 更新メソッド `UpdateProgress`: `AbsolutePosition`の値を直接渡すことで、現在位置を更新します。`m_lngUpdateInterval`に基づいて、頻繁すぎるUI更新を抑制し、`DoEvents`の呼び出し回数も最適化しています。
  • 終了メソッド `Finish`: 処理完了時に進捗表示を閉じ、リソースを解放します。
  • `Class_Terminate`: クラスインスタンスが破棄される際に自動的に呼び出され、確実にフォームを閉じたり、ステータスバーをクリアしたりする堅牢なリソース解放を実現します。

4.2. プログレスバーフォーム `frmProgressIndicator` の作成

このクラスで利用するカスタムフォーム `frmProgressIndicator` を作成します。

1. 新しいフォームをデザインビューで作成します。
2. フォームの`名前`プロパティを `frmProgressIndicator` に設定します。
3. 以下のコントロールを配置し、それぞれの`名前`プロパティを設定します。

  • `ラベル`: `lblTitle` (処理タイトルを表示)
  • `プログレスバー`: `prgBar` (ツールボックスの「Microsoft Progress Bar Control X.0」を使用)
  • `ラベル`: `lblPercent` (パーセンテージを表示)
  • `ラベル`: `lblStatus` (現在のステータス詳細を表示)

4. フォームのプロパティで、以下を設定すると良いでしょう。

  • `ポップアップ`: はい
  • `モーダル`: いいえ (処理中に他の操作を許可する場合。進捗表示はモーダルでない方が良いことが多い)
  • `レコードセレクタ`: いいえ
  • `移動ボタン`: いいえ
  • `スクロールバー`: なし
  • `閉じるボタン`: いいえ (ユーザーに強制終了させないため)
  • `コントロールボックス`: いいえ
  • `最小化/最大化ボタン`: なし
  • `自動サイズ調整`: はい
  • `自動中央寄せ`: はい

4.3. `CProgressIndicator` の使用例

標準モジュール (`Module1`など) に以下のプロシージャを作成します。

‘ 標準モジュール名: Module1
Option Compare Database
Option Explicit

Public Sub ProcessLargeRecordsWithProgress()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim prg As CProgressIndicator ‘ 進捗表示クラスのインスタンス
Dim lngTotalRecords As Long
Dim strTableName As String
Dim i As Long ‘ ループカウンタ (AbsolutePositionの代わりとしても使う)

strTableName = “YourTableName” ‘ ★処理対象のテーブル名に置き換える

Set db = CurrentDb

On Error GoTo ErrorHandler

‘ 1. レコードセットを開く
‘ 読み取り専用で十分なら dbOpenSnapshot を使うとパフォーマンスが良い
Set rs = db.OpenRecordset(strTableName, dbOpenDynaset)

With rs
If .EOF And .BOF Then ‘ レコードが一件もない場合
MsgBox “処理対象のレコードがありません。”, vbInformation
GoTo CleanUp
End If

‘ 2. 全レコード数を取得 (初回のみコストがかかるが、ProgressIndicatorの初期化に必要)
.MoveLast
lngTotalRecords = .RecordCount
.MoveFirst

‘ 3. 進捗表示クラスのインスタンス化と初期化
‘ タイトル: “データ処理中”
‘ 総ステップ数: lngTotalRecords
‘ 更新間隔: 100レコードごとに更新
‘ ステータスバー表示: True
‘ フォーム表示: True
Set prg = New CProgressIndicator
prg.Initialize _
strTitle:=”大量データ処理中”, _
lngTotalSteps:=lngTotalRecords, _
lngUpdateInterval:=50, _
booUseStatusBar:=True, _
booUseForm:=True ‘ カスタムフォームを使用

‘ 4. レコードをループ処理
i = 0
Do While Not .EOF
‘ ここに実際のデータ処理ロジックを記述します
‘ 例: フィールドの値を更新する
‘ .Edit
‘ !SomeField = “Processed Value ” & i
‘ .Update

‘ シミュレーションのために少しだけ処理を遅延させる
‘ (実際の業務では不要。複雑な処理自体が遅延要因)
‘ Application.Wait Now + TimeValue(“00:00:00”) ‘ 非常に短い時間

‘ 5. 進捗の更新 (AbsolutePosition + 1 を渡す)
‘ AbsolutePositionは0ベースなので、ユーザー表示や進捗率計算のために +1 する
prg.UpdateProgress i + 1 ‘ AbsolutePositionはEOF時に-1を返すため、iを使用する方が安全

.MoveNext ‘ 次のレコードへ移動
i = i + 1 ‘ ループカウンタをインクリメント

‘ ユーザーがキャンセルできるようなロジックを追加することも可能
‘ If prg.IsCanceled Then Exit Do
Loop

‘ 6. 処理完了
prg.Finish
End With

MsgBox “全レコードの処理が完了しました。”, vbInformation

CleanUp:
‘ 重要なリソース解放
If Not prg Is Nothing Then
prg.Finish ‘ 念のため、正常終了時もFinishを呼び出す
Set prg = Nothing
End If
If Not rs Is Nothing Then
If rs.State = adStateOpen Then rs.Close ‘ ADOの場合
If rs.Updatable = True Then rs.Close ‘ DAOの場合
Set rs = Nothing
End If
If Not db Is Nothing Then Set db = Nothing

Exit Sub

ErrorHandler:
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
‘ エラー時もリソース解放処理へ
Resume CleanUp
End Sub

使用例のポイント:

  • `AbsolutePosition`の活用: ループカウンタ `i` を`prg.UpdateProgress`に渡すことで、`AbsolutePosition`が-1を返す可能性のあるEOF状態でも安全に進捗を更新できます。`AbsolutePosition`は0ベースなので、`i + 1`として渡すことで、ユーザーには1から始まる進捗として表示されます。
  • リソースの確実な解放: `On Error GoTo ErrorHandler` と `CleanUp` ラベルにより、エラーが発生しても、`Recordset`や`Database`オブジェクト、そして`CProgressIndicator`インスタンスが確実にクローズされ、解放されるように設計されています。これは堅牢なアプリケーション開発における最も基本的ながら、最も重要な原則です。
  • データベース連携の注意点:
  • トランザクション: 複数の更新処理を原子的に行いたい場合は、`db.BeginTrans` / `db.CommitTrans` / `db.Rollback` を適切に使用してください。進捗表示はトランザクションの外側で行うか、トランザクションの進捗を管理する別の仕組みが必要です。
  • 排他制御: ネットワーク上の共有データベースを扱う場合、`dbOpenDynaset`は共有ロックをかける可能性があります。`dbOpenSnapshot`であれば読み取り専用なので排他制御の問題は生じにくいですが、更新を伴う場合は排他制御の設計を熟慮する必要があります。

5. パフォーマンスとスケーラビリティへの配慮

今回紹介した進捗表示は、Accessのローカルデータベース環境では非常に効果的です。しかし、さらに大規模なデータセットや、SQL Serverなどの外部データベースに接続する場合、以下の点を考慮する必要があります。

  • `RecordCount`のコスト: 外部データベースでは`MoveLast`による`RecordCount`の取得は、ネットワーク負荷とサーバー負荷が大きくなる可能性があります。場合によっては、事前に`SELECT COUNT()`クエリを発行して総件数を取得する方が効率的です。ただし、この場合、COUNTクエリと実処理のクエリの間でデータが変更される可能性も考慮する必要があります。
  • `DoEvents`の頻度: `CProgressIndicator`クラスでは`m_lngUpdateInterval`で更新頻度を調整しています。処理が非常に高速な場合や、非常に低速な場合に応じて、この値を最適化してください。処理が遅すぎる場合は更新頻度を上げる(間隔を狭める)、処理が速すぎる場合は更新頻度を下げる(間隔を広げる)ことで、`DoEvents`のオーバーヘッドを最小限に抑えられます。
  • プログレスバーの更新自体がボトルネックになる場合: 極端なケースでは、UIの更新処理そのものがボトルネックとなることがあります。その場合は、進捗表示の粒度を粗くする(例:1%更新ごとに表示するのではなく、5%更新ごとにする)、あるいは表示内容を簡素化するなどの工夫が必要です。
  • 非同期処理の検討: VBA単体では真の非同期処理は困難ですが、外部DLLやCOMコンポーネント、またはADOとイベントハンドリングを組み合わせることで、バックグラウンド処理を実現し、UIフリーズを完全に回避する高度な設計も可能です。しかし、これはAccess VBAの範疇を大きく超えるため、今回は割愛します。

6. まとめ:極限の知見を実務へ

`DAO.Recordset.AbsolutePosition`は、Access VBAにおける進捗表示を堅牢かつ効率的に実装するための強力なツールです。しかし、その真価を発揮させるためには、`RecordCount`の特性、`DoEvents`の適切な利用、そして徹底したエラーハンドリングとリソース解放という、VBA開発の基本にして奥義を深く理解している必要があります。

この記事で紹介した`CProgressIndicator`クラスは、これらの知見を結集したプロダクションコードのテンプレートです。これをベースに、あなたの業務システムに息吹を与え、ユーザーに安心と信頼を提供する「使える」ツールへと昇華させてください。

単なるコードのコピペに終わらず、「なぜこの設計なのか」「なぜこの実装が必要なのか」という背景にある設計思想を理解し、あなたのシステムを次のレベルへと引き上げることを願っています。これこそが、業務自動化エンジニアとしてあるべき「極限の知見」です。

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