PR

Access VBA GetOpenFileName実装でAccessがクラッシュした話【Win32 API構造体サイズエラー】

vba

FREE ACCESS TEMPLATE VOL.3

スポンサーリンク

Access VBA GetOpenFileName実装でAccessがクラッシュした話【Win32 API構造体サイズエラー】

こんにちは。ニンジンです🥕

「対応履歴管理データベース」シリーズも今回で最終回です。前回、Excel出力機能を紹介した際に少し触れましたが、このテンプレートのファイル選択ダイアログには、実はWizHook.GetFileNameという非公式・非サポートのAPIを使っています。

「非公式なAPIをテンプレートに残すのはさすがにどうなんだ」と思い、Windows標準のGetOpenFileNameW API(Win32 API)を直接呼び出す、より「モダン」な実装に置き換えようとしました。結論から言うと、この挑戦は失敗し、最終的にAccessごとクラッシュする事態にまでなりました。今回は、その一部始終と、そこから得た教訓を紹介します。

この記事で分かること

  • なぜAccess VBAには「標準のファイル選択ダイアログ」がないのか
  • Win32 APIをVBAから呼び出す際の構造体サイズエラーの正体
  • VBAのUDT(ユーザー定義型)でハマりやすい落とし穴
  • 生成AIが提案する「モダンな書き方」を鵜呑みにしてはいけない理由

スポンサーリンク

なぜ非公式APIを使う必要があったのか

Word・Excel・PowerPointのApplicationオブジェクトにはFileDialogプロパティがあり、これを使えば数行でファイル選択ダイアログを表示できます。ところが、AccessのApplicationオブジェクトにはこのプロパティが存在しません。

これは実際に今回、最初につまずいたポイントでもあります。「Officeの標準機能だから使えるはず」と思ってApplication.FileDialog(msoFileDialogFilePicker)を試したところ、そもそもAccessにはこのプロパティ自体がなく、コンパイルエラーになりました。

Accessでファイル選択ダイアログを実装する方法は、実質的に次の2択しかありません。

  • 非公式のWizHookオブジェクトを使う(今回のテンプレートの元の実装)
  • comdlg32.dllのWin32 API(GetOpenFileNameW)を直接呼び出す

「非公式のAPIより、Windows標準のAPIを直接呼ぶ方が正当だろう」と考え、後者に挑戦することにしました。


スポンサーリンク

Win32 API(GetOpenFileNameW)への挑戦

Win32 APIを呼び出すには、OPENFILENAMEという構造体をVBAのユーザー定義型(UDT)として定義し、APIにポインタとして渡す必要があります。

Private Type OPENFILENAME
    lStructSize As Long
    hwndOwner As Long
    hInstance As Long
    lpstrFilter As String
    lpstrCustomFilter As String
    nMaxCustFilter As Long
    nFilterIndex As Long
    lpstrFile As String
    nMaxFile As Long
    lpstrFileTitle As String
    nMaxFileTitle As Long
    lpstrInitialDir As String
    lpstrTitle As String
    flags As Long
    nFileOffset As Integer
    nFileExtension As Integer
    lpstrDefExt As String
    lCustData As Long
    lpfnHook As Long
    lpTemplateName As String
End Type

Private Declare PtrSafe Function GetOpenFileNameW Lib "comdlg32.dll" (pOpenfilename As OPENFILENAME) As Long

呼び出し側はこのようになります。

Dim ofn As OPENFILENAME
Dim strFile As String
strFile = String(260, vbNullChar)

With ofn
    .lStructSize = Len(ofn)
    .hwndOwner = Application.hWndAccessApp
    .lpstrFile = strFile
    .nMaxFile = Len(strFile)
    .flags = OFN_FILEMUSTEXIST Or OFN_PATHMUSTEXIST
End With

If GetOpenFileNameW(ofn) <> 0 Then
    PickFile = Left(ofn.lpstrFile, InStr(ofn.lpstrFile, vbNullChar) - 1)
