From 6c32b478afff06f109cde5d5fbc725d7a5884d80 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Wed, 13 May 2026 13:05:31 +0200 Subject: [PATCH] Feature: Add "Play Snapshot" loop preview windows for online hosts --- server/2015Remote/2015Remote.rc | Bin 154782 -> 155232 bytes server/2015Remote/2015RemoteDlg.cpp | 324 +++++++++++++++++- server/2015Remote/2015RemoteDlg.h | 33 +- server/2015Remote/2015Remote_vs2015.vcxproj | 2 + .../2015Remote_vs2015.vcxproj.filters | 2 + server/2015Remote/PreviewTipWnd.cpp | 235 +++++++++++-- server/2015Remote/PreviewTipWnd.h | 30 +- server/2015Remote/lang/en_US.ini | 9 + server/2015Remote/lang/zh_TW.ini | 9 + server/2015Remote/res/Bitmap/Snapshot.bmp | Bin 0 -> 822 bytes server/2015Remote/res/Snapshot.ico | Bin 0 -> 5430 bytes server/2015Remote/resource.h | 8 +- server/2015Remote/stdafx.h | 1 + 13 files changed, 603 insertions(+), 50 deletions(-) create mode 100644 server/2015Remote/res/Bitmap/Snapshot.bmp create mode 100644 server/2015Remote/res/Snapshot.ico diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index 6d15c2d9bdceb1c423320677c56b78a0bc557193..16de7d4c49d0b71b98b5a5bdd0fc77b746b81d89 100644 GIT binary patch delta 176 zcmbQYlk>qI&J8ETCd={h@cJ=0G6XOLGk7rgGlWe3DCo@}%#g>B$WXvg%#gv5&rmXX zBA@hR1xdcR-2$BZ_z&I~~eu0ZGqGzP>9WAFr$;SBK% zJ`DcT|F30~oV-DZWBP@Kj3V3CGS)1c9`J`z3}^|8#nV4BGlg>_**x8!g-LvSO)!%Z FCjjDgF~tA? delta 34 qcmaE`hjZRe&J8ETCa;s^Yfcg0o+8euHEp}fI>wr1+wDS`lsEwai470{ diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index bf81fc6..a0147a3 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -88,6 +88,7 @@ #define TIMER_STATUSBAR_UPDATE 6 #define TIMER_STATUSBAR_INIT 7 #define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用") +#define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定) #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" @@ -98,6 +99,7 @@ struct OfflineInfo { CString ip; std::string aliveInfo; bool hasLogin; + uint64_t clientId; // 用于通知 UI 关闭循环快照窗口 }; // DLL 请求限流配置 (缓存,避免频繁读取注册表) @@ -621,6 +623,7 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl m_bmOnline[51].LoadBitmap(IDB_BITMAP_TRIGGER); m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP); m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG); + m_bmOnline[54].LoadBitmap(IDB_BITMAP_SNAPSHOT); // "播放快照" 菜单的眼睛图标 for (int i = 0; i < PAYLOAD_MAXTYPE; i++) { m_ServerDLL[i] = nullptr; @@ -812,6 +815,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient) ON_MESSAGE(WM_UPDATE_ACTIVEWND, UpdateUserEvent) ON_MESSAGE(WM_PREVIEW_RESPONSE, OnPreviewResponse) + ON_MESSAGE(WM_PREVIEW_LOOP_CLOSED, OnLoopTipDestroyed) ON_WM_HELPINFO() ON_COMMAND(ID_ONLINE_SHARE, &CMy2015RemoteDlg::OnOnlineShare) ON_COMMAND(ID_TOOL_AUTH, &CMy2015RemoteDlg::OnToolAuth) @@ -897,6 +901,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_COMMAND(ID_CANCEL_SHARE, &CMy2015RemoteDlg::OnCancelShare) ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl) ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun) + ON_COMMAND(ID_SCREENPREVIEW_LOOP, &CMy2015RemoteDlg::OnScreenpreviewLoop) END_MESSAGE_MAP() @@ -3058,6 +3063,9 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent) m_pPreviewTip->MarkPreviewUnavailable(); } } + if (nIDEvent == TIMER_PREVIEW_LOOP) { + TickLoopTips(); + } if (nIDEvent == TIMER_CLEAR_BALLOON) { KillTimer(TIMER_CLEAR_BALLOON); @@ -3405,6 +3413,7 @@ void CMy2015RemoteDlg::Release() UninitFileUpload(); DeletePopupWindow(TRUE); + CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口 isClosed = TRUE; ShowWindow(SW_HIDE); @@ -3673,6 +3682,7 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult) Menu.SetMenuItemBitmaps(ID_ONLINE_HOSTNOTE, MF_BYCOMMAND, &m_bmOnline[5], &m_bmOnline[5]); Menu.SetMenuItemBitmaps(ID_ONLINE_VIRTUAL_DESKTOP, MF_BYCOMMAND, &m_bmOnline[6], &m_bmOnline[6]); Menu.SetMenuItemBitmaps(ID_ONLINE_GRAY_DESKTOP, MF_BYCOMMAND, &m_bmOnline[7], &m_bmOnline[7]); + Menu.SetMenuItemBitmaps(ID_SCREENPREVIEW_LOOP, MF_BYCOMMAND, &m_bmOnline[54], &m_bmOnline[54]); Menu.SetMenuItemBitmaps(ID_ONLINE_REMOTE_DESKTOP, MF_BYCOMMAND, &m_bmOnline[8], &m_bmOnline[8]); Menu.SetMenuItemBitmaps(ID_ONLINE_H264_DESKTOP, MF_BYCOMMAND, &m_bmOnline[9], &m_bmOnline[9]); Menu.SetMenuItemBitmaps(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND, &m_bmOnline[10], &m_bmOnline[10]); @@ -4549,6 +4559,7 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject) // Copy info before removing (context will be freed after this function returns) info->hWnd = ContextObject->hWnd; info->ip = ContextObject->GetClientData(ONLINELIST_IP); + info->clientId = ContextObject->GetClientID(); auto tm = ContextObject->GetAliveTime(); info->aliveInfo = tm >= 86400 ? floatToString(tm / 86400.f) + " d" : tm >= 3600 ? floatToString(tm / 3600.f) + " h" : @@ -5477,11 +5488,16 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject); break; case TOKEN_SCREEN_PREVIEW_RSP: { - // 屏幕预览响应:把整个包拷到堆上,转给主线程处理(IO 线程不接触 UI) + // 屏幕预览响应:把整个包 + 来源 clientId 一并堆分配,转给主线程处理 + // (IO 线程不接触 UI;clientId 用于循环快照路由到对应窗口)。 + // 不用 WPARAM 携带 ID:32 位 Windows 上 WPARAM 是 32 位,会截 64 位 clientID。 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; + auto* msg = new CMy2015RemoteDlg::PreviewRspMsg{ + ContextObject->GetClientID(), + std::vector(szBuffer, szBuffer + len) + }; + if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)msg)) { + delete msg; } break; } @@ -5887,6 +5903,11 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam) Mprintf("%s 主机下线 [%s]\n", info->ip.GetString(), info->aliveInfo.c_str()); } + // 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。 + if (info->clientId != 0) { + CloseLoopTip(info->clientId); + } + // Close child dialog window HWND p = info->hWnd; delete info; @@ -7736,35 +7757,59 @@ void CMy2015RemoteDlg::SendScreenPreviewRequest(context* ctx, WORD reqId, WORD m } // 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程) -// wParam: 未使用(避免依赖 WPARAM 宽度,Win32 上 32 位会截 64 位 clientID) -// LPARAM: 指向堆分配的 std::vector*(含完整响应包:[Header|JPEG]),处理完必须 delete。 -// 校验思路:reqId 由对话框单调递增、每次双击重置 + 同时只有 1 个 in-flight tip,足以 -// 过滤过期/乱串响应,无需再叠加 clientID 校验。 +// wParam: 未使用 +// LPARAM: 指向堆分配的 PreviewRspMsg*(含 clientId + 完整响应包:[Header|JPEG]), +// 处理完必须 delete。clientId 走载荷传递的原因见 PreviewRspMsg 注释。 +// +// 派发策略(双路径): +// 1) 若 clientId 命中 m_LoopTips("播放快照"在该主机上开着)→ 路由到对应循环窗口; +// 2) 否则走既有单发流程(双击主机弹出的浮窗,由 m_pPreviewTip / m_PreviewReqId 控制)。 LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam) { - auto* pkt = reinterpret_cast*>(lParam); - if (!pkt) return 0; - std::unique_ptr> guard(pkt); + std::unique_ptr msg(reinterpret_cast(lParam)); + if (!msg) return 0; - if (pkt->size() < sizeof(ScreenPreviewRspHeader)) return 0; - const ScreenPreviewRspHeader* hdr = reinterpret_cast(pkt->data()); + const std::vector& pkt = msg->packet; + if (pkt.size() < sizeof(ScreenPreviewRspHeader)) return 0; + const ScreenPreviewRspHeader* hdr = reinterpret_cast(pkt.data()); if (hdr->token != TOKEN_SCREEN_PREVIEW_RSP) return 0; + bool dataOk = + hdr->status == SCREEN_PREVIEW_OK && + hdr->bytes > 0 && + pkt.size() >= sizeof(ScreenPreviewRspHeader) + hdr->bytes && + hdr->format == SCREEN_PREVIEW_FMT_JPEG; + const BYTE* jpeg = dataOk ? pkt.data() + sizeof(ScreenPreviewRspHeader) : nullptr; + + // ---------- 路径 1:循环快照(按 clientId 命中)---------- + auto it = m_LoopTips.find(msg->clientId); + if (it != m_LoopTips.end()) { + LoopTipEntry& entry = it->second; + // 校验 reqId:每条目独立计数,过滤过期/串扰响应 + if (entry.expectedReqId == 0 || hdr->reqId != entry.expectedReqId) return 0; + if (!entry.tip || !::IsWindow(entry.tip->GetSafeHwnd())) return 0; + + if (dataOk) { + entry.tip->SetImageFromJpeg(jpeg, hdr->bytes); + } else { + // 单帧失败不直接关窗,标"不可用",下一轮定时器再尝试 + entry.tip->MarkPreviewUnavailable(); + } + return 0; + } + + // ---------- 路径 2:既有单发流程 ---------- // 序号校验:和当前期待的 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) - { + if (!dataOk) { m_pPreviewTip->MarkPreviewUnavailable(); return 0; } - const BYTE* jpeg = pkt->data() + sizeof(ScreenPreviewRspHeader); m_pPreviewTip->SetImageFromJpeg(jpeg, hdr->bytes); // 图到达后续命 5 秒 @@ -7773,6 +7818,179 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam) return 0; } +// ========== "播放快照" 循环模式:核心实现 ========== +// +// 线程模型:所有 m_LoopTips 的读写都在 UI 线程发生: +// - OnScreenpreviewLoop(菜单) +// - OnTimer(TIMER_PREVIEW_LOOP) +// - OnPreviewResponse(消息泵派发,UI 线程) +// - OnUserOfflineMsg(同上) +// - OnLoopTipDestroyed(同上) +// - Release() / CloseAllLoopTips(窗口销毁阶段,UI 线程) +// 因此 m_LoopTips 自身不需要锁。涉及 context* 的访问全部走 FindHost(clientId), +// 由 m_cs 保护,避免与 IO 线程的上下线并发踩坑。 +// +// CPreviewTipWnd(循环模式)的生命周期: +// - 创建:OpenLoopTip 中 new;SetLoopOwner 后窗口在 OnDestroy 会回告 owner。 +// - 销毁路径 A(用户主动 / 我们关掉):CloseLoopTip 先 erase 表项再调用 +// SetLoopOwner(NULL,...) 拆除回调,再 DestroyWindow;PostNcDestroy 自删对象。 +// - 销毁路径 B(用户从 UI 直接关掉窗口 —— 当前没有暴露关闭按钮,但保留兼容): +// OnDestroy 触发 WM_PREVIEW_LOOP_CLOSED → OnLoopTipDestroyed 擦表; +// PostNcDestroy 自删对象。 + +void CMy2015RemoteDlg::OpenLoopTip(context* ctx, CPoint anchor) +{ + if (!ctx) return; + uint64_t cid = ctx->GetClientID(); + if (m_LoopTips.find(cid) != m_LoopTips.end()) return; // 已存在,直接返回 + + // 构造简短显示文本(不复用 OnListClick 的完整版,避免逻辑重复 / 文本过长) + // 标签走 _TRF 走多语言;IP/ID 是通用术语不翻译。 + CString computer = ctx->GetClientData(ONLINELIST_COMPUTER_NAME); + CString ip = ctx->GetClientData(ONLINELIST_IP); + CString reso = ctx->GetAdditionalData(RES_RESOLUTION); + CString text; + text.FormatL(_T("%s %s\r\nIP: %s\r\n%s %s\r\nID: %llu"), + _TRF("主机:"), computer.IsEmpty() ? _T("(unknown)") : computer.GetString(), + ip.IsEmpty() ? _T("(unknown)") : ip.GetString(), + _TRF("分辨率:"), reso.IsEmpty() ? _T("-") : reso.GetString(), + (unsigned long long)cid); + + // MBCS → wide(与 OnListClick 单发路径一致的转换方式) + CStringW textW; + int wlen = MultiByteToWideChar(CP_ACP, 0, text.GetString(), -1, NULL, 0); + if (wlen > 1) { + MultiByteToWideChar(CP_ACP, 0, text.GetString(), -1, textW.GetBufferSetLength(wlen - 1), wlen); + textW.ReleaseBuffer(wlen - 1); + } + + // 选择预览参数(与单发同口径) + WORD maxWidth = 480; + BYTE jpegQ = 70; + ChooseScreenPreviewParams(ctx, maxWidth, jpegQ); + + // 标题栏 / 任务栏文本:"<快照> - ()",方便多窗口区分 + CString titleA; + titleA.FormatL(_T("%s - %s (%s)"), + _TRF("快照"), + computer.IsEmpty() ? _T("(unknown)") : computer.GetString(), + ip.IsEmpty() ? _T("(unknown)") : ip.GetString()); + CStringW titleW; + { + int twlen = MultiByteToWideChar(CP_ACP, 0, titleA.GetString(), -1, NULL, 0); + if (twlen > 1) { + MultiByteToWideChar(CP_ACP, 0, titleA.GetString(), -1, titleW.GetBufferSetLength(twlen - 1), twlen); + titleW.ReleaseBuffer(twlen - 1); + } + } + + auto* tip = new CPreviewTipWnd(); + if (!tip->Create(this, anchor, textW, maxWidth, /*loopMode*/true, titleW)) { + delete tip; + return; + } + tip->SetLoopOwner(GetSafeHwnd(), WM_PREVIEW_LOOP_CLOSED, cid); + + LoopTipEntry entry; + entry.tip = tip; + entry.maxWidth = maxWidth; + entry.jpegQuality = jpegQ; + entry.expectedReqId = 0; // SendLoopRequest 中自增并写入 + m_LoopTips.emplace(cid, entry); + + // 第一次请求立刻发,避免 3 秒空等 + SendLoopRequest(cid, m_LoopTips[cid]); + + // 表非空时启动循环定时器(已经在跑就不重启,避免漂移) + if (m_LoopTips.size() == 1) { + SetTimer(TIMER_PREVIEW_LOOP, LOOP_INTERVAL_MS, nullptr); + } +} + +void CMy2015RemoteDlg::CloseLoopTip(uint64_t clientID) +{ + auto it = m_LoopTips.find(clientID); + if (it == m_LoopTips.end()) return; + + CPreviewTipWnd* tip = it->second.tip; + // 先 erase 表项,避免:DestroyWindow → OnDestroy → PostMessage 回 OnLoopTipDestroyed + // 重入时再 find 到自己。Post 即使发出,到达时 find 不到也会被静默丢弃。 + m_LoopTips.erase(it); + + if (tip && ::IsWindow(tip->GetSafeHwnd())) { + // 显式拆掉 owner 回调,避免下面 DestroyWindow 又触发一次(虽然 erase 已防住, + // 但留下未送达的 WM_PREVIEW_LOOP_CLOSED 仍会泄漏一个 new uint64_t)。 + tip->SetLoopOwner(nullptr, 0, 0); + tip->DestroyWindow(); + // 对象由 PostNcDestroy 自删 + } + + if (m_LoopTips.empty()) { + KillTimer(TIMER_PREVIEW_LOOP); + } +} + +void CMy2015RemoteDlg::CloseAllLoopTips() +{ + // 拷贝键集合再逐个关闭,避免迭代中修改 m_LoopTips + std::vector ids; + ids.reserve(m_LoopTips.size()); + for (const auto& kv : m_LoopTips) ids.push_back(kv.first); + for (uint64_t id : ids) CloseLoopTip(id); +} + +void CMy2015RemoteDlg::SendLoopRequest(uint64_t clientID, LoopTipEntry& entry) +{ + // 自增本条目的 reqId(0 跳过,与单发流程一致) + if (++m_LoopReqId == 0) m_LoopReqId = 1; + entry.expectedReqId = m_LoopReqId; + + // 锁内拿 ctx 并立刻发送:避免 UI 线程持锁时间过长,但 Send2Client 是上下文内部线程 + // 安全的(每个 context 有自己的写入串行化),等价于把同步 IO 委派给底层; + // 在锁内调用是为了防止刚拿到的 ctx 被 OfflineProc 同时 RemoveFromHostList。 + CLock L(m_cs); + context* ctx = FindHostNoLock(clientID); + if (!ctx) return; + SendScreenPreviewRequest(ctx, entry.expectedReqId, entry.maxWidth, entry.jpegQuality); +} + +void CMy2015RemoteDlg::TickLoopTips() +{ + if (m_LoopTips.empty()) { + KillTimer(TIMER_PREVIEW_LOOP); + return; + } + // 拷贝键集合:极少数情况下 SendLoopRequest 内部可能引起表项失效(比如未来扩展), + // 安全起见用快照迭代。 + std::vector ids; + ids.reserve(m_LoopTips.size()); + for (const auto& kv : m_LoopTips) ids.push_back(kv.first); + + for (uint64_t id : ids) { + auto it = m_LoopTips.find(id); + if (it == m_LoopTips.end()) continue; + if (!it->second.tip || !::IsWindow(it->second.tip->GetSafeHwnd())) continue; + SendLoopRequest(id, it->second); + } +} + +LRESULT CMy2015RemoteDlg::OnLoopTipDestroyed(WPARAM /*wParam*/, LPARAM lParam) +{ + std::unique_ptr pId(reinterpret_cast(lParam)); + if (!pId) return 0; + uint64_t cid = *pId; + + // 仅擦表项:窗口已自销毁路径中,CWnd 对象由 PostNcDestroy 自删 + auto it = m_LoopTips.find(cid); + if (it != m_LoopTips.end()) { + m_LoopTips.erase(it); + if (m_LoopTips.empty()) { + KillTimer(TIMER_PREVIEW_LOOP); + } + } + return 0; +} + void CMy2015RemoteDlg::OnOnlineUnauthorize() { @@ -9859,3 +10077,73 @@ void CMy2015RemoteDlg::OnWebRemoteControl() MessageBoxL("如需Web远程桌面跨网使用方案,请联系管理员!", "提示", MB_ICONINFORMATION); } } + +// "播放快照"菜单响应: +// - 单条 / 多条选中:每条做 toggle —— 已在循环中则关闭,否则打开。 +// - 不支持 SCREEN_PREVIEW 能力位的客户端:跳过(弹个状态栏提示,避免静默)。 +// - 多选时浮窗按 (30,30) 瀑布偏移叠放,避免完全重叠。 +// - 锁策略:先在 m_cs 内取出 ctx 指针快照后立即放锁;UI 后续操作(Create/Open) +// 不再持锁,但 OpenLoopTip 内会再次锁内 FindHost / Send,对齐离线竞态。 +void CMy2015RemoteDlg::OnScreenpreviewLoop() +{ + // 收集选中行索引 + std::vector selectedItems; + { + CLock L(m_cs); + POSITION Pos = m_CList_Online.GetFirstSelectedItemPosition(); + while (Pos) { + selectedItems.push_back(m_CList_Online.GetNextSelectedItem(Pos)); + } + } + if (selectedItems.empty()) return; + + CPoint origin; + GetCursorPos(&origin); + + int offsetIdx = 0; + int notSupported = 0; + for (int iItem : selectedItems) { + // 锁内拿 ctx 和 clientId(避免与 OfflineProc 竞态导致 ctx 失效) + // 注意:ctx 出锁后理论上可能被 IO 线程清掉。我们仅用它做"立刻打开浮窗 + + // 发首帧",所有耗时操作(Send2Client / 浮窗构造)都在 OpenLoopTip 内再次 + // 锁内做 FindHost,是 belt-and-suspenders。 + context* ctxSnap = nullptr; + uint64_t cid = 0; + bool supports = false; + { + CLock L(m_cs); + ctxSnap = GetContextByListIndex(iItem); + if (ctxSnap) { + cid = ctxSnap->GetClientID(); + supports = ctxSnap->SupportsScreenPreview(); + } + } + if (!ctxSnap || cid == 0) continue; + + // toggle:已存在则关闭 + if (m_LoopTips.find(cid) != m_LoopTips.end()) { + CloseLoopTip(cid); + continue; + } + + if (!supports) { + ++notSupported; + continue; + } + + // 出锁后再次 FindHost,OpenLoopTip 内部还会用一次。多次查找成本低,但能消 + // 除"刚获得 ctx 立刻被下线"这个窗口期。 + context* ctx = FindHost(cid); + if (!ctx) continue; + + CPoint anchor = origin + CSize(30 * offsetIdx, 30 * offsetIdx); + OpenLoopTip(ctx, anchor); + ++offsetIdx; + } + + if (notSupported > 0) { + CString msg; + msg.FormatL(_TR("有 %d 个主机不支持屏幕预览,已跳过"), notSupported); + ShowMessage(_TR("提示"), msg); + } +} diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index fb45046..523f99b 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -234,6 +234,36 @@ public: // 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。 class CPreviewTipWnd* m_pPreviewTip = nullptr; WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应 + + // 屏幕预览响应消息载荷(PostMessage WM_PREVIEW_RESPONSE 的 LPARAM 指向它)。 + // IO 线程在 MessageHandle/TOKEN_SCREEN_PREVIEW_RSP 分支堆分配,UI 线程消费后释放。 + // 把 clientId 放进来是为了:1) 循环快照场景按 clientId 路由到目标窗口; + // 2) 避免依赖 WPARAM —— 32 位 Windows 上 WPARAM 是 32 位,截 64 位 clientID。 + struct PreviewRspMsg { + uint64_t clientId; + std::vector packet; + }; + + // "播放快照"循环模式:每个表项对应一台主机的浮窗 + 调度状态。 + // 仅 UI 线程访问(菜单 / OnTimer / OnPreviewResponse / OnUserOfflineMsg / + // OnLoopTipDestroyed / Release),不加锁;context* 走 FindHost 在 m_cs 下取。 + struct LoopTipEntry { + class CPreviewTipWnd* tip = nullptr; + WORD expectedReqId = 0; // 上次发请求时写入;响应时校验 + WORD maxWidth = 480; + BYTE jpegQuality = 70; + }; + std::map m_LoopTips; + WORD m_LoopReqId = 0; // 循环快照专用 reqId;与 m_PreviewReqId 解耦 + static const int LOOP_INTERVAL_MS = 3000; // 循环快照间隔(暂定 3 秒) + + // 循环快照帮助函数(仅 UI 线程调用) + void OpenLoopTip(class context* ctx, CPoint anchor); + void CloseLoopTip(uint64_t clientID); + void CloseAllLoopTips(); + void TickLoopTips(); + void SendLoopRequest(uint64_t clientID, LoopTipEntry& entry); + afx_msg LRESULT OnLoopTipDestroyed(WPARAM wParam, LPARAM lParam); // 记录 clientID(心跳更新) std::set m_DirtyClients; // 待处理的上线/下线事件(批量更新减少闪烁) @@ -286,7 +316,7 @@ public: bool IsDllRequestLimited(const std::string& ip); void RecordDllRequest(const std::string& ip); CMenu m_MainMenu; - CBitmap m_bmOnline[54]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons + CBitmap m_bmOnline[55]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons + 1 snapshot uint64_t m_superID; std::map m_RemoteWnds; FileTransformCmd m_CmdList; @@ -520,4 +550,5 @@ public: afx_msg void OnCancelShare(); afx_msg void OnWebRemoteControl(); afx_msg void OnProxyPortAutorun(); + afx_msg void OnScreenpreviewLoop(); }; diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj b/server/2015Remote/2015Remote_vs2015.vcxproj index a22b13b..45c6567 100644 --- a/server/2015Remote/2015Remote_vs2015.vcxproj +++ b/server/2015Remote/2015Remote_vs2015.vcxproj @@ -562,6 +562,7 @@ + @@ -589,6 +590,7 @@ + diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj.filters b/server/2015Remote/2015Remote_vs2015.vcxproj.filters index 97523df..20c26e6 100644 --- a/server/2015Remote/2015Remote_vs2015.vcxproj.filters +++ b/server/2015Remote/2015Remote_vs2015.vcxproj.filters @@ -212,6 +212,7 @@ + @@ -245,6 +246,7 @@ + diff --git a/server/2015Remote/PreviewTipWnd.cpp b/server/2015Remote/PreviewTipWnd.cpp index d18caf7..02f9650 100644 --- a/server/2015Remote/PreviewTipWnd.cpp +++ b/server/2015Remote/PreviewTipWnd.cpp @@ -1,6 +1,7 @@ // PreviewTipWnd.cpp #include "stdafx.h" #include "PreviewTipWnd.h" +#include "resource.h" // IDI_ICON_SNAPSHOT(循环模式标题栏图标) #include // IUnknown / IStream — gdiplus.h 依赖它们已声明 #include @@ -21,6 +22,9 @@ constexpr int LOADING_H = 270; BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd) ON_WM_PAINT() ON_WM_ERASEBKGND() + ON_WM_DESTROY() + ON_WM_SIZE() + ON_WM_GETMINMAXINFO() END_MESSAGE_MAP() CPreviewTipWnd::CPreviewTipWnd() = default; @@ -28,11 +32,15 @@ CPreviewTipWnd::CPreviewTipWnd() = default; CPreviewTipWnd::~CPreviewTipWnd() { // m_image 通过 unique_ptr 自动释放 + if (m_hIconSmall) { ::DestroyIcon(m_hIconSmall); m_hIconSmall = nullptr; } + if (m_hIconBig) { ::DestroyIcon(m_hIconBig); m_hIconBig = nullptr; } } -BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW) +BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW, + bool loopMode, const CStringW& windowTitle) { m_text = text; + m_loopMode = loopMode; // 注意:影响后续 SetImageFromJpeg / OnPaint / OnSize 行为 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) { @@ -53,25 +61,60 @@ BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, HFONT hF = ::CreateFontIndirectW(&lf); if (hF) m_font.Attach(hF); - // 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容 + // 窗口类:单发 tooltip 走 CS_SAVEBITS(短时显示,省 BitBlt), + // 循环窗口是长存窗口,加 CS_HREDRAW|CS_VREDRAW 让拉伸时实时重绘 + UINT classStyle = loopMode ? (CS_HREDRAW | CS_VREDRAW) : CS_SAVEBITS; LPCTSTR kClass = AfxRegisterWndClass( - CS_SAVEBITS, + classStyle, ::LoadCursor(NULL, IDC_ARROW), - (HBRUSH)(COLOR_INFOBK + 1), + (HBRUSH)(COLOR_BTNFACE + 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); + + // 样式选择: + // 单发 tooltip —— WS_POPUP|WS_BORDER + EX_TOPMOST|TOOLWINDOW|NOACTIVATE + // (不激活、不上任务栏、置顶;光标移开会被主对话框关掉) + // 循环监视窗口 —— WS_OVERLAPPEDWINDOW (标题栏 / 系统菜单 / 最大化 / 最小化 / 可调整边框) + // + WS_EX_APPWINDOW(强制独立任务栏图标,便于多窗口切换) + DWORD style = loopMode ? (WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN) + : (WS_POPUP | WS_BORDER); + DWORD styleEx = loopMode ? WS_EX_APPWINDOW + : (WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE); + + // 单发模式下窗口文本无意义(无标题栏可显示);循环模式下用宽字符 SetWindowTextW 显式设。 + BOOL bOk = CWnd::CreateEx(styleEx, kClass, _T(""), style, rc, pParent, 0); if (!bOk) return FALSE; + if (loopMode && !windowTitle.IsEmpty()) { + ::SetWindowTextW(GetSafeHwnd(), windowTitle); + } + + // 循环模式:给窗口装上眼睛图标(标题栏 + 任务栏 + Alt-Tab)。 + // ICO 资源里包含 16x16 与 32x32 两份,ICON_SMALL/ICON_BIG 各取最佳尺寸。 + // HICON 由本类持有,析构时 DestroyIcon(WM_SETICON 不转移所有权)。 + if (loopMode) { + m_hIconSmall = (HICON)::LoadImage(AfxGetInstanceHandle(), + MAKEINTRESOURCE(IDI_ICON_SNAPSHOT), IMAGE_ICON, + ::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON), + LR_DEFAULTCOLOR); + m_hIconBig = (HICON)::LoadImage(AfxGetInstanceHandle(), + MAKEINTRESOURCE(IDI_ICON_SNAPSHOT), IMAGE_ICON, + ::GetSystemMetrics(SM_CXICON), ::GetSystemMetrics(SM_CYICON), + LR_DEFAULTCOLOR); + if (m_hIconSmall) SendMessage(WM_SETICON, ICON_SMALL, (LPARAM)m_hIconSmall); + if (m_hIconBig) SendMessage(WM_SETICON, ICON_BIG, (LPARAM)m_hIconBig); + } + RecalcLayoutAndResize(); - ShowWindow(SW_SHOWNOACTIVATE); + // 单发:不激活;循环:正常显示,允许接收焦点(系统菜单 / 拖拽 / 最小化都需要可激活) + ShowWindow(loopMode ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE); + if (loopMode) { + // 任务栏图标 + 标题在多窗口情况下立刻可见 + UpdateWindow(); + } return TRUE; } @@ -102,7 +145,11 @@ void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes) m_image = std::move(bmp); m_hasImage = true; m_unavailable = false; - RecalcLayoutAndResize(); + // 循环模式下窗口尺寸交给用户控制(OnSize / WS_THICKFRAME),后续帧不再自动重排版, + // 只 Invalidate 触发重绘;图像在 OnPaint 中按 client rect 等比例适配。 + if (!m_loopMode) { + RecalcLayoutAndResize(); + } if (GetSafeHwnd()) Invalidate(); } @@ -113,6 +160,44 @@ void CPreviewTipWnd::MarkPreviewUnavailable() if (GetSafeHwnd()) Invalidate(); } +void CPreviewTipWnd::SetLoopOwner(HWND ownerHwnd, UINT msgId, uint64_t clientId) +{ + // 仅更新回调订阅;m_loopMode 由 Create() 一次性设定,不在这里改 —— 否则 + // CloseLoopTip 用 SetLoopOwner(NULL,0,0) 解订阅时会顺手把 loopMode 翻成 false, + // 导致 PostNcDestroy 跳过 delete this → 每关一次循环窗口泄漏一个对象。 + m_loopOwner = ownerHwnd; + m_loopMsg = msgId; + m_clientId = clientId; +} + +void CPreviewTipWnd::OnDestroy() +{ + // 在窗口实际销毁前回告 owner(仅循环模式)。owner 据此从映射表擦掉本条目。 + // 注意:把字段先置零,避免极端情况下被 PostNcDestroy 再触发一次。 + if (m_loopMode && m_loopOwner && ::IsWindow(m_loopOwner) && m_loopMsg != 0) { + HWND owner = m_loopOwner; + UINT msg = m_loopMsg; + m_loopOwner = nullptr; + m_loopMsg = 0; + // 64 位 clientId 用 LPARAM 指针传递;WPARAM 在 32 位 Windows 不够宽。 + auto* pId = new uint64_t(m_clientId); + if (!::PostMessageA(owner, msg, 0, (LPARAM)pId)) { + delete pId; + } + } + CWnd::OnDestroy(); +} + +void CPreviewTipWnd::PostNcDestroy() +{ + // 仅循环模式自管理生命周期;单次提示窗口由主对话框 SAFE_DELETE 释放。 + bool selfDelete = m_loopMode; + CWnd::PostNcDestroy(); + if (selfDelete) { + delete this; + } +} + void CPreviewTipWnd::RecalcLayoutAndResize() { HWND hWnd = GetSafeHwnd(); @@ -181,6 +266,7 @@ void CPreviewTipWnd::OnPaint() CPaintDC pdc(this); CRect rcClient; GetClientRect(&rcClient); + if (rcClient.IsRectEmpty()) return; // 双缓冲 CDC memDC; @@ -189,41 +275,115 @@ void CPreviewTipWnd::OnPaint() memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height()); CBitmap* oldBmp = memDC.SelectObject(&memBmp); - memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK)); + // 单发 tooltip 用 COLOR_INFOBK(系统提示底色);循环窗口用 COLOR_BTNFACE(普通窗口背景) + COLORREF bg = m_loopMode ? ::GetSysColor(COLOR_BTNFACE) : ::GetSysColor(COLOR_INFOBK); + memDC.FillSolidRect(&rcClient, bg); CFont* oldFont = memDC.SelectObject(&m_font); - memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT)); + memDC.SetTextColor(::GetSysColor(m_loopMode ? COLOR_WINDOWTEXT : 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; + if (m_loopMode) { + // 循环模式:基于 client rect 动态分配;图像区按 client 大小自适应,文本固定在底部 + CRect rcImg, rcText; + LayoutForLoopMode(rcClient, rcImg, rcText); + if (!rcImg.IsRectEmpty()) DrawImageArea(memDC, rcImg); + if (!rcText.IsRectEmpty()) DrawTextArea(memDC, rcText); + } else { + // 单发 tooltip:保持原行为,按 RecalcLayoutAndResize 算好的固定尺寸排版 + 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); } - 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::LayoutForLoopMode(const CRect& client, CRect& outImg, CRect& outText) +{ + outImg.SetRectEmpty(); + outText.SetRectEmpty(); + + int innerW = client.Width() - 2 * PADDING; + if (innerW < 50) return; // 太窄,啥也别画,避免计算崩溃 + + // 计算文本以当前可用宽度换行后的高度(用一份临时 DC) + int textH = 0; + { + CClientDC dc(this); + CFont* old = dc.SelectObject(&m_font); + CRect rcCalc(0, 0, innerW, 32767); + ::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcCalc, + DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX); + textH = rcCalc.Height(); + dc.SelectObject(old); + } + + // 文本固定在底部 + outText.SetRect(client.left + PADDING, + client.bottom - PADDING - textH, + client.right - PADDING, + client.bottom - PADDING); + + // 图像区占顶部剩余空间 + outImg.SetRect(client.left + PADDING, + client.top + PADDING, + client.right - PADDING, + outText.top - IMAGE_TEXT_GAP); + + // 高度不足以放图像(被压扁)→ 只画文本 + if (outImg.Height() < 60) { + outImg.SetRectEmpty(); + // 文本提到中间,避免压在底部边缘 + outText.MoveToY(client.top + PADDING); + } +} + void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc) { + if (rc.IsRectEmpty()) return; + // 边框 dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW)); + CRect rcInner = rc; + rcInner.DeflateRect(1, 1); + 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); + if (m_loopMode) { + // 循环模式:保留长宽比适配 rect,黑色背景充当 letterbox 留白 + dc.FillSolidRect(&rcInner, RGB(32, 32, 32)); + double imgW = (double)m_image->GetWidth(); + double imgH = (double)m_image->GetHeight(); + if (imgW > 0 && imgH > 0 && rcInner.Width() > 0 && rcInner.Height() > 0) { + double sx = rcInner.Width() / imgW; + double sy = rcInner.Height() / imgH; + double scale = sx < sy ? sx : sy; + int drawW = (int)(imgW * scale + 0.5); + int drawH = (int)(imgH * scale + 0.5); + int x = rcInner.left + (rcInner.Width() - drawW) / 2; + int y = rcInner.top + (rcInner.Height() - drawH) / 2; + Graphics g(dc.GetSafeHdc()); + g.SetInterpolationMode(InterpolationModeHighQualityBicubic); + g.SetSmoothingMode(SmoothingModeHighQuality); + g.DrawImage(m_image.get(), x, y, drawW, drawH); + } + } else { + // 单发:与原行为一致,铺满 rc + 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 ..."; @@ -231,10 +391,29 @@ void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc) 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)); + dc.SetTextColor(::GetSysColor(m_loopMode ? COLOR_WINDOWTEXT : COLOR_INFOTEXT)); } } +void CPreviewTipWnd::OnSize(UINT nType, int cx, int cy) +{ + CWnd::OnSize(nType, cx, cy); + if (m_loopMode && GetSafeHwnd()) { + // 循环模式:所有排版交给 OnPaint 读取 client rect,重画即可 + Invalidate(); + } +} + +void CPreviewTipWnd::OnGetMinMaxInfo(MINMAXINFO* lpMMI) +{ + if (m_loopMode && lpMMI) { + // 防止用户把窗口拽到肉眼不可见。240x200 大致能保证标题栏 + 一行文本 + 缩略图可见。 + lpMMI->ptMinTrackSize.x = 240; + lpMMI->ptMinTrackSize.y = 200; + } + CWnd::OnGetMinMaxInfo(lpMMI); +} + void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc) { RECT r = rc; diff --git a/server/2015Remote/PreviewTipWnd.h b/server/2015Remote/PreviewTipWnd.h index 8ce01d5..0a4431e 100644 --- a/server/2015Remote/PreviewTipWnd.h +++ b/server/2015Remote/PreviewTipWnd.h @@ -20,7 +20,12 @@ public: // text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染) // imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局) // 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本) - BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW); + // loopMode true: 真正的可拖拽/缩放/最小化/最大化的独立窗口("播放快照"循环), + // 带标题栏 + 任务栏图标,关闭按钮走系统菜单; + // false: 单发 tooltip(双击主机的预览),无 chrome、不激活、置顶。 + // windowTitle loopMode=true 时显示在标题栏与任务栏的文本;false 时忽略。 + BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW, + bool loopMode = false, const CStringW& windowTitle = L""); // 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。 void SetImageFromJpeg(const BYTE* data, size_t bytes); @@ -30,15 +35,28 @@ public: WORD GetReqId() const { return m_reqId; } void SetReqId(WORD id) { m_reqId = id; } + // 循环快照模式:开启后窗口销毁时会向 ownerHwnd 发送 msgId(LPARAM 是堆上的 + // uint64_t* 携带 clientId,接收方负责 delete),且 PostNcDestroy 会自动 delete this。 + // 调用方需在自行销毁前用 SetLoopOwner(NULL, 0, 0) 解除回调,避免重复通知。 + void SetLoopOwner(HWND ownerHwnd, UINT msgId, uint64_t clientId); + uint64_t GetClientID() const { return m_clientId; } + bool IsLoopMode() const { return m_loopMode; } + protected: afx_msg void OnPaint(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); + afx_msg void OnDestroy(); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnGetMinMaxInfo(MINMAXINFO* lpMMI); + virtual void PostNcDestroy(); DECLARE_MESSAGE_MAP() private: void RecalcLayoutAndResize(); void DrawImageArea(CDC& dc, const CRect& rc); void DrawTextArea(CDC& dc, const CRect& rc); + // 循环模式:基于当前 client rect 重新分配图像区/文本区 + void LayoutForLoopMode(const CRect& client, CRect& outImg, CRect& outText); CStringW m_text; int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位) @@ -54,4 +72,14 @@ private: CFont m_font; WORD m_reqId = 0; + + // 循环快照模式相关 + bool m_loopMode = false; + HWND m_loopOwner = nullptr; + UINT m_loopMsg = 0; + uint64_t m_clientId = 0; + // 标题栏 / 任务栏图标。WM_SETICON 不转移所有权(MSDN:originator 必须自己释放), + // 析构时 DestroyIcon;不能在 OnDestroy 里销毁,因为系统在 WM_NCDESTROY 之前还在用它。 + HICON m_hIconSmall = nullptr; + HICON m_hIconBig = nullptr; }; diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index 159e2b6..33ac27f 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -1836,3 +1836,12 @@ IOCP Ƽ: macOS install.sh װ (űԶǩ)=Recommended: Copy to macOS and run install.sh (the script re-signs automatically). ֶǩ:=Or re-sign manually: <ı滻Զ̼а>= + +; Screen Preview Loop - English Translation +; Format: Simplified Chinese=English + +:=Host: +ֱ:=Resolution: + %d ֧ĻԤ=%d host(s) do not support screen preview, skipped +ſ=Play Snapshot +=Snapshot diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index 8973c0e..d8d17c0 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1827,3 +1827,12 @@ IOCP Ƽ: macOS install.sh װ (űԶǩ)=]: }u macOS install.sh b (_Ԅº) ֶǩ:=քº: <ı滻Զ̼а>=<ı滻Զ̼а> + +; Screen Preview Loop - Traditional Chinese Translation +; Format: Simplified Chinese=Traditional Chinese + +:=C: +ֱ:=: + %d ֧ĻԤ= %d C֧ԮΞĻA[^ +ſ=ſ += diff --git a/server/2015Remote/res/Bitmap/Snapshot.bmp b/server/2015Remote/res/Bitmap/Snapshot.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f28d44874a28aa539eb9b34e9db42d03f978c6ac GIT binary patch literal 822 zcmZ?rHDhJ~12Z700mK4O%*Y@C76%bW_#hZ2@L{lm>({ThwY5b@N9*hBYrp`ITUAwc z;lc$pS65e8J2^Scnl$&){R{P+(8K<@J8%RN0kGcqz>zI+MP1tfv&Wy_Yql>Gnv z;_l6DlY!{n{R?oxJ$v>5?RxU$Nl#BtZ*MPL{LSr?+luwK}4KwE$&14*FYfr77JzXk?%V`C#cR9CNF4YVsIC1vvD$%ppuJ+gTr5FJ0f mAIJq7Wo>N@G!w2E&e*^Q-`3L_Sz7*Ubgi*HGUe9tCITbC8MIl8za`m=3 zt@YX@V{N*cu9Z=n4x96pDe{;wM}Hn;iM1>(Lwic|HL272eZKE|Y4*K^`E|^*Fpq~UG0SJ7D9pc>Atolq zl$@OWT1H03S)0u^W3gCBB6z8(sf}i{8KE(N-EMEr%*>psudko+`Fs=%21yIxH8(d? zc6N3kD=X{s=;-L>*aQ^sO-)UgwDMD&6f!Y2~qL!@jGg2 zYG$+ozh3ZB`}PDn@w7$7k#n6|5n)SudipW;>Bqsr!8vn%oc1 zBPQx{<<60roSY=qeF<@aZ(2C@Vir|yxP#a|fm`ogE$q8*y{DiF)=X!eR7?i za(>*;KDjs-`Vbq5;BhWF^z#4rcYI7H#vDe}BLOHpi?A4hYa{~bE8+Ua)pU{d+X zL9R7k#v$3iB<1-L%rR{a$vR{&`Zu=l9wy!o`X;0fJ>^$tlz3RSsd3jV@TT5+iZG@gA`++F@p$FmGjxaaVmlg5Vp_fqtrJ&6`=EHy69BX9)Vx? zo}|g~pXIcHqNgK%a(|`=wKnj(L(VlcG#J)v8PMx?#8I8oM*E$23Vgl5O71N-I@A8H zR@5-=?(W9jLc{_&3LB84JVx$PV`HOXjh6BLt}>ywwcM=Uf8<^0vNH`q-P)dsuSxN;+C{kFX7K*zvH1kR#z>U|>M%N8E>?KT@8Lk6xtehi3PO z-w!0-qUz=&6l2`yWK0kz#48f;9ao;iS?Krs{b9xXJ6g!|Xso(F?ypj>q&F;GqR#Ix zxSPB!p0NAj4iBBc&>2L`J$zrtap~V(tCt(}ue@=oI=`>LOg*jPzavc!`ne`N2s=7Ykr}@7A>JxT@erNivs{WmuE%ej3U&-YLid+x*FKQ8w#ToE@J**A; zDmKuE+cW9U%d`JKZ$7(;;?}GXDE|WxpJSzFs@ZTr$gL}8xRsH>Mm#hBDbNS>c3+;AT>HBZ`^~;4{{C^<*o<^$XI+8^eXv)PUozf99NSWP-qPQND6h$3{q>9!(vE*Zc|GduRrxoA iEsRBsF5c5SAM%`=`IV5xSuW(?;{0L+79;TAM&NIh_P{6r literal 0 HcmV?d00001 diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index 5f41c56..70531e7 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -255,6 +255,8 @@ #define IDR_MACOS_GHOST 372 #define IDB_BITMAP_WEBDESKTOP 373 #define IDB_BITMAP_PLUGINCONFIG 374 +#define IDB_BITMAP_SNAPSHOT 375 +#define IDI_ICON_SNAPSHOT 376 #define IDR_LANG_EN_US 380 #define IDR_LANG_ZH_TW 381 #define IDC_MESSAGE 1000 @@ -974,14 +976,16 @@ #define ID_33046 33046 #define ID_PROXY_PORT_AUTORUN 33047 #define ID_TRIGGER_SETTINGS 33048 +#define ID_33048 33048 +#define ID_SCREENPREVIEW_LOOP 33049 #define ID_EXIT_FULLSCREEN 40001 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 373 -#define _APS_NEXT_COMMAND_VALUE 33048 +#define _APS_NEXT_RESOURCE_VALUE 377 +#define _APS_NEXT_COMMAND_VALUE 33050 #define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_SYMED_VALUE 105 #endif diff --git a/server/2015Remote/stdafx.h b/server/2015Remote/stdafx.h index c76ec09..55a0992 100644 --- a/server/2015Remote/stdafx.h +++ b/server/2015Remote/stdafx.h @@ -102,6 +102,7 @@ #define WM_DISCONNECT WM_USER+3032 #define WM_OPENTERMINALDIALOG WM_USER+3033 #define WM_PREVIEW_RESPONSE WM_USER+3034 +#define WM_PREVIEW_LOOP_CLOSED WM_USER+3035 #ifdef _UNICODE #if defined _M_IX86