布局管理

總結

布局的計算過程

  • 如果設置了最小尺寸(或者最小尺寸提示)、最大尺寸,則組件獲取的空間不能超過這些限制
  • 如果沒有設置尺寸限制,則組件會盡可能多的占據空間。如果有多個組件,則空間分配以拉伸因子為標準
  • 如果沒有設置拉升因子,則以組件的尺寸策略 QWidget :: sizePolicy() 和尺寸提示 QWidget :: sizeHint() 為依據,在有多余空間的情況下,盡可能的占據多的空間。這也是組件的默認尺寸

組件中對布局有影響的函數

一個組件要想影響布局管理,需要使用以下任何或全部機制:

  • 重新實現 QWidget :: sizeHint() 以返回小部件的首選大小。
  • 重新實現 QWidget :: minimumSizeHint() 以返回小部件可以具有的最小尺寸。
  • 調用 QWidget :: setSizePolicy() 來指定窗口小部件的空間要求。

在上面的任何一個操作之后,都應調用 QWidget :: updateGeometry() ,這將導致布局重新計算。即使多次連續調用 QWidget :: updateGeometry() 也只會進行一次布局重新計算。

如果 Widget 的高度依賴于其寬度(例如,具有自動斷字的標簽),請在窗口小部件的大小策略中設置 height-for-width 標志,并重新實現 QWidget :: heightForWidth() 函數。

即使實現 QWidget :: heightForWidth() ,提供一個合理的 sizeHint()仍然是一個好主意。

自定義布局管理器

要編寫自己的布局類,必須定義以下內容:

  • 一個用于存儲被管理組件的數據結構,其中項是 QLayoutItem 類型 ,一般使用列表。
  • addItem(),如何添加項目到布局。
  • setGeometry(),如何執行布局。
  • sizeHint(),布局的首選大小。
  • itemAt(),如何迭代布局。
  • takeAt(),如何從布局中刪除項目。
  • 在大多數情況下,您還將實現 minimumSize()

Qt 布局系統提供了一種簡單而強大的方式,可以在 Widget 中自動排列子元素,以確保它們充分利用可用空間。

簡介

Qt 包括一組布局管理類,用于描述在應用程序的用戶界面中如何布局 Widget。 當可用的空間發生變化時,這些布局會自動定位和調整 Widget 的大小,確保它們一致地排列,并且整個用戶界面保持可用。

所有 QWidget 的子類都可以使用布局來管理他們的子元素, QWidget :: setLayout() 函數將一個 Layout 應用于 Widget, 當以這種方式在 Widget 上設置了一個 Layout 時,Layout 負責以下任務:

  • 子元素的定位。
  • 合理的窗口默尺寸。
  • 合理的的窗口最小尺寸。
  • 調整大小處理
  • 內容變化時自動更新:
    • 字體大小,文本或子元素的其他內容。
    • 隱藏或顯示一個 Widget
    • 刪除子元素

Qt 的布局類

省略......

水平、豎直、網格以及表格布局

為 Widget 設置布局的最簡單方法是使用內置的布局管理器:QHBoxLayoutQVBoxLayoutQGridLayoutQFormLayout ,這些類繼承自 QLayoutQLayout 又來自 QObject (而不是 QWidget ), 他們負責一組 Widgets 的幾何管理。 要創建更復雜的布局,可以將布局管理器嵌套在一起。

  • QHBoxLayout 從左到右(或從右到左)橫向排列出 Widget。

    img
  • QVBoxLayout 從上到下在垂直排列 Widget

    img
  • QGridLayout 在二維網格中顯示 Widget,一個 小部件可以占用多個單元格

    img
  • QFormLayout 在以兩列描述性的“標簽-字段樣式” 樣式布局 Widget

    img

用代碼布局 Widget

以下代碼用一個 QHBoxLayout 管理五個 QPushButton 的幾何特征,如上圖第一個截圖所示:

QWidget *window = new QWidget;

// 創建五個按鈕
QPushButton *button1 = new QPushButton("One");
QPushButton *button2 = new QPushButton("Two");
QPushButton *button3 = new QPushButton("Three");
QPushButton *button4 = new QPushButton("Four");
QPushButton *button5 = new QPushButton("Five");

