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:
188
client/ScreenPreview.cpp
Normal file
188
client/ScreenPreview.cpp
Normal 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, ¶ms);
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user