3

Problem description

I try to get code working under 64-Bit VBA which works fine in 32-Bit VBA.

It is regarding Common Controls TaskDialogs.

I use Microsoft Access, but the problem should be the same in other VBA hosts.

One part works fine in both (32- and 64-Bit) VBA, the other part doesn't.

TaskDialog API working well in both (32- and 64-Bit) VBA

You can start the procedure TestTaskDlg for a test.

Option Explicit

'Original API definition:
'------------------------
'HRESULT TaskDialog(
'  HWND                           hwndOwner,
'  HINSTANCE                      hInstance,
'  PCWSTR                         pszWindowTitle,
'  PCWSTR                         pszMainInstruction,
'  PCWSTR                         pszContent,
'  TASKDIALOG_COMMON_BUTTON_FLAGS dwCommonButtons,
'  PCWSTR                         pszIcon,
'  int                            *pnButton
');
Private Declare PtrSafe Function TaskDialog Lib "Comctl32.dll" _
                            (ByVal hWndParent As LongPtr, _
                             ByVal hInstance As LongPtr, _
                             ByVal pszWindowTitle As LongPtr, _
                             ByVal pszMainInstruction As LongPtr, _
                             ByVal pszContent As LongPtr, _
                             ByVal dwCommonButtons As Long, _
                             ByVal pszIcon As LongPtr, _
                             ByRef pnButton As Long _
                             ) As Long

'Works fine with 32-Bit VBA and 64-Bit VBA:
Public Sub TestTaskDlg()
    Debug.Print TaskDlg("Title", "MainInstructionText", "ContentText")
End Sub

Public Function TaskDlg( _
                    sWindowTitle As String, _
                    sMainInstruction As String, _
                    sContent As String _
                    ) As Long

    On Local Error GoTo Catch

    Dim clickedButton As Long
    TaskDlg = TaskDialog(0, _
                0, _
                StrPtr(sWindowTitle), _
                StrPtr(sMainInstruction), _
                StrPtr(sContent), _
                0, _
                0, _
                clickedButton)

    Debug.Print "Clicked button:", clickedButton

Done:
    Exit Function

Catch:
    MsgBox Err.Description, , Err.Number
    Resume Done
End Function

TaskDialogIndirect API working well only in 32-Bit VBA

You can start the procedure TestTaskDlgIndirect for a test.

In 64-Bit VBA it returns E_INVALIDARG (0x80070057 | -2147024809), pointing to invalid arguments somehow...

If I use Len() instead of LenB() and comment this three lines of code, it shows a proper (empty) dialog, so the call of TaskDialogIndirect should be correct.

tdlgConfig.pszWindowTitle = StrPtr(sWindowTitle)
tdlgConfig.pszMainInstruction = StrPtr(sMainInstruction)
tdlgConfig.pszContent = StrPtr(sContent)

Does anybody have an idea why it is not working in 64-bit VBA?

In my opinion I already converted the types from Long to LongPtr properly.

I expect it is a problem with the values/pointers which will be stored in the structure at runtime.

Maybe some Hi-/Low-Byte stuff?

Any help appreciated. :-)

Option Explicit

'Original API definition:
'------------------------
'typedef struct _TASKDIALOGCONFIG {
'  UINT                           cbSize;
'  HWND                           hwndParent;
'  HINSTANCE                      hInstance;
'  TASKDIALOG_FLAGS               dwFlags;
'  TASKDIALOG_COMMON_BUTTON_FLAGS dwCommonButtons;
'  PCWSTR                         pszWindowTitle;
'  union {
'    HICON  hMainIcon;
'    PCWSTR pszMainIcon;
'  } DUMMYUNIONNAME;
'  PCWSTR                         pszMainInstruction;
'  PCWSTR                         pszContent;
'  UINT                           cButtons;
'  const TASKDIALOG_BUTTON        *pButtons;
'  int                            nDefaultButton;
'  UINT                           cRadioButtons;
'  const TASKDIALOG_BUTTON        *pRadioButtons;
'  int                            nDefaultRadioButton;
'  PCWSTR                         pszVerificationText;
'  PCWSTR                         pszExpandedInformation;
'  PCWSTR                         pszExpandedControlText;
'  PCWSTR                         pszCollapsedControlText;
'  union {
'    HICON  hFooterIcon;
'    PCWSTR pszFooterIcon;
'  } DUMMYUNIONNAME2;
'  PCWSTR                         pszFooter;
'  PFTASKDIALOGCALLBACK           pfCallback;
'  LONG_PTR                       lpCallbackData;
'  UINT                           cxWidth;
'} TASKDIALOGCONFIG;
Public Type TASKDIALOGCONFIG
    cbSize As Long                                  'UINT
    hWndParent As LongPtr                           'HWND
    hInstance As LongPtr                            'HINSTANCE
    dwFlags As Long                                 'TASKDIALOG_FLAGS
    dwCommonButtons As Long                         'TASKDIALOG_COMMON_BUTTON_FLAGS
    pszWindowTitle As LongPtr                       'PCWSTR
