2 Commits

Author SHA1 Message Date
yuanyuanxiang
e762e3cbd1 Feature: Add live thumbnail preview column to online host list 2026-05-13 18:43:20 +02:00
yuanyuanxiang
6c32b478af Feature: Add "Play Snapshot" loop preview windows for online hosts 2026-05-13 13:05:31 +02:00
16 changed files with 1244 additions and 80 deletions

View File

@@ -1348,11 +1348,13 @@ enum {
SHELLCODE = 0,
MEMORYDLL = 1,
RUNTYPE_MAX = 2,
CALLTYPE_DEFAULT = 0, // 默认调用方式: 只是加载DLL,需要在DLL加载时执行代码
CALLTYPE_IOCPTHREAD = 1, // 调用run函数启动线程: DWORD (__stdcall *run)(void* lParam)
CALLTYPE_FRPC_CALL = 2, // 调用FRPC
CALLTYPE_FRPC_STDCALL = 3, // 调用FRPC标准方式使用开源FRP项目
CALLTYPE_MAX = 4,
};
typedef DWORD(__stdcall* PidCallback)(void);

View File

@@ -11,6 +11,7 @@
#define SCH_MODE_MONTHLY 4 // 每月定时模式
#define SCH_MODE_YEARLY 5 // 每年定时模式
#define SCH_MODE_OFF 6 // 关闭
#define SCH_MODE_MAX 7
#pragma pack(push, 1)
// 严格定义 16 字节结构

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -234,6 +234,77 @@ public:
// 用于在收到 JPEG 后调用 SetImageFromJpegDeletePopupWindow 释放时一并置空。
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<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);
// ===== 主机列表缩略图(在线列表第 0 列) =====
// 设计文档:纯主控端 UI 偏好,不进 MasterSettings 协议包。持久化走 THIS_CFG
// [thumbnail] 节。所有运行时状态仅 UI 线程访问。
struct ThumbnailSettings {
bool Enabled = true;
int RefreshIntervalSec = 30; // 单台主机的刷新周期
int ThumbWidth = 60; // 列表内显示宽度(高 = 9w/16
int NetReqWidth = 120; // 网络请求的源宽HiDPI 余量,>= ThumbWidth
int JpegQuality = 60; // 1..100
};
ThumbnailSettings m_ThumbnailCfg;
struct ThumbCacheEntry {
HBITMAP bmp = nullptr; // 预先缩到显示尺寸的 24bpp DIBBitBlt 友好
int w = 0;
int h = 0;
};
std::map<uint64_t, ThumbCacheEntry> m_HostThumbnails; // 缩略图缓存
std::map<uint64_t, DWORD> m_ThumbNextDueTick; // 下次到期 GetTickCount() 时间
std::set<uint64_t> m_ThumbnailPending; // 在飞的 clientId 集合
WORD m_ThumbnailReqId = 0; // 缩略图专用 reqId与 m_PreviewReqId / m_LoopReqId 解耦)
// 行高占位 ImageListCListCtrl 没有 SetRowHeight标准做法是装一个 1×rowH 的
// 空 ImageList 强行撑高行。关闭缩略图时换回 1×default或 detach让行高回缩。
CImageList m_thumbRowHeightImgList;
int m_thumbRowHeightApplied = 0; // 已应用的行高(避免重复创建)
// 一次性配置应用入口:加载/重新加载 m_ThumbnailCfg 后调用,处理:
// - 启动/停止 TIMER_THUMBNAIL_REFRESH
// - 列宽 / 行高 调整
// - 尺寸变化时丢弃旧缓存 HBITMAP
void ApplyThumbnailSettings();
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
void ClearThumbnailCacheEntry(uint64_t clientID);
void ClearAllThumbnailCache();
void InvalidateHostRow(uint64_t clientID); // 仅重绘对应行
// 记录 clientID心跳更新
std::set<uint64_t> m_DirtyClients;
// 待处理的上线/下线事件(批量更新减少闪烁)
@@ -286,7 +357,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<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList;
@@ -438,6 +509,8 @@ public:
afx_msg void OnWhatIsThis();
afx_msg void OnOnlineAuthorize();
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
// 单击缩略图列COL_THUMBNAIL命中时弹出循环监视窗口其余列保持默认行为
afx_msg void OnListSingleClick(NMHDR* pNMHDR, LRESULT* pResult);
// 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数4K/超宽屏在 LAN 档自适应放大
void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const;
// 发起预览请求reqId 应与 m_PreviewReqId 同步
@@ -494,6 +567,7 @@ public:
afx_msg void OnParamEnableLog();
afx_msg void OnParamPrivacyWallpaper();
afx_msg void OnParamFileV2();
afx_msg void OnParamThumbnailPreview();
afx_msg void OnParamRunAsUser();
void ProxyClientTcpPort(bool isStandard, bool autoRun=false);
afx_msg void OnProxyPort();
@@ -520,4 +594,5 @@ public:
afx_msg void OnCancelShare();
afx_msg void OnWebRemoteControl();
afx_msg void OnProxyPortAutorun();
afx_msg void OnScreenpreviewLoop();
};

View File

