在现代的移动设备使用中,远程控制功能变得越来越重要。本文将介绍如何开发一个桌面应用,通过该应用可以远程控制Windows Mobile设备。将使用鼠标和键盘来操作设备,实现远程控制。
本文的代码基于之前的文章《远程Windows Mobile屏幕抓取器》。与之前不同的是,这个应用实现了一个流式RAPI服务器,允许桌面应用与设备保持永久连接。此外,本文中放弃了GAPI和DirectDraw屏幕抓取技术,转而使用更简单的基于GDI的屏幕抓取技术。为了提高通信性能,代码在两端都使用了ZLIB压缩库。
桌面端的代码主要集中在CeRemoteClientView.h文件中。这是一个WTL 8.0框架子窗口,实现了所有桌面客户端功能。
设备连接是通过公共方法Connect()和Disconnect()实现的,这些方法由框架窗口类的菜单和工具栏处理程序调用。当桌面成功连接到设备时,会创建一个200毫秒的定时器,用于轮询设备的压缩屏幕位图。
私有方法GetScreen()用于获取设备屏幕。首先停止定时器,然后向设备服务器发送消息请求当前屏幕:
        KillTimer(SCREEN_TIMER);
        hr = Write(RCM_GETSCREEN);
    
设备服务器返回的消息包含相同的消息代码、屏幕缓冲区的压缩大小、展开大小和压缩字节流。读取前三个DWORD后,代码确保压缩和展开缓冲区有足够的空间,然后读取压缩字节流:
        hr = m_pStream->Read(m_pZipBuf, cbZipBuf, &ulRead);
    
如果一切顺利,压缩缓冲区将被解压缩,生成的DIB将查询位图尺寸。如果尺寸与上次不同,那么很可能设备屏幕被旋转了,因此整个窗口将被无效化以清除任何垃圾:
        zr = uncompress(m_pScrBuf, &nDestLen, m_pZipBuf, cbZipBuf);
        if (zr == Z_OK) {
            DIBINFO* pDibInfo = (DIBINFO*)m_pScrBuf;
            BYTE* pBmpData = (BYTE*)(m_pScrBuf + sizeof(DIBINFO));
            BOOL bErase = FALSE;
            if (m_xDevScr != pDibInfo->bmiHeader.biWidth || m_yDevScr != pDibInfo->bmiHeader.biHeight) {
                m_xDevScr = pDibInfo->bmiHeader.biWidth;
                m_yDevScr = pDibInfo->bmiHeader.biHeight;
                SetScrollSize(m_xDevScr, m_yDevScr);
                bErase = TRUE;
            }
            m_dib.SetBitmap((BITMAPINFO*)pDibInfo, pBmpData);
            InvalidateRect(NULL, bErase);
            UpdateWindow();
        }
    
在强制窗口更新后,定时器重新启动,以便可以获取下一个屏幕。
向设备发送键盘和鼠标输入非常简单:处理相应的窗口消息,将它们的内容转换为INPUT结构,并将它们发送到服务器进行处理。以下是WM_KEYDOWN处理程序:
        LRESULT OnKeyDown(TCHAR vk, UINT cRepeat, UINT flags) {
            HRESULT hr;
            INPUT input;
            if (!m_bConnected) return 0;
            input.type = INPUT_KEYBOARD;
            input.ki.wVk = MapKey(vk);
            input.ki.wScan = 0;
            input.ki.dwFlags = 0;
            input.ki.time = 0;
            input.ki.dwExtraInfo = 0;
            hr = Write(RCM_SETINPUT);
            if (SUCCEEDED(hr)) {
                hr = Write(&input, sizeof(input));
                if (SUCCEEDED(hr)) GetScreen();
            }
            return 0;
        }
    
MapKey()函数执行桌面和设备键盘之间的基本键位映射。使用F1键作为左功能按钮,F2键作为右功能按钮。F3和F4键自然映射到电话键。
发送鼠标动作类似:
        LRESULT OnLButtonDown(UINT Flags, CPoint pt) {
            HRESULT hr;
            INPUT input;
            if (!m_bConnected) return 0;
            m_bLeftBtn = true;
            input.type = INPUT_MOUSE;
            input.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_LEFTDOWN;
            input.mi.dx = pt.x * 65536 / m_xDevScr;
            input.mi.dy = pt.y * 65536 / m_yDevScr;
            input.mi.mouseData = 0;
            input.mi.time = 0;
            input.mi.dwExtraInfo = 0;
            hr = Write(RCM_SETINPUT);
            if (SUCCEEDED(hr)) {
                hr = Write(&input, sizeof(input));
                if (SUCCEEDED(hr)) hr = GetScreen();
            }
            return 0;
        }
    
注意鼠标屏幕坐标是如何为设备屏幕标准化的。这是设备服务器上使用的SendInput API的要求。
设备端的代码主要集中在CRemoteControl类中。所有由桌面客户端发送的消息都在Run方法中实现的执行循环中处理和分派。
设备屏幕的捕获由SendScreen方法完成,其结构与桌面端类似。注意设备屏幕是如何轻松被捕获的:
        hDC = GetWindowDC(NULL);
    
获取设备屏幕的HDC后,可以很容易地将其复制到位图中,并序列化到桌面。无需使用之前使用的GAPI或DirectDraw技术。
将设备屏幕复制到DIB后,整个内容被压缩并发送回桌面客户端:
        memcpy(m_pScrBuf + i, m_dib.GetBitmapInfo(), sizeof(DIBINFO));
        i += sizeof(DIBINFO);
        memcpy(m_pScrBuf + i, m_dib.GetDIBits(), m_dib.GetImageSize());
        i += m_dib.GetImageSize();
        ULONG len = m_cbZipBuf;
        int zr = compress(m_pZipBuf, &len, m_pScrBuf, cbNew);
        if (zr != Z_OK) len = 0;
        hr = m_pStream->Write(&dwMsg, sizeof(DWORD), &ulWritten);
        hr = m_pStream->Write(&len, sizeof(ULONG), &ulWritten);
        hr = m_pStream->Write(&cbNew, sizeof(DWORD), &ulWritten);
        hr = m_pStream->Write(m_pZipBuf, len, &ulWritten);
    
处理桌面输入非常简单:
        HRESULT CRemoteControl::GetInput() {
            INPUT input;
            HRESULT hr;
            DWORD dwRead;
            hr = m_pStream->Read(&input, sizeof(input), &dwRead);
            if (FAILED(hr)) return hr;
            if (dwRead != sizeof(input)) return E_FAIL;
            SendInput(1, &input, sizeof(input));
            return S_OK;
        }
    
有两个有趣的点可能想知道:是如何模拟双击鼠标事件的,以及为什么这段代码在WM5和WM6设备上不能直接工作。