From 06bfb4c1b54140eeeeef66f249931181c8676d51 Mon Sep 17 00:00:00 2001 From: DangTAnh <163684532+DangTAnh@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:07:21 +0700 Subject: [PATCH] fix(windows): show app icon in MessageDialog via MessageBoxIndirectW --- .../frontend/desktop/windows/dialog.go | 21 ++++++- .../frontend/desktop/windows/dialog_test.go | 61 +++++++++++++++++++ .../desktop/windows/winc/w32/typedef.go | 14 +++++ .../desktop/windows/winc/w32/user32.go | 24 ++++++++ website/src/pages/changelog.mdx | 4 ++ 5 files changed, 123 insertions(+), 1 deletion(-) diff --git a/v2/internal/frontend/desktop/windows/dialog.go b/v2/internal/frontend/desktop/windows/dialog.go index 573325886a1..05e8a747730 100644 --- a/v2/internal/frontend/desktop/windows/dialog.go +++ b/v2/internal/frontend/desktop/windows/dialog.go @@ -9,6 +9,7 @@ import ( "syscall" "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd" "golang.org/x/sys/windows" @@ -177,6 +178,15 @@ func calculateMessageDialogFlags(options frontend.MessageDialogOptions) uint32 { return flags } +// messageDialogUserIconFlags prepares flags for MB_USERICON: the stock icon bits +// must be stripped (mixing MB_ICON* with MB_USERICON is undefined), while the +// button configuration is preserved (#4233). MB_SYSTEMMODAL is kept: it controls +// modality, not the icon. +func messageDialogUserIconFlags(flags uint32) uint32 { + const mbIconMask = windows.MB_ICONHAND | windows.MB_ICONQUESTION | windows.MB_ICONASTERISK | windows.MB_USERICON + return (flags &^ mbIconMask) | windows.MB_USERICON | windows.MB_SYSTEMMODAL +} + // MessageDialog show a message dialog to the user func (f *Frontend) MessageDialog(options frontend.MessageDialogOptions) (string, error) { @@ -191,7 +201,16 @@ func (f *Frontend) MessageDialog(options frontend.MessageDialogOptions) (string, flags := calculateMessageDialogFlags(options) - button, _ := windows.MessageBox(windows.HWND(f.getHandleForDialog()), message, title, flags|windows.MB_SYSTEMMODAL) + // Show the application icon (the same icon the main window uses) in the + // message box; MessageBoxW can only show the four stock icons. + hwnd := f.getHandleForDialog() + button, err := w32.MessageBoxWithIcon(hwnd, message, title, uint16(winc.AppIconID), messageDialogUserIconFlags(flags)) + if err != nil { + // The icon resource is not embedded in dev builds (`go run`, `wails dev`), + // which makes MessageBoxIndirect fail outright (#4233). Fall back to a + // stock-icon dialog so the message is still shown. + button, _ = windows.MessageBox(windows.HWND(hwnd), message, title, flags|windows.MB_SYSTEMMODAL) + } // This maps MessageBox return values to strings responses := []string{"", "Ok", "Cancel", "Abort", "Retry", "Ignore", "Yes", "No", "", "", "Try Again", "Continue"} result := "Error" diff --git a/v2/internal/frontend/desktop/windows/dialog_test.go b/v2/internal/frontend/desktop/windows/dialog_test.go index e91058e9235..3f13f5dc3b4 100644 --- a/v2/internal/frontend/desktop/windows/dialog_test.go +++ b/v2/internal/frontend/desktop/windows/dialog_test.go @@ -75,3 +75,64 @@ func Test_calculateMessageDialogFlags(t *testing.T) { }) } } + +func Test_messageDialogUserIconFlags(t *testing.T) { + tests := []struct { + name string + options frontend.MessageDialogOptions + want uint32 + }{ + { + name: "Test Info Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.InfoDialog, + }, + want: windows.MB_OK | windows.MB_USERICON | windows.MB_SYSTEMMODAL, + }, + { + name: "Test Error Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.ErrorDialog, + }, + want: windows.MB_OK | windows.MB_USERICON | windows.MB_SYSTEMMODAL, + }, + { + name: "Test Question Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.QuestionDialog, + }, + want: windows.MB_YESNO | windows.MB_USERICON | windows.MB_SYSTEMMODAL, + }, + { + name: "Test Question Dialog with default cancel", + options: frontend.MessageDialogOptions{ + Type: frontend.QuestionDialog, + DefaultButton: "No", + }, + want: windows.MB_YESNO | windows.MB_DEFBUTTON2 | windows.MB_USERICON | windows.MB_SYSTEMMODAL, + }, + { + name: "Test Question Dialog with default cancel (lowercase)", + options: frontend.MessageDialogOptions{ + Type: frontend.QuestionDialog, + DefaultButton: "no", + }, + want: windows.MB_YESNO | windows.MB_DEFBUTTON2 | windows.MB_USERICON | windows.MB_SYSTEMMODAL, + }, + { + name: "Test Warning Dialog", + options: frontend.MessageDialogOptions{ + Type: frontend.WarningDialog, + }, + want: windows.MB_OK | windows.MB_USERICON | windows.MB_SYSTEMMODAL, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flags := calculateMessageDialogFlags(tt.options) + if got := messageDialogUserIconFlags(flags); got != tt.want { + t.Errorf("messageDialogUserIconFlags() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/typedef.go b/v2/internal/frontend/desktop/windows/winc/w32/typedef.go index 13735204c2c..7b14f285ecb 100644 --- a/v2/internal/frontend/desktop/windows/winc/w32/typedef.go +++ b/v2/internal/frontend/desktop/windows/winc/w32/typedef.go @@ -279,6 +279,20 @@ type MINMAXINFO struct { PtMaxTrackSize POINT } +// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msgboxparamsw +type MSGBOXPARAMS struct { + Size uint32 + Owner HWND + Instance HINSTANCE + Text *uint16 + Caption *uint16 + Style uint32 + Icon *uint16 + ContextHelpID uintptr + MsgBoxCallback uintptr + LanguageID uint32 +} + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd145037.aspx type LOGFONT struct { Height int32 diff --git a/v2/internal/frontend/desktop/windows/winc/w32/user32.go b/v2/internal/frontend/desktop/windows/winc/w32/user32.go index 707701f5e06..c66901b9555 100644 --- a/v2/internal/frontend/desktop/windows/winc/w32/user32.go +++ b/v2/internal/frontend/desktop/windows/winc/w32/user32.go @@ -66,6 +66,7 @@ var ( procReleaseCapture = moduser32.NewProc("ReleaseCapture") procGetWindowThreadProcessId = moduser32.NewProc("GetWindowThreadProcessId") procMessageBox = moduser32.NewProc("MessageBoxW") + procMessageBoxIndirect = moduser32.NewProc("MessageBoxIndirectW") procGetSystemMetrics = moduser32.NewProc("GetSystemMetrics") procPostThreadMessageW = moduser32.NewProc("PostThreadMessageW") procRegisterWindowMessageA = moduser32.NewProc("RegisterWindowMessageA") @@ -623,6 +624,29 @@ func MessageBox(hwnd HWND, title, caption string, flags uint) int { return int(ret) } +// MessageBoxWithIcon shows a modal message box that displays the icon stored in +// resource iconID of the given module. MessageBoxW cannot show an application +// icon, only the four stock icons. Returns an error if the icon resource is +// missing, which happens in dev builds (`go run`, `wails dev`) where no icon +// resource is embedded - see #4233. +func MessageBoxWithIcon(hwnd HWND, text, caption *uint16, iconID uint16, flags uint32) (int32, error) { + params := MSGBOXPARAMS{ + Size: uint32(unsafe.Sizeof(MSGBOXPARAMS{})), + Owner: hwnd, + Instance: GetModuleHandle(""), + Text: text, + Caption: caption, + Style: flags, + Icon: (*uint16)(unsafe.Pointer(uintptr(iconID))), // MAKEINTRESOURCE + } + + ret, _, err := procMessageBoxIndirect.Call(uintptr(unsafe.Pointer(¶ms))) + if ret == 0 { + return 0, err + } + return int32(ret), nil +} + func GetSystemMetrics(index int) int { ret, _, _ := procGetSystemMetrics.Call( uintptr(index)) diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index cbb024bbb36..955004cc57c 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fixed `MessageDialog` not showing the application icon on Windows [#3684](https://github.com/wailsapp/wails/issues/3684) by @DangTAnh + ## v2.16.0 - 2026-09-14 ### Fixed