How to Write High-DPI Applications

 

Nick Kramer
Microsoft Corporation

March 2001

Summary: This article discusses writing applications that work on high-density displays, focusing on four general areas: text and fonts, images (graphics, icons, and cursors), layout, and painting. (8 printed pages)

Contents

Introduction
System Metrics
Key Issues
GDI+
Visual Basic and the Common Language Runtime
How to Test High-DPI Applications

Introduction

Does your application work on high-density displays? Probably not. The standard computer monitor today has a display of about 96 dots per inch (DPI), and most applications assume that they are running on a 96-DPI display. But that’s an increasingly dangerous assumption. Today, you can readily buy a laptop with a 133-DPI display; 170-DPI displays are just around the corner, and within the next few years, 200-DPI displays will be shipping in quantity. The industry journal DisplaySearch predicts that 40 percent of laptop sales will have greater than 100 DPI by the end of 2002. Many observers expect much faster adoption.

Applications without high-DPI capability will range from ugly to unusable, depending on the application and the DPI. Most applications that have large font support are disproportionate but usable at 130 DPI. At 200 DPI, the same application is unusable. Sometimes the application will be uniformly smaller, but more often some elements will be drawn at the correct size (say, if the application uses DEFAULT_GUI_FONT, which gets bigger as the DPI grows), while other parts shrink, as shown in Figure 1.

Figure 1. Effects of changing resolution

Upgrading your application to high-DPI displays is becoming increasingly necessary, then, but doing so brings important advantages: Images look better, and text is crisper and more legible. Text on a 200-DPI monitor is as clear as a printout from a laser printer. (Because computer displays have sharper pixels and support gray scale, a 200-DPI monitor is equivalent to a 600-DPI printer.) Makers of PDAs and eBooks are increasingly looking to high-DPI displays to better compete with paper.

Overall, writing high-DPI applications is easy: Don’t make assumptions about the DPI, and avoid things that don’t scale well (like bitmaps and bitmapped fonts). Though writing high-DPI applications can occasionally be tedious, it is rarely difficult. And if your application is handicapped-accessible, you’ve done much of the work already. (The high-contrast mode uses extra-large fonts, which is similar to high DPI in many ways.)

System Metrics

Microsoft® Windows® provides a number of system metrics that you should use to eliminate assumptions about the user’s system. GetDeviceCaps() can be used to obtain a display’s DPI (pass in LOGPIXELSX or LOGPIXELSY for the second parameter). GetSystemMetrics() and SystemParametersInfo() provide sizes for nearly every graphical element in Windows, from the width of a three-dimensional border to the size of a small icon. One of the less obvious metrics is the minimum line thickness (pass SM_CYBORDER or SM_CXBORDER to GetSystemMetrics). At very high DPIs, a one-pixel-wide line is nearly invisible.

You can define SCALEX and SCALEY macros to apply a scaling factor based on information from GetDeviceCaps:

// how much to scale a design that assumes 96-DPI pixels
double scaleX;
double scaleY;
#define SCALEX(argX) ((int) ((argX) * scaleX))
#define SCALEY(argY) ((int) ((argY) * scaleY))

void InitScaling() {
   HDC screen = GetDC(0);
   scaleX = GetDeviceCaps(screen, LOGPIXELSX) / 96.0;
   scaleY = GetDeviceCaps(screen, LOGPIXELSY) / 96.0;
   ReleaseDC(0, screen);
}

Key Issues

In high-DPI applications, you need to pay attention to four general areas: text and fonts, images (graphics, icons, and cursors), layout, and painting.

Text and Fonts

There are two kinds of fonts: bitmapped fonts and TrueType fonts. High-DPI applications should use only TrueType fonts. Bitmapped fonts are written for 96-DPI screens, and don’t scale. TrueType fonts, on the other hand, look good at any DPI. Windows has supported TrueType for a long time, so finding a good TrueType font should not be a problem. (Another reason to use only TrueType fonts: some newer technologies, such as GDI+, only support TrueType fonts.)

The default font for window handles (HWNDs) and graphics device contexts (HDCs) is a bitmapped font, so you should always change the font to something else. Despite the name, DEFAULT_GUI_FONT is not the default HWND and HDC font, but is TrueType, so consider using it:

HFONT font = (HFONT) GetStockObject(DEFAULT_GUI_FONT);
SendMessage(hwnd, WM_SETFONT, (WPARAM) font, 0);
SelectObject(hdc, font);

