How to control the font DPI in .NET WinForms app

Assuming you do not try to honor the user’s UI font choice (SystemFonts.IconTitleFont), and hard-code your forms for one font size only (e.g. Tahoma 8pt, Microsoft Sans Serif 8.25pt), you can set your form’s AutoScaleMode to ScaleMode.Dpi.

This will scale the size of the form and most of it child controls by the factor CurrentDpiSetting / 96 by calling Form.Scale(), which in turns calls the protected ScaleControl() method recursivly on itself and all child controls. ScaleControl will increase a control’s position, size, font, etc as needed for the new scaling factor.

Warning: Not all controls properly scale themselves. The columns of a
listview, for example, will not get
wider as the font gets larger. In
order to handle that you’ll have to
manually perform additional scaling as
required. i do this by overriding the
protected ScaleControl() method, and
scaling the listview columns manually:

public class MyForm : Form
{
   protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
   {
      base.ScaleControl(factor, specified);
      Toolkit.ScaleListViewColumns(listView1, factor);
   }
}

public class Toolkit  
{
   /// <summary>
   /// Scale the columns of a listview by the Width scale factor specified in factor
   /// </summary>
   /// <param name="listview"></param>
   /// <param name="factor"></param>
   /// <example>/*
   /// protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
   /// {
   ///    base.ScaleControl(factor, specified);
   ///    
   ///    //ListView columns are not automatically scaled with the ListView, so we
   ///    //must do it manually
   ///    Toolkit.ScaleListViewColumns(lvPermissions, factor);
   /// }
   ///</example>
   public static void ScaleListViewColumns(ListView listview, SizeF factor)
   {
      foreach (ColumnHeader column in listview.Columns)
      {
          column.Width = (int)Math.Round(column.Width * factor.Width);
      }
   }
}

This is all well and good if you’re just using controls. But if you ever use any hard-coded pixel sizes, you’ll need to scale your pixel widths and lengths by the current scale factor of the form. Some examples of situations that could have hard-coded pixel sizes:

  • drawing a 25px high rectangle
  • drawing an image at location (11,56) on the form
  • stretch drawing an icon to 48×48
  • drawing text using Microsoft Sans Serif 8.25pt
  • getting the 32×32 format of an icon and stuffing it into a PictureBox

If this is the case, you’ll need to scale those hard-coded values by the “current scaling factor“. Unfortunatly the “current” scale factor is not provided, we need to record it ourselves. The solution is to assume that initially the scaling factor is 1.0 and each time ScaleControl() is called, modify the running scale factor by the new factor.

public class MyForm : Form
{
   private SizeF currentScaleFactor = new SizeF(1f, 1f);

   protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
   {
      base.ScaleControl(factor, specified);

      //Record the running scale factor used
      this.currentScaleFactor = new SizeF(
         this.currentScaleFactor.Width * factor.Width,
         this.currentScaleFactor.Height * factor.Height);

      Toolkit.ScaleListViewColumns(listView1, factor);
   }
}

Initially the scaling factor is 1.0. If form is then scaled by 1.25, the scaling factor then becomes:

1.00 * 1.25 = 1.25    //scaling current factor by 125%

If the form is then scaled by 0.95, the new scaling factor becomes

1.25 * 0.95 = 1.1875  //scaling current factor by 95%

The reason a SizeF is used (rather than a single floating point value) is that scaling amounts can be different in the x and y directions. If a form is set to ScaleMode.Font, the form is scaled to the new font size. Fonts can have different aspect ratios (e.g. Segoe UI is taller font than Tahoma). This means you have to scale x and y values independantly.

So if you wanted to place a control at location (11,56), you would have to change your positioning code from:

Point pt = new Point(11, 56);
control1.Location = pt;

to

Point pt = new Point(
      (int)Math.Round(11.0*this.scaleFactor.Width),
      (int)Math.Round(56.0*this.scaleFactor.Height));
control1.Location = pt;

The same applies if you were going to pick a font size:

Font f = new Font("Segoe UI", 8, GraphicsUnit.Point);

would have to become:

Font f = new Font("Segoe UI", 8.0*this.scaleFactor.Width, GraphicsUnit.Point);

And extracting a 32×32 icon to a bitmap would change from:

Image i = new Icon(someIcon, new Size(32, 32)).ToBitmap();

to

Image i = new Icon(someIcon, new Size(
     (int)Math.Round(32.0*this.scaleFactor.Width), 
     (int)Math.Round(32.0*this.scaleFactor.Height))).ToBitmap();

etc.

Supporting non-standard DPI displays is a tax that all developers should pay. But the fact that nobody wants to is why Microsoft gave up and added to Vista the ability for the graphics card to stretch any applications that don’t say they properly handle high-dpi.

Leave a Comment