From c5f613c9413ff11c123d8634c15dc9949fd828a9 Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Mon, 28 Nov 2022 18:55:45 +0000 Subject: [PATCH] Revert "Remove wxStaticBox custom drawing code entirely" This reverts commit 616586f3011aa90c4bbced92d09e2c02a989d441 because it turns out that using WS_EX_TRANSPARENT is incompatible with using WS_EX_COMPOSITED (see parent commit). Reintroducing all this code is painful, but there doesn't seem to be any way of avoiding to have it as long as we use WS_EX_COMPOSITED. If we stop using this style, we could revert this commit, this re-applying the original change. Closes #22982. --- include/wx/msw/radiobox.h | 2 + include/wx/msw/statbox.h | 19 +- interface/wx/sysopt.h | 8 +- samples/widgets/static.cpp | 4 +- src/msw/radiobox.cpp | 25 ++ src/msw/statbox.cpp | 460 ++++++++++++++++++++++++++++++++++--- 6 files changed, 472 insertions(+), 46 deletions(-) diff --git a/include/wx/msw/radiobox.h b/include/wx/msw/radiobox.h index d00f949f9f..9b6356650f 100644 --- a/include/wx/msw/radiobox.h +++ b/include/wx/msw/radiobox.h @@ -160,6 +160,8 @@ protected: virtual void DoSetItemToolTip(unsigned int n, wxToolTip * tooltip) override; #endif + virtual WXHRGN MSWGetRegionWithoutChildren() override; + virtual void MSWUpdateFontOnDPIChange(const wxSize& newDPI) override; // resolve ambiguity in base classes diff --git a/include/wx/msw/statbox.h b/include/wx/msw/statbox.h index 51586caeee..f8bb9f9ae8 100644 --- a/include/wx/msw/statbox.h +++ b/include/wx/msw/statbox.h @@ -65,8 +65,6 @@ public: virtual bool SetForegroundColour(const wxColour& colour) override; virtual bool SetFont(const wxFont& font) override; - virtual void SetLabel(const wxString& label) override; - virtual WXDWORD MSWGetStyle(long style, WXDWORD *exstyle) const override; // returns true if the platform should explicitly apply a theme border @@ -81,9 +79,26 @@ public: protected: virtual wxWindowList GetCompositeWindowParts() const override; + // return the region with all the windows inside this static box excluded + virtual WXHRGN MSWGetRegionWithoutChildren(); + + // remove the parts which are painted by static box itself from the given + // region which is embedded in a rectangle (0, 0)-(w, h) + virtual void MSWGetRegionWithoutSelf(WXHRGN hrgn, int w, int h); + + // paint the given rectangle with our background brush/colour + virtual void PaintBackground(wxDC& dc, const struct tagRECT& rc); + // paint the foreground of the static box + virtual void PaintForeground(wxDC& dc, const struct tagRECT& rc); + + void OnPaint(wxPaintEvent& event); + private: void PositionLabelWindow(); + bool ShouldUseCustomPaint() const; + void UseCustomPaint(); + using base_type = wxCompositeWindowSettersOnly; wxDECLARE_DYNAMIC_CLASS_NO_COPY(wxStaticBox); diff --git a/interface/wx/sysopt.h b/interface/wx/sysopt.h index b0a4d75dbb..19cc635270 100644 --- a/interface/wx/sysopt.h +++ b/interface/wx/sysopt.h @@ -62,10 +62,10 @@ Note that this won't disable the theme on the actual notebook background (noticeable only if there are no pages). @flag{msw.staticbox.optimized-paint} - This obsolete option doesn't do anything any more, wxMSW now always - behaves as if it were set to 0. Note that programs painting over the - static box may still not work correctly when double buffering is - enabled (which is the case by default) and could need to disable it. + If set to 0, switches off optimized wxStaticBox painting. + Setting this to 0 causes more flicker, but allows applications to paint + graphics on the parent of a static box (the optimized refresh causes any + such drawing to disappear). @flag{msw.font.no-proof-quality} If set to 1, use default fonts quality instead of proof quality when creating fonts. With proof quality the fonts have slightly better diff --git a/samples/widgets/static.cpp b/samples/widgets/static.cpp index c49115f049..02fc775ddf 100644 --- a/samples/widgets/static.cpp +++ b/samples/widgets/static.cpp @@ -583,9 +583,7 @@ void StaticWidgetsPage::OnBoxCheckBox(wxCommandEvent& event) void StaticWidgetsPage::OnButtonBoxText(wxCommandEvent& WXUNUSED(event)) { - wxStaticBox* const box = m_sizerStatBox->GetStaticBox(); - box->SetLabel(m_textBox->GetValue()); - wxLogMessage("Box label changed, now is '%s'", box->GetLabel()); + m_sizerStatBox->GetStaticBox()->SetLabel(m_textBox->GetValue()); } void StaticWidgetsPage::OnButtonLabelText(wxCommandEvent& WXUNUSED(event)) diff --git a/src/msw/radiobox.cpp b/src/msw/radiobox.cpp index af4d8257b6..ae0f13e631 100644 --- a/src/msw/radiobox.cpp +++ b/src/msw/radiobox.cpp @@ -755,6 +755,31 @@ void wxRadioBox::MSWUpdateFontOnDPIChange(const wxSize& newDPI) m_radioButtons->SetFont(m_font); } +// ---------------------------------------------------------------------------- +// radio box drawing +// ---------------------------------------------------------------------------- + +WXHRGN wxRadioBox::MSWGetRegionWithoutChildren() +{ + RECT rc; + ::GetWindowRect(GetHwnd(), &rc); + HRGN hrgn = ::CreateRectRgn(rc.left, rc.top, rc.right + 1, rc.bottom + 1); + + const unsigned int count = GetCount(); + for ( unsigned int i = 0; i < count; ++i ) + { + // don't clip out hidden children + if ( !IsItemShown(i) ) + continue; + + ::GetWindowRect((*m_radioButtons)[i], &rc); + AutoHRGN hrgnchild(::CreateRectRgnIndirect(&rc)); + ::CombineRgn(hrgn, hrgn, hrgnchild, RGN_DIFF); + } + + return (WXHRGN)hrgn; +} + // --------------------------------------------------------------------------- // window proc for radio buttons // --------------------------------------------------------------------------- diff --git a/src/msw/statbox.cpp b/src/msw/statbox.cpp index fe5367b060..13b789086a 100644 --- a/src/msw/statbox.cpp +++ b/src/msw/statbox.cpp @@ -30,7 +30,6 @@ #include "wx/dcmemory.h" #include "wx/image.h" #include "wx/sizer.h" - #include "wx/stattext.h" #endif #include "wx/notebook.h" @@ -90,9 +89,38 @@ bool wxStaticBox::Create(wxWindow *parent, if ( !MSWCreateControl(wxT("BUTTON"), label, pos, size) ) return false; + if ( ShouldUseCustomPaint() ) + UseCustomPaint(); + return true; } +bool wxStaticBox::ShouldUseCustomPaint() const +{ + // When not using double buffering, we paint the box ourselves by default + // because using the standard control default WM_PAINT handler results in + // awful flicker. However this can be disabled by setting a system option + // which can be useful if the application paints on the box itself (which + // should be avoided, but some existing code does it). + return !IsDoubleBuffered() && + !wxSystemOptions::IsFalse(wxT("msw.staticbox.optimized-paint")); +} + +void wxStaticBox::UseCustomPaint() +{ + // If background style is already set to this value, we must have been + // already called -- and as we currently never unbind the handler, this + // means we don't need to do anything. + if ( GetBackgroundStyle() != wxBG_STYLE_PAINT ) + { + Bind(wxEVT_PAINT, &wxStaticBox::OnPaint, this); + + // Our OnPaint() completely erases our background, so don't do it in + // WM_ERASEBKGND too to avoid flicker. + SetBackgroundStyle(wxBG_STYLE_PAINT); + } +} + bool wxStaticBox::Create(wxWindow* parent, wxWindowID id, wxWindow* labelWin, @@ -144,7 +172,8 @@ WXDWORD wxStaticBox::MSWGetStyle(long style, WXDWORD *exstyle) const // navigation ourselves, but this could change in the future). *exstyle |= WS_EX_CONTROLPARENT; - *exstyle |= WS_EX_TRANSPARENT; + if ( !ShouldUseCustomPaint() ) + *exstyle |= WS_EX_TRANSPARENT; } styleWin |= BS_GROUPBOX; @@ -226,10 +255,9 @@ bool wxStaticBox::SetForegroundColour(const wxColour& colour) if ( colour.IsOk() && !m_labelWin ) { - // Switch to using a custom label as we can't support custom colours - // otherwise. - m_labelWin = new wxStaticText(this, wxID_ANY, GetLabel()); - PositionLabelWindow(); + // We need to be using our custom paint handler to support non-default + // colours. + UseCustomPaint(); } return true; @@ -249,23 +277,6 @@ bool wxStaticBox::SetFont(const wxFont& font) return true; } -void wxStaticBox::SetLabel(const wxString& label) -{ - if ( m_labelWin ) - { - // Ensure that GetLabel() returns the correct value. - m_labelOrig = label; - - // And update the actually shown label. - m_labelWin->SetLabel(label); - PositionLabelWindow(); - } - else - { - base_type::SetLabel(label); - } -} - WXLRESULT wxStaticBox::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { if ( nMsg == WM_NCHITTEST ) @@ -302,26 +313,401 @@ WXLRESULT wxStaticBox::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lPar // no, we don't, erase the background ourselves RECT rc; ::GetClientRect(GetHwnd(), &rc); - - HDC hdc = (HDC)wParam; - HBRUSH hbr = MSWGetBgBrush(hdc); - - // if there is no special brush for painting this control, just use - // the solid background colour - wxBrush brush; - if ( !hbr ) - { - brush = wxBrush(GetParent()->GetBackgroundColour()); - hbr = GetHbrushOf(brush); - } - - ::FillRect(hdc, &rc, hbr); + wxDCTemp dc((WXHDC)wParam); + PaintBackground(dc, rc); } return 0; } + if ( nMsg == WM_UPDATEUISTATE ) + { + // DefWindowProc() redraws just the static box text when it gets this + // message and it does it using the standard (blue in standard theme) + // colour and not our own label colour that we use in PaintForeground() + // resulting in the label mysteriously changing the colour when e.g. + // "Alt" is pressed anywhere in the window, see #12497. + // + // To avoid this we simply refresh the window forcing our own code + // redrawing the label in the correct colour to be called. This is + // inefficient but there doesn't seem to be anything else we can do. + // + // Notice that the problem is XP-specific and doesn't arise under later + // systems. + if ( m_hasFgCol && wxGetWinVersion() == wxWinVersion_XP ) + Refresh(); + } + return wxControl::MSWWindowProc(nMsg, wParam, lParam); } +// ---------------------------------------------------------------------------- +// static box drawing +// ---------------------------------------------------------------------------- + +/* + We draw the static box ourselves because it's the only way to prevent it + from flickering horribly on resize (because everything inside the box is + erased twice: once when the box itself is repainted and second time when + the control inside it is repainted) without using WS_EX_TRANSPARENT style as + we used to do and which resulted in other problems. + */ + +// MSWGetRegionWithoutSelf helper: removes the given rectangle from region +static inline void +SubtractRectFromRgn(HRGN hrgn, int left, int top, int right, int bottom) +{ + AutoHRGN hrgnRect(::CreateRectRgn(left, top, right, bottom)); + if ( !hrgnRect ) + { + wxLogLastError(wxT("CreateRectRgn()")); + return; + } + + ::CombineRgn(hrgn, hrgn, hrgnRect, RGN_DIFF); +} + +void wxStaticBox::MSWGetRegionWithoutSelf(WXHRGN hRgn, int w, int h) +{ + HRGN hrgn = (HRGN)hRgn; + + // remove the area occupied by the static box borders from the region + int borderTop, border; + GetBordersForSizer(&borderTop, &border); + + // top + if ( m_labelWin ) + { + // Don't exclude the entire rectangle at the top, we do need to paint + // the background of the gap between the label window and the box + // frame. + const wxRect labelRect = m_labelWin->GetRect(); + const int gap = FromDIP(LABEL_HORZ_BORDER); + + SubtractRectFromRgn(hrgn, 0, 0, labelRect.GetLeft() - gap, borderTop); + SubtractRectFromRgn(hrgn, labelRect.GetRight() + gap, 0, w, borderTop); + } + else + { + SubtractRectFromRgn(hrgn, 0, 0, w, borderTop); + } + + // bottom + SubtractRectFromRgn(hrgn, 0, h - border, w, h); + + // left + SubtractRectFromRgn(hrgn, 0, 0, border, h); + + // right + SubtractRectFromRgn(hrgn, w - border, 0, w, h); +} + +namespace { +RECT AdjustRectForRtl(wxLayoutDirection dir, RECT const& childRect, RECT const& boxRect) { + RECT ret = childRect; + if( dir == wxLayout_RightToLeft ) { + // The clipping region too is mirrored in RTL layout. + // We need to mirror screen coordinates relative to static box window priot to + // intersecting with region. + ret.right = boxRect.right - (childRect.left - boxRect.left); + ret.left = boxRect.left + (boxRect.right - childRect.right); + } + + return ret; +} +} + +WXHRGN wxStaticBox::MSWGetRegionWithoutChildren() +{ + RECT boxRc; + ::GetWindowRect(GetHwnd(), &boxRc); + HRGN hrgn = ::CreateRectRgn(boxRc.left, boxRc.top, boxRc.right + 1, boxRc.bottom + 1); + bool foundThis = false; + + // Iterate over all sibling windows as in the old wxWidgets API the + // controls appearing inside the static box were created as its siblings + // and not children. This is now deprecated but should still work. + // + // Also notice that we must iterate over all windows, not just all + // wxWindows, as there may be composite windows etc. + HWND child; + for ( child = ::GetWindow(GetHwndOf(GetParent()), GW_CHILD); + child; + child = ::GetWindow(child, GW_HWNDNEXT) ) + { + if ( ! ::IsWindowVisible(child) ) + { + // if the window isn't visible then it doesn't need clipped + continue; + } + + wxMSWWinStyleUpdater updateStyle(child); + wxString str(wxGetWindowClass(child)); + str.MakeUpper(); + if ( str == wxT("BUTTON") && updateStyle.IsOn(BS_GROUPBOX) ) + { + if ( child == GetHwnd() ) + foundThis = true; + + // Any static boxes below this one in the Z-order can't be clipped + // since if we have the case where a static box with a low Z-order + // is nested inside another static box with a high Z-order then the + // nested static box would be painted over. Doing it this way + // unfortunately results in flicker if the Z-order of nested static + // boxes is not inside (lowest) to outside (highest) but at least + // they are still shown. + if ( foundThis ) + continue; + } + + RECT rc; + ::GetWindowRect(child, &rc); + rc = AdjustRectForRtl(GetLayoutDirection(), rc, boxRc ); + if ( ::RectInRegion(hrgn, &rc) ) + { + // need to remove WS_CLIPSIBLINGS from all sibling windows + // that are within this staticbox if set + if ( updateStyle.IsOn(WS_CLIPSIBLINGS) ) + { + updateStyle.TurnOff(WS_CLIPSIBLINGS).Apply(); + + // MSDN: "If you have changed certain window data using + // SetWindowLong, you must call SetWindowPos to have the + // changes take effect." + ::SetWindowPos(child, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_FRAMECHANGED); + } + + AutoHRGN hrgnChild(::CreateRectRgnIndirect(&rc)); + ::CombineRgn(hrgn, hrgn, hrgnChild, RGN_DIFF); + } + } + + // Also iterate over all children of the static box, we need to clip them + // out as well. + for ( child = ::GetWindow(GetHwnd(), GW_CHILD); + child; + child = ::GetWindow(child, GW_HWNDNEXT) ) + { + if ( !::IsWindowVisible(child) ) + { + // if the window isn't visible then it doesn't need clipped + continue; + } + + RECT rc; + ::GetWindowRect(child, &rc); + rc = AdjustRectForRtl(GetLayoutDirection(), rc, boxRc ); + AutoHRGN hrgnChild(::CreateRectRgnIndirect(&rc)); + ::CombineRgn(hrgn, hrgn, hrgnChild, RGN_DIFF); + } + + return (WXHRGN)hrgn; +} + +// helper for OnPaint(): really erase the background, i.e. do it even if we +// don't have any non default brush for doing it (DoEraseBackground() doesn't +// do anything in such case) +void wxStaticBox::PaintBackground(wxDC& dc, const RECT& rc) +{ + wxMSWDCImpl *impl = (wxMSWDCImpl*) dc.GetImpl(); + HBRUSH hbr = MSWGetBgBrush(impl->GetHDC()); + + // if there is no special brush for painting this control, just use the + // solid background colour + wxBrush brush; + if ( !hbr ) + { + brush = wxBrush(GetParent()->GetBackgroundColour()); + hbr = GetHbrushOf(brush); + } + + ::FillRect(GetHdcOf(*impl), &rc, hbr); +} + +void wxStaticBox::PaintForeground(wxDC& dc, const RECT&) +{ + wxMSWDCImpl *impl = (wxMSWDCImpl*) dc.GetImpl(); + MSWDefWindowProc(WM_PAINT, (WPARAM)GetHdcOf(*impl), 0); + +#if wxUSE_UXTHEME + // when using XP themes, neither setting the text colour nor transparent + // background mode changes anything: the static box def window proc + // still draws the label in its own colours, so we need to redraw the text + // ourselves if we have a non default fg colour + if ( m_hasFgCol && wxUxThemeIsActive() && !m_labelWin ) + { + // draw over the text in default colour in our colour + HDC hdc = GetHdcOf(*impl); + ::SetTextColor(hdc, GetForegroundColour().GetPixel()); + + // Get dimensions of the label + const wxString label = GetLabel(); + + // choose the correct font + AutoHFONT font; + SelectInHDC selFont; + if ( m_hasFont ) + { + selFont.Init(hdc, GetHfontOf(GetFont())); + } + else // no font set, use the one set by the theme + { + wxUxThemeHandle hTheme(this, L"BUTTON"); + if ( hTheme ) + { + LOGFONTW themeFont; + if ( ::GetThemeFont + ( + hTheme, + hdc, + BP_GROUPBOX, + GBS_NORMAL, + TMT_FONT, + &themeFont + ) == S_OK ) + { + font.Init(themeFont); + if ( font ) + selFont.Init(hdc, font); + } + } + } + + // Get the font extent + int width, height; + dc.GetTextExtent(wxStripMenuCodes(label, wxStrip_Mnemonics), + &width, &height); + + // first we need to correctly paint the background of the label + // as Windows ignores the brush offset when doing it + // NOTE: Border intentionally does not use DIPs in order to match native look + const int x = LABEL_HORZ_OFFSET; + RECT dimensions = { x, 0, 0, height }; + dimensions.left = x; + dimensions.right = x + width; + + // need to adjust the rectangle to cover all the label background + dimensions.left -= LABEL_HORZ_BORDER; + dimensions.right += LABEL_HORZ_BORDER; + dimensions.bottom += LABEL_VERT_BORDER; + + if ( UseBgCol() ) + { + // our own background colour should be used for the background of + // the label: this is consistent with the behaviour under pre-XP + // systems (i.e. without visual themes) and generally makes sense + wxBrush brush = wxBrush(GetBackgroundColour()); + ::FillRect(hdc, &dimensions, GetHbrushOf(brush)); + } + else // paint parent background + { + PaintBackground(dc, dimensions); + } + + UINT drawTextFlags = DT_SINGLELINE | DT_VCENTER; + + // determine the state of UI queues to draw the text correctly under XP + // and later systems + static const bool isXPorLater = wxGetWinVersion() >= wxWinVersion_XP; + if ( isXPorLater ) + { + if ( ::SendMessage(GetHwnd(), WM_QUERYUISTATE, 0, 0) & + UISF_HIDEACCEL ) + { + drawTextFlags |= DT_HIDEPREFIX; + } + } + + // now draw the text + RECT rc2 = { x, 0, x + width, height }; + ::DrawText(hdc, label.t_str(), label.length(), &rc2, + drawTextFlags); + } +#endif // wxUSE_UXTHEME +} + +void wxStaticBox::OnPaint(wxPaintEvent& WXUNUSED(event)) +{ + RECT rc; + ::GetClientRect(GetHwnd(), &rc); + wxPaintDC dc(this); + + // No need to do anything if the client rectangle is empty and, worse, + // doing it would result in an assert when creating the bitmap below. + if ( !rc.right || !rc.bottom ) + return; + + // draw the entire box in a memory DC + wxMemoryDC memdc(&dc); + wxBitmap bitmap(rc.right, rc.bottom); + memdc.SelectObject(bitmap); + + PaintBackground(memdc, rc); + PaintForeground(memdc, rc); + + // now only blit the static box border itself, not the interior, to avoid + // flicker when background is drawn below + // + // note that it seems to be faster to do 4 small blits here and then paint + // directly into wxPaintDC than painting background in wxMemoryDC and then + // blitting everything at once to wxPaintDC, this is why we do it like this + int borderTop, border; + GetBordersForSizer(&borderTop, &border); + + // top + if ( m_labelWin ) + { + // We also have to exclude the area taken by the label window, + // otherwise there would be flicker when it draws itself on top of it. + const wxRect labelRect = m_labelWin->GetRect(); + + // We also leave a small border around label window to make it appear + // more similarly to a plain text label. + const int gap = FromDIP(LABEL_HORZ_BORDER); + + dc.Blit(border, 0, + labelRect.GetLeft() - gap - border, + borderTop, + &memdc, border, 0); + + const int xStart = labelRect.GetRight() + gap; + dc.Blit(xStart, 0, rc.right - xStart, borderTop, + &memdc, xStart, 0); + } + else + { + dc.Blit(border, 0, rc.right - border, borderTop, + &memdc, border, 0); + } + + // bottom + dc.Blit(border, rc.bottom - border, rc.right - border, border, + &memdc, border, rc.bottom - border); + // left + dc.Blit(0, 0, border, rc.bottom, + &memdc, 0, 0); + // right (note that upper and bottom right corners were already part of the + // first two blits so we shouldn't overwrite them here to avoi flicker) + dc.Blit(rc.right - border, borderTop, + border, rc.bottom - borderTop - border, + &memdc, rc.right - border, borderTop); + + + // create the region excluding box children + AutoHRGN hrgn((HRGN)MSWGetRegionWithoutChildren()); + RECT rcWin; + ::GetWindowRect(GetHwnd(), &rcWin); + ::OffsetRgn(hrgn, -rcWin.left, -rcWin.top); + + // and also the box itself + MSWGetRegionWithoutSelf((WXHRGN) hrgn, rc.right, rc.bottom); + wxMSWDCImpl *impl = (wxMSWDCImpl*) dc.GetImpl(); + HDCClipper clipToBg(GetHdcOf(*impl), hrgn); + + // paint the inside of the box (excluding box itself and child controls) + PaintBackground(dc, rc); +} + #endif // wxUSE_STATBOX