top of page

TaskDialogIndirect ... supercharged MsgBox

  • Writer: John
    John
  • 11 hours ago
  • 9 min read
This post is one of a series providing implementation examples of Windows API Functions, Types, Enums and Consts using VBA. The code in this post can be used as-is, however, if you regularly (or even just occasionally) work with Windows API declarations in VBA, you may want to see the posts Automatically add Windows API declaration(s) and Using 'F1' to view Windows API web pages which explain some of the functionality that can be added to the VBE by VBE_Extras.

This is the third of four posts relating to TaskDialog and TaskDialogIndirect. This post provides an example of how to use the TaskDialogIndirect Windows API Function along with some associated Types, Enums and Consts. To make the code in this post work, you will also need the code from the first post in the series, TaskDialog and TaskDialogIndirect helper functionality.


TaskDialogIndirect provides functionality way beyond that of a normal MsgBox (and way beyond that of a normal TaskDialog ... for which see the  TaskDialog ... MsgBox plus post) - ignoring the fact that it can also act as a progress bar (I'll write a post about that separately), TaskDialogIndirect can include:


  • Both a 'main instruction' heading and 'content' within the body of the dialog

  • An icon in the body of the dialog (either a built-in icon or a custom icon)

  • Radio buttons

  • Either buttons with custom text or command links

  • Standard buttons ... lots of them (including a 'Help' button)

  • A verification checkbox

  • A footer

  • An 'expanded information' area

  • You can set a specific width (or allow it to set its own width)

  • Flags to control other behaviour such as displaying right-to-left

  • A callback to handle clicks on hyperlinks (which can be in the content, the footer or the 'expanded information' area), clicks of the 'Help' button, and provide custom handling for various other 'events'


... and other than the buttons with custom text and the command links, you can show them all at once (if you really want to confuse your end-users).


Here are some examples along with example code of how to generate these dialogs using the CTaskDialogIndirect Class Module which is also provided below ...


A 'basic' example


A basic TaskDialog

A 'many buttons' example


TaskDialog with many buttons

... so far, these are the same as for TaskDialog other than selecting the default button


A 'custom text' buttons example


A task dialog with custom buttons


An example with a custom icon


... the icon can be loaded from .ico, .exe or .dll files or via a handle if you have already loaded an icon separately


A task dialog with a custom icon


An example with command links


A task dialog with command links

An example with radio buttons


A task dialog with radio buttons

With a footer


A task dialog with a footer

With a verification checkbox


A task dialog with a verification check box

With 'expanded information'


A task dialog with an 'expanded information' area

Right-to-left layout and sized-to-content


A task dialog with with a right-to-left layout and sized-to-content

With a 'Help' button


... and see the callback code, below, to handle the Help button click.


A task dialog with a 'Help' button


With hyperlinks in the content, footer and 'expanded information' area


... and see the callback code, below, to handle the hyperlink clicks.



A task dialog with hyperlinks


And ... with nearly everything at once


... please don't do this!


A task dialog with ... nearly everything

The Class Module code


So this is the code that actually generates the TaskDialogIndirect ... you should add this to your Project as a Class Module named CTaskDialogIndirect ...



The code is documented to explain what is going on. Some things to note are:


  • It includes a TODO that you will want to review ... you can locate TODO comments in your code using the Tasks command of VBE_Extras - see Sometimes it's just the little things - #4: Tasks (aka TODOs) for details

  • TaskDialog (and TaskDialogIndirect) require Common Controls version 6. If the code is running in a host application that is using Common Control version 5 then the Show() Function includes code to create and activate (and subsequently deactivate and release) an 'activation context' which temporarily loads a Common Controls version 6 manifest. See the TaskDialog and TaskDialogIndirect helper functionality post for further details.


... but the main thing to note in the code is the way that the two Types are declared ... as follows ...


The TASKDIALOGCONFIG and TASKDIALOG_BUTTON Types


First, some terminology. The C++ (the language of the Windows API) equivalent of a VBA Type is a struct (short for structure). When reading C++ documentation and you see struct ... think Type.

If you read through the code, you may have noticed the declarations of the TASKDIALOGCONFIG and TASKDIALOG_BUTTON Types were odd. And this is the challenge with TaskDialogIndirect. The declaration of the TaskDialogIndirect Function itself is simple. But the Types are not. The challenge is in the way that the members are 'packed'.


Types are laid out in memory sequentially, in the order of the members. Normally, each member is automatically aligned with memory boundaries ... these boundaries define individual 'chunks' of memory. In 32-bit systems, these chunks consist of 4 bytes (32 bits). In 64-bit systems, these chunks are 8 bytes (64 bits). The rule is that, normally ('normally' is the key word here!), any one member will be within a single chunk (it will not cross a memory boundary).


So, on a 32-bit device, a Type consisting of an Integer (2 bytes) followed by a LongPtr (4 bytes on a 32-bit device) will use 8 bytes ... 2 bytes for the Integer followed by 2 bytes of 'padding' (to prevent the LongPtr crossing over into another chunk) followed by 4 bytes for the LongPtr.


On a 64-bit device, the same Type will use 16 bytes (the LongPtr now uses 8 bytes ... 64-bits) ... 2 bytes for the Integer followed by 6 bytes of 'padding' (to prevent the LongPtr crossing over into another chunk) followed by 8 bytes for the LongPtr.


So far so clear?


Both C++ (which is the language of the Windows API) and VBA use the same rules. What can go wrong?


Well, what goes wrong is that word 'normally'. C++ can use different rules. And here it does. If you look in the C++ header file "commctrl.h" in which both TASKDIALOGCONFIG and TASKDIALOG_BUTTON are defined, before they are defined you will see this ...


#include <pshpack1.h>


... the "abstract" for which say "turns 1 byte packing of structures on (that is, it disables automatic alignment of structure fields)"... and then just after the definitions you will see this ...


#include <poppack.h>


... the "abstract" for which says "turns packing of structures off (that is, it enables automatic alignment of structure fields)". In other words, for the definitions of these 2 Types, the 'normal' alignment to memory boundaries is switched off ... there is no padding between each member, the next member starts immediately following the previous. And the problem for us mere VBA developers is that there is no equivalent way to do this in VBA. VBA will add what it thinks is (and 99.9% of the time, actually is) the correct padding to avoid members crossing a memory boundary.


The other thing to know is this: when working in 32-bits, the definitions of both TASKDIALOGCONFIG and TASKDIALOG_BUTTON are entirely made up of 4 byte members ... ie every member is a Long. So, in 32-bits, it makes no difference at all that a non-standard memory alignment is being used as no padding is ever required ... every member neatly aligns to a chunk and no memory boundaries are crossed. And, in 64-bits ... it all goes wrong. The first member of TASKDIALOGCONFIG is cbSize which is a UINT which is 4 bytes ... a Long in VBA. The second member hwndParent is a HWND which is 8 bytes ... a LongPtr in VBA. As we now know, the C++ definition places hwndParent in memory immediately after cbSize with no padding. But VBA inserts (the 'normal') 4 bytes of padding and so places hwndParent in memory 8 bytes after the first byte of cbSize. And it carries on from there with the placement in memory of the members in C++ and VBA drifting further and further apart (though it doesn't really matter that they drift further and further apart because even 1 byte apart is 1 byte too much).


So ... the workaround. Is to declare the entire Type as an array of Longs ... 1 Long for each 'actual Long' (eg such as cbSize) and 2 Longs for each 'actual LongPtr' (eg such as hwndParent). In 64-bits, TASKDIALOGCONFIG is declared as

... (in 32-bits, the bounds are 0 to 23). As everything is now a Long then everything lines up with the required memory boundaries (in both 32-bits and 64-bits). But we've lost any real sense of where each member is within the array of Longs. And that's where the TDC Enum comes in to play ... it's specific purpose is to know where the members are within the array and it is declared separately for 64-bits and 32-bits ... this is the 64-bit declaration ...

... the 32-bit definition uses exactly the same Enum member names, in exactly the same order, but uses different values to represent the different locations (as every member uses 4 bytes, whereas the 64-bit definition has a mix of members using 4 or 8 bytes).


TASKDIALOG_BUTTON, which only has 2 members, uses the same principle with this being the (64-bit) Type declaration ...

... and this being the associated (64-bit) Enum ...

So now we know where a member is within the array of bytes and setting the right member is simple ... in 32-bits. Because a Long is a Long ... you have a value ... you assign it.


Again, though, there's a challenge in 64-bits with the members consisting of 8 bytes because we've broken them down into two Longs. This is where the LongLongToLongs Sub from UtilsTaskDialog (see the TaskDialog and TaskDialogIndirect helper functionality post) comes in. Just like it sounds, it takes a LongPtr (which, on a 64-bit device, is an 8-byte LongLong) value that we want to assign to a member and breaks it down into 2 Longs ... one Long for the 4 high bytes and one Long for the 4 low bytes ... and then those values can be assigned to the relevant Longs.


This, then, allows VBA to represent the structures in memory in the same way as C++.


So what's the 'union'?


If you look at the TDC Enum above (which represents the members in the definition of the TASKDIALOGCONFIG struct), you will notice that members hMainIcon and pszMainIcon are in a 'union' (as are members hFooterIcon and pszFooterIcon). And in the TDC Enum, the value for hMainIcon is the same as that for pszMainIcon. How can two members (one being a handle ... the 'h' in 'hMainIcon' ... and one being a pointer to a null terminated string ... the 'psz' in 'pszMainIcon') use the same bytes in memory?


Well, this is the whole point of a union. Those bytes define either one or the other member as only one or the other can ever be required. How does the TaskDialogIndirect Function (the 'consumer' of TASKDIALOGCONFIG) know whether hMainIcon or pszMainIcon is being specified in those bytes? The answer is the TDF_USE_HICON_MAIN flag that can be set (or not) in the dwFlags member of TASKDIALOGCONFIG. If the flag is set then the union contains hMainIcon, and if it's not set then it contains pszMainIcon.


The same principle applies for hFooterIcon and pszFooterIcon with the TDF_USE_HICON_FOOTER flag.


Note that unions can have more than 2 possible states (see the INPUT struct for an example with 3 states). But, basically, unions just save memory by using the same bytes for different members where only 1 of those members is required at any one time.


The callback


The examples that include a Help button and/or hyperlinks use the SetCallback member of CTaskDialogIndirect to assign a Function as the callback in order to handle clicks of the Help button and clicks of the hyperlinks.


The first thing to note is that the value passed in to SetCallback is a Long/Ptr being the address of the Function to be called as the callback. The UtilsTaskDialog.FunctionPointerToValue Function is used to create this Long/Ptr using VBA's AddressOf keyword.


It is essential that the Function passed in to UtilsTaskDialog.FunctionPointerToValue uses the exact 'signature' as is shown in the example code (below) for the TaskDialogCallbackProc Function ... the actual name of the Function and the names of the parameters are not important (I'd suggest you use the same parameter names ... the name of the Function would need to change if, for example, you wanted to have multiple callbacks in the same Module) but the value types of the parameters, the fact that they are all ByVal and the return type of the Function most all be as shown. Additionally, the Function must be in a standard Module.


Having declared the Function and assigned it via SetCallback, then it's really up to you to handle the clicks of the Help button and hyperlinks. The msg parameter contains a value indicating why the callback has been called ... this will be one of the TDN Enum members. The header documentation of the example TaskDialogCallbackProc Function, below, explains the other parameters and for more information see the MS Docs. The below example demonstrates handling clicks of both the Help button and hyperlinks, but the callback will be called for many other 'events' which you can handle, if you want, to provide custom behaviour.


Also, I advise always including error handling in the callback. Unhandled errors can result in unexpected behaviour in the host application, including the host crashing.


Finally ...


The code here is intended to be an example of using the specific Windows API Functions / Types / Enums and Consts … it is not intended to be production-ready code … you may want to extend and re-work the code to be production ready, for example: the Show() Function actually returns a Boolean to indicate whether the dialog was successfully shown; this code has no error handling; the TODO (mentioned above) should be reviewed.



Comments


bottom of page