移除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 #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
1 2 3 4 5 6 7 8 9 10 11 12 13 #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); Widget w; w.show (); return app.exec (); }
一般而言,只需要做如下操作,QWidget即可支持拖入文件:
1 2 3 4 5 6 7 8 setDragDropMode (QAbstractItemView::DropOnly); void dragEnterEvent (QDragEnterEvent* e) override ;void dropEvent (QDropEvent* e) override ;
但在执行上述操作后,我们拖入文件到QListWidget时,却只能收到dragEnterEvent事件,却无法收到dropEvent事件。
因为还需要重写dragMoveEvent方法,如:
1 2 3 void dragMoveEvent (QDragMoveEvent* e) { e->acceptProposedAction (); }
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设置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样式的问题是一个老生常谈的问题,在使用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); } 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 ; padding-bottom : 12px ; } QScrollBar:horizontal { background : transparent; height : 10px ; margin : 0px 0px 0px 0px ; padding-left : 12px ; padding-right : 12px ; } 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; } 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 ); } 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 ); } 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 ); } 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 ); } 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" ) ;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 子菜单" ); { 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 ); 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 ); 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" ; min-width : 160px ; padding : 10px 0px 10px 0px ; } QMenu::item { border : none; background-color : transparent; color : white; min-height : 20px ; min-width : 160px ; 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; margin : 0 x 6px 0px 6px ; } QMenu::icon { width : 12px ; height : 12px ; margin : 0 0 0 12px ; } QMenu::indicator:non-exclusive:checked, QMenu::indicator:exclusive:checked { width : 12px ; height : 12px ; margin : 0 0 0 8px ; image : url (:/MenuBeauty/check.png ); } QMenu::indicator:non-exclusive:unchecked, QMenu::indicator:exclusive:unchecked { width : 12px ; height : 12px ; 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);
美化后的菜单如下图所示:
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 (); QPainter painter (this ) ; QPixmap pix ("D:\\gril.png" ) ; pix.setDevicePixelRatio (4 ); 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, 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); 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; }