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