Der Vista-TaskDialog

Achtung! Das gezeigte Beispiel läuft nur unter Windows Vista und benötigt zwingend ein Manifest, das unter dem Namen "XP-Manifest" bekannt geworden ist.

TaskDialog

Den TaskDialog von Windows Vista könnte man als erweiterte Form der MessageBox ansehen. Und für die einfachere Variante des Dialogs mag das sogar zutreffen. Man muss lediglich beachten, dass der Dialog zwei Inhaltsangaben besitzt. Einmal die so genannte pszMainInstruction, die sozusagen als kurze Zusammenfassung der Meldung dient. Wie eine Art Überschrift steht sie innerhalb des Dialogs und lenkt die Aufmerksamkeit auf sich. Darunter befindet sich mit pszContent eine erweiterte Angabe zur Meldung. Die Deklaration des Dialogs sieht wie folgt aus:

function TaskDialog(hwndParent: HWND;                          // Fensterhandle
                    hInstance: longword;                       // Instanz
                    pszWindowTitle: PWideChar;                 // Fenstertitel
                    pszMainInstruction : PWideChar;            // Haupttext
                    pszContent: PWideChar;                     // erweiterte Info
                    dwCommonButtons: dword;                    // Dialogbuttons
                    pszIcon : PWideChar;                       // Symbol
                    var pnButton: integer): HRESULT; stdcall;  // Dialog-Rückgabe

hwndParent ist das Fensterhandle Ihrer Anwendung bzw. kann auch Null sein. hInstance ist das Instanzen-Handle Ihrer Anwendung. Sie benötigen es, wenn Sie bspw. Strings aus den Ressourcen laden wollen. Andernfalls setzen Sie es auch auf Null. pszWindowTitle ist der Titel der Dialogbox. pszMainInstruction und pszContent hatte ich bereits erwähnt. dwCommonButtons ist ein DWORD-Wert, mit dem Sie die Buttons definieren können, die im Dialog angezeigt werden sollen. Möglich sind dabei (sinnvolle!) Kombinationen von

KonstanteBedeutung
TDCBF_OK_BUTTONOK
TDCBF_YES_BUTTONJa
TDCBF_NO_BUTTONNein
TDCBF_CANCEL_BUTTONAbbrechen
TDCBF_RETRY_BUTTONWiederholen
TDCBF_CLOSE_BUTTONSchließen


pszIcon kann entweder der Name einer Icon-Resource sein (wenn hInstance nicht Null ist), oder Sie verwenden einen der vordefinierten Werte, den Sie aber mit Hilfe von "MAKEINTRESOURCEW" in einen PWideChar umwandeln müssen:

KonstanteBedeutung
TD_ICON_BLANKkein Symbol
TD_ICON_WARNINGgelbes Warndreieck
TD_ICON_QUESTIONFragezeichensymbol
TD_ICON_ERRORrotes Fehlersymbol
TD_ICON_INFORMATIONInformationssymbol
TD_ICON_SHIELDVista-Schildsymbol

Beachten müssen Sie beim Aufruf, dass die Funktion "TaskDialog" ein Rückgabeergebnis vom Typ HRESULT hat. Ist dieses Ergebnis nicht S_OK, wird der Dialog auch nicht angezeigt. Mein kleines Beispielprogramm (dazu später mehr) macht sich das übrigens zunutze und zeigt eine Exception an, wenn der Dialog nicht korrekt aufgerufen werden konnte.
Das Ergebnis, das den Anwender mehr interessiert, ist die Auswahl des Buttons, und diese wird mit Hilfe der letzten Variablen beim Funktionsaufruf zurückgeliefert. Das Ergebnis entspricht der Standard-Dialogbox, weil je nach gewähltem Button IDOK, IDYES usw. als Ergebnis erscheint. Ein recht einfacher Aufruf des Dialogs könnte also wie folgt aussehen:

TaskDialog(0, 0, 'TaskDialog-Demo', 'Exception auslösen',
  'Wollen Sie eine Exception auslösen und die TaskDialogIndirect-Funktion sehen?',
  TDCBF_YES_BUTTON or TDCBF_NO_BUTTON, MAKEINTRESOURCEW(TD_ICON_QUESTION),
  DialogResult);
if DialogResult = IDYES then // raise exception

was dann so aussieht:



TaskDialogIndirect

