From: Joachim Priesner Subject: [3/5] comctl32: Implement TaskDialogIndirect as a custom dialog box (try 2) Message-Id: <201502231342.16188.joachim.priesner@web.de> Date: Mon, 23 Feb 2015 13:42:15 +0100 The amount of functionality is the same as with the old MessageBox solution, except now both the "content" and "main instruction" texts are shown. The dialog should now also be more easily extendable. Most of this code is adapted from user32/msgbox.c. Try 2 that fixes issues found by Nikolay Sivov. --- dlls/comctl32/Makefile.in | 2 +- dlls/comctl32/comctl32.h | 6 + dlls/comctl32/comctl32.rc | 14 ++ dlls/comctl32/taskdialog.c | 412 ++++++++++++++++++++++++++++++++++++--- dlls/comctl32/tests/taskdialog.c | 17 +- 5 files changed, 409 insertions(+), 42 deletions(-) diff --git a/dlls/comctl32/Makefile.in b/dlls/comctl32/Makefile.in index e1a6812..adcb55f 100644 --- a/dlls/comctl32/Makefile.in +++ b/dlls/comctl32/Makefile.in @@ -1,7 +1,7 @@ EXTRADEFS = -D_COMCTL32_ MODULE = comctl32.dll IMPORTLIB = comctl32 -IMPORTS = uuid user32 gdi32 advapi32 +IMPORTS = uuid user32 gdi32 advapi32 shlwapi DELAYIMPORTS = winmm uxtheme C_SRCS = \ diff --git a/dlls/comctl32/comctl32.h b/dlls/comctl32/comctl32.h index b9b0574..84b8fa0 100644 --- a/dlls/comctl32/comctl32.h +++ b/dlls/comctl32/comctl32.h @@ -65,6 +65,12 @@ extern HBRUSH COMCTL32_hPattern55AABrush DECLSPEC_HIDDEN; #define IDS_SEPARATOR 1024 +/* Task dialog */ +#define IDD_TASKDIALOG 510 + +#define IDC_MAIN_INSTRUCTION 512 +#define IDC_CONTENT 513 + /* Toolbar imagelist bitmaps */ #define IDB_STD_SMALL 120 #define IDB_STD_LARGE 121 diff --git a/dlls/comctl32/comctl32.rc b/dlls/comctl32/comctl32.rc index 87e2fd1..fa8f7ea 100644 --- a/dlls/comctl32/comctl32.rc +++ b/dlls/comctl32/comctl32.rc @@ -93,6 +93,20 @@ BEGIN LISTBOX IDC_TOOLBARBTN_LBOX, 194,17,120,100,LBS_NOTIFY | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_BORDER | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP END +IDD_TASKDIALOG DIALOG 100, 80, 216, 168 +STYLE DS_MODALFRAME | DS_NOIDLEMSG | DS_SETFONT | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +BEGIN + LTEXT "", IDC_MAIN_INSTRUCTION, 32, 4, 176, 48, WS_CHILD | WS_VISIBLE | WS_GROUP | SS_NOPREFIX + LTEXT "", IDC_CONTENT, 32, 4, 176, 48, WS_CHILD | WS_VISIBLE | WS_GROUP | SS_NOPREFIX + PUSHBUTTON "OK", IDOK, 16, 56, 50, 14, WS_CHILD | WS_VISIBLE | WS_TABSTOP + PUSHBUTTON "&Yes", IDYES, 306, 56, 50, 14, WS_CHILD | WS_VISIBLE | WS_TABSTOP + PUSHBUTTON "&No", IDNO, 364, 56, 50, 14, WS_CHILD | WS_VISIBLE | WS_TABSTOP + PUSHBUTTON "Cancel", IDCANCEL, 74, 56, 50, 14, WS_CHILD | WS_VISIBLE | WS_TABSTOP + PUSHBUTTON "&Retry", IDRETRY, 190, 56, 50, 14, WS_CHILD | WS_VISIBLE | WS_TABSTOP + PUSHBUTTON "&Close", IDCLOSE, 132, 56, 50, 14, WS_CHILD | WS_VISIBLE | WS_TABSTOP +END + + LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL #define WINE_FILEDESCRIPTION_STR "Wine Common Controls" diff --git a/dlls/comctl32/taskdialog.c b/dlls/comctl32/taskdialog.c index 84e3e76..4921a5f 100644 --- a/dlls/comctl32/taskdialog.c +++ b/dlls/comctl32/taskdialog.c @@ -17,51 +17,403 @@ */ #include "comctl32.h" -#include "winuser.h" +#include "shlwapi.h" + +#include "windef.h" +#include "winbase.h" +#include "wingdi.h" +#include "winternl.h" +#include "dlgs.h" #include "wine/debug.h" +#include "wine/unicode.h" WINE_DEFAULT_DEBUG_CHANNEL(commctrl); +/* Fills "result" with a pointer to the string specified by "text", which may be a resource identifier. */ +static HRESULT TASKDIALOG_GetText(const WCHAR *text, HINSTANCE instance, const WCHAR **result) +{ + if (IS_INTRESOURCE(text)) + { + SetLastError(S_OK); + LoadStringW(instance, LOWORD(text), (LPWSTR)result, 0); + return HRESULT_FROM_WIN32(GetLastError()); + } + else + { + *result = text; + return S_OK; + } +} + +/* + * TaskDialog layout (not everything implemented yet): + * + * +------------------------------------------------------------+ + * | Window title (fallback: executable file name) X | + * +------------------------------------------------------------+ + * | MAIN Main instruction | + * | ICON | + * | HERE Content | + * | | + * | Expanded information if TDF_EXPAND_FOOTER AREA | + * | is not set. | + * | | + * | [Progress Bar or Marquee Progress Bar_____________] | + * | | + * | (*) Radio button 1 | + * | ( ) Radio button 2 | + * | | + * | => User buttons if TDF_USE_COMMAND_LINKS or | + * | TDF_USE_COMMAND_LINKS_NOICON is set | + * | | + * | => In this case, the user buttons don't appear | + * | below. | + * | | + * +============================================================+ + * | (V) Expand/collapse [User buttons] [Common buttons] | + * | [ ] Verification checkbox | + * +============================================================+ + * | ICON Footer text | + * | which can span multiple lines. | + * +============================================================+ + * | Expanded information if TDF_EXPAND_FOOTER_AREA is set | + * +------------------------------------------------------------+ + */ +static void TASKDIALOG_OnInit(HWND hwnd, TASKDIALOGCONFIG *config) +{ + HFONT normalFont, mainInstructionFont; + LOGFONTW mainInstructionFontAttributes = {0}; + HWND defaultButton = 0; + HDC hdc; + HMONITOR monitor = 0; + HRESULT result; + MONITORINFO monitorInfo; + int nButtons = 0; + int i, currentX, currentY; + int buttonWidth, buttonAreaWidth, buttonHeight; + int windowClientWidth, windowClientHeight, windowLeft, windowTop; + int windowBorderHeight, windowBorderWidth; + int mainAreaTextLeft, mainAreaTextWidth; + int contentTextWidth, contentTextHeight; + int mainInstructionWidth, mainInstructionHeight; + int spacing; + const WCHAR *contentText, *mainInstructionText, *txt; + RECT rect; + static const int commonButtons[6] = { IDOK, IDYES, IDNO, IDCANCEL, IDRETRY, IDCLOSE }; + + /* Compute spacing between the dialog elements: 5 dialog units. */ + rect.top = rect.left = 0; + rect.right = rect.bottom = 5; + MapDialogRect(hwnd, &rect); + spacing = rect.right - rect.left; + + /* Get the texts to be displayed. */ + TRACE("pszWindowTitle=%s\n", debugstr_w(config->pszWindowTitle)); + result = TASKDIALOG_GetText(config->pszWindowTitle, config->hInstance, &txt); + if (FAILED(result) || !txt) + { + /* If the supplied window title is invalid, display the executable file name instead. */ + WCHAR *windowTitleBuffer = Alloc((MAX_PATH + 1) * sizeof(WCHAR)); + if (!windowTitleBuffer) + { + EndDialog(hwnd, E_OUTOFMEMORY); + return; + } + GetModuleFileNameW(NULL, windowTitleBuffer, MAX_PATH); + SetWindowTextW(hwnd, PathFindFileNameW(windowTitleBuffer)); + Free(windowTitleBuffer); + windowTitleBuffer = NULL; + } + else SetWindowTextW(hwnd, txt); + + TRACE("pszMainInstruction=%s\n", debugstr_w(config->pszMainInstruction)); + result = TASKDIALOG_GetText(config->pszMainInstruction, config->hInstance, &mainInstructionText); + if (FAILED(result)) + { + EndDialog(hwnd, result); + return; + } + SetWindowTextW(GetDlgItem(hwnd, IDC_MAIN_INSTRUCTION), mainInstructionText); + + TRACE("pszContent=%s\n", debugstr_w(config->pszContent)); + result = TASKDIALOG_GetText(config->pszContent, config->hInstance, &contentText); + if (FAILED(result)) + { + EndDialog(hwnd, result); + return; + } + SetWindowTextW(GetDlgItem(hwnd, IDC_CONTENT), contentText); + + if (config->pszFooter) + { + FIXME("pszFooter=%s\n", debugstr_w(config->pszFooter)); + } + if (config->pszVerificationText) + { + FIXME("pszVerificationText=%s\n", debugstr_w(config->pszVerificationText)); + } + if (config->pszExpandedInformation) + { + FIXME("pszExpandedInformation=%s\n", debugstr_w(config->pszExpandedInformation)); + } + if (config->pszExpandedControlText) + { + FIXME("pszExpandedControlText=%s\n", debugstr_w(config->pszExpandedControlText)); + } + if (config->pszCollapsedControlText) + { + FIXME("pszCollapsedControlText=%s\n", debugstr_w(config->pszCollapsedControlText)); + } + + /* Add user-defined buttons. */ + if (config->cButtons) + FIXME("Custom task dialog buttons not implemented\n"); + if (config->cRadioButtons) + FIXME("Task dialog radio buttons not implemented\n"); + + /* Destroy unused common buttons. */ + TRACE("dwCommonButtons=%x\n", config->dwCommonButtons); + if (!(config->dwCommonButtons & TDCBF_YES_BUTTON)) + DestroyWindow(GetDlgItem(hwnd, IDYES)); + else nButtons++; + if (!(config->dwCommonButtons & TDCBF_NO_BUTTON)) + DestroyWindow(GetDlgItem(hwnd, IDNO)); + else nButtons++; + if (!(config->dwCommonButtons & TDCBF_CANCEL_BUTTON)) + DestroyWindow(GetDlgItem(hwnd, IDCANCEL)); + else nButtons++; + if (!(config->dwCommonButtons & TDCBF_RETRY_BUTTON)) + DestroyWindow(GetDlgItem(hwnd, IDRETRY)); + else nButtons++; + if (!(config->dwCommonButtons & TDCBF_CLOSE_BUTTON)) + DestroyWindow(GetDlgItem(hwnd, IDCLOSE)); + else nButtons++; + /* If no buttons are specified, the OK button remains. */ + if (!(config->dwCommonButtons & TDCBF_OK_BUTTON) && nButtons > 0) + DestroyWindow(GetDlgItem(hwnd, IDOK)); + else nButtons++; + + /* Position everything. */ + GetWindowRect(hwnd, &rect); + windowBorderHeight = rect.bottom - rect.top; + windowBorderWidth = rect.right - rect.left; + GetClientRect(hwnd, &rect); + windowBorderHeight -= rect.bottom - rect.top; + windowBorderWidth -= rect.right - rect.left; + + /* Set a bold font for the main instruction. */ + hdc = GetDC(hwnd); + normalFont = SelectObject(hdc, (HFONT)SendMessageW(hwnd, WM_GETFONT, 0, 0)); + + GetObjectW(normalFont, sizeof(mainInstructionFontAttributes), &mainInstructionFontAttributes); + mainInstructionFontAttributes.lfWeight = FW_BOLD; + mainInstructionFont = CreateFontIndirectW(&mainInstructionFontAttributes); + SendMessageW(GetDlgItem(hwnd, IDC_MAIN_INSTRUCTION), WM_SETFONT, (WPARAM)mainInstructionFont, 0); + + /* Calculate maximum button text width and height. */ + buttonHeight = buttonWidth = 1; + for (i = 0; i < (sizeof(commonButtons) / sizeof(commonButtons[0])); i++) + { + HWND hItem = GetDlgItem(hwnd, commonButtons[i]); + if (GetWindowLongW(hItem, GWL_STYLE) & WS_VISIBLE) + { + WCHAR buttonText[1024]; /* enough to fit a (translated) text of any of the common buttons */ + int w, h; + if (GetWindowTextW(hItem, buttonText, 1024)) + { + DrawTextW(hdc, buttonText, -1, &rect, DT_LEFT | DT_EXPANDTABS | DT_CALCRECT); + h = rect.bottom - rect.top; + w = rect.right - rect.left; + if (h > buttonHeight) buttonHeight = h; + if (w > buttonWidth) buttonWidth = w; + } + } + } + + /* Give the buttons some white space. */ + buttonHeight *= 2; + buttonWidth = max(buttonWidth, buttonHeight * 2) * 2; + + buttonAreaWidth = (buttonWidth + spacing) * nButtons - spacing; + mainAreaTextLeft = spacing; + + /* Calculate main area text size. The texts are at least as wide as the button area. */ + GetClientRect(GetDlgItem(hwnd, IDC_MAIN_INSTRUCTION), &rect); + rect.top = rect.left = rect.bottom = 0; + rect.right = max(rect.right, buttonAreaWidth); + SelectObject(hdc, mainInstructionFont); + DrawTextW(hdc, mainInstructionText, -1, &rect, + DT_LEFT | DT_EXPANDTABS | DT_WORDBREAK | DT_CALCRECT); + DeleteObject(mainInstructionFont); + mainInstructionWidth = rect.right; + mainInstructionHeight = rect.bottom; + + GetClientRect(GetDlgItem(hwnd, IDC_CONTENT), &rect); + rect.top = rect.left = rect.bottom = 0; + rect.right = max(rect.right, buttonAreaWidth); + SelectObject(hdc, normalFont); + DrawTextW(hdc, contentText, -1, &rect, + DT_LEFT | DT_EXPANDTABS | DT_WORDBREAK | DT_CALCRECT); + ReleaseDC(hwnd, hdc); + contentTextWidth = rect.right; + contentTextHeight = rect.bottom; + + mainAreaTextWidth = max(mainInstructionWidth, contentTextWidth); + windowClientWidth = max(spacing + buttonAreaWidth + spacing, + mainAreaTextLeft + mainAreaTextWidth + spacing); + if (config->cxWidth) + FIXME("Ignoring config->cxWidth.\n"); + + /* Position everything from top to bottom. */ + currentY = spacing; + + /* Position the texts in the main area. */ + if (mainInstructionHeight) + { + SetWindowPos(GetDlgItem(hwnd, IDC_MAIN_INSTRUCTION), 0, mainAreaTextLeft, currentY, + mainInstructionWidth, mainInstructionHeight, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW); + currentY += mainInstructionHeight + spacing; + } + else DestroyWindow(GetDlgItem(hwnd, IDC_MAIN_INSTRUCTION)); + + if (contentTextHeight) + { + SetWindowPos(GetDlgItem(hwnd, IDC_CONTENT), 0, mainAreaTextLeft, currentY, + contentTextWidth, contentTextHeight, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW); + currentY += contentTextHeight + spacing; + } + else DestroyWindow(GetDlgItem(hwnd, IDC_CONTENT)); + + /* Position the buttons: right-aligned */ + currentX = windowClientWidth - spacing - buttonAreaWidth; + for (i = 0; i < (sizeof(commonButtons) / sizeof(commonButtons[0])); i++) + { + HWND hItem = GetDlgItem(hwnd, commonButtons[i]); + if (GetWindowLongW(hItem, GWL_STYLE) & WS_VISIBLE) + { + if (defaultButton == 0 || config->nDefaultButton == commonButtons[i]) + defaultButton = hItem; + SetWindowPos(hItem, 0, currentX, currentY, buttonWidth, buttonHeight, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW); + currentX += buttonWidth + spacing; + } + } + + if (defaultButton) + { + SetFocus(defaultButton); + SendMessageW(defaultButton, BM_SETSTYLE, BS_DEFPUSHBUTTON, TRUE); + } + + currentY += buttonHeight + spacing; + windowClientHeight = currentY; + + /* Query parent window/desktop size and center window. */ + monitor = MonitorFromWindow(config->hwndParent ? config->hwndParent : GetActiveWindow(), + MONITOR_DEFAULTTOPRIMARY); + monitorInfo.cbSize = sizeof(monitorInfo); + GetMonitorInfoW(monitor, &monitorInfo); + windowLeft = (monitorInfo.rcWork.left + monitorInfo.rcWork.right + - (windowClientWidth + windowBorderWidth)) / 2; + windowTop = (monitorInfo.rcWork.top + monitorInfo.rcWork.bottom + - (windowClientHeight + windowBorderHeight)) / 2; + SetWindowPos(hwnd, 0, windowLeft, windowTop, + windowClientWidth + windowBorderWidth, windowClientHeight + windowBorderHeight, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW); +} + + +/************************************************************************** + * TASKDIALOG_DlgProc + * + * Dialog procedure for task dialogs. + */ +static INT_PTR CALLBACK TASKDIALOG_DlgProc(HWND hwnd, UINT message, + WPARAM wParam, LPARAM lParam) +{ + switch(message) + { + case WM_INITDIALOG: + { + TASKDIALOGCONFIG *config = (TASKDIALOGCONFIG*)lParam; + TASKDIALOG_OnInit(hwnd, config); + break; + } + + case WM_COMMAND: + { + if (LOWORD(wParam) == IDCANCEL) + { + /* TODO: Dialog may be cancelled only if the IDCANCEL button is present + * or the TDF_ALLOW_DIALOG_CANCELLATION flag is set. */ + FIXME("Not checking if dialog cancellation is allowed\n"); + } + EndDialog(hwnd, wParam); + break; + } + + default: + { + TRACE("Message number 0x%04x is being ignored.\n", message); + break; + } + } + return 0; +} + /*********************************************************************** * TaskDialogIndirect [COMCTL32.@] */ HRESULT WINAPI TaskDialogIndirect(const TASKDIALOGCONFIG *pTaskConfig, int *pnButton, int *pnRadioButton, BOOL *pfVerificationFlagChecked) { - UINT uType = 0; - INT ret; - FIXME("%p, %p, %p, %p\n", pTaskConfig, pnButton, pnRadioButton, pfVerificationFlagChecked); + LPVOID template; + HRSRC hRes; + INT_PTR ret; + TRACE("pTaskConfig=%p, pnButton=%p\n", pTaskConfig, pnButton); if (pnButton) *pnButton = 0; if (!pTaskConfig || pTaskConfig->cbSize != sizeof(TASKDIALOGCONFIG)) return E_INVALIDARG; - if (pTaskConfig->dwCommonButtons & TDCBF_YES_BUTTON && - pTaskConfig->dwCommonButtons & TDCBF_NO_BUTTON && - pTaskConfig->dwCommonButtons & TDCBF_CANCEL_BUTTON) - uType |= MB_YESNOCANCEL; - else - if (pTaskConfig->dwCommonButtons & TDCBF_YES_BUTTON && - pTaskConfig->dwCommonButtons & TDCBF_NO_BUTTON) - uType |= MB_YESNO; - else - if (pTaskConfig->dwCommonButtons & TDCBF_RETRY_BUTTON && - pTaskConfig->dwCommonButtons & TDCBF_CANCEL_BUTTON) - uType |= MB_RETRYCANCEL; - else - if (pTaskConfig->dwCommonButtons & TDCBF_OK_BUTTON && - pTaskConfig->dwCommonButtons & TDCBF_CANCEL_BUTTON) - uType |= MB_OKCANCEL; + TRACE("hWndParent=%p, hInstance=%p, dwCommonButtons=%x", + pTaskConfig->hwndParent, pTaskConfig->hInstance, pTaskConfig->dwCommonButtons); + + if (!(hRes = FindResourceExW(COMCTL32_hModule, (LPWSTR)RT_DIALOG, + MAKEINTRESOURCEW(IDD_TASKDIALOG), LANG_NEUTRAL))) + { + ERR("Cannot find TASKDIALOG resource\n"); + return E_FAIL; + } + if (!(template = LoadResource(COMCTL32_hModule, hRes))) + { + ERR("Cannot load TASKDIALOG resource\n"); + return E_FAIL; + } + + ret = DialogBoxIndirectParamW(pTaskConfig->hInstance, template, pTaskConfig->hwndParent, + TASKDIALOG_DlgProc, (LPARAM)pTaskConfig); + if (pnRadioButton) + { + FIXME("Task dialog radio buttons not implemented\n"); + *pnRadioButton = pTaskConfig->nDefaultButton; + } + if (pfVerificationFlagChecked) + { + FIXME("Task dialog verification check box not implemented\n"); + *pfVerificationFlagChecked = TRUE; + } + if (ret > 0) + { + if (pnButton) *pnButton = ret; + return S_OK; + } else - if (pTaskConfig->dwCommonButtons & TDCBF_OK_BUTTON) - uType |= MB_OK; - ret = MessageBoxW(pTaskConfig->hwndParent, pTaskConfig->pszMainInstruction, - pTaskConfig->pszWindowTitle, uType); - FIXME("dwCommonButtons=%x uType=%x ret=%x\n", pTaskConfig->dwCommonButtons, uType, ret); - - if (pnButton) *pnButton = ret; - if (pnRadioButton) *pnRadioButton = pTaskConfig->nDefaultButton; - if (pfVerificationFlagChecked) *pfVerificationFlagChecked = TRUE; - return S_OK; + { + /* DialogBoxIndirect returns -1 on failure. */ + return ret == -1 ? E_FAIL : ret; + } } diff --git a/dlls/comctl32/tests/taskdialog.c b/dlls/comctl32/tests/taskdialog.c index bfec670..eb44b50 100644 --- a/dlls/comctl32/tests/taskdialog.c +++ b/dlls/comctl32/tests/taskdialog.c @@ -58,17 +58,12 @@ static void test_TaskDialogIndirect(void) ok(result == E_INVALIDARG, "pTaskConfig.size == sizeof(TASKDIALOGCONFIG) + 1: " "got result %#x, expected E_INVALIDARG\n", result); - /* Skip this test on Wine. Cannot use todo_wine because the TaskDialogIndirect call - * does not fail, leading to a modal dialog being displayed. */ - if (strcmp(winetest_platform, "wine")) - { - config.cbSize = sizeof(TASKDIALOGCONFIG); - config.hInstance = GetModuleHandleW(NULL); - config.pszContent = MAKEINTRESOURCEW(-1000); /* invalid resource */ - result = pTaskDialogIndirect(&config, NULL, NULL, NULL); - ok(HRESULT_CODE(result) == ERROR_RESOURCE_NAME_NOT_FOUND, "invalid pszContent: " - "got result %#x, expected ERROR_RESOURCE_NAME_NOT_FOUND\n", result); - } + config.cbSize = sizeof(TASKDIALOGCONFIG); + config.hInstance = GetModuleHandleW(NULL); + config.pszContent = MAKEINTRESOURCEW(-1000); /* invalid resource */ + result = pTaskDialogIndirect(&config, NULL, NULL, NULL); + ok(HRESULT_CODE(result) == ERROR_RESOURCE_NAME_NOT_FOUND, "invalid pszContent: " + "got result %#x, expected ERROR_RESOURCE_NAME_NOT_FOUND\n", result); } START_TEST(taskdialog) -- 1.8.4.5