1+1=10

记记笔记,放松一下...

如何优雅地实现Qt无边框程序

现在的软件界面多姿多彩,越来越少软件直接使用系统默认的窗口边框。这样一来,Qt Widgets默认的风格反而显得有些土里土气。

可是,一个C++工程师,美工不行,搞qss都有些费劲了。如何才能优雅地(无感地)实现无边框程序呢?

Frameless Application

目标

  • 工程师写代码时无需考虑边框问题,一切都使用默认风格
  • 程序完成后,将 QApplicaiton 直接换成 FramelessApplication ,完工!
  • 适用 C++ Qt 和 PySide/PyQt

愿望很美好,可是网上铺天盖地的BorderlessWidgetFramelessWindow,...,都是对Widget这些控件直接上手。你怎么整了个 Application ?能好使吗??

FramelessApplication 从哪儿来??

没有现成的,手撸就好了,做成一个基础库:

  • FramelessWindow:毫无疑问,首先要实现这个东西。从QWidget派生成,实现无边框风格。这一部分有很多杂乱的细节,网上的各种实现各有特点,此处不赘述。
  • FramelessDockWidgetTitleBar:QMainWindow的停靠窗口,一旦悬浮,也有自己的边框风格。
  • FramelessApplication:为程序中所有的窗口(QMainWindow,QDialog,等)自动应用上面两个类,隐藏实现细节。

FramelassApplication

接口很简单,如果不是为了处理个别情况,它不需要添加任何额外的API:

classDiagram class QApplication class BorderlessApplication { + bool isAutoWrapWidgetsEnabled() + void setAutoWrapWidgetsEnabled(bool enabled) + void addNonAutoWrapWidget(QWidget *window) + void removeNonAutoWrapWidget(QWidget *window) # bool notify(QObject *receiver, QEvent *e) } QApplication <|-- BorderlessApplication

一般来说,需要为以下所有的窗口分别自动包裹一个无边框的widget:

  • QMainWindow
  • QDialog
  • QMessageBox
  • 其他非 Qt::Popup 的QWidget窗口等

对QDockWidget,自动应用自定义的titlebar。

另外,特别注意,Qt有时候会使用原生窗口(样式表对他们不生效),需要考虑好是否想包裹他们,尤其是:

  • QFileDialog
  • QFontDialog
  • QColorDialog

无论如何,遇到如下标识,我们自动包裹它们是安全的:

1
2
3
4
5
QFileDialog::DontUseNativeDialog
QFontDialog::DontUseNativeDialog
QColorDialog::DontUseNativeDialog

Qt::AA_DontUseNativeDialogs

准备工作做好后,注意,从类图可以看到,我们重写了notify函数,去截获我们要处理对象的如下两个事件就好了,要点:

  • QEvent::Show:如果需要,用无边框窗口包裹
  • QEvent::Close:如果存在自动包裹的无边框窗口,将其移除【确保无内存泄漏】

另外,样式表也可以由FramelessApplication统一处理。

FramelessWindow

网上实现方式多样,反正接口也很简单:

classDiagram class QWidget class FramelessWindow { + void setCentralWidget(QWidget *widget) + QWidget *centralWidget() const + QWidget *takeCentralWidget() + int exec() + void open() } QWidget <|-- FramelessWindow

我们知道,显示一个窗口,有三种方式:

  • show():非模态,不阻塞
  • open():模态,不阻塞
  • exec():模态,阻塞(因为事件循环嵌套的问题,大程序中尽量避免使用)

为了偷懒,我不想为QDialog、QMainWindow 等分别定制无边框窗口,所以所有东西都塞到一个类里面了(注意处理Qt::WindowFlags)。

特别注意,一个窗口一旦被Frameless窗口包裹后,Qt将不认为原窗口是一个窗口(废话),所以它关闭时收不到CloseEvent,需要特别处理。

python

Python下实现和这个一样,只不过,我不想用python再写一遍,但需要为我们前面实现的类库,使用SIP或Shibokenw创建python绑定。

这是另一个有趣故事。

使用方式

所有的代码封装到一个库中,写应用程序时,无需关注细节,直接使用就好:

让 FramelessApplication 自动包裹

Frameless Application

1
2
3
4
5
6
7
8
9
int main(int argc, char *argv[])
{
    FramelessApplication a(argc, argv);

    MainWindow *mainWindow = new MainWindow;
    mainWindow.show();

    return a.exec();
}

手动包裹

对个别窗口或所有窗口,手动创建FramelessWindow并显示。

没问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int main(int argc, char *argv[])
{
    FramelessApplication a(argc, argv);

    MainWindow *mainWindow = new MainWindow;

    FramelessWindow framelessWidget;
    framelessWidget.setCentralWidget(mainWindow);
    framelessWidget.show();

    return a.exec();
}

注意事项

尽管主打一个用户无感,但是有时可能是会有点影响,具体和FramelessWindow的实现也有关系。

场景1

1
2
3
4
5
void MainWindow::on_button_clicked()
{
    QDialog dlg;
    dlg.exec();
}

我们需要清楚,在被包裹以后,这个dlg就不是一个真正的Dialog了。尽管一般都是无感的。

但如果这个QDialog内部还做了很多奇奇怪怪更精细的事情,特别时需要多次显示、隐藏、关闭,可能如下写法更安全一些:

1
2
3
4
5
void MainWindow::on_button_clicked()
{
    //dlg->exec();
    dlg->window()->exec();
}

不管它是否被包裹,都和肉眼所见的逻辑一样。

Qt Qt