redesign the title bar interface

Signed-off-by: Yuhang Zhao <2546789017@qq.com>
This commit is contained in:
Yuhang Zhao 2022-05-04 14:50:11 +08:00
parent 8ccbe2cf94
commit a97b1782ae
11 changed files with 187 additions and 118 deletions

View File

@ -1,14 +1,20 @@
# FramelessHelper 2.x
## Highlights compared to 2.1 (TODO list)
- Common: Added cross-platform customizable system menu for both Qt Widgets and Qt Quick. Also supports both light and dark theme.
- Common: More configurable options from environment variables and settings file.
- Common: Migrate to categorized logging output.
- Examples: Added QtWebEngine based demo projects for both Qt Widgets and Qt Quick.
## Highlights compared to 2.0
- Windows: Added support for the snap layout feature introduced in Windows 11.
- Quick: Restored some 1.x interfaces which may be convenient for Qt Quick users.
- Examples: Added QtWebEngine based demo projects for both Qt Widgets and Qt Quick.
- Common: Added cross-platform customizable system menu for both Qt Widgets and Qt Quick. Also supports both light and dark theme.
- Widgets: Redesigned the public interface, the use of FramelessHelper is now more elegant.
- Quick: Redesigned the public interface, the use of FramelessHelper is now more elegant.
- Common: Redesigned the standard title bar interface, it's now possible to customize it from outside. Previously there's no standard title bar in the widgets module, now it's added and exported.
- Misc: Removed bundled Qt internal classes that are licensed under Commercial/GPL/LGPL. This library is now pure MIT licensed.
- Misc: Migrate to categorized logging output.
- Misc: Bug fixes and internal refactorings, improved stability on all supported platforms.
- Bug fixes and internal refactorings.
## Highlights compared to 1.x
@ -65,9 +71,23 @@ cmake --build . --config Release --target all --parallel
## Use
For Qt Widgets applications: subclass `FramelessWidget` or `FramelessMainWindow`.
### Qt Widgets
For Qt Quick applications: use `FramelessWindow` instead of `Window`.
To customize the window frame of a QWidget, you need to instantiate a `FramelessWidgetsHelper` object and then attach it to the widget's top level parent for the widget.
`FramelessWidgetsHelper` will do all the work for you: the window frame will be removed automatically once you attach it to your top level widget. In theory you can instantiate
multiple `FramelessWidgetsHelper` objects for the same widget, in this case there will be only one object that keeps functional, all other objects of `FramelessWidgetsHelper` will
become a wrapper of that one. But to make sure everything goes smoothly and normally, you should not do that in any case. The simplest way to instantiate a `FramelessWidgetsHelper`
object is to call the static method `FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *)`. It will return the previously instantiated object if any, or it will
instantiate a new object if it can't find one. It's safe to call it multiple times for a same widget, it won't instantiate any new object if there is one already. It also does
not matter where you call that function as long as the top level widget is the same. The internally created object will always be parented to the top level widget. Once you get
the `FramelessWidgetsHelper` object, you should call `void FramelessWidgetsHelper::attach()` to let it attach to the top level widget. The window frame
will be removed automatically once it has attached to the top level widget successfully. In order to make sure `FramelessWidgetsHelper` can find the correct top level widget,
you should call the `get` function on a widget which has a complete parent-chain. After these two steps, the window frame should be removed now. However, it can't be moved by
dragging because it doesn't have a title bar now. You should set a title bar widget to make the window be movable, the title bar doesn't need to be a rectangle, it also doesn't need to be on the top of the window. Call `void FramelessWidgetsHelper::setTitleBarWidget(QWidget *)` to do that. By default, all the widgets in the title bar area won't be responsible due to the mouse events are intercepted by FramelessHelper. To make them still work normally, you should make them visible to hit test. Call `void FramelessWidgetsHelper::setHitTestVisible(QWidget* )` to do that. You can of course call it on a widget that is not inside the title bar at all, but it won't have any effect. Due to Qt's own limitations, you need to make sure your widget has a complete parent-chain which the root parent is the top level widget.
### Qt Quick
TODO
Please refer to the demo applications to see more detailed usages: [examples](./examples/)

View File