Und damit sind wir auch schon bei "TaskDialogIndirect". Prinzipiell handelt es sich um die selbe Sache. Sie geben nur diesmal den Text für den Inhalt usw. nicht mehr direkt an, sondern Sie nutzen ein Record. Und das Gute dabei: hier sind mehr Dinge möglich. Vom einfachen MessageBox-Ersatz mausert sich der TaskDialog so zum echten Hingucker. Aber, wie üblich, zuerst die Deklaration:

type
  TASKDIALOGCONFIG = packed record
    cbSize : uint;
    hwndParent : HWND;
    hInstance : longword;
    dwFlags : dword;
    dwCommonButtons : dword;
    pszWindowTitle : PWideChar;
    case integer of
      0 : (hMainIcon : HICON);
      1 : (pszMainIcon : PWideChar;
           pszMainInstruction : PWideChar;
           pszContent : PWideChar;
           cButtons : uint;
           pButtons : pointer;
           iDefaultButton : integer;
           cRadioButtons : uint;
           pRadioButtons : pointer;
           iDefaultRadioButton : integer;
           pszVerificationText,
           pszExpandedInformation,
           pszExpandedControlText,
           pszCollapsedControlText : PWideChar;
           case integer of
             0 : (hFooterIcon : HICON);
             1 : (pszFooterIcon : PWideChar;
                  pszFooterText : PWideChar;
                  pfCallback : pointer;
                  lpCallbackData : pointer;
                  cxWidth : uint;));
  end;
  PTaskDialogConfig = ^TASKDIALOGCONFIG;
  TTaskDialogConfig = TASKDIALOGCONFIG;
function TaskDialogIndirect(ptc : PTaskDialogConfig; pnButton: PInteger; pnRadioButton: PInteger; pfVerificationFlagChecked: PBool): HRESULT; stdcall;

Wie man sehen kann, hat "TaskDialogIndirect" gleich zwei verschiedene Felder für die Rückgabe des Ergebnisses. Die Variable pnButton kennen Sie bereits von "TaskDialog". Sie enthält das Dialogergebnis (OK-Button, Ja-Button, usw.), wenn man normale Schaltflächen nutzt. Wenn Sie stattdessen Radiobuttons verwenden, dann müssen Sie auch pnRadioButton abfragen. Der einzige Unterschied zum einfachen Dialog ist, dass ich beide Variablen als Zeiger auf einen integer deklariert habe, wodurch man das nicht benötigte Feld auch einfach mit nil ignorieren kann.

Das Record, das Sie am Anfang sehen können, enthält alle Möglichkeiten des Dialogs. Sie müssen es zuerst mit seiner Größe initialisieren.

  ZeroMemory(@tc, sizeof(tc));
  tc.cbSize := sizeof(tc);

Seine Vorteile entfaltet "TaskDialogIndirect" mit zusätzlichen Flags und Angaben. So ist es möglich, gewisse Textbereiche auszublenden und auf User-Wunsch anzuzeigen. Damit eignet sich der Dialog hervorragend für Exception-Behandlungen, in denen der Anwender zuerst nur den Fehler an sich genannt bekommt. Auf Wunsch kann er dann zum Beispiel die Exception einblenden.

Genau das macht eigentlich auch mein Beispielprogramm. Wenn Sie bei "TaskDialog" Ja wählen, dann wird eine Exception ausgelöst, die abgefangen und von "TaskDialogIndirect" dargestellt wird. Ausgehend vom eben gezeigten Aufruf möchte ich nur die neuen Zeilen hervorheben:

  ZeroMemory(@tc, sizeof(tc));
  tc.cbSize := sizeof(tc);
  tc.hwndParent := 0;
  tc.hInstance := 0;
  tc.dwFlags := TDF_ENABLE_HYPERLINKS or TDF_EXPAND_FOOTER_AREA;
  tc.dwCommonButtons := TDCBF_CLOSE_BUTTON;
  tc.pszWindowTitle := 'TaskDialogIndirect-Demo';
  tc.pszMainIcon := MAKEINTRESOURCEW(TD_ICON_ERROR);
  tc.pszMainInstruction := 'Exception';
  tc.pszContent := 'Es wurde eine Exception ausgelöst ...';
  tc.pszExpandedInformation := PWideChar(ExceptionMsg);
  tc.pszExpandedControlText := 'Details';
  tc.pszCollapsedControlText := 'Details';
  TaskDialogIndirect(@tc, @DialogResult, nil, nil);