@@ -562,6 +562,7 @@
<Image Include="res\Bitmap\Settings.bmp" />
<Image Include="res\Bitmap\Share.bmp" />
<Image Include="res\Bitmap\Show.bmp" />
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\Bitmap\Shutdown.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\Trial.bmp" />
@@ -589,6 +590,7 @@
<Image Include="res\password.ico" />
<Image Include="res\proxifler.ico" />
<Image Include="res\screen.ico" />
<Image Include="res\Snapshot.ico" />
<Image Include="res\system.ico" />
<Image Include="res\toolbar1.bmp" />
<Image Include="res\toolbar2.bmp" />

View File

@@ -212,6 +212,7 @@
<Image Include="res\password.ico" />
<Image Include="res\proxifler.ico" />
<Image Include="res\screen.ico" />
<Image Include="res\Snapshot.ico" />
<Image Include="res\system.ico" />
<Image Include="res\toolbar1.bmp" />
<Image Include="res\toolbar2.bmp" />
@@ -245,6 +246,7 @@
<Image Include="res\Bitmap\Logout.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\Show.bmp" />
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\Bitmap\Exit.bmp" />
<Image Include="res\Bitmap\Settings.bmp" />
<Image Include="res\Bitmap\Wallet.bmp" />

View File

@@ -109,7 +109,7 @@ void CPluginSettingsDlg::LoadPluginsToList()
m_listPlugins.DeleteAllItems();
const char* runTypeNames[] = { "Shellcode", "内存DLL" };
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时" };
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时", "每月定时", "每年定时", "关闭执行", };
int index = 0;
for (const auto& dll : m_DllList) {
@@ -132,8 +132,8 @@ void CPluginSettingsDlg::LoadPluginsToList()
int runType = cfg ? cfg->RunType : info->RunType;
int mode = cfg ? cfg->Mode : info->Schedule.Mode;
m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < 2 ? runType : 0]));
m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < 4 ? mode : 0]));
m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < RUNTYPE_MAX ? runType : MEMORYDLL]));
m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < SCH_MODE_MAX ? mode : SCH_MODE_NONE]));
m_listPlugins.SetItemText(index, 4, CString(info->Md5));
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
@@ -172,9 +172,9 @@ void CPluginSettingsDlg::UpdateSelectedPluginInfo()
unsigned int interval = cfg ? cfg->Interval : info->Schedule.Config.Startup.Interval;
unsigned char maxCount = cfg ? cfg->MaxCount : info->Schedule.MaxCount;
m_comboRunType.SetCurSel(runType < 2 ? runType : 0);
m_comboCallType.SetCurSel(callType < 4 ? callType : 0);
m_comboMode.SetCurSel(mode < 4 ? mode : 0);
m_comboRunType.SetCurSel(runType < RUNTYPE_MAX ? runType : MEMORYDLL);
m_comboCallType.SetCurSel(callType < CALLTYPE_MAX ? callType : CALLTYPE_IOCPTHREAD);
m_comboMode.SetCurSel(mode < SCH_MODE_MAX ? mode : SCH_MODE_NONE);
CString str;
str.Format(_T("%u"), interval);

View File

@@ -1,6 +1,7 @@
// PreviewTipWnd.cpp
#include "stdafx.h"
#include "PreviewTipWnd.h"
#include "resource.h" // IDI_ICON_SNAPSHOT循环模式标题栏图标
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
#include <gdiplus.h>
@@ -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 由本类持有,析构时 DestroyIconWM_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;
// 循环模式下窗口尺寸交给用户控制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);
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);
}
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) {
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;

View File

@@ -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 发送 msgIdLPARAM 是堆上的
// 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 不转移所有权MSDNoriginator 必须自己释放),
// 析构时 DestroyIcon不能在 OnDestroy 里销毁,因为系统在 WM_NCDESTROY 之前还在用它。
HICON m_hIconSmall = nullptr;
HICON m_hIconBig = nullptr;
};

View File

@@ -1836,3 +1836,14 @@ IOCP
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=Recommended: Copy to macOS and run install.sh (the script re-signs automatically).
或手动重签:=Or re-sign manually:
<请输入文本用于替换远程剪切板>=<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
预览=Preview
主机列表预览图=Host List Thumbnails

View File

@@ -1827,3 +1827,14 @@ IOCP
推荐: 拷贝到 macOS 后运行 install.sh 安装 (脚本会自动重签)。=推薦: 複製到 macOS 後執行 install.sh 安裝 (腳本會自動重新簽章)。
或手动重签:=或手動重新簽章:
<请输入文本用于替换远程剪切板>=<请输入文本用于替换远程剪切板>
; Screen Preview Loop - Traditional Chinese Translation
; Format: Simplified Chinese=Traditional Chinese
主机:=主機:
分辨率:=解析度:
有 %d 个主机不支持屏幕预览,已跳过=有 %d 個主機不支援螢幕預覽,已跳過
播放快照=播放快照
快照=快照
预览=預覽
主机列表预览图=主機列表預覽圖

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -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,17 @@
#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_PARAM_THUMBNAIL_PREVIEW 33050
#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 33051
#define _APS_NEXT_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105
#endif

View File

@@ -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