// 創建布局管理器
QHBoxLayout *layout = new QHBoxLayout;

// 將按鈕添加到布局中
layout->addWidget(button1);
layout->addWidget(button2);
layout->addWidget(button3);
layout->addWidget(button4);
layout->addWidget(button5);

// 設置布局
window->setLayout(layout);

window->show();

除了布局類本身不同之外,QVBoxLayout 的使用方式是相同的。 QGridLayout 的代碼有一些不同,因為我們需要指定 Widget 的行和列位置:

QWidget *window = new QWidget;

// 創建五個按鈕
QPushButton *button1 = new QPushButton("One");
QPushButton *button2 = new QPushButton("Two");
QPushButton *button3 = new QPushButton("Three");
QPushButton *button4 = new QPushButton("Four");
QPushButton *button5 = new QPushButton("Five");

// 創建布局管理器
QGridLayout *layout = new QGridLayout;

// 將按鈕添加到布局中
layout->addWidget(button1, 0, 0);
layout->addWidget(button2, 0, 1);
// 注意:這個按鈕占用兩個單元
layout->addWidget(button3, 1, 0, 1, 2);
layout->addWidget(button4, 2, 0);
layout->addWidget(button5, 2, 1);

// 設置布局
window->setLayout(layout);

window->show();

第三個 QPushButton 跨越兩列,需要為 QGridLayout :: addWidget() 指定第四和第五個參數。

QFormLayout 在每一行上添加兩個 Widget ,通常是 QLabelQLineEdit ,用來創建表單。 在同一行添加 QLabelQLineEdit 會將 QLineEdit 設置為 QLabel 的伙伴控件。 以下代碼將使用 QFormLayout 在三行上放置 QPushButtons 和相應的 QLineEdit

QWidget *window = new QWidget;

QPushButton *button1 = new QPushButton("One");
QLineEdit *lineEdit1 = new QLineEdit();
QPushButton *button2 = new QPushButton("Two");
QLineEdit *lineEdit2 = new QLineEdit();
QPushButton *button3 = new QPushButton("Three");
QLineEdit *lineEdit3 = new QLineEdit();

// 創建表格布局
QFormLayout *layout = new QFormLayout;

// 一組一組的將按鈕和編輯器放到布局中
layout->addRow(button1, lineEdit1);
layout->addRow(button2, lineEdit2);
layout->addRow(button3, lineEdit3);

window->setLayout(layout);
window->show();

使用布局的一些提示

使用布局時,創建 Widget 不需要傳遞父對象,布局管理器會自動重新設置 Widget 的父對象(使用 QWidget :: setParent()),讓它們稱為設置了布局管理器的那個 Widget 的子元素。

注意:布局中的管理的 Widget 是設置布局的 Widget 的子節點,而不是布局本身的子節點, Widget 只能將其他 Widget 作為父類而不是布局。

可以在布局上使用 addLayout() 嵌套布局,內部布局外部布局的子元素

將 Widget 添加到布局中

當您將 Widget 添加到布局中時,過程如下:

  1. 所有的 Widget 最初將根據其 QWidget :: sizePolicy()QWidget :: sizeHint() 分配一定的空間。
  2. 如果 Widget 設置了大于零的拉伸因子,則按照其拉伸因子的比例分配空間(如下所述)。
  3. 如果 Widget 的拉伸因子設置為零,則只有在其他 Widget 不需要空間的情況下才會獲得更多的空間。其中,首先將空間分配給具有 Expanding 策略的小部件。
  4. 分配的空間小于其最小尺寸的 Widget(如果沒有指定最小尺寸,則為最小尺寸提示)將分配其所需的最小尺寸。 (Widget 不一定非要設置最小尺寸或最小尺寸提示,在這種情況下,拉伸因子是其決定因素。)
  5. 分配的空間大于其最大尺寸的 Widget 都將分配其所需的最大尺寸。 (小部件不一定非要設置最大尺寸,在這種情況下,拉伸因子是其決定因素。)

拉伸因子

Widget 創建的時候一般沒有設置拉伸因子。 當它們被放到布局管理器中時,Widget 將根據其 QWidget :: sizePolicy() 或其最小大小提示給予一定的空間份額,以較大者為準。 拉伸因子用于改變彼此占據空間的比例

