Qt开发路上的“绊脚石”与“垫脚石”

移除Qt控件虚线框

方式一:使用 StyleSheet

1
2
3
QWidget:focus {
outline: none; /* 去掉得到焦点时的虚线框 */
}

方式二:继承 QProxyStyle

继承 QProxyStyle,PrimitiveElement 为 QStyle::PE_FrameFocusRect 时不绘制虚线框,然后在 main() 函数里调用 QApplication::setStyle() 使用新的样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文件名: NoFocusRectStyle.h
#ifndef NOFOCUSRECTSTYLE_H
#define NOFOCUSRECTSTYLE_H
#include <QProxyStyle>
class NoFocusRectStyle : public QProxyStyle
{
public:
NoFocusRectStyle(QStyle *baseStyle) : QProxyStyle(baseStyle) {}
void drawPrimitive(PrimitiveElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget = 0) const
{
if (element == QStyle::PE_FrameFocusRect)
{
return;
}
QProxyStyle::drawPrimitive(element, option, painter, widget);
}
};
#endif // NOFOCUSRECTSTYLE_H
1
2
3
4
5
6
7
8
9
10
11
12
13
// 文件名: main.cpp
#include "Widget.h"
#include "NoFocusRectStyle.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
NoFocusRectStyle *style = new NoFocusRectStyle(app.style());
app.setStyle(style); // Ownership of the style object is transferred to QApplication
Widget w;
w.show();
return app.exec();
}

无法拖入文件到QListWidget

一般而言,只需要做如下操作,QWidget即可支持拖入文件:

1
2
3
4
5
6
7
8
setDragDropMode(QAbstractItemView::DropOnly); // 仅支持拖入文件


// 重写dragEnterEvent,当拖入文件进入时被调用,可以在该函数中取消拖入操作
void dragEnterEvent(QDragEnterEvent* e) override;

// 重写dropEvent,当拖入文件并释放鼠标时被调用
void dropEvent(QDropEvent* e) override;

但在执行上述操作后,我们拖入文件到QListWidget时,却只能收到dragEnterEvent事件,却无法收到dropEvent事件。

因为还需要重写dragMoveEvent方法,如:

1
2
3
void dragMoveEvent(QDragMoveEvent* e) {
e->acceptProposedAction();
}

使QListWidgetItem自适应尺寸

QListWidget添加item的方法如下:

1
2
3
4
5
6
QListWidget* list = new QListWidget();

// ...

QListWidgetItem* item = new QListWidgetItem();
list->addItem(item);

如需要自定义Item的布局,还可以通过自定义Widget来实现,如:

1
2
3
4
5
6
7
8
9
QListWidget* list = new QListWidget();

// ...

QListWidgetItem* item = new QListWidgetItem();
list->addItem(item);

CustomWidget * customWidget = new CustomWidget();
list->setItemWidget(item, customWidget); // 设置Item的自定义Widget

此时,我们还没有为item设置size hint,item会自动根据item的数据(如text)计算size hint。但如果是自定义的Widget,则无法自动计算size hint,需要手动设置size hint,如:

1
2
3
4
5
6
int vScrollbarWidth = 0;
if (verticalScrollBar())
vScrollbarWidth = verticalScrollBar()->width();

// 减去纵坐标的宽度
item->setSizeHint(QSize(list->width() - vScrollbarWidth, widget->height()));

QListWidget的滚动条默认显示策略是ScrollBarAsNeeded,当宽度不够显示Item时,滚动条就会显示出来,此时虽然QListWidget宽度改变了,但Item的宽度却不会改变,Item不会收到ResizeEvent事件。如需Item的宽度跟随QListWidget宽度而改变,则需要关闭滚动条显示,并在 QListWidget 的 ResizeEvent 事件中实时设置每个Item的size hint,代码如下:

1
2
// 关闭滚动条
list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FileList::resizeEvent(QResizeEvent* e) {
int vScrollbarWidth = 0;
if (verticalScrollBar())
vScrollbarWidth = verticalScrollBar()->width();

int w = width();
int cnt = count();
for (int i = 0; i < cnt; i++) {
QListWidgetItem* it = item(i);
if (it) {
CustomWidget* customWidget = dynamic_cast<CustomWidget*>(itemWidget(it));
if (customWidget) {
it->setSizeHint(QSize(w - vScrollbarWidth, customWidget->height()));
}
}
}
QListWidget::resizeEvent(e);
}

子线程中更新UI

方法1: 使用信号槽

这是一种非常常规的方式,通过自定义信号、槽,连接该信号和槽,在子线程中发送信号,在槽中更新 UI。

定义信号和槽:

1
2
3
4
signals:
void updateUi(int v);
private slots:
void onUpdateUi(int v);

在子线程中发送信号:

