Win32: Fix window content will be cut-off for some pixels when window is fullscreened

1. Fix that bug
2. Some adjustments of the variable names to make them more clear
3. Some tweaks to the title bar height value

TODO: Add a button to switch between fullscreen and windowed mode for the widget example
TODO: Add a title text to the widget example

Signed-off-by: Yuhang Zhao <2546789017@qq.com>
This commit is contained in:
Yuhang Zhao 2021-09-04 22:40:13 +08:00
parent 2c106ffef9
commit bc8a70dbb2
8 changed files with 135 additions and 56 deletions

View File

@ -1,3 +1,5 @@
/*
* MIT License
*
@ -21,7 +23,6 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import QtQuick 2.0
import QtQuick.Window 2.0
import QtQuick.Controls 2.0
@ -35,7 +36,10 @@ Window {
title: qsTr("Hello, World!")
color: "#f0f0f0"
property real _flh_margin: ((window.visibility === Window.Maximized) || (window.visibility === Window.FullScreen)) ? 0.0 : (1.0 / Screen.devicePixelRatio)
property real _flh_margin: ((window.visibility === Window.Maximized)
|| (window.visibility
=== Window.FullScreen)) ? 0.0 : (1.0 / Screen.devicePixelRatio)
property var _win_prev_state: null
FramelessHelper {
id: framelessHelper
@ -51,7 +55,7 @@ Window {
Rectangle {
id: titleBar
height: framelessHelper.titleBarHeight + framelessHelper.resizeBorderHeight
height: framelessHelper.titleBarHeight
color: "transparent"
anchors {
top: parent.top
@ -79,12 +83,14 @@ Window {
MinimizeButton {
id: minimizeButton
onClicked: window.showMinimized()
Component.onCompleted: framelessHelper.setHitTestVisibleInChrome(minimizeButton, true)
Component.onCompleted: framelessHelper.setHitTestVisibleInChrome(
minimizeButton, true)
}
MaximizeButton {
id: maximizeButton
maximized: ((window.visibility === Window.Maximized) || (window.visibility === Window.FullScreen))
maximized: ((window.visibility === Window.Maximized)
|| (window.visibility === Window.FullScreen))
onClicked: {
if (maximized) {
window.showNormal()
@ -92,13 +98,15 @@ Window {
window.showMaximized()
}
}
Component.onCompleted: framelessHelper.setHitTestVisibleInChrome(maximizeButton, true)
Component.onCompleted: framelessHelper.setHitTestVisibleInChrome(
maximizeButton, true)
}
CloseButton {
id: closeButton
onClicked: window.close()
Component.onCompleted: framelessHelper.setHitTestVisibleInChrome(closeButton, true)
Component.onCompleted: framelessHelper.setHitTestVisibleInChrome(
closeButton, true)
}
}
}
@ -112,6 +120,28 @@ Window {
}
}
Button {
anchors {
horizontalCenter: parent.horizontalCenter
top: label.bottom
topMargin: 15
}
property bool _full: window.visibility === Window.FullScreen
text: _full ? qsTr("Exit FullScreen") : qsTr("Enter FullScreen")
onClicked: {
if (_full) {
if (_win_prev_state == Window.Maximized) {
window.showMaximized()
} else if (_win_prev_state == Window.Windowed) {
window.showNormal()
}
} else {
_win_prev_state = window.visibility
window.showFullScreen()
}
}
}
Rectangle {
id: windowFrame
anchors.fill: parent

View File

@ -116,10 +116,8 @@ void Widget::setupUi()
setWindowTitle(tr("Hello, World!"));
resize(800, 600);
const QWindow *win = windowHandle();
const int resizeBorderThickness = Utilities::getSystemMetric(win, SystemMetric::ResizeBorderThickness, false);
const int titleBarHeight = Utilities::getSystemMetric(win, SystemMetric::TitleBarHeight, false);
const int systemButtonHeight = titleBarHeight + resizeBorderThickness;
const QSize systemButtonSize = {qRound(static_cast<qreal>(systemButtonHeight) * 1.5), systemButtonHeight};
const QSize systemButtonSize = {qRound(static_cast<qreal>(titleBarHeight) * 1.5), titleBarHeight};
m_minimizeButton = new QPushButton(this);
m_minimizeButton->setObjectName(QStringLiteral("MinimizeButton"));
m_minimizeButton->setFixedSize(systemButtonSize);

View File

@ -71,7 +71,8 @@ bool FramelessHelper::eventFilter(QObject *object, QEvent *event)
}
const QEvent::Type type = event->type();
// We are only interested in mouse events.
if ((type != QEvent::MouseButtonDblClick) && (type != QEvent::MouseButtonPress) && (type != QEvent::MouseMove)) {
if ((type != QEvent::MouseButtonDblClick) && (type != QEvent::MouseButtonPress)
&& (type != QEvent::MouseMove)) {
return false;
}
const auto window = qobject_cast<QWindow *>(object);
@ -125,7 +126,7 @@ bool FramelessHelper::eventFilter(QObject *object, QEvent *event)
}
if (window->windowState() == Qt::WindowNoState) {
isInTitlebarArea = (localMousePosition.y() > resizeBorderThickness)
&& (localMousePosition.y() <= (titleBarHeight + resizeBorderThickness))
&& (localMousePosition.y() <= titleBarHeight)
&& (localMousePosition.x() > resizeBorderThickness)
&& (localMousePosition.x() < (windowWidth - resizeBorderThickness))
&& !hitTestVisible;

View File

@ -90,6 +90,7 @@ namespace Constants
[[maybe_unused]] constexpr char kFramelessModeFlag[] = "_FRAMELESSHELPER_FRAMELESS_MODE";
[[maybe_unused]] constexpr char kResizeBorderThicknessFlag[] = "_FRAMELESSHELPER_RESIZE_BORDER_THICKNESS";
[[maybe_unused]] constexpr char kCaptionHeightFlag[] = "_FRAMELESSHELPER_CAPTION_HEIGHT";
[[maybe_unused]] constexpr char kTitleBarHeightFlag[] = "_FRAMELESSHELPER_TITLE_BAR_HEIGHT";
[[maybe_unused]] constexpr char kHitTestVisibleInChromeFlag[] = "_FRAMELESSHELPER_HIT_TEST_VISIBLE_IN_CHROME";
[[maybe_unused]] constexpr char kUseNativeTitleBarFlag[] = "_FRAMELESSHELPER_USE_NATIVE_TITLE_BAR";

View File

@ -589,21 +589,21 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
qWarning() << "Failed to retrieve the client rect of the current window.";
break;
}
const LONG ww = clientRect.right;
const int rbt = Utilities::getSystemMetric(window, SystemMetric::ResizeBorderThickness, true);
const int tbh = Utilities::getSystemMetric(window, SystemMetric::TitleBarHeight, true);
const LONG windowWidth = clientRect.right;
const int resizeBorderThickness = Utilities::getSystemMetric(window, SystemMetric::ResizeBorderThickness, true);
const int titleBarHeight = Utilities::getSystemMetric(window, SystemMetric::TitleBarHeight, true);
bool isTitleBar = false;
if (IsMaximized(msg->hwnd) || (window->windowState() == Qt::WindowFullScreen)) {
isTitleBar = (localMouse.y() >= 0) && (localMouse.y() <= tbh)
&& (localMouse.x() >= 0) && (localMouse.x() <= ww)
isTitleBar = (localMouse.y() >= 0) && (localMouse.y() <= titleBarHeight)
&& (localMouse.x() >= 0) && (localMouse.x() <= windowWidth)
&& !Utilities::isHitTestVisibleInChrome(window);
}
if (window->windowState() == Qt::WindowNoState) {
isTitleBar = (localMouse.y() > rbt) && (localMouse.y() <= (rbt + tbh))
&& (localMouse.x() > rbt) && (localMouse.x() < (ww - rbt))
isTitleBar = (localMouse.y() > resizeBorderThickness) && (localMouse.y() <= titleBarHeight)
&& (localMouse.x() > resizeBorderThickness) && (localMouse.x() < (windowWidth - resizeBorderThickness))
&& !Utilities::isHitTestVisibleInChrome(window);
}
const bool isTop = localMouse.y() <= rbt;
const bool isTop = localMouse.y() <= resizeBorderThickness;
if (shouldHaveWindowFrame()) {
// This will handle the left, right and bottom parts of the frame
// because we didn't change them.
@ -628,19 +628,19 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
*result = HTCLIENT;
return true;
} else {
const LRESULT hitTestResult = [clientRect, msg, isTitleBar, &localMouse, rbt, ww, isTop, window]{
const LRESULT hitTestResult = [clientRect, msg, isTitleBar, &localMouse, resizeBorderThickness, windowWidth, isTop, window]{
if (IsMaximized(msg->hwnd)) {
if (isTitleBar) {
return HTCAPTION;
}
return HTCLIENT;
}
const LONG wh = clientRect.bottom;
const bool isBottom = (localMouse.y() >= (wh - rbt));
const LONG windowHeight = clientRect.bottom;
const bool isBottom = (localMouse.y() >= (windowHeight - resizeBorderThickness));
// Make the border a little wider to let the user easy to resize on corners.
const qreal factor = (isTop || isBottom) ? 2.0 : 1.0;
const bool isLeft = (localMouse.x() <= qRound(static_cast<qreal>(rbt) * factor));
const bool isRight = (localMouse.x() >= (ww - qRound(static_cast<qreal>(rbt) * factor)));
const bool isLeft = (localMouse.x() <= qRound(static_cast<qreal>(resizeBorderThickness) * factor));
const bool isRight = (localMouse.x() >= (windowWidth - qRound(static_cast<qreal>(resizeBorderThickness) * factor)));
const bool fixedSize = Utilities::isWindowFixedSize(window);
const auto getBorderValue = [fixedSize](int value) -> int {
return fixedSize ? HTCLIENT : value;
@ -689,15 +689,32 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
const auto oldStyle = GetWindowLongPtrW(msg->hwnd, GWL_STYLE);
// Prevent Windows from drawing the default title bar by temporarily
// toggling the WS_VISIBLE style.
SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle & ~WS_VISIBLE);
if (SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle & ~WS_VISIBLE) == 0) {
qWarning() << "SetWindowLongPtrW() failed.";
break;
}
const auto winId = reinterpret_cast<WId>(msg->hwnd);
Utilities::triggerFrameChange(winId);
const LRESULT ret = DefWindowProcW(msg->hwnd, msg->message, msg->wParam, msg->lParam);
SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle);
if (SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle) == 0) {
qWarning() << "SetWindowLongPtrW() failed.";
break;
}
Utilities::triggerFrameChange(winId);
*result = ret;
return true;
}
#if 0
case WM_SIZE: {
const bool normal = (msg->wParam == SIZE_RESTORED);
const bool max = (msg->wParam == SIZE_MAXIMIZED);
const bool full = (window->windowState() == Qt::WindowFullScreen);
if (normal || max || full) {
Utilities::updateFrameMargins(reinterpret_cast<WId>(msg->hwnd), (max || full));
Utilities::updateQtFrameMargins(const_cast<QWindow *>(window), true);
}
} break;
#endif
default:
break;
}

View File

@ -54,6 +54,15 @@ void FramelessWindowsManager::addWindow(QWindow *window)
framelessHelperUnix()->removeWindowFrame(window);
#else
FramelessHelperWin::addFramelessWindow(window);
QObject::connect(window, &QWindow::windowStateChanged, [window](Qt::WindowState state){
const bool normal = (state == Qt::WindowNoState);
const bool max = (state == Qt::WindowMaximized);
const bool full = (state == Qt::WindowFullScreen);
if (normal || max || full) {
Utilities::updateFrameMargins(window->winId(), (max || full));
Utilities::updateQtFrameMargins(window, true);
}
});
// Work-around a Win32 multi-monitor bug.
QObject::connect(window, &QWindow::screenChanged, [window](QScreen *screen){
Q_UNUSED(screen);

View File

@ -32,6 +32,7 @@ FRAMELESSHELPER_BEGIN_NAMESPACE
enum class SystemMetric : int
{
ResizeBorderThickness = 0,
CaptionHeight,
TitleBarHeight
};

View File

@ -50,11 +50,10 @@ Q_DECLARE_METATYPE(QMargins)
FRAMELESSHELPER_BEGIN_NAMESPACE
static constexpr char kDwmRegistryKey[] = R"(Software\Microsoft\Windows\DWM)";
static constexpr char kDwmCompositionRegistryKey[] = "Composition";
static constexpr int kDefaultResizeBorderThickness = 8;
static constexpr int kDefaultResizeBorderThicknessClassic = 4;
static constexpr int kDefaultTitleBarHeight = 23;
static constexpr int kDefaultResizeBorderThicknessAero = 8;
static constexpr int kDefaultCaptionHeight = 23;
bool Utilities::isDwmCompositionAvailable()
{
@ -68,7 +67,7 @@ bool Utilities::isDwmCompositionAvailable()
}
const QSettings registry(QString::fromUtf8(kDwmRegistryKey), QSettings::NativeFormat);
bool ok = false;
const int value = registry.value(QString::fromUtf8(kDwmCompositionRegistryKey), 0).toInt(&ok);
const int value = registry.value(QStringLiteral("Composition"), 0).toInt(&ok);
return (ok && (value != 0));
}
@ -78,51 +77,67 @@ int Utilities::getSystemMetric(const QWindow *window, const SystemMetric metric,
if (!window) {
return 0;
}
const qreal devicePixelRatio = window->devicePixelRatio();
const qreal scaleFactor = (dpiScale ? devicePixelRatio : 1.0);
switch (metric) {
case SystemMetric::ResizeBorderThickness: {
const int rbt = window->property(Constants::kResizeBorderThicknessFlag).toInt();
if ((rbt > 0) && !forceSystemValue) {
return qRound(static_cast<qreal>(rbt) * (dpiScale ? window->devicePixelRatio() : 1.0));
const int resizeBorderThickness = window->property(Constants::kResizeBorderThicknessFlag).toInt();
if ((resizeBorderThickness > 0) && !forceSystemValue) {
return qRound(static_cast<qreal>(resizeBorderThickness) * scaleFactor);
} else {
const int result = GetSystemMetrics(SM_CXSIZEFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER);
if (result > 0) {
if (dpiScale) {
return result;
} else {
return qRound(static_cast<qreal>(result) / window->devicePixelRatio());
return qRound(static_cast<qreal>(result) / devicePixelRatio);
}
} else {
// The padded border will disappear if DWM composition is disabled.
const int defaultRBT = (isDwmCompositionAvailable() ? kDefaultResizeBorderThickness : kDefaultResizeBorderThicknessClassic);
const int defaultResizeBorderThickness = (isDwmCompositionAvailable() ? kDefaultResizeBorderThicknessAero : kDefaultResizeBorderThicknessClassic);
if (dpiScale) {
return qRound(static_cast<qreal>(defaultRBT) * window->devicePixelRatio());
return qRound(static_cast<qreal>(defaultResizeBorderThickness) * devicePixelRatio);
} else {
return defaultRBT;
return defaultResizeBorderThickness;
}
}
}
}
case SystemMetric::TitleBarHeight: {
const int tbh = window->property(Constants::kTitleBarHeightFlag).toInt();
if ((tbh > 0) && !forceSystemValue) {
return qRound(static_cast<qreal>(tbh) * (dpiScale ? window->devicePixelRatio() : 1.0));
case SystemMetric::CaptionHeight: {
const int captionHeight = window->property(Constants::kCaptionHeightFlag).toInt();
if ((captionHeight > 0) && !forceSystemValue) {
return qRound(static_cast<qreal>(captionHeight) * scaleFactor);
} else {
const int result = GetSystemMetrics(SM_CYCAPTION);
if (result > 0) {
if (dpiScale) {
return result;
} else {
return qRound(static_cast<qreal>(result) / window->devicePixelRatio());
return qRound(static_cast<qreal>(result) / devicePixelRatio);
}
} else {
if (dpiScale) {
return qRound(static_cast<qreal>(kDefaultTitleBarHeight) * window->devicePixelRatio());
return qRound(static_cast<qreal>(kDefaultCaptionHeight) * devicePixelRatio);
} else {
return kDefaultTitleBarHeight;
return kDefaultCaptionHeight;
}
}
}
}
case SystemMetric::TitleBarHeight: {
const int titleBarHeight = window->property(Constants::kTitleBarHeightFlag).toInt();
if ((titleBarHeight > 0) && !forceSystemValue) {
return qRound(static_cast<qreal>(titleBarHeight) * scaleFactor);
} else {
const int captionHeight = getSystemMetric(window,SystemMetric::CaptionHeight,
dpiScale, forceSystemValue);
const int resizeBorderThickness = getSystemMetric(window, SystemMetric::ResizeBorderThickness,
dpiScale, forceSystemValue);
return (((window->windowState() == Qt::WindowMaximized)
|| (window->windowState() == Qt::WindowFullScreen))
? captionHeight : (captionHeight + resizeBorderThickness));
}
}
}
return 0;
}
@ -134,14 +149,15 @@ void Utilities::triggerFrameChange(const WId winId)
return;
}
const auto hwnd = reinterpret_cast<HWND>(winId);
if (SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOOWNERZORDER) == FALSE) {
constexpr UINT flags = (SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOOWNERZORDER);
if (SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, flags) == FALSE) {
qWarning() << "SetWindowPos() failed.";
}
}
void Utilities::updateFrameMargins(const WId winId, const bool reset)
{
// DwmExtendFrameIntoClientArea() will always fail if DWM Composition is disabled.
// DwmExtendFrameIntoClientArea() will always fail if DWM composition is disabled.
// No need to try in this case.
if (!isDwmCompositionAvailable()) {
return;
@ -163,9 +179,12 @@ void Utilities::updateQtFrameMargins(QWindow *window, const bool enable)
if (!window) {
return;
}
const int rbt = enable ? Utilities::getSystemMetric(window, SystemMetric::ResizeBorderThickness, true, true) : 0;
const int tbh = enable ? Utilities::getSystemMetric(window, SystemMetric::TitleBarHeight, true, true) : 0;
const QMargins margins = {-rbt, -(rbt + tbh), -rbt, -rbt}; // left, top, right, bottom
const bool shouldApplyCustomFrameMargins = (enable
&& (window->windowState() != Qt::WindowMaximized)
&& (window->windowState() != Qt::WindowFullScreen));
const int resizeBorderThickness = shouldApplyCustomFrameMargins ? Utilities::getSystemMetric(window, SystemMetric::ResizeBorderThickness, true, true) : 0;
const int titleBarHeight = shouldApplyCustomFrameMargins ? Utilities::getSystemMetric(window, SystemMetric::TitleBarHeight, true, true) : 0;
const QMargins margins = {-resizeBorderThickness, -titleBarHeight, -resizeBorderThickness, -resizeBorderThickness}; // left, top, right, bottom
const QVariant marginsVar = QVariant::fromValue(margins);
window->setProperty("_q_windowsCustomMargins", marginsVar);
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
@ -185,28 +204,31 @@ void Utilities::updateQtFrameMargins(QWindow *window, const bool enable)
bool Utilities::isWin8OrGreater()
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8;
static const bool result = (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8);
#else
return QSysInfo::WindowsVersion >= QSysInfo::WV_WINDOWS8;
static const bool result = (QSysInfo::WindowsVersion >= QSysInfo::WV_WINDOWS8);
#endif
return result;
}
bool Utilities::isWin8Point1OrGreater()
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1;
static const bool result = (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1);
#else
return QSysInfo::WindowsVersion >= QSysInfo::WV_WINDOWS8_1;
static const bool result = (QSysInfo::WindowsVersion >= QSysInfo::WV_WINDOWS8_1);
#endif
return result;
}
bool Utilities::isWin10OrGreater()
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10;
static const bool result = (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10);
#else
return QSysInfo::WindowsVersion >= QSysInfo::WV_WINDOWS10;
static const bool result = (QSysInfo::WindowsVersion >= QSysInfo::WV_WINDOWS10);
#endif
return result;
}
FRAMELESSHELPER_END_NAMESPACE