如果將三個沒有設置拉伸因子的 Widget 放到 QHBoxLayout 布局中,我們將得到如下布局:

img

如果我們對每個 Widget 設置拉伸因子,它們將按比例(但不得小于其最小大小提示)布局。

img

自定義組件的布局

當您創建自己的 Widget 類時,還應該處理其布局屬性。如果 Widget 使用 Qt 的布局之一,這已經被處理了。如果 Widget 沒有任何子部件,也沒有使用手動布局,則可以使用以下任何或全部機制更改窗口小部件的行為:

  • 重新實現 QWidget :: sizeHint() 以返回小部件的首選大小。
  • 重新實現 QWidget :: minimumSizeHint() 以返回小部件可以具有的最小尺寸。
  • 調用 QWidget :: setSizePolicy() 來指定窗口小部件的空間要求。

每當大小提示、最小大小提示或大小策略更改時,都應調用 QWidget :: updateGeometry() ,這將導致布局重新計算。即使多次連續調用 QWidget :: updateGeometry() 也只會進行一次布局重新計算。

如果您的小部件的首選高度依賴于其實際寬度(例如,具有自動斷字的標簽),請在窗口小部件的大小策略中設置 height-for-width 標志,并重新實現 QWidget :: heightForWidth() 函數。

即使實現 QWidget :: heightForWidth() ,提供一個合理的 sizeHint()仍然是一個好主意。

有關實施這些功能的進一步指導,請參閱Qt季刊文章 《寬度的交易高度》。

布局可能出現問題

在 Label 組件中使用富文本可能會在其父窗口的布局中引入一些問題,當 Label 設置了 “word wrapped” 時,布局管理器會發生問題。

在某些情況下,布局被設置為 QLayout :: FreeResize 模式,這意味著它不會調整其內容的布局以適合較小尺寸的窗口,甚至會阻止用戶將窗口變小。 這個問題可以通過對有問題的小部件進行子類化,并實現適當的 sizeHint()minimumSizeHint() 函數來克服。

在某些情況下,Widget 必須有布局才能被使用。 當您為 QDockWidgetQScrollArea (使用 QDockWidget :: setWidget()QScrollArea :: setWidget() )設置 Widget 時,必須已在 Widget 上設置布局。 如果沒有,該 Widget 將不可見。

手動布局

如果您正在制作獨一無二的特殊布局,可以按照上述方式制作自定義 Widget 。 在 QWidget :: resizeEvent() 中計算所需的空間,并調用每個子元素上調用 setGeometry()

當需要重新計算布局時,Widget 將接收到一個 QEvent :: LayoutRequest 的事件對象。 可以在 QWidget :: event() 中處理 QEvent :: LayoutRequest 事件。

如何編寫自定義布局管理器

手動布局的替代方法是通過對 QLayout 進行子類化來編寫自己的布局管理器。 邊框布局和流程布局示例顯示了如何做到這一點。

我們在這里詳細介紹一個例子。 CardLayout 類收到來自同名的 Java 布局管理器的啟發。 它將 Widget 放在彼此之上,每個 Item 偏移 QLayout :: spacing() 距離

要編寫自己的布局類,您必須定義以下內容:

  • 用于存儲被布局處理的組件的數據結構。 每個組件都是一個QLayoutItem , 在這個例子中我們將使用一個 QList
  • addItem(),如何添加項目到布局。
  • setGeometry(),如何執行布局。
  • sizeHint(),布局的首選大小。
  • itemAt(),如何迭代布局。
  • takeAt(),如何從布局中刪除項目。
  • 在大多數情況下,您還將實現 minimumSize()
#ifndef CARD_H
#define CARD_H

#include <QtWidgets>
#include <QList>

class CardLayout : public QLayout
{
public:
    CardLayout(QWidget *parent, int dist): QLayout(parent, 0, dist) {}
    CardLayout(QLayout *parent, int dist): QLayout(parent, dist) {}
    CardLayout(int dist): QLayout(dist) {}
    ~CardLayout();

    void addItem(QLayoutItem *item);
    QSize sizeHint() const;
    QSize minimumSize() const;
    int count() const;
    QLayoutItem *itemAt(int) const;
    QLayoutItem *takeAt(int);
    void setGeometry(const QRect &rect);

private:
    QList<QLayoutItem*> list;
};