End If

一見、教科書的には正しく見えるコードです。コンパイルも通りました。ところが、実際にボタンを押してファイルを選んでも、常に「キャンセルされました」という結果になってしまいました。


スポンサーリンク

構造体サイズエラーとの格闘

GetOpenFileNameWが失敗した理由を調べるため、CommDlgExtendedErrorというAPIでエラーコードを取得したところ、CDERR_STRUCTSIZE(構造体サイズ不正)でした。

Private Declare PtrSafe Function CommDlgExtendedError Lib "comdlg32.dll" () As Long

原因を調べていくと、Windows 2000以降のOPENFILENAME構造体には、実はpvReserveddwReservedFlagsExという3つの追加メンバーがあることが分かりました。これを構造体定義に加え、.lStructSize = Len(ofn)のまま再挑戦しましたが、それでも同じエラーです。

そこで、正しいはずのバイトサイズ(88バイト)を計算し、直接指定する方法に切り替えました。

Private Const OFN_STRUCT_SIZE As Long = 88
'...
.lStructSize = OFN_STRUCT_SIZE

これも失敗。ここで、VBAのLen(ofn)LenB(ofn)がそれぞれ何バイトを返しているのか確認してみることにしました。

MsgBox "Len(ofn) = " & Len(ofn) & vbCrLf & "LenB(ofn) = " & LenB(ofn)

結果はLen(ofn) = 120LenB(ofn) = 136。理論値の88とも一致せず、LenLenBの間でも16バイトの差がありました。ここで「もしかしたらLenBの値が正しいのでは」と考え、.lStructSize = LenB(ofn)を試したところ、Accessごとクラッシュしました。

VBAのUDT内で可変長String型メンバーを扱う場合、実際のメモリ上のレイアウトが、LenLenBが計算する値のどちらとも、Win32 APIが期待するC言語の構造体レイアウトとも一致しない場合がある、ということを身をもって確認する結果になりました。


スポンサーリンク

安定性を取って元の方式に戻した理由

ここまでで、Win32 API直接呼び出し方式は次の3つの結果を出しています。

  • 構造体サイズエラーで動作しない
  • 固定値を指定しても動作しない
  • 別の値を試したらAccessごとクラッシュ

これ以上原因を追求することも不可能ではありませんが、「非公式だが20年以上の実績があるWizHookを使い続ける」のと「正しく動くかどうか分からないWin32 API実装を追求し続ける」のとでは、配布用テンプレートとしての安定性は前者に軍配が上がると判断しました。最終的に、ファイル選択ダイアログの実装は元のWizHook.GetFileNameに戻しています。

Public Function PickFile(Optional ByVal InitialDir As String = "", Optional ByVal DlgTitle As String = "ファイルの指定") As String

    Const ENABLE_WIZHOOK As Long = 51488399
    Const DISABLE_WIZHOOK As Long = 0

    Dim strFile As String
    WizHook.Key = ENABLE_WIZHOOK

    Dim lngResult As Long
    lngResult = WizHook.GetFileName( _
                    Application.hWndAccessApp, _
                    Application.Name, _
                    DlgTitle, _
                    DlgTitle, _
                    strFile, _
                    InitialDir, _
                    "すべてのファイル (*.*)|*.*", _
                    1, 0, 0, -1)

    WizHook.Key = DISABLE_WIZHOOK
    PickFile = strFile

End Function

「非公式だから」という理由だけで安易に採用・却下を決めず、実際に動かして比較したうえで判断する。当たり前のようですが、今回はまさにそれが必要になった場面でした。


スポンサーリンク

よくある質問

QなぜAccess VBAには標準のファイル選択ダイアログがないのですか?

AWord・Excel・PowerPointのApplicationオブジェクトにはFileDialogプロパティがありますが、AccessのApplicationオブジェクトにはこのプロパティ自体が存在しません。Microsoftの設計上の制約で、Accessだけこの機能が用意されていないのが実情です。

