原文:http://www.codeproject.com/KB/mobile/Caleidoscope.aspx?msg=2056946
本文介绍SmartPhone与Pocket PC上基本的图形图像编程。我假设你是Pocket PC上图形图像新手,所以收集了你需要的资料。我写了一个示例应用程序, 它展示了如何全屏你的窗口与如何处理你手持设备的显存。我不会介绍如何用Windows GDI来处理。
我想更快的让你进入在显存中操作像素的世界,其实很简单。我知道有许多Windows Mobile上的商业与非商业的图形库。 但是它们可能是相关设备的特有支持,他们可能不能为个人免费。 我想,如果你理解了Windows Mobile图像的运行原理, 你将不再想为任何库付费。我承认有些库可能比这儿介绍的方法快, 但这些方法并没有任何速度问题。 并且,代码可以用多种方法优化。
我把所有图像处理的方法放在了ZGfx类中, 它可以很轻松的利用在别的像素处理应用程序中, 当然包括Pocket PC与SmartPhone. 当然并没有规定这个类为一个完整的图像处理库, 它仅仅是一套可以提高图形开发速度的方法。
像素,跨度,帧缓存
Pocket PC/SmartPhone并没有提供可选的显示模式, 相反,它们是固定的模式, 设备相关的显示模式。当前(2006年10月),许多Pocket PC设备只有以下分辨率:
QVGA: 240×320, 16bits每像素
VGA: 480×640, 16bits每像素
SmartPhones的则如下:
Standard: 176×220, 16bits每像素
QVGA:240×320, 16bits每像素
当然这儿还有方形屏(QGVA:240*240, VGA:480*480).这儿有几点关于像素格式的要待说明。我们将讨论16bpp格式的, 它叫着RGB-565。还有其它的像素格式,包括以前的与我坚信将来不久会出现的24bpp, 24bpp的叫关RGB-888。因此编程的时候小心了,要检测你的硬件,准备好利用设备上不同的像素模式。
但是,当前设备主要提供16bpp:5位表示红色,6位表示绿色,5位表示蓝色。16bits占两字节,它们在内存中存储方式如下:
0xffff表示白色, 0x0000表示黑色, 0xf800表示红色。是不是很简单?这儿有一个将24-bit RGB转化为RGB-565的宏。
#define RGB_TO_565(r,g,b) (word)((r*0xf8)<<8)|((g&0xfc)<<3)|((b&0xf8)>>3)
现在,显示一个像素我们还需要什么?答案是:将16-bit的值写入显存。显存又叫着帧缓存。一但你得到了帧缓存的指针,你就可以将像素值直接写入显存。 那么帧缓存的地址是什么呢? 这依赖于设备。这儿有三种方法得到需要的帧缓存地址。我把它们全部引进了,如果一种方法不提供, 别的方法还可以试一试。这些主法如下:
1. 用GDI的方法:ExtEscape(). 这个方法连同其它参数一起返回帧缓存的地址。它很简单,但是不是最好的方法, 并且好像不久将来的设备就不支持这种方法。
2. 用GAPI。GAPI是由微软提供的图形辅助库(GX.dll).它在QVGA设备上运行得很好,但是与VGA设备不兼容。(VGA:像素翻倍,缩小显示).微软好像不会再更新它了。
3. 用DirectDraw.这是最好的选择,但是它只在windwos 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个字节,指针就向下移动一个像素。其实这种猜像是错误的, 因为有不同的帧缓存操作。 我们不会在此深入讨论细节,你只需要记到以下几点:
1. 让指针指向同一行中下一个像素的字节数叫着XPitch
2. 让指针指向下一行中的一个像素的字节数叫着YPitch。(同一列,不同行)
因此,当我们计算像素(x,y)位置的指针时,你可以利用跨度值:
pxy = pbuffr + x*xpitch + y*ypitch;
让我们来看一个示例。帧缓存的指针是:0x10000000.在VGA设备上, XPitch与YPitch可能为2与960(0x3c0).此因像素(1,1)的指针为: 0x10000000 + 1*2 + 1*960 = 0x100003c2
在许多系统上, XPitch等于2(即像素的大小,以字节为单位)。但是大多数情况下,你应查询跨度的值。 利用跨度你也可以用来得到相应位置的指针, 并且只用跨度一个值。如何得到跨度值?我将在下一节指出。
实始化
要显示一些图像,我总结出几步必须的步骤:
1. 创建一个应用程序窗口
2. 让它全屏
3. 得到设备参数: 帧缓存地址与跨度
4. 写像素值到缓存中
我想第一步不用多说, 如果你需要帮助, 利用Visual Studio向导。它可以创建一个完整的窗口应用程序。
如果要让窗口全屏, 请用下面的代码:
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(). 在ZGfx类中我就是这样用的:
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.这个方法需要你很小心。1-2年前,运行Pocket PC2003 的老设备不支持这种功能。因此如果你想用这些设备,你将不能连接ddraw.dll。也就是说,你不能直接用DirectDraw,因此你通常调用其它的系统方法。 这儿有一个经典的方法来调用Windows API方法。
MessageBox(hWnd, "Text", "Caption", MB_OK);
输入一个方法名,然后接着参数。我确定你这样做已不只一次吧。结果,编译/连接器导入了一些必要的东西到你的exe文件中,这些东西就是包含了MessageBox()方法的系统.dll。如果相关.dll没有找到,Windows就不会载入你的exe。也就是说,如果exe文件包含了DirectDraw调用,而又在系统上找不到DirectDraw相关的ddraw.dll, 那么就不会运行你的exe。在WM5以前的设备上如何访问DirectDraw? 你可以通过LoadLibrary()API手动的加载DirectDraw dll文件, 然后用GetProcAddress()得到你需要方法的入口指针,最后通过该指针调用方法。不要紧张,并没有你想象的那样难:
//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,你需要DirectDraw的头文件,Windows mobile5.0sdk中的ddraw.h。在ZGfx源码的文件夹中有该文件。如果用ZGfx类,你的工程中不需要WM5 SDK,Pocket PC2003 SDK就能很好的工作。提供的示例中就用到了Pocket PC 2003 SDK)
OKay, 我们已经得到了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由微软提供的图形辅助库,相关文件是GX.dll .MSDN描述了所有的结构体与方法, 我就不再细论。初始化的时候你只需要两个方法: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设备。它提供一种像素翻倍的技术。也就是说240×320显示的可以放缩到VGA设备上480×640显示效果,但是它不支持VGA图像。
使用 ZGfx类
该类的目的就是提供基本的方法去轻松处理显存。它创建了一个软缓存,这缓存可以显示任何东西。它也用到了GAPI与DirectDraw的初始化,无认哪一个适合。你可以把软件缓存的XPitch总是等于2字节, YPitch等于宽度的2倍, 例如:480-pixel宽的为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如果DirectDraw可用的话。如果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() returns a pointer to the specified pixel. Use this before block operations.
GfxDrawPixel() draws a pixel with a specified color.
GfxGetPixelColor() returns the color of a pixel.
GfxDrawLine() draws a line between two points.
GfxFillRect() fills a rectangle on the screen with a color.
GfxGetBufferYPitch() returns the number of bytes to add to the buffer pointer to move one pixel down.
GfxGetWidth() returns the width of the software buffer.
GfxGetHeight() returns the height of the software buffer. (note: to get the width/height of the physical display device, use the GetSystemMetrics() API function).
GfxUpdateScreen() will copy the contents of the software buffer to the video memory. Use this function when drawing is done.
GfxSuspend() will suspend the video buffer access. Use if if your application loses input focus.
GfxResume() will resume the video buffer access.
还有一些方法应当加入的。比如,字体输入,画多边形,显示位图。我已写了单独的方法处理这些任务,但是没有整合到图形类中。
为了在你的程序中应用这个类,你还得在你消息处理方法中稍稍必变一点东西。用下面的代码替换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_SEPFOCUS与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 向导建立的Windows GUI程序。它创建了一个应用程序窗口,然后周期性的调用DrawFrame(), 该方法就绘制万花筒里的图像。 显示的宝石很小,用距矩形填充的。宝石在屏幕的左上方慢慢移动,在右边角落里渐像素的为左边的镜像。下半部份是上半部份渐行的镜像。DoMirror()中展示了用指针进行像素与行操作,并且用memcpy()进行了渐块移动。向上或点一下屏, 应用程序就会退出。它不光在Pocket PC 2003与Windows Mobile 5.0上运行, 它也可以在SmartPhone上运行, 因为它关没有用任何特殊设备相关的东西。
改进:
这个图像处理类可以从以下几点进行改进:
1. 加入一些绘制模块(圆,三角形,等)
2. 加入图片显示模块(jpg,bmp)
3. 加入字体显示模块
4. 优化执行效率, 可以用ARM汇编重写
5. 修改一些小bugs
还有什么建意,欢迎提出。