Merge branch 'dpi-docs-improve'

Improve high DPI support documentation.

Closes #18889, #22011.
This commit is contained in:
Vadim Zeitlin 2022-01-17 17:44:06 +01:00
commit 94b4f738d4
2 changed files with 185 additions and 33 deletions

View File

@ -2,7 +2,7 @@ High DPI Support in wxWidgets {#overview_high_dpi}
=============================
[TOC]
Introduction
Introduction {#high_dpi_intro}
============
Many modern displays have way more pixels on the same surface than used to be
@ -50,10 +50,10 @@ on high DPI displays is needed: one which allows to scale some pixel values
drawing, which should remain unscaled to use the full available resolution).
Pixel Values in wxWidgets
Pixel Values in wxWidgets {#high_dpi_pixel_types}
=========================
Logical and Device-Independent Pixels
Logical and Device-Independent Pixels {#high_dpi_lp_and_dip}
-------------------------------------
Some systems like eg Apple's OSes automatically scale all the coordinates by
@ -69,55 +69,206 @@ pixels**, abbreviated as "DIP", that are always of the same size on all
displays and all platforms.
Thus, the first thing do when preparing your application for high DPI support
is to stop using raw pixel values. Actually, using any pixel values is not
recommended and replacing them with the values based on the text metrics, i.e.
obtained using wxWindow::GetTextExtent(), or expressing them in dialog units
(see wxWindow::ConvertDialogToPixels()) is preferable. However the simplest
change is to just replace the pixel values with the values in DIP: for this,
just use wxWindow::FromDIP() to convert from one to the other.
is to stop using raw pixel values, as they mean different things under
different platforms when DPI scaling is used. Actually, using any pixel values
is not recommended and replacing them with the values based on the text
metrics, i.e. obtained using wxWindow::GetTextExtent(), or expressing them in
dialog units (see wxWindow::ConvertDialogToPixels()) is preferable. However
the simplest change is to just replace the pixel values with the values in
DIP: for this, just use wxWindow::FromDIP() to convert from one to the other.
For example, if you have the existing code:
```cpp
~~~{cpp}
myFrame->SetClientSize(wxSize(400, 300));
```
~~~
you can just replace it with
```cpp
~~~{cpp}
myFrame->SetClientSize(myFrame->FromDIP(wxSize(400, 300)));
```
~~~
although replacing it with something like
~~~{cpp}
const wxSize sizeM = myFrame->GetTextExtent("M");
myFrame->SetClientSize(100*sizeM.x, 40*sizeM.y));
~~~
might be even better.
Physical Pixels
In any case, remember that window and wxDC or wxGraphicsContext coordinates
must be in logical pixels that can depend on the current DPI scaling, and so
should never be fixed at compilation time.
Physical Pixels {#high_dpi_pp}
---------------
In addition to (logical) pixels and DIPs discussed above, you may also need to
work in physical pixel coordinates, corresponding to the actual display pixels.
Physical pixels are never scaled, on any platform, and must be used when
drawing graphics elements to ensure that the best possible resolution is used.
For example, all operations on wxGLCanvas use physical pixels.
Physical pixels are never scaled, on any platform, and are used for the real
bitmap sizes. They are also used for drawing operations on wxGLCanvas, which
is _different_ from wxDC and wxGraphicsContext that use logical pixels.
To convert between logical and physical pixels, you can use
wxWindow::GetContentScaleFactor(): this is a value greater than or equal to 1,
so a value in logical pixels needs to be multiplied by it in order to obtain
the value in physical pixels.
Under MSW physical pixels are same as logical ones, but when writing portable
code you need to convert between logical and physical pixels. This can be done
using convenience wxWindow::FromPhys() and wxWindow::ToPhys() functions
similar to the DIP functions above or by directly multiplying or dividing by
wxWindow::GetContentScaleFactor(): this function returns a value greater than
or equal to 1 (and always just 1 under MSW), so a value in logical pixels
needs to be multiplied by it in order to obtain the value in physical pixels.
For example, in a wxGLCanvas created with the size of 100 (logical) pixels, the
rightmost physical pixel coordinate will be `100*GetContentScaleFactor()`.
rightmost physical pixel coordinate will be `100*GetContentScaleFactor()`
under all platforms.
You can convert from DIPs to physical pixels by converting DIPs to the logical
pixels first, but you can also do it directly, by using
wxWindow::GetDPIScaleFactor(). This function can return a value different from
1 even under MSW, i.e. it returns DPI scaling for physical display pixels.
**Warning:** It is possible that conversion between different pixel
coordinates is not lossless due to rounding. E.g. to create a window big
enough to show a bitmap 15 pixels wide, you need to use `FromPhys(15)`,
however the exact result of this function is not representable as an `int`
when using 200% DPI scaling. In this case, the value is always rounded
upwards, i.e. the function returns `8`, to ensure that a window of this size
in logical pixels is always big enough to show the bitmap, but this can only
be done at the price of having one "extra" pixel in the window.
High-Resolution Images and Artwork
Summary of Different Pixel Kinds {#high_dpi_pixel_conversions}
--------------------------------
Under MSW, logical pixels are always the same as physical pixels, but are
different from DIPs, while under all the other platforms with DPI scaling
support (currently only GTK 3 and macOS), logical pixels are the same as DIP,
but different from physical pixels.
However under all platforms the following functions can be used to convert
between different kinds of pixels:
* From DIP to logical pixels: use wxWindow::FromDIP() or wxWindow::ToDIP().
* From physical to logical pixels: use wxWindow::FromPhys() or wxWindow::ToPhys().
* From DIP to physical pixels: multiply or divide by wxWindow::GetDPIScaleFactor().
Or, in the diagram form:
@dot
digraph Pixels
{
node [shape = hexagon, style = filled, fontname = Helvetica];
LP [fillcolor = lightblue, label = "Logical\nPixels"];
DIP [fillcolor = yellow, label = "DI\nPixels"];
PP [fillcolor = green, label = "Physical\nPixels"];
LP -> PP [fontname = Helvetica, labeldistance = 5, labelangle = 30, dir = both, weight = 2, minlen = 3, headlabel = "ToPhys()", taillabel = "FromPhys()"];
LP -> DIP [fontname = Helvetica, labeldistance = 6, dir = both, weight = 2, minlen = 3, headlabel = "ToDIP()", taillabel = "FromDIP()"];
DIP -> PP [fontname = Helvetica, dir = both, minlen = 10, label = "GetDPIScaleFactor()" headlabel = "multiply by", taillabel = "divide by", constraint = false] ;
}
@enddot
High-Resolution Images and Artwork {#high_dpi_artwork}
==================================
In order to benefit from the increased detail on High DPI devices you might want
In order to really benefit from the increased detail on High DPI devices you need
to provide the images or artwork your application uses in higher resolutions as
well. Note that it is not recommended to just provide a high-resolution version
and let the system scale that down on 1x displays. Apart from performance
consideration also the quality might suffer, contours become more blurry.
consideration also the quality might suffer, contours become more blurry, so
for best results it is recommended to use the images that can be used without
scaling at the common DPI values, i.e. at least 100% and 200% scaling. If you
don't want providing several copies of all bitmaps, you can use a single
vector image in [SVG format][1] instead.
You can use vector based graphics like SVG or you can add the same image at different
sizes / resolutions.
[1]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics
[comment]: # (TODO: API and Use Cases)
In either case, you must use wxBitmapBundle class representing several
different versions of the same bitmap (or even potentially just a single one,
which makes it upwards-compatible with wxBitmap). Most functions accepting
wxBitmap or wxImage in wxWidgets API have been updated to work with
wxBitmapBundle instead, which allows the library to select the appropriate
size depending on the current DPI and, for the platforms supporting it
(currently only MSW and macOS), even update the bitmap automatically if the
DPI changes, as can happen, for example, when the window showing the bitmap is
moved to another monitor with a different resolution.
Platform-Specific Build Issues
Note that other than creating wxBitmapBundle instead of wxBitmap, no other
changes are needed. Moreover, when upgrading the existing code it is possible
to replace some wxBitmap objects by wxBitmapBundle while keeping the others.
Using Multiple Bitmaps {#high_dpi_bundle_bitmaps}
----------------------
When using multiple bitmaps, the simplest way to create a wxBitmapBundle is to
do it from a vector of bitmaps of different sizes. In the most common case of
just two bitmaps, a special constructor taking these bitmaps directly can be
used rather than having to construct a vector from them:
~~~{cpp}
wxBitmap normal(32, 32);
wxBitmap highDPI(64, 64);
... initialize the bitmaps somehow ...
wxBitmapBundle bundle(normal, bitmap);
// Now the bundle can be passed to any wxWidgets control using bitmaps.
~~~
For the platforms where it is common to embed bitmaps in the resources, it is
possible to use wxBitmapBundle::FromResources() to create a bundle containing
all bitmaps using the given base name, i.e. `foo` for the normal bitmap and
`foo_2x` for the bitmap for 200% scaling (for fractional values decimal
separator must be replaced with underscore, e.g. use `foo_1_5x` for the bitmap
to use 150% scaling).
It is also possible to create wxBitmapBundle from the files using the same
naming convention with wxBitmapBundle::FromFiles(). And, to abstract the
differences between the platforms using resources and the other ones, a helper
wxBITMAP_BUNDLE_2() macro which uses resources if possible or files otherwise
is provided, similar to wxBITMAP_PNG() macro for plain bitmaps.
Independently of the way in which the bundle was created, it will provide the
bitmap closest in size to the expected size at the current DPI, while trying
to avoid having scale it. This means that at 175% DPI scaling, for example,
the high DPI (i.e. double-sized) bitmap will be used _without_ scaling rather
than scaling it by 0.875, which almost certainly wouldn't look good. However
if the current DPI scaling is 300%, the 2x bitmap will be scaled, if it's the
closest one available, as using it without scaling would appear in bitmaps too
small to use. The cut-off for the decision whether to scale bitmap or use an
existing one without scaling is the factor of 1.5: if the mismatch between the
required and closest available size is equal or greater than 1.5, the bitmap
will be scaled. Otherwise it will be used in its natural size.
If this behaviour is inappropriate for your application, it is also possible
to define a custom wxBitmapBundle implementation, see wxBitmapBundleImpl for
more details.
Using Vector Graphics {#high_dpi_bundle_vector}
---------------------
As an alternative to providing multiple versions of the bitmaps, it is
possible to use a single SVG image and create the bitmap bundle using either
wxBitmapBundle::FromSVG() or wxBitmapBundle::FromSVGFile(). Such bitmap
bundles will always produce bitmaps of exactly the required size, at any
resolution. At normal DPI, i.e. without any scaling, the size of the resulting
bitmap will be the default bundle size, which must be provided when creating
this kind of bitmap bundle, as SVG image itself doesn't necessarily contain
this information.
Note that wxWidgets currently uses [NanoSVG][1] library for SVG support and so
doesn't support all SVG standard features and you may need to simplify or
tweak the SVG files to make them appear correctly.
[1]: https://github.com/memononen/nanosvg
wxBitmapBundle and XRC {#high_dpi_bundle_xrc}
----------------------
XRC format has been updated to allow specifying wxBitmapBundle with
`<bitmaps>` tag in all the places where wxBitmap could be specified using
`<bitmap>` before (of course, using the latter tag is still supported). Either
multiple bitmaps or a single SVG image can be used.
Platform-Specific Build Issues {#high_dpi_platform_specific}
==============================
Generally speaking, all systems handle applications not specifically marked as
@ -126,7 +277,7 @@ up, resulting in blurry graphics and fonts, but globally preserving the
application appearance. For the best results, the application needs to be
explicitly marked as DPI-aware in a platform-dependent way.
MSW
MSW {#high_dpi_platform_msw}
---
The behaviour of the application when running on a high-DPI display depends on
@ -141,7 +292,7 @@ full, per-monitor DPI awareness supported by Windows 10 version 1703 or later.
[1]: https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests
[2]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
macOS
macOS {#high_dpi_platform_mac}
-----
DPI-aware applications must set their `NSPrincipalClass` to `wxNSApplication`

View File

@ -199,7 +199,8 @@ project, there are several things you must do.
`defaults write com.apple.dt.Xcode UseSanitizedBuildSystemEnvironment -bool NO`
- Set the variables for use with the launch agent (application to OSX 10.10
and up)
```
~~~{xml}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@ -218,7 +219,7 @@ launchctl setenv WXWIN /Users/dconnet/devtools/wx/wxWidgets-3.1.5
<true/>
</dict>
</plist>
```
~~~
### Other IDEs