'    Union
'    {
        hMainIcon As LongPtr                        'Union means that the biggest type has to be declared: So LongPtr
'       hMainIcon                                   'HICON
'       pszMainIcon                                 'PCWSTR
'    };
    pszMainInstruction As LongPtr                   'PCWSTR
    pszContent As LongPtr                           'PCWSTR
    cButtons As Long                                'UINT
    pButtons As LongPtr                             'TASKDIALOG_BUTTON  *pButtons;
    nDefaultButton As Long                          'INT
    cRadioButtons As Long                           'UINT
    pRadioButtons As LongPtr                        'TASKDIALOG_BUTTON  *pRadioButtons;
    nDefaultRadioButton As Long                     'INT
    pszVerificationText As LongPtr                  'PCWSTR
    pszExpandedInformation As LongPtr               'PCWSTR
    pszExpandedControlText As LongPtr               'PCWSTR
    pszCollapsedControlText As LongPtr              'PCWSTR
    'Union
    '{
        hFooterIcon As LongPtr                      'Union means that the biggest type has to be declared: So LongPtr
    '   hFooterIcon                                 'HICON
    '   pszFooterIcon                               'PCWSTR
    '};
    pszFooter As LongPtr                            'PCWSTR
    pfCallback As LongPtr                           'PFTASKDIALOGCALLBACK
    lpCallbackData As LongPtr                       'LONG_PTR
    cxWidth As Long                                 'UINT
End Type

'Original API definition:
'------------------------
'HRESULT TaskDialogIndirect(
'  const TASKDIALOGCONFIG *pTaskConfig,
'  int                    *pnButton,
'  int                    *pnRadioButton,
'  BOOL                   *pfVerificationFlagChecked
');
Private Declare PtrSafe Function TaskDialogIndirect Lib "Comctl32.dll" ( _
                            ByRef pTaskConfig As TASKDIALOGCONFIG, _
                            ByRef pnButton As Long, _
                            ByRef pnRadioButton As Long, _
                            ByRef pfVerificationFlagChecked As Long _
                            ) As Long

'Works fine with 32-Bit VBA. But with 64-Bit VBA it returns E_INVALIDARG (0x80070057 | -2147024809)
Public Sub TestTaskDlgIndirect()
    Debug.Print TaskDlgIndirect("Title", "MainInstructionText", "ContentText")
End Sub

Public Function TaskDlgIndirect( _
                    sWindowTitle As String, _
                    sMainInstruction As String, _
                    sContent As String _
                    ) As Long

    On Local Error GoTo Catch

    Dim tdlgConfig As TASKDIALOGCONFIG
    tdlgConfig.cbSize = LenB(tdlgConfig)

    'Usually LenB() should be the right way to use, but when I use Len() and comment the three texts below, it shows a proper empty dialog!
    tdlgConfig.pszWindowTitle = StrPtr(sWindowTitle)
    tdlgConfig.pszMainInstruction = StrPtr(sMainInstruction)
    tdlgConfig.pszContent = StrPtr(sContent)

    Dim clickedButton As Long
    Dim selectedRadio As Long
    Dim verificationFlagChecked As Long
    TaskDlgIndirect = TaskDialogIndirect(tdlgConfig, clickedButton, _
                        selectedRadio, verificationFlagChecked)

    Debug.Print "Clicked button:", clickedButton

Done:
    Exit Function