Die letzten drei rot markierten Zeilen sind für den auf Wunsch anzeigbaren Text verantwortlich. Der Dialog erzeugt ein Label mit der Aufschrift "Details". Davor befindet sich ein Pfeil, mit dem Sie am unteren Rand die Meldung (pszExpandedInformation) ausklappen können. Wenn Sie für pszExpandedControlText etwas anderes wählen würden, würde sich der Text des Labels ändern, sobald Sie den versteckten Text ausklappen. Aber für unser Beispiel reicht der Dialog in dieser Form:


Jetzt müssen wir uns noch die Flags angucken, denn damit der Dialog exakt so wie im Bild erscheint, ist ein wenig Feintuning nötig. Standardmäßig zeigt der Dialog Hyperlinks als HTML-Code an. Erst durch das Flag TDF_ENABLE_HYPERLINKS wird daraus ein anklickbarer Link wie auf einer Webseite. Und das Flag TDF_EXPAND_FOOTER_AREA sorgt dafür, dass der zusätzliche Text am unteren Rand ausgeklappt wird. Sonst würde der Text nämlich direkt unter dem pszContent-Text erscheinen. Was haben wir sonst noch?

KonstanteBedeutung
TDF_ENABLE_HYPERLINKSHyperlinks aktivieren (Achtung! evtl. Sicherheitsproblem; s. Windows SDK)
TDF_USE_HICON_MAINhMainIcon-Member nutzen
TDF_USE_HICON_FOOTERhFooterIcon-Member nutzen
TDF_ALLOW_DIALOG_CANCELLATIONDialog kann per ESC beendet werden
TDF_USE_COMMAND_LINKSCommandLinks benutzen (nur mit eigenen Buttons!)
TDF_USE_COMMAND_LINKS_NO_ICONCommandLinks ohne Symbole nutzen (nur mit eigenen Buttons!)
TDF_EXPAND_FOOTER_AREAZusatzinfos am unteren Rand, nicht im Dialogfeld selbst
TDF_EXPANDED_BY_DEFAULTZusatzinfos bereits beim Anzeigen des Dialogs ausklappen
TDF_VERIFICATION_FLAG_CHECKEDCheckbox-Häkchen setzen
TDF_TIMERTimer nutzen

(mehr Flags finden Sie im MSDN oder Windows SDK)

Hyperlinks

Auch wenn der Dialog einen Hyperlink anzeigt, passiert beim Klick darauf nichts. Dazu benötigen wir eine Callback-Funktion, die einen bestimmten Aufbau haben muss:

function TDICallbackProc(hwndDlg: HWND; uNotification: UINT;
  wp: WPARAM; lp: LPARAM; dwRefData: PDWORD): HRESULT; stdcall;

Die Funktion kann grundsätzlich S_OK als Ergebnis liefern. In dieser Funktion können wir verschiedene Benachrichtigungen behandeln. Die hier für uns interessante lautet TDN_HYPERLINK_CLICKED und wird ausgelöst, sobald ein Hyperlink im Dialog angeklickt wird. Ob es sich dabei um einen Link im Text oder im Footer handelt, spielt keine Rolle. Die URL wird als PWideString im LPARAM-Wert übermittelt:

  case uNotification of
    TDN_HYPERLINK_CLICKED:
      begin
        MessageBoxW(hwndDlg, PWideChar(lp), 'Got a Hyperlink click',
          MB_OK or MB_ICONINFORMATION);
      end;
  end;


Die Checkbox

Vielleicht kennen Sie den Artikel in der Delphi-PRAXiS, in dem Dialogboxen mit zusätzlichem Optionsfeld angesprochen worden sind. Mit "TaskDialogIndirect" kann man etwas Ähnliches nutzen. Per Feld pszVerificationText legen Sie einen Text fest, der als Checkbox angezeigt wird:

  tc.pszVerificationText := 'Klicken Sie hier!';



Mit dem Flag TDF_VERIFICATION_FLAG_CHECKED können Sie beim Initialisieren des Records auch dafür sorgen, dass die Checkbox bereits vorausgewählt ist. Mein Beispielprogramm nutzt erneut die Callback-Funktion, um dann im Dialog den Status der Checkbox anzuzeigen:

    TDN_VERIFICATION_CLICKED:
      begin
        SendMessage(hwndDlg, TDM_SET_ELEMENT_TEXT, WPARAM(TDE_MAIN_INSTRUCTION),
          LPARAM(CheckedUnchecked[bool(wp)]));
      end;

