WindowsQPA: Draw custom titlebar with QPainter

- Draw custom titlebars that are requested by setting the
  Qt::ExpandedClientAreaHint with QPainter instead of GdiPlus.
- Draw the application icon, in case it was set, for the custom titlebar
- Add DPI awareness to the custom titlebar

Pick-to: 6.9
Change-Id: I276e7d8948e5a436f1835d96b59756b7237f63d2
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Wladimir Leuschner 2024-12-04 16:43:53 +01:00
parent 7740ac36d2
commit 438aa1524e
4 changed files with 126 additions and 116 deletions

View File

@ -64,7 +64,6 @@ qt_internal_add_plugin(QWindowsDirect2DIntegrationPlugin
dxgi dxgi
dxguid dxguid
gdi32 gdi32
gdiplus
uxtheme uxtheme
imm32 imm32
ole32 ole32

View File

@ -52,7 +52,6 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin
advapi32 advapi32
dwmapi dwmapi
gdi32 gdi32
gdiplus
uxtheme uxtheme
imm32 imm32
ole32 ole32

View File

@ -58,7 +58,6 @@
#include <dbt.h> #include <dbt.h>
#include <wtsapi32.h> #include <wtsapi32.h>
#include <shellscalingapi.h> #include <shellscalingapi.h>
#include <gdiplus.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -134,7 +133,6 @@ QWindowsContext *QWindowsContext::m_instance = nullptr;
struct QWindowsContextPrivate { struct QWindowsContextPrivate {
QWindowsContextPrivate(); QWindowsContextPrivate();
~QWindowsContextPrivate();
unsigned m_systemInfo = 0; unsigned m_systemInfo = 0;
QSet<QString> m_registeredWindowClassNames; QSet<QString> m_registeredWindowClassNames;
@ -149,7 +147,6 @@ struct QWindowsContextPrivate {
#if QT_CONFIG(tabletevent) #if QT_CONFIG(tabletevent)
QScopedPointer<QWindowsTabletSupport> m_tabletSupport; QScopedPointer<QWindowsTabletSupport> m_tabletSupport;
#endif #endif
ULONG_PTR m_gdiplusToken = 0;
const HRESULT m_oleInitializeResult; const HRESULT m_oleInitializeResult;
QWindow *m_lastActiveWindow = nullptr; QWindow *m_lastActiveWindow = nullptr;
bool m_asyncExpose = false; bool m_asyncExpose = false;
@ -175,14 +172,6 @@ QWindowsContextPrivate::QWindowsContextPrivate()
qWarning() << "QWindowsContext: OleInitialize() failed: " qWarning() << "QWindowsContext: OleInitialize() failed: "
<< QSystemError::windowsComString(m_oleInitializeResult); << QSystemError::windowsComString(m_oleInitializeResult);
} }
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, nullptr);
}
QWindowsContextPrivate::~QWindowsContextPrivate()
{
Gdiplus::GdiplusShutdown(m_gdiplusToken);
} }
QWindowsContext::QWindowsContext() : QWindowsContext::QWindowsContext() :

View File