Catch:
    MsgBox Err.Description, , Err.Number
    Resume Done
End Function

Update

Some new insights:

It seems that TASKDIALOGCONFIG uses a 1-byte packing internally.

  • In 32-bit VBA (which uses 4-byte padding for structs) this didn't matter because all members of the struct were of type Long and so 4 byte, so no padding occured at all.
    Also in this constellation there is no difference in using Len(tdlgConfig), which calculates the sum of the datatypes only, and LenB(tdlgConfig), which calculates the real size of the struct indeed.
    Both result in 96 bytes here.

  • But in 64-bit VBA (which uses 8-byte padding for structs) some members of the struct are of type Long (4 byte) and some are LongLong (8 byte) (declared as LongPtr for 32-bit compatibility). This results to VBA applies padding and that is the reason why Len(tdlgConfig) returns 160 and LenB(tdlgConfig) 176.

  • So because my test without providing any texts (commenting the mentioned 3 lines of code) displays a dialog only when I use Len(tdlgConfig) (instead of LenB(tdlgConfig)) leads to the same conclusion, that the 64-bit API expects a structure of 160 bytes only.

So to provide a struct of 160 bytes I used this for a test:

Public Type TASKDIALOGCONFIG
    cbSize As Long
    dummy2 As Long
    dummy3 As Long
    dummy4 As Long
    dummy5 As Long
    dummy6 As Long
    dwCommonButtons As Long
    dummy8 As Long
    dummy9 As Long
    dummy10 As Long
    dummy11 As Long
    dummy12 As Long
    dummy13 As Long
    dummy14 As Long
    dummy15 As Long
    dummy16 As Long
    dummy17 As Long
    dummy18 As Long
    nDefaultButton As Long
    dummy20 As Long
    dummy21 As Long
    dummy22 As Long
    dummy23 As Long
    dummy24 As Long
    dummy25 As Long
    dummy26 As Long
    dummy27 As Long
    dummy28 As Long
    dummy29 As Long
    dummy30 As Long
    dummy31 As Long
    dummy32 As Long
    dummy33 As Long
    dummy34 As Long
    dummy35 As Long
    dummy36 As Long
    dummy37 As Long
    dummy38 As Long
    dummy39 As Long
    dummy40 As Long
End Type

Now both, Len(tdlgConfig) and LenB(tdlgConfig) return 160.

Calling the empty dialog without texts still runs well.

And I now can set dwCommonButtons and nDefaultButton (both type Long) and it works correct so far.

For example:

Public Enum TD_COMMON_BUTTON_FLAGS
    TDCBF_OK_BUTTON = &H1&               '// Selected control returns value IDOK
    TDCBF_YES_BUTTON = &H2&              '// Selected control returns value IDYES
    TDCBF_NO_BUTTON = &H4&               '// Selected control returns value IDNO
    TDCBF_CANCEL_BUTTON = &H8&           '// Selected control returns value IDCANCEL
    TDCBF_RETRY_BUTTON = &H10&           '// Selected control returns value IDRETRY
    TDCBF_CLOSE_BUTTON = &H20&           '// Selected control returns value IDCLOSE
End Enum
'typedef DWORD TASKDIALOG_COMMON_BUTTON_FLAGS;           // Note: _TASKDIALOG_COMMON_BUTTON_FLAGS is an int

Public Enum TD_COMMON_BUTTON_RETURN_CODES
    IDOK = 1
    IDCANCEL = 2
    IDRETRY = 4
    IDYES = 6
    IDNO = 7
    IDCLOSE = 8
End Enum

    tdlgConfig.dwCommonButtons = TDCBF_YES_BUTTON Or TDCBF_NO_BUTTON
    tdlgConfig.nDefaultButton = IDNO

So I can expect the size of the struct is fine and now I have to find out how to set the LongLong (LongPtr) types...