When you create fonts in Windows, you use pixels to specify the font size, so you should adjust for DPI:

LOGFONT lf;
memset(&lf, 0, sizeof(lf));
lf.lfHeight = SCALEY(13);
HFONT font = CreateFontIndirect(&lf);

The Windows ChooseFont dialog box allows the use of points rather than pixels to specify font size; ChooseFont takes DPI into account when converting the font size into pixels. You may want to restrict the dialog box to TrueType fonts; do this with the CF_TTONLY flag:

CHOOSEFONT data;
memset(&data, 0, sizeof(data));
data.lStructSize = sizeof(data);
data.hwndOwner = form;
data.Flags = CF_TTONLY | CF_SCREENFONTS;
ChooseFont(&data);

Often the best way to specify sizes and locations in a high-DPI application is to use font size as a measurement unit for specifying other elements on the page. You might, for instance, make the space between buttons equal to the height of the default system font. Use GetTextMetrics() to retrieve the height of a font:

TEXTMETRIC metrics;
GetTextMetrics(hdc, &metrics);
int height = metrics.tmHeight;

It is probably best not to use TEXTMETRIC.tmAveCharWidth. This value only considers English letters, and besides, who’s to say the characters you want are average? Instead, use the GetTextExtent family of functions to measure the size of the string you care about. The following example uses GetTextExtentPoint32() to draw a rectangle around a string:

SIZE size;
GetTextExtentPoint32(hdc, string, strlen(string), &size);

int paddingX = SCALEX(8);
int paddingY = SCALEX(8);
Rectangle(hdc, x - paddingX, y - paddingY, x + size.cx 
+ paddingX, y + size.cy + paddingY);
TextOut(hdc, x, y, string, strlen(string));

Finally, be aware that although TrueType fonts scale nicely, they don’t scale linearly: Increasing the DPI by 10 percent does not generally increase a string’s length by exactly 10 percent. (GDI+ doesn’t have this problem; see the GDI+ section.) This happens because any given letter only looks good at certain sizes, and TrueType picks the nearest size that looks good. This is another reason to use the GetTextExtent functions.

Images

“Images” covers all raster-based image files (such as BMP, JPEG, and GIF), icons, and cursors. Images are more difficult to deal with than fonts, since images consist of discrete pixels. If the display’s DPI is not the same as the DPI for which the image was designed, the image needs to be scaled in order to appear the correct physical size. You can scale a bitmap by calling StretchBlt() instead of BitBlt(). It’s often easier for the application to scale the image when it is loaded, rather than scaling when the image is drawn. StretchBlt can be used to do this, as well. This example takes a bitmap designed for 96 DPI, and draws it scaled:

BITMAP info;
GetObject(bitmap, sizeof(info), (PTSTR) &info);

HDC hdcBitmap = CreateCompatibleDC(target);
SelectObject(hdcBitmap, bitmap);

StretchBlt(target, x, y, 
SCALEX(info.bmWidth), SCALEY(info.bmHeight), 
hdcBitmap, 0, 0, info.bmWidth, info.bmHeight, SRCCOPY);
DeleteDC(hdcBitmap);

Scaling does degrade the quality of the image. This is most noticeable when scaling from a small DPI to a large one, but scaling down also has problems; the details the scaling algorithm chooses to keep are not necessarily the details you would choose. The default stretching mode, COLORONCOLOR, is fast but loses a lot of details. HALFTONE stretching is slower, but provides much higher quality. (GDI+ offers additional stretching options; see the GDI+ section.) Use SetStretchBltMode() to change between stretching modes:

SetStretchBltMode(hdc, HALFTONE);

An alternative to scaling is using multiple images, each designed for a different DPI. The .ico and .cur formats are capable of storing multiple images in a single file. When you load the icon or cursor, the application asks for the size that the GetSystemMetrics() suggests; the system then picks the closest image and scales if necessary. BMP and most other image file formats do not support multiple sizes in a single file, but it’s easy to create multiple files and choose the most appropriate one when you load them:

if (GetDeviceCaps(hdc, LOGPIXELSX) < 130)
   bitmap = LoadBitmap(hInstance, (char*) IDB_BITMAP1);
else
   bitmap = LoadBitmap(hInstance, (char*) IDB_BITMAP2);

