Version: 4.2.2 (also present in current master), Delphi 13, VCL application with Per-Monitor v2 DPI awareness.
Symptom
A TStyledToolbar (Align = alTop, ButtonWidth = 91, ButtonHeight = 25, ShowCaptions = True) with a few TStyledToolButtons. Run the form on a 175% (or 150%) monitor and drag it to a 100% monitor and back - or change the display scale in Windows Settings while it runs.
- After the first DPI change the buttons still look right, but
ButtonWidth / ButtonHeight now read 51 x 14 instead of the expected 91 x 25 (the ratio applied twice: 159 x 0.571^2).
- After the second change (back to 175%) they read 487 x 135 and the buttons are drawn 279 x 77 inside a 44 px bar. Going the other way the captions are clipped to a single letter on a squashed row.
- Any later
ResizeButtons (a SetBounds, an AutoSize toggle, the next DPI change) applies the wrong values to every button.
Measured with a log of ButtonWidth/ButtonHeight and each button's bounds around OnAfterMonitorDpiChanged:
| step |
ButtonWidth x ButtonHeight |
actual buttons |
| 175% steady state |
159 x 44 |
159 x 44 (correct) |
| after 175% -> 100% |
51 x 14 |
91 x 25 (looks right, poisoned) |
| after 100% -> 175% |
487 x 135 |
279 x 77 |
Cause
FRescaling is raised only inside TStyledToolbar.ChangeScale. On a Per-Monitor DPI change (isDpiChange = True) the VCL scales the children first: TWinControl.ScaleForPPI runs ScaleControlsForDpi (each TStyledToolButton.ScaleForPPI -> ChangeScale -> SetBounds) and only then calls inherited ScaleForPPI, which reaches the toolbar's own ChangeScale.
So every button scales while FRescaling is still False; TStyledToolButton.SetBounds therefore stores its already-scaled width/height into FToolBar.FButtonWidth / FButtonHeight (and calls ResizeButtons), and afterwards TStyledToolbar.ChangeScale(M, D, isDpiChange) does FButtonWidth := MulDiv(FButtonWidth, M, D) on that value a second time.
The guard works on the legacy ChangeScale(M, D) / ScaleBy path because there TWinControl.ChangeScale scales the children inside the parent's ChangeScale (if not isDpiChange then ScaleControls(M, D)), i.e. while the guard is up. The Per-Monitor path reverses the order.
Suggested fix
Raise the guard around the whole DPI pass by overriding ScaleForPPI in TStyledToolbar:
{$IFDEF D10_1+}
procedure TStyledToolbar.ScaleForPPI(NewPPI: Integer);
begin
// Per-Monitor DPI: the VCL scales the child buttons BEFORE calling our
// ChangeScale, so FRescaling must already be up while they scale, or
// TStyledToolButton.SetBounds writes their scaled size back into
// FButtonWidth/FButtonHeight and ChangeScale then scales it again.
FRescaling := True;
try
inherited;
finally
FRescaling := False;
end;
end;
{$ENDIF}
(override next to the existing ChangeScale overrides; both ChangeScale bodies can stay as they are - with the guard up, FButtonWidth/FButtonHeight get their single MulDiv and nothing else touches them.)
We are working around it from the outside with an interposer that, after every ScaleForPPI, resets ButtonWidth/ButtonHeight to the design values x the control's ScaleFactor and calls ResizeButtons; with that in place the buttons are 159 x 44 at 175% and 91 x 25 at 100% on every crossing, repeatedly. CurrentPPI is not usable as a "before" reference inside the override, by the way - the form has already stamped the new PPI onto its children when the toolbar's ScaleForPPI runs.
Thanks for the library - happy to test a fix.
Version: 4.2.2 (also present in current
master), Delphi 13, VCL application with Per-Monitor v2 DPI awareness.Symptom
A
TStyledToolbar(Align = alTop, ButtonWidth = 91, ButtonHeight = 25, ShowCaptions = True) with a fewTStyledToolButtons. Run the form on a 175% (or 150%) monitor and drag it to a 100% monitor and back - or change the display scale in Windows Settings while it runs.ButtonWidth/ButtonHeightnow read 51 x 14 instead of the expected 91 x 25 (the ratio applied twice: 159 x 0.571^2).ResizeButtons(aSetBounds, anAutoSizetoggle, the next DPI change) applies the wrong values to every button.Measured with a log of
ButtonWidth/ButtonHeightand each button's bounds aroundOnAfterMonitorDpiChanged:Cause
FRescalingis raised only insideTStyledToolbar.ChangeScale. On a Per-Monitor DPI change (isDpiChange = True) the VCL scales the children first:TWinControl.ScaleForPPIrunsScaleControlsForDpi(eachTStyledToolButton.ScaleForPPI->ChangeScale->SetBounds) and only then callsinherited ScaleForPPI, which reaches the toolbar's ownChangeScale.So every button scales while
FRescalingis still False;TStyledToolButton.SetBoundstherefore stores its already-scaled width/height intoFToolBar.FButtonWidth/FButtonHeight(and callsResizeButtons), and afterwardsTStyledToolbar.ChangeScale(M, D, isDpiChange)doesFButtonWidth := MulDiv(FButtonWidth, M, D)on that value a second time.The guard works on the legacy
ChangeScale(M, D)/ScaleBypath because thereTWinControl.ChangeScalescales the children inside the parent'sChangeScale(if not isDpiChange then ScaleControls(M, D)), i.e. while the guard is up. The Per-Monitor path reverses the order.Suggested fix
Raise the guard around the whole DPI pass by overriding
ScaleForPPIinTStyledToolbar:(
overridenext to the existingChangeScaleoverrides; bothChangeScalebodies can stay as they are - with the guard up,FButtonWidth/FButtonHeightget their singleMulDivand nothing else touches them.)We are working around it from the outside with an interposer that, after every
ScaleForPPI, resetsButtonWidth/ButtonHeightto the design values x the control'sScaleFactorand callsResizeButtons; with that in place the buttons are 159 x 44 at 175% and 91 x 25 at 100% on every crossing, repeatedly.CurrentPPIis not usable as a "before" reference inside the override, by the way - the form has already stamped the new PPI onto its children when the toolbar'sScaleForPPIruns.Thanks for the library - happy to test a fix.