Developing DPI-Aware Applications

 

Microsoft

March 2004

Applies to:
   Windows Mobile™ 2003 Second Edition software
   Microsoft® eMbedded Visual C++® version 4.0

Summary: Windows Mobile 2003 Second Edition-based devices can run at higher resolutions than earlier devices. Learn about the changes your applications will need to determine the DPI of the device and take advantage of high-DPI displays. (9 printed pages)

Download Crossword.msi from the Microsoft Download Center.

Contents

Introduction
Key Issues
Legacy Support
GAPI Legacy Support
Conclusion

Introduction

As display technology improves, it is becoming possible to fit more pixels into the same area. This additional resolution can be used to fit more items on the screen, or it can be used to draw the same items more sharply. In the latter case, the display is said to have increased in dots per inch (DPI), a logical value representing how densely packed with pixels a display area is.

Print media has taken advantage of high-DPI technology for a long time. A document printed on a 1200-dpi printer prints the same amount of text as a 300-dpi printer, but the text is much sharper.

Windows Mobile™-based devices have traditionally used 96-dpi displays, but Windows Mobile 2003 Second Edition-based devices can run at higher resolutions. Upgrading your application to support DPI-awareness will not only ensure correct appearance on high-DPI devices but will also prepare your application for these high-DPI advantages:

  • Sharper text: The most noticeable improvement that comes with almost no cost. Every application that is DPI-aware and uses TrueType fonts inherits this improvement.
  • More detailed graphics: If steps are taken to provide high-resolution bitmaps, applications can use the increased resolution to display more detailed icons and graphics.

This document describes the changes you will need to make to your applications so that they can determine the DPI of the device and take advantage of high-DPI displays.

Key Issues

In DPI-aware applications for Pocket PCs and Smartphones, it's important to focus on the following three general areas:

  • Layout
  • Text and fonts
  • Images

Layout

UI elements whose positions and sizes are specified in pixel coordinates that assume 96-DPI will be incorrect on high-DPI devices. In general, all UI elements should be laid out using scaled positions and sizes, or relative to controls, fonts, or system metrics.

The GetDeviceCaps Windows Mobile function can be used to obtain a display's DPI by passing in either LOGPIXELSX or LOGPIXELSY as the second parameter. The CrosswordSample sample demonstrates how you can define SCALEX and SCALEY macros to apply a scaling factor based on information from GetDeviceCaps.

You can continue to work in pixels but remove assumptions about the DPI by using the following techniques:

  • Using the SCALEX and SCALEY macros to scale 96-DPI coordinates, or using the metrics returned by GetSystemMetrics when applicable.
  • Expressing sizes or positions relative to other controls.
  • Expressing sizes or positions relative to a font.

Dialog boxes already use font sizes to determine their layout, so typically they need no special modification to work on high-DPI devices.

Following is an example of positioning a window with DPI-awareness, where x, y, dx, and dy is pixel coordinates in 96-DPI:

SetWindowPos(hwnd, NULL, SCALEX(x), SCALEY(y), SCALEX(dx), SCALEY(dy), SWP_NOZORDER);

If you choose to scale 96-DPI pixel metrics, be aware of rounding problems when using integers. For example, SCALEX(a + b) may not equal (SCALEX(a) + SCALEX(b)) because of rounding issues.

System Metrics

To be DPI-aware, an application can make no assumptions about the pixel sizes of various screen elements, such as icon sizes or border widths. Windows Mobile-based devices provide a number of system metrics to provide information about the user's system. For example, the following information can be queried via the GetSystemMetrics Windows CE function.

  • Screen size dimensions should be obtained by using GetSystemMetrics(SM_CXSCREEN) or GetSystemMetrics(SM_CXSCREEN).
  • Border dimensions should be obtained by using GetSystemMetrics(SM_CXBORDER) or GetSystemMetrics(SM_CYBORDER).
  • Large and small icon sizes should be obtained by using GetSystemMetrics(SM_CXICON) or GetSystemMetrics(SM_CXSMICON).

Drawing with DPI-Aware Thick Pens

At higher DPI values, line thickness has to be increased to maintain proportionate visual size. To be DPI-aware, your application should scale the width of any pens with which it draws.