Der jeweilige Status (TRUE, FALSE) wird im WPARAM übermittelt. Mein Programm nutzt ein bool'sches String-Array, das je nach Status den Text der pszMainInstruction ändert.

Die Auswertung stellte sich nun als doch recht einfach heraus. Wenn die Dokumentationsschreiber bei Microsoft nicht wissen, was die Programmierer tun, dann ist es natürlich schwer, den Fehler zu finden. In diesem Fall vergaß man in der Deklaration von "TaskDialogIndirect" im SDK den dritten Parameter, der eigentlich für die Radiobuttons zuständig ist. Dadurch verschiebt sich der Parameter zur Auswertung der Checkbox natürlich um eine Stelle. Herausgefunden habe ich es auch nur, weil sich im Windows SDK ein CSharp-Sample befindet, dass die korrekte Deklaration zeigt. Mit anderen Worten, so klappt es:

if TaskDialogIndirect(@tc, @DialogResult, nil, @VerBoolFlag) = S_OK then
  if VerBoolFlag then
    MessageBox(0, 'Häkchen ist drin', 'Info', MB_ICONINFORMATION);


Eigene Buttons

Die Standardbuttons lassen sich leider nicht verändern. Aber Sie können eigene Buttons definieren. Dazu benötigen Sie ein Array vom Typ TASKDIALOG_BUTTON:

type
  TASKDIALOG_BUTTON = packed record
     nButtonId     : integer;
     pszButtonText : PWideChar;
  end;

Das Array sollte die von Ihnen gewünschten Buttons enthalten. Etwa so:

const
  Buttons : array[0..1] of TASKDIALOG_BUTTON =
    ((nButtonId     : 100;
      pszButtonText : 'Checkbox-Demo'),
     (nButtonId     : 101;
      pszButtonText : 'Zeig mir jetzt den Timer'));

Danach entfernen Sie die Zuweisung dwCommonButtons und nutzen stattdessen cButtons (die Anzahl der Buttons = Arraygröße) und pButtons (ein Zeiger auf das Button-Array):

  ZeroMemory(@tc, sizeof(tc));
  tc.cbSize := sizeof(tc);
  tc.hwndParent := 0;
  tc.hInstance := 0;
  tc.dwCommonButtons := TDCBF_CLOSE_BUTTON;
  tc.pszWindowTitle := 'Eigene Buttons';
  tc.pszMainIcon := MAKEINTRESOURCEW(TD_ICON_INFORMATION);
  tc.pszMainInstruction := 'Wow! Eigene Knöpfe';
  tc.pszContent := 'Wählen Sie, welche Box Sie jetzt sehen möchten.';
  tc.cButtons := length(Buttons);
  tc.pButtons := @Buttons;

und voilà:


Mit Hilfe des Flags TDF_USE_COMMAND_LINKS könnten Sie daraus auch so genannte CommandLink-Buttons machen. Das sind etwas größere Schaltflächen mit einem kleinen Pfeilsymbol. Das Ergebnis fragen Sie, wie bei den normalen Buttons, per DialogResult-Variable ab, die aber in dem Fall die von Ihnen benutzten Werte im Array zurückliefert:

  if TaskDialogIndirect(@tc, @DialogResult, nil, nil) = S_OK then
    case DialogResult of
      100:
        // "Checkbox-Demo" geklickt
      101:
        // "Zeig mir jetzt den Timer" geklickt
    end;


Radiobuttons

Als Ergänzung möchte ich noch die Variante mit Radiobuttons vorstellen. Das Beispielprogramm zeigt Ihnen dazu noch einen dritten Knopf, der im obigen Array nicht zu sehen ist. Mit diesem zusätzlichen Button kann der Dialog ein zweites Mal angezeigt werden, wobei dann aber Radiobuttons verwendet werden. Die Änderungen im Code halten sich in Grenzen. Anstelle von cButtons und pButtons benutzen wir cRadioButtons und pRadioButtons:

tc.cRadioButtons := length(Buttons) - 1;
tc.pRadioButtons := @Buttons;

Dass ich die Arraylänge um Eins verringere, hat natürlich einen Grund. Ich will jetzt den dritten Button ausblenden, weil er hier nicht benötigt wird. Durch die Verwendung von Radiobuttons erhält der Dialog automatisch einen OK-Button, der letztlich der Bestätigung dient. Diese Vorgabe kann nur umgangen werden, indem Sie entweder weitere Standardbuttons in dwCommonButtons benutzen, oder indem Sie andere eigene Buttons per cButtons und pButtons festlegen. Das Ergebnis meiner Anpassung ist jedenfalls dieser Dialog:


