トップ回答者
Win32 API CHOOSEFONTについて

質問
-
標準モデュールにてパラメーターを設定し、ChooseFontを実行してもダイアログは表示されずにデバッグは次行に移行し予告なしでエクセルが終了してしまうのですが…
hMem = GlobalAlloc(GHND, Len(LF)) 'メモリブロックを確保してそのハンドルを取得
Address = GlobalLock(hMem) 'グローバルヒープに確保されたメモリブロックをロックする
If Address = 0 Then Exit Sub
CopyMemory Address, LF, Len(LF) 'メモリの領域をコピーする
With CF 'CHoosefont_B構造体の設定(Choosefont構造体の名前だけ変えたもの)
.lStructSize = Len(CF) '構造体のサイズを設定
.hwndOwner = Application.hWND 'ウインドウハンドルを設定
.lpLogFont = Address 'フォント情報の初期設定
.flags = CF_INITTOLOGFONTSTRUCT Or _
CF_SCREENFONTS Or _
CF_LIMITSIZE Or _
CF_EFFECTS 'フラグを設定
.rgbColors = aFontColor 'フォントのカラーを設定
.nSizeMin = FONT_SIZE_MIN '最小フォントサイズを設定(変更可能)
.nSizeMax = FONT_SIZE_MAX '最大フォントサイズを設定(変更可能)
End With
Ret = CHOOSEFONT(CF) 'フォント選定のコモンダイアログボックスを表示する
CopyMemory LF, ByVal Address, Len(LF) 'メモリの領域をコピーする(選択した情報)
LF(LOGFONT)の設定が、悪いのでしょうか???
回答
-
そうですね。LOGFONT のユーザー定義型が正しく宣言されているなら、ローカル変数 LF に対する「.lpLogFont = VarPtr(LF)」を指定しようと、API で確保したアドレスを「.lpLogFont = Address」で指定しようと、どちらでも良いと思います。
重要なのは、指定したポインターが読み書き可能なメモリを指しており、かつ、そのポインターが指し示す領域に十分なサイズを確保してあるどうかかと。
.
一番最初の質問時点では、CHOOSEFONT の nFontType 宣言漏れ問題もあって、.lStructSize = 90 によって「パラメーターが間違っています。」になったわけですよね(32bitなら「60」、64bitなら「104」とする必要があるはず)。
そこへ、さらにパディングの問題が加わっています。
Win64 環境においては「hMem = GlobalAlloc(GHND, Len(LF))」の時点で、90 バイトの領域しか確保されていなかったのでしょう(nFontType ありだったのなら 92 バイト)。
この時、64bit 版のユーザー定義型は 8 バイトのアライメントなので、実際には 104 バイトを必要としていたはずです(nFontType の有無によらず、 LenB(LF) は 104 を返す)。
LenB(LF) ではなく Len(LF) を用いていたことから、「Address = GlobalLock(hMem)」「CopyMemory LF, ByVal Address, Len(LF)」では、先頭から 90 バイトまでの 0x00~0x59 の範囲のみしか転写されていません。0x5A~0x67 のエリア(nSizeMin/nSizeMax)は漏れていたことでしょう。
0x00-0x03:lStructSize (中略) 0x50-0x57:lpszStyle 0x58-0x59:本来は nFontType の位置だが、宣言漏れ時は MISSING_ALIGNMENT
修正版では、明示的なパディングを入れるか、Long を LongPtr 化することで、Len(LF) でも 104 が返されるようにしたみたいですけれどね。- 回答としてマーク my_think 2022年11月12日 12:50
すべての返信
-
ちなみに CHOOSEFONTの値は下記です
: flags : 8513 : Long
: hdc : 0^ : LongLong
: hInstance : 0^ : LongLong
: hwndOwner : 330058^ : LongLong
: iPointSize : 0 : Long
: lCustData : 0^ : LongLong
: lpfnHook : 0^ : LongLong
: lpLogFont : 21^ : LongLong
: lpszStyle : "" : String
: lpTemplateName : "" : String
: lStructSize : 90 : Long
: MISSING_ALIGNMENT : 0 : Integer
: nSizeMax : 24 : Long
: nSizeMin : 6 : Long
: rgbColors : 0 : Long
-
それと、たとえ、エラーによりダイアログが開かなかったとしても、Copymemory LF, ByVal Address, Len(LF)自体は正常に実行されると思うのですが、ここでクラッシュしてしまうのは、よくわからない。
クラッシュに遭遇する箇所が悪いとは限りません。
スタック破壊やヒープ破壊の怖いところは、原因(コードの誤り)がある行はなんとなく実行された後、離れたタイミングで落ちる・不正動作を招き、原因の特定が難しくなることです。
たとえば、ChooseFont の戻り値を誤った型にしていた場合は、スタックズレが発生し、関数終了時に不正なアドレスを実行しようとして落ちると思います。 -
ご返答いただき誠にありがとうございます。
続報ですが、ローカルウィンドウにて、データを追っかけてみたところ、下記「>>」部分のCopyMemory
を実行した段階で、ローカル変数のLFのデータ内容が変わってしまうことに気がつきました。
この関数はロックされたメモリーにLFの内容をコピーしているだけだと思うのですが、LFが変わってしまうのは理解できません。
なぜでしょうか?
以上 よろしくおねがいします。
Option Explicit
'CONST
Public Const GMEM_MOVEABLE = &H2 '利用可能なメモリを確保
Public Const GMEM_ZEROINIT = &H40 '新しく確保するメモリブロックの内容を0で初期化
Public Const GHND = (GMEM_MOVEABLE Or GMEM_ZEROINIT)
Public Const FONT_SIZE_MIN = 6 '最小フォントサイズを設定(変更可能)
Public Const FONT_SIZE_MAX = 24 '最大フォントサイズを設定(変更可能)
Public Const ONE_CHAR = 1
Public Const POINTER_ZERO = 0
Private Const POINTS_IN_AN_INCH = 72
'API
Private Declare PtrSafe Function GetLastError _
Lib "kernel32" () As Long
Private Declare PtrSafe Function GlobalSize _
Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function CHOOSEFONT _
Lib "comdlg32.dll" Alias "ChooseFontA" (pChoosefont As CHOOSEFONT) As Long
Private Declare PtrSafe Function GlobalAlloc _
Lib "kernel32" (ByVal wFlags As Long, ByVal dwBytes As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalFree _
Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalLock _
Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalUnlock _
Lib "kernel32" (ByVal hMem As LongPtr) As Long
Private Declare PtrSafe Sub CopyMemory _
Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hdc As LongPtr) As Long
Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hdc As LongPtr, ByVal nIndex As Long) As Long
'使用する変数
Public nFontName As String 'フォントの名前
Public nFontSize As Long 'フォントのサイズ
Public nFontBold As Boolean 'フォントの太字
Public nFontItalic As Boolean 'フォントの斜体
Public nFontStrikethrough As Boolean 'フォントの取り消し線
Public nFontUnderline As Boolean 'フォント の下線
Public nColor As Long 'フォントのカラー値
'■関数名 ShowFontDialog
'■用途 フォント選定のコモンダイアログボックスを表示して選択した各種情報を取得する
'■引数
' nHwnd :ウインドウのハンドル
' FontName :フォント名
' FontColor :フォントのカラー値
' FontSize :フォントのサイズ
' FontBold :フォントに太字が付加されているか
' FontItalic :フォントに斜体が付加されているのか
' FontStrikethrough :フォントに取り消し線が付加されているか
' FontUnderline :フォントに下線が付加されているか
'■戻り値 OKボタンを押した時0,キャンセルボタンを押した時1
Public Function ShowFontDialog(ahWnd As LongPtr, _
aFontName As String, _
aFontColor As Long, _
aFontSize As Long, _
aFontBold As Boolean, _
aFontItalic As Boolean, _
aFontStrikethrough As Boolean, _
aFontUnderline As Boolean _
) As Long
Dim CF As CHOOSEFONT 'CHOOSEFONT構造体
Dim LF As LOGFONT 'LOGFONT構造体
Dim Address As LongPtr 'ポインタ
Dim hMem As LongPtr '戻り値(メモリブロックのハンドル)
Dim mRet As LongPtr '戻り値(GlobalUnlock、GlobalFreeで使用)
Dim Ret As Long '戻り値(CHOOSEFONTで使用)
Dim Tmp() As Byte
Dim HeightPix As Long
Dim DPI As Long
Dim hdc As LongPtr
Dim i As Long
hdc = GetDC(ahWnd)
DPI = GetDeviceCaps(hdc, DC_LOGPIXELSY) '画面のDPIを取得
ReleaseDC ahWnd, hdc
Tmp = StrConv(aFontName, vbFromUnicode) 'フォント名UnicodeからShift_Jisコードに変更
With LF
For i = LBound(Tmp) To UBound(Tmp)
.lfFaceName(i + 1) = Tmp(i)
Next 'フォント名を「Shift_Jis」コードで設定
.lfHeight = aFontSize / POINTS_IN_AN_INCH * DPI 'フォントのサイズを設定
.lfItalic = aFontItalic 'フォントの斜体情報を設定
.lfUnderline = aFontUnderline 'フォントの下線情報を設定
.lfStrikeOut = aFontStrikethrough 'フォントの取り消し線情報を設定
.lfWeight = aFontBold 'フォントの幅情報を設定
End With
hMem = GlobalAlloc(GHND, Len(LF)) 'メモリブロックを確保してそのハンドルを取得
Address = GlobalLock(hMem) 'グローバルヒープに確保されたメモリブロックをロックする
If Address = 0 Then Exit Sub
>> Call CopyMemory(ByVal Address, LF, Len(LF))
With CF
.lStructSize = Len(CF) '構造体のサイズを設定
.hwndOwner = ahWnd 'ウインドウハンドルを設定
.lpLogFont = Address 'フォント情報の初期設定
.lpLogFont = VarPtr(LF) 'フォント情報の初期設定
.flags = CF_INITTOLOGFONTSTRUCT Or _
CF_SCREENFONTS Or _
CF_LIMITSIZE Or _
CF_EFFECTS 'フラグを設定
.rgbColors = aFontColor 'フォントのカラーを設定
.nSizeMin = FONT_SIZE_MIN '最小フォントサイズを設定
.nSizeMax = FONT_SIZE_MAX '最大フォントサイズを設定
.iPointSize = 8
End With
Ret = CHOOSEFONT(CF) 'フォント選定のコモンダイアログボックスを表示する
CopyMemory LF, ByVal Address, Len(LF) 'メモリの領域をコピーする(選択した情報)
mRet = GlobalUnlock(hMem) 'メモリブロックのロックを解除する
mRet = GlobalFree(hMem) 'メモリブロックのロックを解放する
If Ret <> 0 Then
With LF
nFontName = StrConv(.lfFaceName, vbUnicode) 'フォントの名前のみ取得
nFontBold = IIf(.lfWeight = FW_NORMAL, False, True) 'フォントの幅情報を取得(lfWeightの値はTrue,Falseではないので注意)
nFontItalic = .lfItalic 'フォントの斜体情報を取得
nFontStrikethrough = .lfStrikeOut 'フォントの取り消し線情報を取得
nFontUnderline = .lfUnderline 'フォントの下線情報を取得
nFontSize = .lfHeight / DPI * POINTS_IN_AN_INCH 'フォントのサイズ情報を取得
End With
'フォントのカラー情報を取得
nColor = CF.rgbColors
Else
ShowFontDialog = 1
End If
End Function
-
ちなみに LOGFONT は Win32API_PtrSafe.TXTの定義を使っております。
(lfFaceName as string*LF_FACESIZE とする場合もあるようですが
Public Type LOGFONT
lfHeight As Long '8
lfWidth As Long '8
lfEscapement As Long '8
lfOrientation As Long '8
lfWeight As Long '8
lfItalic As Byte '1
lfUnderline As Byte '1
lfStrikeOut As Byte '1
lfCharSet As Byte '1
lfOutPrecision As Byte '1
lfClipPrecision As Byte '1
lfQuality As Byte '1
lfPitchAndFamily As Byte '1
lfFaceName(1 To LF_FACESIZE) As Byte '32
End Type
-
隅々までは読んでいません。
CHOOSEFONTA 構造体を C++ 環境でサイズを測ったところ、x64 で 104 バイト、x86 で 60 バイトですので、lStructureSize が 90 になるのはおかしいと思います。
現状の CopyMemory の定義でデータが壊れるとなると、VBA の実行環境がどうなっているかを気にしたくなります。
実行環境の Office は 64bit なのですか? 32bit なのですか?
(32bit だったとすると壊れて当然ですが…)- 編集済み AzuleanMVP 2022年11月7日 13:40
-
ちなみに CHOOSEFONTの値は下記です
: lStructSize : 90 : Long
90 バイトですか…? Win32 API の CHOOSEFONT 構造体を、VBA の CHOOSEFONT ユーザー定義型として再定義する際に、どのように宣言していますか?
<削除>この構造体は 32bit 環境ではシングルバイトアライメントですが、64bit 環境では 4 バイト境界に配置される仕様です。
そのため VBA の場合、このユーザー定義型は 32bit 版と 64bit 版で宣言が変化します。その結果、32bit 環境では 60 、64bit 環境では 104 というサイズになります。
</削除>
<訂正>CHOOSEFONTA/CHOOSEFONTW 構造体は、32bit ではシングルバイトアライメント(1 バイト単位のパッキング)で定義されています。64bit では既定のアライメント(8 バイト単位のパッキング)ですので、この構造体のサイズは 32bit では 60 バイト、64bit では 104 バイトとなるはずです。
それに対し、32bit 版の VBA におけるユーザー定義型は各メンバーが常に 32bit 境界に配置される仕様(4 バイト単位のパッキング)です。64bit 版 VBA のユーザー定義型であれば、64bit 境界に配置されます(8 バイト単位のパッキング)。
そのため VBA でユーザー定義型としてそのまま定義した場合(Win32API_PtrSafe.txt における Type CHOOSEFONT 定義をそのまま用いた場合)、32bit 環境では Len(CF)=LenB(CF)=60、64bit 環境では Len(CF)=92, LenB(CF)=104 というサイズになります。
いずれにしても「90 バイト」になっているのはおかしいです。
</訂正>64bit 版の場合、CHOOSEFONT の各メンバーはこのように配置されることになるはず。
0x00-0x03 : DWORD lStructSize; 0x04-0x07 : 《4 バイトのパディング》 0x08-0x0F : HWND hwndOwner; 0x10-0x17 : HDC hDC; 0x18-0x1F : LPLOGFONT lpLogFont; 0x20-0x23 : INT iPointSize; 0x24-0x27 : DWORD Flags; 0x28-0x2B : DWORD rgbColors; 0x2C-0x2F : 《4 バイトのパディング》 0x30-0x37 : LPARAM lCustData; 0x38-0x3F : LPCFHOOKPROC lpfnHook; 0x40-0x47 : LPTCSTR lpTemplateName; 0x48-0x4F : HINSTANCE hInstance; 0x50-0x57 : LPTSTR lpszStyle; 0x58-0x59 : WORD nFontType; 0x5A-0x5B : WORD ___MISSING_ALIGNMENT__; 0x5C-0x5F : INT nSizeMin; 0x60-0x63 : INT nSizeMax; 0x64-0x67 : 《4 バイトのパディング》
なお、CHOOSEFONT 構造体は、commdlg.h 上で下記のように定義されています。
// --- 略 --- #include <prsht.h> #if !defined(_WIN64) #include <pshpack1.h> /* Assume byte packing throughout */ #endif // --- 略 --- typedef struct tagCHOOSEFONTA { DWORD lStructSize; HWND hwndOwner; // caller's window handle HDC hDC; // printer DC/IC or NULL LPLOGFONTA lpLogFont; // ptr. to a LOGFONT struct INT iPointSize; // 10 * size in points of selected font DWORD Flags; // enum. type flags COLORREF rgbColors; // returned text color LPARAM lCustData; // data passed to hook fn. LPCFHOOKPROC lpfnHook; // ptr. to hook function LPCSTR lpTemplateName; // custom template name HINSTANCE hInstance; // instance handle of.EXE that // contains cust. dlg. template LPSTR lpszStyle; // return the style field here // must be LF_FACESIZE or bigger WORD nFontType; // same value reported to the EnumFonts // call back with the extra FONTTYPE_ // bits added WORD ___MISSING_ALIGNMENT__; INT nSizeMin; // minimum pt size allowed & INT nSizeMax; // max pt size allowed if // CF_LIMITSIZE is used } CHOOSEFONTA; typedef struct tagCHOOSEFONTW { DWORD lStructSize; HWND hwndOwner; // caller's window handle HDC hDC; // printer DC/IC or NULL LPLOGFONTW lpLogFont; // ptr. to a LOGFONT struct INT iPointSize; // 10 * size in points of selected font DWORD Flags; // enum. type flags COLORREF rgbColors; // returned text color LPARAM lCustData; // data passed to hook fn. LPCFHOOKPROC lpfnHook; // ptr. to hook function LPCWSTR lpTemplateName; // custom template name HINSTANCE hInstance; // instance handle of.EXE that // contains cust. dlg. template LPWSTR lpszStyle; // return the style field here // must be LF_FACESIZE or bigger WORD nFontType; // same value reported to the EnumFonts // call back with the extra FONTTYPE_ // bits added WORD ___MISSING_ALIGNMENT__; INT nSizeMin; // minimum pt size allowed & INT nSizeMax; // max pt size allowed if // CF_LIMITSIZE is used } CHOOSEFONTW; // --- 略 --- #if !defined(_WIN64) #include <poppack.h> #endif // --- 略 ---
- 編集済み 魔界の仮面弁士MVP 2022年11月10日 18:58 アライメントの説明を見直し
-
Win32API_PtrSafe.txtに書いてあるCHOOSEFONTA構造体であるなら、(64bitでの)アラインメントに伴うパディングが入っていないのかな。(いやでも、64bitOfficeのVBAでLen(CF)が90になるなら、ちゃんとアラインメントそろえないOfficeVBAのバグなのでは、、)
とりあえずは下記のようにLongPtrにしてしまえば動くとは思いますケド。Type CHOOSEFONT lStructSize As Long → LongPtr hwndOwner As LongPtr ' caller's window handle hdc As LongPtr ' printer DC/IC or NULL lpLogFont As LongPtr ' ptr. to a LOGFONT struct iPointSize As Long ' 10 * size in points of selected font flags As Long ' enum. type flags rgbColors As Long → LongPtr ' returned text color lCustData As LongPtr ' data passed to hook fn. lpfnHook As LongPtr ' ptr. to hook function lpTemplateName As String ' custom template name hInstance As LongPtr ' instance handle of.EXE that ' contains cust. dlg. template lpszStyle As String ' return the style field here ' must be LF_FACESIZE or bigger nFontType As Integer ' same value reported to the EnumFonts ' call back with the extra FONTTYPE_ ' bits added MISSING_ALIGNMENT As Integer nSizeMin As Long ' minimum pt size allowed & nSizeMax As Long→LongPtr ' max pt size allowed if ' CF_LIMITSIZE is used End Type
jzkey
- 編集済み jzkey 2022年11月7日 14:35 Officeのせい?
-
たびたび御指摘、返答いただき誠にありがとうございます。
続報となります。
①実行環境について
OSはWindows11で「システムの種類」 64 ビット オペレーティング システム、x64 ベース プロセッサ
②エクセルのバージョン
Microsoft EXCEL For Microsoft 365 MSO 64ビット
⓷ユーザー定義型Choosefontのサイズ変更
ChooseFontの値を投稿した時に実はミスっておりまして、nFontTypeメンバーが漏れていまして90ではなく92でした。
ご指摘いただいたとうり、lStructSize,rgbColors,nSizeMaxの3つのメンバーを、LongからLongLongに変更し
3*4=12バイト増 データサイズ92+12=104バイトとしました
④実行
フォントダイアログが表示されました。値も取得できました、
⑤感想
フォーラムに投稿しなかったら、自分で解決することはできなかったと感じています。
どうもありがとうございました。
API関数のスタックを破壊しているなんて、考える余地もありませんでした。
パディングというコトバも今回初めて知りました
Win32API_PtrSafe.TXTの定義のバグといえるのでしょうか?
また、LFのlfHeight値が‐21となっているのが、少し気がかりです。
Public Type CHOOSEFONT
lStructSize As LongLong
hwndOwner As LongPtr
hdc As LongPtr
lpLogFont As LongPtr
iPointSize As Long
flags As Long
rgbColors As LongLong
lCustData As LongPtr
lpfnHook As LongPtr
lpTemplateName As String
hInstance As LongPtr
lpszStyle As String
nFontType As Integer
MISSING_ALIGNMENT As Integer
nSizeMin As Long
nSizeMax As LongLong
End Type
ちなみに、CFの値は
: flags : 8513 : Long
: hdc : 0^ : LongLong
: hInstance : 0^ : LongLong
: hwndOwner : 657072^ : LongLong
: iPointSize : 80 : Long
: lCustData : 0^ : LongLong
: lpfnHook : 0^ : LongLong
: lpLogFont : 2723845560480^ : LongLong
: lpszStyle : "" : String
: lpTemplateName : "" : String
: lStructSize : 104^ : LongLong
: MISSING_ALIGNMENT : 0 : Integer
: nFontType : -7932 : Integer
: nSizeMax : 24^ : LongLong
: nSizeMin : 6 : Long
また LFの値は
: lfCharSet : 134 : Byte
: lfClipPrecision : 2 : Byte
: lfEscapement : 0 : Long
: lfFaceName(1) : 83 : Byte
: lfFaceName(2) : 84 : Byte
: lfFaceName(3) : 72 : Byte
: lfFaceName(4) : 117 : Byte
: lfFaceName(5) : 112 : Byte
: lfFaceName(6) : 111 : Byte
: lfFaceName(7) : 0 : Byte
: lfFaceName(8) : 109 : Byte
: lfFaceName(9) : 97 : Byte
: lfFaceName(10) : 108 : Byte
: lfFaceName(11) : 108 : Byte
: lfFaceName(12) : 0 : Byte
: lfFaceName(13) : 100 : Byte
: lfFaceName(14) : 105 : Byte
: lfFaceName(15) : 110 : Byte
: lfFaceName(16) : 103 : Byte
: lfFaceName(17) : 0 : Byte
: lfFaceName(18) : 32 : Byte
: lfFaceName(19) : 68 : Byte
: lfFaceName(20) : 105 : Byte
: lfFaceName(21) : 115 : Byte
: lfFaceName(22) : 112 : Byte
: lfFaceName(23) : 108 : Byte
: lfFaceName(24) : 97 : Byte
: lfFaceName(25) : 121 : Byte
: lfFaceName(26) : 0 : Byte
: lfFaceName(27) : 0 : Byte
: lfFaceName(28) : 0 : Byte
: lfFaceName(29) : 0 : Byte
: lfFaceName(30) : 0 : Byte
: lfFaceName(31) : 0 : Byte
: lfFaceName(32) : 0 : Byte
: lfHeight : -21 : Long
: lfItalic : 0 : Byte
: lfOrientation : 0 : Long
: lfOutPrecision : 3 : Byte
: lfPitchAndFamily : 2 : Byte
: lfQuality : 1 : Byte
: lfStrikeOut : 0 : Byte
: lfUnderline : 0 : Byte
: lfWeight : 700 : Long
: lfWidth : 0 : Long -
Win32API_PtrSafe.TXTの定義のバグといえるのでしょうか?
Win32API.TXT や Win32API_PtrSafe.TXT で用意されているユーザー定義型では、アライメントについてはあまり考慮されていないと思います。Len と LenB が異なるサイズのユーザー定義型を扱う場合は、特に注意してください。(各メンバーが 32bit 境界に並ばない API の場合に、API 側がどのような配置を期待しているのかは、ヘッダーファイルを読み解くか、あるいは実際に C/C++ で sizeof するなどして確認する必要があります)
他の有名どころとしては、SHFileOperation API で用いられる SHFILEOPSTRUCT 構造体が挙げられます。
この構造体、Win32API_PtrSafe.TXT のユーザー定義型定義では
「32bit では Len = 30、LenB = 32」
「64bit では Len = 50、LenB = 56」
となるのですが、本来の正しいサイズは
「32bit では 30 バイト」
「64bit では 56 バイト」
というブレがあります。(ANSI 版 / WIDE 版共に同じ)32bit 環境の場合、2 バイトの「FILEOP_FLAGS fFlags;」の後ろに 2 バイトの余白が生じます。しかし、API 側は余白無しで並んでいる物として動作するため、その後に続く「BOOL fAnyOperationsAborted;」への書き込みや「PC(W)STR lpszProgressTitle;」の読み込み位置がずれることになり、キャンセル判定に失敗したり、タイトルの文字化けやクラッシュを引き起こす要因になりえます。
また、LFのlfHeight値が‐21となっているのが、少し気がかりです。
LF というのは LOGFONT 構造体のことですね。
- >0 な正数を指定した場合は、文字セルの高さとして解釈されます。
- =0 を指定すると、既定の高さと解釈されます。
- <0 な負数を指定した場合は、その絶対値が文字の高さとして解釈されます。これは、文字セルの高さから internal leading (内部レディング…アクセント記号などのためのスペースのこと) の高さを引いたものです。
- 編集済み 魔界の仮面弁士MVP 2022年11月8日 10:09
-
返信ありがとうございます。
さらに疑問が浮かんだので、投稿させていただきます。
① ちなみにlStructSize, rgbColors、nSizeMaxの次に任意のLongのメンバーを入れても
動くということでしょうか?また、あえて私はLongをLongLongとしましたが
やはりご指摘のとうり、Lonptr にしておけば、32ビット環境でも動くということですか?
Type CHOOSEFONT
lStructSize As Long
lPadding1 as long
hwndOwner As LongPtr
hdc As LongPtr
lpLogFont As LongPtr
iPointSize As Long
flags As Long
rgbColors As Long
lPadding2 as long
lCustData As LongPtr
lpfnHook As LongPtr
lpTemplateName As String
hInstance As LongPt
lpszStyle As String
nFontType As Integer
MISSING_ALIGNMENT As Integer
nSizeMin As Long
nSizeMax As Long
lPadding3 as long
End Type
② C++の構造体宣言で64ビット環境ではパディングは自動的に挿入される仕組みになっているのですか?
-
①ちなみにlStructSize, rgbColors、nSizeMaxの次に任意のLongのメンバーを入れても
動くということでしょうか?試してはいませんが、おそらく動くと思います。こうしたダミーの余白は、元の構造体定義にある「WORD ___MISSING_ALIGNMENT__;」というメンバーに通ずるものがありますね。
ただし、32bit 環境と 64bit 環境ではアライメントが異なるため、その書き方をする場合は、可能であれば「#If Win64 Then~#Else」を使うなどして、32bit / 64bit で分けて両対応にした方が良いでしょう。
あるいは実行環境が固定的であり、常にいずれか一方でしか使わない状況に限っては、条件付きコンパイルでの切り分けをあえて省略するという選択肢もあります。
ただし、そもそもの原因は「.lStructSize に LenB(CF) ではなく Len(CF) を渡してしまっていたこと」と「nFontType 宣言を忘れていたこと」にあるとみています。そこを間違えなければ、Len=LenB となるような明示的なパディング調整を行う必要は無いように思えます。
やはりご指摘のとうり、Lonptr にしておけば、32ビット環境でも動くということですか?
× Lonptr
○ LongPtrLongPtr のうち、Long の範囲しか扱わないという条件を守るのであれば、それも一つの方法です。
ただし、LongPtr を使えないバージョンも想定する場合は、「#If VBA7 Then」の併用が必要となります。これは Declare 宣言の PtrSafe に対しても言えることですね。
② C++の構造体宣言で64ビット環境ではパディングは自動的に挿入される仕組みになっているのですか?
32bit プロセスや 64bit プロセスといった違いで決まるわけではありません。DLL がコンパイルされる際のオプションによって決まります。
そしてその動作は、Win16 であれ Win32 であれ Win64 であれ、アライメントのサイズが異なるだけであり、事情は一緒です。
32bit 向けの DLL と 64bit 向けの DLL は別々のものとしてコンパイルされていますが、既定のアライメントサイズは、コンパイル時の指定によって決まります。
上記オプションにて、1 / 2 / 4 / 8 / 16 バイトの境界のいずれかを固定的に指定してコンパイルできることがわかります。
とはいえ、構造体によって別のアライメントに変更したい場合は、『#pragma pack(n)』であるとか、あるいは先の投稿で紹介した『#include <pshpack1.h>』によって個別に変更されるケースもあります。
具体例でいえば、winnt.h というヘッダーファイルの中では、同じファイル内でも、pshpack1.h、pshpack2.h、pshpack4.h、pshpack8.h という 4 種類が使われていることを確認できます。
多くの構造体は、パッキングサイズの違いに影響を受けないよう、アライメント境界を跨いだり、無駄なパディングが発生しないような設計になっていることが多いのですが、今回のように一部の構造体はそうではありませんので、注意が必要であるというわけですね。
.NET であれば、構造体宣言時にアライメントを明示できます(0、1、2、4、8、16、32、64、128)が、VBA にはそのようなものが無いので、実際のメモリー配置を考慮した上で、適宜調整する必要が生じます。
今回の CHOOSEFONT 構造体の場合、Len指定に合わせて「64bit では追加の余白を設けて、メンバー位置を後ろにずらす必要がある」という対処を取りましたが、先ほど紹介した SHFILEOPSTRUCT の場合はその逆で、「32bit では、VBA によって自動的に差し込まれる 2 バイトのパディング分を除去するため、メンバーを前に詰めなければならない」といったように。
- 編集済み 魔界の仮面弁士MVP 2022年11月10日 19:27
-
iいつも、丁寧かつ詳細なご回答をいただきまして、ありがとうございます。
また、ふと思ったことを、投稿します。
API関数 CHOOSEFONTが 求める、CHOOSEFONT構造体のアラインメントが、下記であるというのは
どうやって知ることができたのですか? 実行状態で ChooseFontのサイズを取得して、宣言バイト数の総計との比較で、パディングの位置を推察するということですか?
64bit 版の場合、CHOOSEFONT の各メンバーはこのように配置されることになるはず。
0x00-0x03 : DWORD lStructSize; 0x04-0x07 : 《4 バイトのパディング》 0x08-0x0F : HWND hwndOwner; 0x10-0x17 : HDC hDC; 0x18-0x1F : LPLOGFONT lpLogFont; 0x20-0x23 : INT iPointSize; 0x24-0x27 : DWORD Flags; 0x28-0x2B : DWORD rgbColors; 0x2C-0x2F : 《4 バイトのパディング》 0x30-0x37 : LPARAM lCustData; 0x38-0x3F : LPCFHOOKPROC lpfnHook; 0x40-0x47 : LPTCSTR lpTemplateName; 0x48-0x4F : HINSTANCE hInstance; 0x50-0x57 : LPTSTR lpszStyle; 0x58-0x59 : WORD nFontType; 0x5A-0x5B : WORD ___MISSING_ALIGNMENT__; 0x5C-0x5F : INT nSizeMin; 0x60-0x63 : INT nSizeMax; 0x64-0x67 : 《4 バイトのパディング》
-
API関数 CHOOSEFONTが 求める、CHOOSEFONT構造体のアラインメントが、下記であるというのは
どうやって知ることができたのですか?先の投稿で『CHOOSEFONT 構造体は、commdlg.h 上で下記のように定義されています。』と書いたかと思います。
そのヘッダーファイルの内容を読み解くことで、_WIN64 では既定サイズのパッキング、それ以外では 1 バイト単位のパッキングであることを推察できました。
.
構造体のサイズについては、実際に C++ で sizeof してみるのが確実でしょう。自分も sizeof によって『32bit 環境では 60 、64bit 環境では 104 というサイズ』であることを確認しました。ポインタの中身を読み解けば、そのメモリ配置も見えてきますね。
.
一方、VBA 側のユーザー定義型のパディングに関しては、LSet ステートメント を使って確認できます。
Option Explicit Type UDT_BIN Binary(0 To 300) As Byte '十分な長さを確保しておく End Type Type UDT_CHOOSEFONT lStructSize As Long hwndOwner As LongPtr '以下略 End Type Sub Test() Dim BIN As UDT_BIN Dim CF As UDT_CHOOSEFONT CF.lStructSize = &H11111111 '4バイトなので8桁 #If Win64 Then CF.hwndOwner = &H2222222222222222^ '8 バイトなので 16 桁 #Else CF.hwndOwner = &H22222222 '4 バイトなので 8 桁 #End If
'上記以外のメンバーも同様に、バイト数一杯までの分かりやすい値で埋めておく 'メモリ配置確認のため、Byte配列に変換 LSet BIN = CF 'あとは BIN.Binary の中身を確認すれば OK。 ' 'ウォッチウィンドウで確認したり、 'For ループで Hex 変換していっても良いけど、 'ここでは bin.hex を使って確認してみます。 With CreateObject("Microsoft.XMLDOM").createElement("h") .DataType = "bin.hex" .NodeTypedValue = BIN.Binary Debug.Print .Text End With End Sub32bit 環境であれば、"1111111122222222…" な出力となり
64bit 環境であれば、"111111110000000022222222…" な出力になるかと思います。
(手元に Win64 な VBA 環境が無いので未確認)- 編集済み 魔界の仮面弁士MVP 2022年11月9日 6:34
-
そうですね。LOGFONT のユーザー定義型が正しく宣言されているなら、ローカル変数 LF に対する「.lpLogFont = VarPtr(LF)」を指定しようと、API で確保したアドレスを「.lpLogFont = Address」で指定しようと、どちらでも良いと思います。
重要なのは、指定したポインターが読み書き可能なメモリを指しており、かつ、そのポインターが指し示す領域に十分なサイズを確保してあるどうかかと。
.
一番最初の質問時点では、CHOOSEFONT の nFontType 宣言漏れ問題もあって、.lStructSize = 90 によって「パラメーターが間違っています。」になったわけですよね(32bitなら「60」、64bitなら「104」とする必要があるはず)。
そこへ、さらにパディングの問題が加わっています。
Win64 環境においては「hMem = GlobalAlloc(GHND, Len(LF))」の時点で、90 バイトの領域しか確保されていなかったのでしょう(nFontType ありだったのなら 92 バイト)。
この時、64bit 版のユーザー定義型は 8 バイトのアライメントなので、実際には 104 バイトを必要としていたはずです(nFontType の有無によらず、 LenB(LF) は 104 を返す)。
LenB(LF) ではなく Len(LF) を用いていたことから、「Address = GlobalLock(hMem)」「CopyMemory LF, ByVal Address, Len(LF)」では、先頭から 90 バイトまでの 0x00~0x59 の範囲のみしか転写されていません。0x5A~0x67 のエリア(nSizeMin/nSizeMax)は漏れていたことでしょう。
0x00-0x03:lStructSize (中略) 0x50-0x57:lpszStyle 0x58-0x59:本来は nFontType の位置だが、宣言漏れ時は MISSING_ALIGNMENT
修正版では、明示的なパディングを入れるか、Long を LongPtr 化することで、Len(LF) でも 104 が返されるようにしたみたいですけれどね。- 回答としてマーク my_think 2022年11月12日 12:50
-
末尾にパディングを含めるかどうか、ということですね。結論から言えば、LOGFONT においては不要です。
LOGFONT は条件によらず常に 4 バイトのパッキングであるためです。
...
API 側は構造体によってアライメントが異なります。
実際に C++ で確認する場合は、alignof() からアライメントを、sizeof() からサイズを得られます。なお、sizeof で返されるのは通常、末尾パディングを含んだ値になります。
それぞれの構造体のアライメントとサイズは下記の通り。
alignof(LOGFONTA)⇒{Win32: 4 バイト, Win64: 4バイト}
alignof(LOGFONTW)⇒{Win32: 4 バイト, Win64: 4バイト}
alignof(CHOOSEFONTA)⇒{Win32: 1 バイト, Win64: 4バイト}
alignof(CHOOSEFONTW)⇒{Win32: 1 バイト, Win64: 4バイト}
sizeof(LOGFONTA)⇒{Win32: 60バイト, Win64: 60 バイト}
sizeof(LOGFONTW)⇒{Win32: 92バイト, Win64: 92 バイト}
sizeof(CHOOSEFONTA)⇒{Win32: 60 バイト, Win64: 104 バイト}
sizeof(CHOOSEFONTW)⇒{Win32: 60 バイト, Win64: 104 バイト}..
一方、VBA 側のユーザー定義型においてはアライメントは常に一定であり、Win32 なら 4 バイト境界、Win64 ならば 8 バイト境界です。
ユーザー定義型のサイズは、Len() はパディングを含めない論理サイズ、LenB() はパディングを含めた物理サイズを返します。
Win32API_PtrSafe.TXT の宣言をそのまま使う場合、Type LOGFONT の定義は、Win32/Win64 のいずれにおいても、 Len と LenB が共に 60 という同じ値を返します。
つまり、API 側もパディング無しの 60 バイトを要求しているわけですから、メモリ配置としては、VBA 側と API 側との間での乖離がありません。そのため LOGFONT においてはパディングを考慮する必要が無い、ということになります。
- 編集済み 魔界の仮面弁士MVP 2022年11月12日 16:40
-
たびたび、ご回答、誠にありがとございます。
Win64環境下で、C,C++でAPIのDLLをコンパイルする際に、CHOOSEFONTは8バイトパッキングなので、末尾パディングを含み、LOGFONTAは4バイトパッキングなので、は含まないということでしょうか?
とりあえず、VBA上でCHOOSEFONTの宣言を、Win32_PtrSafe.txtに戻し、lStrructSize=LenB(CF)とすれば、フォントダイアログは表示されることhが、確認できました。C,C++にあまり詳しくないので、投稿していただいた内容をすべて把握することはできていないのですが、いろいろと、ご教示いただきありがとうございました。
- 編集済み my_think 2022年11月12日 13:54
-
ごめんなさい。
先の私の回答には幾つか誤りがありました。ここでもう一度改めて説明しなおします。
Win64環境下で、C,C++でAPIのDLLをコンパイルする際に、CHOOSEFONTは8バイトパッキングなので、末尾パディングを含み、LOGFONTAは4バイトパッキングなので、は含まないということでしょうか?
アラインされることによって発生するパディングの有無およびその位置は、アライメントのサイズと各要素のメンバー定義によって決まります。(より詳しいルールを知りたい場合は、こちらを参照してみてください)
そしてその配置は通常、「既定のアライメント」に従ってアラインされることになります。ただし構造体によっては、「意図的に異なるアライメントでコンパイル」されているものも存在します。
既定のアライメントですが、これは構造体(あるいはユーザー定義型)に含まれる各メンバーの型のアライメントの中で、最大のアライメントに一致します。
.
LOGFONTA のメンバーでは、LONG, BYTE, CHAR という 3 つの型が使われていますが、このうち LONG 型が最大のアライメントを持っています。そのため、LOGFONTA 構造体の既定のアライメントは「4 バイト」です。そして 4 バイト単位でメンバーをパッキングしていった場合、隙間なく詰め込むことができるので、この構造体にはパディングが発生しません。
.
CHOOSEFONTA のメンバーでは、最大のアライメントとなるのがハンドル系(HWND, HDC, HINSTANCE)や LP何某系です。そのため本来であれば Win32 では 4 バイト、Win64 では 8 バイトが既定のアライメントにあたります。しかしながら、Windows SDK のヘッダーファイルにおいて
#if !defined(_WIN64) #include <pshpack1.h> /* Assume byte packing throughout */ #endif
が指定されているため、Win32 においては、アライメントが「1 バイト」に変更されています。
Win32 では 1 バイトアライメントに変更されているため、CHOOSEFONTA にはパディングが発生しません。とはいえ、既定の 4 バイトのままであったとしても、「WORD nFontType;」の後に、わざわざ「WORD ___MISSING_ALIGNMENT__」を設けてあることもあり、追加のパディングは発生しません。そのため、VBA (32bit 版)から呼び出すときも、結果的にメンバーの位置調整は不要です。
一方 Win64 では既定の 8 バイトアライメントなので、先頭の「DWORD lStructSize;」とその次の「HWND hwndOwner;」の間に、4 バイトのパディングを設けてアラインされます。同様に、「LPARAM lCustData;」の直前に 4 バイトと、末尾「INT nSizeMax;」の直後に 4 バイトの、計 12 バイトのパディングが生まれます(これは C++ だけでなく VBA の場合にも当てはまります)。
ちなみに VBA のユーザー定義型は、メモリ上には「既定のアライメント」で配置される仕様ですが、ファイル入出力の場合は常に 1 バイトのアライメントとして扱われる仕様です(具体的には、Put #/Get # ステートメントでユーザー定義型を読み書きする場合です)。
.
注意が必要なのは、先述の SHFILEOPSTRUCT 構造体のケースのように、既定のアライメントだとパディングが発生するような定義になっていおり、かつ、既定のものよりも小さなアライメントが割り当てられているケースです。VBA では既定のアライメントしか扱えないというのに、このケースでは、パディングのズレ以降にポインタ(LPVOID と PCSTR) があるため、このズレを自前で調整して呼び出さないと、クラッシュを誘発させる要因となってしまうのです。
.
とはいえ API で利用される構造体は、そもそも無駄なパディングが生まれないようなメンバー配置になっているものが多いはずです。なので、普段はパディングを意識する必要はありません。
一方、あえて異なるアライメントに調整されている構造体は、その多くが「既定のアライメントだとパディングが発生してしまう構造体」です。VBA 側へ移植する際に、『Len と LenB が異なる結果となるユーザー定義型』を見つけた場合は、注意が必要であるという事ですね。
なお、対象の構造体が求めるアライメントサイズを確認する場合、そのサイズがドキュメントに明記してあるわけではありませんので、C++ などで実際に呼び出して alignof や sizeof で確認してみないと分からないと思います。
.
最後に、VBA 側の 既定のアライメントを確認するために、いくつかの例を載せておきます。
Type U1 'これは 1 バイトのアライメントになる A As Byte 'アライメント 1 バイト、サイズ 1 バイト End Type Type U2 'これも 1 バイトのアライメント A As Byte 'サイズ 1 バイト、アライメント 1 バイト B As Byte 'サイズ 1 バイト、アライメント 1 バイト End Type Type U3 'これも 1 バイトのアライメント A As Byte 'サイズ 1 バイト、アライメント 1 バイト B As Byte 'サイズ 1 バイト、アライメント 1 バイト C As Byte 'サイズ 1 バイト、アライメント 1 バイト End Type Type U4 'これは 2 バイトのアライメント A As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに 1 バイトのパディングが入る) B As Integer 'サイズ 2 バイト、アライメント 2 バイト End Type Type U5 'これも 2 バイトのアライメント A As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに 1 バイトのパディングが入る) B As Integer 'サイズ 2 バイト、アライメント 2 バイト C As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに 1 バイトのパディングが入る) End Type Type U6 'これは 4 バイトのアライメント A As Byte 'アライメント 1 バイト、サイズ 1 バイト '(ここに 3 バイトのパディングが入る) B As Long 'サイズ 4 バイト、アライメント 4 バイト End Type Type U7 'これも 4 バイトのアライメント A As Byte 'サイズ 1 バイト、アライメント 1 バイト B As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに 2 バイトのパディングが入る) C As Long 'サイズ 4 バイト、アライメント 4 バイト End Type Type U8 'これも 4 バイトのアライメント A As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに 3 バイトのパディングが入る) B As Long 'サイズ 4 バイト、アライメント 4 バイト C As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに 3 バイトのパディングが入る) End Type Type U9 'これは Win32 では 4 バイト、Win64 では 8 バイトのアライメント A As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに、Win32 なら 3 バイト、Win64 なら 7 バイトのパディングが入る) B As Currency 'サイズ 8 バイト、アライメント Win32=4 バイト/Win64=8バイト End Type Type U0 'これも Win32 では 4 バイト、Win64 では 8 バイトのアライメント A As Currency 'サイズ 8 バイト、アライメント Win32=4 バイト/Win64=8バイト B As Byte 'サイズ 1 バイト、アライメント 1 バイト '(ここに、Win32 なら 3 バイト、Win64 なら 7 バイトのパディングが入る) End Type
ユーザー
定義型Win32 Win64 アライ
メントLen LenB アライ
メントLen LenB U1 1 1 1 1 1 1 U2 1 2 2 1 2 2 U3 1 3 3 1 3 3 U4 2 3 4 2 3 4 U5 2 4 6 2 4 6 U6 4 5 8 4 5 8 U7 4 6 8 4 6 8 U8 4 6 12 4 6 12 U9 8 9 12 8 9 16 U0 8 9 12 8 9 16 - 編集済み 魔界の仮面弁士MVP 2022年11月13日 8:15