09 May 2019

Task Dialog as a replacement for Alert

Since Vista Windows supports the Task Dialog, an extended Message Box with a lot of new features. As the message box the task dialog displays an application-defined message, title, icons and any combination of predefined push buttons. In addition it supports a verification checkbox, command links, and radio buttons. In this post I’ll show you how to use the latest GB update (version 2.56) to easily create a task dialog. At the end of this post we will create the following dialog as an replacement for the famous Alert box.

There are two APIs that create a dialog box: TaskDialog and TaskDialogIndirect. The links will take you to the MS SDK site, to the pages that formally describe the APIs. Starting with GFA-BASIC update version 2.56 the APIs are declared in the include library file commctrl.inc.lg32. To get to the actual declaration you should inspect commctrl.inc.g32 – the source file for the library.

The TaskDialog API
The TaskDialog API is declared as follows:

Declare Function TaskDialog Lib "comctl32.dll" ( _
  ByVal hwndParent As Long, ByVal hInstance As Long, _
  ByVal pszWindowTitle As Long, ByVal pszMainInstruction As Long, _
  ByVal pszContent As Long, _
  ByVal dwCommonButtons As Long, _
  ByVal pszIcon As Long, pnButton As Long) As Long

Note that only the last parameter takes a variable by reference, all others are declared as ByVal. The TaskDialog function returns the selected button through this variable. The return value of the function itself indicates success or failure.

The parameters that take a string expect a wide string, the string must be formatted as an Unicode string. I discussed Unicode strings in Ansi and Unicode. To convert an Ansi string to Unicode I’ll use the function Wide() from gfawinx.lg32, which is located in the Include directory as well. The name of this file does not include the inc part, because it is a library with executable code, which will add to the program’s size (be it minimal). The include files (those that include the inc extension in the filename) only contain declarations and definitions that don’t add to the program’s size.

If you didn’t change the path to the Include directory after installing the update the Extra tab in the GB Properties should contain a valid library-path. This also means that you can include commctrl.inc.lg32 and gfawinx.lg32 as shown in this code:

$Library "commctrl.inc"
$Library "gfawinx"
OpenW 1
Print DlgTask(Me.hWnd, "Prompt", "Content", "Demo" , , -3)
Do
  Sleep
Until Me Is Nothing

Function DlgTask(hOwner As Handle, sMainText$, sContent$, _
  Optional sTitle$, Optional iButtons% = TDCBF_OK_BUTTON, _
  Optional Icon& = 0) As Long
  Local Long RetVal, lIcon
  Local String Title

  sMainText$ = Wide(sMainText$)
  sContent$ = Wide(sContent$)
  Title = Wide( Iif(IsMissing(sTitle$), App.Name, sTitle$))
  lIcon = MakeLongHiLo(0, Icon&)

  If TaskDialog(hOwner, 0, V:Title, V:sMainText$, V:sContent$, _
    iButtons%, lIcon, RetVal) == S_OK
    Return RetVal
  EndIf
EndFunc

This code produces the following dialog box at the center of the parent window:

You can pass Null to the hOwner parameter of the TaskDialog, but that would display the dialog at the center of the main screen. Not very useful when using multiple monitors with high resolutions. The hInstance parameter can be 0 because we don’t use an icon from the EXE’s resources. Instead we pass a predefined value for the icon to display (-3). The wide strings are passed by providing their address. The RetVal variable is passed by reference to receive the button’s number (>1) that is selected. The DlgTask function returns 0 if TaskDialog doesn’t return with S_OK (=0).

Although commctrl.inc defines all constants to be used with the task dialog functions, it doesn’t provide constants for the icons (GFA-BASIC versions <= 2.56). The icons are defined as follows:

Icon Value (Word)
Warning -1
Error -2
Information                    -3
Shield -4

The icon parameter must be an integer resource value created with the macro MAKEINTRESOURCE(). This macro, defined in some SDK header, creates a long where the high word is zero and the low word contains the resource identifier (Word). Here we use MakeLongHiLo() to create the integer resource value. With a newer version of GB (> 2.56) you can use the constants as defined in commctrl.inc without the need to convert with MakeLongHiLo.

The TaskDialogIndirect API
The TaskDialogIndirect function allows further fine tuning of the task dialog. For this to happen you need to fill out a structure of type TASKDIALOGCONFIG.

Declare Function TaskDialogIndirect Lib "comctl32.dll" ( _
  ByVal pTaskConfig As Long, pnButton As Long, _
  pnRadioButton As Long, pfVerificationFlagChecked As Long) As Long

