From b20552116ce57506900b7ffec579079e7032c131 Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Tue, 19 Oct 2021 01:37:06 +0100 Subject: [PATCH 1/3] Allow wxBitmapBundle to specify its preferred bitmap size Using bitmaps of preferred size avoids scaling and results in much better appearance, so add methods allowing querying the bundle about the bitmaps it supports and implement them in the various implementations. This is not actually used anywhere yet, but will be soon. --- include/wx/bmpbndl.h | 12 ++++++++ interface/wx/bmpbndl.h | 42 ++++++++++++++++++++++++++ samples/toolbar/toolbar.cpp | 7 +++++ src/common/bmpbndl.cpp | 58 ++++++++++++++++++++++++++++++++++++ src/generic/bmpsvg.cpp | 7 +++++ src/msw/bmpbndl.cpp | 47 +++++++++++++++++++++++++++++ src/osx/core/bmpbndl.mm | 9 ++++++ tests/graphics/bmpbundle.cpp | 21 +++++++++++++ 8 files changed, 203 insertions(+) diff --git a/include/wx/bmpbndl.h b/include/wx/bmpbndl.h index f5db5ab140..4024e3154c 100644 --- a/include/wx/bmpbndl.h +++ b/include/wx/bmpbndl.h @@ -15,6 +15,7 @@ #include "wx/vector.h" class wxBitmapBundleImpl; +class WXDLLIMPEXP_FWD_CORE wxWindow; // It should be possible to implement SVG rasterizing without raw bitmap // support using wxDC::DrawSpline(), but currently we don't do it and so @@ -96,6 +97,12 @@ public: // default DPI, i.e. 100% scaling. Returns invalid size for empty bundle. wxSize GetDefaultSize() const; + // Get preferred size, i.e. usually the closest size in which a bitmap is + // available to the ideal size determined from the default size and the DPI + // scaling, for the given window. + wxSize GetPreferredSizeFor(wxWindow* window) const; + wxSize GetPreferredSizeAtScale(double scale) const; + // Get bitmap of the specified size, creating a new bitmap from the closest // available size by rescaling it if necessary. // @@ -177,6 +184,11 @@ public: // Must always return a valid size. virtual wxSize GetDefaultSize() const = 0; + // Return the preferred size that should be used at the given scale. + // + // Must always return a valid size. + virtual wxSize GetPreferredSizeAtScale(double scale) const = 0; + // Retrieve the bitmap of exactly the given size. // // Note that this function is non-const because it may generate the bitmap diff --git a/interface/wx/bmpbndl.h b/interface/wx/bmpbndl.h index c17273ef04..c9392657fa 100644 --- a/interface/wx/bmpbndl.h +++ b/interface/wx/bmpbndl.h @@ -252,6 +252,31 @@ public: */ wxSize GetDefaultSize() const; + /** + Get the size that would be best to use for this bundle at the given DPI + scaling factor. + + For bundles containing some number of the fixed-size bitmaps, this + function returns the size of an existing bitmap closest to the ideal + size at the given scale, i.e. GetDefaultSize() multiplied by @a scale. + + Passing a size returned by this function to GetBitmap() ensures that + bitmap doesn't need to be rescaled, which typically significantly + lowers its quality. + */ + wxSize GetPreferredSizeAtScale(double scale) const; + + /** + Get the size that would be best to use for this bundle at the DPI + scaling factor used by the given window. + + This is just a convenient wrapper for GetPreferredSizeAtScale() calling + that function with the result of wxWindow::GetDPIScaleFactor(). + + @param window Non-null and fully created window. + */ + wxSize GetPreferredSizeFor(wxWindow* window) const; + /** Get bitmap of the specified size, creating a new bitmap from the closest available size by rescaling it if necessary. @@ -285,6 +310,16 @@ public: ... determine the minimum/default size for bitmap to use ... } + wxSize GetPreferredSizeAtScale(double scale) const wxOVERRIDE + { + // If it's ok to scale the bitmap, just use the standard size + // at the given scale: + return GetDefaultSize()*scale; + + ... otherwise, an existing bitmap of the size closest to the + one above would need to be found and its size returned ... + } + wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE { ... get the bitmap of the requested size from somewhere and @@ -314,6 +349,13 @@ public: */ virtual wxSize GetDefaultSize() const = 0; + /** + Return the preferred size that should be used at the given scale. + + Must always return a valid size. + */ + virtual wxSize GetPreferredSizeAtScale(double scale) const = 0; + /** Retrieve the bitmap of exactly the given size. diff --git a/samples/toolbar/toolbar.cpp b/samples/toolbar/toolbar.cpp index 9222ace1bb..41c5fb73a3 100644 --- a/samples/toolbar/toolbar.cpp +++ b/samples/toolbar/toolbar.cpp @@ -520,6 +520,13 @@ void MyFrame::PopulateToolbar(wxToolBarBase* toolBar) return m_sizeDef; } + wxSize GetPreferredSizeAtScale(double scale) const wxOVERRIDE + { + // We just scale the bitmap to fit the requested size, so + // we don't really have any preferences. + return m_sizeDef*scale; + } + wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE { // In this simple implementation we don't bother caching diff --git a/src/common/bmpbndl.cpp b/src/common/bmpbndl.cpp index 0628b5ecd0..401a74b523 100644 --- a/src/common/bmpbndl.cpp +++ b/src/common/bmpbndl.cpp @@ -23,6 +23,7 @@ #include "wx/bmpbndl.h" #include "wx/icon.h" +#include "wx/window.h" #include "wx/private/bmpbndl.h" @@ -64,6 +65,7 @@ public: } virtual wxSize GetDefaultSize() const wxOVERRIDE; + virtual wxSize GetPreferredSizeAtScale(double scale) const wxOVERRIDE; virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE; #ifdef __WXOSX__ @@ -157,6 +159,47 @@ wxSize wxBitmapBundleImplSet::GetDefaultSize() const return m_entries[0].bitmap.GetSize(); } +wxSize wxBitmapBundleImplSet::GetPreferredSizeAtScale(double scale) const +{ + // Target size is the ideal size we'd like the bitmap to have at this scale. + const wxSize sizeTarget = GetDefaultSize()*scale; + + const size_t n = m_entries.size(); + for ( size_t i = 0; i < n; ++i ) + { + const wxSize sizeThis = m_entries[i].bitmap.GetSize(); + + // Keep looking for the exact match which we still can hope to find + // while the current bitmap is smaller. + if ( sizeThis.y < sizeTarget.y ) + continue; + + // If we've found the exact match, just return it. + if ( sizeThis.y == sizeTarget.y ) + return sizeThis; + + // We've found the closest bigger bitmap. + + // If there is no smaller bitmap, we have no choice but to use this one. + if ( i == 0 ) + return sizeThis; + + // Decide whether we should use this one or the previous smaller one + // depending on which of them is closer to the target size, breaking + // the tie in favour of the bigger size. + const wxSize sizeLast = m_entries[i - 1].bitmap.GetSize(); + + return sizeThis.y - sizeTarget.y <= sizeTarget.y - sizeLast.y + ? sizeThis + : sizeLast; + + } + + // We only get here if the target size is bigger than all the available + // sizes, in which case we have no choice but to use the biggest bitmap. + return m_entries.back().bitmap.GetSize(); +} + wxBitmap wxBitmapBundleImplSet::GetBitmap(const wxSize& size) { // We use linear search instead if binary one because it's simpler and the @@ -326,6 +369,21 @@ wxSize wxBitmapBundle::GetDefaultSize() const return m_impl->GetDefaultSize(); } +wxSize wxBitmapBundle::GetPreferredSizeFor(wxWindow* window) const +{ + wxCHECK_MSG( window, wxDefaultSize, "window must be valid" ); + + return GetPreferredSizeAtScale(window->GetDPIScaleFactor()); +} + +wxSize wxBitmapBundle::GetPreferredSizeAtScale(double scale) const +{ + if ( !m_impl ) + return wxDefaultSize; + + return m_impl->GetPreferredSizeAtScale(scale); +} + wxBitmap wxBitmapBundle::GetBitmap(const wxSize& size) const { if ( !m_impl ) diff --git a/src/generic/bmpsvg.cpp b/src/generic/bmpsvg.cpp index 8d1b4fc1bc..1c34e5215b 100644 --- a/src/generic/bmpsvg.cpp +++ b/src/generic/bmpsvg.cpp @@ -93,6 +93,7 @@ public: } virtual wxSize GetDefaultSize() const wxOVERRIDE; + virtual wxSize GetPreferredSizeAtScale(double scale) const wxOVERRIDE; virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE; private: @@ -126,6 +127,12 @@ wxSize wxBitmapBundleImplSVG::GetDefaultSize() const return m_sizeDef; } +wxSize wxBitmapBundleImplSVG::GetPreferredSizeAtScale(double scale) const +{ + // We consider that we can render at any scale. + return m_sizeDef*scale; +} + wxBitmap wxBitmapBundleImplSVG::GetBitmap(const wxSize& size) { if ( !m_cachedBitmap.IsOk() || m_cachedBitmap.GetSize() != size ) diff --git a/src/msw/bmpbndl.cpp b/src/msw/bmpbndl.cpp index f4c82894da..4380df8f14 100644 --- a/src/msw/bmpbndl.cpp +++ b/src/msw/bmpbndl.cpp @@ -138,6 +138,7 @@ public: const wxBitmap& bitmap); virtual wxSize GetDefaultSize() const wxOVERRIDE; + virtual wxSize GetPreferredSizeAtScale(double scale) const wxOVERRIDE; virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE; private: @@ -185,6 +186,52 @@ wxSize wxBitmapBundleImplRC::GetDefaultSize() const return m_bitmaps[0].GetSize(); } +wxSize wxBitmapBundleImplRC::GetPreferredSizeAtScale(double scale) const +{ + // Optimistically assume we're going to use this exact scale by default. + double scalePreferred = scale; + + for ( size_t i = 0; ; ++i ) + { + if ( i == m_resourceInfos.size() ) + { + // The requested scale is bigger than anything we have, so use the + // biggest available one. + scalePreferred = m_resourceInfos[i - 1].scale; + break; + } + + const double scaleThis = m_resourceInfos[i].scale; + + // Keep looking for the exact match which we still can hope to find + // while the current scale is smaller. + if ( scaleThis < scale ) + continue; + + // If we've found the exact match, just use it. + if ( scaleThis == scale ) + break; + + // We've found the closest bigger scale. + + // If there is no smaller one, we have no choice but to use this one. + if ( i == 0 ) + break; + + // Decide whether we should use this one or the previous smaller one + // depending on which of them is closer to the target scale, breaking + // the tie in favour of the bigger one. + const double scaleLast = m_resourceInfos[i - 1].scale; + + scalePreferred = scaleThis - scale <= scale - scaleLast + ? scaleThis + : scaleLast; + break; + } + + return GetDefaultSize()*scalePreferred; +} + wxBitmap wxBitmapBundleImplRC::AddBitmap(const ResourceInfo& info, const wxSize& sizeExpected, diff --git a/src/osx/core/bmpbndl.mm b/src/osx/core/bmpbndl.mm index 7fdb89a78a..10663232de 100644 --- a/src/osx/core/bmpbndl.mm +++ b/src/osx/core/bmpbndl.mm @@ -46,6 +46,7 @@ public: ~wxOSXImageBundleImpl(); virtual wxSize GetDefaultSize() const wxOVERRIDE; + virtual wxSize GetPreferredSizeAtScale(double scale) const wxOVERRIDE; virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE; virtual WXImage OSXGetImage() const wxOVERRIDE; @@ -76,6 +77,14 @@ wxSize wxOSXImageBundleImpl::GetDefaultSize() const return wxSize(sz.width, sz.height); } +wxSize wxOSXImageBundleImpl::GetPreferredSizeAtScale(double scale) const +{ + // The system always performs scaling, as the scaling factor is integer and + // so it doesn't make sense to round it up or down, hence we should use the + // theoretical best size for given scale. + return GetDefaultSize()*scale; +} + wxBitmap wxOSXImageBundleImpl::GetBitmap(const wxSize& size) { return wxBitmap(); diff --git a/tests/graphics/bmpbundle.cpp b/tests/graphics/bmpbundle.cpp index aaddfc546a..b14a25148c 100644 --- a/tests/graphics/bmpbundle.cpp +++ b/tests/graphics/bmpbundle.cpp @@ -47,6 +47,27 @@ TEST_CASE("BitmapBundle::FromBitmaps", "[bmpbundle]") CHECK( b.GetBitmap(wxSize(24, 24)).GetSize() == wxSize(24, 24) ); } +TEST_CASE("BitmapBundle::GetPreferredSize", "[bmpbundle]") +{ + CHECK( wxBitmapBundle().GetPreferredSizeAtScale(1) == wxDefaultSize ); + + const wxSize normal(32, 32); + const wxSize bigger(64, 64); + + const wxBitmapBundle + b = wxBitmapBundle::FromBitmaps(wxBitmap(normal), wxBitmap(bigger)); + + CHECK( b.GetPreferredSizeAtScale(0 ) == normal ); + CHECK( b.GetPreferredSizeAtScale(1 ) == normal ); + CHECK( b.GetPreferredSizeAtScale(1.25) == normal ); + CHECK( b.GetPreferredSizeAtScale(1.4 ) == normal ); + CHECK( b.GetPreferredSizeAtScale(1.5 ) == bigger ); + CHECK( b.GetPreferredSizeAtScale(1.75) == bigger ); + CHECK( b.GetPreferredSizeAtScale(2 ) == bigger ); + CHECK( b.GetPreferredSizeAtScale(2.5 ) == bigger ); + CHECK( b.GetPreferredSizeAtScale(3 ) == bigger ); +} + #ifdef wxHAS_SVG TEST_CASE("BitmapBundle::FromSVG", "[bmpbundle][svg]") From 1f8af4d5a2cfd4f09b147277b43e701e09a7a683 Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Tue, 19 Oct 2021 01:41:36 +0100 Subject: [PATCH 2/3] Use preferred size for the button bitmaps in wxMSW Avoid scaling the bitmaps by using the preferred size for them. This results in significantly better appearance without any real drawbacks in practice at 125% and 175% DPI scaling in the common case when just a single and double-sized bitmaps are available, and still seems to be acceptable at 150% scaling in this case. --- src/msw/anybutton.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/msw/anybutton.cpp b/src/msw/anybutton.cpp index 0364d24ee5..b62d516aa0 100644 --- a/src/msw/anybutton.cpp +++ b/src/msw/anybutton.cpp @@ -107,7 +107,7 @@ class wxButtonImageData: public wxObject public: wxButtonImageData(wxWindow* btn, const wxBitmapBundle& normalBundle) { - m_bitmapSize = normalBundle.GetDefaultSize() * btn->GetDPIScaleFactor(); + m_bitmapSize = normalBundle.GetPreferredSizeFor(btn); m_bitmapBundles[wxAnyButton::State_Normal] = normalBundle; } @@ -254,7 +254,7 @@ public: // the image list wxXPButtonImageData(wxAnyButton *btn, const wxBitmapBundle& bitmapBundle) : wxButtonImageData(btn, bitmapBundle), - m_hwndBtn(GetHwndOf(btn)) + m_btn(btn) { InitImageList(); @@ -403,7 +403,7 @@ private: void UpdateImageInfo() { - if ( !::SendMessage(m_hwndBtn, BCM_SETIMAGELIST, 0, (LPARAM)&m_data) ) + if ( !::SendMessage(GetHwndOf(m_btn), BCM_SETIMAGELIST, 0, (LPARAM)&m_data) ) { wxLogDebug("SendMessage(BCM_SETIMAGELIST) failed"); } @@ -415,7 +415,7 @@ private: // We need to recreate the image list using the new size and re-add all // bitmaps to it. - m_bitmapSize = event.Scale(m_bitmapSize); + m_bitmapSize = m_bitmapBundles[wxAnyButton::State_Normal].GetPreferredSizeFor(m_btn); m_iml.Destroy(); InitImageList(); @@ -431,7 +431,7 @@ private: BUTTON_IMAGELIST m_data; // the button we're associated with - const HWND m_hwndBtn; + wxWindow* const m_btn; wxDECLARE_NO_COPY_CLASS(wxXPButtonImageData); From ae5bc69d9f39e095a6ea60bb5915ec064ae3c49e Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Tue, 19 Oct 2021 02:17:26 +0100 Subject: [PATCH 3/3] Use preferred bundle bitmaps size in wxMSW wxToolBar This avoids resizing the bitmaps in some common use cases and is similar to the previous commit doing the same thing for wxButton. Notice that this introduces a slight change of behaviour, as AdjustToolBitmapSize() now can reduce, as well as increase, the size of bitmaps used, which is necessary in order to shrink the bitmaps when moving from a higher DPI resolution display to a lower-resolution one. This is not 100% backwards-compatible, but should only have not very bad cosmetic effects. --- src/common/tbarbase.cpp | 83 ++++++++++++++++++++++++++++++++++++++--- src/msw/toolbar.cpp | 4 -- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/common/tbarbase.cpp b/src/common/tbarbase.cpp index 9d2cbbbcbe..cda354fe36 100644 --- a/src/common/tbarbase.cpp +++ b/src/common/tbarbase.cpp @@ -32,6 +32,7 @@ #include "wx/image.h" #endif // WXWIN_COMPATIBILITY_2_8 #include "wx/menu.h" + #include "wx/vector.h" #endif extern WXDLLEXPORT_DATA(const char) wxToolBarNameStr[] = "toolbar"; @@ -432,6 +433,39 @@ void wxToolBarBase::ClearTools() } } +namespace +{ + +// Struct containing the number of tools preferring to use the given size. +struct SizePrefWithCount +{ + SizePrefWithCount() : count(0) { } + + wxSize size; + int count; +}; + +typedef wxVector SizePrefs; + +void RecordSizePref(SizePrefs& prefs, const wxSize& size) +{ + for ( size_t n = 0; n < prefs.size(); ++n ) + { + if ( prefs[n].size == size ) + { + prefs[n].count++; + return; + } + } + + SizePrefWithCount pref; + pref.size = size; + pref.count++; + prefs.push_back(pref); +} + +} // anonymous namespace + void wxToolBarBase::AdjustToolBitmapSize() { if ( HasFlag(wxTB_NOICONS) ) @@ -442,8 +476,10 @@ void wxToolBarBase::AdjustToolBitmapSize() const wxSize sizeOrig(m_defaultWidth, m_defaultHeight); - wxSize sizeActual(sizeOrig); - + // We want to use preferred bitmap size, but the preferred sizes can be + // different for different bitmap bundles, so record all their preferences + // first. + SizePrefs prefs; const double scale = GetDPIScaleFactor(); for ( wxToolBarToolsList::const_iterator i = m_tools.begin(); i != m_tools.end(); @@ -451,11 +487,48 @@ void wxToolBarBase::AdjustToolBitmapSize() { const wxBitmapBundle& bmp = (*i)->GetNormalBitmapBundle(); if ( bmp.IsOk() ) - sizeActual.IncTo(bmp.GetDefaultSize()*scale); + RecordSizePref(prefs, bmp.GetPreferredSizeAtScale(scale)); } - if ( sizeActual != sizeOrig ) - SetToolBitmapSize(sizeActual); + // Now find the size preferred by most tools. + int countMax = 0; + wxSize sizePreferred; + for ( size_t n = 0; n < prefs.size(); ++n ) + { + const int countThis = prefs[n].count; + const wxSize sizeThis = prefs[n].size; + + if ( countThis > countMax ) + { + countMax = countThis; + sizePreferred = sizeThis; + } + else if ( countThis == countMax ) + { + // We have a tie between different sizes, choose the one + // corresponding to the current scale factor, if possible, as this + // is the ideal bitmap size that should be consistent with all the + // other bitmaps. + if ( sizePreferred != sizeOrig*scale ) + { + if ( sizeThis == sizeOrig*scale ) + { + sizePreferred = sizeThis; + } + else // Neither of the sizes is the ideal one. + { + // Choose the larger one as like this some bitmaps will be + // downscaled, which should look better than upscaling some + // (other) ones. + if ( sizeThis.y > sizePreferred.y ) + sizePreferred = sizeThis; + } + } + } + } + + if ( sizePreferred != sizeOrig ) + SetToolBitmapSize(sizePreferred); } bool wxToolBarBase::Realize() diff --git a/src/msw/toolbar.cpp b/src/msw/toolbar.cpp index b9d38736c8..081a864d67 100644 --- a/src/msw/toolbar.cpp +++ b/src/msw/toolbar.cpp @@ -1949,10 +1949,6 @@ void wxToolBar::RealizeHelper() void wxToolBar::OnDPIChanged(wxDPIChangedEvent& event) { - // Ensure that when Realize() is called, the bitmaps size corresponding to - // the new resolution will be used. - SetToolBitmapSize(event.Scale(wxSize(m_defaultWidth, m_defaultHeight))); - // Manually scale the size of the controls. Even though the font has been // updated, the internal size of the controls does not. wxToolBarToolsList::compatibility_iterator node;