Signed-off-by: Yuhang Zhao <2546789017@qq.com>
This commit is contained in:
Yuhang Zhao 2020-04-06 12:12:32 +08:00
parent c7231b45b2
commit c1fddf0028
5 changed files with 139 additions and 87 deletions

View File

@ -44,6 +44,7 @@ Notes
## For Windows developers
- The `FramelessHelper` class is just a simple wrapper of the `WinNativeEventFilter` class, you can use the latter directly instead if you encounter with some strange problems.
- If you are using `WinNativeEventFilter` directly, don't forget to call `FramelessHelper::updateQtFrame` everytime after you make a widget or window become frameless, it will make the new frame margins work correctly if `setGeometry` or `frameGeometry` is called.
- Don't change the window flags (for example, enable the Qt::FramelessWindowHint flag) because it will break the functionality of this code. I'll get rid of the window frame (including the titlebar of course) in Win32 native events.
- All traditional Win32 APIs are replaced by their DPI-aware ones, if there is one.
- Start from Windows 10, normal windows usually have a one pixel width border line, I don't add it because not everyone like it. You can draw one manually if you really need it.

View File

@ -46,7 +46,9 @@ Q_DECLARE_METATYPE(QMargins)
FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) {
connect(this, &FramelessHelper::titlebarHeightChanged, this,
&FramelessHelper::updateQtFrame);
&FramelessHelper::updateQtFrame_internal);
connect(this, &FramelessHelper::framelessWindowsChanged,
[this]() { updateQtFrame_internal(m_titlebarHeight); });
#ifdef Q_OS_WINDOWS
m_borderWidth = WinNativeEventFilter::borderWidth(nullptr);
m_borderHeight = WinNativeEventFilter::borderHeight(nullptr);
@ -67,7 +69,26 @@ FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) {
<< "Window border height:" << m_borderHeight
<< "Window titlebar height:" << m_titlebarHeight;
#endif
updateQtFrame(m_titlebarHeight);
updateQtFrame_internal(m_titlebarHeight);
}
void FramelessHelper::updateQtFrame(QWindow *window, int titlebarHeight) {
if (window && (titlebarHeight > 0)) {
// Reduce top frame to zero since we paint it ourselves. Use
// device pixel to avoid rounding errors.
const QMargins margins = {0, -titlebarHeight, 0, 0};
const QVariant marginsVar = QVariant::fromValue(margins);
// The dynamic property takes effect when creating the platform
// window.
window->setProperty("_q_windowsCustomMargins", marginsVar);
// If a platform window exists, change via native interface.
QPlatformWindow *platformWindow = window->handle();
if (platformWindow) {
QGuiApplication::platformNativeInterface()->setWindowProperty(
platformWindow, QString::fromUtf8("WindowsCustomMargins"),
marginsVar);
}
}
}
int FramelessHelper::borderWidth() const { return m_borderWidth; }
@ -433,27 +454,12 @@ QWindow *FramelessHelper::getWindowHandle(QObject *val) {
return nullptr;
}
void FramelessHelper::updateQtFrame(int val) {
void FramelessHelper::updateQtFrame_internal(int val) {
if (!m_framelessWindows.isEmpty()) {
for (auto &&object : qAsConst(m_framelessWindows)) {
QWindow *window = getWindowHandle(object);
if (window) {
// Reduce top frame to zero since we paint it ourselves. Use
// device pixel to avoid rounding errors.
const QMargins margins = {0, -val, 0, 0};
const QVariant marginsVar = QVariant::fromValue(margins);
// The dynamic property takes effect when creating the platform
// window.
window->setProperty("_q_windowsCustomMargins", marginsVar);
// If a platform window exists, change via native interface.
QPlatformWindow *platformWindow = window->handle();
if (platformWindow) {
QGuiApplication::platformNativeInterface()
->setWindowProperty(
platformWindow,
QString::fromUtf8("WindowsCustomMargins"),
marginsVar);
}
updateQtFrame(window, val);
} else {
qWarning().noquote() << "Can't modify the window frame: failed "
"to acquire the window handle.";

View File

@ -56,6 +56,8 @@ public:
explicit FramelessHelper(QObject *parent = nullptr);
~FramelessHelper() override = default;
static void updateQtFrame(QWindow *window, int titlebarHeight);
int borderWidth() const;
void setBorderWidth(int val);
@ -84,7 +86,7 @@ private:
#ifdef Q_OS_WINDOWS
void *getWindowRawHandle(QObject *object);
#endif
void updateQtFrame(int val);
void updateQtFrame_internal(int val);
Q_SIGNALS:
void borderWidthChanged(int);

View File

@ -155,7 +155,7 @@ void WinNativeEventFilter::uninstall() {
}
if (!m_framelessWindows.isEmpty()) {
for (auto &&window : qAsConst(m_framelessWindows)) {
updateWindow(window);
refreshWindow(window);
}
m_framelessWindows.clear();
}
@ -185,7 +185,7 @@ void WinNativeEventFilter::addFramelessWindow(HWND window, WINDOWDATA *data) {
void WinNativeEventFilter::removeFramelessWindow(HWND window) {
if (window && m_framelessWindows.contains(window)) {
m_framelessWindows.removeAll(window);
updateWindow(window);
refreshWindow(window);
}
}
@ -295,10 +295,15 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
reinterpret_cast<LPCREATESTRUCTW>(msg->lParam)->lpCreateParams;
SetWindowLongPtrW(msg->hwnd, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(userData));
refreshWindow(msg->hwnd);
break;
}
case WM_NCCALCSIZE: {
// MSDN: No special handling is needed when wParam is FALSE.
// If wParam is TRUE, it specifies that the application should indicate
// which part of the client area contains valid information. The system
// copies the valid information to the specified area within the new
// client area. If wParam is FALSE, the application does not need to
// indicate the valid part of the client area.
if (static_cast<BOOL>(msg->wParam)) {
if (IsMaximized(msg->hwnd)) {
const HMONITOR monitor =
@ -352,9 +357,16 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
}
}
}
// This line removes the window frame (including the titlebar).
// "*result = 0" removes the window frame (including the titlebar).
// But the frame shadow is lost at the same time. We'll bring it
// back later.
// Don't use "*result = WVR_REDRAW", although it can also remove
// the window frame, it will cause child widgets have strange behaviors.
// "*result = 0" means we have processed this message and let Windows
// ignore it to avoid Windows process this message again.
// "return true" means we have filtered this message and let Qt ignore
// it, in other words, it'll block Qt's own handling of this message,
// so if you don't know what Qt does internally, don't block it.
*result = 0;
return true;
}
@ -487,7 +499,7 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
auto &mmi = *reinterpret_cast<LPMINMAXINFO>(msg->lParam);
if (QOperatingSystemVersion::current() <
QOperatingSystemVersion::Windows8) {
// Buggy on Windows 7:
// FIXME: Buggy on Windows 7:
// The origin of coordinates is the top left edge of the
// monitor's work area. Why? It should be the top left edge of
// the monitor's area.
@ -499,8 +511,15 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
qAbs(rcWorkArea.left - rcMonitorArea.left);
mmi.ptMaxPosition.y = qAbs(rcWorkArea.top - rcMonitorArea.top);
}
mmi.ptMaxSize.x = qAbs(rcWorkArea.right - rcWorkArea.left);
mmi.ptMaxSize.y = qAbs(rcWorkArea.bottom - rcWorkArea.top);
if (data->windowData.maximumSize.isEmpty()) {
mmi.ptMaxSize.x = qAbs(rcWorkArea.right - rcWorkArea.left);
mmi.ptMaxSize.y = qAbs(rcWorkArea.bottom - rcWorkArea.top);
} else {
mmi.ptMaxSize.x = getDevicePixelRatioForWindow(msg->hwnd) *
data->windowData.maximumSize.width();
mmi.ptMaxSize.y = getDevicePixelRatioForWindow(msg->hwnd) *
data->windowData.maximumSize.height();
}
mmi.ptMaxTrackSize.x = mmi.ptMaxSize.x;
mmi.ptMaxTrackSize.y = mmi.ptMaxSize.y;
if (!data->windowData.minimumSize.isEmpty()) {
@ -526,9 +545,11 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
// Prevent Windows from drawing the default title bar by temporarily
// toggling the WS_VISIBLE style.
SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle & ~WS_VISIBLE);
refreshWindow(msg->hwnd);
const LRESULT ret = DefWindowProcW(msg->hwnd, msg->message,
msg->wParam, msg->lParam);
SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle);
refreshWindow(msg->hwnd);
*result = ret;
return true;
}
@ -555,8 +576,9 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
const auto dpi = dpiX == dpiY ? dpiY : dpiX;
qDebug().noquote() << "Window DPI changed: new DPI -->" << dpi
<< ", new DPR -->"
<< qreal(dpi) / qreal(m_defaultDotsPerInch);
updateWindow(msg->hwnd);
<< getPreferedNumber(qreal(dpi) /
qreal(m_defaultDotsPerInch));
refreshWindow(msg->hwnd);
break;
}
default: {
@ -618,8 +640,14 @@ void WinNativeEventFilter::handleDwmCompositionChanged(LPWINDOW data) {
const MARGINS margins = {-1, -1, -1, -1};
DwmExtendFrameIntoClientArea(data->hWnd, &margins);
}
WTA_OPTIONS options;
options.dwFlags = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON;
options.dwMask = options.dwFlags;
// This is the official way to hide the window caption text and window icon.
SetWindowThemeAttribute(data->hWnd, WTA_NONCLIENT, &options,
sizeof(options));
handleBlurForWindow(data);
updateWindow(data->hWnd);
refreshWindow(data->hWnd);
}
void WinNativeEventFilter::handleThemeChanged(LPWINDOW data) {
@ -724,62 +752,17 @@ UINT WinNativeEventFilter::getDotsPerInchForWindow(HWND handle) {
}
qreal WinNativeEventFilter::getDevicePixelRatioForWindow(HWND handle) {
qreal dpr = handle
const qreal dpr = handle
? (qreal(getDotsPerInchForWindow(handle)) / qreal(m_defaultDotsPerInch))
: m_defaultDevicePixelRatio;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
switch (QGuiApplication::highDpiScaleFactorRoundingPolicy()) {
case Qt::HighDpiScaleFactorRoundingPolicy::PassThrough:
// Default behavior for Qt 6.
break;
case Qt::HighDpiScaleFactorRoundingPolicy::Floor:
dpr = qFloor(dpr);
break;
case Qt::HighDpiScaleFactorRoundingPolicy::Ceil:
dpr = qCeil(dpr);
break;
default:
// Default behavior for Qt 5.6 to 5.15
dpr = qRound(dpr);
break;
}
#else
// Default behavior for Qt 5.6 to 5.15
dpr = qRound(dpr);
#endif
return dpr;
return getPreferedNumber(dpr);
}
int WinNativeEventFilter::getSystemMetricsForWindow(HWND handle, int index) {
if (m_GetSystemMetricsForDpi) {
UINT dpi = getDotsPerInchForWindow(handle);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
const bool shouldRound =
QGuiApplication::highDpiScaleFactorRoundingPolicy() !=
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough;
#else
const bool shouldRound = true;
#endif
if (shouldRound) {
if (dpi < (m_defaultDotsPerInch * 1.5)) {
dpi = m_defaultDotsPerInch;
} else if (dpi == (m_defaultDotsPerInch * 1.5)) {
dpi = m_defaultDotsPerInch * 1.5;
} else if (dpi < (m_defaultDotsPerInch * 2.5)) {
dpi = m_defaultDotsPerInch * 2;
} else if (dpi == (m_defaultDotsPerInch * 2.5)) {
dpi = m_defaultDotsPerInch * 2.5;
} else if (dpi < (m_defaultDotsPerInch * 3.5)) {
dpi = m_defaultDotsPerInch * 3;
} else if (dpi == (m_defaultDotsPerInch * 3.5)) {
dpi = m_defaultDotsPerInch * 3.5;
} else if (dpi < (m_defaultDotsPerInch * 4.5)) {
dpi = m_defaultDotsPerInch * 4;
} else {
qWarning().noquote() << "DPI too large:" << dpi;
}
}
return m_GetSystemMetricsForDpi(index, dpi);
return m_GetSystemMetricsForDpi(index,
static_cast<UINT>(getPreferedNumber(
getDotsPerInchForWindow(handle))));
} else {
return GetSystemMetrics(index) * getDevicePixelRatioForWindow(handle);
}
@ -788,7 +771,7 @@ int WinNativeEventFilter::getSystemMetricsForWindow(HWND handle, int index) {
void WinNativeEventFilter::setWindowData(HWND window, WINDOWDATA *data) {
if (window && data) {
createUserData(window, data);
updateWindow(window);
refreshWindow(window);
}
}
@ -824,11 +807,12 @@ void WinNativeEventFilter::createUserData(HWND handle, WINDOWDATA *data) {
}
SetWindowLongPtrW(handle, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(_data));
refreshWindow(handle);
}
}
}
void WinNativeEventFilter::updateWindow(HWND handle) {
void WinNativeEventFilter::refreshWindow(HWND handle) {
if (handle) {
// SWP_FRAMECHANGED: Applies new frame styles set using the
// SetWindowLong function. Sends a WM_NCCALCSIZE message to the window,
@ -851,6 +835,8 @@ void WinNativeEventFilter::updateWindow(HWND handle) {
SetWindowPos(handle, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOSIZE |
SWP_NOMOVE | SWP_NOZORDER | SWP_NOOWNERZORDER);
// Inform the window to adjust it's size to let it's contents fit the
// window.
SendMessageW(handle, WM_SIZE, 0, 0);
// The UpdateWindow function updates the client area of the specified
// window by sending a WM_PAINT message to the window if the window's
@ -927,3 +913,56 @@ void WinNativeEventFilter::setTitlebarHeight(int tbh) {
m_titlebarHeight = tbh;
}
}
qreal WinNativeEventFilter::getPreferedNumber(qreal num) {
qreal result = -1.0;
const auto getRoundedNumber = [](qreal in) -> qreal {
// If the given number is not very large, we assume it's a
// device pixel ratio (DPR), otherwise we assume it's a DPI.
if (in < m_defaultDotsPerInch) {
return qRound(in);
} else {
if (in < (m_defaultDotsPerInch * 1.5)) {
return m_defaultDotsPerInch;
} else if (in == (m_defaultDotsPerInch * 1.5)) {
return m_defaultDotsPerInch * 1.5;
} else if (in < (m_defaultDotsPerInch * 2.5)) {
return m_defaultDotsPerInch * 2;
} else if (in == (m_defaultDotsPerInch * 2.5)) {
return m_defaultDotsPerInch * 2.5;
} else if (in < (m_defaultDotsPerInch * 3.5)) {
return m_defaultDotsPerInch * 3;
} else if (in == (m_defaultDotsPerInch * 3.5)) {
return m_defaultDotsPerInch * 3.5;
} else if (in < (m_defaultDotsPerInch * 4.5)) {
return m_defaultDotsPerInch * 4;
} else {
qWarning().noquote()
<< "DPI too large:" << static_cast<int>(in);
}
}
return -1.0;
};
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
switch (QGuiApplication::highDpiScaleFactorRoundingPolicy()) {
case Qt::HighDpiScaleFactorRoundingPolicy::PassThrough:
// Default behavior for Qt 6.
result = num;
break;
case Qt::HighDpiScaleFactorRoundingPolicy::Floor:
result = qFloor(num);
break;
case Qt::HighDpiScaleFactorRoundingPolicy::Ceil:
result = qCeil(num);
break;
default:
// Default behavior for Qt 5.6 to 5.15
result = getRoundedNumber(num);
break;
}
#else
// Default behavior for Qt 5.6 to 5.15
result = getRoundedNumber(num);
#endif
return result;
}

View File

@ -37,7 +37,7 @@ public:
BOOL blurEnabled = FALSE;
int borderWidth = -1, borderHeight = -1, titlebarHeight = -1;
QVector<QRect> ignoreAreas, draggableAreas;
QSize minimumSize = {-1, -1};
QSize maximumSize = {-1, -1}, minimumSize = {-1, -1};
};
typedef struct tagWINDOW {
HWND hWnd = nullptr;
@ -76,11 +76,14 @@ public:
static void setBorderHeight(int bh);
static void setTitlebarHeight(int tbh);
// DPI-aware border width of the given window.
// DPI-aware border width of the given window (if the pointer is null,
// return the system's standard value).
static int borderWidth(HWND handle);
// DPI-aware border height of the given window.
// DPI-aware border height of the given window (if the pointer is null,
// return the system's standard value).
static int borderHeight(HWND handle);
// DPI-aware titlebar height of the given window.
// DPI-aware titlebar height of the given window (if the pointer is null,
// return the system's standard value).
static int titlebarHeight(HWND handle);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
@ -98,7 +101,8 @@ private:
void handleDwmCompositionChanged(LPWINDOW data);
void handleThemeChanged(LPWINDOW data);
void handleBlurForWindow(LPWINDOW data);
static void updateWindow(HWND handle);
static void refreshWindow(HWND handle);
static qreal getPreferedNumber(qreal num);
static UINT getDotsPerInchForWindow(HWND handle);
static qreal getDevicePixelRatioForWindow(HWND handle);
static int getSystemMetricsForWindow(HWND handle, int index);