远程 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 - 首次发布。


