Skip to content

Commit

Permalink
Implement the Windows Task Dialog (#1133)
Browse files Browse the repository at this point in the history
Fixes #146
  • Loading branch information
kpreisser authored Apr 18, 2020
1 parent dc8dfba commit 5fbdbde
Show file tree
Hide file tree
Showing 59 changed files with 9,089 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Windows Task Dialog Issue

## Access Violation when receiving [`TDN_NAVIGATED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-navigated) within [`TDN_BUTTON_CLICKED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-button-clicked)

An Access violation can occur when receiving a navigation notification ([`TDN_NAVIGATED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-navigated))
within a [`TDN_BUTTON_CLICKED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-button-clicked)
notification (e.g. due to running the message loop) and then returning `S_OK`.

* Run the code.
* Click on the "My Custom Button" button.
* Notice that the dialog navigates and an inner dialog shows.
* Close the inner dialog by clicking the OK button.
* **Issue:** In most cases, an access violation occurs now (if not, try again a
few times).
* Notice the problem also occurs when you (after the navigation) first close
the original dialog, and then close the inner dialog.
* The problem can be avoided by returning `S_FALSE` from the `TDN_BUTTON_CLICKED`
notification.

```cpp
#include "stdafx.h"
#include "CommCtrl.h"

HRESULT WINAPI TaskDialogCallbackProc(_In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam, _In_ LONG_PTR lpRefData)
{
switch (msg) {
case TDN_BUTTON_CLICKED:
if (wParam == 100) {
TASKDIALOGCONFIG* navigationConfig = new TASKDIALOGCONFIG;
*navigationConfig = { 0 };
navigationConfig->cbSize = sizeof(TASKDIALOGCONFIG);

navigationConfig->pszMainInstruction = L"After navigation !!!";
navigationConfig->pszContent = L"Text";
navigationConfig->pszMainIcon = MAKEINTRESOURCEW(0xFFF7);

// Navigate the dialog.
SendMessageW(hwnd, TDM_NAVIGATE_PAGE, 0, (LPARAM)navigationConfig);
delete navigationConfig;

// After that, run the event loop by show an inner dialog.
TaskDialog(nullptr, nullptr, L"Inner Dialog", L"Inner Dialog", nullptr, 0, 0, nullptr);
}

break;
}

return S_OK;
}

void ShowTaskDialogNavigationInButtonClickedNotification()
{
TASKDIALOGCONFIG* tConfig = new TASKDIALOGCONFIG;
*tConfig = { 0 };
tConfig->cbSize = sizeof(TASKDIALOGCONFIG);

tConfig->nDefaultRadioButton = 100;
tConfig->pszMainInstruction = L"Before navigation";
tConfig->pfCallback = TaskDialogCallbackProc;

// Create 50 radio buttons.
int radioButtonCount = 50;
TASKDIALOG_BUTTON* radioButtons = new TASKDIALOG_BUTTON[radioButtonCount];
for (int i = 0; i < radioButtonCount; i++) {
radioButtons[i].nButtonID = 100 + i;
radioButtons[i].pszButtonText = L"My Radio Button";
}
tConfig->pRadioButtons = radioButtons;
tConfig->cRadioButtons = radioButtonCount;

// Create a custom button.
TASKDIALOG_BUTTON* customButton = new TASKDIALOG_BUTTON;
customButton->nButtonID = 100;
customButton->pszButtonText = L"My Custom Button";
tConfig->pButtons = customButton;
tConfig->cButtons = 1;

int nButton, nRadioButton;
BOOL checkboxSelected;
TaskDialogIndirect(tConfig, &nButton, &nRadioButton, &checkboxSelected);

delete customButton;
delete radioButtons;
delete tConfig;
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

ShowTaskDialogNavigationInButtonClickedNotification();

return 0;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Windows Task Dialog Issue

## Access Violation when receiving [`TDN_NAVIGATED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-navigated) within [`TDN_RADIO_BUTTON_CLICKED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-radio-button-clicked)

An Access Violation occurs when receiving a navigation notification
([`TDN_NAVIGATED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-navigated))
within a [`TDN_RADIO_BUTTON_CLICKED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-radio-button-clicked)
notification (e.g. due to running the message loop).

* Run the code and select one of the radio buttons.
* Notice the dialog has navigated and an inner dialog is opened.
* Close the inner dialog.
* **Issue:** Notice the application crashes with an access violation.

```cpp
#include "stdafx.h"
#include "CommCtrl.h"

HRESULT WINAPI TaskDialogCallbackProc(_In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam, _In_ LONG_PTR lpRefData)
{
switch (msg) {
case TDN_RADIO_BUTTON_CLICKED:
// When the user clicked the second radio button, navigate the dialog, then show an inner dialog.

// Navigate
TASKDIALOGCONFIG* navigationConfig = new TASKDIALOGCONFIG;
*navigationConfig = { 0 };
navigationConfig->cbSize = sizeof(TASKDIALOGCONFIG);

navigationConfig->pszMainInstruction = L"After navigation !!";

SendMessageW(hwnd, TDM_NAVIGATE_PAGE, 0, (LPARAM)navigationConfig);
delete navigationConfig;

// After navigating, run the event loop by showing an inner dialog.
TaskDialog(nullptr, nullptr, L"Inner Dialog", L"Inner Dialog", nullptr, 0, 0, nullptr);

break;
}

return S_OK;
}

void ShowTaskDialogRadioButtonsBehavior()
{
TASKDIALOGCONFIG* tConfig = new TASKDIALOGCONFIG;
*tConfig = { 0 };
tConfig->cbSize = sizeof(TASKDIALOGCONFIG);

tConfig->dwFlags = TDF_NO_DEFAULT_RADIO_BUTTON;
tConfig->pszMainInstruction = L"Before Navigation";
tConfig->pfCallback = TaskDialogCallbackProc;

// Create 2 radio buttons.
int radioButtonCount = 2;

TASKDIALOG_BUTTON* radioButtons = new TASKDIALOG_BUTTON[radioButtonCount];
for (int i = 0; i < radioButtonCount; i++) {
radioButtons[i].nButtonID = 100 + i;
radioButtons[i].pszButtonText = i == 0 ? L"Radio Button 1" : L"Radio Button 2";
}
tConfig->pRadioButtons = radioButtons;
tConfig->cRadioButtons = radioButtonCount;

int nButton, nRadioButton;
BOOL checkboxSelected;
TaskDialogIndirect(tConfig, &nButton, &nRadioButton, &checkboxSelected);

delete radioButtons;
delete tConfig;
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

ShowTaskDialogRadioButtonsBehavior();

return 0;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Windows Task Dialog Issue

## [`TDN_BUTTON_CLICKED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-button-clicked) notification sent twice when pressing a button with its access key

When you "click" a button by pressing its access key (mnemonic) and the dialog
is still open when the message loop continues, the [`TDN_BUTTON_CLICKED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-button-clicked)
notification is sent twice instead of once.

* Run the code.
* Click one of the buttons with the mouse, or press space or enter, and notice
that the counter increments by 1.
* **Issue:** Press <kbd>Alt</kbd>+<kbd>M</kbd> or <kbd>Alt</kbd>+<kbd>N</kbd> and
notice the counter increments by 2.

```cpp
#include "stdafx.h"
#include "CommCtrl.h"

int counter = 0;
wchar_t* counterTextBuffer;
HRESULT WINAPI ShowTaskDialogButtonClickHandlerCalledTwice_Callback(_In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam, _In_ LONG_PTR lpRefData) {
switch (msg) {
case TDN_BUTTON_CLICKED:
// Increment the counter.
counter++;
swprintf_s(counterTextBuffer, 100, L"Counter: %d", counter);
SendMessageW(hwnd, TDM_SET_ELEMENT_TEXT, TDE_MAIN_INSTRUCTION, (LPARAM)counterTextBuffer);

// When the user clicks the custom button, return false so that the dialog
// stays open.
if (wParam == 100) {
return S_FALSE;
}
else if (wParam == IDNO) {
// Otherwise, when the user clicks the common button, run the message loop.
MSG msg;
int bRet;
while ((bRet = GetMessageW(&msg, nullptr, 0, 0)) != 0) {
if (bRet == -1) {
// Error
}
else {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}

break;
}

return S_OK;
}

void ShowTaskDialogButtonClickHandlerCalledTwice() {
counterTextBuffer = new wchar_t[100];

TASKDIALOGCONFIG* tConfig = new TASKDIALOGCONFIG;
*tConfig = { 0 };
tConfig->cbSize = sizeof(TASKDIALOGCONFIG);

tConfig->dwFlags = TDF_USE_COMMAND_LINKS;
tConfig->pszMainInstruction = L"Text...";
tConfig->pfCallback = ShowTaskDialogButtonClickHandlerCalledTwice_Callback;

// Create a custom button and a common ("No") button.
tConfig->dwCommonButtons = TDCBF_NO_BUTTON;

TASKDIALOG_BUTTON customButton = { 0 };
customButton.nButtonID = 100;
customButton.pszButtonText = L"&My Button";
tConfig->pButtons = &customButton;
tConfig->cButtons = 1;

int nButton, nRadioButton;
BOOL checkboxSelected;
TaskDialogIndirect(tConfig, &nButton, &nRadioButton, &checkboxSelected);

delete tConfig;
delete[] counterTextBuffer;
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

ShowTaskDialogButtonClickHandlerCalledTwice();

return 0;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Windows Task Dialog Issue

## Infinite loop with radio buttons

An infinite loop occurs when sending a
[`TDM_CLICK_RADIO_BUTTON`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdm-click-radio-button) message within a
[`TDN_RADIO_BUTTON_CLICKED`](https://docs.microsoft.com/en-us/windows/desktop/Controls/tdn-radio-button-clicked) notification.

* Run the code and select the second radio button.
* **Issue:** Notice that the GUI doesn't respond any more. When debugging, you can see that
the callback is flooded with `TDN_RADIO_BUTTON_CLICKED` notifications even though we
don't send any more messages to the dialog.

```cpp
#include "stdafx.h"
#include "CommCtrl.h"

bool done = false;
HRESULT WINAPI TaskDialogCallbackProc(_In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam, _In_ LONG_PTR lpRefData)
{
switch (msg) {
case TDN_RADIO_BUTTON_CLICKED:
// When the user clicked the second radio button, select the first one.
// However, do this only once.
if (wParam == 101 && !done) {
done = true;
SendMessageW(hwnd, TDM_CLICK_RADIO_BUTTON, 100, 0);
}
break;
}

return S_OK;
}

void ShowTaskDialogRadioButtonsBehavior()
{
TASKDIALOGCONFIG* tConfig = new TASKDIALOGCONFIG;
*tConfig = { 0 };
tConfig->cbSize = sizeof(TASKDIALOGCONFIG);

tConfig->dwFlags = TDF_NO_DEFAULT_RADIO_BUTTON;
tConfig->pszMainInstruction = L"Radio Button Example";
tConfig->pfCallback = TaskDialogCallbackProc;

// Create 2 radio buttons.
int radioButtonCount = 2;

TASKDIALOG_BUTTON* radioButtons = new TASKDIALOG_BUTTON[radioButtonCount];
for (int i = 0; i < radioButtonCount; i++) {
radioButtons[i].nButtonID = 100 + i;
radioButtons[i].pszButtonText = i == 0 ? L"Radio Button 1" : L"Radio Button 2";
}
tConfig->pRadioButtons = radioButtons;
tConfig->cRadioButtons = radioButtonCount;

int nButton, nRadioButton;
BOOL checkboxSelected;
TaskDialogIndirect(tConfig, &nButton, &nRadioButton, &checkboxSelected);

delete radioButtons;
delete tConfig;
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

ShowTaskDialogRadioButtonsBehavior();

return 0;
}
```
Loading

0 comments on commit 5fbdbde

Please sign in to comment.