The behavior of the Windows Mobile Graphics Device Interface (GDI) with respect to drawing thick (greater than 1 pixel) lines makes precise and correct positioning tricky. For calls to the Windows Mobile functions LineTo and Polyline, the GDI takes the specified origin point as the center of the pen's width. For lines of odd thickness, the width of the line is equally distributed to each side of the origin point; but for lines of even thickness, the GDI biases the top and left sides.

If your application has been positioning lines without making assumptions about the thickness of pens, it will probably handle thicker pens due to high-DPI without any changes. But if it has been assuming a constant pen width, such as one pixel, you should be aware of the effect that GDI behavior can have on layouts.

Figure 1 shows how GDI behavior can cause unexpected shifts in your layout. The following code is executed on a 96-DPI and a 192-DPI device, with a scaled pen selected into hDC, and the results are shown side-by-side:

rgptEndpoints[0].x = SCALEX(1);
rgptEndpoints[0].y = SCALEY(1);
rgptEndpoints[1].x = SCALEX(6);
rgptEndpoints[1].y = SCALEY(1);
Polyline(hDC, rgptEndpoints, 2);

Figure 1. Unexpected shifts in layout due to GDI behavior

Although the use of the SCALEX and SCALEY macros correctly placed the origin and the scaled pen ensured a line of the right thickness, the line draws higher in 192-DPI than in 96-DPI. This is because the GDI centers the ink around the origin and biases to the top for lines of even thickness.

Included in the CrosswordSample code are some helpers that let you precisely control how to draw thick lines relative to origin coordinates. The HIDPI_Rectangle and HIDPI_Polyline functions in the code wrap GDI calls that are likely to be problematic. Returning to the preceding example, here is how you can draw a DPI-aware horizontal line in which the 192-DPI layout will match the 96-DPI layout:

rgptEndpoints[0].x = SCALEX(1);
rgptEndpoints[0].y = SCALEY(1);
rgptEndpoints[1].x = SCALEX(6);
rgptEndpoints[1].y = SCALEY(1);
HIDPI_Polyline(hDC, rgptEndpoints, 2, PS_DOWNRIGHT);

Internet Explorer and HTML Controls

HTML layouts are an exception to the rule that all pixel coordinates should be scaled. On Windows Mobile-based Pocket PCs, Internet Explorer interprets pixel coordinates in HTML specifically as 96-DPI pixels. Although it is always best to use relative coordinates in HTML (defining a table column as 50% of the table width, for example), if you are currently using pixel metrics, they will continue to produce the same layout at higher DPI.

The same is not true for Windows Mobile-based Smartphone however. Pixel coordinates will continue to refer to actual pixels. For this reason, a high-resolution Smartphone will display more web page content, and possibly render the Web page differently, than would a low-resolution Smartphone.

The screen size can be determined on the device side via the window.screen.width and window.screen.height Microsoft® JScript® variables. If you would like to know the device's screen size on the web server side, you find out by querying the HTTP_UA_PIXELS header.

Text and Fonts

While TrueType fonts scale nicely, they don't scale linearly: increasing the DPI by 10 percent generally does not increase a string's length by exactly 10 percent. The cumulative effect, especially in longer strings, may cause text in different resolutions to be relatively shorter or to be clipped or to wrap unexpectedly. It is therefore important to use the Windows Mobile GetTextExtent function instead of making assumptions about how much space a string will take up. For static layouts like dialogs, you should allocate some extra width for text elements to fit properly in any DPI.

In addition, different fonts exhibit different nonlinear scaling effects, which means that the use of multiple fonts on a screen may cause some to appear disproportionate to others in different resolutions.

Creating DPI-Aware Fonts

When you create fonts for Smartphones and Pocket PCs, you should use points to specify the font size. A point refers to a logical size (1/72 of a logical inch), not a pixel height, so when used correctly it is inherently DPI-aware. Following is an example of code that creates a DPI-aware font using point size:

LOGFONT lf;
memset(&lf, 0, sizeof(lf));
// Set other LOGFONT values as appropriate.
lf.lfHeight = -(iPointSize * GetDeviceCaps(hdc, LOGPIXELSY)) / 72;
HFONT font = CreateFontIndirect(&lf); 

As lf.lfHeight is an integer pixel height, it is subject to rounding differences that are introduced through the MulDiv call in different resolutions. The size of the resulting font may not be linearly scaled from 96 DPI, but it will be the proper approximation of the desired point size. This is the recommended method for creating fonts.

