Use Qt's High-DPI scaling on Windows (#4868)

This commit is contained in:
nerix
2024-05-12 13:59:14 +02:00
committed by GitHub
parent 8202cd0d99
commit febcf464fe
21 changed files with 459 additions and 320 deletions
+336 -201
View File
@@ -29,12 +29,163 @@
# pragma comment(lib, "Dwmapi.lib")
# include <QHBoxLayout>
# define WM_DPICHANGED 0x02E0
# include <QOperatingSystemVersion>
#endif
#include "widgets/helper/TitlebarButton.hpp"
namespace {
#ifdef USEWINSDK
// From kHiddenTaskbarSize in Firefox
constexpr UINT HIDDEN_TASKBAR_SIZE = 2;
bool isWindows11OrGreater()
{
static const bool result = [] {
// This calls RtlGetVersion under the hood so we don't have to.
// The micro version corresponds to dwBuildNumber.
auto version = QOperatingSystemVersion::current();
return (version.majorVersion() > 10) ||
(version.microVersion() >= 22000);
}();
return result;
}
/// Finds the taskbar HWND on a specific monitor (or any)
HWND findTaskbarWindow(LPRECT rcMon = nullptr)
{
HWND taskbar = nullptr;
RECT taskbarRect;
// return value of IntersectRect, unused
RECT intersectionRect;
while ((taskbar = FindWindowEx(nullptr, taskbar, L"Shell_TrayWnd",
nullptr)) != nullptr)
{
if (!rcMon)
{
// no monitor was specified, return the first encountered window
break;
}
if (GetWindowRect(taskbar, &taskbarRect) != 0 &&
IntersectRect(&intersectionRect, &taskbarRect, rcMon) != 0)
{
// taskbar intersects with the monitor - this is the one
break;
}
}
return taskbar;
}
/// Gets the edge of the taskbar if it's automatically hidden
std::optional<UINT> hiddenTaskbarEdge(LPRECT rcMon = nullptr)
{
HWND taskbar = findTaskbarWindow(rcMon);
if (!taskbar)
{
return std::nullopt;
}
APPBARDATA state = {sizeof(state), taskbar};
APPBARDATA pos = {sizeof(pos), taskbar};
auto appBarState =
static_cast<LRESULT>(SHAppBarMessage(ABM_GETSTATE, &state));
if ((appBarState & ABS_AUTOHIDE) == 0)
{
return std::nullopt;
}
if (SHAppBarMessage(ABM_GETTASKBARPOS, &pos) == 0)
{
qCDebug(chatterinoApp) << "Failed to get taskbar pos";
return ABE_BOTTOM;
}
return pos.uEdge;
}
/// @brief Gets the window borders for @a hwnd
///
/// Each side of the returned RECT has the correct sign, so they can be added
/// to a window rect.
/// Shrinking by 1px would return {left: 1, top: 1, right: -1, left: -1}.
RECT windowBordersFor(HWND hwnd, bool isMaximized)
{
RECT margins{0, 0, 0, 0};
auto addBorders = isMaximized || isWindows11OrGreater();
if (addBorders)
{
auto dpi = GetDpiForWindow(hwnd);
auto systemMetric = [&](auto index) {
if (dpi != 0)
{
return GetSystemMetricsForDpi(index, dpi);
}
return GetSystemMetrics(index);
};
auto paddedBorder = systemMetric(SM_CXPADDEDBORDER);
auto borderWidth = systemMetric(SM_CXSIZEFRAME) + paddedBorder;
auto borderHeight = systemMetric(SM_CYSIZEFRAME) + paddedBorder;
margins.left += borderWidth;
margins.right -= borderWidth;
if (isMaximized)
{
margins.top += borderHeight;
}
margins.bottom -= borderHeight;
}
if (isMaximized)
{
auto *hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi;
mi.cbSize = sizeof(mi);
auto *monitor = [&]() -> LPRECT {
if (GetMonitorInfo(hMonitor, &mi))
{
return &mi.rcMonitor;
}
return nullptr;
}();
auto edge = hiddenTaskbarEdge(monitor);
if (edge)
{
switch (*edge)
{
case ABE_LEFT:
margins.left += HIDDEN_TASKBAR_SIZE;
break;
case ABE_RIGHT:
margins.right -= HIDDEN_TASKBAR_SIZE;
break;
case ABE_TOP:
margins.top += HIDDEN_TASKBAR_SIZE;
break;
case ABE_BOTTOM:
margins.bottom -= HIDDEN_TASKBAR_SIZE;
break;
default:
break;
}
}
}
return margins;
}
#endif
} // namespace
namespace chatterino {
BaseWindow::BaseWindow(FlagsEnum<Flags> _flags, QWidget *parent)
@@ -117,95 +268,80 @@ float BaseWindow::scale() const
return std::max<float>(0.01f, this->overrideScale().value_or(this->scale_));
}
float BaseWindow::qtFontScale() const
{
return this->scale() / std::max<float>(0.01F, this->nativeScale_);
}
void BaseWindow::init()
{
#ifdef USEWINSDK
if (this->hasCustomWindowFrame())
{
// CUSTOM WINDOW FRAME
QVBoxLayout *layout = new QVBoxLayout();
auto *layout = new QVBoxLayout(this);
this->ui_.windowLayout = layout;
layout->setContentsMargins(1, 1, 1, 1);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
this->setLayout(layout);
if (!this->frameless_)
{
if (!this->frameless_)
{
QHBoxLayout *buttonLayout = this->ui_.titlebarBox =
new QHBoxLayout();
buttonLayout->setContentsMargins(0, 0, 0, 0);
layout->addLayout(buttonLayout);
QHBoxLayout *buttonLayout = this->ui_.titlebarBox =
new QHBoxLayout();
buttonLayout->setContentsMargins(0, 0, 0, 0);
layout->addLayout(buttonLayout);
// title
Label *title = new Label;
QObject::connect(this, &QWidget::windowTitleChanged,
[title](const QString &text) {
title->setText(text);
});
// title
Label *title = new Label;
QObject::connect(this, &QWidget::windowTitleChanged,
[title](const QString &text) {
title->setText(text);
});
QSizePolicy policy(QSizePolicy::Ignored,
QSizePolicy::Preferred);
policy.setHorizontalStretch(1);
title->setSizePolicy(policy);
buttonLayout->addWidget(title);
this->ui_.titleLabel = title;
QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred);
policy.setHorizontalStretch(1);
title->setSizePolicy(policy);
buttonLayout->addWidget(title);
this->ui_.titleLabel = title;
// buttons
TitleBarButton *_minButton = new TitleBarButton;
_minButton->setButtonStyle(TitleBarButtonStyle::Minimize);
TitleBarButton *_maxButton = new TitleBarButton;
_maxButton->setButtonStyle(TitleBarButtonStyle::Maximize);
TitleBarButton *_exitButton = new TitleBarButton;
_exitButton->setButtonStyle(TitleBarButtonStyle::Close);
// buttons
auto *minButton = new TitleBarButton;
minButton->setButtonStyle(TitleBarButtonStyle::Minimize);
auto *maxButton = new TitleBarButton;
maxButton->setButtonStyle(TitleBarButtonStyle::Maximize);
auto *exitButton = new TitleBarButton;
exitButton->setButtonStyle(TitleBarButtonStyle::Close);
QObject::connect(_minButton, &TitleBarButton::leftClicked, this,
[this] {
this->setWindowState(Qt::WindowMinimized |
this->windowState());
});
QObject::connect(_maxButton, &TitleBarButton::leftClicked, this,
[this, _maxButton] {
this->setWindowState(
_maxButton->getButtonStyle() !=
QObject::connect(minButton, &TitleBarButton::leftClicked, this,
[this] {
this->setWindowState(Qt::WindowMinimized |
this->windowState());
});
QObject::connect(
maxButton, &TitleBarButton::leftClicked, this,
[this, maxButton] {
this->setWindowState(maxButton->getButtonStyle() !=
TitleBarButtonStyle::Maximize
? Qt::WindowActive
: Qt::WindowMaximized);
});
QObject::connect(_exitButton, &TitleBarButton::leftClicked,
this, [this] {
this->close();
});
});
QObject::connect(exitButton, &TitleBarButton::leftClicked, this,
[this] {
this->close();
});
this->ui_.titlebarButtons = new TitleBarButtons(
this, _minButton, _maxButton, _exitButton);
this->ui_.titlebarButtons =
new TitleBarButtons(this, minButton, maxButton, exitButton);
this->ui_.buttons.push_back(_minButton);
this->ui_.buttons.push_back(_maxButton);
this->ui_.buttons.push_back(_exitButton);
this->ui_.buttons.push_back(minButton);
this->ui_.buttons.push_back(maxButton);
this->ui_.buttons.push_back(exitButton);
// buttonLayout->addStretch(1);
buttonLayout->addWidget(_minButton);
buttonLayout->addWidget(_maxButton);
buttonLayout->addWidget(_exitButton);
buttonLayout->setSpacing(0);
}
buttonLayout->addWidget(minButton);
buttonLayout->addWidget(maxButton);
buttonLayout->addWidget(exitButton);
buttonLayout->setSpacing(0);
}
this->ui_.layoutBase = new BaseWidget(this);
this->ui_.layoutBase->setContentsMargins(1, 0, 1, 1);
layout->addWidget(this->ui_.layoutBase);
}
// DPI
// auto dpi = getWindowDpi(this->safeHWND());
// if (dpi) {
// this->scale = dpi.value() / 96.f;
// }
#endif
// TopMost flag overrides setting
@@ -571,29 +707,8 @@ void BaseWindow::resizeEvent(QResizeEvent *)
}
#ifdef USEWINSDK
if (this->hasCustomWindowFrame() && !this->isResizeFixing_)
{
this->isResizeFixing_ = true;
QTimer::singleShot(50, this, [this] {
auto hwnd = this->safeHWND();
if (!hwnd)
{
this->isResizeFixing_ = false;
return;
}
RECT rect;
::GetWindowRect(*hwnd, &rect);
::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left + 1,
rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left,
rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
QTimer::singleShot(10, this, [this] {
this->isResizeFixing_ = false;
});
});
}
this->calcButtonsSizes();
this->updateRealSize();
#endif
}
@@ -655,10 +770,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
switch (msg->message)
{
case WM_DPICHANGED:
returnValue = this->handleDPICHANGED(msg);
break;
case WM_SHOWWINDOW:
returnValue = this->handleSHOWWINDOW(msg);
break;
@@ -697,12 +808,15 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
{
*result = 0;
returnValue = true;
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
RECT winrect;
GetWindowRect(msg->hwnd, &winrect);
QPoint globalPos(x, y);
POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)};
ScreenToClient(msg->hwnd, &p);
QPoint globalPos(p.x, p.y);
globalPos /= this->devicePixelRatio();
globalPos = this->mapToGlobal(globalPos);
// TODO(nerix): use TrackMouseEvent here
this->ui_.titlebarButtons->hover(msg->wParam, globalPos);
this->lastEventWasNcMouseMove_ = true;
}
@@ -748,12 +862,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
*result = 0;
auto ht = msg->wParam;
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
RECT winrect;
GetWindowRect(msg->hwnd, &winrect);
QPoint globalPos(x, y);
POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)};
ScreenToClient(msg->hwnd, &p);
QPoint globalPos(p.x, p.y);
globalPos /= this->devicePixelRatio();
globalPos = this->mapToGlobal(globalPos);
if (msg->message == WM_NCLBUTTONDOWN)
{
this->ui_.titlebarButtons->mousePress(ht, globalPos);
@@ -784,7 +900,7 @@ void BaseWindow::scaleChangedEvent(float scale)
#endif
this->setFont(
getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale()));
getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->scale()));
}
void BaseWindow::paintEvent(QPaintEvent *)
@@ -802,10 +918,9 @@ void BaseWindow::paintEvent(QPaintEvent *)
void BaseWindow::updateScale()
{
auto scale =
this->nativeScale_ * (this->flags_.has(DisableCustomScaling)
? 1
: getSettings()->getClampedUiScale());
auto scale = this->flags_.has(DisableCustomScaling)
? 1
: getSettings()->getClampedUiScale();
this->setScale(scale);
@@ -815,6 +930,22 @@ void BaseWindow::updateScale()
}
}
#ifdef USEWINSDK
void BaseWindow::updateRealSize()
{
auto hwnd = this->safeHWND();
if (!hwnd)
{
return;
}
RECT real;
::GetWindowRect(*hwnd, &real);
this->realBounds_ = QRect(real.left, real.top, real.right - real.left,
real.bottom - real.top);
}
#endif
void BaseWindow::calcButtonsSizes()
{
if (!this->shown_)
@@ -846,34 +977,28 @@ void BaseWindow::drawCustomWindowFrame(QPainter &painter)
{
QColor bg = this->overrideBackgroundColor_.value_or(
this->theme->window.background);
painter.fillRect(QRect(1, 2, this->width() - 2, this->height() - 3),
bg);
if (this->isMaximized_)
{
painter.fillRect(this->rect(), bg);
}
else
{
// Draw a border that's exactly 1px wide
//
// There is a bug where the border can get <dpr>px wide while dragging.
// this "fixes" itself when deselecting the window.
auto dpr = this->devicePixelRatio();
if (dpr != 1)
{
painter.setTransform(QTransform::fromScale(1 / dpr, 1 / dpr));
}
painter.fillRect(1, 1, this->realBounds_.width() - 2,
this->realBounds_.height() - 2, bg);
}
}
#endif
}
bool BaseWindow::handleDPICHANGED(MSG *msg)
{
#ifdef USEWINSDK
int dpi = HIWORD(msg->wParam);
float _scale = dpi / 96.f;
auto *prcNewWindow = reinterpret_cast<RECT *>(msg->lParam);
SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top,
prcNewWindow->right - prcNewWindow->left,
prcNewWindow->bottom - prcNewWindow->top,
SWP_NOZORDER | SWP_NOACTIVATE);
this->nativeScale_ = _scale;
this->updateScale();
return true;
#else
return false;
#endif
}
bool BaseWindow::handleSHOWWINDOW(MSG *msg)
{
#ifdef USEWINSDK
@@ -883,16 +1008,6 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg)
return true;
}
if (auto dpi = getWindowDpi(msg->hwnd))
{
float currentScale = (float)dpi.value() / 96.F;
if (currentScale != this->nativeScale_)
{
this->nativeScale_ = currentScale;
this->updateScale();
}
}
if (!this->shown_)
{
this->shown_ = true;
@@ -906,14 +1021,12 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg)
if (!this->initalBounds_.isNull())
{
::SetWindowPos(msg->hwnd, nullptr, this->initalBounds_.x(),
this->initalBounds_.y(), this->initalBounds_.width(),
this->initalBounds_.height(),
SWP_NOZORDER | SWP_NOACTIVATE);
this->setGeometry(this->initalBounds_);
this->currentBounds_ = this->initalBounds_;
}
this->calcButtonsSizes();
this->updateRealSize();
}
return true;
@@ -929,23 +1042,54 @@ bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result)
#endif
{
#ifdef USEWINSDK
if (this->hasCustomWindowFrame())
if (!this->hasCustomWindowFrame())
{
if (msg->wParam == TRUE)
{
// remove 1 extra pixel on top of custom frame
auto *ncp = reinterpret_cast<NCCALCSIZE_PARAMS *>(msg->lParam);
if (ncp)
{
ncp->lppos->flags |= SWP_NOREDRAW;
ncp->rgrc[0].top -= 1;
}
}
return false;
}
if (msg->wParam != TRUE)
{
*result = 0;
return true;
}
return false;
auto *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(msg->lParam);
auto *r = &params->rgrc[0];
WINDOWPLACEMENT wp;
wp.length = sizeof(WINDOWPLACEMENT);
this->isMaximized_ = GetWindowPlacement(msg->hwnd, &wp) != 0 &&
(wp.showCmd == SW_SHOWMAXIMIZED);
auto borders = windowBordersFor(msg->hwnd, this->isMaximized_);
r->left += borders.left;
r->top += borders.top;
r->right += borders.right;
r->bottom += borders.bottom;
if (borders.left != 0 || borders.top != 0 || borders.right != 0 ||
borders.bottom != 0)
{
// We added borders -> we changed the rect, so we can't return
// WVR_VALIDRECTS
*result = 0;
return true;
}
// This is an attempt at telling Windows to not redraw (or at least to do a
// better job at redrawing) the window. There is a long list of tricks
// people tried to prevent this at
// https://stackoverflow.com/q/53000291/16300717
//
// We set the source and destination rectangles to a 1x1 rectangle at the
// top left. Windows is instructed by WVR_VALIDRECTS to copy and preserve
// some parts of the window image.
QPoint fixed = {r->left, r->top};
params->rgrc[1] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1};
params->rgrc[2] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1};
*result = WVR_VALIDRECTS;
return true;
#else
return false;
#endif
@@ -962,28 +1106,11 @@ bool BaseWindow::handleSIZE(MSG *msg)
}
else if (this->hasCustomWindowFrame())
{
if (msg->wParam == SIZE_MAXIMIZED)
{
auto offset =
int(getWindowDpi(msg->hwnd).value_or(96) * 8 / 96);
this->ui_.windowLayout->setContentsMargins(offset, offset,
offset, offset);
}
else
{
this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0);
}
this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED;
if (this->isNotMinimizedOrMaximized_)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
this->currentBounds_ =
QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
this->currentBounds_ = this->geometry();
}
this->useNextBounds_.stop();
@@ -993,6 +1120,12 @@ bool BaseWindow::handleSIZE(MSG *msg)
// the minimize button, so we have to emulate it.
this->ui_.titlebarButtons->leave();
}
RECT real;
::GetWindowRect(msg->hwnd, &real);
this->realBounds_ =
QRect(real.left, real.top, real.right - real.left,
real.bottom - real.top);
}
}
return false;
@@ -1006,11 +1139,7 @@ bool BaseWindow::handleMOVE(MSG *msg)
#ifdef USEWINSDK
if (this->isNotMinimizedOrMaximized_)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
this->nextBounds_ = QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
this->nextBounds_ = this->geometry();
this->useNextBounds_.start(10);
}
#endif
@@ -1024,31 +1153,37 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
#endif
{
#ifdef USEWINSDK
const LONG border_width = 8; // in pixels
RECT winrect;
GetWindowRect(msg->hwnd, &winrect);
const LONG borderWidth = 8; // in device independent pixels
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
auto rect = this->rect();
QPoint point(x - winrect.left, y - winrect.top);
POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)};
ScreenToClient(msg->hwnd, &p);
QPoint point(p.x, p.y);
point /= this->devicePixelRatio();
auto x = point.x();
auto y = point.y();
if (this->hasCustomWindowFrame())
{
*result = 0;
bool resizeWidth = minimumWidth() != maximumWidth();
bool resizeHeight = minimumHeight() != maximumHeight();
bool resizeWidth =
minimumWidth() != maximumWidth() && !this->isMaximized();
bool resizeHeight =
minimumHeight() != maximumHeight() && !this->isMaximized();
if (resizeWidth)
{
// left border
if (x < winrect.left + border_width)
if (x < rect.left() + borderWidth)
{
*result = HTLEFT;
}
// right border
if (x >= winrect.right - border_width)
if (x >= rect.right() - borderWidth)
{
*result = HTRIGHT;
}
@@ -1056,12 +1191,12 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
if (resizeHeight)
{
// bottom border
if (y >= winrect.bottom - border_width)
if (y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOM;
}
// top border
if (y < winrect.top + border_width)
if (y < rect.top() + borderWidth)
{
*result = HTTOP;
}
@@ -1069,26 +1204,26 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
if (resizeWidth && resizeHeight)
{
// bottom left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
if (x >= rect.left() && x < rect.left() + borderWidth &&
y < rect.bottom() && y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOMLEFT;
}
// bottom right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
if (x < rect.right() && x >= rect.right() - borderWidth &&
y < rect.bottom() && y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOMRIGHT;
}
// top left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y >= winrect.top && y < winrect.top + border_width)
if (x >= rect.left() && x < rect.left() + borderWidth &&
y >= rect.top() && y < rect.top() + borderWidth)
{
*result = HTTOPLEFT;
}
// top right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y >= winrect.top && y < winrect.top + border_width)
if (x < rect.right() && x >= rect.right() - borderWidth &&
y >= rect.top() && y < rect.top() + borderWidth)
{
*result = HTTOPRIGHT;
}