AHeyne
  • 3,377
  • 2
  • 11
  • 16
  • @DavidHeffernan: As I read (https://stackoverflow.com/a/17156922/7658533) you are an expert for such belongs. Maybe you could take a look? ;-) – AHeyne Apr 24 '20 at 06:58
  • It doesn't solve your problem, but the function declaration is wrong. `hWndParent`, `hInstance` and `pszIcon` are all `LongPtr`. – GSerg Apr 25 '20 at 06:43
  • `Len` is the correct function to use when dealing with structure sizes for interop purposes. `LenB` is for Unicode strings when you need to count bytes. 160 bytes returned by `Len` is the correct structure size for x64 (and 96 is correct for x86), as confirmed by checking `sizeof TASKDIALOGCONFIG` from a C++ application. Your structure is defined correctly as far as I can see. – GSerg Apr 25 '20 at 07:14
  • Thanks a lot for those infos. So I'm on the right way. Regarding the declaration of the `TaskDialog` function you're right, I corrected that now. – AHeyne Apr 25 '20 at 09:31

2 Answers2

0

Finally I got it working to set the icon to be used and a string in the struct in 64-Bit VBA.

This is the new struct, where I named the members for the main icon and the main instruction text additionally:

Public Type TASKDIALOGCONFIG
    cbSize As Long
    dummy2 As Long
    dummy3 As Long
    dummy4 As Long
    dummy5 As Long
    dummy6 As Long
    dwCommonButtons As Long
    dummy8 As Long
    dummy9 As Long
    hMainIcon1 As Long
    hMainIcon2 As Long
    pszMainInstruction1 As Long
    pszMainInstruction2 As Long
    dummy14 As Long
    dummy15 As Long
    dummy16 As Long
    dummy17 As Long
    dummy18 As Long
    nDefaultButton As Long
    dummy20 As Long
    dummy21 As Long
    dummy22 As Long
    dummy23 As Long
    dummy24 As Long
    dummy25 As Long
    dummy26 As Long
    dummy27 As Long
    dummy28 As Long
    dummy29 As Long
    dummy30 As Long
    dummy31 As Long
    dummy32 As Long
    dummy33 As Long
    dummy34 As Long
    dummy35 As Long
    dummy36 As Long
    dummy37 As Long
    dummy38 As Long
    dummy39 As Long
    dummy40 As Long
End Type

Because the LongLong values in the struct now all are split into separate Long values, I couldn't set them in a common way.

With some try and error I found a way to set the icon. It is enough to set the first Long value in the same way it has to be done in 32-Bit VBA:

Const TD_SECURITY_ICON_OK As Integer = -8

tdlgConfig.hMainIcon1 = &HFFFF And TD_SECURITY_ICON_OK

Setting the pointer to a string also was a bit tricky. I finally declare the CopyMemory API sub...

Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" ( _
    ByVal destination As LongPtr, _
    ByVal source As LongPtr, _
    ByVal dataLength As LongPtr)

...and use it like this to set a string reference in the struct:

CopyMemory VarPtr(tdlgConfig.pszMainInstruction1), VarPtr(StrPtr("My main instruction")), 8

Finally I can use the function TaskDialogIndirect like this:

    Dim clickedButton As Long
    Dim selectedRadio As Long
    Dim verificationFlagChecked As Long
    Call TaskDialogIndirect(tdlgConfig, clickedButton, _
                        selectedRadio, verificationFlagChecked)

    Debug.Print "Clicked button:", clickedButton

The rest is pure diligence to set the other texts etc. and make the code executable for 32-bit and 64-bit using case distinctions.

Thanks again to GSerg for replying.