Eine Auswertung der Funktion könnte wie folgt aussehen, wobei Sie hier dann auch den dritten Parameter nutzen müssen:

if TaskDialogIndirect(@tc, @DialogResult, @RadioResult, nil) = S_OK then
  if DialogResult = IDOK then
    case RadioResult of
      101:
        // Checkbox-Demo
      102:
        // Timer-Demo
    end;


Eigene Symbole

Sowohl in "TaskDialog" als auch in "TaskDialogIndirect" ist es möglich, eigene Symbole zu verwenden. Idealerweise sind die gewünschten Symbole in den Programmressourcen eingebunden. Dann lassen sie sich einfach durch Zuweisung von hInstance und internem Ressourcenbezeichner verwenden:

//
// TaskDialog
//
TaskDialog(0,
  hInstance,             // hInstance
  'Titel',
  'MainInstruction',
  'Text',
  TDCBF_OK_BUTTON,
  MAKEINTRESOURCEW(100), // Symbolressource
  DialogResult);
// // TaskDialogIndirect // ZeroMemory(@tc, sizeof(tc)); tc.cbSize := sizeof(tc); tc.hInstance := hInstance; // hInstance tc.pszMainIcon := MAKEINTRESOURCEW(100); // Symbolressource

Die zweite Variante funktioniert nur bei "TaskDialogIndirect". In diesem Fall nutzen Sie zum Beispiel das hMainIcon-Flag und die API-Funktion "LoadImage". hInstance kann hierbei Null bleiben, weil es bereits in der API-Funktion benutzt wird. Auch hier sollte das Symbol in den Programmressourcen vorhanden sein. Allerdings wäre es mit besagter Funktion auch möglich, ein Symbol aus einer externen ICO-Datei zu laden.

tc.hInstance := 0;
tc.dwFlags := TDF_USE_HICON_MAIN;
tc.hMainIcon := LoadImage(hInstance, MAKEINTRESOURCE(101),
  IMAGE_ICON, 32, 32, LR_DEFAULTSIZE);

Als Beispiel eine angepasste Version des Exception-Dialogs:



Selbstschließender TaskDialog

Ausgehend von Michaels Artikel zur API-Variante möchte ich Ihnen noch die Version für "TaskDialogIndirect" zeigen. Hierzu benötigen wir das Flag TDF_CALLBACK_TIMER und natürlich auch wieder die Callback-Funktion. Durch das Flag wird in der Callback-Funktion regelmäßig die TDN_TIMER-Benachrichtigung ausgelöst. Das Windows SDK spricht von ca. 200 Millisekunden Abstand.

  tc.dwFlags := TDF_ALLOW_DIALOG_CANCELLATION or TDF_CALLBACK_TIMER;

Sie können gern genauere Berechnungen durchführen, ich jedoch habe mich für das einfachste aller Beispiele entschieden: Ich dividiere den übermittelten Wert im WPARAM durch 1000 und vergleiche ihn mit der gewünschten Zeit (10 Sekunden als Beispiel). Der WPARAM-Wert gibt die Zeit in Millisekunden an, die der Dialog bereits sichtbar ist:

    TDN_TIMER:
      begin
        timerVal := trunc(DWORD(wp) div 1000);
        tmp := WideString(Format('Bereits %d Sekunden sichtbar', [timerVal]));
        SendMessage(hwndDlg, TDM_SET_ELEMENT_TEXT, WPARAM(TDE_CONTENT),
          LPARAM(tmp));
if timerVal >= 10 then SendMessage(hwndDlg, TDM_CLICK_BUTTON, WPARAM(IDOK), 0); end;

Ist die Zeit erreicht oder überschritten, lasse ich den Dialog verschwinden, indem ich den Klick auf den OK-Button simuliere. Während der Laufzeit aktualisiere ich den Inhalt des Dialoges, so dass man ungefähr sehen kann, wie lange die Box bereits sichtbar ist:

Der Fortschrittsbalken ist ein kleines Extra. Es wird zwar durch die Nachrichtenbehandlung auch verändert, allerdings geschieht das sichtbar nach der Textänderung. Aber egal, es geht ja ohnehin erst einmal nur um die Möglichkeiten.


Downloads

TaskDialogDemo.zip Wednesday, 29-Dec-2010 23:46:02 CET 62K
2010-12-29T23:44:57 +0100, mail+homepage[at]michael-puff.de