If you need a more exact linear scaling of your font pixel size, you can create a temporary font for 96 DPI, scale its tmHeight text metric, and create the new font based on the resulting value, as shown in the following example. Linearly, the resulting font will be exactly scaled in height according to CDC::GetTextExtent, but it may not be linearly scaled in width and may also be visually taller or shorter.

Note   Even when you use this method to more precisely scale fonts, no assumptions should be made about how much space a string will occupy.

// Create font for 96 DPI.
LOGFONT lf;
memset(&lf, 0, sizeof(lf)); 
// Set other LOGFONT values as appropriate.
lf.lfHeight = -MulDiv(iPointSize, 96, 72);
HFONT hfont = CreateFontIndirect(&lf); 
   // Scale the tmHeight; create target font.
   TEXTMETRIC tm;
HDC hdc = GetDC(target);
HFONT hfontOld = (HFONT)SelectObject(hdc, hfont);
GetTextMetrics(hdc, &tm);
SelectObject(hdc, hfontOld);
DeleteObject(hfont);
ReleaseDC(target, hdc);
lf.lfHeight = SCALEY(tm.tmHeight);
hfont = CreateFontIndirect(&lf);

Getting the User Font Size

The Windows Mobile-based Pocket PC has a new Control Panel item (Settings/Screen/Text Size) that allows the user to choose the font size that looks best on the device screen. New applications written for the Pocket PC should be aware of this and should use the user-selected font size for their main UI text whenever possible. To retrieve this value, use the SHGetUIMetric function provided with the CrosswordSample code.

LONG  dwFontSize = 12;
SHGetUIMetrics(SHUIM_FONTSIZE_PIXEL, &dwFontSize, sizeof(dwFontSize),  NULL);
lf.lfHeight = -dwFontSize;

Your application should also be aware when the user font size changes. To receive this notification, your application should call RegisterWindowMessage for the SH_UIMETRIC_CHANGE message. When your application receives this message, it should query SHGetUIMetric to find the new font size and then repaint itself.

Images

  • In this document image refers to all raster-based image files (BMP/JPEG/GIF), icons, and cursors. Since their dimensions are fixed in pixel units, they present unique problems for a DPI-aware application. While image scaling can stretch images to the correct logical sizes, to truly take advantage of the high-DPI screen and create a positive customer experience, high-visibility graphics should be manually re-created.

Stretching Images

If the display DPI is not the same as the DPI for which the image was designed, the image needs to be stretched to display at the correct physical size. However, you will need to determine whether the image should be stretched every time it is painted or whether a new bitmap should be created that is stretched only once.

If the bitmap painting code is accessible to you, changing the code to stretch on paint is less memory intensive than stretching the bitmap on load. If, however, you are using a function or object that does not allow you to scale the bitmap (for example, an image list function), you may need to stretch on load.

To stretch on paint, you can scale a bitmap by calling the Windows Mobile function StretchBlt instead of the BitBlt function. The following example assumes the bitmap was authored at 96-DPI dimensions (and therefore should be stretched from its native width and height to SCALEX(width) and SCALEY(height)):

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);

To stretch on load, you can use the functions HIDPI_StretchBitmap or HIDPI_ImageList_LoadImage provided with the CrosswordSample code.

Using Image List Load Functions to Scale Bitmaps

The ImageList_LoadImage function assumes no scaling, so it has only one parameter, cx, which represents the width of each image in the bitmap on disk as well as on screen. HIDPI_ImageList_LoadImage likewise has only one cx parameter, which represents the width of each image on disk. So how does HIDPI_ImageList_LoadImage determine how wide each image should appear on screen? The answer is that bitmaps have a special field to identify the DPI resolution they were authored for: the biXPelsPerMeter and biYPelsPerMeter fields in the BITMAPINFO header. HIDPI_ImageList_LoadImage examines these fields and scales the bitmap only if this information is different than the DPI resolution of the screen.

Most graphics editing tools set these fields to 96 DPI by default. It's very important that your high-resolution image lists have them set correctly; otherwise, your bitmaps will be improperly scaled on high-DPI displays.

Multiple Images

An alternative to scaling is using multiple images, each authored for a different DPI. This technique yields the best results.

The .ico format is already capable of storing multiple image sizes in a single file. When you load the icon or cursor, the application asks for the size that the GetSystemMetrics function suggests; the system then picks the closest image.

