【テクニカル・上級編】DAO.RecordsetのCloneとBookmarkを活用した、フォームの表示位置を維持したデータ更新 – Access VBA解析バイブル

スポンサーリンク

1. 序論:Accessにおける「画面のちらつき」と「スクロール飛び」の本質

MS Accessを単なる「簡易データベース」として切り捨てるのは、Accessの内部アーキテクチャとWindowsメッセージループの同期メカニズムを理解していない者の過ちです。堅牢に構築されたAccessシステムは、大規模な基幹システムの後続UIや、現場主導の超高応答性フロントエンドとして、他の追随を許さない生産性を発揮します。

しかし、現場のシニアエンジニアを悩ませ続ける定番の病癖が存在します。データ更新(`Requery` やレコードセットの操作)に伴う「フォームのスクロール位置リセット」および「フォーカス行の失踪」です。

更新のたびに画面が最上部へ跳ね戻り、ユーザーが視界を見失う——この不快なUI挙動は、システムの品質評価を著しく低下させます。

多くの開発者は、主キーを保持して `DoCmd.Requery` を実行した後に `FindFirst` で再検索する古典的アプローチをとります。しかし、この方法は以下の致命的な問題を抱えています。

1. クエリの完全再実行コスト:ネットワークI/OとデータベースエンジンのCPUサイクルを浪費する。
2. スクロール位置の破綻:選択行の位置は復元できても、画面内の相対位置(ビューポート内の表示位置)がズレる。
3. GDI描画の激しいちらつき:Windowsの描画パイプラインが強制的に再描画を起こし、画面が明滅する。

本記事では、`DAO.Recordset.Clone``Bookmark`、そしてWin32 APIによるウィンドウ描画制御を組み合わせ、フォームのスクロール位置とフォーカス状態を「完全に静止させたまま」データをアトミックに更新する極限のテクニックを解説します。

2. アーキテクチャの解剖:RecordsetCloneとBookmarkの内部構造

なぜ `Me.Recordset` ではなく `Me.RecordsetClone` を使用するのか。その本質はメモリ構造とポインタの独立性にあります。

[ Form Buffer (UI Thread) ] ── (Synchronized) ──> [ Me.Recordset ]

(Independent Pointer)


[ Me.RecordsetClone ]

`Me.Recordset` と `Me.RecordsetClone` の決定的な違い

  • `Me.Recordset`:フォームのUI(コントロール、スクロールバー、フォーカス行)と直接バインドされているライブポインタ。これを操作すると、ユーザーの画面上のカレントレコードが即座に移動し、UI描画イベントが発生します。
  • `Me.RecordsetClone`:フォームが内部的に保持しているデータキャッシュ(レコードバッファ)への別タスク用ポインタです。`RecordsetClone` のカレントレコード位置をどれほど高速に移動・操作しても、フォームのUI描画やユーザーのフォーカス位置には1ミリ秒の影響も与えません

`Bookmark`(ブックマーク)の物理的実体

DAOにおける `Bookmark` は、レコードセット内の物理的な行を指し示すバイナリトークン(Byte配列)です。単なる「行番号(Index)」ではありません。

`Me.Bookmark = rsClone.Bookmark` という代入文を実行した瞬間、Accessのフォームエンジンはクエリを再検索することなく、メモリ上の同一バッファ内にある該当アドレスへUIのポインタをダイレクトに再接続します。これが、インデックス検索よりも圧倒的に高速に動作する理由です。

3. レガシーを超越する Win32 API 描画パイプライン制御

VBAの `Application.Echo False` だけでは、Accessのフォーム領域(HWND)に対するWindowsネイティブの再描画メッセージ(`WM_PAINT` / `WM_ERASEBKGND`)を完全に抑え込むことはできません。完全に画面のチラつき(Flicker)を抹消するには、Windows APIを通じて該当フォームのHWNDに直接 `WM_SETREDRAW` を打ち込む必要があります。

採用する Win32 API

  • `SendMessage` (WM_SETREDRAW):指定したウィンドウ(HWND)とその子コントロールの描画フラグを物理レベルでON/OFFします。
  • `RedrawWindow`:描画フラグをONに戻した際、蓄積された非描画領域(Invalid Region)を一括で再描画・更新させます。

4. 極限の完全同期更新コードの実装

以下に、シニアエンジニアがそのまま既存のエンタープライズシステムへ組み込める、プロダクションレベルの標準VBAモジュールを示します。

64ビット(VBA7)および32ビット(レガシーVBA6)の両環境に完全互換(`PtrSafe` 対応)し、エラーハンドリングとメモリ解放処理を完備しています。