#endif

實現文件 card.cpp

//#include "card.h"

首先得定義一個 count() 函數來獲得列表中的項數

int CardLayout::count() const
{
    // QList::size() returns the number of QLayoutItems in the list
    return list.size();
}

然后重寫 itemAt()takeAt() 這兩個函數,這些函數在布局系統內部用于處理 Widget 的刪除,當然,程序員也可以直接使用它們。

itemAt() 返回給定索引處的 Item 。 takeAt() 刪除給定索引處的 Item ,并返回它。 這里,我們使用列表索引作為布局索引。 在其他情況下,我們有一個更復雜的數據結構,我們可能需要花費更多的精力為 Item 定義線性順序。

QLayoutItem *CardLayout::itemAt(int idx) const
{
    // QList::value() performs index checking, and returns 0 if we are
    // outside the valid range
    return list.value(idx);
}

QLayoutItem *CardLayout::takeAt(int idx)
{
    // QList::take does not do index checking
    return idx >= 0 && idx < list.size() ? list.takeAt(idx) : 0;
}

addItem() 為布局 Item 實現了默認放置策略,這個函數必須被實現,他會被 QLayout :: add() 使用。 如果您的布局具有需要參數的高級放置選項,則必須提供額外的訪問函數,例如還來間距 QGridLayout :: addItem()QGridLayout :: addWidget()QGridLayout :: addLayout() 的重載

void CardLayout::addItem(QLayoutItem *item)
{
  list.append(item);
}

注意:QLayoutItem 不是繼承自 QObject 因此,必須手動銷毀

 CardLayout::~CardLayout()
  {
       QLayoutItem *item;
       while ((item = takeAt(0)))
           delete item;
  }

setGeometry() 實際計算各個組件的位置和尺寸,如果需要,計算的時候可以將 spacing() 考慮進去,但是不考慮 margin()

void CardLayout::setGeometry(const QRect &r)
{
  QLayout::setGeometry(r);

  if (list.size() == 0)
      return;

  int w = r.width() - (list.count() - 1) * spacing();
  int h = r.height() - (list.count() - 1) * spacing();
  int i = 0;
  while (i < list.size()) {
      QLayoutItem *o = list.at(i);
      QRect geom(r.x() + i * spacing(), r.y() + i * spacing(), w, h);
      o->setGeometry(geom);
      ++i;
  }
}

sizeHint()minimumSize() 的實現非常類似,他們都可以考慮 spacing() 但是不考慮 margin()

QSize CardLayout::sizeHint() const
{
  QSize s(0,0);
  int n = list.count();
  if (n > 0)
          s = QSize(100,70); //start with a nice default size
      int i = 0;
      while (i < n) {
          QLayoutItem *o = list.at(i);
          s = s.expandedTo(o->sizeHint());
          ++i;
      }
      return s + n*QSize(spacing(), spacing());
  }

  QSize CardLayout::minimumSize() const
  {
      QSize s(0,0);
      int n = list.count();
      int i = 0;
      while (i < n) {
          QLayoutItem *o = list.at(i);
          s = s.expandedTo(o->minimumSize());
          ++i;
      }
      return s + n*QSize(spacing(), spacing());
  }

其他注意事項

  • 此自定義布局不處理寬度的高度。
  • 我們忽略了 QLayoutItem :: isEmpty(), 這意味著布局會將隱藏的小部件視為可見的。
  • 對于復雜的布局,通過緩存計算值可以大大提高速度。 在這種情況下,實現 QLayoutItem :: invalidate() 來標記緩存的數據是臟的。
  • 調用 QLayoutItem :: sizeHint() 等可能是昂貴的。 因此,如果您稍后在同一功能中再次需要,您應該將該值存儲在局部變量中。
  • 你不應該在同一個函數的同一個項目上調用 QLayoutItem :: setGeometry() 兩次。 如果該項目具有多個子窗口小部件,則此調用可能非常昂貴,因為布局管理器必須每次執行完整的布局。 而是計算幾何體,然后進行設置。 (這不僅適用于布局,如果您實現自己的resizeEvent() ,則應該這樣做)。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容