1 /////////////////////////////////////////////////////////////////////////////
2 // Name: src/msw/msgdlg.cpp
3 // Purpose: wxMessageDialog
4 // Author: Julian Smart
8 // Copyright: (c) Julian Smart
9 // Licence: wxWindows licence
10 /////////////////////////////////////////////////////////////////////////////
12 // For compilers that support precompilation, includes "wx.h".
13 #include "wx/wxprec.h"
21 #include "wx/ptr_scpd.h"
23 // there is no hook support under CE so we can't use the code for message box
26 #define wxUSE_MSGBOX_HOOK 1
28 #define wxUSE_MSGBOX_HOOK 0
35 #include "wx/dialog.h"
37 #include "wx/hashmap.h"
41 #include "wx/dynlib.h"
42 #include "wx/msw/private.h"
43 #include "wx/msw/private/button.h"
44 #include "wx/msw/private/metrics.h"
45 #include "wx/msw/private/msgdlg.h"
46 #include "wx/msgdlg.h"
49 #include "wx/fontutil.h"
50 #include "wx/textbuf.h"
51 #include "wx/display.h"
56 #include "wx/msw/wince/missing.h"
59 using namespace wxMSWMessageDialog
;
61 IMPLEMENT_CLASS(wxMessageDialog
, wxDialog
)
65 // there can potentially be one message box per thread so we use a hash map
66 // with thread ids as keys and (currently shown) message boxes as values
68 // TODO: replace this with wxTLS once it's available
69 WX_DECLARE_HASH_MAP(unsigned long, wxMessageDialog
*,
70 wxIntegerHash
, wxIntegerEqual
,
73 // the order in this array is the one in which buttons appear in the
75 const wxMessageDialog::ButtonAccessors
wxMessageDialog::ms_buttons
[] =
77 { IDYES
, &wxMessageDialog::GetYesLabel
},
78 { IDNO
, &wxMessageDialog::GetNoLabel
},
79 { IDOK
, &wxMessageDialog::GetOKLabel
},
80 { IDCANCEL
, &wxMessageDialog::GetCancelLabel
},
86 wxMessageDialogMap
& HookMap()
88 static wxMessageDialogMap s_Map
;
94 All this code is used for adjusting the message box layout when we mess
95 with its contents. It's rather complicated because we try hard to avoid
96 assuming much about the standard layout details and so, instead of just
97 laying out everything ourselves (which would have been so much simpler!)
98 we try to only modify the existing controls positions by offsetting them
99 from their default ones in the hope that this will continue to work with
100 the future Windows versions.
103 // convert the given RECT from screen to client coordinates in place
104 void ScreenRectToClient(HWND hwnd
, RECT
& rc
)
106 // map from desktop (i.e. screen) coordinates to ones of this window
108 // notice that a RECT is laid out as 2 consecutive POINTs so the cast is
110 ::MapWindowPoints(HWND_DESKTOP
, hwnd
, reinterpret_cast<POINT
*>(&rc
), 2);
113 // set window position to the given rect
114 inline void SetWindowRect(HWND hwnd
, const RECT
& rc
)
118 rc
.right
- rc
.left
, rc
.bottom
- rc
.top
,
122 // set window position expressed in screen coordinates, whether the window is
123 // child or top level
124 void MoveWindowToScreenRect(HWND hwnd
, RECT rc
)
126 ScreenRectToClient(::GetParent(hwnd
), rc
);
128 SetWindowRect(hwnd
, rc
);
131 // helper of AdjustButtonLabels(): move the given window by dx
133 // works for both child and top level windows
134 void OffsetWindow(HWND hwnd
, int dx
)
136 RECT rc
= wxGetWindowRect(hwnd
);
141 MoveWindowToScreenRect(hwnd
, rc
);
144 } // anonymous namespace
148 wxMessageDialog::HookFunction(int code
, WXWPARAM wParam
, WXLPARAM lParam
)
150 // Find the thread-local instance of wxMessageDialog
151 const DWORD tid
= ::GetCurrentThreadId();
152 wxMessageDialogMap::iterator node
= HookMap().find(tid
);
153 wxCHECK_MSG( node
!= HookMap().end(), false,
154 wxT("bogus thread id in wxMessageDialog::Hook") );
156 wxMessageDialog
* const wnd
= node
->second
;
158 const HHOOK hhook
= (HHOOK
)wnd
->m_hook
;
159 const LRESULT rc
= ::CallNextHookEx(hhook
, code
, wParam
, lParam
);
161 if ( code
== HCBT_ACTIVATE
)
163 // we won't need this hook any longer
164 ::UnhookWindowsHookEx(hhook
);
166 HookMap().erase(tid
);
168 wnd
->SetHWND((HWND
)wParam
);
170 // replace the static text with an edit control if the message box is
171 // too big to fit the display
172 wnd
->ReplaceStaticWithEdit();
174 // update the labels if necessary: we need to do it before centering
175 // the dialog as this can change its size
176 if ( wnd
->HasCustomLabels() )
177 wnd
->AdjustButtonLabels();
179 // centre the message box on its parent if requested
180 if ( wnd
->GetMessageDialogStyle() & wxCENTER
)
181 wnd
->Center(); // center on parent
182 //else: default behaviour, center on screen
184 // there seems to be no reason to leave it set
191 void wxMessageDialog::ReplaceStaticWithEdit()
193 // check if the message box fits the display
194 int nDisplay
= wxDisplay::GetFromWindow(this);
195 if ( nDisplay
== wxNOT_FOUND
)
197 const wxRect rectDisplay
= wxDisplay(nDisplay
).GetClientArea();
199 if ( rectDisplay
.Contains(GetRect()) )
206 // find the static control to replace: normally there are two of them, the
207 // icon and the text itself so search for all of them and ignore the icon
209 HWND hwndStatic
= ::FindWindowEx(GetHwnd(), NULL
, wxT("STATIC"), NULL
);
210 if ( ::GetWindowLong(hwndStatic
, GWL_STYLE
) & SS_ICON
)
211 hwndStatic
= ::FindWindowEx(GetHwnd(), hwndStatic
, wxT("STATIC"), NULL
);
215 wxLogDebug("Failed to find the static text control in message box.");
219 // set the right font for GetCharHeight() call below
220 wxWindowBase::SetFont(GetMessageFont());
222 // put the new edit control at the same place
223 RECT rc
= wxGetWindowRect(hwndStatic
);
224 ScreenRectToClient(GetHwnd(), rc
);
226 // but make it less tall so that the message box fits on the screen: we try
227 // to make the message box take no more than 7/8 of the screen to leave
228 // some space above and below it
229 const int hText
= (7*rectDisplay
.height
)/8 -
231 2*::GetSystemMetrics(SM_CYFIXEDFRAME
) +
232 ::GetSystemMetrics(SM_CYCAPTION
) +
233 5*GetCharHeight() // buttons + margins
235 const int dh
= (rc
.bottom
- rc
.top
) - hText
; // vertical space we save
238 // and it also must be wider as it needs a vertical scrollbar (in order
239 // to preserve the word wrap, otherwise the number of lines would change
240 // and we want the control to look as similar as possible to the original)
242 // NB: you would have thought that 2*SM_CXEDGE would be enough but it
243 // isn't, somehow, and the text control breaks lines differently from
244 // the static one so fudge by adding some extra space
245 const int dw
= ::GetSystemMetrics(SM_CXVSCROLL
) +
246 4*::GetSystemMetrics(SM_CXEDGE
);
250 // chop of the trailing new line(s) from the message box text, they are
251 // ignored by the static control but result in extra lines and hence extra
252 // scrollbar position in the edit one
253 wxString
text(wxGetWindowText(hwndStatic
));
254 for ( wxString::reverse_iterator i
= text
.rbegin(); i
!= text
.rend(); ++i
)
258 // found last non-newline char, remove everything after it and stop
259 text
.erase(i
.base() + 1, text
.end());
264 // do create the new control
265 HWND hwndEdit
= ::CreateWindow
268 wxTextBuffer::Translate(text
).wx_str(),
269 WS_CHILD
| WS_VSCROLL
| WS_VISIBLE
|
270 ES_MULTILINE
| ES_READONLY
| ES_AUTOVSCROLL
,
272 rc
.right
- rc
.left
, rc
.bottom
- rc
.top
,
281 wxLogDebug("Creation of replacement edit control failed in message box");
285 // copy the font from the original control
286 LRESULT hfont
= ::SendMessage(hwndStatic
, WM_GETFONT
, 0, 0);
287 ::SendMessage(hwndEdit
, WM_SETFONT
, hfont
, 0);
290 ::DestroyWindow(hwndStatic
);
293 // shrink and centre the message box vertically and widen it box to account
294 // for the extra scrollbar
295 RECT rcBox
= wxGetWindowRect(GetHwnd());
296 const int hMsgBox
= rcBox
.bottom
- rcBox
.top
- dh
;
297 rcBox
.top
= (rectDisplay
.height
- hMsgBox
)/2;
298 rcBox
.bottom
= rcBox
.top
+ hMsgBox
+ (rectDisplay
.height
- hMsgBox
)%2
;
300 rcBox
.right
+= dw
- dw
/2;
301 SetWindowRect(GetHwnd(), rcBox
);
303 // and adjust all the buttons positions
304 for ( unsigned n
= 0; n
< WXSIZEOF(ms_buttons
); n
++ )
306 const HWND hwndBtn
= ::GetDlgItem(GetHwnd(), ms_buttons
[n
].id
);
308 continue; // it's ok, not all buttons are always present
310 RECT rc
= wxGetWindowRect(hwndBtn
);
315 MoveWindowToScreenRect(hwndBtn
, rc
);
319 void wxMessageDialog::AdjustButtonLabels()
321 // changing the button labels is the easy part but we also need to ensure
322 // that the buttons are big enough for the label strings and increase their
323 // size (and maybe the size of the message box itself) if they are not
325 // TODO-RTL: check whether this works correctly in RTL
327 // we want to use this font in GetTextExtent() calls below but we don't
328 // want to send WM_SETFONT to the message box, who knows how is it going to
329 // react to it (right now it doesn't seem to do anything but what if this
331 wxWindowBase::SetFont(GetMessageFont());
333 // first iteration: find the widest button and update the buttons labels
334 int wBtnOld
= 0, // current buttons width
335 wBtnNew
= 0; // required new buttons width
336 RECT rcBtn
; // stores the button height and y positions
337 unsigned numButtons
= 0; // total number of buttons in the message box
339 for ( n
= 0; n
< WXSIZEOF(ms_buttons
); n
++ )
341 const HWND hwndBtn
= ::GetDlgItem(GetHwnd(), ms_buttons
[n
].id
);
343 continue; // it's ok, not all buttons are always present
347 const wxString label
= (this->*ms_buttons
[n
].getter
)();
348 const wxSize sizeLabel
= wxWindowBase::GetTextExtent(label
);
350 // check if the button is big enough for this label
351 const RECT rc
= wxGetWindowRect(hwndBtn
);
354 // initialize wBtnOld using the first button width, all the other
355 // ones should have the same one
356 wBtnOld
= rc
.right
- rc
.left
;
358 rcBtn
= rc
; // remember for use below when we reposition the buttons
362 wxASSERT_MSG( wBtnOld
== rc
.right
- rc
.left
,
363 "all buttons are supposed to be of same width" );
366 const int widthNeeded
= wxMSWButton::GetFittingSize(this, sizeLabel
).x
;
367 if ( widthNeeded
> wBtnNew
)
368 wBtnNew
= widthNeeded
;
370 ::SetWindowText(hwndBtn
, label
.wx_str());
373 if ( wBtnNew
<= wBtnOld
)
375 // all buttons fit, nothing else to do
379 // resize the message box to be wider if needed
380 const int wBoxOld
= wxGetClientRect(GetHwnd()).right
;
382 const int CHAR_WIDTH
= GetCharWidth();
383 const int MARGIN_OUTER
= 2*CHAR_WIDTH
; // margin between box and buttons
384 const int MARGIN_INNER
= CHAR_WIDTH
; // margin between buttons
386 RECT rcBox
= wxGetWindowRect(GetHwnd());
388 const int wAllButtons
= numButtons
*(wBtnNew
+ MARGIN_INNER
) - MARGIN_INNER
;
389 int wBoxNew
= 2*MARGIN_OUTER
+ wAllButtons
;
390 if ( wBoxNew
> wBoxOld
)
392 const int dw
= wBoxNew
- wBoxOld
;
394 rcBox
.right
+= dw
- dw
/2;
396 SetWindowRect(GetHwnd(), rcBox
);
398 // surprisingly, we don't need to resize the static text control, it
399 // seems to adjust itself to the new size, at least under Windows 2003
400 // (TODO: test if this happens on older Windows versions)
402 else // the current width is big enough
408 // finally position all buttons
410 // notice that we have to take into account the difference between window
412 rcBtn
.left
= (rcBox
.left
+ rcBox
.right
- wxGetClientRect(GetHwnd()).right
+
413 wBoxNew
- wAllButtons
) / 2;
414 rcBtn
.right
= rcBtn
.left
+ wBtnNew
;
416 for ( n
= 0; n
< WXSIZEOF(ms_buttons
); n
++ )
418 const HWND hwndBtn
= ::GetDlgItem(GetHwnd(), ms_buttons
[n
].id
);
422 MoveWindowToScreenRect(hwndBtn
, rcBtn
);
424 rcBtn
.left
+= wBtnNew
+ MARGIN_INNER
;
425 rcBtn
.right
+= wBtnNew
+ MARGIN_INNER
;
429 #endif // wxUSE_MSGBOX_HOOK
432 wxFont
wxMessageDialog::GetMessageFont()
434 const NONCLIENTMETRICS
& ncm
= wxMSWImpl::GetNonClientMetrics();
435 return wxNativeFontInfo(ncm
.lfMessageFont
);
438 int wxMessageDialog::ShowMessageBox()
440 if ( !wxTheApp
->GetTopWindow() )
442 // when the message box is shown from wxApp::OnInit() (i.e. before the
443 // message loop is entered), this must be done or the next message box
444 // will never be shown - just try putting 2 calls to wxMessageBox() in
445 // OnInit() to see it
446 while ( wxTheApp
->Pending() )
447 wxTheApp
->Dispatch();
450 // use the top level window as parent if none specified
451 m_parent
= GetParentForModalDialog();
452 HWND hWnd
= m_parent
? GetHwndOf(m_parent
) : NULL
;
455 // native message box always uses the current user locale but the program
456 // may be using a different one and in this case we need to manually
457 // translate the button labels to avoid mismatch between the language of
458 // the message box text and its buttons
459 wxLocale
* const loc
= wxGetLocale();
460 if ( loc
&& loc
->GetLanguage() != wxLocale::GetSystemLanguage() )
462 if ( m_dialogStyle
& wxYES_NO
)
464 // use the strings with mnemonics here as the native message box
466 SetYesNoLabels(_("&Yes"), _("&No"));
469 // we may or not have the Ok/Cancel buttons but either we do have them
470 // or we already made the labels custom because we called
471 // SetYesNoLabels() above so doing this does no harm -- and is
472 // necessary in wxYES_NO | wxCANCEL case
474 // note that we don't use mnemonics here for consistency with the
475 // native message box (which probably doesn't use them because
476 // Enter/Esc keys can be already used to dismiss the message box
478 SetOKCancelLabels(_("OK"), _("Cancel"));
482 // translate wx style in MSW
483 unsigned int msStyle
;
484 const long wxStyle
= GetMessageDialogStyle();
485 if ( wxStyle
& wxYES_NO
)
487 #if !(defined(__SMARTPHONE__) && defined(__WXWINCE__))
488 if (wxStyle
& wxCANCEL
)
489 msStyle
= MB_YESNOCANCEL
;
491 #endif // !(__SMARTPHONE__ && __WXWINCE__)
494 if ( wxStyle
& wxNO_DEFAULT
)
495 msStyle
|= MB_DEFBUTTON2
;
496 else if ( wxStyle
& wxCANCEL_DEFAULT
)
497 msStyle
|= MB_DEFBUTTON3
;
499 else // without Yes/No we're going to have an OK button
501 if ( wxStyle
& wxCANCEL
)
503 msStyle
= MB_OKCANCEL
;
505 if ( wxStyle
& wxCANCEL_DEFAULT
)
506 msStyle
|= MB_DEFBUTTON2
;
514 // set the icon style
515 switch ( GetEffectiveIcon() )
518 msStyle
|= MB_ICONHAND
;
522 msStyle
|= MB_ICONEXCLAMATION
;
525 case wxICON_QUESTION
:
526 msStyle
|= MB_ICONQUESTION
;
529 case wxICON_INFORMATION
:
530 msStyle
|= MB_ICONINFORMATION
;
534 if ( wxStyle
& wxSTAY_ON_TOP
)
535 msStyle
|= MB_TOPMOST
;
538 if ( wxTheApp
->GetLayoutDirection() == wxLayout_RightToLeft
)
539 msStyle
|= MB_RTLREADING
| MB_RIGHT
;
543 msStyle
|= MB_APPLMODAL
;
545 msStyle
|= MB_TASKMODAL
;
547 // per MSDN documentation for MessageBox() we can prefix the message with 2
548 // right-to-left mark characters to tell the function to use RTL layout
549 // (unfortunately this only works in Unicode builds)
550 wxString message
= GetFullMessage();
552 if ( wxTheApp
->GetLayoutDirection() == wxLayout_RightToLeft
)
554 // NB: not all compilers support \u escapes
555 static const wchar_t wchRLM
= 0x200f;
556 message
.Prepend(wxString(wchRLM
, 2));
558 #endif // wxUSE_UNICODE
560 #if wxUSE_MSGBOX_HOOK
561 // install the hook in any case as we don't know in advance if the message
562 // box is not going to be too big (requiring the replacement of the static
563 // control with an edit one)
564 const DWORD tid
= ::GetCurrentThreadId();
565 m_hook
= ::SetWindowsHookEx(WH_CBT
,
566 &wxMessageDialog::HookFunction
, NULL
, tid
);
567 HookMap()[tid
] = this;
568 #endif // wxUSE_MSGBOX_HOOK
570 // do show the dialog
571 int msAns
= MessageBox(hWnd
, message
.wx_str(), m_caption
.wx_str(), msStyle
);
573 return MSWTranslateReturnCode(msAns
);
576 int wxMessageDialog::ShowTaskDialog()
578 #ifdef wxHAS_MSW_TASKDIALOG
579 TaskDialogIndirect_t taskDialogIndirect
= GetTaskDialogIndirectFunc();
580 if ( !taskDialogIndirect
)
583 WinStruct
<TASKDIALOGCONFIG
> tdc
;
584 wxMSWTaskDialogConfig
wxTdc( *this );
585 wxTdc
.MSWCommonTaskDialogInit( tdc
);
588 HRESULT hr
= taskDialogIndirect( &tdc
, &msAns
, NULL
, NULL
);
591 wxLogApiError( "TaskDialogIndirect", hr
);
595 return MSWTranslateReturnCode( msAns
);
597 wxFAIL_MSG( "Task dialogs are unavailable." );
600 #endif // wxHAS_MSW_TASKDIALOG
605 int wxMessageDialog::ShowModal()
607 if ( HasNativeTaskDialog() )
608 return ShowTaskDialog();
610 return ShowMessageBox();
613 // ----------------------------------------------------------------------------
614 // Helpers of the wxMSWMessageDialog namespace
615 // ----------------------------------------------------------------------------
617 #ifdef wxHAS_MSW_TASKDIALOG
619 wxMSWTaskDialogConfig::wxMSWTaskDialogConfig(const wxMessageDialogBase
& dlg
)
620 : buttons(new TASKDIALOG_BUTTON
[3])
622 parent
= dlg
.GetParentForModalDialog();
623 caption
= dlg
.GetCaption();
624 message
= dlg
.GetMessage();
625 extendedMessage
= dlg
.GetExtendedMessage();
626 iconId
= dlg
.GetEffectiveIcon();
627 style
= dlg
.GetMessageDialogStyle();
628 useCustomLabels
= dlg
.HasCustomLabels();
629 btnYesLabel
= dlg
.GetYesLabel();
630 btnNoLabel
= dlg
.GetNoLabel();
631 btnOKLabel
= dlg
.GetOKLabel();
632 btnCancelLabel
= dlg
.GetCancelLabel();
635 void wxMSWTaskDialogConfig::MSWCommonTaskDialogInit(TASKDIALOGCONFIG
&tdc
)
637 tdc
.dwFlags
= TDF_EXPAND_FOOTER_AREA
| TDF_POSITION_RELATIVE_TO_WINDOW
;
638 tdc
.hInstance
= wxGetInstance();
639 tdc
.pszWindowTitle
= caption
.wx_str();
641 // use the top level window as parent if none specified
642 tdc
.hwndParent
= parent
? GetHwndOf(parent
) : NULL
;
644 if ( wxTheApp
->GetLayoutDirection() == wxLayout_RightToLeft
)
645 tdc
.dwFlags
|= TDF_RTL_LAYOUT
;
646 tdc
.pszMainInstruction
= message
.wx_str();
647 tdc
.pszContent
= extendedMessage
.wx_str();
649 // set an icon to be used, if possible
653 tdc
.pszMainIcon
= TD_ERROR_ICON
;
657 tdc
.pszMainIcon
= TD_WARNING_ICON
;
660 case wxICON_INFORMATION
:
661 tdc
.pszMainIcon
= TD_INFORMATION_ICON
;
665 // custom label button array that can hold all buttons in use
666 tdc
.pButtons
= buttons
.get();
668 if ( style
& wxYES_NO
)
670 AddTaskDialogButton(tdc
, IDYES
, TDCBF_YES_BUTTON
, btnYesLabel
);
671 AddTaskDialogButton(tdc
, IDNO
, TDCBF_NO_BUTTON
, btnNoLabel
);
673 if (style
& wxCANCEL
)
674 AddTaskDialogButton(tdc
, IDCANCEL
,
675 TDCBF_CANCEL_BUTTON
, btnCancelLabel
);
677 if ( style
& wxNO_DEFAULT
)
678 tdc
.nDefaultButton
= IDNO
;
679 else if ( style
& wxCANCEL_DEFAULT
)
680 tdc
.nDefaultButton
= IDCANCEL
;
682 else // without Yes/No we're going to have an OK button
684 AddTaskDialogButton(tdc
, IDOK
, TDCBF_OK_BUTTON
, btnOKLabel
);
686 if ( style
& wxCANCEL
)
688 AddTaskDialogButton(tdc
, IDCANCEL
,
689 TDCBF_CANCEL_BUTTON
, btnCancelLabel
);
691 if ( style
& wxCANCEL_DEFAULT
)
692 tdc
.nDefaultButton
= IDCANCEL
;
697 void wxMSWTaskDialogConfig::AddTaskDialogButton(TASKDIALOGCONFIG
&tdc
,
700 const wxString
& customLabel
)
702 if ( useCustomLabels
)
704 // use custom buttons to implement custom labels
705 TASKDIALOG_BUTTON
&tdBtn
= buttons
[tdc
.cButtons
];
707 tdBtn
.nButtonID
= btnCustomId
;
708 tdBtn
.pszButtonText
= customLabel
.wx_str();
713 tdc
.dwCommonButtons
|= btnCommonId
;
717 // Task dialog can be used from different threads (and wxProgressDialog always
718 // uses it from another thread in fact) so protect access to the static
719 // variable below with a critical section.
720 wxCRIT_SECT_DECLARE(gs_csTaskDialogIndirect
);
722 TaskDialogIndirect_t
wxMSWMessageDialog::GetTaskDialogIndirectFunc()
724 static TaskDialogIndirect_t s_TaskDialogIndirect
= NULL
;
726 wxCRIT_SECT_LOCKER(lock
, gs_csTaskDialogIndirect
);
728 if ( !s_TaskDialogIndirect
)
730 wxLoadedDLL
dllComCtl32("comctl32.dll");
731 wxDL_INIT_FUNC(s_
, TaskDialogIndirect
, dllComCtl32
);
733 // We must always succeed as this code is only executed under Vista and
734 // later which must have task dialog support.
735 wxASSERT_MSG( s_TaskDialogIndirect
,
736 "Task dialog support unexpectedly not available" );
739 return s_TaskDialogIndirect
;
742 #endif // wxHAS_MSW_TASKDIALOG
744 bool wxMSWMessageDialog::HasNativeTaskDialog()
746 #ifdef wxHAS_MSW_TASKDIALOG
747 return wxGetWinVersion() >= wxWinVersion_6
;
753 int wxMSWMessageDialog::MSWTranslateReturnCode(int msAns
)
759 wxFAIL_MSG(wxT("unexpected return code"));
779 #endif // wxUSE_MSGDLG