远程 Windows Mobile 屏幕抓取器






4.68/5 (9投票s)
通过 ActiveSync 或 WMDC 捕获你的 Windows Mobile 设备屏幕。
引言
本文介绍了一个远程 Windows Mobile 屏幕截图器的实现。根据目标平台的不同,设备屏幕可以使用 GAPI 或 DirectDraw 进行捕获。提供了一个示例桌面应用程序来显示捕获的位图,并将位图复制到剪贴板,以便可以在其他应用程序中使用。
背景
设备屏幕内容的捕获对我来说一直是个谜。利用我周末的一些闲暇时间,我决定解决这个问题,因为我的网络搜索一无所获。结果虽然不完整,但清楚地说明了两种可能的方法,并为你提供了一个免费的基于 RAPI 的工具,用于屏幕捕获会话。
Using the Code
屏幕捕获组件实现为一个 RAPI 扩展 DLL,并部署在目标设备上,通常在 \Windows 文件夹中。示例代码包括 Pocket PC 2002、2003、Windows Mobile 5(Pocket PC 和 SmartPhone)以及 Windows Mobile 6 Professional 的编译版本。
根据目标设备平台的不同,设备屏幕内容使用 GAPI(适用于 Windows CE 3.0 和 4.2)和 Direct Draw(适用于更高版本)进行捕获。
GAPI 屏幕捕获
使用 GAPI 捕获屏幕涉及获取屏幕帧缓冲区的地址并将其转换为 GDI 位图(DIB 段)。示例代码使用一个名为 CGC
的类来封装所有 GAPI 函数调用(它最初是为另一篇文章 [^] 开发的)。
屏幕捕获过程首先检索物理屏幕坐标
int nScreenX = GetSystemMetrics(SM_CXSCREEN), nScreenY = GetSystemMetrics(SM_CYSCREEN);
接下来,我们使用 gx.Open(NULL)
打开 GAPI 显示,并使用 gx.BeginDraw()
进入绘制模式。现在,我们可以访问屏幕上的原始像素数据,但必须将其存储在某处。我选择了 DIB 段,因为你可以轻松地对其进行序列化,这对于本项目很重要(我们必须以某种方式将位图数据发送到桌面)。
DIB 段代码摘自Chris Maunder 的文章评论 [^]。如你所见,Pocket PC 2002 版本(zip 中的 eVC3 文件夹)的实现略有不同,但这与本文无关。
用原始像素数据填充 DIB 段非常简单
CDIBSection dib; if(dib.CreateBitmap(nScreenX, nScreenY, (int)gx.GetBitsPerPixel())) { int x, y; WORD* pBits = (WORD*)dib.GetDIBits(); for(y = nScreenY - 1; y >= 0; --y) { for(x = 0; x < nScreenX; ++x) { *pBits++ = gx.GetPixel(x, y); } } // ... }
现在,我们必须确保 DIB 段具有正确的颜色信息。我们从 GAPI 本身获取颜色位掩码信息
DWORD dwSize; BYTE* pBuffer; BITMAPINFO* pInfo = dib.GetBitmapInfo(); pInfo->bmiHeader.biCompression = BI_BITFIELDS; DWORD dw[3]; if(gx.GetDisplayFormat() & kfDirect555) { dw[0] = 31744; // RED bitmask Bits: 0 11111 00000 00000 dw[1] = 992; // GREEN bitmask Bits: 0 00000 11111 00000 dw[2] = 31; // BLUE bitmask Bits: 0 00000 00000 11111 } else if(gx.GetDisplayFormat() & kfDirect565) { dw[0] = 0xF800; // RED bitmask Bits: 11111 000000 00000 dw[1] = 0x7E0; // GREEN bitmask Bits: 00000 111111 00000 dw[2] = 0x1F; // BLUE bitmask Bits: 00000 000000 11111 } CopyMemory(dib.GetColorTable(), dw, sizeof(DWORD) * 3);
最后,我们必须将 DIB 段数据封送回桌面
dwSize = sizeof(DIBINFO) + dib.GetImageSize(); pBuffer = (BYTE*)LocalAlloc(LPTR, dwSize); if(pBuffer) { memcpy(pBuffer, dib.GetBitmapInfo(), sizeof(DIBINFO)); memcpy(pBuffer + sizeof(DIBINFO), dib.GetDIBits(), dib.GetImageSize()); *pcbOutput = dwSize; *ppOutput = pBuffer; }
正如你所看到的,此代码仅限于 16 位像素和纵向方向。不过,应该可以轻松地扩展以处理其他分辨率。
封送 DIB 段非常简单:只需分配一个足够大的缓冲区来容纳 DIBINFO
结构和位图本身。在桌面上,此数据流将被读回另一个 CDIBSection
类实例并显示(见下文)。
DirectDraw 屏幕捕获
DirectDraw 代码遵循与 GAPI 代码相同的原理,但不是手动将显示像素提取到 DIB 段中,而是使用 BitBlt
API 将屏幕位图渲染到 DIB 段中。事实上,DirectDraw 曲面可以暴露一个 HDC
句柄,它作为 BitBlt
源效果非常好。
让我们从 DirectDraw 初始化开始回顾代码
int nScreenX = GetSystemMetrics(SM_CXSCREEN), nScreenY = GetSystemMetrics(SM_CYSCREEN); CComPtr<IDirectDraw> spDD; // Get a pointer to the Direct Draw object hr = DirectDrawCreate(NULL, &spDD, NULL);
请注意,我使用的是 CComPtr
智能指针,而不是原始 COM 接口指针,因为它会自动管理底层 COM 接口的生命周期。
现在,我们必须确定我们的 DirectDraw 会话将如何与常规 GDI 应用程序协作
hr = spDD->SetCooperativeLevel(NULL, DDSCL_NORMAL);
此级别足以进行屏幕捕获。现在,我们必须获取主曲面(屏幕内存)的指针
CComPtr<IDirectDrawSurface> spSurface; DDSURFACEDESC ddsd; ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; hr = spDD->CreateSurface(&ddsd, &spSurface, NULL);
如果此调用成功,我们就拥有了主曲面的智能指针(spSurface
),我们可以轻松地使用它将屏幕渲染到位图中。让我们看看如何做到这一点
CDIBSection dib; HDC hCaptureDC = CreateCompatibleDC(hDC); dib.CreateBitmap(nScreenX, nScreenY, 16); if(dib.GetSafeHandle() != NULL) { SelectObject(hCaptureDC, dib.GetSafeHandle()); if(BitBlt(hCaptureDC, 0, 0, nScreenX, nScreenY, hDC, 0, 0, SRCCOPY)) { // Marshal the DIB section } }
封送 DIB 段的代码与 GAPI 情况完全相同。
现在我们已经了解了如何捕获设备屏幕、将其打包到 DIB 段中,以及将其发送到桌面进行显示,现在我们可以查看桌面应用程序代码了。
桌面
桌面示例应用程序是一个 WTL 8.0 SDI 应用程序,具有非常简单的功能集:从设备请求屏幕位图、在可滚动窗格中显示它,并允许将位图复制到剪贴板以供其他应用程序使用。
所有相关的桌面代码都位于 CeScreenGrabView.h 文件中,该文件实现了 SDI 视图窗口。
首先,让我们看看如何从设备请求屏幕位图(请注意,此处未执行 RAPI 初始化和关闭)
void GrabCeScreen() { if(m_bConnect) { DWORD dwOutput; BYTE* pBuffer; HRESULT hr; hr = m_rapi.Invoke(_T("CeScreenCapture.dll"), _T("CeScreenCapture"), 0, NULL, &dwOutput, &pBuffer, NULL, 0); if(SUCCEEDED(hr)) { if(dwOutput > sizeof(DIBINFO)) { BITMAPINFO* pBitmap = (DIBINFO*)pBuffer; BYTE* pBits = pBuffer + sizeof(DIBINFO); SetScrollSize(pBitmap->bmiHeader.biWidth, pBitmap->bmiHeader.biHeight); m_dib.SetBitmap(pBitmap, pBits); Invalidate(); UpdateWindow(); } m_rapi.FreeBuffer(pBuffer); } } }
m_rapi
对象类型为 CRemoteAPI
(参见 RemoteAPI.h 和 RemoteAPI.cpp),并以与版本无关的方式封装了大多数 RAPI 函数调用。
初始化与设备的 RAPI 连接后,上述代码将动态调用 CeScreenCapture.dll 模块中的 CeScreenCapture
函数。请注意,为了使此代码正常工作,DLL 必须位于设备上的 \Windows 文件夹中。
返回后,pBuffer
包含序列化的 DIB 段,该段被重新组装到一个 CDIBSection
对象(m_dib
)中,然后显示
void DoPaint(CDCHandle dc) { HBITMAP hBmp = m_dib; if(hBmp) { CDC dcMem; dcMem.CreateCompatibleDC(dc); dcMem.SelectBitmap(hBmp); dc.BitBlt(0, 0, m_dib.GetWidth(), m_dib.GetHeight(), dcMem, 0, 0, SRCCOPY); } }
再简单不过了……那么复制到剪贴板功能呢?在这里
void CopyToClipboard() { if(m_dib.GetSafeHandle() == NULL) return; if(OpenClipboard()) { CDC dcMemSrc, dcMemTgt; HDC hDC = GetDC(); HBITMAP hBmp; dcMemSrc.CreateCompatibleDC(hDC); dcMemTgt.CreateCompatibleDC(hDC); hBmp = CreateCompatibleBitmap(hDC, m_dib.GetWidth(), m_dib.GetHeight()); dcMemSrc.SelectBitmap(m_dib.GetSafeHandle()); dcMemTgt.SelectBitmap(hBmp); dcMemTgt.BitBlt(0, 0, m_dib.GetWidth(), m_dib.GetHeight(), dcMemSrc, 0, 0, SRCCOPY); EmptyClipboard(); SetClipboardData(CF_BITMAP, hBmp); CloseClipboard(); } }
如你所见,代码只是将现有的 DIB 段复制到一个 HBITMAP
并将其设置为剪贴板数据。当不再需要该位图时,剪贴板将释放它。
限制
此代码未经广泛测试,也未在大量设备上暴露。正如我上面所说,该代码仅限于具有 16 位像素的设备,尽管扩展到其他像素格式应该不难。
历史
- 2008-02-11 - 首次发布。