@ -29,6 +29,7 @@
#include <QtGui/qwindow.h> #include <QtGui/qwindow.h>
#include <QtGui/qregion.h> #include <QtGui/qregion.h>
#include <QtGui/qopenglcontext.h> #include <QtGui/qopenglcontext.h>
#include <QtGui/qpainterpath.h>
#include <QtGui/private/qwindowsthemecache_p.h> #include <QtGui/private/qwindowsthemecache_p.h>
#include <private/qwindow_p.h> // QWINDOWSIZE_MAX #include <private/qwindow_p.h> // QWINDOWSIZE_MAX
#include <private/qguiapplication_p.h> #include <private/qguiapplication_p.h>
@ -3335,10 +3336,14 @@ bool QWindowsWindow::handleNonClientHitTest(const QPoint &globalPos, LRESULT *re
*result == HTCLIENT){ *result == HTCLIENT){
QWindow* wnd = window(); QWindow* wnd = window();
auto buttons = GetAsyncKeyState(VK_LBUTTON) != 0 ? Qt::LeftButton : Qt::NoButton; auto buttons = GetAsyncKeyState(VK_LBUTTON) != 0 ? Qt::LeftButton : Qt::NoButton;
QMouseEvent event(QEvent::MouseButtonPress, localPos, globalPos, buttons, buttons, Qt::NoModifier); if (buttons != Qt::NoButton) {
QGuiApplication::sendEvent(wnd, &event); QMouseEvent event(QEvent::MouseButtonPress, localPos, globalPos, buttons, buttons, Qt::NoModifier);
if (!event.isAccepted()) QGuiApplication::sendEvent(wnd, &event);
*result = HTCAPTION; if (!event.isAccepted() && GetAsyncKeyState(VK_RBUTTON))
*result = HTSYSMENU;
else if (!event.isAccepted())
*result = HTCAPTION;
}
} }
} }
} }
@ -3371,6 +3376,11 @@ bool QWindowsWindow::handleNonClientHitTest(const QPoint &globalPos, LRESULT *re
case HTSIZE: case HTSIZE:
const_cast<QWindow *>(w)->showNormal(); const_cast<QWindow *>(w)->showNormal();
break; break;
case HTSYSMENU: {
HWND hwnd = reinterpret_cast<HWND>(w->winId());
HMENU sysMenu = GetSystemMenu(hwnd, false);
TrackPopupMenu(sysMenu, 0, globalPos.x(), globalPos.y(), 0, hwnd, nullptr);
}
default: default:
break; break;
} }
@ -3440,18 +3450,17 @@ bool QWindowsWindow::handleNonClientHitTest(const QPoint &globalPos, LRESULT *re
return false; return false;
} }
static void _q_drawCustomTitleBarButton(Gdiplus::Graphics& graphics, Gdiplus::RectF& r, Gdiplus::SolidBrush& brush) static void _q_drawCustomTitleBarButton(QPainter& p, const QRectF& r)
{ {
r.Height += 1; QPainterPath path(QPointF(r.x(), r.y()));
Gdiplus::GraphicsPath path; QRectF rightCorner(r.x() + r.width() - 2.0, r.y() + 4.0, 2, 2);
Gdiplus::RectF Corner(r.X + r.Width - 4, r.Y, 2, 2); QRectF leftCorner(r.x(), r.y() + 4, 2, 2);
path.Reset(); path.lineTo(r.x() + r.width() - 5.0f, r.y());
path.AddLine(r.X, r.Y, r.X + r.Width - 4.0f, r.Y); path.arcTo(rightCorner, 90, -90);
path.AddArc(Corner, 90, 90); path.lineTo(r.x() + r.width(), r.y() + r.height() - 1);
path.AddLine(r.X + r.Width, r.Y + 4, r.X + r.Width, r.Y + r.Height - 2); path.lineTo(r.x(), r.y() + r.height() - 1);
path.AddLine(r.X + r.Width, r.Y + r.Height - 2, r.X, r.Y + r.Height - 2); path.closeSubpath();
path.CloseFigure(); p.drawPath(path);
graphics.FillPath(&brush, &path);
} }
void QWindowsWindow::updateCustomTitlebar() void QWindowsWindow::updateCustomTitlebar()
@ -3464,137 +3473,151 @@ void QWindowsWindow::updateCustomTitlebar()
const int titleBarHeight = getTitleBarHeight_sys(savedDpi()); const int titleBarHeight = getTitleBarHeight_sys(savedDpi());
const int titleButtonWidth = titleBarHeight * 1.5; const int titleButtonWidth = titleBarHeight * 1.5;
const qreal factor = QHighDpiScaling::factor(wnd);
const int windowWidth = windowRect.right - windowRect.left; const int windowWidth = windowRect.right - windowRect.left;
POINT localPos; POINT localPos;
GetCursorPos(&localPos); GetCursorPos(&localPos);
MapWindowPoints(HWND_DESKTOP, hwnd, &localPos, 1); MapWindowPoints(HWND_DESKTOP, hwnd, &localPos, 1);
bool isDarkmode = QWindowsIntegration::instance()->darkModeHandling().testFlags(QWindowsApplication::DarkModeWindowFrames) && const bool isDarkmode = QWindowsIntegration::instance()->darkModeHandling().testFlags(QWindowsApplication::DarkModeWindowFrames) &&
qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark; qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
const bool isWindows11orAbove = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11; const bool isWindows11orAbove = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11;
const wchar_t *iconFontName = isWindows11orAbove ? L"Segoe Fluent Icons" : L"Segoe MDL2 Assets";
const std::wstring titleTextFontName = QWindowsIntegration::instance()->fontDatabase()->defaultFont().family().toStdWString();
Gdiplus::Bitmap softwareBitmap(windowWidth, titleBarHeight, PixelFormat32bppARGB);
Gdiplus::Graphics graphics(&softwareBitmap);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAliasGridFit);
Gdiplus::SolidBrush iconBrush(isDarkmode ? Gdiplus::Color(255, 255, 255, 255) : Gdiplus::Color(255, 0, 0, 0));
Gdiplus::FontFamily titleFontFamily(titleTextFontName.data());
Gdiplus::Font titleFont(&titleFontFamily, 14, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
Gdiplus::FontFamily iconFontFamily(iconFontName);
Gdiplus::Font iconFont(&iconFontFamily, 8, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
Gdiplus::StringFormat titleFormat; const QBrush closeButtonBrush(QColor(0xC4, 0x2B, 0x1C, 255));
titleFormat.SetAlignment(Gdiplus::StringAlignmentNear); const QBrush minMaxButtonBrush = QBrush(isDarkmode ? QColor(0xFF, 0xFF, 0xFF, 0x40) : QColor(0x00, 0x00, 0x00, 0x20));
titleFormat.SetLineAlignment(Gdiplus::StringAlignmentCenter); const QBrush titleBarBackgroundColor = QBrush(isDarkmode ? QColor(0x1F, 0x1F, 0x1F, 0xFF) : QColor(0xF3, 0xF3, 0xF3, 0xFF));
Gdiplus::StringFormat buttonFormat; const QPen textPen = QPen(isDarkmode ? QColor(255, 255, 255, 255) : QColor(0, 0, 0, 255));
buttonFormat.SetAlignment(Gdiplus::StringAlignmentCenter);
buttonFormat.SetLineAlignment(Gdiplus::StringAlignmentCenter);
Gdiplus::SolidBrush closeButtonBrush(Gdiplus::Color(255, 255, 0, 0)); QImage image(windowWidth, titleBarHeight, QImage::Format_ARGB32);
Gdiplus::SolidBrush minMaxButtonBrush = Gdiplus::SolidBrush(isDarkmode ? Gdiplus::Color(0x40, 0xFF, 0xFF, 0xFF) : Gdiplus::Color(0x20, 0x00, 0x00, 0x00)); QPainter p(&image);
p.setCompositionMode(QPainter::CompositionMode_Clear);
p.fillRect(0, 0, windowWidth, titleBarHeight, Qt::transparent);
Gdiplus::Color titleBarBackgroundColor = isDarkmode ? Gdiplus::Color(0xFF, 0x1F, 0x1F, 0x1F) : Gdiplus::Color(0xFF, 0xF3, 0xF3, 0xF3); p.setCompositionMode(QPainter::CompositionMode_SourceOver);
Gdiplus::SolidBrush titleBarBackgroundBrush(titleBarBackgroundColor);
int buttons = 1; p.setBrush(titleBarBackgroundColor);
Gdiplus::RectF titleRect; p.setPen(Qt::NoPen);
graphics.Clear(Gdiplus::Color(0x00, 0x00, 0x00, 0x00));
if (!wnd->flags().testFlags(Qt::NoTitleBarBackgroundHint)) { if (!wnd->flags().testFlags(Qt::NoTitleBarBackgroundHint)) {
titleRect.Y = 1; QRect titleRect;
titleRect.X = 1; titleRect.setY(1);
titleRect.Width = windowWidth - 2; titleRect.setX(2);
titleRect.Height = titleBarHeight; titleRect.setWidth(windowWidth - 2);
titleRect.setHeight(titleBarHeight);
if (isWindows11orAbove) { if (isWindows11orAbove) {
Gdiplus::GraphicsPath path; QPainterPath path(QPointF(titleRect.x() + 4.0f, titleRect.y()));
titleRect.Height += 1; QRectF rightCorner(titleRect.x() + titleRect.width() - 4.0, titleRect.y() + 4.0, 2, 2);
Gdiplus::RectF rightCorner(titleRect.X + titleRect.Width - 4, titleRect.Y, 2, 2); QRectF leftCorner(titleRect.x(), titleRect.y() + 4, 2, 2);
Gdiplus::RectF leftCorner(titleRect.X, titleRect.Y + 4, 2, 2); path.lineTo(titleRect.x() + titleRect.width() - 7.0f, titleRect.y());
path.Reset(); path.arcTo(rightCorner, 90, -90);
path.AddLine(titleRect.X + 4.0f, titleRect.Y, titleRect.X + titleRect.Width - 4.0f, titleRect.Y); path.lineTo(titleRect.x() + titleRect.width() - 2.0, titleRect.y() + titleRect.height() - 1);
path.AddArc(rightCorner, 90, 90); path.lineTo(titleRect.x(), titleRect.y() + titleRect.height() - 1);
path.AddLine(titleRect.X + titleRect.Width, titleRect.Y + 4, titleRect.X + titleRect.Width, titleRect.Y + titleRect.Height - 2); path.lineTo(titleRect.x(), titleRect.y() + 4.0f);
path.AddLine(titleRect.X + titleRect.Width, titleRect.Y + titleRect.Height - 2, titleRect.X, titleRect.Y + titleRect.Height - 2); path.arcTo(leftCorner, -90, -90);
path.AddLine(titleRect.X, titleRect.Y + titleRect.Height - 2, titleRect.X, titleRect.Y + 4.0f); path.closeSubpath();
path.AddArc(leftCorner, -90, -90); p.drawPath(path);
path.CloseFigure();
graphics.FillPath(&titleBarBackgroundBrush, &path);
} else { } else {
graphics.FillRectangle(&titleBarBackgroundBrush, titleRect); p.drawRect(titleRect);
} }
} }
if (wnd->flags().testFlags(Qt::WindowTitleHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) { if (wnd->flags().testFlags(Qt::WindowTitleHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) {
titleRect.Y = 1; QRect titleRect;
titleRect.X = 12; titleRect.setY(1);
titleRect.Width = windowWidth; titleRect.setX(12);
titleRect.Height = titleBarHeight; titleRect.setWidth(windowWidth);
titleRect.setHeight(titleBarHeight);
std::wstring titleString = wnd->title().toStdWString(); const QIcon icon = wnd->icon();
graphics.DrawString(titleString.c_str(), -1, &titleFont, titleRect, &titleFormat, &iconBrush); if (!icon.isNull()) {
titleRect.adjust(factor * 4, 0, 0, 0);
QRect iconRect(titleRect.x(), titleRect.y() + factor * 8, factor * 16, factor * 16);
icon.paint(&p, iconRect);
titleRect.adjust(factor * 24, 0, 0, 0);
}
p.setPen(textPen);
QFont titleFont = QWindowsIntegration::instance()->fontDatabase()->defaultFont();
titleFont.setPointSize(factor * 9);
titleFont.setWeight(QFont::Thin);
titleFont.setHintingPreference(QFont::PreferFullHinting);
p.setFont(titleFont);
p.drawText(titleRect, wnd->title(), QTextOption(Qt::AlignVCenter));
} }
int buttons = 1;
const QString assetFontName = isWindows11orAbove ? QStringLiteral("Segoe Fluent Icons") : QStringLiteral("Segoe MDL2 Assets");
QFont assetFont = QFont(assetFontName, factor * 7);
assetFont.setWeight(QFont::Thin);
assetFont.setHintingPreference(QFont::PreferFullHinting);
p.setFont(assetFont);
p.setBrush(closeButtonBrush);
p.setPen(Qt::NoPen);
if (wnd->flags().testFlags(Qt::WindowCloseButtonHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) { if (wnd->flags().testFlags(Qt::WindowCloseButtonHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) {
Gdiplus::RectF rect; QRectF rect;
rect.Y = 1; rect.setY(1);
rect.X = windowWidth - titleButtonWidth * buttons; rect.setX(windowWidth - titleButtonWidth * buttons);
rect.Width = titleButtonWidth - 1; rect.setWidth(titleButtonWidth - 1);
rect.Height = titleBarHeight; rect.setHeight(titleBarHeight);
if (localPos.x > (windowWidth - buttons * titleButtonWidth) && if (localPos.x > (windowWidth - buttons * titleButtonWidth) &&
localPos.x < (windowWidth - (buttons - 1) * titleButtonWidth) && localPos.x < (windowWidth - (buttons - 1) * titleButtonWidth) &&
localPos.y > rect.Y && localPos.y < rect.Y + rect.Height) { localPos.y > rect.y() && localPos.y < rect.y() + rect.height()) {
if (isWindows11orAbove && buttons == 1) { if (isWindows11orAbove && buttons == 1)
_q_drawCustomTitleBarButton(graphics, rect, closeButtonBrush); _q_drawCustomTitleBarButton(p, rect);
} else { else
graphics.FillRectangle(&closeButtonBrush, rect); p.drawRect(rect);
}
} }
graphics.DrawString(L"\uE8BB", -1, &iconFont, rect, &buttonFormat, &iconBrush); p.setPen(textPen);
p.drawText(rect,QStringLiteral("\uE8BB"), QTextOption(Qt::AlignVCenter | Qt::AlignHCenter));
buttons++; buttons++;
} }
p.setBrush(minMaxButtonBrush);
p.setPen(Qt::NoPen);
if (wnd->flags().testFlags(Qt::WindowMaximizeButtonHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) { if (wnd->flags().testFlags(Qt::WindowMaximizeButtonHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) {
Gdiplus::RectF rect; QRectF rect;
rect.Y = 1; rect.setY(1);
rect.X = windowWidth - titleButtonWidth * buttons; rect.setX(windowWidth - titleButtonWidth * buttons);
rect.Width = titleButtonWidth - 1; rect.setWidth(titleButtonWidth - 1);
rect.Height = titleBarHeight; rect.setHeight(titleBarHeight);
if (localPos.x > (windowWidth - buttons * titleButtonWidth) && if (localPos.x > (windowWidth - buttons * titleButtonWidth) &&
localPos.x < (windowWidth - (buttons - 1) * titleButtonWidth) && localPos.x < (windowWidth - (buttons - 1) * titleButtonWidth) &&
localPos.y > rect.Y && localPos.y < rect.Y + rect.Height) { localPos.y > rect.y() && localPos.y < rect.y() + rect.height()) {
if (isWindows11orAbove && buttons == 1) { if (isWindows11orAbove && buttons == 1)
_q_drawCustomTitleBarButton(graphics, rect, minMaxButtonBrush); _q_drawCustomTitleBarButton(p, rect);
} else { else
graphics.FillRectangle(&minMaxButtonBrush, rect); p.drawRect(rect);
}
} }
graphics.DrawString(L"\uE922", -1, &iconFont, rect, &buttonFormat, &iconBrush); p.setPen(textPen);
p.drawText(rect,QStringLiteral("\uE922"), QTextOption(Qt::AlignVCenter | Qt::AlignHCenter));
buttons++; buttons++;
} }
p.setBrush(minMaxButtonBrush);
p.setPen(Qt::NoPen);
if (wnd->flags().testFlags(Qt::WindowMinimizeButtonHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) { if (wnd->flags().testFlags(Qt::WindowMinimizeButtonHint | Qt::CustomizeWindowHint) || !wnd->flags().testFlag(Qt::CustomizeWindowHint)) {
Gdiplus::RectF rect; QRectF rect;
rect.Y = 1; rect.setY(1);
rect.X = windowWidth - titleButtonWidth * buttons; rect.setX(windowWidth - titleButtonWidth * buttons);
rect.Width = titleButtonWidth - 1; rect.setWidth(titleButtonWidth - 1);
rect.Height = titleBarHeight; rect.setHeight(titleBarHeight);
if (localPos.x > (windowWidth - buttons * titleButtonWidth) && if (localPos.x > (windowWidth - buttons * titleButtonWidth) &&
localPos.x < (windowWidth - (buttons - 1) * titleButtonWidth) && localPos.x < (windowWidth - (buttons - 1) * titleButtonWidth) &&
localPos.y > rect.Y && localPos.y < rect.Y + rect.Height) { localPos.y > rect.y() && localPos.y < rect.y() + rect.height()) {
if (isWindows11orAbove && buttons == 1) { if (isWindows11orAbove && buttons == 1)
_q_drawCustomTitleBarButton(graphics, rect, minMaxButtonBrush); _q_drawCustomTitleBarButton(p, rect);
} else { else
graphics.FillRectangle(&minMaxButtonBrush, rect); p.drawRect(rect);
}
} }
graphics.DrawString(L"\uE921", -1, &iconFont, rect, &buttonFormat, &iconBrush); p.setPen(textPen);
p.drawText(rect,QStringLiteral("\uE921"), QTextOption(Qt::AlignVCenter | Qt::AlignHCenter));
buttons++; buttons++;
} }
HBITMAP bmp; p.end();
softwareBitmap.GetHBITMAP(Gdiplus::Color(0, 0, 0, 0), &bmp);
HBITMAP bmp = image.toHBITMAP();
HDC hdc = GetDC(hwnd); HDC hdc = GetDC(hwnd);