標準モジュール:`mod_UI_StateKeeper.bas`

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ Win32 API 宣言 (64bit / 32bit 双方に対応)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function SendMessage Lib “user32” Alias “SendMessageA” ( _
ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal wParam As LongPtr, _
ByRef lParam As Any) As LongPtr

Private Declare PtrSafe Function RedrawWindow Lib “user32” ( _
ByVal hwnd As LongPtr, _
ByVal lprcUpdate As LongPtr, _
ByVal hrgnUpdate As LongPtr, _
ByVal flags As Long) As Long
Else
Private Declare Function SendMessage Lib “user32” Alias “SendMessageA” ( _
ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
ByRef lParam As Any) As Long

Private Declare Function RedrawWindow Lib “user32” ( _
ByVal hwnd As Long, _
ByVal lprcUpdate As Long, _
ByVal hrgnUpdate As Long, _
ByVal flags As Long) As Long
End If

‘ Windows メッセージ定数
Private Const WM_SETREDRAW As Long = &HB
Private Const RDW_INVALIDATE As Long = &H1
Private Const RDW_UPDATENOW As Long = &H100
Private Const RDW_ALLCHILDREN As Long = &H80

‘ ==============================================================================
‘ 機能 : フォームの表示位置・選択行を完全保持した状態でレコード更新を行う
‘ 引数 : frm – 対象となるAccessフォームオブジェクト
‘ strPKField – 一意にレコードを特定する主キーフィールド名
‘ pfnUpdateProc – データ更新ロジックを実装したコールバック関数名 (Optional)
‘ ==============================================================================
Public Sub ExecuteSilentUpdate(ByRef frm As Access.Form, ByVal strPKField As String)
On Error GoTo ErrorHandler

Dim rsClone As DAO.Recordset2
Dim savedPKValue As Variant
Dim savedBookmark As Variant
Dim frmHwnd As LongPtr

‘ 1. フォームのHWND取得と描画フリーズ (Win32 API)
frmHwnd = frm.hwnd
Call SendMessage(frmHwnd, WM_SETREDRAW, 0, ByVal 0&) ‘ 描画停止

‘ 2. カレント状態の無破壊退避
If Not (frm.NewRecord Or frm.Recordset.BOF And frm.Recordset.EOF) Then
‘ 主キー値とBookmarkの両方を退避(構造変更時のフェイルセーフ)
savedPKValue = frm.Controls(strPKField).Value
savedBookmark = frm.Bookmark
End If

‘ 3. RecordsetCloneを用いたデータ操作バッファの確保
‘ ※Me.RecordsetClone は既存バッファの参照を返すため開く負荷はゼロ
Set rsClone = frm.RecordsetClone

‘ ──────────────────────────────────────────────────────────
‘ 4. データ更新処理(例: メモリバッファに対する直接操作またはRequery)
‘ ここでは画面をリセットさせる Me.Requery または Me.Refresh をシミュレート
‘ ──────────────────────────────────────────────────────────

‘ DBエンジン層の変更をフォームバッファに反映
frm.Refresh

‘ ──────────────────────────────────────────────────────────
‘ 5. 位置の完全復元ロジック
‘ ──────────────────────────────────────────────────────────
If Not IsEmpty(savedPKValue) Then
‘ まず高速な Bookmark で同期を試みる
On Error Resume Next
frm.Bookmark = savedBookmark

‘ Bookmarkが失効している(Requery等が行われた)場合は主キーで高速シーク
If Err.Number <> 0 Then
Err.Clear
On Error GoTo ErrorHandler

‘ Clone側で対象行を走査(UIは動かない)
Dim strCriteria As String
If VarType(savedPKValue) = vbString Then
strCriteria = “[” & strPKField & “] = ‘” & Replace(savedPKValue, “‘”, “””) & “‘”
Else
strCriteria = “[” & strPKField & “] = ” & savedPKValue
End If

rsClone.FindFirst strCriteria
If Not rsClone.NoMatch Then
‘ Cloneで見つけた絶対ポインタをフォームに即時同期
frm.Bookmark = rsClone.Bookmark
End If
Else
On Error GoTo ErrorHandler
End If
End If

CleanExit:
‘ 6. オブジェクトライフサイクルの厳格な明示的解放(メモリリーク防止)
Set rsClone = Nothing

‘ 7. Win32 API 描画解除と完全な再描画命令
Call SendMessage(frmHwnd, WM_SETREDRAW, 1, ByVal 0&)
Call RedrawWindow(frmHwnd, 0, 0, RDW_INVALIDATE Or RDW_UPDATENOW Or RDW_ALLCHILDREN)
Exit Sub