@ -59,21 +59,6 @@ FramelessWindow {
left: parent.left
right: parent.right
}
active: window.active
maximized: window.zoomed
title: window.title
minimizeButton {
id: minimizeButton
onClicked: window.showMinimized2()
}
maximizeButton {
id: maximizeButton
onClicked: window.toggleMaximized()
}
closeButton {
id: closeButton
onClicked: window.close()
}
Component.onCompleted: {
// Make our homemade title bar snap to the window top frame border.
window.snapToTopBorder(titleBar, FramelessHelperConstants.Top, FramelessHelperConstants.Bottom);
@ -82,9 +67,9 @@ FramelessWindow {
FramelessHelper.titleBarItem = titleBar;
// Make our own items visible to the hit test and on Windows, enable
// the snap layout feature (available since Windows 11).
FramelessHelper.setSystemButton(minimizeButton, FramelessHelperConstants.Minimize);
FramelessHelper.setSystemButton(maximizeButton, FramelessHelperConstants.Maximize);
FramelessHelper.setSystemButton(closeButton, FramelessHelperConstants.Close);
FramelessHelper.setSystemButton(titleBar.minimizeButton, FramelessHelperConstants.Minimize);
FramelessHelper.setSystemButton(titleBar.maximizeButton, FramelessHelperConstants.Maximize);
FramelessHelper.setSystemButton(titleBar.closeButton, FramelessHelperConstants.Close);
}
}
}

View File

@ -64,7 +64,7 @@ public Q_SLOTS:
void setWindowFixedSize(const bool value);
protected:
void itemChange(const ItemChange change, const ItemChangeData &data) override;
void itemChange(const ItemChange change, const ItemChangeData &value) override;
Q_SIGNALS:
void titleBarItemChanged();

View File

@ -80,6 +80,7 @@ FRAMELESSHELPER_STRING_CONSTANT(CreateWindowExW)
FRAMELESSHELPER_STRING_CONSTANT(SetLayeredWindowAttributes)
FRAMELESSHELPER_STRING_CONSTANT(SetWindowPos)
FRAMELESSHELPER_STRING_CONSTANT(TrackMouseEvent)
FRAMELESSHELPER_STRING_CONSTANT(FindWindowW)
[[nodiscard]] static inline LRESULT CALLBACK DragBarWindowProc
(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam)
@ -553,8 +554,7 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
// height window border). This can be confirmed in Windows
// Terminal's source code, you can also try yourself to verify
// it. So things will become quite complicated if you want to
// preserve the four window borders. So we just remove the whole
// window frame, otherwise the code will become much more complex.
// preserve the four window borders.
// If `wParam` is `FALSE`, `lParam` points to a `RECT` that contains
// the proposed window rectangle for our window. During our
@ -677,7 +677,7 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
edge = _abd.uEdge;
}
} else {
qWarning() << "Failed to retrieve the task bar window handle.";
qWarning() << Utils::getSystemErrorMessage(kFindWindowW);
break;
}
top = (edge == ABE_TOP);
@ -743,15 +743,20 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
// 会看起来是在内部进行,这个问题通过常规方法非常难以解决。我测试过
// QQ和钉钉的窗口它们的窗口就是在外部resize但实际上它们是通过
// 把窗口实际的内容,嵌入到一个完全透明的但尺寸要大一圈的窗口中实现
// 的,虽然看起来效果还行,但在我看来不是正途。而且我之所以能发现,
// 也是由于这种方法在很多情况下会露馅,比如窗口未响应卡住或贴边的时
// 候,能明显看到窗口周围多出来一圈边界。我曾经尝试再把那三个区域弄
// 透明但无一例外都会破坏DWM绘制的边框阴影因此只好作罢。
// 的,虽然看起来效果还不错,但对于此项目而言,代码和窗口结构过于复
// 杂,因此我没有采用此方案。然而,对于具体的软件项目而言,其做法也
// 不失为一个优秀的解决方案,毕竟其在大多数条件下的表现都还可以。
//
// 和1.x的做法不同现在的2.x选择了保留窗口三边去除整个窗口顶部
// 好处是保留了系统的原生边框外观较好且与系统结合紧密而且resize
// 的表现也有很大改善,缺点是需要自行绘制顶部边框线。原本以为只能像
// Windows Terminal那样在WM_PAINT里搞黑魔法但后来发现其实只
// 要颜色相近,我们自行绘制一根实线也几乎能以假乱真,而且这样也不会
// 破坏Qt自己的绘制系统能做到不依赖黑魔法就能实现像Windows Terminal
// 那样外观和功能都比较完美的自定义边框。
// As you may have found, if you use this code, the resize areas
// will be inside the frameless window, however, a normal Win32
// window can be resized outside of it. Here is the reason: the
// WS_THICKFRAME window style will cause a window has three
// A normal Win32 window can be resized outside of it. Here is the
// reason: the WS_THICKFRAME window style will cause a window has three
// transparent areas beside the window's left, right and bottom
// edge. Their width or height is eight pixels if the window is not
// scaled. In most cases, they are totally invisible. It's DWM's
@ -782,9 +787,20 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
// horrible in dark mode. This solution only supports Windows 10
// because the border width on Win10 is only one pixel, however it's
// eight pixels on Windows 7 so preserving the three window borders
// looks terrible on old systems. I'm testing this solution in
// another branch, if you are interested in it, you can give it a
// try.
// looks terrible on old systems.
//
// Unlike the 1.x code, we choose to preserve the three edges of the
// window in 2.x, and get rid of the whole top part of the window.
// There are quite some advantages such as the appearance looks much
// better and due to we have the original system window frame, our
// window can behave just like a normal Win32 window even if we now
// doesn't have a title bar at all. Most importantly, the flicker and
// jitter during window resizing is totally gone now. The disadvantage
// is we have to draw a top frame border ourself. Previously I thought
// we have to do the black magic in WM_PAINT just like what Windows
// Terminal does, however, later I found that if we choose a proper
// color, our homemade top border can almost have exactly the same
// appearance with the system's one.
if (data.params.isWindowFixedSize()) {
*result = HTCLIENT;
@ -1014,7 +1030,7 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
const bool dark = Utils::shouldAppsUseDarkMode();
Utils::updateWindowFrameBorderColor(windowId, dark);
if (Utils::isWindowsVersionOrGreater(WindowsVersion::_10_1809)) {
//Utils::updateGlobalWin32ControlsTheme(windowId, dark);
//Utils::updateGlobalWin32ControlsTheme(windowId, dark); // Causes some QtWidgets paint incorrectly.
}
}
}

