在现代的移动设备使用中,远程控制功能变得越来越重要。本文将介绍如何开发一个桌面应用,通过该应用可以远程控制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设备上不能直接工作。