1
2
3
4
5
6
7
8
9
10
// 连接信号
connect(this, &UpdateUIInSubThread::updateUi, this, &UpdateUIInSubThread::onUpdateUi, Qt::AutoConnection);

std::thread t = std::thread([this]() {
for (int i = 0; i < 10000; i++) {
emit updateUi(i); // 发送信号
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
t.detach();

在槽函数中更新 UI:

1
2
3
4
void UpdateUIInSubThread::onUpdateUi(int v)
{
ui.label->setText(QString::number(v));
}

这种方式需要单独额外定义信号和槽,使用起来比较繁琐。

方法2: 使用invokeMethod

QMetaObject::invokeMethod 函数的原型如下:

1
template <typename Functor, typename FunctorReturnType> bool QMetaObject::invokeMethod(QObject *context, Functor function, Qt::ConnectionType type = Qt::AutoConnection, FunctorReturnType *ret = nullptr)

该函数可以在context的事件循环中执行function函数。

1
2
3
4
5
6
7
8
9
10
11
12
std::thread t = std::thread([this]() {
for (int i = 0; i < 10000; i++) {
if (QMetaObject::invokeMethod(this, [i, this]() {
ui.label->setText(QString::number(i));
})) {
qDebug() << "Update UI success";
}

std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
t.detach();

由于在子线程中更新 UI,因此信号和槽肯定使用的是 QueuedConnection 的连接方式,所以无法将FunctorReturnType返回给调用者,否则会出现如下错误:

1
QMetaObject::invokeMethod: Unable to invoke methods with return values in queued connections

当然上述示例中也可以不使用 lambda 表达式,直接调用槽函数:

1
2
3
4
5
6
7
8
std::thread t = std::thread([this]() {
for (int i = 0; i < 10000; i++) {
QMetaObject::invokeMethod(this, "onUpdateUi", Qt::AutoConnection, Q_ARG(int, i));

std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
t.detach();

加载字体文件

如何使用 Qt 加载外部字体文件,并遍历字体名称和样式名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bool LoadFont(const QString& fontPath) {
const int fontId = QFontDatabase::addApplicationFont(fontPath);
if (fontId == -1) {
return false;
}

// 遍历字体名和样式名
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const QStringList fontFamilyList = fontDb.applicationFontFamilies(fontId);

QString fontFamily;
foreach (fontFamily, fontFamilyList) {
qDebug() << "Family:" << fontFamily;

const QStringList fontStyleList = QFontDatabase::styles(fontFamily);
QString fontStyle;
foreach (fontStyle, fontStyleList) {
qDebug() << "\tStyle:" << fontStyle;
}
}
#else
QFontDatabase fontDb;
const QStringList fontFamilyList = fontDb.applicationFontFamilies(fontId);

QString fontFamily;
foreach (fontFamily, fontFamilyList) {
qDebug() << "Family:" << fontFamily;

const QStringList fontStyleList = fontDb.styles(fontFamily);
QString fontStyle;
foreach (fontStyle, fontStyleList) {
qDebug() << "\tStyle:" << fontStyle;
}
}
#endif

return true;
}

QWidget子类无法应用QSS

QWidget子类无法应用QSS样式的问题是一个老生常谈的问题,在使用Qt进行界面开发时,我们经常会继承自QWidget来实现自定义控件(当然这里说的是非顶级窗口),此时我们发现在该自定义控件上应用QSS样式会无效。

导致这个问题的主要原因是QWidget的paintEvent函数实现为空,未做任何绘制,也没有对样式表进行初始化和绘制,其代码如下:

1
2
void QWidget::paintEvent(QPaintEvent *){
}

解决这个问题也比较简单,在QWidget子类的paintEvent函数中初始化并绘制样式表:

1
2
3
4
5
6
void CustomWidget::paintEvent(QPaintEvent* e) {
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

当然我们也可以采用变通的方法,改为继承自QFrame,因为QFrame默认初始化和绘制了样式表,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void QFrame::paintEvent(QPaintEvent *)
{
QPainter paint(this);
drawFrame(&paint);
}

/*!
\internal

Used by QLabel and QLCDNumber
*/
void QFrame::drawFrame(QPainter *p)
{
QStyleOptionFrame opt;
initStyleOption(&opt);
style()->drawControl(QStyle::CE_ShapedFrame, &opt, p, this);
}

QSS 设置滚动条的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/*垂直滚动条*/
QScrollBar:vertical {
background: transparent; /*背景色*/
width: 10px; /*宽度*/
margin: 0px 0px 0px 0px; /**/
padding-top: 12px; /*距离上面12px*/
padding-bottom: 12px; /*距离底部12px*/
}

/*横向滚动条*/
QScrollBar:horizontal {
background: transparent; /*背景色*/
height: 10px; /*高度*/
margin: 0px 0px 0px 0px;
padding-left: 12px; /*距离左边12px*/
padding-right: 12px; /*距离右边12px*/
}

/*垂直/横向滚动条Hover效果*/
QScrollBar:vertical:hover,
QScrollBar:horizontal:hover {
background: rgba(0, 0, 0, 30); /*背景色*/
border-radius: 5px; /*圆角*/
}

/*垂直滚动条 - 滑块*/
QScrollBar::handle:vertical {
background: rgba(0, 0, 0, 50);
width: 10px;
border-radius: 5px;
border: none;
}

/*横向滚动条 - 滑块*/
QScrollBar::handle:horizontal {
background: rgba(0, 0, 0, 50);
height: 10px;
border-radius: 5px;
border: none;
}

/*垂直/横向滚动条滑块Hover效果*/
QScrollBar::handle:vertical:hover,
QScrollBar::handle:horizontal:hover {
background: rgba(0, 0, 0, 100);
}

/*垂直滚动条下部分块*/
QScrollBar::add-page:vertical {
width: 10px;
background: transparent;
}

/*垂直滚动条上面部分块*/
QScrollBar::sub-page:vertical {
width: 10px;
background: transparent;
}

/*横向滚动条右边部分块*/
QScrollBar::add-page:horizontal {
height: 10px;
background: transparent;
}

/*横向滚动条左边部分块*/
QScrollBar::sub-page:horizontal {
height: 10px;
background: transparent;
}

/* ----------- 对于扁平化设计风格,下面的样式可以不用设置 ----------- */

/*垂直滚动条顶部三角形位置*/
QScrollBar::sub-line:vertical {
height: 12px;
width: 10px;
background: transparent;
subcontrol-position: top;
}

/*垂直滚动条顶部三角形图标*/
QScrollBar::up-arrow:vertical {
image: url(:/client/images/scrollbar_arrowup_normal.png);
}

/*垂直滚动条顶部三角形图标 鼠标Hover效果*/
QScrollBar::up-arrow:vertical:hover {
image: url(:/client/images/scrollbar_arrowup_down.png);
}

/*垂直滚动条顶部三角形图标 鼠标按下效果*/
QScrollBar::up-arrow:vertical:pressed {
image: url(:/client/images/scrollbar_arrowup_highlight.png);
}

/*垂直滚动条底部三角形位置*/
QScrollBar::add-line:vertical {
height: 12px;
width: 10px;
background: transparent;
subcontrol-position: bottom;
}

/*垂直滚动条底部三角形图标*/
QScrollBar::down-arrow:vertical {
image: url(:/client/images/scrollbar_arrowdown_normal.png);
}
/*垂直滚动条底部三角形图标 鼠标Hover效果*/
QScrollBar::down-arrow:vertical:hover {
image: url(:/client/images/scrollbar_arrowdown_down.png);
}

/*垂直滚动条底部三角形图标 鼠标按下效果*/
QScrollBar::down-arrow:vertical:pressed {
image: url(:/client/images/scrollbar_arrowdown_highlight.png);
}

/*横向滚动条左侧三角形位置*/
QScrollBar::sub-line:horizontal {
height: 10px;
width: 12px;
background: transparent;
subcontrol-position: left;
}

/*横向滚动条左侧三角形图标*/
QScrollBar::left-arrow:horizontal {
image: url(:/client/images/scrollbar_arrowleft_normal.png);
}

/*横向滚动条左侧三角形图标 鼠标Hover效果*/
QScrollBar::left-arrow:horizontal:hover {
image: url(:/client/images/scrollbar_arrowleft_down.png);
}

/*横向滚动条左侧三角形图标 鼠标按下效果*/
QScrollBar::left-arrow:horizontal:pressed {
image: url(:/client/images/scrollbar_arrowleft_highlight.png);
}

/*横向滚动条右侧三角形部分*/
QScrollBar::add-line:horizontal {
height: 10px;
width: 12px;
background: transparent;
subcontrol-position: right;
}

/*横向滚动条右侧三角形图标*/
QScrollBar::right-arrow:horizontal {
image: url(:/client/images/scrollbar_arrowright_normal.png);
}

/*横向滚动条右侧三角形图标 鼠标Hover效果*/
QScrollBar::right-arrow:horizontal:hover {
image: url(:/client/images/scrollbar_arrowright_down.png);
}

/*横向滚动条右侧三角形图标 鼠标按下效果*/
QScrollBar::right-arrow:horizontal:pressed {
image: url(:/client/images/scrollbar_arrowright_highlight.png);
}

自定义和美化菜单

在Qt中可以通过QSS对菜单进行样式设置,而且对于非标准菜单项我也可以通过自定义Widget的方法来实现,本文讲述在Qt中QMenu的使用方法。

菜单基本使用

下面示例演示了右键菜单的创建方法,该菜单包含了图标、选中/未选中状态、二级菜单、互斥选择等常用菜单特性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
QIcon settingIcon(":/MenuBeauty/setting.png");

// 定义Actions
QAction* action1 = new QAction("动作1");
QAction* action2 = new QAction(settingIcon, "动作2 有图标");

QAction* action3 = new QAction("动作3 [未选中]");
action3->setCheckable(true);
action3->setChecked(false);
connect(action3, &QAction::triggered, this, [action3](bool checked) {
action3->setText(checked ? "动作3 [选中]" : "动作3 [未选中]");
});

QAction* action4 = new QAction("Action4 动作四测试");

QAction* action5 = new QAction(settingIcon, "动作5 禁用");
action5->setEnabled(false);

QAction* action6 = new QAction(settingIcon, "动作6 子菜单");
{
// 动作6的子动作
QAction* action6_1 = new QAction("动作6.1");
action6_1->setCheckable(true);

QAction* action6_2 = new QAction("动作6.1");
action6_2->setCheckable(true);

QAction* action6_3 = new QAction("动作6.1");
action6_3->setCheckable(true);

// 动作6的子菜单
QMenu* action6SubMenu = new QMenu(this);

action6SubMenu->addAction(action6_1);
action6SubMenu->addAction(action6_2);
action6SubMenu->addAction(action6_3);

// 互斥子菜单项,同时只能选择一个
QActionGroup* action6Group = new QActionGroup(this);
action6Group->setExclusive(true);
action6Group->addAction(action6_1);
action6Group->addAction(action6_2);
action6Group->addAction(action6_3);

action6->setMenu(action6SubMenu);
}

// 定义菜单
QMenu* menu = new QMenu(this);

// 添加Action
menu->addActions({action1, action2, action3});
menu->addSeparator();
menu->addActions({action4, action5, action6});

// 鼠标右键弹出菜单
this->setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QWidget::customContextMenuRequested, this, [this, menu](const QPoint& pos) {
menu->exec(this->mapToGlobal(pos));
});

上述菜单未经过任何美化,样式如下图所示:

使用QSS美化菜单

现在我们使用如下QSS对上面创建的菜单进行美化,可以美化的项包含背景、文本颜色、边距、图标等。

为了使大家不被颜色值所困扰,下面QSS中的颜色统一使用颜色名称表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/* 菜单的属性 */
QMenu {
border: 1px solod red;
/* 圆角生效的前提是菜单窗口背景透明 */
border-radius: 10px;
background-color: blue;
font-size: 14px;
font-family: "Microsoft YaHei";
/* 设置菜单项最小宽度,此处有bug:虽可自动增长但实际宽度比所需宽度小 */
min-width: 160px;
/* 菜单内容上下边距为10px,预留圆角区域 */
padding: 10px 0px 10px 0px;
}

/* 菜单项的属性 */
QMenu::item {
border: none;
background-color: transparent;
color: white;
/* 设置菜单项最小高度,可以让菜单项的高适应内容自动增长 */
min-height: 20px;
/* 设置菜单项最小宽度,此处有bug:虽可自动增长但实际宽度比所需宽度小 */
min-width: 160px;
/* 使菜单项之间及菜单项和图标之间保持一定间隔,设置上下左右边距为8px,此处不能用margin */
padding: 8px 8px;
}

/* 菜单项-鼠标移入时的属性 */
QMenu::item:selected {
background-color: green;
color: black;
}

/* 菜单项-禁用时的属性 */
QMenu::item:disabled {
background-color: gray;
color: white;
}

/* 分割线的属性 */
QMenu::separator {
height: 1px;
background-color: red;
/* 左右边距:6px */
margin: 0x 6px 0px 6px;
}

/* 图标的属性 */
QMenu::icon {
width: 12px;
height: 12px;
/* 左边距:12px */
margin: 0 0 0 12px;
}

/* 指示器的属性 */
/* 这里将互斥和非互斥Action的指示器样式设置为一样的,也可以设置为不一样 */
QMenu::indicator:non-exclusive:checked, QMenu::indicator:exclusive:checked {
width: 12px;
height: 12px;
/* 左边距:8px */
margin: 0 0 0 8px;
image: url(:/MenuBeauty/check.png);
}

QMenu::indicator:non-exclusive:unchecked, QMenu::indicator:exclusive:unchecked {
width: 12px;
height: 12px;
/* 左边距:8px */
margin: 0 0 0 8px;
image: url(:/MenuBeauty/unchecked.png);
}

为了使菜单的圆角生效,我们还需要移除菜单的边框、阴影,并将背景设置为透明,代码如下:

1
2
3
4
5
menu->setWindowFlags(action6SubMenu->windowFlags() |
Qt::FramelessWindowHint | // 无边框
Qt::NoDropShadowWindowHint // 移除系统阴影
);
menu->setAttribute(Qt::WA_TranslucentBackground); // 背景透明

美化后的菜单如下图所示:

QSS美化QMenu

QMenu支持QSS盒子模型,其通用属性如下:

1
2
3
4
5
6
7
8
border
border-radius
margin
padding
background
color
font
border-image

QSS还可以对QMenu的子控件进行设置,支持QMenu子控件如下:

1
2
3
4
5
6
7
item
indicator
separator
right-arrow
left-arrow
scroller
tearoff

QMenu不支持伪状态,只有其子控件支持伪状态,支持的伪状态如下:

1
2
3
4
default
selected
exclusive
non-exclusive

具体的子控件和伪状态的含义,可以参考 QSS基本使用方法 文章。

自定义菜单项

大多数情况下,上述常规的菜单项就可以满足要求,但在有些情况下,我们的菜单项可能由更加复杂的Widget组合而成,比如下面播放器的菜单:

我们可以使用QWidgetAction来实现上述菜单效果,QWidgetAction可以将一个QWidget放入菜单项的容器内,从而实现自定义菜单项。

下面代码定义了一个包含3个按钮的菜单项,点击按钮会弹出对话框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
QWidgetAction* action7 = new QWidgetAction(this);
{
QWidget* widget = new QWidget();
QHBoxLayout* hl = new QHBoxLayout(widget);

auto createPushButtonFn = [this](QString title) {
QPushButton* btn = new QPushButton(title);
connect(btn, &QPushButton::clicked, this, [this]() {
QMessageBox::information(this, "Clicked", ((QPushButton*)sender())->text());
});
return btn;
};

hl->addWidget(createPushButtonFn("Button1"));
hl->addWidget(createPushButtonFn("Button2"));
hl->addWidget(createPushButtonFn("Button3"));

action7->setDefaultWidget(widget);
}

menu->addAction(action7);

效果如图所示:

我们也可以使用QSS对菜单项内的控件进行样式设置,如:

1
2
3
4
5
6
QMenu QPushButton {
border: none;
border-radius: 10px;
background-color: black;
color: white;
}

📌限于篇幅,不在此提供完整的示例代码,如需获取完整的示例代码,可以联系我

弹窗外部点击时自动关闭

在 Qt 编程中如何实现当鼠标点击弹窗外部区域时,该弹窗可以自动关闭的效果。

ActivationChange 事件

弹窗通过监听 Activation 改变事件,来判断自身是否还是当前的活动窗口,如果不是则关闭自身。

在 QWidget 里面重写 event,捕获 QEvent::ActivationChange 事件:

1
2
3
4
5
6
7
8
9
bool UserCenterDlg::event(QEvent* e) {
if (e->type() == QEvent::ActivationChange) {
QWidget* curActiveWin = QApplication::activeWindow();
if (curActiveWin != this) {
this->close();
}
}
return QWidget::event(e);
}

Qt::WA_NoMouseReplay 介绍

摘自 Qt 帮助文档:

Qt::WA_NoMouseReplay: Used for pop-up widgets. Indicates that the most recent mouse press event should not be replayed when the pop-up widget closes. The flag is set by the widget’s author and cleared by the Qt kernel every time the widget receives a new mouse event.

Qt::WA_NoMouseReplay 属性可以用来避免如下情况的发生:
在鼠标点击 Popup Widget 外部区域时,该 Widget 接收到自身的 Activation 状态发生改变,关闭自身,但在该 Widget 在关闭后,Qt 依然会将鼠标点击事件继续向下传递,从而窗口外区域下面的控件也会被点击。

由于 Qt::WA_NoMouseReplay 属性只对具有 Popup 属性的 Widget 起作用,因此只能使用 QWidget,不能使用 QDialog,并设置 Qt::Popup 属性。

1
setWindowFlags(windowFlags() | Qt::Popup);

何时设置 Qt::WA_NoMouseReplay 属性了?

重写 mousePressEvent,在鼠标按下事件发生时,设置 Qt::WA_NoMouseReplay 属性。

1
2
3
4
void UserCenterDlg::mousePressEvent(QMouseEvent* e) {
setAttribute(Qt::WA_NoMouseReplay);
QWidget::mousePressEvent(e);
}

自定义 closed 信号

QWidget 没有关闭信号,我们可以自定义关闭信号,该信号在 closeEvent 中触发。

1
2
3
4
void UserCenterDlg::closeEvent(QCloseEvent* e) {
emit closed();
QWidget::closeEvent(e);
}

QPixmap使用要点

  • QPixmap::size() 值与程序的 devicePixelRatio 无关,输出的是图片原始尺寸。
  • AspectRatioMode 取值:
    • Qt::IgnoreAspectRatio 忽略图片原长宽比,将图片缩放到指定尺寸
    • Qt::KeepAspectRatio 在保持图片原长宽比和图片所有元素的情况下,尽量填充满目标矩形。
    • Qt::KeepAspectRatioByExpanding 在保持长宽比的情况下,拉伸图片保证填充满目标矩形,可能会舍弃部分图片元素。

  • scaledToWidth、scaledToHeight 函数可以实现在保持原图片长宽比的前提下,将图片缩放到指定的宽或者高。
  • drawPixmap 函数的参数从左到右依次是:目标区域 -> pix -> 源区域。目标区域会考虑当前 Widget 的 devicePixelRatio,而源区域则与 devicePixelRatio 无关。

开启程序 DPI 缩放

1
2
3
4
5
    QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif

QPixmap::devicePixelRatio

首先 QPixmap::devicePixelRatio 仅适用于开启了 DPI 缩放的程序。

高分辨率版本的图片有大于 1 的设备像素比(即QPixmap::devicePixelRatio),如果该值匹配底层 QPaintDevice 的值,它将直接绘制到设备上,而不应用额外的转换。

该值用于指定当前 QPixmap 图片是基于 1 倍图的几倍缩放图。

需要注意的是,在使用drawPixmap函数时,如果该目标区域参数未指定宽和高,QPixmap::devicePixelRatio 才生效。

如下面的 drawPixmap 原型:

1
2
3
4
5
6
7
void QPainter::drawPixmap(int x, int y, const QPixmap &pixmap, int sx, int sy, int sw, int sh)
void QPainter::drawPixmap(const QPointF &point, const QPixmap &pixmap, const QRectF &source)
void QPainter::drawPixmap(const QPoint &point, const QPixmap &pixmap, const QRect &source)
void QPainter::drawPixmap(const QPointF &point, const QPixmap &pixmap)
void QPainter::drawPixmap(const QPoint &point, const QPixmap &pixmap)
void QPainter::drawPixmap(int x, int y, const QPixmap &pixmap)

示例(程序已开启 DPI 缩放):

1
2
3
4
5
6
7
8
9
10
11
12
void QtWidgetsApplication1::paintEvent(QPaintEvent* e)
{
qDebug() << this->devicePixelRatioF(); // 当前程序的DPI缩放为1.5
QPainter painter(this);
QPixmap pix("D:\\gril.png"); // gril.png图片实际尺寸为400*400

// 告知程序该图片是基于1倍图进行4倍放大后的,即1倍图为宽高为400/4
pix.setDevicePixelRatio(4);

// 实际绘制的图片宽度为150*150,左上角坐标为150*150
painter.drawPixmap(QPoint(100,100), pix);
}

Qt的布局技巧

手动拖放

通过在 Qt Designer 中手动拖放控件,可以快速实现界面布局,如下图所示:

这种方式对初学者非常友好,可以快速上手开发一个界面,增加成就感。但我们需要知道,拖放布局不是 Qt 布局的全部,这种方式仅适合简单的界面布局。

在实际的项目开发中,界面上的控件会比上面示例中的多得多,布局也会更加复杂,而且界面通常还需要反复地调整。在这种情况下,如果继续通过手动拖放的方式来进行布局,在每次界面调整时,我们都需要打破(分拆)原有的布局,并在修改完成之后重新进行布局,可能还需要重新设置布局的 Spacing 和 ContentsMargins,每一次修改都非常费劲。

题外话:手动拖放控件对手的稳定性要求较高,可能手稍一抖,整个布局就错乱了,又得还原了重新来。

代码布局

我通常使用代码的方式来进行 Qt 界面布局,例如下面的代码实现了与上面示例一样的布局效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
QLabel* lblUserName = new QLabel("用户名: ");
QLabel* lblPassword = new QLabel("密码: ");
QLineEdit* editUserName = new QLineEdit();
QLineEdit* editPassword = new QLineEdit();
QPushButton* btnLogin = new QPushButton("登录");
QPushButton* btnRegister = new QPushButton("注册");

QHBoxLayout* hUserName = new QHBoxLayout();
hUserName->addWidget(lblUserName);
hUserName->addWidget(editUserName);

QHBoxLayout* hPassword = new QHBoxLayout();
hPassword->addWidget(lblPassword);
hPassword->addWidget(editPassword);

QHBoxLayout* hButton = new QHBoxLayout();
hButton->addStretch();
hButton->addWidget(btnRegister);
hButton->addWidget(btnLogin);
hButton->addStretch();

QVBoxLayout* vMain = new QVBoxLayout();
vMain->addLayout(hUserName);
vMain->addLayout(hPassword);
vMain->addStretch();
vMain->addLayout(hButton);

this->setLayout(vMain);

大家看到上面代码后,第一感觉肯定是“我的个老天,这么复杂”,这个可以理解。但这种方式的好处也非常明显,比如灵活性更大,后期调整、复用、维护也更加方便,而且对于自定义控件比较多的界面,这种方式可以直接创建对应类型的实例,不需要在设计器上进行类型提升。

布局描述语言

我在这里不是为了创造一种新的语言,而是受到了 Flutter 界面布局方式的启发,对 Qt 的布局进行了封装,简化了上述布局代码。

下面是上述示例简化后的布局代码:

1
2
3
4
5
6
7
auto layout = VBox(
HBox(lblUserName, editUserName),
HBox(lblPassword, editPassword),
Stretch(),
HBox(Stretch(), btnRegister, btnLogin, Stretch()));

setLayout(layout);

从代码量来看,明显少了很多,而且我们还可以通过代码的层次结构快速看出界面的布局结构。

在此省略了 VBox、HBox 等类的实现代码,如果您对上述 Qt 布局方式的具体实现感兴趣,可以联系我。

图片按九宫格模式缩放

我们在缩放图片时,都期望能保持图片的原长宽比,因为这样可以防止图片变形,但往往事与愿违,有些时候我们没办法保持原图的长宽比不变,比如需要在保持图片高度不变的情况下,仅横向拉伸图片,此时就会导致图片变形。

为了解决这种问题,我们可以考虑使用九宫格模式进行图片的缩放。

九宫格模式就是将图片切分为九块(不强制切分的每一块必须等分),如下图所示,在图片缩放时,我们通常保持1、3、7、9四个顶点位置的图片不变,对2、4、5、6、8五个区域进行缩放。

使用QSS实现

在Qt中实现图片按九宫格缩放最简单的方法是使用QSS的border-image属性来实现,通过上下左右四个边框的宽度或高度来设置1、3、7、9四个顶点的大小。

1
2
3
4
5
border-image: url(:/Sample/image/NinePatchTest.png);
border-left: 210px;
border-top: 180px;
border-right: 300px;
border-bottom: 227px;

为了便于测试,我制作了一个九宫格测试图片(整个图片的宽高为1280*720):

通过这种方法实现的九宫格缩放有一个弊端:

该方案会始终保持1、3、7、9四个区域图片的宽高不变,如1号区域是210*180,当我们对图片进行放大时,这个行为是没有问题的,但当我们缩小图片时,缩小后的图片宽高不足以容纳4个顶角图片时,缩小操作就会出现非预期的行为,可能如下图所示:

这种弊端还会体现在支持高DPI缩放的Qt程序中,例如在一个支持DPI缩放的程序中,为宽高为640*360的QWidget设置背景图片,我们知道不同的 devicePixelRatio 会导致实际渲染的图片宽高不同,如devicePixelRatio为1时,图片渲染区域为640*360, 而devicePixelRatio为2时,实际渲染区域为1280*720,为了保证程序在不同DPI的环境下都有比较好的界面呈现,我们通常简单得指定一个倍图(如2倍图)让程序自动进行图片缩放(当然也可以使用SVG或针对不同DPI使用不同的图片)。

而使用 border-left 等属性指定的宽度是固定的,顶点图片无法参与到自动缩放中来,从而会导致这一 DPI 适配机制失效。下一节介绍的方法可以解决这一弊端。

使用代码实现九宫格缩放

本节介绍如何使用代码来实现九宫格模式缩放图片,实现原理大致如下:

提取九个区域的图片,保持1、3、7、9四个图片不变,对2、4、5、6、8五个区域图片进行缩放,分别得到新的图片,最后再将九个区域的图片合并成一张图。

为了解决上述QSS方案的弊端,我们可以采取先将图片在保持长宽比的情况缩放到相应尺寸:

1
QPixmap keepRatioScaledPix = src.scaled(destSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);

由于keepRatioScaledPix实际尺寸肯定与我们想要得到的destSize不匹配,所以我们再把keepRatioScaledPix图片按九宫格模式缩放。

具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
QPixmap ScaleByNinePatch(const QPixmap& src, // 原图
QSize destSize, // 目标尺寸
bool keepVertexImageSize, // 是否保持1、3、7、9顶点图片大小不变,设置为true则与QSS方式效果类似(仅类似,因为该函数是直接返回原图)
int pos1Width,
int pos1Height,
int pos3Width,
int pos7Height,
Qt::TransformationMode mode = Qt::SmoothTransformation) {
// 参数校准
pos1Width = qMax(pos1Width, 0);
pos1Height = qMax(pos1Height, 0);
pos3Width = qMax(pos3Width, 0);
pos7Height = qMax(pos7Height, 0);

int handlePixW = 0, handlePixH = 0;
const QPixmap* handlePix = nullptr;
QPixmap keepRatioScaledPix;

if (keepVertexImageSize) {
// 保持顶点图片大小不变的情况,如果顶点图片宽或高已经大于了目标宽高,则直接返回原图
if (pos1Width + pos3Width > destSize.width() || pos1Height + pos7Height > destSize.height())
return src;

handlePixW = src.width();
handlePixH = src.height();
handlePix = &src;
}
else {
const qreal srcW = src.width();
const qreal srcH = src.height();

// 先等比缩放到适合的尺寸
keepRatioScaledPix = src.scaled(destSize, Qt::KeepAspectRatio, mode);
handlePixW = keepRatioScaledPix.width();
handlePixH = keepRatioScaledPix.height();
qreal krsPixWidth = handlePixW;
qreal krsPixHeight = handlePixH;

// 按照之前的比例获取新顶点图片的大小
pos1Width = krsPixWidth * (qreal)pos1Width / srcW;
pos1Height = krsPixHeight * (qreal)pos1Height / srcH;
pos3Width = krsPixWidth * (qreal)pos3Width / srcW;
pos7Height = krsPixHeight * (qreal)pos7Height / srcH;

handlePix = &keepRatioScaledPix;
}

// 提前各个区域图片
QPixmap pix1 = handlePix->copy(0, 0, pos1Width, pos1Height);
QPixmap pix2 = handlePix->copy(pos1Width, 0, qMax(handlePixW - pos1Width - pos3Width, 0), pos1Height);
QPixmap pix3 = handlePix->copy(qMax(handlePixW - pos3Width, 0), 0, pos3Width, pos1Height);
QPixmap pix4 = handlePix->copy(0, pos1Height, pos1Width, qMax(handlePixH - pos1Height - pos7Height, 0));
QPixmap pix5 = handlePix->copy(pos1Width, pos1Height, qMax(handlePixW - pos1Width - pos3Width, 0), qMax(handlePixH - pos1Height - pos7Height, 0));
QPixmap pix6 = handlePix->copy(qMax(handlePixW - pos3Width, 0), pos1Height, pos3Width, qMax(handlePixH - pos1Height - pos7Height, 0));
QPixmap pix7 = handlePix->copy(0, qMax(handlePixH - pos7Height, 0), pos1Width, pos7Height);
QPixmap pix8 = handlePix->copy(pos1Width, qMax(handlePixH - pos7Height, 0), qMax(handlePixW - pos1Width - pos3Width, 0), pos7Height);
QPixmap pix9 = handlePix->copy(qMax(handlePixW - pos3Width, 0), qMax(handlePixH - pos7Height, 0), pos3Width, pos7Height);

// 对2、4、5、6、8区域图片进行缩放
pix2 = pix2.scaled(qMax(destSize.width() - pos1Width - pos3Width, 0), pos1Height, Qt::IgnoreAspectRatio, mode);
pix4 = pix4.scaled(pos1Width, qMax(destSize.height() - pos1Height - pos7Height, 0), Qt::IgnoreAspectRatio, mode);
pix5 = pix5.scaled(qMax(destSize.width() - pos1Width - pos3Width, 0), qMax(destSize.height() - pos1Height - pos7Height, 0), Qt::IgnoreAspectRatio, mode);
pix6 = pix6.scaled(pos3Width, qMax(destSize.height() - pos1Height - pos7Height, 0), Qt::IgnoreAspectRatio, mode);
pix8 = pix8.scaled(qMax(destSize.width() - pos1Width - pos3Width, 0), pos7Height, Qt::IgnoreAspectRatio, mode);

// 拼接新的图片
QPixmap dest(destSize);
dest.fill(Qt::transparent); // 使用透明色填充图片

QPainter painter(&dest);

painter.drawPixmap(0, 0, pix1);
painter.drawPixmap(pos1Width, 0, pix2);
painter.drawPixmap(qMax(destSize.width() - pos3Width, 0), 0, pix3);
painter.drawPixmap(0, pos1Height, pix4);
painter.drawPixmap(pos1Width, pos1Height, pix5);
painter.drawPixmap(qMax(destSize.width() - pos3Width, 0), pos1Height, pix6);
painter.drawPixmap(0, qMax(destSize.height() - pos7Height, 0), pix7);
painter.drawPixmap(pos1Width, qMax(destSize.height() - pos7Height, 0), pix8);
painter.drawPixmap(qMax(destSize.width() - pos3Width, 0), qMax(destSize.height() - pos7Height, 0), pix9);
painter.end();

return dest;
}