For icons and cursors in particular, it is recommended that you create additional images. Today, standard practice is to have both 16x16 pixel and 32x32 pixel images for the application icon. High-DPI applications should at a minimum add 64x64 pixel images to their application icon. Other sizes are worth considering as well, ideally large and small icons for each of the major resolutions.

If you’re using image lists (HIMAGELIST) with Common Controls version 5 (comctl), you’ll need to scale the images before putting them in the image list. A better choice is to switch to version 6 of the Common Controls, which is included with the Microsoft Windows XP operating system. Version 6 automatically scales the images using a halftone StretchBlt. (This can be disabled if you have authored your images for high DPI.)

Layout

Layout is another area that can cause problems on high-DPI systems. Most dialog boxes are laid out using dialog units, which scale with the system DPI. However, custom layout logic usually needs to be revisited. Most custom layout logic does the work in pixels, and makes a lot of assumptions about how big a pixel is. You can rework the layout logic to use other units, such as dialog units (although you’ll need to convert back to pixels before calling SetWindowPos). Or you can continue to work in pixels but remove assumptions about the DPI by expressing positions relative to other controls or relative to the size of the font, or using system metrics.

Painting

Painting is similar. Any time you paint to the screen, you need to account for different DPIs. If you are writing a custom control, it is probably easiest to continue to work in pixels, but you need to use system metrics to avoid DPI assumptions. If you’re doing complicated graphics, you may want to have the graphics engine scale for you by calling SetMapMode.

If you do scaling manually (as in my sample SCALEX macro), be aware of rounding problems when using integers. For example, SCALEX(a + b) may not equal SCALEX(a) + SCALEX(b) because of rounding issues. Be consistent about how you apply scaling, or use floating point numbers for intermediate results.

GDI+

GDI+ is the next generation of the Microsoft two-dimensional graphics library, the successor to GDI. GDI+ brings a number of advantages to high-DPI applications, and text scales both linearly and smoothly (GDI+ subtly manipulates the character and word spacing).

Image scaling is also greatly improved. GDI+ offers several image scaling algorithms with differing speed versus quality trade-offs, but all of them produce higher quality images than GDI StretchBlt. InterpolationModeBilinear is the fastest, and for small images, the overall best. For large images, where quality matters, InterpolationModeHighQualityBicubic is a good choice.

Another feature of GDI+ is that images carry around the DPI they were designed for (as in Image::GetPhysicalDimension and Bitmap::SetResolution, for example). You can use this information to scale images properly, or you can let GDI+ do it. If you don’t specify a height and width when you call Graphics::DrawImage, GDI+ calculates them based on the screen’s DPI and the image’s DPI.

Visual Basic and the Common Language Runtime

In Microsoft Visual Basic® 6 and earlier, forms are specified in dialog box units, so controls and shapes generally scale correctly. Because text doesn’t scale linearly, you’ll still need to test the application, and possibly enlarge controls to fit their text.

In Microsoft .NET WinForms (used by Visual Basic .NET), everything is done in pixels. When a form is first created, however, the controls will be scaled based on the system’s DPI, and initial form layout scales well. Of course, it is up to you to make any custom painting or layout logic scalable. WindowsForms and System.Drawing both use GDI+, which improves text and image scaling.

How to Test High-DPI Applications

Testing a high-DPI application consists of putting your system in high-DPI mode and looking for things that are improperly sized. You don’t need a high-DPI monitor in order to find most problems.

To change the system DPI settings:

  1. Right-click the Windows desktop.
  2. Click Properties.
  3. Open the Settings tab and click Advanced.
  4. On the General tab, in the Font Size box, change your system DPI.
  5. Reboot the system to allow the new settings to take effect.

You’ll want to check all the visuals in your application, paying special attention to:

  • Text that doesn’t fit in the given space.
  • Text and controls overlapping or not properly spaced.
  • Text and images that are too small.
  • Images that are properly sized, but are of poor quality because of scaling.
  • Lines that are too thin to see easily. (At 200 DPI, a one-pixel-wide line is nearly invisible.)
  • Edges that don’t quite line up because of rounding problems when scaling.

You should test your application at several different DPIs. Manufacturers differ on the exact DPIs, but some good ones to test for are 96 (standard CRT), 120 (standard CRT with “large fonts” setting), 135, 170, and, soon, 200.

© Microsoft Corporation. All rights reserved.