Feature: Add "Play Snapshot" loop preview windows for online hosts
This commit is contained in:
Binary file not shown.
@@ -88,6 +88,7 @@
|
|||||||
#define TIMER_STATUSBAR_UPDATE 6
|
#define TIMER_STATUSBAR_UPDATE 6
|
||||||
#define TIMER_STATUSBAR_INIT 7
|
#define TIMER_STATUSBAR_INIT 7
|
||||||
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
#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 TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION);
|
||||||
#define TINY_DLL_NAME "TinyRun.dll"
|
#define TINY_DLL_NAME "TinyRun.dll"
|
||||||
#define FRPC_DLL_NAME "Frpc.dll"
|
#define FRPC_DLL_NAME "Frpc.dll"
|
||||||
@@ -98,6 +99,7 @@ struct OfflineInfo {
|
|||||||
CString ip;
|
CString ip;
|
||||||
std::string aliveInfo;
|
std::string aliveInfo;
|
||||||
bool hasLogin;
|
bool hasLogin;
|
||||||
|
uint64_t clientId; // 用于通知 UI 关闭循环快照窗口
|
||||||
};
|
};
|
||||||
|
|
||||||
// DLL 请求限流配置 (缓存,避免频繁读取注册表)
|
// DLL 请求限流配置 (缓存,避免频繁读取注册表)
|
||||||
@@ -621,6 +623,7 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
|
|||||||
m_bmOnline[51].LoadBitmap(IDB_BITMAP_TRIGGER);
|
m_bmOnline[51].LoadBitmap(IDB_BITMAP_TRIGGER);
|
||||||
m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP);
|
m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP);
|
||||||
m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG);
|
m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG);
|
||||||
|
m_bmOnline[54].LoadBitmap(IDB_BITMAP_SNAPSHOT); // "播放快照" 菜单的眼睛图标
|
||||||
|
|
||||||
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
||||||
m_ServerDLL[i] = nullptr;
|
m_ServerDLL[i] = nullptr;
|
||||||
@@ -812,6 +815,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
|
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
|
||||||
ON_MESSAGE(WM_UPDATE_ACTIVEWND, UpdateUserEvent)
|
ON_MESSAGE(WM_UPDATE_ACTIVEWND, UpdateUserEvent)
|
||||||
ON_MESSAGE(WM_PREVIEW_RESPONSE, OnPreviewResponse)
|
ON_MESSAGE(WM_PREVIEW_RESPONSE, OnPreviewResponse)
|
||||||
|
ON_MESSAGE(WM_PREVIEW_LOOP_CLOSED, OnLoopTipDestroyed)
|
||||||
ON_WM_HELPINFO()
|
ON_WM_HELPINFO()
|
||||||
ON_COMMAND(ID_ONLINE_SHARE, &CMy2015RemoteDlg::OnOnlineShare)
|
ON_COMMAND(ID_ONLINE_SHARE, &CMy2015RemoteDlg::OnOnlineShare)
|
||||||
ON_COMMAND(ID_TOOL_AUTH, &CMy2015RemoteDlg::OnToolAuth)
|
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_CANCEL_SHARE, &CMy2015RemoteDlg::OnCancelShare)
|
||||||
ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl)
|
ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl)
|
||||||
ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun)
|
ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun)
|
||||||
|
ON_COMMAND(ID_SCREENPREVIEW_LOOP, &CMy2015RemoteDlg::OnScreenpreviewLoop)
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
|
||||||
@@ -3058,6 +3063,9 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
|||||||
m_pPreviewTip->MarkPreviewUnavailable();
|
m_pPreviewTip->MarkPreviewUnavailable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (nIDEvent == TIMER_PREVIEW_LOOP) {
|
||||||
|
TickLoopTips();
|
||||||
|
}
|
||||||
if (nIDEvent == TIMER_CLEAR_BALLOON) {
|
if (nIDEvent == TIMER_CLEAR_BALLOON) {
|
||||||
KillTimer(TIMER_CLEAR_BALLOON);
|
KillTimer(TIMER_CLEAR_BALLOON);
|
||||||
|
|
||||||
@@ -3405,6 +3413,7 @@ void CMy2015RemoteDlg::Release()
|
|||||||
|
|
||||||
UninitFileUpload();
|
UninitFileUpload();
|
||||||
DeletePopupWindow(TRUE);
|
DeletePopupWindow(TRUE);
|
||||||
|
CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口
|
||||||
isClosed = TRUE;
|
isClosed = TRUE;
|
||||||
ShowWindow(SW_HIDE);
|
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_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_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_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_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_H264_DESKTOP, MF_BYCOMMAND, &m_bmOnline[9], &m_bmOnline[9]);
|
||||||
Menu.SetMenuItemBitmaps(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND, &m_bmOnline[10], &m_bmOnline[10]);
|
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)
|
// Copy info before removing (context will be freed after this function returns)
|
||||||
info->hWnd = ContextObject->hWnd;
|
info->hWnd = ContextObject->hWnd;
|
||||||
info->ip = ContextObject->GetClientData(ONLINELIST_IP);
|
info->ip = ContextObject->GetClientData(ONLINELIST_IP);
|
||||||
|
info->clientId = ContextObject->GetClientID();
|
||||||
auto tm = ContextObject->GetAliveTime();
|
auto tm = ContextObject->GetAliveTime();
|
||||||
info->aliveInfo = tm >= 86400 ? floatToString(tm / 86400.f) + " d" :
|
info->aliveInfo = tm >= 86400 ? floatToString(tm / 86400.f) + " d" :
|
||||||
tm >= 3600 ? floatToString(tm / 3600.f) + " h" :
|
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);
|
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
case TOKEN_SCREEN_PREVIEW_RSP: {
|
case TOKEN_SCREEN_PREVIEW_RSP: {
|
||||||
// 屏幕预览响应:把整个包拷到堆上,转给主线程处理(IO 线程不接触 UI)
|
// 屏幕预览响应:把整个包 + 来源 clientId 一并堆分配,转给主线程处理
|
||||||
|
// (IO 线程不接触 UI;clientId 用于循环快照路由到对应窗口)。
|
||||||
|
// 不用 WPARAM 携带 ID:32 位 Windows 上 WPARAM 是 32 位,会截 64 位 clientID。
|
||||||
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
||||||
auto* pkt = new std::vector<BYTE>(szBuffer, szBuffer + len);
|
auto* msg = new CMy2015RemoteDlg::PreviewRspMsg{
|
||||||
if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)pkt)) {
|
ContextObject->GetClientID(),
|
||||||
delete pkt;
|
std::vector<BYTE>(szBuffer, szBuffer + len)
|
||||||
|
};
|
||||||
|
if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)msg)) {
|
||||||
|
delete msg;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -5887,6 +5903,11 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
|
|||||||
Mprintf("%s 主机下线 [%s]\n", info->ip.GetString(), info->aliveInfo.c_str());
|
Mprintf("%s 主机下线 [%s]\n", info->ip.GetString(), info->aliveInfo.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。
|
||||||
|
if (info->clientId != 0) {
|
||||||
|
CloseLoopTip(info->clientId);
|
||||||
|
}
|
||||||
|
|
||||||
// Close child dialog window
|
// Close child dialog window
|
||||||
HWND p = info->hWnd;
|
HWND p = info->hWnd;
|
||||||
delete info;
|
delete info;
|
||||||
@@ -7736,35 +7757,59 @@ void CMy2015RemoteDlg::SendScreenPreviewRequest(context* ctx, WORD reqId, WORD m
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程)
|
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程)
|
||||||
// wParam: 未使用(避免依赖 WPARAM 宽度,Win32 上 32 位会截 64 位 clientID)
|
// wParam: 未使用
|
||||||
// LPARAM: 指向堆分配的 std::vector<BYTE>*(含完整响应包:[Header|JPEG]),处理完必须 delete。
|
// LPARAM: 指向堆分配的 PreviewRspMsg*(含 clientId + 完整响应包:[Header|JPEG]),
|
||||||
// 校验思路:reqId 由对话框单调递增、每次双击重置 + 同时只有 1 个 in-flight tip,足以
|
// 处理完必须 delete。clientId 走载荷传递的原因见 PreviewRspMsg 注释。
|
||||||
// 过滤过期/乱串响应,无需再叠加 clientID 校验。
|
//
|
||||||
|
// 派发策略(双路径):
|
||||||
|
// 1) 若 clientId 命中 m_LoopTips("播放快照"在该主机上开着)→ 路由到对应循环窗口;
|
||||||
|
// 2) 否则走既有单发流程(双击主机弹出的浮窗,由 m_pPreviewTip / m_PreviewReqId 控制)。
|
||||||
LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
||||||
{
|
{
|
||||||
auto* pkt = reinterpret_cast<std::vector<BYTE>*>(lParam);
|
std::unique_ptr<PreviewRspMsg> msg(reinterpret_cast<PreviewRspMsg*>(lParam));
|
||||||
if (!pkt) return 0;
|
if (!msg) return 0;
|
||||||
std::unique_ptr<std::vector<BYTE>> guard(pkt);
|
|
||||||
|
|
||||||
if (pkt->size() < sizeof(ScreenPreviewRspHeader)) return 0;
|
const std::vector<BYTE>& pkt = msg->packet;
|
||||||
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(pkt->data());
|
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;
|
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 不符 → 过期响应,丢弃
|
// 序号校验:和当前期待的 reqId 不符 → 过期响应,丢弃
|
||||||
if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0;
|
if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0;
|
||||||
if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0;
|
if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0;
|
||||||
|
|
||||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||||
|
|
||||||
if (hdr->status != SCREEN_PREVIEW_OK || hdr->bytes == 0 ||
|
if (!dataOk) {
|
||||||
pkt->size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes ||
|
|
||||||
hdr->format != SCREEN_PREVIEW_FMT_JPEG)
|
|
||||||
{
|
|
||||||
m_pPreviewTip->MarkPreviewUnavailable();
|
m_pPreviewTip->MarkPreviewUnavailable();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BYTE* jpeg = pkt->data() + sizeof(ScreenPreviewRspHeader);
|
|
||||||
m_pPreviewTip->SetImageFromJpeg(jpeg, hdr->bytes);
|
m_pPreviewTip->SetImageFromJpeg(jpeg, hdr->bytes);
|
||||||
|
|
||||||
// 图到达后续命 5 秒
|
// 图到达后续命 5 秒
|
||||||
@@ -7773,6 +7818,179 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
|||||||
return 0;
|
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()
|
void CMy2015RemoteDlg::OnOnlineUnauthorize()
|
||||||
{
|
{
|
||||||
@@ -9859,3 +10077,73 @@ void CMy2015RemoteDlg::OnWebRemoteControl()
|
|||||||
MessageBoxL("如需Web远程桌面跨网使用方案,请联系管理员!", "提示", MB_ICONINFORMATION);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -234,6 +234,36 @@ public:
|
|||||||
// 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
|
// 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
|
||||||
class CPreviewTipWnd* m_pPreviewTip = nullptr;
|
class CPreviewTipWnd* m_pPreviewTip = nullptr;
|
||||||
WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应
|
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<BYTE> 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<uint64_t, LoopTipEntry> 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(心跳更新)
|
// 记录 clientID(心跳更新)
|
||||||
std::set<uint64_t> m_DirtyClients;
|
std::set<uint64_t> m_DirtyClients;
|
||||||
// 待处理的上线/下线事件(批量更新减少闪烁)
|
// 待处理的上线/下线事件(批量更新减少闪烁)
|
||||||
@@ -286,7 +316,7 @@ public:
|
|||||||
bool IsDllRequestLimited(const std::string& ip);
|
bool IsDllRequestLimited(const std::string& ip);
|
||||||
void RecordDllRequest(const std::string& ip);
|
void RecordDllRequest(const std::string& ip);
|
||||||
CMenu m_MainMenu;
|
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;
|
uint64_t m_superID;
|
||||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||||
FileTransformCmd m_CmdList;
|
FileTransformCmd m_CmdList;
|
||||||
@@ -520,4 +550,5 @@ public:
|
|||||||
afx_msg void OnCancelShare();
|
afx_msg void OnCancelShare();
|
||||||
afx_msg void OnWebRemoteControl();
|
afx_msg void OnWebRemoteControl();
|
||||||
afx_msg void OnProxyPortAutorun();
|
afx_msg void OnProxyPortAutorun();
|
||||||
|
afx_msg void OnScreenpreviewLoop();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -562,6 +562,7 @@
|
|||||||
<Image Include="res\Bitmap\Settings.bmp" />
|
<Image Include="res\Bitmap\Settings.bmp" />
|
||||||
<Image Include="res\Bitmap\Share.bmp" />
|
<Image Include="res\Bitmap\Share.bmp" />
|
||||||
<Image Include="res\Bitmap\Show.bmp" />
|
<Image Include="res\Bitmap\Show.bmp" />
|
||||||
|
<Image Include="res\Bitmap\Snapshot.bmp" />
|
||||||
<Image Include="res\Bitmap\Shutdown.bmp" />
|
<Image Include="res\Bitmap\Shutdown.bmp" />
|
||||||
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
||||||
<Image Include="res\Bitmap\Trial.bmp" />
|
<Image Include="res\Bitmap\Trial.bmp" />
|
||||||
@@ -589,6 +590,7 @@
|
|||||||
<Image Include="res\password.ico" />
|
<Image Include="res\password.ico" />
|
||||||
<Image Include="res\proxifler.ico" />
|
<Image Include="res\proxifler.ico" />
|
||||||
<Image Include="res\screen.ico" />
|
<Image Include="res\screen.ico" />
|
||||||
|
<Image Include="res\Snapshot.ico" />
|
||||||
<Image Include="res\system.ico" />
|
<Image Include="res\system.ico" />
|
||||||
<Image Include="res\toolbar1.bmp" />
|
<Image Include="res\toolbar1.bmp" />
|
||||||
<Image Include="res\toolbar2.bmp" />
|
<Image Include="res\toolbar2.bmp" />
|
||||||
|
|||||||
@@ -212,6 +212,7 @@
|
|||||||
<Image Include="res\password.ico" />
|
<Image Include="res\password.ico" />
|
||||||
<Image Include="res\proxifler.ico" />
|
<Image Include="res\proxifler.ico" />
|
||||||
<Image Include="res\screen.ico" />
|
<Image Include="res\screen.ico" />
|
||||||
|
<Image Include="res\Snapshot.ico" />
|
||||||
<Image Include="res\system.ico" />
|
<Image Include="res\system.ico" />
|
||||||
<Image Include="res\toolbar1.bmp" />
|
<Image Include="res\toolbar1.bmp" />
|
||||||
<Image Include="res\toolbar2.bmp" />
|
<Image Include="res\toolbar2.bmp" />
|
||||||
@@ -245,6 +246,7 @@
|
|||||||
<Image Include="res\Bitmap\Logout.bmp" />
|
<Image Include="res\Bitmap\Logout.bmp" />
|
||||||
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
||||||
<Image Include="res\Bitmap\Show.bmp" />
|
<Image Include="res\Bitmap\Show.bmp" />
|
||||||
|
<Image Include="res\Bitmap\Snapshot.bmp" />
|
||||||
<Image Include="res\Bitmap\Exit.bmp" />
|
<Image Include="res\Bitmap\Exit.bmp" />
|
||||||
<Image Include="res\Bitmap\Settings.bmp" />
|
<Image Include="res\Bitmap\Settings.bmp" />
|
||||||
<Image Include="res\Bitmap\Wallet.bmp" />
|
<Image Include="res\Bitmap\Wallet.bmp" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// PreviewTipWnd.cpp
|
// PreviewTipWnd.cpp
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "PreviewTipWnd.h"
|
#include "PreviewTipWnd.h"
|
||||||
|
#include "resource.h" // IDI_ICON_SNAPSHOT(循环模式标题栏图标)
|
||||||
|
|
||||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||||
#include <gdiplus.h>
|
#include <gdiplus.h>
|
||||||
@@ -21,6 +22,9 @@ constexpr int LOADING_H = 270;
|
|||||||
BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
|
BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
|
||||||
ON_WM_PAINT()
|
ON_WM_PAINT()
|
||||||
ON_WM_ERASEBKGND()
|
ON_WM_ERASEBKGND()
|
||||||
|
ON_WM_DESTROY()
|
||||||
|
ON_WM_SIZE()
|
||||||
|
ON_WM_GETMINMAXINFO()
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
CPreviewTipWnd::CPreviewTipWnd() = default;
|
CPreviewTipWnd::CPreviewTipWnd() = default;
|
||||||
@@ -28,11 +32,15 @@ CPreviewTipWnd::CPreviewTipWnd() = default;
|
|||||||
CPreviewTipWnd::~CPreviewTipWnd()
|
CPreviewTipWnd::~CPreviewTipWnd()
|
||||||
{
|
{
|
||||||
// m_image 通过 unique_ptr 自动释放
|
// 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_text = text;
|
||||||
|
m_loopMode = loopMode; // 注意:影响后续 SetImageFromJpeg / OnPaint / OnSize 行为
|
||||||
m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
|
m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
|
||||||
m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
|
m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
|
||||||
if (m_imageReserveW > 0 && m_imageReserveW < LOADING_W) {
|
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);
|
HFONT hF = ::CreateFontIndirectW(&lf);
|
||||||
if (hF) m_font.Attach(hF);
|
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(
|
LPCTSTR kClass = AfxRegisterWndClass(
|
||||||
CS_SAVEBITS,
|
classStyle,
|
||||||
::LoadCursor(NULL, IDC_ARROW),
|
::LoadCursor(NULL, IDC_ARROW),
|
||||||
(HBRUSH)(COLOR_INFOBK + 1),
|
(HBRUSH)(COLOR_BTNFACE + 1),
|
||||||
NULL);
|
NULL);
|
||||||
|
|
||||||
// 临时尺寸;RecalcLayoutAndResize 会在创建后调整
|
// 临时尺寸;RecalcLayoutAndResize 会在创建后调整
|
||||||
CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
|
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(""),
|
// 单发 tooltip —— WS_POPUP|WS_BORDER + EX_TOPMOST|TOOLWINDOW|NOACTIVATE
|
||||||
WS_POPUP | WS_BORDER,
|
// (不激活、不上任务栏、置顶;光标移开会被主对话框关掉)
|
||||||
rc, pParent, 0);
|
// 循环监视窗口 —— 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 (!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();
|
RecalcLayoutAndResize();
|
||||||
|
|
||||||
ShowWindow(SW_SHOWNOACTIVATE);
|
// 单发:不激活;循环:正常显示,允许接收焦点(系统菜单 / 拖拽 / 最小化都需要可激活)
|
||||||
|
ShowWindow(loopMode ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE);
|
||||||
|
if (loopMode) {
|
||||||
|
// 任务栏图标 + 标题在多窗口情况下立刻可见
|
||||||
|
UpdateWindow();
|
||||||
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +145,11 @@ void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
|
|||||||
m_image = std::move(bmp);
|
m_image = std::move(bmp);
|
||||||
m_hasImage = true;
|
m_hasImage = true;
|
||||||
m_unavailable = false;
|
m_unavailable = false;
|
||||||
|
// 循环模式下窗口尺寸交给用户控制(OnSize / WS_THICKFRAME),后续帧不再自动重排版,
|
||||||
|
// 只 Invalidate 触发重绘;图像在 OnPaint 中按 client rect 等比例适配。
|
||||||
|
if (!m_loopMode) {
|
||||||
RecalcLayoutAndResize();
|
RecalcLayoutAndResize();
|
||||||
|
}
|
||||||
if (GetSafeHwnd()) Invalidate();
|
if (GetSafeHwnd()) Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +160,44 @@ void CPreviewTipWnd::MarkPreviewUnavailable()
|
|||||||
if (GetSafeHwnd()) Invalidate();
|
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()
|
void CPreviewTipWnd::RecalcLayoutAndResize()
|
||||||
{
|
{
|
||||||
HWND hWnd = GetSafeHwnd();
|
HWND hWnd = GetSafeHwnd();
|
||||||
@@ -181,6 +266,7 @@ void CPreviewTipWnd::OnPaint()
|
|||||||
CPaintDC pdc(this);
|
CPaintDC pdc(this);
|
||||||
CRect rcClient;
|
CRect rcClient;
|
||||||
GetClientRect(&rcClient);
|
GetClientRect(&rcClient);
|
||||||
|
if (rcClient.IsRectEmpty()) return;
|
||||||
|
|
||||||
// 双缓冲
|
// 双缓冲
|
||||||
CDC memDC;
|
CDC memDC;
|
||||||
@@ -189,41 +275,115 @@ void CPreviewTipWnd::OnPaint()
|
|||||||
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
||||||
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
|
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);
|
CFont* oldFont = memDC.SelectObject(&m_font);
|
||||||
memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
memDC.SetTextColor(::GetSysColor(m_loopMode ? COLOR_WINDOWTEXT : COLOR_INFOTEXT));
|
||||||
memDC.SetBkMode(TRANSPARENT);
|
memDC.SetBkMode(TRANSPARENT);
|
||||||
|
|
||||||
|
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;
|
int curY = PADDING;
|
||||||
if (m_imageDrawW > 0 && m_imageDrawH > 0) {
|
if (m_imageDrawW > 0 && m_imageDrawH > 0) {
|
||||||
CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
|
CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
|
||||||
DrawImageArea(memDC, rcImg);
|
DrawImageArea(memDC, rcImg);
|
||||||
curY += m_imageDrawH + IMAGE_TEXT_GAP;
|
curY += m_imageDrawH + IMAGE_TEXT_GAP;
|
||||||
}
|
}
|
||||||
|
|
||||||
CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
|
CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
|
||||||
DrawTextArea(memDC, rcText);
|
DrawTextArea(memDC, rcText);
|
||||||
|
}
|
||||||
|
|
||||||
memDC.SelectObject(oldFont);
|
memDC.SelectObject(oldFont);
|
||||||
pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
|
pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
|
||||||
memDC.SelectObject(oldBmp);
|
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)
|
void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
|
||||||
{
|
{
|
||||||
|
if (rc.IsRectEmpty()) return;
|
||||||
|
|
||||||
// 边框
|
// 边框
|
||||||
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
|
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
|
||||||
|
|
||||||
|
CRect rcInner = rc;
|
||||||
|
rcInner.DeflateRect(1, 1);
|
||||||
|
|
||||||
if (m_hasImage && m_image) {
|
if (m_hasImage && m_image) {
|
||||||
|
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());
|
Graphics g(dc.GetSafeHdc());
|
||||||
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
||||||
g.SetSmoothingMode(SmoothingModeHighQuality);
|
g.SetSmoothingMode(SmoothingModeHighQuality);
|
||||||
g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
|
g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 占位灰色背景
|
// 占位灰色背景
|
||||||
CRect rcInner = rc;
|
|
||||||
rcInner.DeflateRect(1, 1);
|
|
||||||
dc.FillSolidRect(&rcInner, RGB(245, 245, 245));
|
dc.FillSolidRect(&rcInner, RGB(245, 245, 245));
|
||||||
|
|
||||||
const wchar_t* placeholder = m_unavailable ? L"Preview Unavailable" : L"Loading Preview ...";
|
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));
|
dc.SetTextColor(m_unavailable ? RGB(160, 80, 80) : RGB(120, 120, 120));
|
||||||
RECT rcInnerRaw = rcInner;
|
RECT rcInnerRaw = rcInner;
|
||||||
::DrawTextW(dc.GetSafeHdc(), placeholder, -1, &rcInnerRaw, fmt);
|
::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)
|
void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
|
||||||
{
|
{
|
||||||
RECT r = rc;
|
RECT r = rc;
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ public:
|
|||||||
// text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
|
// text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
|
||||||
// imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
|
// imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
|
||||||
// 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本)
|
// 为 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 线程调用。
|
// 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。
|
||||||
void SetImageFromJpeg(const BYTE* data, size_t bytes);
|
void SetImageFromJpeg(const BYTE* data, size_t bytes);
|
||||||
@@ -30,15 +35,28 @@ public:
|
|||||||
WORD GetReqId() const { return m_reqId; }
|
WORD GetReqId() const { return m_reqId; }
|
||||||
void SetReqId(WORD id) { m_reqId = id; }
|
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:
|
protected:
|
||||||
afx_msg void OnPaint();
|
afx_msg void OnPaint();
|
||||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
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()
|
DECLARE_MESSAGE_MAP()
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void RecalcLayoutAndResize();
|
void RecalcLayoutAndResize();
|
||||||
void DrawImageArea(CDC& dc, const CRect& rc);
|
void DrawImageArea(CDC& dc, const CRect& rc);
|
||||||
void DrawTextArea(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;
|
CStringW m_text;
|
||||||
int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
|
int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
|
||||||
@@ -54,4 +72,14 @@ private:
|
|||||||
|
|
||||||
CFont m_font;
|
CFont m_font;
|
||||||
WORD m_reqId = 0;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1836,3 +1836,12 @@ IOCP
|
|||||||
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=Recommended: Copy to macOS and run install.sh (the script re-signs automatically).
|
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=Recommended: Copy to macOS and run install.sh (the script re-signs automatically).
|
||||||
或手动重签:=Or re-sign manually:
|
或手动重签:=Or re-sign manually:
|
||||||
<请输入文本用于替换远程剪切板>=<Please input text to replace remote clipboard>
|
<请输入文本用于替换远程剪切板>=<Please input text to replace remote clipboard>
|
||||||
|
|
||||||
|
; Screen Preview Loop - English Translation
|
||||||
|
; Format: Simplified Chinese=English
|
||||||
|
|
||||||
|
主机:=Host:
|
||||||
|
分辨率:=Resolution:
|
||||||
|
有 %d 个主机不支持屏幕预览,已跳过=%d host(s) do not support screen preview, skipped
|
||||||
|
播放快照=Play Snapshot
|
||||||
|
快照=Snapshot
|
||||||
|
|||||||
@@ -1827,3 +1827,12 @@ IOCP
|
|||||||
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=推薦: 複製到 macOS 後執行 install.sh 安裝 (腳本會自動重新簽章)。
|
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=推薦: 複製到 macOS 後執行 install.sh 安裝 (腳本會自動重新簽章)。
|
||||||
或手动重签:=或手動重新簽章:
|
或手动重签:=或手動重新簽章:
|
||||||
<请输入文本用于替换远程剪切板>=<请输入文本用于替换远程剪切板>
|
<请输入文本用于替换远程剪切板>=<请输入文本用于替换远程剪切板>
|
||||||
|
|
||||||
|
; Screen Preview Loop - Traditional Chinese Translation
|
||||||
|
; Format: Simplified Chinese=Traditional Chinese
|
||||||
|
|
||||||
|
主机:=主機:
|
||||||
|
分辨率:=解析度:
|
||||||
|
有 %d 个主机不支持屏幕预览,已跳过=有 %d 個主機不支援螢幕預覽,已跳過
|
||||||
|
播放快照=播放快照
|
||||||
|
快照=快照
|
||||||
|
|||||||
BIN
server/2015Remote/res/Bitmap/Snapshot.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/Snapshot.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
BIN
server/2015Remote/res/Snapshot.ico
Normal file
BIN
server/2015Remote/res/Snapshot.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -255,6 +255,8 @@
|
|||||||
#define IDR_MACOS_GHOST 372
|
#define IDR_MACOS_GHOST 372
|
||||||
#define IDB_BITMAP_WEBDESKTOP 373
|
#define IDB_BITMAP_WEBDESKTOP 373
|
||||||
#define IDB_BITMAP_PLUGINCONFIG 374
|
#define IDB_BITMAP_PLUGINCONFIG 374
|
||||||
|
#define IDB_BITMAP_SNAPSHOT 375
|
||||||
|
#define IDI_ICON_SNAPSHOT 376
|
||||||
#define IDR_LANG_EN_US 380
|
#define IDR_LANG_EN_US 380
|
||||||
#define IDR_LANG_ZH_TW 381
|
#define IDR_LANG_ZH_TW 381
|
||||||
#define IDC_MESSAGE 1000
|
#define IDC_MESSAGE 1000
|
||||||
@@ -974,14 +976,16 @@
|
|||||||
#define ID_33046 33046
|
#define ID_33046 33046
|
||||||
#define ID_PROXY_PORT_AUTORUN 33047
|
#define ID_PROXY_PORT_AUTORUN 33047
|
||||||
#define ID_TRIGGER_SETTINGS 33048
|
#define ID_TRIGGER_SETTINGS 33048
|
||||||
|
#define ID_33048 33048
|
||||||
|
#define ID_SCREENPREVIEW_LOOP 33049
|
||||||
#define ID_EXIT_FULLSCREEN 40001
|
#define ID_EXIT_FULLSCREEN 40001
|
||||||
|
|
||||||
// Next default values for new objects
|
// Next default values for new objects
|
||||||
//
|
//
|
||||||
#ifdef APSTUDIO_INVOKED
|
#ifdef APSTUDIO_INVOKED
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 373
|
#define _APS_NEXT_RESOURCE_VALUE 377
|
||||||
#define _APS_NEXT_COMMAND_VALUE 33048
|
#define _APS_NEXT_COMMAND_VALUE 33050
|
||||||
#define _APS_NEXT_CONTROL_VALUE 2542
|
#define _APS_NEXT_CONTROL_VALUE 2542
|
||||||
#define _APS_NEXT_SYMED_VALUE 105
|
#define _APS_NEXT_SYMED_VALUE 105
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
#define WM_DISCONNECT WM_USER+3032
|
#define WM_DISCONNECT WM_USER+3032
|
||||||
#define WM_OPENTERMINALDIALOG WM_USER+3033
|
#define WM_OPENTERMINALDIALOG WM_USER+3033
|
||||||
#define WM_PREVIEW_RESPONSE WM_USER+3034
|
#define WM_PREVIEW_RESPONSE WM_USER+3034
|
||||||
|
#define WM_PREVIEW_LOOP_CLOSED WM_USER+3035
|
||||||
|
|
||||||
#ifdef _UNICODE
|
#ifdef _UNICODE
|
||||||
#if defined _M_IX86
|
#if defined _M_IX86
|
||||||
|
|||||||
Reference in New Issue
Block a user