View File

@ -552,14 +552,14 @@ FramelessQuickHelper *FramelessQuickHelper::qmlAttachedProperties(QObject *paren
if (!parentObject) {
return nullptr;
}
const auto item = new FramelessQuickHelper;
const auto instance = new FramelessQuickHelper;
const auto parentItem = qobject_cast<QQuickItem *>(parentObject);
if (parentItem) {
item->setParentItem(parentItem);
instance->setParentItem(parentItem);
} else {
item->setParent(parentObject);
instance->setParent(parentObject);
}
return item;
return instance;
}
QQuickItem *FramelessQuickHelper::titleBarItem() const
@ -644,10 +644,10 @@ void FramelessQuickHelper::setWindowFixedSize(const bool value)
d->setWindowFixedSize(value);
}
void FramelessQuickHelper::itemChange(const ItemChange change, const ItemChangeData &data)
void FramelessQuickHelper::itemChange(const ItemChange change, const ItemChangeData &value)
{
QQuickItem::itemChange(change, data);
if ((change == ItemSceneChange) && data.window) {
QQuickItem::itemChange(change, value);
if ((change == ItemSceneChange) && value.window) {
Q_D(FramelessQuickHelper);
d->attachToWindow();
}

View File

@ -48,7 +48,7 @@ public:
explicit QuickStandardCloseButton(QQuickItem *parent = nullptr);
~QuickStandardCloseButton() override;
public Q_SLOTS:
private Q_SLOTS:
void updateForeground();
void updateBackground();
void updateToolTip();

View File

@ -52,7 +52,7 @@ public:
Q_NODISCARD bool isMaximized() const;
void setMaximized(const bool max);
public Q_SLOTS:
private Q_SLOTS:
void updateForeground();
void updateBackground();
void updateToolTip();

View File

@ -48,7 +48,7 @@ public:
explicit QuickStandardMinimizeButton(QQuickItem *parent = nullptr);
~QuickStandardMinimizeButton() override;
public Q_SLOTS:
private Q_SLOTS:
void updateForeground();
void updateBackground();
void updateToolTip();

View File

@ -46,30 +46,6 @@ QuickStandardTitleBar::QuickStandardTitleBar(QQuickItem *parent) : QQuickRectang
QuickStandardTitleBar::~QuickStandardTitleBar() = default;
bool QuickStandardTitleBar::isActive() const
{
return m_active;
}
void QuickStandardTitleBar::setActive(const bool value)
{
if (m_active == value) {
return;
}
m_active = value;
Q_EMIT activeChanged();
}
bool QuickStandardTitleBar::isMaximized() const
{
return m_maxBtn->isMaximized();
}
void QuickStandardTitleBar::setMaximized(const bool value)
{
m_maxBtn->setMaximized(value);
}
Qt::Alignment QuickStandardTitleBar::titleLabelAlignment() const
{
return m_labelAlignment;
@ -112,16 +88,6 @@ void QuickStandardTitleBar::setTitleLabelAlignment(const Qt::Alignment value)
Q_EMIT titleLabelAlignmentChanged();
}
QString QuickStandardTitleBar::title() const
{
return m_label->text();
}
void QuickStandardTitleBar::setTitle(const QString &value)
{
m_label->setText(value);
}
QuickStandardMinimizeButton *QuickStandardTitleBar::minimizeButton() const
{
return m_minBtn.data();
@ -137,11 +103,33 @@ QuickStandardCloseButton *QuickStandardTitleBar::closeButton() const
return m_closeBtn.data();
}
void QuickStandardTitleBar::updateMaximizeButton()
{
const QQuickWindow * const w = window();
if (!w) {
return;
}
m_maxBtn->setMaximized(w->visibility() == QQuickWindow::Maximized);
}
void QuickStandardTitleBar::updateTitleLabelText()
{
const QQuickWindow * const w = window();
if (!w) {
return;
}
m_label->setText(w->title());
}
void QuickStandardTitleBar::updateTitleBarColor()
{
const QQuickWindow * const w = window();
if (!w) {
return;
}
QColor backgroundColor = {};
QColor foregroundColor = {};
if (m_active) {
if (w->isActive()) {
if (Utils::isTitleBarColorized()) {
#ifdef Q_OS_WINDOWS
backgroundColor = Utils::getDwmColorizationColor();
@ -174,11 +162,42 @@ void QuickStandardTitleBar::updateTitleBarColor()
m_label->setColor(foregroundColor);
}
void QuickStandardTitleBar::clickMinimizeButton()
{
QQuickWindow * const w = window();
if (!w) {
return;
}
w->setVisibility(QQuickWindow::Minimized);
}
void QuickStandardTitleBar::clickMaximizeButton()
{
QQuickWindow * const w = window();
if (!w) {
return;
}
if (w->visibility() == QQuickWindow::Maximized) {
w->setVisibility(QQuickWindow::Windowed);
} else {
w->setVisibility(QQuickWindow::Maximized);
}
}
void QuickStandardTitleBar::clickCloseButton()
{
QQuickWindow * const w = window();
if (!w) {
return;
}
w->close();
}
void QuickStandardTitleBar::initialize()
{
QQuickPen * const _border = border();
_border->setWidth(0.0);
_border->setColor(kDefaultTransparentColor);
QQuickPen * const b = border();
b->setWidth(0.0);
b->setColor(kDefaultTransparentColor);
setHeight(kDefaultTitleBarHeight);
m_label.reset(new QQuickLabel(this));
@ -192,15 +211,43 @@ void QuickStandardTitleBar::initialize()
rowAnchors->setTop(thisPriv->top());
rowAnchors->setRight(thisPriv->right());
m_minBtn.reset(new QuickStandardMinimizeButton(m_row.data()));
connect(m_minBtn.data(), &QuickStandardMinimizeButton::clicked, this, &QuickStandardTitleBar::clickMinimizeButton);
m_maxBtn.reset(new QuickStandardMaximizeButton(m_row.data()));
connect(m_maxBtn.data(), &QuickStandardMaximizeButton::clicked, this, &QuickStandardTitleBar::clickMaximizeButton);
m_closeBtn.reset(new QuickStandardCloseButton(m_row.data()));
connect(m_closeBtn.data(), &QuickStandardCloseButton::clicked, this, &QuickStandardTitleBar::clickCloseButton);
connect(FramelessWindowsManager::instance(), &FramelessWindowsManager::systemThemeChanged, this, &QuickStandardTitleBar::updateTitleBarColor);
connect(this, &QuickStandardTitleBar::activeChanged, this, &QuickStandardTitleBar::updateTitleBarColor);
connect(m_label.data(), &QQuickLabel::textChanged, this, &QuickStandardTitleBar::titleChanged);
connect(m_maxBtn.data(), &QuickStandardMaximizeButton::maximizedChanged, this, &QuickStandardTitleBar::maximizedChanged);
setTitleLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter);
updateAll();
}
void QuickStandardTitleBar::itemChange(const ItemChange change, const ItemChangeData &value)
{
QQuickRectangle::itemChange(change, value);
if ((change == ItemSceneChange) && value.window) {
if (m_windowStateChangeConnection) {
disconnect(m_windowStateChangeConnection);
}
if (m_windowActiveChangeConnection) {
disconnect(m_windowActiveChangeConnection);
}
if (m_windowTitleChangeConnection) {
disconnect(m_windowTitleChangeConnection);
}
m_windowStateChangeConnection = connect(value.window, &QQuickWindow::visibilityChanged, this, &QuickStandardTitleBar::updateMaximizeButton);
m_windowActiveChangeConnection = connect(value.window, &QQuickWindow::activeChanged, this, &QuickStandardTitleBar::updateTitleBarColor);
m_windowTitleChangeConnection = connect(value.window, &QQuickWindow::windowTitleChanged, this, &QuickStandardTitleBar::updateTitleLabelText);
updateAll();
}
}
void QuickStandardTitleBar::updateAll()
{
updateMaximizeButton();
updateTitleLabelText();
updateTitleBarColor();
}

View File

@ -46,10 +46,7 @@ class FRAMELESSHELPER_QUICK_API QuickStandardTitleBar : public QQuickRectangle
QML_NAMED_ELEMENT(StandardTitleBar)
#endif
Q_DISABLE_COPY_MOVE(QuickStandardTitleBar)
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged FINAL)
Q_PROPERTY(bool maximized READ isMaximized WRITE setMaximized NOTIFY maximizedChanged FINAL)
Q_PROPERTY(Qt::Alignment titleLabelAlignment READ titleLabelAlignment WRITE setTitleLabelAlignment NOTIFY titleLabelAlignmentChanged FINAL)
Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged FINAL)
Q_PROPERTY(QuickStandardMinimizeButton* minimizeButton READ minimizeButton CONSTANT FINAL)
Q_PROPERTY(QuickStandardMaximizeButton* maximizeButton READ maximizeButton CONSTANT FINAL)
Q_PROPERTY(QuickStandardCloseButton* closeButton READ closeButton CONSTANT FINAL)
@ -58,42 +55,41 @@ public:
explicit QuickStandardTitleBar(QQuickItem *parent = nullptr);
~QuickStandardTitleBar() override;
Q_NODISCARD bool isActive() const;
void setActive(const bool value);
Q_NODISCARD bool isMaximized() const;
void setMaximized(const bool value);
Q_NODISCARD Qt::Alignment titleLabelAlignment() const;
void setTitleLabelAlignment(const Qt::Alignment value);
Q_NODISCARD QString title() const;
void setTitle(const QString &value);
Q_NODISCARD QuickStandardMinimizeButton *minimizeButton() const;
Q_NODISCARD QuickStandardMaximizeButton *maximizeButton() const;
Q_NODISCARD QuickStandardCloseButton *closeButton() const;
public Q_SLOTS:
protected:
void itemChange(const ItemChange change, const ItemChangeData &value) override;
private Q_SLOTS:
void updateMaximizeButton();
void updateTitleLabelText();
void updateTitleBarColor();
void clickMinimizeButton();
void clickMaximizeButton();
void clickCloseButton();
Q_SIGNALS:
void activeChanged();
void maximizedChanged();
void titleLabelAlignmentChanged();
void titleChanged();
private:
void initialize();
void updateAll();
private:
bool m_active = false;
Qt::Alignment m_labelAlignment = {};
QScopedPointer<QQuickLabel> m_label;
QScopedPointer<QQuickRow> m_row;
QScopedPointer<QuickStandardMinimizeButton> m_minBtn;
QScopedPointer<QuickStandardMaximizeButton> m_maxBtn;
QScopedPointer<QuickStandardCloseButton> m_closeBtn;
QMetaObject::Connection m_windowStateChangeConnection = {};
QMetaObject::Connection m_windowActiveChangeConnection = {};
QMetaObject::Connection m_windowTitleChangeConnection = {};
};
FRAMELESSHELPER_END_NAMESPACE

View File

@ -559,17 +559,22 @@ FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *object)
if (!object) {
return nullptr;
}
auto helper = object->findChild<FramelessWidgetsHelper *>();
if (!helper) {
helper = new FramelessWidgetsHelper;
if (object->isWidgetType()) {
const auto widget = qobject_cast<QWidget *>(object);
helper->setParent(widget->nativeParentWidget() ? widget->nativeParentWidget() : widget->window());
} else {
helper->setParent(object);
if (object->isWidgetType()) {
const auto widget = qobject_cast<QWidget *>(object);
QWidget * const parentWidget = (widget->nativeParentWidget() ? widget->nativeParentWidget() : widget->window());
Q_ASSERT(parentWidget);
auto instance = parentWidget->findChild<FramelessWidgetsHelper *>();
if (!instance) {
instance = new FramelessWidgetsHelper(parentWidget);
}
return instance;
} else {
auto instance = object->findChild<FramelessWidgetsHelper *>();
if (!instance) {
instance = new FramelessWidgetsHelper(object);
}
return instance;
}
return helper;
}
QWidget *FramelessWidgetsHelper::titleBarWidget() const