From 4b02d42e37a2fc25e16644e5276ac3df22cd1690 Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Thu, 11 Sep 2008 10:39:49 +0000 Subject: [PATCH] replace the static control in wxMessageDialog with an edit control with a vertical scrollbar if the message box is too big to fit on the screen git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@55544 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- docs/changes.txt | 22 +-- include/wx/msw/msgdlg.h | 18 ++ src/msw/msgdlg.cpp | 365 ++++++++++++++++++++++++++++++---------- 3 files changed, 308 insertions(+), 97 deletions(-) diff --git a/docs/changes.txt b/docs/changes.txt index 4ce5e18c2b..bb79146572 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -250,10 +250,10 @@ Major new features in this release All: -- Added (experimental) IPv6 support to wxSocket (Arcen) +- Added (experimental) IPv6 support to wxSocket (Arcen). - Cleaned up wxURI and made it Unicode-friendly. - Add support for wxExecute(wxEXEC_ASYNC) in wxBase (Lukasz Michalski) -- Added wxXLocale class and xlocale-like functions using it +- Added wxXLocale class and xlocale-like functions using it. - Allow loading message catalogs from wxFileSystem (Axel Gembe) - Added wxMessageQueue class for inter-thread communications - Use UTF-8 for Unicode data in wxIPC classes (Anders Larsen) @@ -307,7 +307,7 @@ All (GUI): - Added wxWindow::Show/HideWithEffect() - Added wxWrapSizer (Arne Steinarson) - Added wxSpinCtrlDouble (John Labenski) -- Support custom labels in wxMessageDialog/wxMessageBox in wxMSW and wxGTK. +- Support custom labels in wxMessageDialog (Gareth Simpson for wxMac version). - Allow copying text in the log dialogs. - Added multisample (anti-aliasing) support to wxGLCanvas (Olivier Playez). - Initialize wx{Client,Paint,Window}DC with fonts/colours of its window. @@ -405,12 +405,11 @@ wxGTK: wxMac: - Better IconRef support (Alan Shouls). -- Added support for changing button labels in wxMessageDialog (Gareth Simpson). - Fix duplicate (empty) help menu in non-English programs (Andreas Jacobs). - Allow accelerators to be used with buttons too (Ryan Wilcox). - Support resource forks in wxCopyFile() (Hank Schultz). -- Implement wxLocale::GetInfo() using CFLocale -- Added native wxCollapsiblePane +- Implement wxLocale::GetInfo() using CFLocale. +- Native wxCollapsiblePane implementation. wxMSW: @@ -418,11 +417,12 @@ wxMSW: - Return the restored window size from GetSize() when window is minimized. - wxCheckListBox now looks more native, especially under XP (Marcin Malich). - Allow tooltips longer than 64 (up to 128) characters in wxTaskBarIcon -- Fix centering wxFileDialog and allow positioning it -- Allow centering wxMessageDialog on its parent window (troelsk) -- Show resize gripper on resizeable dialogs (Kolya Kosenko) -- Implement support for display enumeration under WinCE (Vince Harron) -- Use different Win32 class names in different wx instances (Thomas Hauk) +- Fix centering wxFileDialog and allow positioning it. +- Allow centering wxMessageDialog on its parent window (troelsk). +- Use vertical scrollbar in wxMessageDialog if it's too big to fit on screen. +- Show resize gripper on resizeable dialogs (Kolya Kosenko). +- Implement support for display enumeration under WinCE (Vince Harron). +- Use different Win32 class names in different wx instances (Thomas Hauk). - Support multiline labels for wxCheckBox and wxToggleButton. - Print preview is now rendered in the resolution used by printer and accurately represents what will be printed. This fixes wxHtmlEasyPrinting diff --git a/include/wx/msw/msgdlg.h b/include/wx/msw/msgdlg.h index 4ca1be5f41..1ed42ae17d 100644 --- a/include/wx/msw/msgdlg.h +++ b/include/wx/msw/msgdlg.h @@ -27,16 +27,34 @@ public: virtual int ShowModal(); + // implementation-specific + + // return the font used for the text in the message box + static wxFont GetMessageFont(); + private: // hook procedure used to adjust the message box beyond what the standard // MessageBox() function can do for us static WXLRESULT wxCALLBACK HookFunction(int code, WXWPARAM, WXLPARAM); + static const struct ButtonAccessors + { + int id; + wxString (wxMessageDialog::*getter)() const; + } ms_buttons[]; + + // replace the static text control with a text control in order to show + // scrollbar (and also, incidentally, allow text selection) + void ReplaceStaticWithEdit(); + // adjust the button labels // // this is called from HookFunction() and our HWND is valid at this moment void AdjustButtonLabels(); + // offset all buttons starting from the first one given by dx to the right + void OffsetButtonsStartingFrom(int first, int dx); + WXHANDLE m_hook; // HHOOK used to position the message box diff --git a/src/msw/msgdlg.cpp b/src/msw/msgdlg.cpp index 9d91acab81..04e2e68262 100644 --- a/src/msw/msgdlg.cpp +++ b/src/msw/msgdlg.cpp @@ -43,6 +43,8 @@ #if wxUSE_MSGBOX_HOOK #include "wx/fontutil.h" + #include "wx/textbuf.h" + #include "wx/display.h" #endif // For MB_TASKMODAL @@ -62,6 +64,16 @@ WX_DECLARE_HASH_MAP(unsigned long, wxMessageDialog *, wxIntegerHash, wxIntegerEqual, wxMessageDialogMap); +// the order in this array is the one in which buttons appear in the +// message box +const wxMessageDialog::ButtonAccessors wxMessageDialog::ms_buttons[] = +{ + { IDYES, &wxMessageDialog::GetYesLabel }, + { IDNO, &wxMessageDialog::GetNoLabel }, + { IDOK, &wxMessageDialog::GetOKLabel }, + { IDCANCEL, &wxMessageDialog::GetCancelLabel }, +}; + namespace { @@ -72,6 +84,57 @@ wxMessageDialogMap& HookMap() return s_Map; } +/* + All this code is used for adjusting the message box layout when we mess + with its contents. It's rather complicated because we try hard to avoid + assuming much about the standard layout details and so, instead of just + laying out everything ourselves (which would have been so much simpler!) + we try to only modify the existing controls positions by offsetting them + from their default ones in the hope that this will continue to work with + the future Windows versions. + */ + +// convert the given RECT from screen to client coordinates in place +void ScreenRectToClient(HWND hwnd, RECT& rc) +{ + // map from desktop (i.e. screen) coordinates to ones of this window + // + // notice that a RECT is laid out as 2 consecutive POINTs so the cast is + // valid + ::MapWindowPoints(HWND_DESKTOP, hwnd, reinterpret_cast(&rc), 2); +} + +// set window position to the given rect +inline void SetWindowRect(HWND hwnd, const RECT& rc) +{ + ::MoveWindow(hwnd, + rc.left, rc.top, + rc.right - rc.left, rc.bottom - rc.top, + FALSE); +} + +// set window position expressed in screen coordinates, whether the window is +// child or top level +void MoveWindowToScreenRect(HWND hwnd, RECT rc) +{ + ScreenRectToClient(::GetParent(hwnd), rc); + + SetWindowRect(hwnd, rc); +} + +// helper of AdjustButtonLabels(): move the given window by dx +// +// works for both child and top level windows +void OffsetWindow(HWND hwnd, int dx) +{ + RECT rc = wxGetWindowRect(hwnd); + + rc.left += dx; + rc.right += dx; + + MoveWindowToScreenRect(hwnd, rc); +} + } // anonymous namespace /* static */ @@ -98,6 +161,10 @@ wxMessageDialog::HookFunction(int code, WXWPARAM wParam, WXLPARAM lParam) wnd->SetHWND((HWND)wParam); + // replace the static text with an edit control if the message box is + // too big to fit the display + wnd->ReplaceStaticWithEdit(); + // update the labels if necessary: we need to do it before centering // the dialog as this can change its size if ( wnd->HasCustomLabels() ) @@ -115,123 +182,251 @@ wxMessageDialog::HookFunction(int code, WXWPARAM wParam, WXLPARAM lParam) return rc; } -namespace +void wxMessageDialog::ReplaceStaticWithEdit() { + // check if the message box fits the display + int nDisplay = wxDisplay::GetFromWindow(this); + if ( nDisplay == wxNOT_FOUND ) + nDisplay = 0; + const wxRect rectDisplay = wxDisplay(nDisplay).GetClientArea(); -// helper of AdjustButtonLabels(): set window position expressed in screen -// coordinates, whether the window is child or top level -void MoveWindowToScreenRect(HWND hwnd, RECT rc) -{ - if ( const HWND hwndParent = ::GetAncestor(hwnd, GA_PARENT) ) + if ( rectDisplay.Contains(GetRect()) ) { - // map to parent window coordinates (notice that a RECT is laid out as - // 2 consecutive POINTs) - ::MapWindowPoints(HWND_DESKTOP, hwndParent, - reinterpret_cast(&rc), 2); + // nothing to do + return; } - ::MoveWindow(hwnd, - rc.left, rc.top, - rc.right - rc.left, rc.bottom - rc.top, - FALSE); -} -// helper of AdjustButtonLabels(): move the given window by dx -// -// works for both child and top level windows -void OffsetWindow(HWND hwnd, int dx) -{ - RECT rc = wxGetWindowRect(hwnd); + // find the static control to replace: normally there are two of them, the + // icon and the text itself so search for all of them and ignore the icon + // ones + HWND hwndStatic = ::FindWindowEx(GetHwnd(), NULL, _T("STATIC"), NULL); + if ( ::GetWindowLong(hwndStatic, GWL_STYLE) & SS_ICON ) + hwndStatic = ::FindWindowEx(GetHwnd(), hwndStatic, _T("STATIC"), NULL); - rc.left += dx; - rc.right += dx; + if ( !hwndStatic ) + { + wxLogDebug("Failed to find the static text control in message box."); + return; + } - MoveWindowToScreenRect(hwnd, rc); -} + // set the right font for GetCharHeight() call below + wxWindowBase::SetFont(GetMessageFont()); + + // put the new edit control at the same place + RECT rc = wxGetWindowRect(hwndStatic); + ScreenRectToClient(GetHwnd(), rc); + + // but make it less tall so that the message box fits on the screen: we try + // to make the message box take no more than 7/8 of the screen to leave + // some space above and below it + const int hText = (7*rectDisplay.height)/8 - + ( + 2*::GetSystemMetrics(SM_CYFIXEDFRAME) + + ::GetSystemMetrics(SM_CYCAPTION) + + 5*GetCharHeight() // buttons + margins + ); + const int dh = (rc.bottom - rc.top) - hText; // vertical space we save + rc.bottom -= dh; + + // and it also must be wider as it needs a vertical scrollbar (in order + // to preserve the word wrap, otherwise the number of lines would change + // and we want the control to look as similar as possible to the original) + // + // NB: you would have thought that 2*SM_CXEDGE would be enough but it + // isn't, somehow, and the text control breaks lines differently from + // the static one so fudge by adding some extra space + const int dw = ::GetSystemMetrics(SM_CXVSCROLL) + + 4*::GetSystemMetrics(SM_CXEDGE); + rc.right += dw; + + + // chop of the trailing new line(s) from the message box text, they are + // ignored by the static control but result in extra lines and hence extra + // scrollbar position in the edit one + wxString text(wxGetWindowText(hwndStatic)); + for ( wxString::iterator i = text.end() - 1; i != text.begin(); --i ) + { + if ( *i != '\n' ) + { + text.erase(i + 1, text.end()); + break; + } + } -} // anonymous namespace + // do create the new control + HWND hwndEdit = ::CreateWindow + ( + _T("EDIT"), + wxTextBuffer::Translate(text), + WS_CHILD | WS_VSCROLL | WS_VISIBLE | + ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, + rc.left, rc.top, + rc.right - rc.left, rc.bottom - rc.top, + GetHwnd(), + NULL, + wxhInstance, + NULL + ); + + if ( !hwndEdit ) + { + wxLogDebug("Creation of replacement edit control failed in message box"); + return; + } -void wxMessageDialog::AdjustButtonLabels() -{ - // changing the button labels is the easy part but we also need to ensure - // that the buttons are big enough for the label strings and increase their - // size (and hence the size of the message box itself) if they are not + // copy the font from the original control + LRESULT hfont = ::SendMessage(hwndStatic, WM_GETFONT, 0, 0); + ::SendMessage(hwndEdit, WM_SETFONT, hfont, 0); + + // and get rid of it + ::DestroyWindow(hwndStatic); - // TODO-RTL: check whether this works correctly in RTL - // the order in this array is the one in which buttons appear in the - // message box - const static struct ButtonAccessors + // shrink and centre the message box vertically and widen it box to account + // for the extra scrollbar + RECT rcBox = wxGetWindowRect(GetHwnd()); + const int hMsgBox = rcBox.bottom - rcBox.top - dh; + rcBox.top = (rectDisplay.height - hMsgBox)/2; + rcBox.bottom = rcBox.top + hMsgBox + (rectDisplay.height - hMsgBox)%2; + rcBox.left -= dw/2; + rcBox.right += dw - dw/2; + SetWindowRect(GetHwnd(), rcBox); + + // and adjust all the buttons positions + for ( unsigned n = 0; n < WXSIZEOF(ms_buttons); n++ ) { - int id; - wxString (wxMessageDialog::*getter)() const; + const HWND hwndBtn = ::GetDlgItem(GetHwnd(), ms_buttons[n].id); + if ( !hwndBtn ) + continue; // it's ok, not all buttons are always present + + RECT rc = wxGetWindowRect(hwndBtn); + rc.top -= dh; + rc.bottom -= dh; + rc.left += dw/2; + rc.right += dw/2; + MoveWindowToScreenRect(hwndBtn, rc); } - buttons[] = - { - { IDYES, &wxMessageDialog::GetYesLabel }, - { IDNO, &wxMessageDialog::GetNoLabel }, - { IDOK, &wxMessageDialog::GetOKLabel }, - { IDCANCEL, &wxMessageDialog::GetCancelLabel }, - }; +} - // this contains the amount by which we increased the message box width - int dx = 0; +void wxMessageDialog::AdjustButtonLabels() +{ + // changing the button labels is the easy part but we also need to ensure + // that the buttons are big enough for the label strings and increase their + // size (and maybe the size of the message box itself) if they are not - const NONCLIENTMETRICS& ncm = wxMSWImpl::GetNonClientMetrics(); - const wxFont fontMsgBox(wxNativeFontInfo(ncm.lfMessageFont)); + // TODO-RTL: check whether this works correctly in RTL // we want to use this font in GetTextExtent() calls below but we don't // want to send WM_SETFONT to the message box, who knows how is it going to // react to it (right now it doesn't seem to do anything but what if this // changes) - wxWindowBase::SetFont(fontMsgBox); - - for ( unsigned n = 0; n < WXSIZEOF(buttons); n++ ) + wxWindowBase::SetFont(GetMessageFont()); + + // first iteration: find the widest button and update the buttons labels + int wBtnOld = 0, // current buttons width + wBtnNew = 0; // required new buttons width + RECT rcBtn; // stores the button height and y positions + unsigned numButtons = 0; // total number of buttons in the message box + unsigned n; + for ( n = 0; n < WXSIZEOF(ms_buttons); n++ ) { - const HWND hwndBtn = ::GetDlgItem(GetHwnd(), buttons[n].id); + const HWND hwndBtn = ::GetDlgItem(GetHwnd(), ms_buttons[n].id); if ( !hwndBtn ) continue; // it's ok, not all buttons are always present - const wxString label = (this->*buttons[n].getter)(); + numButtons++; + + const wxString label = (this->*ms_buttons[n].getter)(); const wxSize sizeLabel = wxWindowBase::GetTextExtent(label); // check if the button is big enough for this label - RECT rc = wxGetWindowRect(hwndBtn); - const int widthOld = rc.right - rc.left; - const int widthNew = wxMSWButton::GetFittingSize(this, sizeLabel).x; - const int dw = widthNew - widthOld; - if ( dw > 0 ) + const RECT rc = wxGetWindowRect(hwndBtn); + if ( !wBtnOld ) + { + // initialize wBtnOld using the first button width, all the other + // ones should have the same one + wBtnOld = rc.right - rc.left; + + rcBtn = rc; // remember for use below when we reposition the buttons + } + else { - // we need to resize the button - rc.right += dw; - MoveWindowToScreenRect(hwndBtn, rc); - - // and also move all the other buttons - for ( unsigned m = n + 1; m < WXSIZEOF(buttons); m++ ) - { - const HWND hwndBtnNext = ::GetDlgItem(GetHwnd(), buttons[m].id); - if ( hwndBtnNext ) - OffsetWindow(hwndBtnNext, dw); - } - - dx += dw; + wxASSERT_MSG( wBtnOld == rc.right - rc.left, + "all buttons are supposed to be of same width" ); } + const int widthNeeded = wxMSWButton::GetFittingSize(this, sizeLabel).x; + if ( widthNeeded > wBtnNew ) + wBtnNew = widthNeeded; + ::SetWindowText(hwndBtn, label.wx_str()); } + if ( wBtnNew <= wBtnOld ) + { + // all buttons fit, nothing else to do + return; + } + + // resize the message box to be wider if needed + const int wBoxOld = wxGetClientRect(GetHwnd()).right; + + const int CHAR_WIDTH = GetCharWidth(); + const int MARGIN_OUTER = 2*CHAR_WIDTH; // margin between box and buttons + const int MARGIN_INNER = CHAR_WIDTH; // margin between buttons + + RECT rcBox = wxGetWindowRect(GetHwnd()); + + const int wAllButtons = numButtons*(wBtnNew + MARGIN_INNER) - MARGIN_INNER; + int wBoxNew = 2*MARGIN_OUTER + wAllButtons; + if ( wBoxNew > wBoxOld ) + { + const int dw = wBoxNew - wBoxOld; + rcBox.left -= dw/2; + rcBox.right += dw - dw/2; + + SetWindowRect(GetHwnd(), rcBox); + + // surprisingly, we don't need to resize the static text control, it + // seems to adjust itself to the new size, at least under Windows 2003 + // (TODO: test if this happens on older Windows versions) + } + else // the current width is big enough + { + wBoxNew = wBoxOld; + } + + + // finally position all buttons - // resize the message box itself if needed - if ( dx ) - OffsetWindow(GetHwnd(), dx); + // notice that we have to take into account the difference between window + // and client width + rcBtn.left = (rcBox.left + rcBox.right - wxGetClientRect(GetHwnd()).right + + wBoxNew - wAllButtons) / 2; + rcBtn.right = rcBtn.left + wBtnNew; - // surprisingly, we don't need to resize the static text control, it seems - // to adjust itself to the new size, at least under Windows 2003 - // (TODO: test if this happens on older Windows versions) + for ( n = 0; n < WXSIZEOF(ms_buttons); n++ ) + { + const HWND hwndBtn = ::GetDlgItem(GetHwnd(), ms_buttons[n].id); + if ( !hwndBtn ) + continue; + + MoveWindowToScreenRect(hwndBtn, rcBtn); + + rcBtn.left += wBtnNew + MARGIN_INNER; + rcBtn.right += wBtnNew + MARGIN_INNER; + } } #endif // wxUSE_MSGBOX_HOOK +/* static */ +wxFont wxMessageDialog::GetMessageFont() +{ + const NONCLIENTMETRICS& ncm = wxMSWImpl::GetNonClientMetrics(); + return wxNativeFontInfo(ncm.lfMessageFont); +} int wxMessageDialog::ShowModal() { @@ -309,15 +504,13 @@ int wxMessageDialog::ShowModal() #endif // wxUSE_UNICODE #if wxUSE_MSGBOX_HOOK - // install the hook if we need to position the dialog in a non-default way - // or change the labels - if ( (wxStyle & wxCENTER) || HasCustomLabels() ) - { - const DWORD tid = ::GetCurrentThreadId(); - m_hook = ::SetWindowsHookEx(WH_CBT, - &wxMessageDialog::HookFunction, NULL, tid); - HookMap()[tid] = this; - } + // install the hook in any case as we don't know in advance if the message + // box is not going to be too big (requiring the replacement of the static + // control with an edit one) + const DWORD tid = ::GetCurrentThreadId(); + m_hook = ::SetWindowsHookEx(WH_CBT, + &wxMessageDialog::HookFunction, NULL, tid); + HookMap()[tid] = this; #endif // wxUSE_MSGBOX_HOOK // do show the dialog -- 2.45.2