Feature: Add live thumbnail preview column to online host list
This commit is contained in:
@@ -89,6 +89,7 @@
|
||||
#define TIMER_STATUSBAR_INIT 7
|
||||
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
||||
#define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定)
|
||||
#define TIMER_THUMBNAIL_REFRESH 10 // 主机列表缩略图后台刷新(间隔取自 m_ThumbnailCfg)
|
||||
#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"
|
||||
@@ -129,9 +130,12 @@ typedef struct {
|
||||
int nWidth; //列表的宽度
|
||||
} COLUMNSTRUCT;
|
||||
|
||||
const int g_Column_Count_Online = ONLINELIST_MAX; // 报表的列数
|
||||
// 列表列布局:第 0 列固定为"预览"(缩略图,无数据槽),后续列对应 ONLINELIST_* 偏移 1。
|
||||
// 即 listCol == 0 → 缩略图;listCol >= 1 → ONLINELIST_(listCol - 1)。
|
||||
const int g_Column_Count_Online = ONLINELIST_MAX + 1; // 1 缩略图 + 既有数据列
|
||||
|
||||
COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
|
||||
{"预览", 60 },
|
||||
{"IP", 130 },
|
||||
{"端口", 60 },
|
||||
{"地理位置", 130 },
|
||||
@@ -146,6 +150,12 @@ COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
|
||||
{"类型", 50 },
|
||||
};
|
||||
|
||||
// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。
|
||||
static const int COL_THUMBNAIL = 0;
|
||||
static inline int ColumnToDataSlot(int listCol) {
|
||||
return listCol <= COL_THUMBNAIL ? -1 : (listCol - 1);
|
||||
}
|
||||
|
||||
// 用于应用程序“关于”菜单项的 CAboutDlg 对话框
|
||||
|
||||
const int g_Column_Count_Message = 3; // 列表的个数
|
||||
@@ -833,6 +843,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_WHAT_IS_THIS, &CMy2015RemoteDlg::OnWhatIsThis)
|
||||
ON_COMMAND(ID_ONLINE_AUTHORIZE, &CMy2015RemoteDlg::OnOnlineAuthorize)
|
||||
ON_NOTIFY(NM_DBLCLK, IDC_ONLINE, &CMy2015RemoteDlg::OnListClick)
|
||||
ON_NOTIFY(NM_CLICK, IDC_ONLINE, &CMy2015RemoteDlg::OnListSingleClick)
|
||||
ON_COMMAND(ID_ONLINE_UNAUTHORIZE, &CMy2015RemoteDlg::OnOnlineUnauthorize)
|
||||
ON_COMMAND(ID_TOOL_REQUEST_AUTH, &CMy2015RemoteDlg::OnToolRequestAuth)
|
||||
ON_COMMAND(ID_TOOL_INPUT_PASSWORD, &CMy2015RemoteDlg::OnToolInputPassword)
|
||||
@@ -877,6 +888,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_PARAM_ENABLE_LOG, &CMy2015RemoteDlg::OnParamEnableLog)
|
||||
ON_COMMAND(ID_PARAM_PRIVACY_WALLPAPER, &CMy2015RemoteDlg::OnParamPrivacyWallpaper)
|
||||
ON_COMMAND(ID_PARAM_FILE_V2, &CMy2015RemoteDlg::OnParamFileV2)
|
||||
ON_COMMAND(ID_PARAM_THUMBNAIL_PREVIEW, &CMy2015RemoteDlg::OnParamThumbnailPreview)
|
||||
ON_COMMAND(ID_PARAM_RUN_AS_USER, &CMy2015RemoteDlg::OnParamRunAsUser)
|
||||
ON_COMMAND(ID_PROXY_PORT, &CMy2015RemoteDlg::OnProxyPort)
|
||||
ON_COMMAND(ID_HOOK_WIN, &CMy2015RemoteDlg::OnHookWin)
|
||||
@@ -1317,6 +1329,9 @@ VOID CMy2015RemoteDlg::InitControl()
|
||||
m_CList_Message.ModifyStyle(0, LVS_SHOWSELALWAYS);
|
||||
m_CList_Message.SetExtendedStyle(style);
|
||||
m_CList_Message.ModifyStyle(WS_HSCROLL, 0);
|
||||
|
||||
// 不在这里调 ApplyThumbnailSettings —— 调用方在 LoadThumbnailSettingsFromCfg
|
||||
// 之后统一 Apply(避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。
|
||||
}
|
||||
|
||||
|
||||
@@ -2011,6 +2026,12 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
strcpy(m_settings.RequestAuthUrl, THIS_CFG.GetStr("settings", "RequestAuthUrl", BRAND_URL_REQUEST_AUTH).c_str());
|
||||
strcpy(m_settings.GetPluginUrl, THIS_CFG.GetStr("settings", "GetPluginUrl", BRAND_URL_GET_PLUGIN).c_str());
|
||||
m_bEnableFileV2 = THIS_CFG.GetInt("settings", "EnableFileV2", 0) != 0;
|
||||
|
||||
// 缩略图配置:从 [thumbnail] 节读取(独立于 MasterSettings,纯主控端 UI 偏好)。
|
||||
// 注意:InitControl 末尾已经先调过一次 Apply(用默认值),那时 INI 还没读 → 状态是错的。
|
||||
// 这里读完 INI 后再 Apply 一次纠正过来。
|
||||
LoadThumbnailSettingsFromCfg();
|
||||
ApplyThumbnailSettings();
|
||||
m_runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
|
||||
|
||||
CMenu* SubMenu = m_MainMenu.GetSubMenu(2);
|
||||
@@ -2020,6 +2041,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
SubMenu->CheckMenuItem(ID_PARAM_LOGIN_NOTIFY, m_needNotify ? MF_CHECKED : MF_UNCHECKED);
|
||||
SubMenu->CheckMenuItem(ID_PARAM_ENABLE_LOG, m_settings.EnableLog ? MF_CHECKED : MF_UNCHECKED);
|
||||
SubMenu->CheckMenuItem(ID_PARAM_FILE_V2, m_bEnableFileV2 ? MF_CHECKED : MF_UNCHECKED);
|
||||
SubMenu->CheckMenuItem(ID_PARAM_THUMBNAIL_PREVIEW, m_ThumbnailCfg.Enabled ? MF_CHECKED : MF_UNCHECKED);
|
||||
|
||||
// 互斥逻辑:三种模式 (RunNormal: 0=服务+SYSTEM, 1=普通模式, 2=服务+User)
|
||||
if (m_runNormal == 0) {
|
||||
@@ -2068,7 +2090,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
memset(&lvColumn, 0, sizeof(LVCOLUMN));
|
||||
lvColumn.mask = LVCF_TEXT;
|
||||
lvColumn.pszText = (char*)str.data();
|
||||
m_CList_Online.SetColumn(ONLINELIST_VIDEO, &lvColumn);
|
||||
m_CList_Online.SetColumn(ONLINELIST_VIDEO + 1, &lvColumn); // +1:缩略图列占据列 0
|
||||
timeBeginPeriod(1);
|
||||
if (IsFunctionReallyHooked("user32.dll","SetTimer") || IsFunctionReallyHooked("user32.dll", "KillTimer")) {
|
||||
THIS_APP->MessageBox(_TR("FUCK!!! 请勿HOOK此程序!"), _TR("提示"), MB_ICONERROR);
|
||||
@@ -3066,6 +3088,9 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
||||
if (nIDEvent == TIMER_PREVIEW_LOOP) {
|
||||
TickLoopTips();
|
||||
}
|
||||
if (nIDEvent == TIMER_THUMBNAIL_REFRESH) {
|
||||
TickThumbnailRefresh();
|
||||
}
|
||||
if (nIDEvent == TIMER_CLEAR_BALLOON) {
|
||||
KillTimer(TIMER_CLEAR_BALLOON);
|
||||
|
||||
@@ -3413,7 +3438,16 @@ void CMy2015RemoteDlg::Release()
|
||||
|
||||
UninitFileUpload();
|
||||
DeletePopupWindow(TRUE);
|
||||
CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口
|
||||
CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口
|
||||
KillTimer(TIMER_THUMBNAIL_REFRESH);
|
||||
ClearAllThumbnailCache(); // 释放所有缓存 HBITMAP
|
||||
// 先解绑再销毁行高 ImageList,避免 listview HWND 在 m_thumbRowHeightImgList
|
||||
// 之后销毁的极端时序里短暂持有悬空 HIMAGELIST。
|
||||
if (m_thumbRowHeightImgList.GetSafeHandle()) {
|
||||
m_CList_Online.SetImageList(NULL, LVSIL_SMALL);
|
||||
m_thumbRowHeightImgList.DeleteImageList();
|
||||
m_thumbRowHeightApplied = 0;
|
||||
}
|
||||
isClosed = TRUE;
|
||||
ShowWindow(SW_HIDE);
|
||||
|
||||
@@ -3480,6 +3514,9 @@ int CALLBACK CMy2015RemoteDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, L
|
||||
|
||||
void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
||||
{
|
||||
// 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发
|
||||
if (ColumnToDataSlot(nColumn) < 0) return;
|
||||
|
||||
static int m_nSortColumn = 0;
|
||||
static bool m_bSortAscending = false;
|
||||
if (nColumn == m_nSortColumn) {
|
||||
@@ -3491,14 +3528,14 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
||||
m_bSortAscending = true;
|
||||
}
|
||||
|
||||
// 虚拟列表:对数据源进行排序
|
||||
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽)
|
||||
EnterCriticalSection(&m_cs);
|
||||
int col = m_nSortColumn;
|
||||
int slot = ColumnToDataSlot(m_nSortColumn);
|
||||
bool asc = m_bSortAscending;
|
||||
std::sort(m_HostList.begin(), m_HostList.end(),
|
||||
[col, asc](context* a, context* b) {
|
||||
CString s1 = a ? a->GetClientData(col) : "";
|
||||
CString s2 = b ? b->GetClientData(col) : "";
|
||||
[slot, asc](context* a, context* b) {
|
||||
CString s1 = a ? a->GetClientData(slot) : "";
|
||||
CString s2 = b ? b->GetClientData(slot) : "";
|
||||
int result = s1.Compare(s2);
|
||||
return asc ? (result < 0) : (result > 0);
|
||||
});
|
||||
@@ -3564,19 +3601,22 @@ void CMy2015RemoteDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
|
||||
// 提供文本数据
|
||||
if (pItem->mask & LVIF_TEXT) {
|
||||
CString text;
|
||||
int nCol = pItem->iSubItem;
|
||||
|
||||
// 备注列特殊处理
|
||||
if (nCol == ONLINELIST_COMPUTER_NAME) {
|
||||
CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
|
||||
text = !note.IsEmpty() ? note : ctx->GetClientData(nCol);
|
||||
int slot = ColumnToDataSlot(nCol);
|
||||
if (slot < 0) {
|
||||
// 缩略图列:自绘渲染,文本置空
|
||||
if (pItem->pszText && pItem->cchTextMax > 0) pItem->pszText[0] = '\0';
|
||||
} else {
|
||||
text = ctx->GetClientData(nCol);
|
||||
if (text.IsEmpty()) text = "?";
|
||||
CString text;
|
||||
if (slot == ONLINELIST_COMPUTER_NAME) {
|
||||
CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
|
||||
text = !note.IsEmpty() ? note : ctx->GetClientData(slot);
|
||||
} else {
|
||||
text = ctx->GetClientData(slot);
|
||||
if (text.IsEmpty()) text = "?";
|
||||
}
|
||||
lstrcpyn(pItem->pszText, text, pItem->cchTextMax);
|
||||
}
|
||||
|
||||
lstrcpyn(pItem->pszText, text, pItem->cchTextMax);
|
||||
}
|
||||
|
||||
*pResult = 0;
|
||||
@@ -3611,6 +3651,15 @@ void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
if ((pItem->mask & LVIF_TEXT) && pItem->pszText && pItem->cchTextMax > 0) {
|
||||
std::wstring wtext;
|
||||
int nCol = pItem->iSubItem;
|
||||
int slot = ColumnToDataSlot(nCol);
|
||||
if (slot < 0) {
|
||||
// 缩略图列:自绘渲染,文本置空
|
||||
pItem->pszText[0] = L'\0';
|
||||
*pResult = 0;
|
||||
return;
|
||||
}
|
||||
// 下面的逻辑沿用旧版,但用 slot 替代 nCol 访问数据槽
|
||||
nCol = slot;
|
||||
|
||||
if (nCol == ONLINELIST_LOGINTIME) {
|
||||
// "活动窗口"列:心跳到达后用旁路表里的宽字符串(已正确解码);
|
||||
@@ -4371,7 +4420,7 @@ void CMy2015RemoteDlg::OnMainSet()
|
||||
CLock L(m_cs);
|
||||
m_settings.ReportInterval = m;
|
||||
m_settings.DetectSoftware = n;
|
||||
m_CList_Online.SetColumn(ONLINELIST_VIDEO, &lvColumn);
|
||||
m_CList_Online.SetColumn(ONLINELIST_VIDEO + 1, &lvColumn); // +1:缩略图列占据列 0
|
||||
SendMasterSettings(nullptr, m_settings);
|
||||
}
|
||||
|
||||
@@ -5742,6 +5791,11 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam)
|
||||
AddList(strIP,strAddr,strPCName,strOS,strCPU,strVideo,strPing,LoginInfor->moduleVersion,LoginInfor->szStartTime, v, ContextObject);
|
||||
delete LoginInfor;
|
||||
|
||||
// 缩略图调度"急播":新上线主机立刻入队,下次 tick 就发请求(含三道门判定)
|
||||
if (m_ThumbnailCfg.Enabled) {
|
||||
m_ThumbNextDueTick[ContextObject->GetClientID()] = ::GetTickCount();
|
||||
}
|
||||
|
||||
// 执行主机上线触发器
|
||||
ExecuteOnlineTrigger(ContextObject);
|
||||
|
||||
@@ -5906,6 +5960,10 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
|
||||
// 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。
|
||||
if (info->clientId != 0) {
|
||||
CloseLoopTip(info->clientId);
|
||||
// 清理缩略图相关状态(缓存 + 调度 + 在飞标记)。主机已不在列表,重绘不必要。
|
||||
ClearThumbnailCacheEntry(info->clientId);
|
||||
m_ThumbNextDueTick.erase(info->clientId);
|
||||
m_ThumbnailPending.erase(info->clientId);
|
||||
}
|
||||
|
||||
// Close child dialog window
|
||||
@@ -7601,6 +7659,13 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
LPNMITEMACTIVATE pNMItem = (LPNMITEMACTIVATE)pNMHDR;
|
||||
int iItem = pNMItem->iItem;
|
||||
|
||||
// 双击命中缩略图列:单击已触发 OnListSingleClick 打开循环窗口,这里不再叠加单发 tooltip
|
||||
// 避免两个窗口同时出现。命中其他列时保持原有"双击 → 单发预览"行为。
|
||||
if (pNMItem->iSubItem == COL_THUMBNAIL && m_ThumbnailCfg.Enabled) {
|
||||
*pResult = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
CLock lock(m_cs);
|
||||
context* ctx = GetContextByListIndex(iItem);
|
||||
if (ctx) {
|
||||
@@ -7709,6 +7774,47 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
// 单击列表:仅对缩略图列起效 —— 打开循环监视窗口(等价于右键菜单"播放快照")。
|
||||
// 其余列保持系统默认(选中),不抢用户体验。
|
||||
void CMy2015RemoteDlg::OnListSingleClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
*pResult = 0;
|
||||
if (!m_ThumbnailCfg.Enabled) return;
|
||||
|
||||
LPNMITEMACTIVATE p = (LPNMITEMACTIVATE)pNMHDR;
|
||||
if (p->iItem < 0) return;
|
||||
if (p->iSubItem != COL_THUMBNAIL) return;
|
||||
|
||||
context* ctx = nullptr;
|
||||
uint64_t cid = 0;
|
||||
{
|
||||
CLock L(m_cs);
|
||||
ctx = GetContextByListIndex(p->iItem);
|
||||
if (ctx) cid = ctx->GetClientID();
|
||||
}
|
||||
if (!ctx || cid == 0) return;
|
||||
|
||||
// 已经开着循环窗 → 不重复开,让现有窗口前置
|
||||
auto it = m_LoopTips.find(cid);
|
||||
if (it != m_LoopTips.end() && it->second.tip && ::IsWindow(it->second.tip->GetSafeHwnd())) {
|
||||
it->second.tip->ShowWindow(SW_SHOWNORMAL);
|
||||
it->second.tip->BringWindowToTop();
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有缓存的缩略图(即单元格显示的是 "…" 或 "N/A")→ 不弹窗。
|
||||
// 用户语义:能看到画面才允许点开放大;占位态下点击不做任何动作。
|
||||
auto cacheIt = m_HostThumbnails.find(cid);
|
||||
bool hasBmp = (cacheIt != m_HostThumbnails.end() && cacheIt->second.bmp);
|
||||
if (!hasBmp) return;
|
||||
|
||||
// 锚点:点击位置附近(列表客户区坐标 → 屏幕坐标)
|
||||
CPoint anchor = p->ptAction;
|
||||
m_CList_Online.ClientToScreen(&anchor);
|
||||
anchor.Offset(20, 10);
|
||||
OpenLoopTip(ctx, anchor);
|
||||
}
|
||||
|
||||
// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile
|
||||
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
|
||||
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
|
||||
@@ -7783,14 +7889,24 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
||||
|
||||
// ---------- 路径 1:循环快照(按 clientId 命中)----------
|
||||
auto it = m_LoopTips.find(msg->clientId);
|
||||
if (it != m_LoopTips.end()) {
|
||||
if (it != m_LoopTips.end() &&
|
||||
it->second.expectedReqId != 0 && hdr->reqId == it->second.expectedReqId)
|
||||
{
|
||||
// reqId 不命中时不直接 return,让流程继续走到缩略图分支 —— 兼容"刚打开循环窗
|
||||
// 时还有上一笔缩略图请求在飞"的情况,防止 m_ThumbnailPending 条目被永久挂死。
|
||||
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);
|
||||
// 顺手刷新列表缩略图(开循环窗的主机不再单独发请求,省一半流量)
|
||||
if (m_ThumbnailCfg.Enabled) {
|
||||
CacheThumbnail(msg->clientId, jpeg, hdr->bytes);
|
||||
InvalidateHostRow(msg->clientId);
|
||||
// 重置该主机的 due 时间,避免循环窗关掉后立刻又有缩略图请求
|
||||
DWORD now = ::GetTickCount();
|
||||
m_ThumbNextDueTick[msg->clientId] = now + (DWORD)m_ThumbnailCfg.RefreshIntervalSec * 1000;
|
||||
}
|
||||
} else {
|
||||
// 单帧失败不直接关窗,标"不可用",下一轮定时器再尝试
|
||||
entry.tip->MarkPreviewUnavailable();
|
||||
@@ -7798,7 +7914,18 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------- 路径 2:既有单发流程 ----------
|
||||
// ---------- 路径 2:列表缩略图(按 clientId 命中 in-flight 集合)----------
|
||||
if (m_ThumbnailPending.count(msg->clientId)) {
|
||||
m_ThumbnailPending.erase(msg->clientId);
|
||||
if (dataOk) {
|
||||
CacheThumbnail(msg->clientId, jpeg, hdr->bytes);
|
||||
InvalidateHostRow(msg->clientId);
|
||||
}
|
||||
// 数据非 OK 也不重试,等下个周期;保留旧缩略(如有)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------- 路径 3:既有单发流程 ----------
|
||||
// 序号校验:和当前期待的 reqId 不符 → 过期响应,丢弃
|
||||
if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0;
|
||||
if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0;
|
||||
@@ -7991,6 +8118,302 @@ LRESULT CMy2015RemoteDlg::OnLoopTipDestroyed(WPARAM /*wParam*/, LPARAM lParam)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ========== 主机列表缩略图:实现 ==========
|
||||
//
|
||||
// 线程模型:与循环模式一致 —— m_HostThumbnails / m_ThumbNextDueTick /
|
||||
// m_ThumbnailPending 全部仅 UI 线程访问。响应 PostMessage 经消息泵串行化派发。
|
||||
//
|
||||
// 缓存策略:响应到达即解码 + 等比缩放到 ThumbWidth × ThumbHeight 的 HBITMAP,
|
||||
// 这样自绘时只需 BitBlt 一次,无 GDI+ 开销。设置变更(尺寸)会清空所有 HBITMAP,
|
||||
// 下一轮请求自然按新尺寸重建。
|
||||
|
||||
void CMy2015RemoteDlg::LoadThumbnailSettingsFromCfg()
|
||||
{
|
||||
m_ThumbnailCfg.Enabled = THIS_CFG.GetInt("thumbnail", "Enabled", 1) != 0;
|
||||
m_ThumbnailCfg.RefreshIntervalSec = THIS_CFG.GetInt("thumbnail", "RefreshIntervalSec", 30);
|
||||
m_ThumbnailCfg.ThumbWidth = THIS_CFG.GetInt("thumbnail", "ThumbWidth", 60);
|
||||
m_ThumbnailCfg.NetReqWidth = THIS_CFG.GetInt("thumbnail", "NetReqWidth", 120);
|
||||
m_ThumbnailCfg.JpegQuality = THIS_CFG.GetInt("thumbnail", "JpegQuality", 60);
|
||||
|
||||
// Clamp 到安全范围(防 INI 被手工编错)
|
||||
if (m_ThumbnailCfg.RefreshIntervalSec < 5) m_ThumbnailCfg.RefreshIntervalSec = 5;
|
||||
if (m_ThumbnailCfg.RefreshIntervalSec > 300) m_ThumbnailCfg.RefreshIntervalSec = 300;
|
||||
if (m_ThumbnailCfg.ThumbWidth < 40) m_ThumbnailCfg.ThumbWidth = 40;
|
||||
if (m_ThumbnailCfg.ThumbWidth > 120) m_ThumbnailCfg.ThumbWidth = 120;
|
||||
if (m_ThumbnailCfg.NetReqWidth < m_ThumbnailCfg.ThumbWidth)
|
||||
m_ThumbnailCfg.NetReqWidth = m_ThumbnailCfg.ThumbWidth * 2;
|
||||
if (m_ThumbnailCfg.NetReqWidth > 240) m_ThumbnailCfg.NetReqWidth = 240;
|
||||
if (m_ThumbnailCfg.JpegQuality < 30) m_ThumbnailCfg.JpegQuality = 30;
|
||||
if (m_ThumbnailCfg.JpegQuality > 85) m_ThumbnailCfg.JpegQuality = 85;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SaveThumbnailSettingsToCfg() const
|
||||
{
|
||||
THIS_CFG.SetInt("thumbnail", "Enabled", m_ThumbnailCfg.Enabled ? 1 : 0);
|
||||
THIS_CFG.SetInt("thumbnail", "RefreshIntervalSec", m_ThumbnailCfg.RefreshIntervalSec);
|
||||
THIS_CFG.SetInt("thumbnail", "ThumbWidth", m_ThumbnailCfg.ThumbWidth);
|
||||
THIS_CFG.SetInt("thumbnail", "NetReqWidth", m_ThumbnailCfg.NetReqWidth);
|
||||
THIS_CFG.SetInt("thumbnail", "JpegQuality", m_ThumbnailCfg.JpegQuality);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::ApplyThumbnailSettings()
|
||||
{
|
||||
// 任何已缓存的 HBITMAP 都是按"旧 ThumbWidth"渲染的,尺寸不再可信 —— 一律清空。
|
||||
// 下一次响应到达时按新尺寸重建。带来的代价是切换尺寸后会有一个空窗期。
|
||||
ClearAllThumbnailCache();
|
||||
|
||||
// 列宽 + 行高 跟着 ThumbWidth 走(高度 = 9/16 比例 + 上下各 2px 留白)
|
||||
int thumbW = m_ThumbnailCfg.ThumbWidth;
|
||||
int thumbH = thumbW * 9 / 16;
|
||||
int rowH = thumbH + 4;
|
||||
if (rowH < 20) rowH = 20;
|
||||
|
||||
if (m_CList_Online.GetSafeHwnd()) {
|
||||
// 用 SetColumnVisible 控制 col 0 的存在感 —— 仅 SetColumn(width=0) 不够:
|
||||
// CListCtrlEx::AdjustColumnWidths 会在窗口拉伸时按 m_Columns[i].Percent
|
||||
// 重新分配宽度,让"已关闭"的列 0 再次得到约 5% 的可见宽度。
|
||||
// SetColumnVisible 会把 m_Columns[0].Visible 置为 false,AdjustColumnWidths
|
||||
// 看到不可见列直接给 0 且不计入 visiblePercent,把空间让给其它列。
|
||||
m_CList_Online.SetColumnVisible(COL_THUMBNAIL, m_ThumbnailCfg.Enabled ? TRUE : FALSE);
|
||||
if (m_ThumbnailCfg.Enabled) {
|
||||
// 启用时,把宽度强制设回我们想要的 thumbW + 10,
|
||||
// SetColumnVisible 内部 AdjustColumnWidths 是按 percent 算出来的可能不一样
|
||||
LVCOLUMN col = {};
|
||||
col.mask = LVCF_WIDTH;
|
||||
col.cx = thumbW + 10;
|
||||
m_CList_Online.SetColumn(COL_THUMBNAIL, &col);
|
||||
}
|
||||
|
||||
// 行高 ImageList 管理:
|
||||
// - 开启缩略图 → 装 1×rowH 的 dummy ImageList(rowH ≈ 38 px 撑高行)
|
||||
// - 关闭缩略图 → 装 1×1 的 dummy ImageList(行高 = max(字体高, 1) ≈ 字体原生高)
|
||||
// 之所以不是 SetImageList(NULL) "完全拆掉":Win32 ListView 在 ImageList 由 X→NULL
|
||||
// 时**不重测行高**(行高被锁死在最后一次有 ImageList 时的值),WM_SETFONT 等踢一脚
|
||||
// 的招数也都不可靠。统一始终装一张 ImageList,只让"高度数值"变化 → ListView 每次都
|
||||
// 重测行高。1×1 的 ImageList 用户感知不到(视觉上等同于无 ImageList)。
|
||||
int targetRowH = m_ThumbnailCfg.Enabled ? rowH : 1;
|
||||
if (targetRowH != m_thumbRowHeightApplied) {
|
||||
m_CList_Online.SetImageList(NULL, LVSIL_SMALL);
|
||||
if (m_thumbRowHeightImgList.GetSafeHandle()) {
|
||||
m_thumbRowHeightImgList.DeleteImageList();
|
||||
}
|
||||
m_thumbRowHeightImgList.Create(1, targetRowH, ILC_COLOR | ILC_MASK, 1, 1);
|
||||
m_CList_Online.SetImageList(&m_thumbRowHeightImgList, LVSIL_SMALL);
|
||||
m_thumbRowHeightApplied = targetRowH;
|
||||
}
|
||||
}
|
||||
|
||||
// 计时器:启用时按 1 秒固定 tick 节奏轮询(per-host 各自的 nextDueTick 决定真发送)
|
||||
KillTimer(TIMER_THUMBNAIL_REFRESH);
|
||||
if (m_ThumbnailCfg.Enabled) {
|
||||
SetTimer(TIMER_THUMBNAIL_REFRESH, 1000, nullptr);
|
||||
} else {
|
||||
// 关闭时连"在飞"和"待发"也清掉,避免之后误派发
|
||||
m_ThumbnailPending.clear();
|
||||
m_ThumbNextDueTick.clear();
|
||||
}
|
||||
|
||||
// 触发一次列表重绘,让列宽变化生效
|
||||
if (m_CList_Online.GetSafeHwnd()) {
|
||||
m_CList_Online.Invalidate(FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::ClearThumbnailCacheEntry(uint64_t clientID)
|
||||
{
|
||||
auto it = m_HostThumbnails.find(clientID);
|
||||
if (it == m_HostThumbnails.end()) return;
|
||||
if (it->second.bmp) ::DeleteObject(it->second.bmp);
|
||||
m_HostThumbnails.erase(it);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::ClearAllThumbnailCache()
|
||||
{
|
||||
for (auto& kv : m_HostThumbnails) {
|
||||
if (kv.second.bmp) ::DeleteObject(kv.second.bmp);
|
||||
}
|
||||
m_HostThumbnails.clear();
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::InvalidateHostRow(uint64_t clientID)
|
||||
{
|
||||
if (!m_CList_Online.GetSafeHwnd()) return;
|
||||
// 查 clientID -> m_HostList 索引 -> 当前过滤后的可见行
|
||||
CLock L(m_cs);
|
||||
auto it = m_ClientIndex.find(clientID);
|
||||
if (it == m_ClientIndex.end()) return;
|
||||
size_t hostIdx = it->second;
|
||||
for (size_t fi = 0; fi < m_FilteredIndices.size(); ++fi) {
|
||||
if (m_FilteredIndices[fi] == hostIdx) {
|
||||
m_CList_Online.RedrawItems((int)fi, (int)fi);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 把收到的 JPEG 解码 + 等比缩到 ThumbWidth × thumbH,存为 24bpp DIB。
|
||||
// 失败时静默丢弃(保留旧缓存或保持空),下一轮重试。
|
||||
void CMy2015RemoteDlg::CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes)
|
||||
{
|
||||
if (!jpeg || bytes == 0) return;
|
||||
|
||||
// GDI+ 解码(与 CPreviewTipWnd::SetImageFromJpeg 同模式)
|
||||
HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||
if (!hMem) return;
|
||||
void* p = ::GlobalLock(hMem);
|
||||
if (!p) { ::GlobalFree(hMem); return; }
|
||||
memcpy(p, jpeg, bytes);
|
||||
::GlobalUnlock(hMem);
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) {
|
||||
::GlobalFree(hMem);
|
||||
return;
|
||||
}
|
||||
// 注意:本 TU 顶部 `#define new DEBUG_NEW`(MFC 模式),而 Gdiplus::GdiplusBase::operator new
|
||||
// 不接受 DEBUG_NEW 的 (size, file, line) 三参形式 → 局部撤宏,构造完再恢复。
|
||||
#pragma push_macro("new")
|
||||
#undef new
|
||||
std::unique_ptr<Gdiplus::Bitmap> bmp(new Gdiplus::Bitmap(stream, FALSE));
|
||||
#pragma pop_macro("new")
|
||||
stream->Release(); // 接管 hMem 的释放
|
||||
if (!bmp || bmp->GetLastStatus() != Gdiplus::Ok) return;
|
||||
|
||||
int dstW = m_ThumbnailCfg.ThumbWidth;
|
||||
int dstH = dstW * 9 / 16;
|
||||
if (dstW <= 0 || dstH <= 0) return;
|
||||
|
||||
// 渲染到一张 24bpp HBITMAP(DC 兼容 + DIB 友好)
|
||||
HDC hScreen = ::GetDC(nullptr);
|
||||
HDC hMemDC = ::CreateCompatibleDC(hScreen);
|
||||
HBITMAP hbm = ::CreateCompatibleBitmap(hScreen, dstW, dstH);
|
||||
::ReleaseDC(nullptr, hScreen);
|
||||
if (!hMemDC || !hbm) {
|
||||
if (hMemDC) ::DeleteDC(hMemDC);
|
||||
if (hbm) ::DeleteObject(hbm);
|
||||
return;
|
||||
}
|
||||
HBITMAP hbmOld = (HBITMAP)::SelectObject(hMemDC, hbm);
|
||||
|
||||
// 底色:让等比 letterbox 留白显眼一点(深灰)
|
||||
RECT rcAll = { 0, 0, dstW, dstH };
|
||||
HBRUSH hBrush = ::CreateSolidBrush(RGB(50, 50, 50));
|
||||
::FillRect(hMemDC, &rcAll, hBrush);
|
||||
::DeleteObject(hBrush);
|
||||
|
||||
// 等比缩放图像,居中放置(letterbox)
|
||||
{
|
||||
Gdiplus::Graphics g(hMemDC);
|
||||
g.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
|
||||
g.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
|
||||
double iw = (double)bmp->GetWidth();
|
||||
double ih = (double)bmp->GetHeight();
|
||||
double sx = dstW / iw;
|
||||
double sy = dstH / ih;
|
||||
double s = sx < sy ? sx : sy;
|
||||
int dw = (int)(iw * s + 0.5);
|
||||
int dh = (int)(ih * s + 0.5);
|
||||
int dx = (dstW - dw) / 2;
|
||||
int dy = (dstH - dh) / 2;
|
||||
g.DrawImage(bmp.get(), dx, dy, dw, dh);
|
||||
}
|
||||
|
||||
::SelectObject(hMemDC, hbmOld);
|
||||
::DeleteDC(hMemDC);
|
||||
|
||||
// 替换/插入缓存
|
||||
ClearThumbnailCacheEntry(clientID);
|
||||
ThumbCacheEntry e;
|
||||
e.bmp = hbm; e.w = dstW; e.h = dstH;
|
||||
m_HostThumbnails[clientID] = e;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SendThumbnailRequest(context* ctx)
|
||||
{
|
||||
if (!ctx) return;
|
||||
if (++m_ThumbnailReqId == 0) m_ThumbnailReqId = 1;
|
||||
SendScreenPreviewRequest(ctx, m_ThumbnailReqId,
|
||||
(WORD)m_ThumbnailCfg.NetReqWidth,
|
||||
(BYTE)m_ThumbnailCfg.JpegQuality);
|
||||
m_ThumbnailPending.insert(ctx->GetClientID());
|
||||
}
|
||||
|
||||
// 主循环(每秒 tick 一次):扫描在线主机,触发到期 + 活跃的请求
|
||||
void CMy2015RemoteDlg::TickThumbnailRefresh()
|
||||
{
|
||||
if (!m_ThumbnailCfg.Enabled) return;
|
||||
|
||||
DWORD now = ::GetTickCount();
|
||||
DWORD intervalMs = (DWORD)m_ThumbnailCfg.RefreshIntervalSec * 1000;
|
||||
|
||||
// 拷一份 clientId 列表,避免在 m_cs 内长时间持锁
|
||||
std::vector<uint64_t> ids;
|
||||
std::vector<uint64_t> loopOpen;
|
||||
{
|
||||
CLock L(m_cs);
|
||||
ids.reserve(m_HostList.size());
|
||||
for (auto* c : m_HostList) {
|
||||
if (c) ids.push_back(c->GetClientID());
|
||||
}
|
||||
}
|
||||
// 同步:开着循环窗口的主机本轮跳过 —— 循环窗的响应会顺手填缓存(OnPreviewResponse)
|
||||
for (const auto& kv : m_LoopTips) loopOpen.push_back(kv.first);
|
||||
std::set<uint64_t> loopSet(loopOpen.begin(), loopOpen.end());
|
||||
|
||||
// 限速:每跳最多发 8 台请求,避免初次铺满时瞬时拥挤
|
||||
const int kMaxPerTick = 8;
|
||||
int sent = 0;
|
||||
for (uint64_t cid : ids) {
|
||||
if (sent >= kMaxPerTick) break;
|
||||
|
||||
// 已在飞,不重复
|
||||
if (m_ThumbnailPending.count(cid)) continue;
|
||||
// 开着循环窗,跳过
|
||||
if (loopSet.count(cid)) continue;
|
||||
|
||||
// 到期判定(首次出现时也算到期:插入 due=now)
|
||||
auto itDue = m_ThumbNextDueTick.find(cid);
|
||||
if (itDue == m_ThumbNextDueTick.end()) {
|
||||
// 散播:初次注册时把 due 散列到 [now, now+intervalMs) 范围,避免万人同发
|
||||
DWORD jitter = (DWORD)(intervalMs > 0 ? (cid % intervalMs) : 0);
|
||||
m_ThumbNextDueTick[cid] = now + jitter;
|
||||
continue;
|
||||
}
|
||||
if ((LONG)(itDue->second - now) > 0) continue; // 未到期
|
||||
|
||||
// 三道门:支持能力位 + 非 Locked/Inactive
|
||||
bool ok = false;
|
||||
context* ctx = nullptr;
|
||||
{
|
||||
CLock L(m_cs);
|
||||
ctx = FindHostNoLock(cid);
|
||||
if (ctx && ctx->SupportsScreenPreview()) {
|
||||
CString a = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
bool isIdleOrLocked =
|
||||
a.Find(_T("Locked")) == 0 || a.Find(_T("Inactive")) == 0;
|
||||
if (!isIdleOrLocked) ok = true;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
// 不活跃 / 不支持:仍把 due 推到下一周期,避免每 tick 都判一次
|
||||
itDue->second = now + intervalMs;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 发送(再次在锁内拿 ctx,因为 ok 判定后到此处可能被下线)
|
||||
{
|
||||
CLock L(m_cs);
|
||||
ctx = FindHostNoLock(cid);
|
||||
if (ctx) {
|
||||
SendThumbnailRequest(ctx);
|
||||
++sent;
|
||||
}
|
||||
}
|
||||
itDue->second = now + intervalMs;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineUnauthorize()
|
||||
{
|
||||
@@ -8390,11 +8813,135 @@ void CMy2015RemoteDlg::OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
}
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
if (!ctx) return;
|
||||
int r = m_ClientMap->GetClientMapInteger(ctx->GetClientID(), MAP_LEVEL);
|
||||
if (r >= 1) pLVCD->clrText = RGB(0, 0, 255); // 字体蓝
|
||||
if (r >= 2) pLVCD->clrText = RGB(255, 0, 0); // 字体红
|
||||
if (r >= 3) pLVCD->clrTextBk = RGB(255, 160, 160); // 背景红
|
||||
if (ctx) {
|
||||
int r = m_ClientMap->GetClientMapInteger(ctx->GetClientID(), MAP_LEVEL);
|
||||
if (r >= 1) pLVCD->clrText = RGB(0, 0, 255); // 字体蓝
|
||||
if (r >= 2) pLVCD->clrText = RGB(255, 0, 0); // 字体红
|
||||
if (r >= 3) pLVCD->clrTextBk = RGB(255, 160, 160); // 背景红
|
||||
}
|
||||
// 启用缩略图时:
|
||||
// 1) 请求 NOTIFYSUBITEMDRAW —— 拿到对每个 subitem 的回调权
|
||||
// 在 col 0 上返回 CDRF_SKIPDEFAULT 让系统**不画** col 0(消除空白闪烁源头)
|
||||
// 2) 请求 NOTIFYPOSTPAINT —— 在 POSTPAINT 阶段用格内 DB 把缩略图画进去
|
||||
// 双管齐下:如果 SUBITEM 在 col 0 不触发,POSTPAINT 仍能兜底(退回旧行为);
|
||||
// 如果 SUBITEM 在 col 0 触发了,系统从此不再写 col 0,闪烁源被掐死。
|
||||
*pResult = m_ThumbnailCfg.Enabled
|
||||
? (CDRF_NOTIFYSUBITEMDRAW | CDRF_NOTIFYPOSTPAINT)
|
||||
: 0;
|
||||
return;
|
||||
}
|
||||
|
||||
case CDDS_ITEMPREPAINT | CDDS_SUBITEM: {
|
||||
if (!m_ThumbnailCfg.Enabled) { *pResult = CDRF_DODEFAULT; return; }
|
||||
// 仅当系统真的回调了 col 0 的 subitem 阶段时生效。CDRF_SKIPDEFAULT 让系统
|
||||
// 跳过自己对 col 0 的所有绘制(label / 选中条延伸 / 焦点框等),后续完全
|
||||
// 由 POSTPAINT 接管,从而消除"空白 col 0 一闪"的视觉污染。
|
||||
if (pLVCD->iSubItem == COL_THUMBNAIL) {
|
||||
*pResult = CDRF_SKIPDEFAULT;
|
||||
return;
|
||||
}
|
||||
*pResult = CDRF_DODEFAULT;
|
||||
return;
|
||||
}
|
||||
|
||||
case CDDS_ITEMPOSTPAINT: {
|
||||
if (!m_ThumbnailCfg.Enabled) return;
|
||||
|
||||
int nRow = static_cast<int>(pLVCD->nmcd.dwItemSpec);
|
||||
uint64_t clientID = 0;
|
||||
bool supportsPreview = false; // 区分"暂未到帧"和"协议不支持"两种占位
|
||||
{
|
||||
CLock L(m_cs);
|
||||
if (nRow >= 0 && nRow < (int)m_FilteredIndices.size()) {
|
||||
size_t realIdx = m_FilteredIndices[nRow];
|
||||
if (realIdx < m_HostList.size() && m_HostList[realIdx]) {
|
||||
clientID = m_HostList[realIdx]->GetClientID();
|
||||
supportsPreview = m_HostList[realIdx]->SupportsScreenPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientID == 0) return;
|
||||
bool isSelected = (m_CList_Online.GetItemState(nRow, LVIS_SELECTED) & LVIS_SELECTED) != 0;
|
||||
|
||||
// 用 GetItemRect + GetColumnWidth(0) 算 col 0 的矩形 —— 比 pLVCD->nmcd.rc 更
|
||||
// 可靠:后者在 POSTPAINT 阶段对虚拟列表有时返回空 / 行片段。
|
||||
RECT rcRow = {};
|
||||
if (!m_CList_Online.GetItemRect(nRow, &rcRow, LVIR_BOUNDS)) return;
|
||||
int col0w = m_CList_Online.GetColumnWidth(0);
|
||||
if (col0w <= 0) return;
|
||||
int cellW = col0w;
|
||||
int cellH = rcRow.bottom - rcRow.top;
|
||||
if (cellW <= 0 || cellH <= 0) return;
|
||||
RECT rcCell = { rcRow.left, rcRow.top, rcRow.left + cellW, rcRow.bottom };
|
||||
|
||||
HDC hdcDst = pLVCD->nmcd.hdc;
|
||||
|
||||
// 格内双缓冲:把整格(底色 + 占位 / 缩略图)全部画到离屏 HBITMAP 上,
|
||||
// 最后一次 BitBlt 落地,保证从用户视角看 col 0 只有"旧内容 → 新内容"两种态。
|
||||
// 解决 LVS_OWNERDATA + LVS_EX_DOUBLEBUFFER + CDDS_ITEMPOSTPAINT 这个组合下
|
||||
// POSTPAINT 偶发绕开 back buffer 写屏导致的占位"一闪"。
|
||||
HDC hMemDC = ::CreateCompatibleDC(hdcDst);
|
||||
HBITMAP hbmCell = ::CreateCompatibleBitmap(hdcDst, cellW, cellH);
|
||||
if (!hMemDC || !hbmCell) {
|
||||
if (hMemDC) ::DeleteDC(hMemDC);
|
||||
if (hbmCell) ::DeleteObject(hbmCell);
|
||||
return;
|
||||
}
|
||||
HBITMAP hbmOld = (HBITMAP)::SelectObject(hMemDC, hbmCell);
|
||||
|
||||
// 全部画在 (0,0)-(cellW,cellH) 局部坐标
|
||||
RECT rcLocal = { 0, 0, cellW, cellH };
|
||||
HBRUSH hBg = ::GetSysColorBrush(isSelected ? COLOR_HIGHLIGHT : COLOR_WINDOW);
|
||||
::FillRect(hMemDC, &rcLocal, hBg);
|
||||
|
||||
auto it = m_HostThumbnails.find(clientID);
|
||||
bool hasBmp = (it != m_HostThumbnails.end() && it->second.bmp);
|
||||
if (!hasBmp) {
|
||||
// 占位文本直接浮在 cell 底色上,不再画内嵌灰框 —— 与表格背景融合。
|
||||
// - 支持预览但还没收到帧 → "…"
|
||||
// - 不支持预览的老客户端 → "N/A"
|
||||
// - 选中态:用系统 HIGHLIGHTTEXT 保证白底/蓝底下都清晰可读
|
||||
HFONT hListFont = (HFONT)m_CList_Online.SendMessage(WM_GETFONT, 0, 0);
|
||||
HFONT hOldFont = hListFont ? (HFONT)::SelectObject(hMemDC, hListFont) : nullptr;
|
||||
::SetBkMode(hMemDC, TRANSPARENT);
|
||||
::SetTextColor(hMemDC, ::GetSysColor(
|
||||
isSelected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT));
|
||||
const wchar_t* placeholder = supportsPreview ? L"…" : L"N/A";
|
||||
::DrawTextW(hMemDC, placeholder, -1, &rcLocal,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
if (hOldFont) ::SelectObject(hMemDC, hOldFont);
|
||||
} else {
|
||||
const ThumbCacheEntry& te = it->second;
|
||||
int maxW = cellW - 2, maxH = cellH - 2;
|
||||
if (maxW < 4) maxW = 4;
|
||||
if (maxH < 4) maxH = 4;
|
||||
|
||||
double sx = (double)maxW / te.w;
|
||||
double sy = (double)maxH / te.h;
|
||||
double s = sx < sy ? sx : sy;
|
||||
int dw = (int)(te.w * s + 0.5);
|
||||
int dh = (int)(te.h * s + 0.5);
|
||||
int dx = (cellW - dw) / 2;
|
||||
int dy = (cellH - dh) / 2;
|
||||
|
||||
HDC hThumbDC = ::CreateCompatibleDC(hMemDC);
|
||||
HBITMAP hThumbOld = (HBITMAP)::SelectObject(hThumbDC, te.bmp);
|
||||
::SetStretchBltMode(hMemDC, HALFTONE);
|
||||
::SetBrushOrgEx(hMemDC, 0, 0, nullptr);
|
||||
::StretchBlt(hMemDC, dx, dy, dw, dh, hThumbDC, 0, 0, te.w, te.h, SRCCOPY);
|
||||
::SelectObject(hThumbDC, hThumbOld);
|
||||
::DeleteDC(hThumbDC);
|
||||
}
|
||||
|
||||
// 一次性原子刷屏
|
||||
::BitBlt(hdcDst, rcCell.left, rcCell.top, cellW, cellH, hMemDC, 0, 0, SRCCOPY);
|
||||
|
||||
::SelectObject(hMemDC, hbmOld);
|
||||
::DeleteObject(hbmCell);
|
||||
::DeleteDC(hMemDC);
|
||||
|
||||
*pResult = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9434,10 +9981,22 @@ void CMy2015RemoteDlg::OnParamFileV2()
|
||||
SubMenu->CheckMenuItem(ID_PARAM_FILE_V2, m_bEnableFileV2 ? MF_CHECKED : MF_UNCHECKED);
|
||||
THIS_CFG.SetInt("settings", "EnableFileV2", m_bEnableFileV2 ? 1 : 0);
|
||||
Mprintf("文件传输V2: %s\n", m_bEnableFileV2 ? "启用" : "禁用");
|
||||
MessageBoxA(m_bEnableFileV2 ? _TR("已启用文件传输协议V2,支持断点续传和C2C传输。") : _TR("已关闭文件传输协议V2。"),
|
||||
MessageBoxA(m_bEnableFileV2 ? _TR("已启用文件传输协议V2,支持断点续传和C2C传输。") : _TR("已关闭文件传输协议V2。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnParamThumbnailPreview()
|
||||
{
|
||||
m_ThumbnailCfg.Enabled = !m_ThumbnailCfg.Enabled;
|
||||
CMenu* SubMenu = m_MainMenu.GetSubMenu(2);
|
||||
if (SubMenu)
|
||||
SubMenu->CheckMenuItem(ID_PARAM_THUMBNAIL_PREVIEW,
|
||||
m_ThumbnailCfg.Enabled ? MF_CHECKED : MF_UNCHECKED);
|
||||
SaveThumbnailSettingsToCfg();
|
||||
ApplyThumbnailSettings(); // 内部处理列宽/行高/定时器/缓存切换
|
||||
Mprintf("主机列表预览图: %s\n", m_ThumbnailCfg.Enabled ? "启用" : "禁用");
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnParamRunAsUser()
|
||||
{
|
||||
CMenu* SubMenu = m_MainMenu.GetSubMenu(2);
|
||||
|
||||
Reference in New Issue
Block a user