// PreviewTipWnd.cpp #include "stdafx.h" #include "PreviewTipWnd.h" #include // IUnknown / IStream — gdiplus.h 依赖它们已声明 #include #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 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); }