Large Font & DPI Scaling in Delphi Apps

Delphi applications designed at 96 DPI look cramped, clipped, or oddly spaced on the high-DPI displays most users have today, and the VCL’s answer to this problem has changed across versions in ways that aren’t obvious from the documentation alone. This post expands on my earlier notes on large fonts, covering how DPI awareness actually works in the VCL, which manifest settings matter, and where automatic scaling still needs manual correction.

The problem

I touched on large-font handling in an earlier post, mostly from the angle of forcing a specific display scale for testing. This one goes further into the actual mechanism — what “DPI aware” means for a Win32 application, how the VCL implements it, and what still breaks even when you’ve done everything the framework asks for.

The short version of the underlying problem: Windows historically assumed 96 DPI as the baseline for pixel-based UI layout. Every modern display exceeds that, often significantly on laptops, so Windows offers a scaling factor — 125%, 150%, 200% — and expects applications to either declare themselves DPI-aware and handle the scaling internally, or be scaled by Windows itself via bitmap stretching. The second option is why unaware old applications look blurry on high-DPI screens: Windows is literally stretching a bitmap of the rendered window.

Declaring DPI awareness

The first decision is which DPI-awareness mode to declare, and this has to happen in the application manifest, not in code, because Windows reads it before your application’s first line runs. Delphi’s IDE-generated manifest for the project controls this. For a modern VCL application targeting current Delphi versions, PerMonitorV2 is the mode to declare — it lets the application respond to DPI changes when moved between monitors with different scaling, rather than only picking up the DPI once at startup.

<!-- In the project's .manifest file, inside the application element -->
<asmv3:application>
  <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
  </asmv3:windowsSettings>
</asmv3:application>

Both dpiAware and dpiAwareness are present deliberately — dpiAwareness is the modern per-monitor-v2 declaration, and dpiAware is the fallback for Windows versions or configurations where the newer element isn’t recognized. Older Delphi versions without built-in per-monitor-v2 support in the VCL runtime will parse this manifest correctly at the OS level, but the VCL itself won’t respond to WM_DPICHANGED messages usefully — check your Delphi version’s release notes for when per-monitor-v2 VCL support actually landed before relying on it, since declaring the manifest without VCL support underneath just gets you an application that’s aware of the DPI change but doesn’t do anything useful with it.

What the VCL scales for you

Once DPI awareness is declared and the VCL version supports it, form and control positions, sizes, and fonts scale automatically when WM_DPICHANGED fires, using the TForm.ScaleForCurrentDpi machinery. This covers the common cases — anchored and aligned controls resize correctly, and fonts scale in proportion. What it does not reliably cover is anything drawn manually with fixed pixel coordinates.

// Breaks under DPI scaling — 20 and 40 are meaningless without
// knowing what DPI they were chosen for
procedure TMyForm.PaintBox1Paint(Sender: TObject);
begin
  PaintBox1.Canvas.TextOut(20, 40, 'Status: OK');
end;

Custom-drawn content needs to scale its own coordinates against the control’s current DPI explicitly:

// Scales the fixed design-time coordinates against the form's
// actual DPI at paint time
procedure TMyForm.PaintBox1Paint(Sender: TObject);
const
  DesignDpi = 96;
var
  X, Y: Integer;
begin
  X := MulDiv(20, FCurrentPPI, DesignDpi);
  Y := MulDiv(40, FCurrentPPI, DesignDpi);
  PaintBox1.Canvas.TextOut(X, Y, 'Status: OK');
end;

FCurrentPPI is a TForm field the VCL maintains for you once DPI awareness is active — it reflects the monitor the form is currently on, which is why per-monitor-v2 awareness matters even for a single-monitor user: laptops docked to an external display change effective DPI when the window moves, and FCurrentPPI is what tells you the new value.

Bitmaps and icons need their own scaling

Font and control scaling is handled by the VCL, but bitmaps you’ve loaded from resources at a fixed size are not automatically resampled — an icon drawn at 16×16 for 96 DPI will either stay tiny or get blurrily stretched at 200%, depending on how you’re drawing it. The correct fix is to ship multiple resolutions and pick the closest match, the way TImageList supports with its DPI-aware image list variants introduced in later Delphi versions:

// Load the image list variant closest to the current DPI rather
// than stretching a single fixed-size bitmap
function PickImageList(PPI: Integer; const Lists: array of TImageList;
  const NativePPIs: array of Integer): TImageList;
var
  I, BestIdx: Integer;
  BestDiff, Diff: Integer;
begin
  BestIdx := 0;
  BestDiff := MaxInt;
  for I := 0 to High(NativePPIs) do
  begin
    Diff := Abs(NativePPIs[I] - PPI);
    if Diff < BestDiff then
    begin
      BestDiff := Diff;
      BestIdx := I;
    end;
  end;
  Result := Lists[BestIdx];
end;

If maintaining multiple bitmap resolutions isn’t practical for a smaller project, vector-based icon fonts or SVG rendered at the target size avoid the problem entirely, at the cost of extra rendering work at runtime — worth it for applications that need to look sharp across a wide range of displays, probably not worth it for an internal tool used on one known machine.

Testing without owning multiple monitors

You don’t need a high-DPI monitor to test this. Windows lets you override the scale factor per-application from the display settings, and more usefully for iterative testing, you can temporarily force a scale factor from within your own application for a quick visual check during development, which is the approach I described in the earlier large-fonts post:

// Development-only override — forces a scale check without
// touching actual display settings
{$IFDEF DEBUG}
procedure ForceTestDpi(Form: TForm; TestPPI: Integer);
begin
  Form.ScaleForPPI(TestPPI);
end;
{$ENDIF}

Wire this to a debug menu item or a command-line switch during development, and you can catch layout problems at 150% and 200% without needing the hardware.

Conclusion

DPI scaling in Delphi is mostly handled once you declare PerMonitorV2 correctly and let the VCL do its job on controls and fonts, but anything you draw or load manually — fixed pixel coordinates, single-resolution bitmaps — is left entirely to you, and skipping that part is where most “we support high DPI” claims quietly fall apart in practice.

Leave a Reply

Your email address will not be published. Required fields are marked *