Feature: Add "Play Snapshot" loop preview windows for online hosts
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// PreviewTipWnd.cpp
|
||||
#include "stdafx.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
#include "resource.h" // IDI_ICON_SNAPSHOT(循环模式标题栏图标)
|
||||
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
@@ -21,6 +22,9 @@ constexpr int LOADING_H = 270;
|
||||
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;
|
||||
@@ -28,11 +32,15 @@ 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 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) {
|
||||
@@ -53,25 +61,60 @@ BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text,
|
||||
HFONT hF = ::CreateFontIndirectW(&lf);
|
||||
if (hF) m_font.Attach(hF);
|
||||
|
||||
// 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容
|
||||
// 窗口类:单发 tooltip 走 CS_SAVEBITS(短时显示,省 BitBlt),
|
||||
// 循环窗口是长存窗口,加 CS_HREDRAW|CS_VREDRAW 让拉伸时实时重绘
|
||||
UINT classStyle = loopMode ? (CS_HREDRAW | CS_VREDRAW) : CS_SAVEBITS;
|
||||
LPCTSTR kClass = AfxRegisterWndClass(
|
||||
CS_SAVEBITS,
|
||||
classStyle,
|
||||
::LoadCursor(NULL, IDC_ARROW),
|
||||
(HBRUSH)(COLOR_INFOBK + 1),
|
||||
(HBRUSH)(COLOR_BTNFACE + 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);
|
||||
|
||||
// 样式选择:
|
||||
// 单发 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(SW_SHOWNOACTIVATE);
|
||||
// 单发:不激活;循环:正常显示,允许接收焦点(系统菜单 / 拖拽 / 最小化都需要可激活)
|
||||
ShowWindow(loopMode ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE);
|
||||
if (loopMode) {
|
||||
// 任务栏图标 + 标题在多窗口情况下立刻可见
|
||||
UpdateWindow();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -102,7 +145,11 @@ void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
|
||||
m_image = std::move(bmp);
|
||||
m_hasImage = true;
|
||||
m_unavailable = false;
|
||||
RecalcLayoutAndResize();
|
||||
// 循环模式下窗口尺寸交给用户控制(OnSize / WS_THICKFRAME),后续帧不再自动重排版,
|
||||
// 只 Invalidate 触发重绘;图像在 OnPaint 中按 client rect 等比例适配。
|
||||
if (!m_loopMode) {
|
||||
RecalcLayoutAndResize();
|
||||
}
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
@@ -113,6 +160,44 @@ void CPreviewTipWnd::MarkPreviewUnavailable()
|
||||
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();
|
||||
@@ -181,6 +266,7 @@ void CPreviewTipWnd::OnPaint()
|
||||
CPaintDC pdc(this);
|
||||
CRect rcClient;
|
||||
GetClientRect(&rcClient);
|
||||
if (rcClient.IsRectEmpty()) return;
|
||||
|
||||
// 双缓冲
|
||||
CDC memDC;
|
||||
@@ -189,41 +275,115 @@ void CPreviewTipWnd::OnPaint()
|
||||
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
||||
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
|
||||
|
||||
memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK));
|
||||
// 单发 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(COLOR_INFOTEXT));
|
||||
memDC.SetTextColor(::GetSysColor(m_loopMode ? COLOR_WINDOWTEXT : 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;
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
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 {
|
||||
// 占位灰色背景
|
||||
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 ...";
|
||||
@@ -231,10 +391,29 @@ void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
|
||||
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));
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user