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構造体には、実はpvReserved・dwReserved・FlagsExという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) = 120、LenB(ofn) = 136。理論値の88とも一致せず、LenとLenBの間でも16バイトの差がありました。ここで「もしかしたらLenBの値が正しいのでは」と考え、.lStructSize = LenB(ofn)を試したところ、Accessごとクラッシュしました。

VBAのUDT内で可変長String型メンバーを扱う場合、実際のメモリ上のレイアウトが、Len・LenBが計算する値のどちらとも、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型メンバーの扱いが原因で、Len・LenBのどちらも正しい構造体サイズを返しませんでした。固定長文字列に変更するなど別のアプローチも考えられますが、そこまですると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
スポンサーリンク

シリーズ記事一覧

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


GPSロガーのCSVを地図に表示する方法|Pico 2 WでGT-502MGG-NとATGM336Hを車に積んで走行・トンネル比較【Python・folium】
GPSロガーで記録したCSVを、Pythonのfoliumと地理院タイルで地図に表示する方法。GT-502MGG-NとATGM336Hを車に積んで4回走り、再起動の原因やトンネルでの違いも比べました。
今週の注目ガジェット〜10/19週〜|電子工作・自作向け新製品まとめ(Romu/W1700K SDR ほか)
大手が拾わないニッチな新製品を毎週チェック。今週はe-Paper開発キット、超小型USBボード、自作SDRなど5本立て。
GT-502MGG-NとATGM336Hの使い方|Raspberry Pi Pico 2 WとMicroPythonでGPSを受信して2台を比較
GT-502MGG-NとATGM336Hの使い方を、Pico 2 W×MicroPythonで解説。北斗で衛星数が多いATGM336H、信号が強いGT-502MGG-N。単精度floatで緯度経度が約1.4m刻みになる落とし穴も紹介します。
今週の注目ガジェット〜10/12週〜|電子工作・自作向け新製品まとめ(Q8botOne/Sipeed SLogic32U3 ほか)
大手が拾わないニッチな新製品を毎週チェック。今週は四足歩行ロボット、ロジックアナライザー、広告ブロッカーなど5本立て。
MOSFETはPicoの3.3Vで足りる?DCソレノイド駆動でゲート電圧不足を実測【Pico 2 W】
Pico 2 WとMOSFET(IRLU3410)で12VのDCソレノイドを駆動。ゲート3.3Vでは発振し、オン抵抗も約2.4倍に。レベル変換で5V駆動にした比較と、電流が8割止まりだった本当の原因を実測で紹介します。
まとめハブ
vba
スポンサーリンク

コメント