Windows Mobile 上的像素图形编程
介绍在基于 Windows Mobile 的设备(Pocket PC、SmartPhone)上进行像素图形编程的基础知识。
引言
本文介绍了在 Pocket PC/SmartPhone 设备上进行像素图形编程的基础知识。我收集了您在开始 Pocket PC 图形编程时可能需要的信息。我编写了一个 Caleidoscope 应用程序示例,演示了如何将窗口设置为全屏,以及如何访问掌上设备的视频内存。我不会讨论如何使用 Windows GDI 函数编程图形。我认为直接将像素写入视频内存更快,而且相对简单。我知道 Windows Mobile 有一些免费和商业的图形库。但是,它们可能使用依赖于设备的技巧,并且对于个人使用来说,它们可能免费也可能不免费。我认为一旦你理解了 Windows Mobile 图形的工作原理,你就不会想为任何库付费了。我承认有些库可能比这里介绍的方法更快,但我确实在我的任何 PDA 上都没有遇到速度问题。而且,代码在很多方面仍然可以针对速度进行优化。
我已经将所有图形处理例程都放入了 ZGfx
类中,该类可以轻松地在 Pocket PC 和 SmartPhone 的其他像素图形应用程序中使用。此类不旨在成为一个完整的图形库;它仅仅是一组方便的例程,用于加快图形开发速度。
我假设您熟悉 Visual Studio 和 C 语言,我也假设您知道什么是指针和窗口句柄。
像素、间距和帧缓冲区
Pocket PC/SmartPhone 不支持可选择的视频模式;取而代之的是固定的、依赖于设备的视频模式。截至今天(2006 年 10 月),您能接触到的大多数 Pocket PC 设备都具有以下分辨率之一:
- QVGA:240x320,每像素 16 位
- VGA:480x640,每像素 16 位
SmartPhones 可以是
- 标准:176x220,每像素 16 位
- QVGA:240x320,每像素 16 位
还有方形屏幕设备(QGVA:240x240,VGA:480x480)。关于像素格式有几点需要说明。我将讨论名为 RGB-565 的 16 位每像素格式。还有(曾有?)其他较旧的像素格式,我相信很快就会有 24 位每像素的 RGB-888 颜色设备。所以,请谨慎假设,并始终检查您的程序运行的硬件,并为新设备可能支持不同像素格式做好准备。
因此,如今大多数设备支持每像素 16 位:5 位红色,6 位绿色和 5 位蓝色。16 位等于 2 字节,以下是它们在视频内存中的存储方式:
值为 0xffff 表示白色像素,值为 0x0000 表示黑色,0xf800 表示红色。很简单,不是吗?这是一个将 24 位 RGB 颜色转换为 RGB-565 的宏。
#define RGB_TO_565(r,g,b) (WORD) ((r & 0xf8 )<<8) | ((g&0xfc)<<3) | ((b&0xf8)>>3)
那么,我们需要什么才能显示一个像素?答案是,将一个 16 位值写入图形内存。图形内存称为帧缓冲区。一旦有了帧缓冲区指针,您就可以直接将像素数据写入视频内存。帧缓冲区地址是多少?这取决于设备。有三种记录在案的方法可以获取所需的帧缓冲区地址。我建议将它们结合起来,这样万一其中一种不支持,还有另一种可以尝试。这些方法是:
- 使用
ExtEscape()
GDI 函数。此 Windows 函数返回帧缓冲区地址以及其他参数。它很简单,但不是最佳方法,并且未来的设备很可能不支持此方法。 - 使用 GAPI。GAPI 是 Microsoft 提供的图形辅助库(GX.dll)。它与 QVGA 设备配合良好,但不支持 VGA 设备的性能(仅支持像素翻倍,即放大显示),而且似乎 Microsoft 不会再更新它了。
- 使用 DirectDraw。这是最佳选择,但仅在 Windows Mobile 5.0 设备上受支持。
我建议使用 DirectDraw,如果设备上不可用,则使用其他方法作为备用。稍后我将举例说明这些方法。
还有一件事需要注意:间距。如果我有一个帧缓冲区指针和一个固定的分辨率,我可能会假设可以使用类似这样的代码逐行填充屏幕:
int x, y; WORD *p; p=GetFramebuffer(); for(x=0; x<WIDTH; x++) for(y=0; y<HEIGHT; y++) *p++=0xffff;
这假设如果您将缓冲区指针增加 2(WORD
的大小),指针就会向右移动一个像素。这也假设如果您添加 WIDTH * 2
,指针就会向下移动一个像素。这些假设是错误的,因为帧缓冲区的方向不同。在不详细介绍的情况下,您需要记住的是:
- 将指针添加到下一行中的下一个像素所需的字节数称为 XPitch。
- 将指针添加到下一行(即向下移动一个像素)所需的字节数称为 YPitch。
因此,在计算像素位置 (x, y) 的指针时,您必须包含间距值:
pxy = pbuffer + x * xpitch + y * ypitch;
我们来看一个例子。帧缓冲区指针可能是 0x10000000。对于 VGA 设备,X 和 Y 间距值可能是 2 和 960 (0x3c0)。因此,像素 (1,1) 的地址是:
0x10000000 + 1 * 2 + 1 * 960 = 0x100003c2.
在大多数系统中,XPitch 是 2(以字节为单位的像素大小)。但在所有情况下,您都必须查询间距值。间距值也可能是负数,所以请使用有符号变量!如何获取间距值?我将在下一节中解释。
初始化图形
我将总结显示一些图形所需的步骤:
- 创建应用程序窗口
- 将其设置为全屏
- 获取设备参数:帧缓冲区地址和间距。
- 将像素数据写入帧缓冲区。
我认为第一步无需解释;如果您需要帮助,请使用 Visual Studio AppWizard。它将为您创建一个功能齐全的应用程序以及一个漂亮的窗口。
要将窗口设置为全屏,让我们使用以下代码:
RECT rc; //hide task bar and other icons SHFullScreen(g_hWnd, SHFS_HIDETASKBAR | SHFS_HIDESTARTICON | SHFS_HIDESIPBUTTON); //resize window to cover screen SetRect(&rc, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)); MoveWindow(g_hWnd, rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top, false);
上面的代码将隐藏任务栏和开始图标,方法是将它们“移到”窗口后面,然后将窗口大小调整为覆盖整个屏幕。让我们获取依赖于设备的参数。如前所述,这可以通过三种方式完成。第一种,也是我认为最简单的一种,是 Windows GDI 函数 ExtEscape()
。以下是我在图形类中如何使用它:
bool ZGfx::GfxInitRawFrameBufferAccess() { RawFrameBufferInfo rfbi; HDC hdc; bool retval; retval=false; hdc=GetDC(m_hwnd); if(hdc) { if(ExtEscape(hdc, GETRAWFRAMEBUFFER, 0, 0, sizeof(RawFrameBufferInfo), (char *) &rfbi)) { if(rfbi.wFormat==FORMAT_565) { m_framebufwidth=rfbi.cxPixels; //store width m_framebufheight=rfbi.cyPixels; //store height m_xpitch=rfbi.cxStride; //store xpitch m_ypitch=rfbi.cyStride; //store ypitch m_cbpp=rfbi.wBPP; //store bits per pixel value m_framebuf=rfbi.pFramePointer; //store pointer retval=true; } } ReleaseDC(m_hwnd,hdc); } return retval; }
调用返回结构中的所有数据,这非常方便。RawFrameBufferInfo
结构在 GX.h 中定义。请注意,并非所有设备都支持这种简单的方法。另外请注意,为了获得最佳性能,如果 DirectDraw 可用,建议使用它。
让我们看看 DirectDraw。这种方法需要小心。即使是运行 Pocket PC 2003 操作系统的 1-2 年前的设备也不支持此功能。因此,如果您想与这些设备兼容(您肯定想!),您不能链接到 ddraw.dll(DirectDraw 库)。这意味着您不能像通常调用任何其他系统函数那样直接调用 DirectDraw 函数!这是调用 Windows API 函数的一种典型方法:
MessageBox(hWnd, "Text", "Caption", MB_OK);
您只需输入函数名称以及参数。我敢打赌您以前已经这样做过一百万次了。结果,编译器/链接器将在您的 EXE 文件中包含一个导入引用,指向包含 MessageBox()
函数的系统 .dll 文件。如果找不到引用的 .dll 文件,Windows 将不会加载您的 EXE。换句话说,如果设备没有 DirectDraw(系统上没有 ddraw.dll),则包含 DirectDraw 调用的 EXE 将无法启动。如何调用 DirectDraw 同时保持与 WM5 之前的设备兼容?答案是,您必须使用 LoadLibrary()
API 手动加载 DirectDraw dll 文件,然后使用 GetProcAddress()
查找所需 DirectDraw 函数(即 DirectDrawCreate()
)的入口点,然后通过指针调用函数。不用担心,这比听起来简单:
//declare the DirectDrawCreate() function typedef LONG (*DIRECTDRAWCREATE)(LPGUID, LPUNKNOWN *, LPUNKNOWN *); ... //load the library and look up the function bool ZGfx::GfxLoadDirectDraw() { m_hDD=LoadLibrary(L"ddraw.dll"); if(m_hDD) { m_DirectDrawCreate=(DIRECTDRAWCREATE) GetProcAddress(m_hDD, L"DirectDrawCreate"); return true; } else return false; }
(注意:要使用 DirectDraw,您需要 Windows Mobile 5.0 SDK 中的 DirectDraw 主头文件 ddraw.h。它位于源代码包的 ZGfx 文件夹中。但是,要使用 ZGfx
类,您的项目无需使用 WM 5 SDK。Pocket PC 2003 SDK 即可。Caleidoscope 示例也使用 Pocket PC 2003 SDK。)
好的,我们已经加载了 DirectDraw,现在让我们获取显示属性:
DIRECTDRAWCREATE m_DirectDrawCreate; //our function declaration IDirectDraw *m_pdd; DDSURFACEDESC m_ddsd; IDirectDrawSurface *m_psurf; ... bool ZGfx::GfxInitDirectDraw() { LONG hr; //create surface hr=m_DirectDrawCreate(0, (IUnknown **)&m_pdd, 0); if(hr!=DD_OK) return false; //failed hr=m_pdd->SetCooperativeLevel(m_hwnd, DDSCL_FULLSCREEN); if(hr!=DD_OK) { m_pdd->Release(); m_pdd=0; return false; } //create a simple buffer memset((void *)&m_ddsd, 0, sizeof(m_ddsd)); m_ddsd.dwSize = sizeof(m_ddsd); m_ddsd.dwFlags = DDSD_CAPS; //no back buffering, only use the visible display area m_ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; //create surface (entire screen) hr=m_pdd->CreateSurface(&m_ddsd, &m_psurf, NULL); if(hr!=DD_OK ) { m_pdd->Release(); m_pdd=0; return false; } //get parameters with locking memset((void *)&m_ddsd, 0, sizeof(m_ddsd)); m_ddsd.dwSize = sizeof(m_ddsd); hr=m_psurf->Lock(0, &m_ddsd, DDLOCK_WAITNOTBUSY, 0); if(hr!=DD_OK) { //locking failed! m_psurf->Release(); m_psurf=0; m_pdd->Release(); m_pdd=0; return false; } //store data m_cbpp=m_ddsd.ddpfPixelFormat.dwRGBBitCount; m_xpitch=m_ddsd.lXPitch; m_ypitch=m_ddsd.lPitch; m_framebufwidth=m_ddsd.dwWidth; m_framebufheight=m_ddsd.dwHeight; //finally unlock surface m_psurf->Unlock(0); return true; }
上面的代码创建了一个大小与整个屏幕相同的简单缓冲区。请注意,使用 DirectDraw 时,如果您想对显示缓冲区进行任何操作,首先必须调用 Lock()
函数,完成操作后必须调用 Unlock()
。换句话说,除非锁定缓冲区,否则您无法获得缓冲区指针!上面的代码仅在获取屏幕参数时需要锁定,此时尚未进行任何绘制。
获取显示属性的第三种方法是使用 GAPI。GAPI 是 Microsoft 的图形辅助库,位于 GX.dll 中。MSDN 文档描述了 GAPI 提供的所有结构和函数,因此我不再赘述。对于初始化图形,您只需要两个函数:GXGetDisplayProperties()
和 GXBeginDraw()
。前者返回 GXDisplayProperties
类型的结构。此结构包含屏幕宽度、高度值以及间距。第二个函数返回一个 void *
,指向帧缓冲区。使用 GAPI 时,您必须仅在更新屏幕之前调用 GXBeginDraw()
,并在绘图工作完成后调用 GXEndDraw()
。(这与 DirectDraw 锁定是相同的概念。)
这是我执行 GAPI 初始化时的方式:
bool ZGfx::GfxInitGAPI() { GXDisplayProperties prop; int sw, sh; prop=m_GXGetDisplayProperties(); m_cbpp=prop.cBPP; m_ypitch=prop.cbyPitch; m_xpitch=prop.cbxPitch; m_framebufheight=prop.cyHeight; m_framebufwidth=prop.cxWidth; if(!(prop.ffFormat&kfDirect565)) //verify pixel format return false; //if it is a vga device, we have to check //if GAPI's not messing it up sw=GetSystemMetrics(SM_CXSCREEN); sh=GetSystemMetrics(SM_CYSCREEN); if(sw!=prop.cxWidth || sh!=prop.cyHeight) return false; return true; }
如果 GAPI 返回的屏幕分辨率与 GetSystemMetrics()
报告的不一致,请不要使用 GAPI。请注意,GAPI 不支持 VGA 设备。它支持一种称为像素翻倍的技术。这意味着 240x320 的显示可以在 VGA 设备上放大到 480x640。但这不是 VGA 图形!
使用 ZGfx 类
此类旨在提供基本级别的抽象,并简化对视频内存的访问。该类创建一个软件缓冲区,所有绘图操作都可以在其中进行。它还负责 GAPI 或 DirectDraw 初始化(取决于哪种适用)。您可以安全地假设软件缓冲区的 XPitch 始终为 2(记住,以字节为单位的像素大小)。您还可以假设 YPitch 是缓冲区宽度的两倍,即,对于 480 像素宽的缓冲区,YPitch 为 960。要使用此类,请先将 zgfx.h 和 zgfx.cpp 文件添加到您的项目中。对于 .cpp 文件,请在文件属性部分禁用使用预编译头文件。然后,将 ZGfx 类型的全局变量添加到您的应用程序中:
ZGfx g_gfx;
在创建了主应用程序窗口后,您需要创建一个显示缓冲区:
GfxRetval gr; GfxSubsys gs; gr=g_gfx.GfxCreateSurface(g_hWnd, g_screen_w, g_screen_h, &gs);
参数是:主窗口的句柄、请求的缓冲区宽度和高度,以及用于返回子系统类型的变量。返回值将指示成功或失败(请参阅 GfxRetval
枚举),并在成功时返回使用的子系统(原始帧缓冲区访问、GAPI 或 DirectDraw)(GfxSubsys
)。该例程将使用 DirectDraw(最佳选项),如果不可用,则回退到 GAPI 或原始帧缓冲区访问。
如果缓冲区小于屏幕分辨率,它将在屏幕上居中显示。缓冲区不能大于设备分辨率。您可以使用 GfxClearHWBuffer()
或 GfxFillHWBuffer()
函数清除整个显示区域,即使它比您的缓冲区大。后者接受一个 RGB-565 颜色值,可以使用 RGB_TO_565()
宏创建。要清除软件缓冲区,请使用 GfxClearScreen()
函数。该函数接受一个布尔参数,指定是否应更新显示。要更新显示(即,将软件缓冲区的内容复制到视频内存),请使用 GfxUpdateScreen()
函数。
您可以使用 GfxGetPixelAddress()
函数访问软件缓冲区:
unsigned short *pbuffer; GfxRetval gr; gr=g_gfx.GfxGetPixelAddress(0, 0, &pbuffer);
上面的调用将返回像素 (0,0) 的地址,即缓冲区的开头(左上角)。此类最重要的功能是:
GfxGetPixelAddress()
返回指定像素的指针。在块操作之前使用它。GfxDrawPixel()
用指定的颜色绘制一个像素。GfxGetPixelColor()
返回像素的颜色。GfxDrawLine()
在两点之间绘制一条线。GfxFillRect()
用颜色填充屏幕上的矩形。GfxGetBufferYPitch()
返回将缓冲区指针增加以向下移动一个像素所需的字节数。GfxGetWidth()
返回软件缓冲区的宽度。GfxGetHeight()
返回软件缓冲区的高度。(注意:要获取物理显示设备的宽度/高度,请使用GetSystemMetrics()
API 函数)。GfxUpdateScreen()
将把软件缓冲区的内容复制到视频内存。绘图完成后使用此函数。GfxSuspend()
将暂停视频缓冲区访问。如果您的应用程序失去输入焦点,请使用它。GfxResume()
将恢复视频缓冲区访问。
有很多方便的功能应该被添加,我同意。例如,文本输出、绘制形状或显示位图。我已经为这些任务编写了单独的例程,但它们应该集成到图形类中。
要在您的应用程序中使用此类,您必须对窗口消息处理程序进行一些修改。将 WM_PAINT
处理程序替换为以下代码:
case WM_PAINT: { if(g_gfx.GfxIsInitialized()) { ValidateRect(hWnd, 0); //no repaint } else { //gfx not yet inited, let windows draw return DefWindowProc(hWnd, message, wParam, lParam); } break; }
我还建议处理 WM_SETFOCUS
和 WM_KILLFOCUS
消息,这样,如果例如弹出低电量警告,您可以适当地暂停和恢复绘图操作:
case WM_SETFOCUS: { if(g_gfx.GfxIsInitialized()) { g_gfx.GfxResume(); g_gfx.GfxClearHWBuffer(); //clear & redraw screen g_gfx.GfxUpdateScreen(); } g_focus++; break; } case WM_KILLFOCUS: { g_focus--; g_gfx.GfxSuspend(); break; }
关于 Caleidoscope 应用程序
我提供了一个使用 Visual Studio 2005 创建的示例图形应用程序,该程序绘制对称的万花筒状图像。该程序演示了图形函数的使用。我已经注释了代码,因此应该易于阅读。该应用程序基于 Visual Studio AppWizard 创建的 Windows GUI 程序。它创建一个应用程序窗口,然后定期调用 DrawFrame()
函数,该函数渲染万花筒图像。显示的宝石实际上是小的填充矩形。宝石在屏幕左上角缓慢移动,然后将此四分之一镜像到右侧,逐像素进行。然后将屏幕的上半部分镜像到底部,逐行进行。DoMirror()
函数是一个很好的例子,说明如何使用指针进行像素和行操作,以及使用 memcpy()
进行块移动。按下按键或触摸屏幕后,应用程序将退出。它不仅应该适用于 Pocket PC 2003 和 Windows Mobile 5.0,也适用于 SmartPhone,因为它不使用任何依赖于设备的特定功能。
待办事项
图形例程可以在许多方面得到改进:
- 添加一些绘图功能(圆形、矩形等)
- 添加显示图像(JPG、BMP)的支持
- 添加绘制文本的支持
- 优化速度,可能通过重写部分 ARM 汇编代码
- 修复一些小错误
欢迎任何评论和建议。