Files
SimpleRemoter/server/2015Remote/PreviewTipWnd.cpp

423 lines
16 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 "resource.h" // IDI_ICON_SNAPSHOT循环模式标题栏图标
#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()
ON_WM_DESTROY()
ON_WM_SIZE()
ON_WM_GETMINMAXINFO()
END_MESSAGE_MAP()
CPreviewTipWnd::CPreviewTipWnd() = default;
CPreviewTipWnd::~CPreviewTipWnd()
{
// m_image 通过 unique_ptr 自动释放
if (m_hIconSmall) { ::DestroyIcon(m_hIconSmall); m_hIconSmall = nullptr; }
if (m_hIconBig) { ::DestroyIcon(m_hIconBig); m_hIconBig = nullptr; }
}
BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW,
bool loopMode, const CStringW& windowTitle)
{
m_text = text;
m_loopMode = loopMode; // 注意:影响后续 SetImageFromJpeg / OnPaint / OnSize 行为
m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
if (m_imageReserveW > 0 && m_imageReserveW < LOADING_W) {
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);
// 窗口类:单发 tooltip 走 CS_SAVEBITS短时显示省 BitBlt
// 循环窗口是长存窗口,加 CS_HREDRAW|CS_VREDRAW 让拉伸时实时重绘
UINT classStyle = loopMode ? (CS_HREDRAW | CS_VREDRAW) : CS_SAVEBITS;
LPCTSTR kClass = AfxRegisterWndClass(
classStyle,
::LoadCursor(NULL, IDC_ARROW),
(HBRUSH)(COLOR_BTNFACE + 1),
NULL);
// 临时尺寸RecalcLayoutAndResize 会在创建后调整
CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
// 样式选择:
// 单发 tooltip —— WS_POPUP|WS_BORDER + EX_TOPMOST|TOOLWINDOW|NOACTIVATE
// (不激活、不上任务栏、置顶;光标移开会被主对话框关掉)
// 循环监视窗口 —— WS_OVERLAPPEDWINDOW (标题栏 / 系统菜单 / 最大化 / 最小化 / 可调整边框)
// + WS_EX_APPWINDOW强制独立任务栏图标便于多窗口切换
DWORD style = loopMode ? (WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN)
: (WS_POPUP | WS_BORDER);
DWORD styleEx = loopMode ? WS_EX_APPWINDOW
: (WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE);
// 单发模式下窗口文本无意义(无标题栏可显示);循环模式下用宽字符 SetWindowTextW 显式设。
BOOL bOk = CWnd::CreateEx(styleEx, kClass, _T(""), style, rc, pParent, 0);
if (!bOk) return FALSE;
if (loopMode && !windowTitle.IsEmpty()) {
::SetWindowTextW(GetSafeHwnd(), windowTitle);
}
// 循环模式:给窗口装上眼睛图标(标题栏 + 任务栏 + Alt-Tab
// ICO 资源里包含 16x16 与 32x32 两份ICON_SMALL/ICON_BIG 各取最佳尺寸。
// HICON 由本类持有,析构时 DestroyIconWM_SETICON 不转移所有权)。
if (loopMode) {
m_hIconSmall = (HICON)::LoadImage(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON_SNAPSHOT), IMAGE_ICON,
::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON),
LR_DEFAULTCOLOR);
m_hIconBig = (HICON)::LoadImage(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON_SNAPSHOT), IMAGE_ICON,
::GetSystemMetrics(SM_CXICON), ::GetSystemMetrics(SM_CYICON),
LR_DEFAULTCOLOR);
if (m_hIconSmall) SendMessage(WM_SETICON, ICON_SMALL, (LPARAM)m_hIconSmall);
if (m_hIconBig) SendMessage(WM_SETICON, ICON_BIG, (LPARAM)m_hIconBig);
}
RecalcLayoutAndResize();
// 单发:不激活;循环:正常显示,允许接收焦点(系统菜单 / 拖拽 / 最小化都需要可激活)
ShowWindow(loopMode ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE);
if (loopMode) {
// 任务栏图标 + 标题在多窗口情况下立刻可见
UpdateWindow();
}
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;
// 循环模式下窗口尺寸交给用户控制OnSize / WS_THICKFRAME后续帧不再自动重排版
// 只 Invalidate 触发重绘;图像在 OnPaint 中按 client rect 等比例适配。
if (!m_loopMode) {
RecalcLayoutAndResize();
}
if (GetSafeHwnd()) Invalidate();
}
void CPreviewTipWnd::MarkPreviewUnavailable()
{
if (m_hasImage) return; // 已经有图就不再覆盖
m_unavailable = true;
if (GetSafeHwnd()) Invalidate();
}
void CPreviewTipWnd::SetLoopOwner(HWND ownerHwnd, UINT msgId, uint64_t clientId)
{
// 仅更新回调订阅m_loopMode 由 Create() 一次性设定,不在这里改 —— 否则
// CloseLoopTip 用 SetLoopOwner(NULL,0,0) 解订阅时会顺手把 loopMode 翻成 false
// 导致 PostNcDestroy 跳过 delete this → 每关一次循环窗口泄漏一个对象。
m_loopOwner = ownerHwnd;
m_loopMsg = msgId;
m_clientId = clientId;
}
void CPreviewTipWnd::OnDestroy()
{
// 在窗口实际销毁前回告 owner仅循环模式。owner 据此从映射表擦掉本条目。
// 注意:把字段先置零,避免极端情况下被 PostNcDestroy 再触发一次。
if (m_loopMode && m_loopOwner && ::IsWindow(m_loopOwner) && m_loopMsg != 0) {
HWND owner = m_loopOwner;
UINT msg = m_loopMsg;
m_loopOwner = nullptr;
m_loopMsg = 0;
// 64 位 clientId 用 LPARAM 指针传递WPARAM 在 32 位 Windows 不够宽。
auto* pId = new uint64_t(m_clientId);
if (!::PostMessageA(owner, msg, 0, (LPARAM)pId)) {
delete pId;
}
}
CWnd::OnDestroy();
}
void CPreviewTipWnd::PostNcDestroy()
{
// 仅循环模式自管理生命周期;单次提示窗口由主对话框 SAFE_DELETE 释放。
bool selfDelete = m_loopMode;
CWnd::PostNcDestroy();
if (selfDelete) {
delete this;
}
}
void CPreviewTipWnd::RecalcLayoutAndResize()
{
HWND hWnd = GetSafeHwnd();
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);
if (rcClient.IsRectEmpty()) return;
// 双缓冲
CDC memDC;
memDC.CreateCompatibleDC(&pdc);
CBitmap memBmp;
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
// 单发 tooltip 用 COLOR_INFOBK系统提示底色循环窗口用 COLOR_BTNFACE普通窗口背景
COLORREF bg = m_loopMode ? ::GetSysColor(COLOR_BTNFACE) : ::GetSysColor(COLOR_INFOBK);
memDC.FillSolidRect(&rcClient, bg);
CFont* oldFont = memDC.SelectObject(&m_font);
memDC.SetTextColor(::GetSysColor(m_loopMode ? COLOR_WINDOWTEXT : COLOR_INFOTEXT));
memDC.SetBkMode(TRANSPARENT);
if (m_loopMode) {
// 循环模式:基于 client rect 动态分配;图像区按 client 大小自适应,文本固定在底部
CRect rcImg, rcText;
LayoutForLoopMode(rcClient, rcImg, rcText);
if (!rcImg.IsRectEmpty()) DrawImageArea(memDC, rcImg);
if (!rcText.IsRectEmpty()) DrawTextArea(memDC, rcText);
} else {
// 单发 tooltip保持原行为按 RecalcLayoutAndResize 算好的固定尺寸排版
int curY = PADDING;
if (m_imageDrawW > 0 && m_imageDrawH > 0) {
CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
DrawImageArea(memDC, rcImg);
curY += m_imageDrawH + IMAGE_TEXT_GAP;
}
CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
DrawTextArea(memDC, rcText);
}
memDC.SelectObject(oldFont);
pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
memDC.SelectObject(oldBmp);
}
void CPreviewTipWnd::LayoutForLoopMode(const CRect& client, CRect& outImg, CRect& outText)
{
outImg.SetRectEmpty();
outText.SetRectEmpty();
int innerW = client.Width() - 2 * PADDING;
if (innerW < 50) return; // 太窄,啥也别画,避免计算崩溃
// 计算文本以当前可用宽度换行后的高度(用一份临时 DC
int textH = 0;
{
CClientDC dc(this);
CFont* old = dc.SelectObject(&m_font);
CRect rcCalc(0, 0, innerW, 32767);
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcCalc,
DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
textH = rcCalc.Height();
dc.SelectObject(old);
}
// 文本固定在底部
outText.SetRect(client.left + PADDING,
client.bottom - PADDING - textH,
client.right - PADDING,
client.bottom - PADDING);
// 图像区占顶部剩余空间
outImg.SetRect(client.left + PADDING,
client.top + PADDING,
client.right - PADDING,
outText.top - IMAGE_TEXT_GAP);
// 高度不足以放图像(被压扁)→ 只画文本
if (outImg.Height() < 60) {
outImg.SetRectEmpty();
// 文本提到中间,避免压在底部边缘
outText.MoveToY(client.top + PADDING);
}
}
void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
{
if (rc.IsRectEmpty()) return;
// 边框
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
CRect rcInner = rc;
rcInner.DeflateRect(1, 1);
if (m_hasImage && m_image) {
if (m_loopMode) {
// 循环模式:保留长宽比适配 rect黑色背景充当 letterbox 留白
dc.FillSolidRect(&rcInner, RGB(32, 32, 32));
double imgW = (double)m_image->GetWidth();
double imgH = (double)m_image->GetHeight();
if (imgW > 0 && imgH > 0 && rcInner.Width() > 0 && rcInner.Height() > 0) {
double sx = rcInner.Width() / imgW;
double sy = rcInner.Height() / imgH;
double scale = sx < sy ? sx : sy;
int drawW = (int)(imgW * scale + 0.5);
int drawH = (int)(imgH * scale + 0.5);
int x = rcInner.left + (rcInner.Width() - drawW) / 2;
int y = rcInner.top + (rcInner.Height() - drawH) / 2;
Graphics g(dc.GetSafeHdc());
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
g.SetSmoothingMode(SmoothingModeHighQuality);
g.DrawImage(m_image.get(), x, y, drawW, drawH);
}
} else {
// 单发:与原行为一致,铺满 rc
Graphics g(dc.GetSafeHdc());
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
g.SetSmoothingMode(SmoothingModeHighQuality);
g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
}
} else {
// 占位灰色背景
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(m_loopMode ? COLOR_WINDOWTEXT : COLOR_INFOTEXT));
}
}
void CPreviewTipWnd::OnSize(UINT nType, int cx, int cy)
{
CWnd::OnSize(nType, cx, cy);
if (m_loopMode && GetSafeHwnd()) {
// 循环模式:所有排版交给 OnPaint 读取 client rect重画即可
Invalidate();
}
}
void CPreviewTipWnd::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
{
if (m_loopMode && lpMMI) {
// 防止用户把窗口拽到肉眼不可见。240x200 大致能保证标题栏 + 一行文本 + 缩略图可见。
lpMMI->ptMinTrackSize.x = 240;
lpMMI->ptMinTrackSize.y = 200;
}
CWnd::OnGetMinMaxInfo(lpMMI);
}
void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
{
RECT r = rc;
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &r,
DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX);
}