ErrorHandler:
‘ エラー時も確実に描画ロックを解除して落ちる(フリーズ防止)
Call SendMessage(frmHwnd, WM_SETREDRAW, 1, ByVal 0&)
Call RedrawWindow(frmHwnd, 0, 0, RDW_INVALIDATE Or RDW_UPDATENOW Or RDW_ALLCHILDREN)

MsgBox “更新処理中に例外が発生しました: ” & Err.Description, vbCritical, “System Architecture Error”
Resume CleanExit
End Sub

5. 技術の極意:Requery と Refresh の運用の明暗

現場の保守で最も混同されるのが `Me.Requery``Me.Refresh` の選定です。この違いを理解せずに `Bookmark` を使用すると、予期せぬ例外(エラー3159:無効なブックマーク)が発生します。

| 操作メソッド | データベース挙動 | Bookmarkの有効性 | パフォーマンス特性 | 適用ユースケース |
| :— | :— | :— | :— | :— |
| `Me.Refresh` | 現在保持している行のフィールド値のみを再読み込み | 完全に維持される | 極めて高速 (低I/O) | 既存レコードの変更(値の書き換え) |
| `Me.Requery` | クエリ全体を再実行し、行構成(追加・削除)を再構築 | 即座に破壊(失効) | 重い (高I/O) | 他ユーザーによる行追加・削除の反映 |

比較コード(呼び出し側フォームの実装例)

Private Sub btnUpdateRow_Click()
‘ 単独行の変更後、UIの位置を一切動かさずに画面更新する場合
‘ 主キーフィールド名を指定して実行
Call ExecuteSilentUpdate(Me, “ID”)
End Sub

`ExecuteSilentUpdate` 内では、万が一 `Requery` により `Bookmark` が無効化(エラー発生)した場合でも、自動的にフォールバック処理へ遷移します。

`RecordsetClone.FindFirst` によってメモリ内で該当行をシークし、新しく生成された `Bookmark` をフォームへアサインするため、どのようなデータ更新操作であってもUI位置の崩れを防止可能です。

6. メモリ最適化とオブジェクトライフサイクルの厳格な管理

Access VBAが長時間の連続運用(社内システムの終日稼働など)で「リソース不足」や「メモリリーク」を引き起こす最大の原因は、DAOオブジェクトの参照カウンタの解放漏れです。

1. RecordsetCloneの所有権

`Set rsClone = Me.RecordsetClone` で取得したオブジェクトは、フォームの内部レコードセットへの参照を共有しています。

そのため、`rsClone.Close` を呼び出す必要はありません(呼び出すと内部バッファの状態に影響を与える、またはエラーとなる場合があります)。

しかし、変数 `rsClone` そのものが保持している参照カウントを減らすため、`Set rsClone = Nothing` の明示的実行は絶対条件です。

2. HWND(ウィンドウハンドル)のスコープライフサイクル

`SendMessage(frmHwnd, WM_SETREDRAW, 0, …)` を呼び出した後、万が一VBAの未処理エラー(Unhandled Exception)によってコードが途中停止すると、Accessのフォーム全体の描画が永久に停止し、アプリケーションがフリーズしたように見えます。

これを回避するため、本実装のように `On Error GoTo ErrorHandler` の例外トラップを用意し、`ErrorHandler` ブロック内でも必ず `WM_SETREDRAW = 1` を投げて描画を復旧させるコードを組むことが、ミッションクリティカルな保守における鉄則です。

7. 結論:レガシーを「洗練された資産」に変える設計思想

MS Accessにおける画面位置の維持やチラつきの防止は、単なる見た目の調整(グラフィック・シュガー)ではありません。システムに対するエンドユーザーの信頼感を決定づける、重要な非機能要件です。

1. `RecordsetClone` による非破壊的なメモリアドレスの走査
2. `Bookmark` 代入による瞬時のUIポインタ再接続
3. `WM_SETREDRAW` によるWindows描画メッセージの直接制御

これら3つの要素を正しく組み合わせることで、数十万件のレコードを抱えるレガシーなAccessアプリケーションであっても、モダンなWeb SPA(Single Page Application)のような、ぬるぬると快適に動く極上のユーザー体験を提供できます。

長年運用されてきたシステムを安易に捨て去るのではなく、アーキテクチャの真理に基づいたコードへのリファクタリングによって、現場の業務効率を極限まで引き上げてください。

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