AHeyne
  • 3,377
  • 2
  • 11
  • 16
  • Why would you do that? Why would you declare this dummy structure like this and create more difficulties for yourself to overcome? Your original structure was correct. There is no need to make sure that `Len` and `LenB` return the same value. Just use `Len`. – GSerg Apr 26 '20 at 06:43
  • @GSerg: One thing is providing the correct size, I could just use `Len`, that's right. The other thing is filling values in the struct before calling the api. In the original structure most of the structs members are at a wrong binary position because of the 8 byte padding used by 64-Bit VBA. For example the 8 byte (`LongLong`) sized `hWndParent` is not located at the 5th byte as expected, but at the 9th byte, because 4 Bytes have been padded by VBA behind the 4 byte (`Long`) sized `cbSize`. – AHeyne Apr 26 '20 at 10:25
  • Remember: `LenB` returns 176, what is the real size of the struct, caused by the automatically done padding by VBA. – AHeyne Apr 26 '20 at 10:32
  • If what you are saying was true, absolutely nothing WinApi-related would work in Office 64, but it actually does. You are saying that you see the dialog when you use `Len`. Can you verify that it displays strings that you assign to the original structure when using `Len`? Because as far as I know it's the opposite, and [VBA structures are packed](https://stackoverflow.com/a/17156922/11683), so require extra manual alignment when the native struct is not packed. – GSerg Apr 26 '20 at 10:57
  • I tested that once more now explicitly: Using the original struct and `Len` crashs Microsoft Access when I use `tdlgConfig.pszWindowTitle = StrPtr("Foo")` and then call the api. As far as I know this depends on how the struct is defined on the api side. – AHeyne Apr 26 '20 at 11:23
  • Regarding `padding` and `packing`: Maybe those terminologies are mixed up by who ever? I read about VBA structs (`type`) use padding. Also your reference uses this naming: `But in 64 bit the pointers need 8 byte alignment and the struct has some extra padding.` – AHeyne Apr 26 '20 at 11:41
  • `But in 64 bit the pointers need 8 byte alignment and the struct has some extra padding` - yes, it means the native struct requires padding, but *VBA does not provide it*, so it has to be provided manually like in the answer. The key is that VBA is not providing it; you are saying that it is providing it, so it needs to be removed. I don't have a 64-bit Office to test, but I would be very curious to see what native code receives. Can you create a native x64 dll that accepts the struct and pass it from VB having declared it as you originally had it, and have the dll report back the values? – GSerg Apr 26 '20 at 12:55
  • VBA provides the padding! The prove is that `Len` returns less than `LenB`. That's how I understand it. I don't know how to convince you more. ;-) Maybe you download a testversion of office 64-bit? Or if you want aditionally a MS test-VM? Sorry, I can't create a native test DLL. – AHeyne Apr 26 '20 at 14:23
  • Then [this](https://stackoverflow.com/a/17156922/11683) would have not worked, but apparently [it did](https://stackoverflow.com/questions/17156699/how-to-make-winhttpcrackurl-work-in-64-bit/17156922#comment24865755_17156922). – GSerg Apr 26 '20 at 14:26
  • Those additional paddings (`padding1-5`) in your sample in my opinion didn't change the real size (`LenB`) of the struct/type at all. But they changed the result what `Len` returned: Namely in this case exactly the same as `LenB` does. It could/should have been enough, if bovender whould have used `LenB` instead of `Len` instead of adding those `padding1-5`. – AHeyne Apr 26 '20 at 14:36
0

This is an old thread at this point but since I was just making a TaskDialogIndirect class I wanted to be compatible with VBA7x64, I came across it and saw there's a lot of misunderstandings that were never cleared up. I've had a hell of a time in the past year dealing with all sorts of packing/alignment issues as I move code to 64bit, so thought I good explainer was in order for anyone else who stumbles upon this question.

VBA will pad the structure under x64. That's the correct behavior-- not because every API expects unpadded structures, as GSerg suggested, but because this API does. If you look in the SDK header where these things are defined, CommCtrl.h, right before the Task Dialog definitions, you'll see this:

#include <pshpack1.h>

Then after the Task Dialogs,

#include <poppack.h>

What these headers do is adjust the alignment. pshpack1 means no packing is applied from the point where it's included to where poppack restores the default native packing rules. So this API, unlike most APIs, requires an unpadded structure. It's fairly uncommon for this to be the case; I don't know why it is here, but it is.

VBA does not provide any option to not pad a structure. So that means using an 8 byte data type is not going to work. But the API interprets the structure according to how it thinks the memory is laid out.

As to the link to the URL_COMPONENTS, I don't know what else was done, maybe it's actually 32bit Office and the structure is passed through without WOW64 converting it (like the event trace API), but you can verify the LenB and offsets are all the same with or without those padding members.

I found the easiest way to implement this API was to just declare

#If VBA7 Then
    #If (Win64 <> 0) And (TWINBASIC = 0) Then
        Private Type TASKDIALOG_BUTTON_VBA7
            data(11) As byte
        End Type
        Private Type TASKDIALOGCONFIG_VBA7
            data(159) As Byte
        End Type
        Private m_uButtons_VBA7() As TASKDIALOG_BUTTON_VBA7
        Private m_uRadioButtons_VBA7() As TASKDIALOG_BUTTON_VBA7
        Private uTDC_VBA7 As TASKDIALOGCONFIG_VBA7

Those are the correct, packing free sizes. The normal structure still there for all other modes (twinBASIC is a 100% compatible successor to VB6/VBA supporting building 64bit exes using VBA7 syntax), right before calling the API, I copy all of the regular structure to their correct offsets, including the button arrays:

#If (VBA7 <> 0) And (TWINBASIC = 0) And (Win64 <> 0) Then
'Special handling for 64bit VBA7, which doesn't support our manually aligned structure.
    ReDim m_uButtons_VBA7(uTDC.cButtons)
    Dim i As Long
    If uTDC.cButtons Then
        For i = 0 to uTDC.cButtons - 1
            CopyMemory m_uButtons_VBA7(i).data(0), m_uButtons(i).nButtonID, 4
            CopyMemory m_uButtons_VBA7(i).data(4), m_uButtons(i).pszButtonText, 8
        next i
    End If
    ReDim m_uRadioButtons_VBA7(uTDC.cRadioButtons)
    If uTDC.cRadioButtons Then
        For i = 0 to uTDC.cRadioButtons - 1
            CopyMemory m_uRadioButtons_VBA7(i).data(0), m_uRadioButtons(i).nButtonID, 4
            CopyMemory m_uRadioButtons_VBA7(i).data(4), m_uRadioButtons(i).pszButtonText, 8
        next i
    End If
    Dim ptrBtn As LongPtr, ptrRbn As LongPtr
    ptrBtn = VarPtr(m_uButtons_VBA7): ptrRbn = VarPtr(m_uRadioButtons_VBA7)
    CopyMemory uTDC_VBA7.data(0), uTDC.cbSize, 4: CopyMemory uTDC_VBA7.data(4), uTDC.hWndParent, 8: CopyMemory uTDC_VBA7.data(12), uTDC.hInstance, 8
    CopyMemory uTDC_VBA7.data(16), uTDC.dwFlags, 4: CopyMemory uTDC_VBA7.data(20), uTDC.dwCommonButtons, 4: CopyMemory uTDC_VBA7.data(24), uTDC.pszWindowTitle, 8
    CopyMemory uTDC_VBA7.data(32), uTDC.pszMainIcon, 8: CopyMemory uTDC_VBA7.data(40), uTDC.pszMainInstruction, 8: CopyMemory uTDC_VBA7.data(48), uTDC.pszContent, 8
    CopyMemory uTDC_VBA7.data(56), uTDC.cButtons, 4: CopyMemory uTDC_VBA7.data(60), ptrBtn, 8: CopyMemory uTDC_VBA7.data(68), uTDC.nDefaultButton, 4
    CopyMemory uTDC_VBA7.data(72), uTDC.cRadioButtons, 4: CopyMemory uTDC_VBA7.data(76), ptrRbn, 8: CopyMemory uTDC_VBA7.data(84), uTDC.nDefaultRadioButton, 4
    CopyMemory uTDC_VBA7.data(88), uTDC.pszVerificationText, 8: CopyMemory uTDC_VBA7.data(96), uTDC.pszExpandedInformation, 8: CopyMemory uTDC_VBA7.data(104), uTDC.pszExpandedControlText, 8
    CopyMemory uTDC_VBA7.data(112), uTDC.pszCollapsedControlText, 8: CopyMemory uTDC_VBA7.data(120), uTDC.pszFooterIcon, 8: CopyMemory uTDC_VBA7.data(128), uTDC.pszFooter, 8
    CopyMemory uTDC_VBA7.data(136), uTDC.pfCallback, 8: CopyMemory uTDC_VBA7.data(144), uTDC.lpCallbackData, 8: CopyMemory uTDC_VBA7.data(156), uTDC.CXWidth, 4

    hr = TaskDialogIndirect_VBA7(uTDC_VBA7, pnButton, pnRadButton, pfVerify)    
#Else
hr = TaskDialogIndirect(uTDC, pnButton, pnRadButton, pfVerify)
#End If

(If you wanted to see the full class, it's on GitHub)

Jon
  • 21
  • 3