TAdvToolBar: docked toolbar caption height uses a frozen unit-global DPI scale (Per-Monitor V2)

TAdvToolBar: docked toolbar caption height uses a frozen unit-global DPI scale (Per-Monitor V2)

Environment

  • TMS VCL UI Pack 13.6.5.1
  • RAD Studio / Delphi 13.1, VCL, Win32 and Win64
  • Windows 11 (build 26200)
  • Application manifest: Per-Monitor V2 (<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>)
  • Two monitors: primary 1280x800 at 125% (120 dpi), secondary 1920x1080 at 100% (96 dpi)

Line numbers below refer to AdvToolBar.pas of 13.6.5.1. The surrounding code is unmodified.

Summary

The caption band of a docked TAdvToolBar (the coloured strip that shows the toolbar caption above the buttons) does not follow a DPI change. Its height is computed from ADVToolBarDPI_Scale, a unit-global variable that is set only once, from the DPI of the monitor where the first window was created, and is never refreshed for toolbars hosted in a TAdvDockPanel.

The height of the toolbar itself, on the other hand, is computed from the current monitor. So the two values disagree with each other inside the same component.

Steps to reproduce

  1. Two monitors with different scaling (we use 125% and 100%).
  2. A form with a TAdvDockPanel and a docked TAdvToolBar with ShowCaption = True and CaptionHeight = 27 (designed at 96 dpi).
  3. Start the application on the 125% monitor.
  4. Drag the window to the 100% monitor.

Expected

The caption band scales down together with the toolbar, back to its design height (27 px at 96 dpi).

Actual

The caption band keeps the height computed for 120 dpi. Measured values:

Toolbar height Caption band
Expected at 96 dpi 95 px 27 px
Actual after dragging 120 dpi → 96 dpi 95 px (correct) 36 px (33% too tall)

The extra 9 px overlap the button area at the bottom of the toolbar.

Starting the application directly on the 100% monitor does not show the problem, because then the global is initialised with the right DPI.

Root cause

ADVToolBarDPI_Scale is a unit-global (interface section, around line 425):

var
  ADVToolBarDPI_Scale: single = 1.0;
  ADVToolBarDPI_ScaleSet: boolean = False;

It is initialised only when it has never been set, in TAdvDockPanel.SetBounds (~12268):

if not ADVToolBarDPI_ScaleSet then begin
  ADVToolBarDPI_FormScaled := frm.Scaled;
  if Assigned(frm.Monitor) then
    ADVToolBar_SetDPIScale(frm.Monitor.PixelsPerInch);
  ...

and TAdvCustomToolBar.ChangeScale (~23233) also guards it the same way, so it never refreshes an existing value:

if not ADVToolBarDPI_ScaleSet then
  ADVToolBarDPI_Scale := GetDPIScale(Self, Canvas);

TAdvCustomToolBar.GetCaptionRect (~13664) then uses that frozen value for every non-TAdvPage parent — which is the docked case:

if Assigned(Parent) and (Parent is TAdvPage) then
  CptHeight := CaptionHeight
else
  CptHeight := Round(ADVToolBarDPI_Scale * CaptionHeight);   // <-- frozen global

Two observations that we think make this a design issue rather than a rounding one:

  1. The component contradicts itself. TAdvCustomToolBar.UpdateSize reserves room for the same caption using Round(CaptionHeight * CalculateDPIScale(Self)) (~18109), which does read the current monitor. So the height reserved and the height painted are computed with two different factors and disagree after a DPI change.

  2. TAdvToolBarPager already does the right thing. TAdvToolBarPager.ChangeScale (~39544) recomputes the global unconditionally:

    ADVToolBarDPI_Scale := GetDPIScale(Self, Canvas);
    

    Toolbars hosted in a TAdvDockPanel never get this. The asymmetry looks unintended.

There is also a more general problem with the value being global at all: under Per-Monitor V2, two windows of the same application can be on monitors with different DPI at the same time, and a single global scale cannot be correct for both.

