淺析 Qt 佈局系統

韓元旭發表於2017-12-13

Qt 佈局系統介紹

佈局系統

作為一名 iOS 開發人員, 見證著 iOS 佈局系統的不斷完善, 從絕對佈局, Autoresizing 到 Autolayout. 使得開發人員的工作效率越來越高, 專案介面的可讀性和易維護性越來越強. 如今 IDE 中的視覺化介面工具已經非常強大, 許多網友"戲稱" iOS 開發者為"UI 拖拽師", 可見, iOS 開發中介面佈局系統的高效. 所以, 優秀的佈局系統的使命在於讓開發者花更少的時間來完成更易維護的介面.

同樣的, 在 Qt 中, 系統提供了強大的排版機制來為視窗中的檢視進行佈局排版, 經過了對 Qt 佈局一個初步的探索, 不得不對 Qt 佈局系統的簡潔高效而又功能強大表示讚歎.

佈局系統的功能

在 Qt 中, 佈局系統可以完成

  • 定位子控制元件
  • 得知窗體預設大小
  • 得知窗體最小大小
  • 窗體大小變化時進行佈局排版
  • 內容改變(字型大小文字等, 隱藏或顯示, 移除)時進行佈局排版

佈局系統的結構

Qt 提供了 QLayout 類及其子類來為介面進行排版佈局. 結構如下圖:

佈局系統結構圖

QLayout 是佈局系統中的抽象基類, 繼承自 QObject 和 QLayoutItem, 其中四個子類分別為

  • QBoxLayout(箱式佈局)
  • QFormLayout(表單佈局)
  • QGridLayout(網格佈局)
  • QStackedLayout(棧佈局)

在真實使用場景中, 往往需要通過多種佈局的相結合來完成介面的設計, 接下來將分別介紹四中佈局.

QBoxLayout 箱式佈局

箱式佈局提供了兩個子類分別處理水平(QHBoxLayout)和垂直(QVBoxLayout)兩個方向的排版, 可以使檢視排成一行或者一列來顯示. 簡單說, 就是可以讓控制元件進行排排站, 比如在我們的 AlphaBox 中, 頂部的頭像, 姓名, 和重新整理按鈕排成了一排, 這就是水平箱式佈局:

什麼叫排排站

你以為我要講一下這個東西如何實現? NO, 我偏偏要以垂直箱式佈局為例, 用一個最簡單的例子來介紹箱式佈局的使用, 首先建立一個基於 QWidget 的介面, 新增我們需要使用的標頭檔案:

#include <QVBoxLayout>
#include <QPushButton>
複製程式碼

並在建構函式中新增如下程式碼

    //  新增兩個按鈕
    QPushButton *okBtn  = new QPushButton;
    okBtn ->setText(tr("我在上面, 我最牛"));
    QPushButton *celBtn = new QPushButton;
    celBtn->setText(tr("我在下面, 我不服"));

    //  建立一個垂直箱式佈局, 將兩個按鈕扔進去
    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(okBtn);
    layout->addWidget(celBtn);

    //  設定介面的佈局為垂直箱式佈局
    setLayout(layout);
複製程式碼

執行看一下效果, 什麼? 這就可以執行了? 座標呢? 尺寸呢? 是的, 沒看錯...點選執行:

最簡單的箱式佈局

兩個按鈕已經一上一下, 乖乖的在垂直方向自己站好了位置, 就是這麼強大, 就是這麼省心.

QFormLayout 表單佈局

強大的 AlphaBox 是很外向的, 可以很輕鬆的將你的資料分享給其他使用者, 當我們分享的時候, 會有這樣一個介面:

在 AlphaBox 中共享資料

看到這個介面, 聰明的你可能會說, 這很簡單啊, 好幾個水平箱式佈局就可以實現, 可是, 更聰明的 Qt 提供了更高效的方式幫助你完成這樣一個介面, 那就是 QFormLayout.

在我所學習 Qt 所使用的書籍中, 將 QFormLayout 翻譯為窗體佈局, 我個人認為, 將其翻譯為表單佈局更為貼切, 因為 QFormLayout 的強大之處正是可以使用最快的速度完成一個使用者輸入的表單介面的搭建.

那麼, 讓我們揭開 AlphaBox 的神祕面紗, 看看這樣一個介面是怎麼實現的.

首先, 拖拽一個 Form Layout 到 Widget 中.

新增表單佈局

雙擊之後即可為表單增加一行.

為表單增加一行

