第 35 课:主控屏幕墙与加入监控
# 第 35 课:主控屏幕墙与加入监控
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课只讲主控侧“屏幕墙/加入监控”的代码结构和检测点,不讲后台屏幕、后台桌面或后台窗口的采集实现,也不提供未授权监控操作步骤。
# 1. 先明确边界
屏幕墙不是后台屏幕,也不是后台桌面/后台窗口的一部分。它只是专栏顺序上放在屏幕类专栏之后,方便读者理解“单个屏幕窗口”和“多目标监控面板”的区别。
| 功能 | 关注点 | 本课是否讲 |
|---|---|---|
| 高速屏幕/差异屏幕/娱乐屏幕 | 被控端如何采集、编码、传输屏幕 | 不展开 |
| 后台桌面/后台窗口 | 后台桌面、窗口绘制和能力限制 | 不展开 |
| 屏幕墙/加入监控 | 主控如何把多个被控加入监控面板、定时请求缩略图并集中显示 | 本课重点 |
屏幕墙可以理解成主控侧的“多目标缩略图看板”。它关心的是:哪些被控在列表里、多久刷新一次、收到缩略图后画到哪里、退出监控时如何清理状态。

# 2. 涉及源码
| 文件 | 作用 |
|---|---|
主控\Quick\QuickView.cpp | 主连接列表右键菜单,“加入监控”“退出监控”的入口 |
主控\Quick\ScreenMonitorDlg.cpp | 屏幕墙主实现,添加/删除客户、定时请求、接收缩略图、右键操作 |
主控\Quick\ScreenMonitorDlg.h | 屏幕墙类定义和消息处理入口 |
主控\Quick\MainFrm.cpp | 创建屏幕监控停靠窗格、处理 TOKEN_MONITOR 返回 |
主控\Quick\macros.h | COMMAND_MONITOR、COMMAND_GETMONITOR、TOKEN_MONITOR、MONITOR_DLG 等常量 |
主插件\登录模块\登录模块\LoginManager.cpp | 被控登录模块收到 COMMAND_MONITOR 后回传监控令牌 |
# 3. 主控界面入口:加入监控和退出监控
主连接列表右键菜单中有“加入监控”和“退出监控”。这两个入口只负责改变屏幕墙列表和触发监控命令,不涉及后台桌面技术。
// 源码位置:主控\Quick\QuickView.cpp:373-374
// 防御分析:这是主连接列表上的屏幕墙入口。
// 它和后台屏幕/后台桌面没有直接实现关系,只是把被控加入主控侧监控面板。
mListmeau.AppendMenu(MF_STRING | MF_ENABLED, MENU_加入监控, _T("加入监控"));
mListmeau.AppendMenu(MF_STRING | MF_ENABLED, MENU_退出监控, _T("退出监控"));
2
3
4
5
菜单处理逻辑如下。
// 源码位置:主控\Quick\QuickView.cpp:1189-1207
case MENU_加入监控:
{
// 防御分析:COMMAND_MONITOR 是加入屏幕墙的启动命令。
// 它会发给当前选中的被控,要求对方进入监控回传链路。
BYTE bToken = COMMAND_MONITOR;
SendSelectCommand(&bToken, sizeof(BYTE));
log_信息("加入监控");
}
break;
case MENU_退出监控:
{
CXTPReportSelectedRows* pSelRows = wndReport->GetSelectedRows();
POSITION pSelRows_POSITION = pSelRows->GetFirstSelectedRowPosition();
while (pSelRows_POSITION)
{
CXTPReportRow* pSelRow_choose = pSelRows->GetNextSelectedRow(pSelRows_POSITION);
if (pSelRow_choose == nullptr) return;
if (pSelRow_choose->IsGroupRow()) return;
ClientContext* pContext = (ClientContext*)(((pSelRow_choose->GetRecord())->GetItem(0))->GetItemData());
if (pContext)
{
// 防御分析:退出监控不发采集命令,而是通知 ScreenMonitorDlg 删除这个上下文。
g_pScreenMonitorDlg->SendMessage(MONITOR_DLG, 0, (LPARAM)pContext);
if (pContext->Item_cmp_old_IsActive)
pContext->Item_cmp_old_IsActive->SetBackgroundColor(4294967295);
}
}
log_信息("退出监控");
}
break;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 4. 被控如何回应加入监控
COMMAND_MONITOR 到达登录模块后,登录模块会返回 TOKEN_MONITOR。这里仍然不是具体屏幕采集,只是把被控加入监控链路。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:436-454
case COMMAND_MONITOR:
{
// 防御分析:收到主控的加入监控命令后,被控回一个 TOKEN_MONITOR。
// 主控收到这个 token 后,会把对应 ClientContext 加入 ScreenMonitorDlg。
BYTE lpPacket = TOKEN_MONITOR;
Send((LPBYTE)&lpPacket, 1);
}
break;
2
3
4
5
6
7
8
9
主控网络回调收到 TOKEN_MONITOR 后,把上下文投递给屏幕墙窗口。
// 源码位置:主控\Quick\MainFrm.cpp:2027-2029
case TOKEN_MONITOR: // 屏幕监控
// 防御分析:主控收到被控确认后,交给 ScreenMonitorDlg 处理。
g_pScreenMonitorDlg->PostMessage(WM_MONITOR_CLIENT, 0, (LPARAM)pContext);
break;
2
3
4
5

