Merge branch 'msw-statbox-paint'

Fix and simplify painting wxStaticBox in wxMSW.

See #22952.

Closes #22940.
This commit is contained in:
Vadim Zeitlin 2022-11-12 17:42:40 +01:00
commit 1c2ec47676
6 changed files with 62 additions and 447 deletions

View file

@ -160,8 +160,6 @@ 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

View file

@ -62,8 +62,11 @@ public:
virtual void GetBordersForSizer(int *borderTop, int *borderOther) const override;
virtual bool SetBackgroundColour(const wxColour& colour) override;
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
@ -78,23 +81,11 @@ 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();
using base_type = wxCompositeWindowSettersOnly<wxStaticBoxBase>;
wxDECLARE_DYNAMIC_CLASS_NO_COPY(wxStaticBox);
};

View file

@ -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}
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).
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.
@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

View file

@ -583,7 +583,9 @@ void StaticWidgetsPage::OnBoxCheckBox(wxCommandEvent& event)
void StaticWidgetsPage::OnButtonBoxText(wxCommandEvent& WXUNUSED(event))
{
m_sizerStatBox->GetStaticBox()->SetLabel(m_textBox->GetValue());
wxStaticBox* const box = m_sizerStatBox->GetStaticBox();
box->SetLabel(m_textBox->GetValue());
wxLogMessage("Box label changed, now is '%s'", box->GetLabel());
}
void StaticWidgetsPage::OnButtonLabelText(wxCommandEvent& WXUNUSED(event))

View file

@ -756,31 +756,6 @@ 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
// ---------------------------------------------------------------------------

View file

@ -30,6 +30,7 @@
#include "wx/dcmemory.h"
#include "wx/image.h"
#include "wx/sizer.h"
#include "wx/stattext.h"
#endif
#include "wx/notebook.h"
@ -89,15 +90,6 @@ bool wxStaticBox::Create(wxWindow *parent,
if ( !MSWCreateControl(wxT("BUTTON"), label, pos, size) )
return false;
if (!wxSystemOptions::IsFalse(wxT("msw.staticbox.optimized-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);
}
return true;
}
@ -152,8 +144,7 @@ WXDWORD wxStaticBox::MSWGetStyle(long style, WXDWORD *exstyle) const
// navigation ourselves, but this could change in the future).
*exstyle |= WS_EX_CONTROLPARENT;
if (wxSystemOptions::IsFalse(wxT("msw.staticbox.optimized-paint")))
*exstyle |= WS_EX_TRANSPARENT;
*exstyle |= WS_EX_TRANSPARENT;
}
styleWin |= BS_GROUPBOX;
@ -228,9 +219,25 @@ bool wxStaticBox::SetBackgroundColour(const wxColour& colour)
return wxStaticBoxBase::SetBackgroundColour(colour);
}
bool wxStaticBox::SetForegroundColour(const wxColour& colour)
{
if ( !base_type::SetForegroundColour(colour) )
return false;
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();
}
return true;
}
bool wxStaticBox::SetFont(const wxFont& font)
{
if ( !wxCompositeWindowSettersOnly<wxStaticBoxBase>::SetFont(font) )
if ( !base_type::SetFont(font) )
return false;
// We need to reposition the label as its size may depend on the font.
@ -242,6 +249,23 @@ 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 )
@ -278,401 +302,26 @@ WXLRESULT wxStaticBox::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lPar
// no, we don't, erase the background ourselves
RECT rc;
::GetClientRect(GetHwnd(), &rc);
wxDCTemp dc((WXHDC)wParam);
PaintBackground(dc, 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);
}
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);
dc.Blit(labelRect.GetRight() + gap, 0,
rc.right - (labelRect.GetRight() + gap),
borderTop,
&memdc, border, 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