By using TaskDialogIndirect you can create custom buttons rather than using predefined buttons only. Therefor it is a perfect candidate to replace the good old Alert box with a nicer version. The next example shows how to set up the TASKDIALOGCONFIG structure and pass it to the TaskDialogIndirect API to create the dialog box as shown at the beginning of this post. The Alert2 function takes the same arguments as the Alert box function. The IconAndFlag% argument specifies the icon and layout of the alert box. The MainText$ argument can specify multiple lines by using | as a separator. The ButtonText$ specifies the custom buttons and DefButton% the button to preselect. These parameters are translated to the task dialog features.

$Library "commctrl.inc"
$Library "gfawinx"
OpenW 1
Print Alert2(2, "Which procedure should|be executed", 1, "Input|Calculate|Print")
Do
  Sleep
Until Me Is Nothing

Function Alert2(IconAndFlag%, MainText$, DefButton%, ButtonText$) As Long
  Dim Icon As Word, RetVal As Long, VerFlag As Long, i As Long
  Dim sTitle As String, aBtn() As String, sVerificationText As String
  Dim tdc As TASKDIALOGCONFIG, taskBtn() As TASKDIALOG_BUTTON

  ' Provide a title (Unicode)
  sTitle = Wide(App.Name)
  sVerificationText = Wide("Don't ask again")

  ' Determine the icon
  Switch IconAndFlag% %& 7
  Case 1       : Icon = -2    ' Stop: Stop/Error icon
  Case 2, 4, 7 : Icon = -3    ' Question, Information: Information icon
  Case 3       : Icon = -1    ' Exclamation: Warning
  Case 5, 6    : Icon = -4    ' Windowsflag, Application: Shield icon
  EndSwitch

  ' Text lines are separated with |, but we need #10
  MainText$ = Replace(MainText$, "|", #10)
  MainText$ = Wide(MainText$) ' to UNICODE

  ' Copy the button text to the TASKDIALOG_BUTTON array
  StrToArr(ButtonText$, "|", aBtn())
  ReDim taskBtn(0 .. UBound(aBtn()))
  For i = 0 To UBound(aBtn())
    aBtn(i) = Wide(aBtn(i))        ' convert to Unicode
    taskBtn(i).nButtonID = i + 1   ' ID's are base 1
    taskBtn(i).pszButtonText = V:aBtn(i)
  Next

  tdc.cbSize = SizeOf(TASKDIALOGCONFIG)
  tdc.hwndParent = Me.hWnd
  tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_POSITION_RELATIVE_TO_WINDOW
  ' Right align text?
  If IconAndFlag% %& 64 Then tdc.dwFlags = tdc.dwFlags | TDF_RTL_LAYOUT
  tdc.pszWindowTitle = V:sTitle
  tdc.hMainIcon = MakeLongHiLo(0, Icon)
  tdc.pszMainInstruction = V:MainText$
  tdc.cButtons = Dim?(taskBtn())
  tdc.pButtons = V:taskBtn(0)
  tdc.nDefaultButton = DefButton%
  tdc.pszVerificationText = V:sVerificationText

  If TaskDialogIndirect(V:tdc, RetVal, 0, VerFlag) == S_OK
    ' VerFlag is 1 if checkbox is checked
    Return RetVal   ' base 1
  EndIf
EndFunc

As with the TaskDialog function all strings must be UNICODE. After setting the tdc.cbSize member to the required size the hwndParent and dwFlags members are used to position the dialog in the center of the parent window.
If the IconAndFlag% parameter specifies right aligned text it is honored by including TDF_RTL_LAYOUT in the dwFlags member.
The main text string must contain LF (#10) characters to separate multiple lines. However, the MainText$ argument will separate multiple lines using the | character. So, before converting MainText$ to a wide string we need to replace all occurrences of | with #10 characters. The Replace function is located in gfawinx.lg32.
Setting up the custom buttons requires a bit more work. First we need to create separate strings for each button text. For this to happen we use the gfawinx procedure StrToArr, which splits a string into multiple array elements. Then, in a loop, each array element is converted to a wide string. In the same loop we assign the button’s ID-value and the string pointer to an array of TASKDIALOG_BUTTONs. After initializing this array, it is assigned it to the tdc members cButtons, pButtons, and nDefaultButton.
Finally, we specify text for an additional checkbox control with the pszVerification member. When pszVerification holds a valid memory address the checkbox control is displayed, but it is enabled only if the pfVerificationFlagChecked parameter of TaskDialogIndirect specifies the address of a return variable. If this parameter is Null the check box is displayed in a disabled state. This is also true for additional option boxes. Note that you may pass Null (0) to a ByRef parameter of a declared DLL function.

No comments:

Post a Comment