# 5. ScreenMonitorDlg 如何添加客户
ScreenMonitorDlg 收到消息后,如果当前数据缓冲区没有图片,就调用 AddClient 把被控加入列表。
// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:190-203
afx_msg LRESULT CScreenMonitorDlg::OnAddClientData(WPARAM wParam, LPARAM lParam)
{
ClientContext* pContext = (ClientContext*)lParam;
if (pContext->m_DeCompressionBuffer.GetBufferLen() > 2)
{
// 防御分析:如果已经带有图片数据,就进入缩略图更新逻辑。
OnReceiveComplete(pContext);
}
else
{
// 防御分析:第一次加入时通常没有图片,先登记到屏幕墙列表。
AddClient(pContext);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AddClient 的作用是把 ClientContext 放入列表控件,标记这个连接已经属于 MONITOR_DLG,并更新状态栏计数。
// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:255-271
VOID CScreenMonitorDlg::AddClient(ClientContext* pContext)
{
// 防御分析:防止重复加入屏幕墙。
if (pContext->m_Dialog[1] == MONITOR_DLG)
return;
// 防御分析:m_Dialog[1] 是主控侧功能占用标记,后续断线和重复登录会用它判断是否在监控面板中。
pContext->m_Dialog[1] = MONITOR_DLG;
int nCnt = m_listCtrl.GetItemCount();
m_listCtrl.InsertItem(nCnt, pContext->szAddress, nCnt);
m_listCtrl.SetItemData(nCnt, (DWORD_PTR)pContext);
// 防御分析:主连接列表中把已监控目标标绿色,便于操作者识别。
if (pContext->Item_cmp_old_IsActive)
pContext->Item_cmp_old_IsActive->SetBackgroundColor(RGB(150, 212, 134));
CBitmap bmp;
bmp.LoadBitmap(IDB_BIGFIRE);
m_ImageList.Add(&bmp, RGB(255, 255, 255));
CString str;
str.Format(_T("监控:%d"), m_listCtrl.GetItemCount());
g_pFrame->m_wndStatusBar.SetPaneText(10, str);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 6. 屏幕墙如何定时请求缩略图
屏幕墙用定时器定期向列表里的被控发送 COMMAND_GETMONITOR,并附带期望缩略图宽高。
// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:407-430
void CScreenMonitorDlg::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == 1 && g_pScreenMonitorDlg->IsWindowVisible())
{
// 防御分析:包格式为 COMMAND_GETMONITOR + 宽度 + 高度。
// 这是屏幕墙缩略图请求,不等于后台桌面采集协议。
byte* lpbyte = new byte[9];
lpbyte[0] = COMMAND_GETMONITOR;
memcpy(lpbyte + 1, &m_w, 4);
memcpy(lpbyte + 5, &m_h, 4);
int nCnt = m_listCtrl.GetItemCount();
for (int i = 0; i < nCnt; i++)
{
m_listCtrl.GetItemRect(i, &m_rect, 0);
// 防御分析:只向当前可见区域内的项目请求缩略图,减少无效刷新。
if (m_rect.left > mcx)
continue;
if (m_rect.top > mcy)
continue;
ClientContext* pContext_old = (ClientContext*)m_listCtrl.GetItemData(i);
if (pContext_old)
g_pSocketBase->Send(pContext_old, lpbyte, 9);
}
}
CXTPResizeFormView::OnTimer(nIDEvent);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这段逻辑说明屏幕墙的核心不是“采集屏幕”,而是“主控侧定时向多个目标请求缩略图,然后集中显示”。
# 7. 缩略图数据如何采集
理解屏幕墙时,新手最容易漏掉一个问题:ScreenMonitorDlg::OnTimer 只是发请求,真正的缩略图数据是在被控端哪里生成的?
答案在登录模块。主控发出的 COMMAND_GETMONITOR 会进入 CLoginManager::OnReceive,登录模块读取主控给出的宽高,然后调用 GetScreen(..., setsize=true, w, h) 生成一张小尺寸当前桌面截图。也就是说,屏幕墙缩略图不是后台桌面数据,也不是差异屏幕数据,而是登录模块临时抓取的一张缩放后的当前桌面 JPEG。
![]()
先看命令处理段。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:443-460
case COMMAND_GETMONITOR:
{
int w = 300, h = 200;
// 主控屏幕墙请求包格式是:
// [0] COMMAND_GETMONITOR
// [1..4] 缩略图宽度
// [5..8] 缩略图高度
// 防御分析:宽高来自主控侧,看起来像普通参数,但它直接影响被控端截图尺寸、
// 内存分配和回传包大小。审计时要确认是否做了合理范围限制。
memcpy(&w, lpBuffer + 1, 4);
memcpy(&h, lpBuffer + 5, 4);
int screendatasize = 0;
int x = 0;
int y = 0;
// setsize=true 表示按主控给出的 w/h 生成缩略图。
// blitter=false 表示这里不是登录模块状态更新里的 1/10 小图路径。
// 防御分析:这是当前桌面快照,不是后台桌面,也不是差异屏幕的增量帧。
byte* screendata = GetScreen(screendatasize, x, y, false, true, w, h);
int SendSize = screendatasize + 5;
BYTE* lpPacket = new BYTE[SendSize];
// 回包格式:
// [0] TOKEN_MONITOR
// [1..4] JPEG 数据长度
// [5..] JPEG 缩略图数据
// 主控的 ScreenMonitorDlg::OnReceiveComplete 会跳过前 5 字节,再用 CImage 加载图片。
lpPacket[0] = TOKEN_MONITOR;
memcpy(lpPacket + 1, &screendatasize, 4);
::memcpy(lpPacket + 5, screendata, screendatasize);
Send((LPBYTE)lpPacket, SendSize);
SAFE_DELETE_AR(screendata);
SAFE_DELETE_AR(lpPacket);
}
break;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
再看 GetScreen 的核心。它没有进入后台桌面,也没有启动某个屏幕插件,而是直接对当前虚拟桌面做 GDI 截图和缩放。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:729-829
byte* CLoginManager::GetScreen(
int& size,
int& x,
int& y,
bool blitter,
bool setsize,
int setw,
int seth)
{
// 取当前桌面窗口 DC。这里对应的是当前会话里的虚拟桌面,
// 不涉及第 34 课讲的后台桌面 CreateDesktop/SwitchDesktop 链路。
HDC hCurrScreen = ::GetDC(GetDesktopWindow());
HDC hCmpDC = CreateCompatibleDC(hCurrScreen);
// 源码用 DESKTOPHORZRES/HORZRES 计算 DPI 缩放比例,
// 目的是让高 DPI 环境下的虚拟屏幕尺寸更接近真实像素尺寸。
HDC hdc = GetDC(NULL);
int client_width_now = GetDeviceCaps(hdc, HORZRES);
int client_width_old = GetDeviceCaps(hdc, DESKTOPHORZRES);
float fDpiRatio = (float)client_width_old / (float)client_width_now;
ReleaseDC(NULL, hdc);
int iScreenWidth = 0;
int iScreenHeight = 0;
if (setsize)
{
// 屏幕墙路径:主控指定缩略图宽高,例如 300x200。
// 防御分析:这会显著降低回传数据量,但仍然属于屏幕内容采集。
iScreenWidth = setw;
iScreenHeight = seth;
}
else if (blitter)
{
// 登录状态更新路径:用虚拟屏幕尺寸的 1/10 生成小图。
iScreenWidth = (int)((float)(::GetSystemMetrics(SM_CXVIRTUALSCREEN)) * fDpiRatio) / 10;
iScreenHeight = (int)((float)(::GetSystemMetrics(SM_CYVIRTUALSCREEN)) * fDpiRatio) / 10;
}
else
{
// 普通截图路径:使用完整虚拟屏幕尺寸。
iScreenWidth = (int)((float)(::GetSystemMetrics(SM_CXVIRTUALSCREEN)) * fDpiRatio);
iScreenHeight = (int)((float)(::GetSystemMetrics(SM_CYVIRTUALSCREEN)) * fDpiRatio);
}
int iScreenX = (int)((float)(::GetSystemMetrics(SM_XVIRTUALSCREEN)) * fDpiRatio);
int iScreenY = (int)((float)(::GetSystemMetrics(SM_YVIRTUALSCREEN)) * fDpiRatio);
HBITMAP hBmp = CreateCompatibleBitmap(hCurrScreen, iScreenWidth, iScreenHeight);
SelectObject(hCmpDC, hBmp);
SetStretchBltMode(hCmpDC, COLORONCOLOR);
// StretchBlt 是采集和缩放同时发生的关键点:
// 源区域是完整虚拟桌面,目标区域是 iScreenWidth/iScreenHeight。
// 防御分析:这类行为可与 GetDesktopWindow、CreateCompatibleBitmap、GetDIBits 关联。
StretchBlt(
hCmpDC,
0,
0,
iScreenWidth,
iScreenHeight,
hCurrScreen,
iScreenX,
iScreenY,
(int)((float)(::GetSystemMetrics(SM_CXVIRTUALSCREEN)) * fDpiRatio),
(int)((float)(::GetSystemMetrics(SM_CYVIRTUALSCREEN)) * fDpiRatio),
SRCCOPY);
// 后续把 HBITMAP 转成 BMP 内存,再调用 BMP2JPEG 压缩成 JPEG。
// 主控端收到后用 CImage 从内存流加载,所以网络包中不需要传复杂对象。
GetDIBits(hCmpDC, hBmp, 0, (UINT)iScreenHeight, bmpSrc, (BITMAPINFO*)&hBmpInfo, DIB_RGB_COLORS);
BMP2JPEG(szBmp, dwPicSize, jpgData);
size = (int)jpgData->length();
x = iScreenWidth;
y = iScreenHeight;
byte* data = new byte[size];
::memcpy(data, jpgData->c_str(), size);
return data;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
和正常屏幕数据采集相比,屏幕墙缩略图有相同点,也有明显差异。
| 对比项 | 屏幕墙缩略图 | 正常屏幕插件 |
|---|---|---|
| 数据来源 | 当前虚拟桌面 GDI 截图 | 通常也是当前桌面或指定桌面/窗口 |
| 触发方式 | 主控定时发 COMMAND_GETMONITOR | 打开某个屏幕插件后持续运行 |
| 图像尺寸 | 主控指定小尺寸 | 通常接近原始屏幕或插件自定义尺寸 |
| 状态维护 | 基本无状态,请求一次回一张 JPEG | 可能维护差异帧、编码器状态、交互状态 |
| 回传 token | TOKEN_MONITOR | 各屏幕插件自己的 token/协议 |
| 是否后台桌面 | 否 | 第 34 课才涉及后台桌面/后台窗口 |
| 防御含义 | 多目标、小尺寸、周期性屏幕内容采集 | 单目标、更高频或更完整的屏幕内容采集 |
所以,屏幕墙不是“另一种后台屏幕”。它更像登录模块里内置的轻量截图接口,被主控屏幕墙定时调用,用来生成多目标缩略图。
# 8. 收到缩略图后如何显示
当被控返回缩略图数据后,主控会进入 OnReceiveComplete。它从解压缓冲区中取出图片数据,加载为 CImage,再画到列表项区域。
// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:274-327
void CScreenMonitorDlg::OnReceiveComplete(ClientContext* pContext)
{
int nCnt = m_listCtrl.GetItemCount();
for (int i = 0; i < nCnt; i++)
{
ClientContext* pContext_old = (ClientContext*)m_listCtrl.GetItemData(i);
if (pContext == pContext_old)
{
// 防御分析:前 5 字节是协议头/附加信息,后面才是图片数据。
int PictureSize = pContext->m_DeCompressionBuffer.GetBufferLen() - 5;
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, PictureSize);
void* pData = GlobalLock(hGlobal);
if (!pData) return;
memcpy(pData, pContext->m_DeCompressionBuffer.GetBuffer() + 5, PictureSize);
GlobalUnlock(hGlobal);
IStream* pStream = NULL;
if (CreateStreamOnHGlobal(hGlobal, FALSE, &pStream) == S_OK)
{
CImage image;
if (SUCCEEDED(image.Load(pStream)))
{
CBitmap bmp;
HBITMAP hbmp = (HBITMAP)image.operator HBITMAP();
if (bmp.Attach(hbmp))
{
// 防御分析:把缩略图绘制到列表项矩形区域。
m_pWnd = CWnd::FromHandle(GetDlgItem(IDC_LIST)->GetSafeHwnd());
m_pDC = m_pWnd->GetDC();
m_dcimage.CreateCompatibleDC(m_pDC);
m_listCtrl.GetItemRect(i, &m_rect, 0);
m_dcimage.SelectObject(&bmp);
m_pDC->StretchBlt(
m_rect.left + 21,
m_rect.top,
m_rect.right - m_rect.left - 42,
m_rect.bottom - m_rect.top - 20,
&m_dcimage,
0,
0,
m_w,
m_h,
SRCCOPY);
// 防御分析:同时替换 ImageList 中对应缩略图,保持列表显示一致。
m_ImageList.Replace(i, &bmp, 0);
}
}
pStream->Release();
}
GlobalFree(hGlobal);
return;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 9. 退出监控如何清理
屏幕墙右键菜单也有“退出监控”。它会从列表中删除对应 ClientContext,清掉 MONITOR_DLG 标记,并更新图像列表和状态栏计数。
// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:206-235
void CScreenMonitorDlg::DelClient(ClientContext* pContext)
{
CCriSecLock recvlock(m_clcs);
int nCnt = m_listCtrl.GetItemCount();
for (int i = 0; i < nCnt; i++)
{
ClientContext* pContext_old = (ClientContext*)m_listCtrl.GetItemData(i);
if (pContext == pContext_old)
{
// 防御分析:清掉功能占用标记,说明该连接不再属于屏幕墙。
pContext_old->m_Dialog[1] = 0;
m_listCtrl.DeleteItem(i);
m_ImageList.Remove(i);
break;
}
}
// 防御分析:删除后重排 ImageList 的索引,避免缩略图错位。
for (int i = 0; i < nCnt; i++)
{
LVITEM lvItem;
lvItem.iItem = i;
lvItem.iSubItem = 0;
lvItem.mask = LVIF_IMAGE;
m_listCtrl.GetItem(&lvItem);
lvItem.iImage = i;
m_listCtrl.SetItem(&lvItem);
}
CString str;
str.Format(_T("监控:%d"), m_listCtrl.GetItemCount());
g_pFrame->m_wndStatusBar.SetPaneText(10, str);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 10. 与单窗口屏幕功能的关系
屏幕墙和单窗口屏幕插件的关系可以这样理解:
| 能力 | 屏幕墙 | 单窗口屏幕插件 |
|---|---|---|
| 目标数量 | 多目标缩略图 | 单目标详细画面 |
| 主控入口 | 加入监控、ScreenMonitorDlg | 高速屏幕、差异屏幕、娱乐屏幕等菜单 |
| 刷新方式 | 定时发 COMMAND_GETMONITOR | 插件自己的采集/编码/传输流程 |
| 主要用途 | 快速观察多个连接状态 | 深入查看和交互单个目标 |
| 本课是否讲采集原理 | 不讲 | 前面屏幕类专栏已经讲过 |
双击屏幕墙里的项目时,源码会打开差异屏幕,这说明屏幕墙更像入口和看板。
// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:450-460
void CScreenMonitorDlg::OnDclickList(NMHDR* pNMHDR, LRESULT* pResult)
{
POSITION pos = m_listCtrl.GetFirstSelectedItemPosition();
int index = m_listCtrl.GetNextSelectedItem(pos);
if (index == -1) return;
ClientContext* pContext = (ClientContext*)m_listCtrl.GetItemData(index);
if (pContext)
{
// 防御分析:双击缩略图会进入差异屏幕插件,说明屏幕墙不是详细屏幕采集实现。
SendDll(_T("差异屏幕"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 11. 企业检测点
| 层面 | 检测点 |
|---|---|
| 主控侧 | 多个目标被加入 ScreenMonitorDlg,状态栏 监控:N 变化 |
| 协议侧 | 下发 COMMAND_MONITOR 和周期性 COMMAND_GETMONITOR |
| 网络侧 | 多个被控周期性返回小尺寸图片数据 |
| 被控侧 | GetDesktopWindow、CreateCompatibleBitmap、StretchBlt、GetDIBits、JPEG 转换短时间内连续出现 |
| 隐私侧 | 即使是缩略图,也属于屏幕内容采集,必须有授权和可见提示 |
| 日志侧 | 关注“加入监控”“退出监控”“使用差异屏幕”等日志事件 |
# 12. 合法练习题
- 只读源码,画出
QuickView.cpp -> COMMAND_MONITOR -> MainFrm.cpp -> ScreenMonitorDlg的链路。 - 根据
ScreenMonitorDlg::OnTimer,说明屏幕墙请求包由哪些字段组成。 - 根据
OnReceiveComplete,说明缩略图数据从解压缓冲区到列表显示经历了哪些对象。 - 设计一条合规要求:屏幕墙监控多个终端时,必须展示哪些授权和审计信息。
- 对比
COMMAND_GETMONITOR和单窗口屏幕插件,说明为什么屏幕墙缩略图是轻量快照而不是完整远程屏幕会话。
# 13. 本课小结
屏幕墙是主控侧的多目标缩略图看板。它通过“加入监控”下发 COMMAND_MONITOR,收到 TOKEN_MONITOR 后把目标加入 ScreenMonitorDlg,再由定时器周期性发送 COMMAND_GETMONITOR 请求缩略图。缩略图在被控登录模块里通过 GetScreen 抓取当前虚拟桌面、缩放、转 JPEG 后回传,主控收到后绘制到列表项中。它不是后台屏幕或后台桌面实现的一部分,也不是差异屏幕那套增量采集链路,不能把这些能力混为一谈。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言