The following example code shows how to select from multiple images that are not in .ico format. To be completely DPI-aware, an application should still scale the loaded bitmap based on the actual DPI of the device, because it should make no assumption about what DPI resolutions are available.

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

When the loaded bitmap is already authored for the correct DPI, scaling it will have no adverse effect on performance or image quality.

Images in Static Controls

Windows Mobile automatically scales bitmaps in static controls on high-DPI displays, unless the bitmap is equal to or larger than the control that contains it, in which case Windows CE assumes that the bitmap was authored for high-DPI. This means that for static controls to work correctly you probably will not need to make any special changes.

To override this automatic scaling, use the SS_REALSIZEIMAGE style in the static control. This will prevent your images from being scaled. Conversely, if you wish to scale an image and Windows CE doesn't do it for you, you can still scale it manually by using STM_SETIMAGE before you set it into the static control.

Legacy Support

High-resolution Windows Mobile-based Pocket PCs also provide an emulation layer for backwards compatibility with old applications. With this emulation layer, the display appears to legacy applications as a traditional 240 x 320 display; however, the operating system scales all the graphics to fit the actual display size. Fonts are linearly scaled in height, using the method described earlier in this document, in the section titled "Creating DPI-Aware Fonts."

The following factors control whether the device should use the emulation layer for an application:

  • Subsystem version information in the executable header.
  • HI_RES_AWARE custom data in the executable's resources.

The subsystem version information is set during the linking phase of compilation. By default, applications compiled with the Windows Mobile 2003 and 2002 SDKs set this value to 4.20 or lower; in future releases of the Pocket PC SDK, this value will be set to 4.21 and higher. In general, applications with a subsystem version of 4.20 or lower are considered legacy applications and will go through the emulation layer.

The HI_RES_AWARE resource item can be used to override this behavior for legacy applications. The operating system looks for this special resource item when the legacy application loads. The following procedure adds it to your program, using Microsoft eMbedded Visual C++®.

Note   Alternatively, you can add the following line to your resource file directly:

HI_RES_AWARE CEUX {1} // To turn off the emulation layer

To add the HI_RES_AWARE resource item to your program

  1. From the Insert menu, select Resource.
  2. Click the Custom button.
  3. Enter CEUX for the resource type.
  4. Set the resource data to 01 00.
  5. Click the Properties tab.
  6. Rename the item to "HI_RES_AWARE", including quotes. (If the quotes are omitted, HI_RES_AWARE will be incorrectly defined as a numeric value in resource.h, and you will need to go back and delete the line from resource.h.)
  7. Deselect the external file checkbox.

GAPI Legacy Support

Applications that use GAPI frequently make assumptions about the screen size based on earlier versions of Pocket PC and Smartphone hardware. To preserve backward compatibility with legacy applications, an emulation layer has been added to GAPI. By default, this layer will scale GAPI drawing to fit the screen; however, a high-resolution-aware application can choose to bypass the emulation layer to have complete access to the display hardware.

To bypass the emulation layer, an application should not call GXBeginDraw or GXEndDraw. Instead, it should call ExtEscape with the nEscape parameter equal to GETRAWFRAMEBUFFER and use the returned frame pointer for drawing.

#define GETRAWFRAMEBUFFER   0x00020001

#define FORMAT_565 1
#define FORMAT_555 2
#define FORMAT_OTHER 3

typedef struct _RawFrameBufferInfo
{
   WORD wFormat;
   WORD wBPP;
   VOID *pFramePointer;
   int  cxStride;
   int  cyStride;
   int  cxPixels;
   int  cyPixels;
} RawFrameBufferInfo;

// ...

RawFrameBufferInfo rfbi;
HDC hdc = GetDC(NULL);
ExtEscape(hdc, GETRAWFRAMEBUFFER, 0, NULL, 
   sizeof(RawFrameBufferInfo), (char *) &rfbi);
ReleaseDC(NULL, hdc);

Legacy applications that use GAPI also get the same backward compatibility support that non-GAPI applications receive. This means that stylus tap coordinates are scaled depending on the subsystem version of the application's executable — even if the GETRAWFRAMEBUFFER ExtEscape is used.

Conclusion

If you do not want to worry about making your existing application high resolution aware you should be able to leave the code as is and the legacy support will scale your fonts and graphics appropriately. However, if you want to take advantage of high DPI screens by, for example, using high resolution graphics there are simple steps you can take to remove DPI dependence from your application.