Skip to content

Commit de56a75

Browse files
akoch-yattaHeikoKlare
authored andcommitted
Add documentation for HiDPI support for Windows
This commits adds a documentation for the HiDPI support for the win32 implementation. It is focusing on the perspective of a SWT developer not a SWT user.
1 parent 361644c commit de56a75

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# SWT HiDPI Support in SWT for Windows
2+
3+
## Table of Contents
4+
5+
1. [Introduction](#introduction)
6+
2. [Design Principles](#design-principles)
7+
3. [Architecture Overview](#architecture-overview)
8+
4. [DPI Change Propagation](#dpi-change-propagation)
9+
5. [Resource Management](#resource-management)
10+
6. [Configuration of Autoscaling behavior](#configuration-of-autoscaling-behavior)
11+
7. [Additional Information and Challenges](#additional-information-and-challenges)
12+
13+
---
14+
15+
## Introduction
16+
17+
HiDPI (High Dots Per Inch) support in SWT for Windows enables applications to function as expected on monitors with high pixel density.
18+
This documentation describes the behavior and implementation of HiDPI functionality in the Windows implementation of SWT.
19+
20+
### What is HiDPI?
21+
22+
HiDPI displays have a higher pixel density than conventional monitors.
23+
Windows supports various scaling levels (e.g., 125%, 150%, 175%, 200%) that allow users to make content readable on high-resolution displays.
24+
This UI scaling is of course not limited to HiDPI displays, but can be applied to conventional monitors as well.
25+
26+
### History of HiDPI support
27+
28+
The HiDPI support in SWT for Windows evolved in two phases:
29+
30+
The first phase can be considered as *static* HiDPI support and was targeted on rendering a sharp UI for primary monitor zoom at startup.
31+
Changing the zoom at runtime was not properly support.
32+
Technically it focused on the SWT autoscaling mode *integer200* and a *System* DPI awareness for the OS.
33+
*integer200* results in the native zoom of amonitor being converted internally either to 100% scaling on native zoom < 175% and 200% otherwise.
34+
This means Widget will behave e.g. on a 150% the same as on a 100%, with the expections of Fonts.
35+
*System* DPI awareness means the native monitor zoom is only considered for the primary monitor on startup.
36+
If the application is moved to another monitor with another zoom, the OS will scale the application itself - usually resulting in a blurry UI.
37+
38+
The second phase can be considered as *dynamic* HiDPI support and was targeted on rendering a sharp UI for every monitor zoom while adapting automatically, e.g. if a Shell is moved to a monitor with different zoom.
39+
The existing implementation was extended and focused on the autoscaling mode *quarter* and a *PerMonitorV2* DPI awareness for the OS.
40+
*quarter* results in all elements using a value rounded down to a multiple of 25% internally in SWT, e.g. 125% will be used as 125%, a custom zoom of 130% would be rounded down to 125%.
41+
This decision was made as the proposed zooms of Windows itself are proposed as *quarter*.
42+
*PerMonitorV2* DPI awareness means the native monitor zoom should be considered for each Window (=Shell) separately.
43+
If a Shell is moved to another monitor with another zoom, the Shell and its contents rescale themselves to the new context.
44+
This is particularly important in multi-monitor environments where different monitors can have different DPI settings.
45+
46+
---
47+
48+
## Design Principles
49+
50+
The introduction and extension of HiDPI support in SWT for Windows required structural changes designed to remain fully backward compatible.
51+
All adaptations follow these principles:
52+
53+
### Backwards Compatibility
54+
55+
Existing behavior should not worsen.
56+
For example, the different autoscale modes ("integer", "quarter", "false" or with fixed value) should still work as previously.
57+
One example for this is that font sizes were always based on the monitor zoom of the Shell, not on the autoscale mode.
58+
This includes of course the implementations for Linux and MacOS of SWT.
59+
Although proper HiDPI behavior in Windows requires more effort and complexity on SWT side than on Linux and MacOS, it must not have a negative side effect on the other implementation.
60+
61+
62+
### Per-Monitor DPI Awareness
63+
64+
Each `Shell` maintains its own zoom.
65+
Widgets inherit this zoom on creation and adjust when the Shell's DPI changes.
66+
67+
### Event-Driven Zoom Propagation
68+
69+
DPI changes are propagated through the widget hierarchy via the standard SWT event system (`SWT.ZoomChanged`) to make it available for consumers as well.
70+
71+
### Consistent Resource Scaling
72+
73+
Images, cursors, and geometric resources maintain multiple OS handles internally for different zooms.
74+
Fonts are managed in a similar way via the `SWTFontProvider`.
75+
76+
---
77+
78+
## Architecture Overview
79+
80+
### Per-Shell Zoom State
81+
82+
The *static* HiDPI support for SWT operated with a single system-wide zoom factor, that was accessible via `DPIUtil.getNativeDeviceZoom()` for the native zoom and `DPIUtil.getDeviceZoom()` for the autoscaled zoom.
83+
While both methods can be used as fallback in certain cases, it is usually not recommended to use those utility function, as they might to fit the zoom of the current SWT `Shell`.
84+
85+
The *dynamic* HiDPI support introduced a per-Shell `nativeZoom`, which:
86+
87+
- is initialized when the Shell is created (from the current monitor),
88+
- is inherited by all child widgets,
89+
- updates when a DPI change event is received from the OS.
90+
91+
Each widget stores its own copy of the zoom to avoid repeated Shell lookups.
92+
93+
#### Widget Initialization
94+
95+
The `nativeZoom` field is defined in the base `Widget` class as a public (but non-API) field:
96+
97+
```java
98+
public int nativeZoom;
99+
```
100+
101+
It should only be accessed from win32 specific components of SWT itself.
102+
103+
**Standard Widgets** (non-Shell widgets):
104+
105+
During widget construction all necessary configuration is done:
106+
107+
```java
108+
public Widget(Widget parent, int style) {
109+
this.nativeZoom = parent != null ? parent.nativeZoom : DPIUtil.getNativeDeviceZoom();
110+
// ...
111+
registerDPIChangeListener();
112+
}
113+
```
114+
115+
- If a parent exists, the widget inherits the parent's `nativeZoom`.
116+
- For top-level widgets (no parent), `nativeZoom` is initialized from the current device zoom via `DPIUtil.getNativeDeviceZoom()`.
117+
This is considered a fallback and should never happen in practice, as top-level widgets are `Shells` and are covered in the next section.
118+
- All widgets automatically register a DPI change listener during construction.
119+
This listener will update the state of each `Widget` to fit the new zoom when a DPI change occurs.
120+
121+
**Shell Initialization**:
122+
123+
Shell widgets as top level widgets use a different initialization sequence:
124+
125+
```java
126+
Shell(Display display, Shell parent, int style, long handle, boolean embedded) {
127+
super(); // Calls parameterless Widget constructor
128+
// ...
129+
initialization code ...
130+
createWidget(); // Creates the native window handle
131+
this.nativeZoom = DPIUtil.mapDPIToZoom(OS.GetDpiForWindow(this.handle));
132+
registerDPIChangeListener(); // Explicitly called since Widget(parent, style) wasn't called
133+
}
134+
```
135+
136+
Key differences for Shell:
137+
138+
- Shell calls `super()` (parameterless constructor) instead of `super(parent, style)`.
139+
- `createWidget()` must be called first to create the native window handle (`this.handle`).
140+
- `nativeZoom` is initialized **after** `createWidget()` using `DPIUtil.mapDPIToZoom(OS.GetDpiForWindow(this.handle))` to convert the DPI value from the window handle to a zoom percentage.
141+
This approach ensures independence from internal SWT configuration and uses the actual native zoom retrieved directly from the OS for the monitor where the window is created.
142+
- `registerDPIChangeListener()` is explicitly called because the Widget constructor that normally registers it wasn't invoked.
143+
144+
This ensures that Shell's `nativeZoom` reflects the actual DPI of the monitor where the window is created, which is essential for Per-Monitor DPI Awareness.
145+
146+
### Event-Driven DPI Propagation (`SWT.ZoomChanged`)
147+
148+
SWT propagates zoom changes through the standard event-based model:
149+
150+
1. Windows emits a `WM_DPICHANGED` message.
151+
2. The Shell computes:
152+
- `newZoom` (percentage),
153+
- `scalingFactor = newZoom / oldZoom`.
154+
3. SWT sends a `SWT.ZoomChanged` event to the affected Shell.
155+
4. The event is propagated through the widget tree according to layout and dispatching rules.
156+
5. Each widget updates its internal DPI-dependent state via its registered callback.
157+
158+
The `scalingFactor` is only used internally and passed to the event handling methods `handleDPIChange(Event event, float scalingFactor)` in each widget.
159+
It represents the relative change between the old and new zoom and is used to rescale pixel values (for example the column width of a `Table`) that cannot simply be recomputed from the absolute zoom alone.
160+
161+
More details about the event propagation in [DPI Change Propagation](#dpi-change-propagation).
162+
163+
### Class-Level Callback Registration
164+
165+
Every `Widget` instance with DPI-dependent state registers a **private** listener for the `SWT.ZoomChanged` event.
166+
All widgets automatically register this listener during construction via `registerDPIChangeListener()`.
167+
168+
This listener:
169+
- Only registers if monitor specific scaling is enabled (`display.isRescalingAtRuntime()`).
170+
- Calls `handleDPIChange()` with the event and scaling factor.
171+
172+
#### Widget-Specific Handlers
173+
174+
Widget provides the package protected DPI change callback method `handleDPIChange()` that is used internally to handle DPI-dependent state.
175+
Subclasses can override it when necessary to update their internal state when DPI changes occur, but must ensure the super call is added as the first item:
176+
177+
```java
178+
@Override
179+
void handleDPIChange(Event event, float scalingFactor) {
180+
super.handleDPIChange(event, scalingFactor);
181+
// Widget-specific DPI change handling
182+
}
183+
```
184+
185+
It is intended for internal use only and its purpose is to reduce the amount of listeners per `Widget` instance.
186+
For external `Widgets` usually you do not need to adapt your state on DPI change, if it is necessary the intended way is to register a separate listener for the `SWT.ZoomChanged` event.
187+
Each class contributes only the logic relevant to the state it introduces.
188+
Because listeners are registered per class, inheritance naturally results in hierarchical, layered updates.
189+
190+
### Multi-Variant Resource Model
191+
192+
DPI-dependent resources (`Font`, `Image`, `Cursor`, `TextLayout`, and geometric primitives such as `Path`, `Pattern`, `Region`, `Transform`) follow a consistent internal model:
193+
194+
- Record the zoom at which the resource was created.
195+
- Maintain a mapping of `zoom → OS handle`.
196+
- Create new variants lazily when needed for rendering at another zoom.
197+
198+
`Font` does not apply this pattern internally within the class, but instead uses the `SWTFontProvider` to get matching `Font` instances for a required zoom.
199+
Access to an OS handle is usually provided via a public static non-API method like `Cursor#win32_getHandle(Cursor, zoom)`.
200+
All resources have similar methods.
201+
202+
---
203+
204+
## DPI Change Propagation
205+
206+
### Synchronous vs.
207+
Asynchronous Execution
208+
209+
There are two different (internal) propagation modes for `SWT.ZoomChanged`: *synchronous* and *asynchronous*.
210+
The purpose of the *asynchronous* mode is to improve user experience when dragging Shells between monitors by keeping the UI responsive while the DPI change event is still processed for the controls.
211+
212+
For the asynchronous execution, the processing is split up into multiple chunks and passed separately to the display via:
213+
214+
```java
215+
display.asyncExec(() -> widget.notifyListeners(SWT.ZoomChanged, event));
216+
```
217+
218+
This split-up is only done:
219+
- When a `Shell` receives `WM_DPICHANGED` (to keep the UI responsive while dragging windows between monitors)
220+
- When a `Composite` *has* a layout manager and delegates changes to its children (to ensure layout computation happens only after all zoom updates are complete)
221+
222+
The *synchronous* processing directly notifies the registered listeners about the DPI change:
223+
```java
224+
widget.notifyListeners(SWT.ZoomChanged, event);
225+
```
226+
227+
It is used when:
228+
229+
- a widget's parent is changed via `Control#setParent` (immediate consistency is required)
230+
- a `Composite` has *no* layout manager and delegates changes to its children (geometry must remain stable during recalculation)
231+
- immediate consistency is required (layout recalculation, cached geometry updates)
232+
233+
234+
### Delegation Rules for Zoom Propagation
235+
236+
#### General Flow
237+
238+
When a widget receives `SWT.ZoomChanged`:
239+
240+
1. It updates its DPI-dependent state (fonts, images, geometry, caches).
241+
2. It propagates the event to its children based on `Composite` rules.
242+
3. It propagates the event to its internal children, like `TableColumn` or `TableItem` for `Table`.
243+
244+
#### Composite Delegation Rules
245+
246+
**Composite with Layout Manager → Asynchronous**
247+
248+
- The event is always dispatched to children via `asyncExec`.
249+
- Ensures layout computation happens only after all zoom updates are complete.
250+
251+
**Composite without Layout Manager → Synchronous**
252+
253+
- Children are updated immediately.
254+
- Required for containers where geometry must remain stable during recalculation.
255+
256+
#### Non-Composite Widgets
257+
258+
- Update only their own state.
259+
- Do not propagate the event further except to internal children.
260+
261+
---
262+
263+
## Resource Management
264+
265+
### Fonts
266+
267+
For the internal font handling in SWT, `Font` (OS) handles should be accessed via the `SWTFontProvider`, as it manages Fonts for different zooms:
268+
269+
- Each font resource stores the zoom at which it was created, to be able to check whether it matches the requested zoom
270+
- Each font resource stores the height in points it was created with as this cannot be recalculated later due to rounding when creating the OS font handle with a height in pixels.
271+
- New font handles are automatically created for new zooms if needed by the `SWTFontProvider`.
272+
- Font size in points is consistent for all zooms, but differs in font size in pixels, e.g. a font with 10pt might be 13px if the OS handle is created for 100%, but 17px on 125%.
273+
274+
### Images
275+
276+
Images adhere to the following concept:
277+
278+
- Each image manages multiple OS handles, one for each zoom it is used on.
279+
- Lazy creation: new OS handles are only created when requested.
280+
- The complexity of the multiple ways to create an Image (e.g. via `ImageFileNameProvider`, `ImageDataProvider`, etc.) is managed inside `Image` and should not affect the usage of `Image`.
281+
282+
### Cursors & Geometric Resources
283+
284+
Cursors and Resources such as `Path`, `Pattern`, `Region`, and `Transform` follow a similar pattern:
285+
- Different OS handles for different zooms.
286+
- Lazy creation: new OS handles are only created when requested.
287+
- Automatic selection of the appropriate OS handle based on the requested zoom.
288+
289+
### TextLayout
290+
291+
`TextLayout` resources behave differently because of their usage.
292+
A `TextLayout` and its internal (font and therefore zoom-dependent) state is always calculated for exactly one zoom.
293+
If it is used in the context of a different zoom, its state will be recreated.
294+
The reasoning is that a `TextLayout` and its usage is usually limited to one zoom and not as shared as, e.g., an `Image`, and this adaptation would require a lot of effort without the necessity to do it.
295+
296+
### Automatic resource cleanup
297+
298+
Unused OS resource handles are automatically destroyed to free memory if they are no longer needed.
299+
This cleanup is triggered after each DPI change and disposes handles that were created for a zoom that is no longer used on any of the existing monitors.
300+
301+
---
302+
303+
## Configuration of Autoscaling behavior
304+
305+
### Autoscale system properties
306+
307+
#### Autoscale Modes
308+
309+
The `swt.autoScale` system property controls how native DPI is converted to effective zoom:
310+
311+
- **`false`**: Always returns 100% zoom
312+
- **`integer`**: Rounds to integer multiples of 100% (e.g., 150% → 100%, 175% → 200%) **(default)**
313+
- **`half`**: Rounds to multiples of 50% while keeping the behavior close to integer mode (e.g., 125% → 100%, 150% → 150%, 175% → 200%)
314+
- **`quarter`**: Rounds to multiples of 25% (e.g., 125%, 150%, 175%)
315+
- **`exact`**: Uses native zoom with 1% precision
316+
- **`<value>`**: Fixed zoom percentage (e.g., `"150"` = 150%)
317+
318+
**Example usage:** To set a specific mode, set the system property to one of the above, e.g. to enable *quarter* autoscale mode use: *-Dswt.autoScale=quarter*
319+
320+
#### Dynamic HiDPI support
321+
322+
The `swt.autoScale.updateOnRuntime` system property controls dynamic DPI changes:
323+
324+
- **`true`**: Application rescales when DPI changes (e.g., window moved to different monitor)
325+
- **`false`**: Application remains at initial scaling **(default)**
326+
- **`force`**: Forces runtime rescaling even with incompatible autoscale modes
327+
328+
**Example usage:** To configure the dynamic HiDPI support, set the system property to one of the above, e.g. to enable it use: *-Dswt.autoScale.updateOnRuntime=true*
329+
330+
**Note**: Runtime rescaling requires compatible autoScale modes (`quarter`, `exact`).
331+
Modes like `integer` or fixed values are incompatible with monitor-specific scaling and will only be accepted when `force` is used.
332+
333+
---
334+
335+
## Additional Information and Challenges
336+
337+
### Event Ordering
338+
339+
Because SWT uses standard event semantics:
340+
341+
- listener registration order,
342+
- synchronous vs asynchronous dispatch,
343+
- depth in the widget tree
344+
345+
all influence the order of callbacks.
346+
This ordering is stable and consistent with the rest of SWT's event model.
347+
348+
### Performance Considerations
349+
350+
- New image, cursor, and font handles must be generated for each zoom.
351+
- Asynchronous propagation prevents deep re-entrant layouts.
352+
- Temporary mixed-zoom states may occur until async updates complete.
353+
354+
### Multi-Monitor Scenarios
355+
356+
In multi-monitor environments:
357+
358+
- Each Shell has its own zoom based on the monitor it is located on.
359+
- When a window is moved between monitors with different DPI settings, `WM_DPICHANGED` is sent.
360+
- Widgets must dynamically adapt to new zooms.
361+
362+
Existing usages or extensions of SWT could (implicitly) rely on a static zoom for all monitors.
363+
364+
### Fractional Zooms
365+
366+
As GDI/GDI+ works with integer pixels, but SWT uses a point coordinate system, fractional zooms (e.g., 125%, 150%) can lead to rounding issues and scaling can lead to slight inaccuracies.
367+
Especially on zooms like 125% and 175%, rendering and layout changes must be carefully tested.
368+
369+
### Process & Thread DPI Awareness
370+
371+
Windows differentiates between process and thread DPI awareness.
372+
The process DPI awareness is defined by the executable, e.g. eclipse.exe will start with DPI awareness *System*, all recent javaw.exe will start with DPI awareness *PerMonitorV2*.
373+
SWT will set the thread DPI awareness to *PerMonitorV2* **if and ony if** monitor specific scaling is activated, either via `Display#setMonitorSpecificScaling` or the `swt.autoScale.updateOnRuntime` system property.
374+
If:
375+
376+
- process and thread DPI awareness are the same, the application should always behave as expected
377+
- process DPI awareness is *System* and thread DPI awareness is *PerMonitorV2*: This combination should behave as expected for the *PerMonitorV2* mode.
378+
Still, there could be scenarios, where the application still behaves like *System* DPI aware for a short amount of time.
379+
- process DPI awareness is *PerMonitorV2* and thread DPI awareness is *System*: This combination is not recommended and could lead to unexpected behavior
380+
381+
#### Enforce Process DPI Awareness
382+
If you want to enforce a specific process DPI Awareness, you can follow the guide in the [FAQ](https://eclipse.dev/eclipse/swt/faq.html#winexternalmanifestfile).
383+
This might be useful to enforce *System* process DPI awareness for the javaw.exe or *PerMonitorV2* process DPI awareness for the eclipse.exe

0 commit comments

Comments
 (0)