Feature: Add "Play Snapshot" loop preview windows for online hosts
This commit is contained in:
@@ -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<BYTE>(szBuffer, szBuffer + len);
|
||||
if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)pkt)) {
|
||||
delete pkt;
|
||||
auto* msg = new CMy2015RemoteDlg::PreviewRspMsg{
|
||||
ContextObject->GetClientID(),
|
||||
std::vector<BYTE>(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<BYTE>*(含完整响应包:[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<std::vector<BYTE>*>(lParam);
|
||||
if (!pkt) return 0;
|
||||
std::unique_ptr<std::vector<BYTE>> guard(pkt);
|
||||
std::unique_ptr<PreviewRspMsg> msg(reinterpret_cast<PreviewRspMsg*>(lParam));
|
||||
if (!msg) return 0;
|
||||
|
||||
if (pkt->size() < sizeof(ScreenPreviewRspHeader)) return 0;
|
||||
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(pkt->data());
|
||||
const std::vector<BYTE>& pkt = msg->packet;
|
||||
if (pkt.size() < sizeof(ScreenPreviewRspHeader)) return 0;
|
||||
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(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);
|
||||
|
||||
// 标题栏 / 任务栏文本:"<快照> - <PCName> (<IP>)",方便多窗口区分
|
||||
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<uint64_t> 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<uint64_t> 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<uint64_t> pId(reinterpret_cast<uint64_t*>(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<int> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user