QCDERR_STRUCTSIZEエラーはどうすれば解決できますか?

A今回のケースでは、UDT内の可変長String型メンバーの扱いが原因で、LenLenBのどちらも正しい構造体サイズを返しませんでした。固定長文字列に変更するなど別のアプローチも考えられますが、そこまでするとAPIが期待する「文字列へのポインタ」という前提と食い違う可能性があり、今回はリスクを避けて非公式API(WizHook)に戻す判断をしました。

QWin32 APIをVBAから呼び出すこと自体が危険なのですか?

A正しく実装すれば問題なく動作します。ただし構造体のメモリレイアウトを誤ると、今回のようにアプリケーションごとクラッシュする可能性があります。特にUDT内に文字列型のメンバーが含まれる構造体は要注意です。

QWizHookは今後も使い続けて大丈夫ですか?

A非公式・非サポートのAPIなので、将来のOfficeアップデートで動作しなくなるリスクはゼロではありません。ただし20年以上にわたって多くのAccess開発者に使われてきた実績があり、今回のように正式な代替手段の実装が難航した以上、実用上は許容範囲と判断しました。

Q生成AIにこの実装を任せるとどうなりますか?

AAIも「モダンな書き方」としてWin32 API直接呼び出しを提案してくることがあります。ですが今回のように、コンパイルは通っても実行時に構造体サイズエラーになる、というパターンは実際に動かしてみないと分かりません。AIの提案はあくまで叩き台として受け取り、必ず動作検証する姿勢が欠かせません。


スポンサーリンク

まとめ

「対応履歴管理データベース」シリーズは今回で完結です。

  • 第1回:テンプレートの配布と使い方
  • 第2回:検索フィルタ・排他制御・Excel出力の設計パターン
  • 第3回(今回):ファイル選択ダイアログの実装で味わった苦労話

生成AIによってVBAのコードそのものは誰でも書ける時代になりましたが、「そのコードが実際に動くかどうか」は、結局のところ自分の手で検証するしかありません。今回のシリーズが、AIと一緒にAccess VBAを書いていく際の参考になれば嬉しいです。



ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket

PROJECT LOG — SERIES INDEX
スポンサーリンク

シリーズ記事一覧

電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。


Tapo P110Mの消費電力をExcelレポート化する【年次集計・電気代試算・セキュリティ判断編】
FastAPI+openpyxlでTapo P110Mの消費電力をExcelレポート化。15秒間隔の生ログを日次集計する設計や、31円/kWhでの電気代試算、外部公開を見送った理由まで解説します。
Tapo P110M × FastAPIで電力監視ダッシュボードを作る【SQLite記録・リアルタイム堅牢化編】
Tapo P110MのデータをSQLiteに記録し、FastAPI+Uvicornでリアルタイム電力監視ダッシュボードを作る方法を解説。通信失敗時の自動復旧など堅牢化のポイントも紹介します。
Tapo P110M × python-kasaで自作PDUを作る【環境構築・Unsupported device(TPAP)エラー解決編】
Tapo P110Mをpython-kasaで自作PDU化。「Unsupported device」TPAPエラーの原因と対処法、MACアドレスでIP変動に強くする設計、個別/一括ON-OFFの実装コードまで解説します。
Raspberry Pi Pico Wで作る熱中症警戒アラート表示器
Raspberry Pi Pico WとDHT11、0.96インチOLEDで熱中症警戒アラート表示器を自作。気温・湿度から簡易的な暑さ指数を計算し、5段階の顔アイコンで危険度を表示する作り方をコード付きで解説します。
CustomTkinterでオリジナルのメニューバー(File・表示・設定・Help)を自作する
CustomTkinterには無いメニューバー(File・表示・設定・Help)を、CTkFrameとCTkButtonで自作する方法を、5段階のサンプルコードとダウンロード付きで解説します。
vba
スポンサーリンク

コメント