相信大家看到這張圖時, 就已經能理解到表單佈局是如何使用的, 提供了標籤作為使用者輸入內容的指引, 提供欄位型別作為使用者輸入的控制元件, 作為 iOS 開發者, 深知這樣一個介面的搭建所需要的繁雜的工作量. 當我第一次開啟這個介面時, 被這樣建立介面的方式所驚呆了.

  1. 按照圖中, 建立表單的第一行, 共享給哪個使用者的輸入框, 可以為輸入框填寫佔位文字.
  2. 雙擊 Form Layout 建立欄位型別為 QComboBox (多選框)的一行. 填寫允許的許可權內容.
  3. 設定整個 Widget 佈局為垂直箱式佈局
  4. 在 Form Layout 下拖拽過去一個 Horizontal Layout(水平箱式佈局)
  5. 在箱式佈局中新增 Horizontal Spacer (水平佔位) 後拖拽兩個 Push Button 完成介面佈局

共享介面的佈局

快不快? 快不快! 快不快!!!

同樣的, 如果是使用純程式碼表單佈局的話可以使用addRow()的方法來新增一行.

QGridLayout 網格佈局

強大的 AlphaBox 是這樣的

事實上, 強大的 AlphaBox 是這樣的, 我們可以共享給多個使用者, 而且, 下方會有一個列表, 展示共享的使用者以及許可權列表. 這時, 表單佈局就沒辦法滿足我們, 只好另求新歡 QGridLayout - 網格佈局.

網格佈局顧名思義, 可以將介面分割成行列來進行佈局管理, 在每個單元格中來擺放控制元件. 所以 AlphaBox 分享的介面使用了一個 兩行三列 的網格佈局來實現的.

QGridLayout - 網格佈局

當然, 更更復雜的介面, 用 Qt 佈局的效率也是非常高的, 我做了一個外鏈分享的佈局 Demo, 可以將內部資料生成一個下載連結共享給任何人去下載.

外鏈分享介面

這個介面中, 我在Tab之內使用了網格佈局, 佈局如圖:

外鏈分享介面佈局

從圖中可以看出, 網格佈局像是在操作一個 Excel 一樣簡單, 佈局單元格, 合併單元格, 等等.

在這個介面中, 更靈活的使用了 QLayout 的屬性來完成了介面佈局排版.

同樣的, 在程式碼中, 可以使用如下等的 Api 來為網格檢視新增一個從幾行幾列開始佔據幾行幾列的控制元件:

void addWidget(QWidget *, int row, int column, int rowSpan, int columnSpan)
複製程式碼

QStackedLayout 棧佈局

如在 AlphaBox 中, 我們可以通過雲端檔案瀏覽器直接檢視和操作雲端檔案, 在載入的過程中, 會有一個轉菊花的介面.

在轉菊花的 AlphaBox

載入失敗時的錯誤提示:

菊花轉失敗了的 AlphaBox

以及載入成功時:

通常情況下我們能看見的 AlphaBox

通常應用的介面會根據不同的狀態有不同的內容, 這時就可以使用 QStackedLayout 棧佈局, 棧佈局提供了一個頁面的棧, 每個頁面有完全獨立的介面佈局. 可以非常清晰的對不同狀態下的介面進行佈局管理.

在 Qt 的視覺化佈局工具中, 通過 Stacked Widget 來完成介面的棧佈局

Stacked Widget

通過右鍵來進行頁面的插入移除和排序等操作.

佈局相關屬性

控制元件大小

對於控制元件大小, 最重要的兩個屬性是 sizeHintminimumSizeHint , 這是 QWidget 的屬性, 是隻讀屬性. 其中, sizeHint 屬性為控制元件的建議大小, 對於不同的控制元件, 有不同的建議大小, 同理 minimumSizeHint 為建議的最小大小. 知道了這兩個屬性才可以理解佈局中控制元件的大小是如何控制的. 如果手動設定了最小尺寸的話(minimumSize), minimumSizeHint 是會被忽略的.

大小策略

大小策略屬性 sizePolicy 也是 QWidget 類的屬性, 這個屬性在水平和垂直兩個方向分別起作用, 控制著控制元件大小變化的策略.

在視覺化工具中可以直觀的看到幾種大小策略, 以垂直為例如圖:

大小策略

  • QSizePolicy::Fixed 只能使用 sizeHint 的大小, 任何操作都不會改變控制元件大小
  • QSizePolicy::Minimum sizeHint 為最小大小, 控制元件可以被拉伸
  • QSizePolicy::Maximum sizeHint 為最大大小, 控制元件可以被壓縮
  • QSizePolicy::Preferred sizeHint 為建議大小, 控制元件既可以被壓縮也可以被拉伸
  • QSizePolicy::MinimumExpanding sizeHint 為最小大小, 不能被壓縮, 被拉伸的優先順序更高
  • QSizePolicy::Expanding sizeHint 為建議大小, 可以被壓縮, 被拉伸的優先順序更高
  • QSizePolicy::Ignored sizeHint 的值將會被忽略

