423 lines
16 KiB
C++
423 lines
16 KiB
C++
// 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 由本类持有,析构时 DestroyIcon(WM_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);
|
||
}
|