开发能够识别 DPI 的应用程序
发布日期: 8/30/2004 | 更新日期: 8/30/2004
Microsoft
适用于:
基于 Windows Mobile™ 2003 Second Edition 的 Pocket PC
基于 Windows Mobile™ 2003 Second Edition 的 Smartphone
Microsoft eMbedded Visual C++ 4.0
摘要:基于 Windows Mobile 2003 Second Edition 的 Pocket PC 和 Smartphone 可以用比早期设备更高的分辨率来运行。了解应用程序为确定设备的 DPI 以及利用高 DPI 显示器而需要进行的更改。
从 Microsoft 下载中心下载 Crossword.msi。
本页内容
简介
随着显示器技术的改进,在同一区域中放置更多的像素正在变为可能。您可以使用这一增大的分辨率在屏幕上放置更多的项,或者使用它来更加清晰地绘制相同的项。在后一种情况下,称为显示器已经增加了每英寸的点数 (DPI);DPI 是一个逻辑值,表示显示器区域的像素密度。
打印媒体已经在很长一段时间内利用了高 DPI 技术。在 1200 dpi 打印机上打印的文档与 300 dpi 打印机打印的文本数量相同,但文本显得更清晰。
基于 Windows Mobile 的 Pocket PC 和 Smartphone 传统上使用 96 dpi 的显示器,但基于 Windows Mobile 2003 Second Edition 的设备可以用更高的分辨率运行。升级您的应用程序以支持 DPI 识别功能,不仅可以确保在高 DPI 设备上能够正确显示,还可以使您的应用程序具备下列高 DPI 优势:
•
更清晰的文本:几乎不必付出任何代价就可以获得的最显著改进。每个能够识别 DPI 并使用 TrueType 字体的应用程序都可以继承这一改进。
•
更详细的图形:如果采取了某些步骤以提供高分辨率位图,则应用程序可以使用提高的分辨率来显示更为详细的图标和图形。
本文介绍了您需要对应用程序进行哪些更改,才能使其能够确定设备的 DPI 以及利用具有较高 DPI 的显示器。
关键问题
在用于 Pocket PC 和 Smartphone 的能够识别 DPI 的应用程序中,需要重点关注下列三个普通领域内的问题:
•
布局
•
文本和字体
•
图像
布局
如果以假定采用 96 DPI 的像素坐标来指定 UI 元素的位置和大小,则这些元素在高 DPI 设备上将是不正确的。通常,应该使用可缩放的位置和大小或者相对于控件、字体或系统度量标准来布置所有 UI 元素。
您可以使用 Windows Mobile 函数 GetDeviceCaps 并通过传入 LOGPIXELSX 或 LOGPIXELSY 作为第二个参数来获取显示器的 DPI。CrosswordSample 示例演示了如何定义 SCALEX 和 SCALEY 宏,以基于 GetDeviceCaps 中的信息应用比例因子。
您可以继续使用像素作为单位,但需要使用下列技术来消除有关 DPI 的假设:
•
使用 SCALEX 和 SCALEY 宏来缩放 96 DPI 坐标,或者在适当的时候使用 GetSystemMetrics 返回的度量标准。
•
相对于其他控件来表示大小或位置。
•
相对于字体来表示大小或位置。
对话框已经使用字号来确定它们的布局,因此它们通常不需要进行特殊修改,就能在高 DPI 设备上工作。
下面是一个通过 DPI 识别功能定位窗口的示例,其中 x、y、dx 和 dy 是采用 96 DPI 的像素坐标:
SetWindowPos(hwnd, NULL, SCALEX(x), SCALEY(y), SCALEX(dx), SCALEY(dy), SWP_NOZORDER);
如果您选择缩放 96 DPI 像素度量标准,请在使用整数时注意舍入问题。例如,因为舍入问题,SCALEX(a + b) 可能不等于 (SCALEX(a) + SCALEX(b))。
系统度量标准
要能够识别 DPI,应用程序不能进行有关各种屏幕元素像素大小(如图标大小或边框宽度)的任何假设。基于 Windows Mobile 的 Smartphone 和 Pocket PC 提供了许多系统度量标准,以提供有关用户系统的信息。例如,可以通过 GetSystemMetrics Windows CE 函数来查询以下信息。
•
应该使用 GetSystemMetrics(SM_CXSCREEN) 或 GetSystemMetrics(SM_CXSCREEN) 来获取屏幕大小尺寸。
•
应该使用 GetSystemMetrics(SM_CXBORDER) 或 GetSystemMetrics(SM_CYBORDER) 来获取边框尺寸。
•
应该使用 GetSystemMetrics(SM_CXICON) 或 GetSystemMetrics(SM_CXSMICON) 来获取大图标尺寸和小图标尺寸。
使用能够识别 DPI 的粗笔绘制
在使用较高的 DPI 值时,必须增加线条的粗细来维持成比例的视觉大小。要能够识别 DPI,您的应用程序应该缩放用于进行绘制的任何画笔的宽度。
与绘制粗(大于 1 个像素)线条有关的 Windows Mobile 图形设备接口 (GDI) 的行为使得进行准确而正确的定位很困难。对于对 Windows Mobile 函数 LineTo 和 Polyline 的调用而言,GDI 使用指定的原点作为画笔宽度的中心。对于奇数宽度的线条,线条的宽度会均匀分布到原点的两侧;但是,对于偶数宽度的线条,GDI 将偏重于顶部和左侧。
如果您的应用程序在定位线条时没有进行有关画笔宽度的任何假设,则它可能会不加更改地处理由于高 DPI 而变宽的画笔。但是,如果它假设了恒定的画笔宽度(如 1 个像素),则您应该注意 GDI 行为对布局产生的影响。
图 1 显示了 GDI 行为如何在您的布局中导致意外偏移。下面的代码在 96 DPI 设备和 192 DPI 设备上执行,并且将缩放的画笔选入 hDC,然后将并排显示以下结果:
rgptEndpoints[0].x = SCALEX(1);
rgptEndpoints[0].y = SCALEY(1);
rgptEndpoints[1].x = SCALEX(6);
rgptEndpoints[1].y = SCALEY(1);
Polyline(hDC, rgptEndpoints, 2);
图 1. 由于 GDI 行为而在布局中导致的意外偏移
尽管使用 SCALEX 和 SCALEY 宏正确放置了原点,并且缩放画笔确保了绘制正确宽度的线条,但在 192 DPI 设备上绘制的线条比在 96 DPI 设备上绘制的线条高。这是因为 GDI 在原点周围分布墨迹,并且对于偶数宽度的线条偏重于顶部。
CrosswordSample 代码中包括了一些 Helper 函数,可让您精确地控制如何相对于原点坐标绘制粗线条。代码中的 HIDPI_Rectangle 和 HIDPI_Polyline 函数包装了可能会出问题的 GDI 调用。返回到前面的示例,下面的代码演示了如何绘制能够识别 DPI 的水平线条,并且使 192 DPI 布局与 96 DPI 布局相符:
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 和 HTML 控件
HTML 布局是应该缩放所有像素坐标这一规则的例外。在基于 Windows Mobile 的 Pocket PC 上,Internet Explorer 专门以 96 DPI 像素 来解释 HTML 中的像素坐标。尽管在 HTML 中使用相对坐标(例如,将表列定义为表宽的 50%)始终是最佳选择,但如果您当前正在使用像素度量标准,则它们将在较高 DPI 下继续产生相同的布局。
但是,对于基于 Windows Mobile 的 Smartphone 而言,却不是这样。像素坐标将继续引用实际像素。因此,与低分辨率 Smartphone 相比,高分辨率 Smartphone 将显示更多的 Web 页内容,并且可能会以不同方式呈现 Web 页。
在设备端,可以通过 window.screen.width 和 window.screen.height Microsoft JScript 变量来确定屏幕大小。如果您希望在 Web 服务器端了解设备的屏幕大小,则可以通过查询 HTTP_UA_PIXELS 头来实现。
文本和字体
尽管 TrueType 字体能够很好地缩放,但它们不能线性缩放:将 DPI 增加 10% 通常并不能将字符串的长度恰好增加 10%。累积效果(特别是在较长的字符串中)可能会导致采用不同分辨率的文本相对短一些,或者被剪短,或者意外换行。因此,需要使用 Windows Mobile 的 GetTextExtent 函数,而不是就字符串将占据多少空间来进行假设。对于对话框之类的静态布局,您应为文本元素分配一些额外的宽度,以适应任何 DPI。
此外,不同的字体显示不同的非线性缩放效果,这意味着采用不同的分辨率时,在屏幕上使用多种字体可能会导致某些字体看起来与其他字体不成比例。
创建能够识别 DPI 的字体
当您为 Smartphone 和 Pocket PC 创建字体时,应该使用磅来指定字号。磅指的是逻辑大小(逻辑英寸的 1/72)而不是像素高度,因此如果使用正确,它本身就能够识别 DPI。下面是一个使用磅大小创建能够识别 DPI 的字体的代码示例:
LOGFONT lf;
memset(&lf, 0, sizeof(lf));
// Set other LOGFONT values as appropriate.
lf.lfHeight = -(iPointSize * GetDeviceCaps(hdc, LOGPIXELSY)) / 72;
HFONT font = CreateFontIndirect(&lf);
因为 lf.lfHeight 是一个整数像素高度,所以在采用不同的分辨率时,它可能会受到通过 MulDiv 调用引入的舍入差的影响。得到的字体大小可能无法从 96 DPI 线性缩放,但它将是所需磅大小的严格近似值。这是建议使用的字体创建方法。
如果您需要对字体像素大小进行更为精确的线性缩放,则可以为 96 DPI 创建临时字体,缩放它的 tmHeight 文本度量标准,并根据得到的值创建新字体,如下面的示例所示。得到的字体将根据 CDC::GetTextExtent 精确地线性缩放高度,但其宽度可能无法线性缩放,并且还可能看起来更高或更矮。
注 即使您使用该方法来更精确地缩放字体,也不应该就字符串将占据多少空间来进行假设。
// 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);
获取用户字号
基于 Windows Mobile 的 Pocket PC 具有一个新的控制面板项(设置/屏幕/文本大小),它使用户能够选择在设备屏幕上具有最佳显示效果的字号。为 Pocket PC 编写的新应用程序应该知道这一点,并且应该尽可能地使用用户选择的字号来显示它们的主要 UI 文本。要检索该值,请使用随 CrosswordSample 代码一起提供的 SHGetUIMetric 函数。
LONG dwFontSize = 12;
SHGetUIMetrics(SHUIM_FONTSIZE_PIXEL, &dwFontSize, sizeof(dwFontSize), NULL);
lf.lfHeight = -dwFontSize;
您的应用程序还应该知道用户字号何时更改。要收到该通知,您的应用程序应该为 SH_UIMETRIC_CHANGE 消息调用 RegisterWindowMessage。当您的应用程序收到该消息时,应该查询 SHGetUIMetric 以找到新的字号,然后重新绘制自身。
图像
在本文中,图像 是指所有基于光栅的图像文件 (BMP/JPEG/GIF)、图标和光标。因为它们以像素单位表示的大小是固定的,所以可以显示出能够识别 DPI 的应用程序的独特问题。尽管图像缩放可以将图像拉伸到正确的逻辑大小,但是要真正利用高 DPI 屏幕并提供积极的客户体验,应该手动重新创建高可见性的图形。
拉伸图像
如果显示器 DPI 与针对其设计图像的 DPI 不同,则需要拉伸图像以使其按正确的物理大小显示。但是,您需要确定是否应该在每次绘制图像时拉伸它,或者是否应该创建仅拉伸一次的新位图。
如果您可以访问位图绘制代码,则更改这些代码以在绘制时拉伸比在加载时拉伸位图所使用的内存少。但是,如果您要使用不允许缩放位图的函数或对象(例如,图像列表函数),则可能需要在加载时拉伸。
要在绘制时拉伸,您可以通过调用 Windows Mobile 函数 StretchBlt(而不是 BitBlt 函数)来缩放位图。下面的示例假设位图是以 96 DPI 尺寸创建的(因此应该从其原生宽度和高度拉伸到 SCALEX(宽度) 和 SCALEY(高度)):
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);
要在加载时拉伸,您可以使用随 CrosswordSample 代码一起提供的 HIDPI_StretchBitmap 或 HIDPI_ImageList_LoadImage 函数。
使用图像列表加载函数来缩放位图
ImageList_LoadImage 函数假设不进行缩放,因此它只有一个参数 cx,该参数表示磁盘以及屏幕上位图中的每个图像的宽度。HIDPI_ImageList_LoadImage 也同样只具有一个 cx 参数,该参数表示磁盘上每个图像的宽度。那么,HIDPI_ImageList_LoadImage 如何确定每个图像在屏幕上应该显示的宽度呢?答案是位图具有特殊的字段,可以标识在创建它们时所使用的 DPI 分辨率:BITMAPINFO 标头中的 biXPelsPerMeter 和 biYPelsPerMeter 字段。HIDPI_ImageList_LoadImage 将检查这些字段,并且仅在该信息与屏幕的 DPI 分辨率不同时缩放位图。
默认情况下,大多数图形编辑工具将这些字段设置为 96 DPI。非常重要的一点是,您的高分辨率图像列表是否可以正确地设置这些字段;否则,您的位图将在高 DPI 显示器上不正确地缩放。
多个图像
缩放的一种替代方法是使用多个图像,每个图像都针对不同的 DPI 进行创建。该技术会产生最佳的结果。
.ico 格式已经能够在单个文件中存储多个图像大小。在您加载图标或光标时,应用程序会寻找 GetSystemMetrics 函数暗示的大小;然后系统会选择最接近的图像。
下面的示例代码显示了如何从多个非 .ico 格式的图像中进行选择。要能够完全识别 DPI,应用程序还应该基于设备的实际 DPI 来缩放加载的位图,因为它不应该就可用的 DPI 分辨率来进行任何假设。
if (GetDeviceCaps(hdc, LOGPIXELSX) < 130)
bitmap = LoadBitmap(hInstance, (char*) IDB_BITMAP1);
else
bitmap = LoadBitmap(hInstance, (char*) IDB_BITMAP2);
如果加载的位图已经针对正确的 DPI 来创建,则缩放它将不会对性能或图像质量造成任何负面影响。
静态控件中的图像
Windows Mobile 可自动缩放高 DPI 显示器的静态控件中的位图,除非该位图的大小等于或大于包含它的控件(在此情况下,Windows CE 将假设该位图是针对高 DPI 来创建的)。这意味着,要使静态控件正确工作,您可能无须进行任何特殊更改。
要重写这一自动缩放行为,请在静态控件中使用 SS_REALSIZEIMAGE 样式。这将防止您的图像被缩放。相反,如果您希望缩放图像,但 Windows CE 没有为您执行该操作,则您仍然可以在将其安置在静态控件之前,使用 STM_SETIMAGE 来手动缩放它。
旧式支持
基于 Windows Mobile 的高分辨率 Pocket PC 还提供了一个模拟层,以实现与旧式应用程序的向后兼容性。通过该模拟层,显示器在旧式应用程序看来像是传统的 240 x 320 显示器;但是,操作系统会缩放所有图形,以适应实际的显示器大小。字体的高度将线性缩放,所使用的方法在本文前面标题为“创建能够识别 DPI 的字体”的部分中进行了介绍。
下列因素将控制设备是否应该对应用程序使用模拟层:
•
可执行标头中的子系统版本信息。
•
可执行文件的资源中的 HI_RES_AWARE 自定义数据。
子系统版本信息是在编译的链接阶段设置的。默认情况下,用 Windows Mobile 2003 和 2002 SDK 编译的应用程序将该值设置为 4.20 或更低;在 Pocket PC SDK 的将来版本中,该值将被设置为 4.21 和更高。通常,子系统版本为 4.20 或更低的应用程序被视为旧式应用程序,并且将经过模拟层。
您可以使用 HI_RES_AWARE 资源项来重写旧式应用程序的这一行为。当旧式应用程序加载时,操作系统将查找这一特殊的资源项。下面的过程使用 Microsoft eMbedded Visual C++ 将其添加到程序中。
注 此外,您也可以直接将下行添加到您的资源文件中:
HI_RES_AWARE CEUX {1} // To turn off the emulation layer
将 HI_RES_AWARE 资源项添加到程序中
1.
从 Insert 菜单中,选择 Resource。
2.
单击 Custom 按钮。
3.
输入 CEUX 作为资源类型。
4.
将资源数据设置为 01 00。
5.
单击 Properties 选项卡。
6.
将该项重命名为 "HI_RES_AWARE"(包括引号)。(如果省略引号,则 HI_RES_AWARE 将在 resource.h 中被错误地定义为数值,并且您将需要回过头去从 resource.h 中删除该行。)
7.
取消选择 external file 复选框。
GAPI 旧式支持
频繁使用 GAPI 的应用程序会基于早期版本的 Pocket PC 和 Smartphone 硬件进行有关屏幕大小的假设。为了保持与旧式应用程序的向后兼容性,已经向 GAPI 中添加了模拟层。默认情况下,该层将缩放 GAPI 图形以适应屏幕;但是,能够识别高分辨率的应用程序可以选择绕过模拟层,以便对显示器硬件进行完全访问。
要绕过模拟层,应用程序不应该调用 GXBeginDraw 或 GXEndDraw。相反,它应该使用等效于 GETRAWFRAMEBUFFER 的 nEscape 参数来调用 ExtEscape,并使用返回的框架指针进行绘制。
#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);
使用 GAPI 的旧式应用程序还可以获得与非 GAPI 应用程序相同的向后兼容性。这意味着,笔针坐标是根据应用程序可执行文件的子系统版本进行缩放的 — 即使使用了 GETRAWFRAMEBUFFER ExtEscape。
小结
如果您不希望担心使现有应用程序能够识别高分辨率的问题,则应该能够将代码保持原样,旧式支持将适当地缩放您的字体和图形。但是,如果您希望通过某种方法(例如,使用高分辨率图形)来利用高 DPI 屏幕,则可以采取一些简单的步骤来消除应用程序中的 DPI 依赖性。