Files
SimpleRemoter/server/2015Remote/PreviewTipWnd.cpp
yuanyuanxiang 566f5b8d42 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>
2026-05-07 18:17:28 +02:00

244 lines
8.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}