Feature: screen preview thumbnail on host double-click
Server sends COMMAND_SCREEN_PREVIEW_REQ when user double-clicks an active (non-Locked/Inactive) host that advertises CLIENT_CAP_SCREEN_PREVIEW. Client BitBlts primary screen, encodes to JPEG via GDI+ and replies. The existing STATIC tooltip is replaced with a self-drawn CPreviewTipWnd showing the thumbnail above the host info text, with wide-character rendering so the popup also works on non-Chinese servers. - Quality tiers reuse QualityProfile pattern: PreviewProfile + 6 levels driven by GetTargetQualityLevel (FRP-aware), with 4K/ultrawide auto upscale on Ultra/High tiers up to min(screenWidth/4, 1280). - Client limits to 1 in-flight capture via atomic counter to defend against flood/DoS; Send2Server is already mutex-serialized. - Server validates responses by reqId only (single in-flight tip); 4s arrival timeout marks "preview unavailable" without blocking the text fallback path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
#include "FrpsForSubDlg.h"
|
||||
#include "PluginSettingsDlg.h"
|
||||
#include "TriggerSettingsDlg.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
#include "common/key.h"
|
||||
#include "UIBranding.h"
|
||||
|
||||
@@ -86,6 +87,7 @@
|
||||
#define TIMER_REFRESH_LIST 5
|
||||
#define TIMER_STATUSBAR_UPDATE 6
|
||||
#define TIMER_STATUSBAR_INIT 7
|
||||
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
||||
#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"
|
||||
@@ -799,6 +801,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
|
||||
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
|
||||
ON_MESSAGE(WM_UPDATE_ACTIVEWND, UpdateUserEvent)
|
||||
ON_MESSAGE(WM_PREVIEW_RESPONSE, OnPreviewResponse)
|
||||
ON_WM_HELPINFO()
|
||||
ON_COMMAND(ID_ONLINE_SHARE, &CMy2015RemoteDlg::OnOnlineShare)
|
||||
ON_COMMAND(ID_TOOL_AUTH, &CMy2015RemoteDlg::OnToolAuth)
|
||||
@@ -3039,6 +3042,12 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
||||
if (nIDEvent == TIMER_CLOSEWND) {
|
||||
DeletePopupWindow();
|
||||
}
|
||||
if (nIDEvent == TIMER_PREVIEW_ARRIVAL) {
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
if (m_pPreviewTip && ::IsWindow(m_pPreviewTip->GetSafeHwnd())) {
|
||||
m_pPreviewTip->MarkPreviewUnavailable();
|
||||
}
|
||||
}
|
||||
if (nIDEvent == TIMER_CLEAR_BALLOON) {
|
||||
KillTimer(TIMER_CLEAR_BALLOON);
|
||||
|
||||
@@ -3354,7 +3363,10 @@ void CMy2015RemoteDlg::DeletePopupWindow(BOOL bForce)
|
||||
m_pFloatingTip->DestroyWindow();
|
||||
|
||||
SAFE_DELETE(m_pFloatingTip);
|
||||
m_pPreviewTip = nullptr;
|
||||
m_PreviewReqId = 0;
|
||||
KillTimer(TIMER_CLOSEWND);
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
}
|
||||
|
||||
|
||||
@@ -5438,6 +5450,15 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
case 137: // 心跳【L】
|
||||
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
|
||||
break;
|
||||
case TOKEN_SCREEN_PREVIEW_RSP: {
|
||||
// 屏幕预览响应:把整个包拷到堆上,转给主线程处理(IO 线程不接触 UI)
|
||||
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
||||
auto* pkt = new std::vector<BYTE>(szBuffer, szBuffer + len);
|
||||
if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)pkt)) {
|
||||
delete pkt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SOCKET_DLLLOADER: {// 请求DLL【L】
|
||||
auto len = ContextObject->InDeCompressedBuffer.GetBufferLength();
|
||||
bool is64Bit = len > 1 ? ContextObject->InDeCompressedBuffer.GetBYTE(1) : false;
|
||||
@@ -7558,36 +7579,141 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
CPoint pt;
|
||||
GetCursorPos(&pt);
|
||||
|
||||
// 清理旧提示
|
||||
// 清理旧提示(DeletePopupWindow 会同时清掉 m_pPreviewTip / m_PreviewReqId / 定时器)
|
||||
DeletePopupWindow(TRUE);
|
||||
|
||||
// 创建提示窗口
|
||||
m_pFloatingTip = new CWnd();
|
||||
int width = res[RES_FILE_PATH].GetLength() * 10 + 36;
|
||||
width = min(max(width, 360), 800);
|
||||
CRect rect(pt.x, pt.y, pt.x + width, pt.y + 120); // 宽度、高度
|
||||
// 屏幕预览触发条件:客户端支持能力位 + 当前活动窗口非 Locked/Inactive
|
||||
CString activeWnd = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
bool isIdleOrLocked =
|
||||
activeWnd.Find(_T("Locked")) == 0 || activeWnd.Find(_T("Inactive")) == 0;
|
||||
bool wantPreview = ctx->SupportsScreenPreview() && !isIdleOrLocked;
|
||||
|
||||
BOOL bOk = m_pFloatingTip->CreateEx(
|
||||
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
||||
_T("STATIC"),
|
||||
strText,
|
||||
WS_POPUP | WS_VISIBLE | WS_BORDER | SS_LEFT | SS_NOTIFY,
|
||||
rect,
|
||||
this,
|
||||
0);
|
||||
// 创建新预览浮窗
|
||||
WORD maxWidth = 480;
|
||||
BYTE jpegQ = 70;
|
||||
if (wantPreview) {
|
||||
ChooseScreenPreviewParams(ctx, maxWidth, jpegQ);
|
||||
}
|
||||
|
||||
auto* tip = new CPreviewTipWnd();
|
||||
// 项目是 MBCS(CP_ACP),浮窗内部用宽字符渲染(避免跨语言系统乱码)。
|
||||
// 这里把 strText (CStringA) 按 CP_ACP 转 wide。
|
||||
CStringW textW;
|
||||
{
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, textW.GetBufferSetLength(wlen - 1), wlen);
|
||||
textW.ReleaseBuffer(wlen - 1);
|
||||
}
|
||||
}
|
||||
BOOL bOk = tip->Create(this, pt, textW, wantPreview ? maxWidth : 0);
|
||||
if (bOk) {
|
||||
m_pFloatingTip->SetFont(GetFont());
|
||||
m_pFloatingTip->ShowWindow(SW_SHOW);
|
||||
SetTimer(TIMER_CLOSEWND, 5000, nullptr);
|
||||
m_pFloatingTip = tip;
|
||||
m_pPreviewTip = tip;
|
||||
// 整体超时:有预览 8 秒(图到达后会续命 5s),纯文本 5 秒
|
||||
SetTimer(TIMER_CLOSEWND, wantPreview ? 8000 : 5000, nullptr);
|
||||
|
||||
if (wantPreview) {
|
||||
// 序号 0 视为"无效",从 1 开始递增
|
||||
if (++m_PreviewReqId == 0) m_PreviewReqId = 1;
|
||||
tip->SetReqId(m_PreviewReqId);
|
||||
SendScreenPreviewRequest(ctx, m_PreviewReqId, maxWidth, jpegQ);
|
||||
// 4 秒未收到 → 在浮窗里标"预览不可用"
|
||||
SetTimer(TIMER_PREVIEW_ARRIVAL, 4000, nullptr);
|
||||
}
|
||||
} else {
|
||||
SAFE_DELETE(m_pFloatingTip);
|
||||
delete tip;
|
||||
}
|
||||
}
|
||||
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile
|
||||
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
|
||||
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
|
||||
void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
|
||||
{
|
||||
int ping = 0;
|
||||
{
|
||||
CString p = ctx->GetClientData(ONLINELIST_PING);
|
||||
if (!p.IsEmpty()) ping = atoi(p.GetString());
|
||||
}
|
||||
// ping 未上报(0/缺失)→ 当中等档处理,避免误判为局域网刷过大缩略图
|
||||
int rttForLevel = ping > 0 ? ping : 250;
|
||||
|
||||
int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
|
||||
const PreviewProfile& prof = GetScreenPreviewProfile(level);
|
||||
int base = prof.maxWidth;
|
||||
BYTE q = (BYTE)prof.jpegQuality;
|
||||
|
||||
// 4K/超宽屏自适应(仅 Ultra/High 启用,弱网档不放大保带宽)
|
||||
if (level <= QUALITY_HIGH) {
|
||||
CString resStr = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
if (!resStr.IsEmpty()) {
|
||||
int sw = 0, sh = 0;
|
||||
if (_stscanf_s(resStr, _T("%dx%d"), &sw, &sh) == 2 && sw >= 2560) {
|
||||
int adj = sw / 4;
|
||||
if (adj > 1280) adj = 1280;
|
||||
if (adj > base) base = adj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = (WORD)base;
|
||||
jpegQuality = q;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality)
|
||||
{
|
||||
if (!ctx) return;
|
||||
ScreenPreviewReq req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.cmd = COMMAND_SCREEN_PREVIEW_REQ;
|
||||
req.reqId = reqId;
|
||||
req.maxWidth = maxWidth;
|
||||
req.jpegQuality = jpegQuality;
|
||||
ctx->Send2Client((PBYTE)&req, sizeof(req));
|
||||
}
|
||||
|
||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程)
|
||||
// wParam: 未使用(避免依赖 WPARAM 宽度,Win32 上 32 位会截 64 位 clientID)
|
||||
// LPARAM: 指向堆分配的 std::vector<BYTE>*(含完整响应包:[Header|JPEG]),处理完必须 delete。
|
||||
// 校验思路:reqId 由对话框单调递增、每次双击重置 + 同时只有 1 个 in-flight tip,足以
|
||||
// 过滤过期/乱串响应,无需再叠加 clientID 校验。
|
||||
LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
||||
{
|
||||
auto* pkt = reinterpret_cast<std::vector<BYTE>*>(lParam);
|
||||
if (!pkt) return 0;
|
||||
std::unique_ptr<std::vector<BYTE>> guard(pkt);
|
||||
|
||||
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;
|
||||
|
||||
// 序号校验:和当前期待的 reqId 不符 → 过期响应,丢弃
|
||||
if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0;
|
||||
if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0;
|
||||
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
|
||||
if (hdr->status != SCREEN_PREVIEW_OK || hdr->bytes == 0 ||
|
||||
pkt->size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes ||
|
||||
hdr->format != SCREEN_PREVIEW_FMT_JPEG)
|
||||
{
|
||||
m_pPreviewTip->MarkPreviewUnavailable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const BYTE* jpeg = pkt->data() + sizeof(ScreenPreviewRspHeader);
|
||||
m_pPreviewTip->SetImageFromJpeg(jpeg, hdr->bytes);
|
||||
|
||||
// 图到达后续命 5 秒
|
||||
KillTimer(TIMER_CLOSEWND);
|
||||
SetTimer(TIMER_CLOSEWND, 5000, nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineUnauthorize()
|
||||
{
|
||||
|
||||
@@ -230,6 +230,10 @@ public:
|
||||
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
|
||||
// 显示用户上线信息
|
||||
CWnd* m_pFloatingTip = nullptr;
|
||||
// 屏幕预览:m_pFloatingTip 实际是 CPreviewTipWnd 时这里有同一指针的有类型副本,
|
||||
// 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
|
||||
class CPreviewTipWnd* m_pPreviewTip = nullptr;
|
||||
WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应
|
||||
// 记录 clientID(心跳更新)
|
||||
std::set<uint64_t> m_DirtyClients;
|
||||
// 待处理的上线/下线事件(批量更新减少闪烁)
|
||||
@@ -434,6 +438,12 @@ public:
|
||||
afx_msg void OnWhatIsThis();
|
||||
afx_msg void OnOnlineAuthorize();
|
||||
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
// 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数;4K/超宽屏在 LAN 档自适应放大
|
||||
void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const;
|
||||
// 发起预览请求;reqId 应与 m_PreviewReqId 同步
|
||||
void SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality);
|
||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程处理)
|
||||
afx_msg LRESULT OnPreviewResponse(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnOnlineUnauthorize();
|
||||
afx_msg void OnToolRequestAuth();
|
||||
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="proxy\HPSocket.h" />
|
||||
<ClInclude Include="proxy\HPTypeDef.h" />
|
||||
@@ -388,6 +389,7 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="msvc_compat.c" />
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -185,6 +186,7 @@
|
||||
<ClInclude Include="WebPage.h" />
|
||||
<ClInclude Include="SimpleWebSocket.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
243
server/2015Remote/PreviewTipWnd.cpp
Normal file
243
server/2015Remote/PreviewTipWnd.cpp
Normal file
@@ -0,0 +1,243 @@
|
||||
// PreviewTipWnd.cpp
|
||||
#include "stdafx.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
namespace {
|
||||
constexpr int PADDING = 8;
|
||||
constexpr int IMAGE_TEXT_GAP = 6; // 图像与下方文本之间的留白
|
||||
constexpr int MIN_TEXT_W = 360;
|
||||
constexpr int MAX_TEXT_W = 720; // 文本可换行宽度上限(垂直布局,宽度不再受图像挤压)
|
||||
constexpr int MAX_IMAGE_W = 960; // 显示上限(防止 LAN 档 1280 的 JPEG 把窗口顶到屏幕外)
|
||||
constexpr int LOADING_W = 480; // 占位的最小宽度(图像未到达时)
|
||||
constexpr int LOADING_H = 270;
|
||||
} // namespace
|
||||
|
||||
BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
|
||||
ON_WM_PAINT()
|
||||
ON_WM_ERASEBKGND()
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CPreviewTipWnd::CPreviewTipWnd() = default;
|
||||
|
||||
CPreviewTipWnd::~CPreviewTipWnd()
|
||||
{
|
||||
// m_image 通过 unique_ptr 自动释放
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW)
|
||||
{
|
||||
m_text = text;
|
||||
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) {
|
||||
m_imageReserveW = LOADING_W;
|
||||
m_imageReserveH = LOADING_H;
|
||||
}
|
||||
|
||||
// 创建字体(项目是 MBCS,但浮窗用宽字符;显式 LOGFONTW + W 版 API 避免编码混淆)
|
||||
LOGFONTW lf = {};
|
||||
HFONT hSysFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
|
||||
if (hSysFont && ::GetObjectW(hSysFont, sizeof(lf), &lf) == 0) {
|
||||
hSysFont = nullptr;
|
||||
}
|
||||
if (!hSysFont) {
|
||||
lf.lfHeight = -12;
|
||||
wcscpy_s(lf.lfFaceName, L"Microsoft YaHei");
|
||||
}
|
||||
HFONT hF = ::CreateFontIndirectW(&lf);
|
||||
if (hF) m_font.Attach(hF);
|
||||
|
||||
// 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容
|
||||
LPCTSTR kClass = AfxRegisterWndClass(
|
||||
CS_SAVEBITS,
|
||||
::LoadCursor(NULL, IDC_ARROW),
|
||||
(HBRUSH)(COLOR_INFOBK + 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);
|
||||
if (!bOk) return FALSE;
|
||||
|
||||
RecalcLayoutAndResize();
|
||||
|
||||
ShowWindow(SW_SHOWNOACTIVATE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
|
||||
{
|
||||
if (!data || bytes == 0) return;
|
||||
|
||||
HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||
if (!hMem) return;
|
||||
void* p = ::GlobalLock(hMem);
|
||||
if (!p) { ::GlobalFree(hMem); return; }
|
||||
memcpy(p, data, bytes);
|
||||
::GlobalUnlock(hMem);
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) {
|
||||
::GlobalFree(hMem);
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<Bitmap> bmp(new Bitmap(stream, FALSE));
|
||||
stream->Release(); // CreateStreamOnHGlobal(..., TRUE) 释放 stream 时会释放 hMem
|
||||
|
||||
if (!bmp || bmp->GetLastStatus() != Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_image = std::move(bmp);
|
||||
m_hasImage = true;
|
||||
m_unavailable = false;
|
||||
RecalcLayoutAndResize();
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::MarkPreviewUnavailable()
|
||||
{
|
||||
if (m_hasImage) return; // 已经有图就不再覆盖
|
||||
m_unavailable = true;
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::RecalcLayoutAndResize()
|
||||
{
|
||||
HWND hWnd = GetSafeHwnd();
|
||||
if (!hWnd) return;
|
||||
|
||||
// 估算图像尺寸(先算图像,因为文本宽度要参考图像宽度对齐)
|
||||
if (m_image) {
|
||||
int iw = (int)m_image->GetWidth();
|
||||
int ih = (int)m_image->GetHeight();
|
||||
if (iw > MAX_IMAGE_W) {
|
||||
ih = (int)((double)ih * MAX_IMAGE_W / iw + 0.5);
|
||||
iw = MAX_IMAGE_W;
|
||||
}
|
||||
m_imageDrawW = iw;
|
||||
m_imageDrawH = ih;
|
||||
} else {
|
||||
m_imageDrawW = m_imageReserveW;
|
||||
m_imageDrawH = m_imageReserveH;
|
||||
}
|
||||
|
||||
// 文本换行宽度:与图像同宽(让文本视觉上对齐到图像下方),但不超过 MAX_TEXT_W
|
||||
// 没有图像时退化到 MAX_TEXT_W
|
||||
int textWrapW = m_imageDrawW > 0 ? min((int)MAX_TEXT_W, m_imageDrawW) : (int)MAX_TEXT_W;
|
||||
if (textWrapW < MIN_TEXT_W) textWrapW = MIN_TEXT_W;
|
||||
|
||||
// 估算文本尺寸(项目是 MBCS,但浮窗文本是宽字符,必须显式调用 W 版本)
|
||||
CClientDC dc(this);
|
||||
CFont* old = dc.SelectObject(&m_font);
|
||||
CRect rcText(0, 0, textWrapW, 32767);
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcText,
|
||||
DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
|
||||
m_textW = max((int)MIN_TEXT_W, rcText.Width());
|
||||
if (m_imageDrawW > 0) m_textW = max(m_textW, m_imageDrawW); // 至少与图像同宽
|
||||
m_textH = rcText.Height();
|
||||
dc.SelectObject(old);
|
||||
|
||||
int contentW = max(m_imageDrawW, m_textW);
|
||||
int gap = (m_imageDrawW > 0 && m_textH > 0) ? IMAGE_TEXT_GAP : 0;
|
||||
int totalW = PADDING + contentW + PADDING;
|
||||
int totalH = PADDING + m_imageDrawH + gap + m_textH + PADDING;
|
||||
|
||||
// 当前左上角
|
||||
CRect rc;
|
||||
GetWindowRect(&rc);
|
||||
int newX = rc.left;
|
||||
int newY = rc.top;
|
||||
|
||||
// 防止越出屏幕:右下夹紧到工作区
|
||||
HMONITOR hMon = MonitorFromPoint(CPoint(newX, newY), MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO mi = { sizeof(mi) };
|
||||
if (hMon && GetMonitorInfo(hMon, &mi)) {
|
||||
if (newX + totalW > mi.rcWork.right) newX = max((int)mi.rcWork.left, mi.rcWork.right - totalW);
|
||||
if (newY + totalH > mi.rcWork.bottom) newY = max((int)mi.rcWork.top, mi.rcWork.bottom - totalH);
|
||||
}
|
||||
|
||||
SetWindowPos(NULL, newX, newY, totalW, totalH, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::OnEraseBkgnd(CDC* /*pDC*/)
|
||||
{
|
||||
return TRUE; // 在 OnPaint 里整体填充
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::OnPaint()
|
||||
{
|
||||
CPaintDC pdc(this);
|
||||
CRect rcClient;
|
||||
GetClientRect(&rcClient);
|
||||
|
||||
// 双缓冲
|
||||
CDC memDC;
|
||||
memDC.CreateCompatibleDC(&pdc);
|
||||
CBitmap memBmp;
|
||||
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
||||
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
|
||||
|
||||
memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK));
|
||||
|
||||
CFont* oldFont = memDC.SelectObject(&m_font);
|
||||
memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
||||
memDC.SetBkMode(TRANSPARENT);
|
||||
|
||||
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::DrawImageArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
// 边框
|
||||
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
|
||||
|
||||
if (m_hasImage && m_image) {
|
||||
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 ...";
|
||||
UINT fmt = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
RECT r = rc;
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &r,
|
||||
DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX);
|
||||
}
|
||||
57
server/2015Remote/PreviewTipWnd.h
Normal file
57
server/2015Remote/PreviewTipWnd.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// PreviewTipWnd.h
|
||||
// 双击在线主机时弹出的浮窗:上方 JPEG 缩略图,下方主机信息文本。
|
||||
// 无图片时显示"加载预览…"占位,供 OnListClick 即时弹窗、收到响应后再 SetImage 重画。
|
||||
#pragma once
|
||||
|
||||
#include <afxwin.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Gdiplus { class Bitmap; }
|
||||
|
||||
class CPreviewTipWnd : public CWnd
|
||||
{
|
||||
public:
|
||||
CPreviewTipWnd();
|
||||
virtual ~CPreviewTipWnd();
|
||||
|
||||
// 创建浮窗。
|
||||
// anchor 屏幕坐标,浮窗左上角
|
||||
// text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
|
||||
// imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
|
||||
// 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本)
|
||||
BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW);
|
||||
|
||||
// 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。
|
||||
void SetImageFromJpeg(const BYTE* data, size_t bytes);
|
||||
// 标记预览不可用(请求超时 / 客户端报错)。
|
||||
void MarkPreviewUnavailable();
|
||||
|
||||
WORD GetReqId() const { return m_reqId; }
|
||||
void SetReqId(WORD id) { m_reqId = id; }
|
||||
|
||||
protected:
|
||||
afx_msg void OnPaint();
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
void RecalcLayoutAndResize();
|
||||
void DrawImageArea(CDC& dc, const CRect& rc);
|
||||
void DrawTextArea(CDC& dc, const CRect& rc);
|
||||
|
||||
CStringW m_text;
|
||||
int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
|
||||
int m_imageReserveH = 0; // 预留图像高度(按 16:9)
|
||||
int m_imageDrawW = 0; // 实际图像绘制宽度
|
||||
int m_imageDrawH = 0; // 实际图像绘制高度
|
||||
int m_textW = 0;
|
||||
int m_textH = 0;
|
||||
|
||||
bool m_hasImage = false;
|
||||
bool m_unavailable = false;
|
||||
std::unique_ptr<Gdiplus::Bitmap> m_image;
|
||||
|
||||
CFont m_font;
|
||||
WORD m_reqId = 0;
|
||||
};
|
||||
@@ -68,4 +68,11 @@ public:
|
||||
if (caps.IsEmpty()) return false;
|
||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_UTF8) != 0;
|
||||
}
|
||||
|
||||
// 检查客户端是否支持屏幕预览(双击主机时拉缩略图)。
|
||||
bool SupportsScreenPreview() const {
|
||||
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||
if (caps.IsEmpty()) return false;
|
||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_SCREEN_PREVIEW) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
#define WM_SHOWNOTIFY WM_USER+3031
|
||||
#define WM_DISCONNECT WM_USER+3032
|
||||
#define WM_OPENTERMINALDIALOG WM_USER+3033
|
||||
#define WM_PREVIEW_RESPONSE WM_USER+3034
|
||||
|
||||
#ifdef _UNICODE
|
||||
#if defined _M_IX86
|
||||
|
||||
Reference in New Issue
Block a user