Suggested fix

Use the DPI of the instance instead of the global, which is what the rest of the VCL does under Per-Monitor V2. In GetCaptionRect:

// CaptionHeight comes from the DFM, i.e. expressed at 96 dpi
CptHeight := MulDiv(CaptionHeight, CurrentPPI, USER_DEFAULT_SCREEN_DPI);

We have been running this change since August 2026 and it fixes the reported symptom on both monitors, on startup and while dragging the window between them. GetCaptionRect is the single point that matters: the background, the caption text and GetMyClientRect are all derived from it.

We deliberately left UpdateSize untouched: with the band correct, the room it reserves is equal (at 96 dpi) or 2 px larger (at 120 dpi), which is not noticeable, and changing it would alter the height of every toolbar in the application at high DPI.

One caveat for the TAdvPage branch, in case you apply something similar there: for a toolbar of a TAdvToolBarPager shown in compact mode, Parent is a TCompactWindow, so it falls into the else branch even though its CaptionHeight has already been scaled by TAdvToolBarPager.ChangeScale — that combination would scale twice.

Why the usual workaround is not an option for us

The advice we found in the support center for high-DPI trouble with TAdvToolBar is to set the manifest to System Aware. That would mean giving up Per-Monitor V2 for the whole application: Windows would stretch the window bitmap and everything would look blurry on any monitor other than the one the application started on. Our application is fully Per-Monitor V2 and the rest of the VCL behaves correctly under it.

We traced & solved this issue. The next update will address this.

v 13.6.8.2 fixes both issues at runtime — confirmed here on a two-monitor setup (primary at 125%, secondary at 100%), window created on either monitor and dragged between them. Thanks for that.

But there is a side effect at design time:

the FIRST time you open a form with a TAdvDockPanel in an IDE session it looks correct; from the second time on, a dock holding several toolbars in one row comes up with the row split in two. Save the form and the IDE writes that geometry into the .dfm.

Cause:

ADVToolBarDPI_Scale is a unit-level global set once in TAdvDockPanel.SetBounds, so the first form loads with 1.0 and every later one with the monitor factor already frozen in. TAdvDockPanel.Loaded then divides Left and multiplies Top by that same factor, which cannot be right either way, and the toolbars stop sharing a row. On top of that, GetCaptionRect and
UpdateSize size the caption band with CalculateDPIScale, which measures against GetDC(0) — the IDE's screen — while the form is at 96 dpi: the band goes 27 -> 36 px and the toolbar 96 -> 105.

The patch that fixed it for us (three csDesigning guards, runtime untouched):

// TAdvDockPanel.SetBounds
if Assigned(frm) and HandleAllocated and not (csDesigning in ComponentState) then

// TAdvCustomToolBar.GetCaptionRect
if Assigned(Parent) and (Parent is TAdvPage) then
CptHeight := CaptionHeight
else if csDesigning in ComponentState then // added
CptHeight := CaptionHeight // added
else
CptHeight := Round(CaptionHeight * CalculateDPIScale(Self));

// TAdvCustomToolBar.UpdateSize
if ShowCaption then
begin
if csDesigning in ComponentState then // added
MaxCtrlS := MaxCtrlS + CaptionHeight // added
else
MaxCtrlS := MaxCtrlS + Round(CaptionHeight * CalculateDPIScale(Self));
end;

All three are needed: the first one keeps the scale at 1.0 in the designer, the other two keep the toolbar at its designed height.

I tested this here with Delphi 13.1 both with the form designer at 96dpi and also in automatic mode on a 150% DPI screen, but the suggested changes break existing designs, for example, the design in our demo Demo\AdvToolBar\AdvToolBarDemo.
I also couldn't reproduce that when opening the form multiple times in the IDE would cause it to change its design.
Do you have more details / sample project / IDE form designer DPI settings / screen DPI settings with which we can reproduce this here?