在網上或者書中, 關於這些策略的說明會有很多, 可是如果不是真的自己嘗試一下, 很難很好的理解在複雜佈局的情況下, 大小策略是如何控制佈局的, 尤其是 MinimumExpanding, Expanding, Ignored 這三種.

對於優先順序的概念大家肯定不會陌生, 這裡我認為優先順序來解釋這些策略是更清晰易懂的.

關於拉伸 Expanding , MinimumExpanding 優先順序相同, 同時要比 PreferredIgnored 拉伸優先順序更高, PreferredIgnored 相同.

換句話說, 如果兩個控制元件在一個水平箱式佈局中管理, 其中一個水平大小策略為 Preferred 另一個為 Expanding 或者 MinimumExpanding 如果水平拉伸窗體, 則 Preferred的控制元件大小不會改變, Expanding 或者是 MinimumExpanding 會被拉伸.

同理, 如果兩個控制元件水平大小策略一個為Expanding, 一個是MinimumExpanding, 這時拉伸窗體, 則兩個控制元件均會拉伸.

多說一句, 如果兩個控制元件都為 Fixed 無法拉伸時, 控制元件間的間隙會被拉伸.

關於壓縮 如果達到了minimumSizeHint是不會被繼續壓縮了, 但是Ignored是會忽略 sizeHintminimumSizeHint 的屬性的, 所以是會繼續被壓縮的.

伸縮性

在 QLayout 中提供了一個和控制元件大小策略相關的屬性, layoutStretch 佈局伸縮性, 這個值是一個比例, 在視覺化工具中可以更直觀的看到這個值的設定, 如果在佈局中有三個控制元件, 則是三個控制元件的佔比, 用逗號分隔, 如: 1, 1, 1 .

伸縮性就好理解一些了, 但是要注意的是, 只有會被壓縮或者拉伸的控制元件才會受該屬性值影響(如 Fixed 是不會受該屬性影響)

還有一點非常重要的是設定了伸縮性的比值(如果都為0, 則表示不設定) 剛剛提到的大小策略的優先順序將會被忽略, 還是剛剛的例子, 如果兩個控制元件在一個水平箱式佈局中管理, 其中一個水平大小策略為 Preferred 另一個為 Expanding, 設定水平箱式佈局的 layoutStretch2, 1 則拉伸時, 並不會像剛剛所說, 只有 Expanding 的控制元件會被拉伸, 而是都會被拉伸, 按照一個 2 : 1 的拉伸比例拉伸.

窗體大小約束策略

最後想介紹一下 QLayout 的 layoutSizeConstraint 屬性, 用來約束窗體大小, 隻影響窗體, 所以該屬性只對最頂級的 QLayout 起作用.

layoutSizeConstraint

關於這幾個屬性同樣的, 簡單的介紹網上和書上會有很多, 如果不嘗試一下, 淺顯的字面意思無法理解這幾個屬性的作用. 根據我的嘗試, 總結如下:

  • QLayout::SetDefaultConstraint 窗體最小值被設定為 minimumSize 值無法再縮小, 如果 QLayout 內控制元件有更大的minimumSize, 則會取更大的minimumSize.
  • QLayout::SetNoConstraint 窗體沒有約束策略
  • QLayout::SetFixedSize,窗體大小被設定為 sizeHint 的大小,無法改變
  • QLayout::SetMinimumSize 窗體最小為 minimumSize 無法再縮小, 如果 QLayout 內控制元件有更小的minimumSize, 則會取更小的minimumSize., 總結就是的話, 和 Default 不同的地方就是儘可能的小.
  • QLayout::SetMaxmumSize 同理, 窗體最大值為 maxmumSize , 無法再放大
  • QLayout::SetMinAndMaxSize 窗體最小為 minimumSize 無法再縮小, 窗體最大值為 maxmumSize , 無法再放大

其他屬性

關於表單佈局和網格佈局還有其他的屬性約束單元格的一些策略, 如 layoutFieldGrowthPolicy 控制元件的變化方式策略等等有興趣可以檢視官方文件, 更多的屬性間隙, 間隔, 對其方式等等都比較好理解了, 在此也不贅述了.

官方文件

Qt 官方文件點這裡

參考

<< Qt Creator 快速入門>> 第三版, 霍亞飛著

相關文章