Return to Linux Life Edit

  ホーム · 全てのクラス · メインのクラス · 注釈付き · グループ別 · 関数一覧

レイアウト管理

Qt のレイアウトシステムでは、子ウィジェットのレイアウトを指定する単純で強力な方法を提供しています。

論理的なレイアウトを指定すると、以下の処理がより簡単になります。

手入力されたレイアウトコードの欠点は、フォームのデザインを試すごとにコンパイル・リンク・実行をしなければならず不便なところです。その問題に対しては Qt Designerと言う GUI visual 設計ツールを用意し、レイアウトの試作と C++ コードの生成を速く簡単に行えるようにしています。

Qt のレイアウトクラスは 手入力の C++ コードのために設計されており、理解と使用は簡単にできます。そして Qt Designer を使って生成したコードもまたこれらのレイアウトクラスを使用します。

Topics:

水平、垂直、格子レイアウト

ウィジェットを見栄えよく並べる一番簡単な方法は、組み込みのレイアウトマネージャを使うことです。 QHBoxLayout QVBoxLayout QGridLayoutといったクラスがあります。これらのクラスは QLayoutを継承しており、 QObject QWidgetではない)から派生しています。これらはウィジェットの座標・サイズを注意深く管理します。より複雑なレイアウトを管理したい場合は、レイアウトマネージャ同士を内側に組み込むことで対応できます。

以下のコードでは QHBoxLayout を作成し、その中で5つの 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 を使ったコードは、子ウィジェットの位置を行と列とで指定しなければならないので、少し異なります。

        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();

ここで3番目の QPushButton は2行にまたがっています。これは QGridLayout::addWidget() の4番目の引数に 2 を指定することで実現可能です。

レイアウトを使う場合、子ウィジェットの構築時に親ウィジェットを渡す必要はありません。レイアウトは子ウィジェットがレイアウトに追加されたときに自動的にウィジェットの親を( QWidget::setParent()を使って)付け替えます。

重要: レイアウト内のウィジェットはレイアウトがインストールされているウィジェットの子ウィジェットとなります。レイアウト自身の子ウィジェット ではありません 。ウィジェットはレイアウトではなくただひとつのウィジェットのみを親ウィジェットとしてもちます。

レイアウトは他のレイアウトの上に addLayout() を使って重ねることができます。内部レイアウトは追加されたレイアウトの子要素となります。 Basic Layouts の例では、この機能を使って複雑なダイアログを作成します。

レイアウトへのウィジェットの追加

レイアウトにウィジェットを追加すると、レイアウトは次のように処理を行います。

  1. すべてのウィジェットはまず最初にあるサイズの領域に割り当てられます。これには QWidget::sizePolicy() が用いられます。
  2. すべてのウィジェットが 0 を超える stretch factor を持っている場合、その stretch factor に比例した領域が割り当てられます(以下で説明します)。
  3. ウィジェットの stretch factor が 0 の場合は、他のウィジェットが領域を必要としない場合のみ領域が広げられます。この場合、まず Expanding サイズポリシーに基づいて領域が割り当てられます。
  4. 最小サイズより小さい領域しか割り当てられていないウィジェットには(最小サイズが特定できない場合は最小サイズヒントを使用)、要求されている最小サイズを割り当てます。(ウィジェットは最小サイズと最小サイズヒントを必ず持つ必要はありません。この場合は stretch factor によってこれらを決定します)。
  5. 最大サイズより大きな領域を割り当てられているウィジェットには、要求されている最大サイズを割り当てます。(ウィジェットは最大サイズを持つ必要はありません。この場合は stretch factor を用いてこれらを決定します)。

Stretch Factors

ウィジェットは通常 stretch factor set を指定せずに生成されます。レイアウトにおかれたときに、ウィジェットには共有の領域が割り当てられ、そのサイズは QWidget::sizePolicy() または最小サイズヒントのどちらか大きいほうとなります。stretch factor はそれぞれに比例してどれだけの領域がウィジェットに割り当てられるかを決めるのに使われます。

ここで3つのウィジェットが QHBoxLayout の中におかれ、それぞれの stretch factor set が指定されていない場合、以下のようなレイアウトとなります。

そして各ウィジェットに stretch factor を適用した場合は、比例した領域を割り当てられて、例えば以下のようなレイアウトとなります。

Custom Widgets in Layouts

ユーザー定義のウィジェットクラスを作成した場合も、レイアウトプロパティとやり取りをするべきです。ウィジェットが QLayoutを持っている場合は、これは既に考慮されています。ウィジェットが子ウィジェットを持たない場合、または手作業のレイアウトを行っている場合は、以下の QWidget メンバー関数を実装しなおしておくべきです。

サイズヒント、、最小サイズヒント、またはサイズポリシーが変更となった場合は QWidget::updateGeometry() を呼び出してください。これはレイアウトの再計算の引き金となります。複数回連続した QWidget::updateGeometry() の呼び出しはまとめられ、一回の再計算となります。

必要な高さが実際の幅に依存する場合(例えば、自動的に単語で改行するラベルなど)、ウィジェットの height-for-width フラグが size policy にあるのでそれをセットし、 QWidget::heightForWidth() を再実装してください。

そして QWidget::heightForWidth() を実装した場合でも、適切な sizeHint() を提供することは良い考えです。

これらの関数を実装する際のガイダンスとしては、 Traiding Height for Width Qt Quarterly内にあります。

Layout Issues

ラベルウィジェットの中でリッチテキストを使用することは親ウィジェットのレイアウトで問題となることがあります。問題がおきるかどうかは、ラベルで改行するときにQtレイアウトマネージャがどうリッチテキストを扱うか、によります。

この場合、親レイアウトを QLayout::FreeResize という、内部のウィンドウの最小サイズに合わせないでレイアウトするモードにするか、ユーザーに使用できないほどウィンドウを小さくされるのを防ぐ、といった対応を行います。これは、問題のおきたクラスの派生クラスを作成し、sizeHint() やminimumSizeHint()といった関数を適切に実装することで行います。

Manual Layout

ある特殊な形式のレイアウトを作成したい場合は、上で説明した方法でカスタムウィジェットを作成するように作成することができます。まず QWidget::resizeEvent() を再実装して子ウィジェットごとに必要なサイズを計算し、 setGeometry() を呼び出します。

ウィジェットはレイアウトの再計算が必要になった時 QEvent::LayoutHint タイプのイベントを受信します。 QWidget::event() を再実装し、 QEvent::LayoutHint イベントを処理します。

Writing Custom Layout Managers

Manual layout の代わりとして、独自のレイアウトマネージャを QLayoutのサブクラスとして実装するという手段があります。 Border Layout Flow Layout という例を使って、どのようにするのかここで説明します。

ここで詳細な例をあげます。CardLayoutは同名のJavaのレイアウトマネージャに想起されたものです。これはアイテム(ウィジェットやネストしたレイアウト)をそれぞれの上に、 QLayout::spacing() で指定した分だけずらしておくものです。

独自のレイアウトクラスを実装するには、以下を定義する必要があります。

多くの場合、さらに minimumSize() の実装が必要です。

The Header File (card.h)

    #ifndef CARD_H
    #define CARD_H
    #include <QLayout>
    #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;
        QLayoutItem *itemAt(int) const;
        QLayoutItem *takeAt(int);
        void setGeometry(const QRect &rect);
    private:
        QList<QLayoutItem*> list;
    };
    #endif

The Implementation File (card.cpp)

    #include "card.h"

まず最初に、レイアウトを横断してアクセスする関数 itemAt() と takeAt() を実装します。これらの関数は内部ではレイアウトシステムがウィジェットを削除するときに使用されます。またアプリケーションプログラマも使用可能です。

ItemAt() は与えられたインデックスのアイテムを返します。takeAt() は与えられたインデックスのアイテムを削除し、そのアイテムを返します。この例ではリストのインデックスをレイアウトのインデックスとして使います。より複雑なデータ構造を使用した場合、これらの処理をアイテムの数に比例したオーダーより効果的に行うことができるかもしれません。

    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() にはデフォルトのアイテムレイアウトの仕方を実装します。必ず実装しなければいけません。これは QLayout::add() の時、つまり QLayout コンストラクタがレイアウトを親としてとった時に使われます。レイアウトが追加の引数によって場所を決定する場合は、追加のアクセス関数を提供します。ちょうど行と列が QGridLayout::addItem(), addWidget(), and addLayout() でオーバーロードされているように、です。

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

レイアウトは追加されたアイテムについて責任を持ちます。 QLayoutItem QObjectを継承していないため、必ず手動で破棄しなければなりません。関数 QLayout::deleteAllItems() は上で定義している takeAt() を使って、レイアウトの中のすべてのアイテムを破棄します。

    CardLayout::~CardLayout()
    {
        deleteAllItems();
    }

そして setGeometry() 関数で、実際のレイアウトを決定します。引数として margin() を含まない長方形の領域が渡されます。そのサイズが妥当なら、spacing() をアイテム間の距離として使います。

    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());
    }

補足説明

ここでは width に対応した height は扱いませんでした。

ここでは QLayoutItem::isEmpty() を無視しました。そのため隠されたウィジェットが見えているかのようにレイアウトします。

複雑なレイアウトの場合、値をキャッシュすることで大幅な速度の向上が望めます。こうした場合、 QLayoutItem::invalidate() をキャッシュが無効となった場合に呼び出すように実装する必要があります。

さらに QLayoutItem::sizeHint() の呼び出しには時間がかかることがあるため、同じ関数の中で後で使う必要がある場合はローカル変数に保持するべきです。

同じ関数の中で、2度同じアイテムの QLayoutItem::setGeometryを呼び出さないようにすべきです。これはアイテムが複数のウィジェットをもっている場合にそれら全てをレイアウトしなおさないとならないため、かなり時間がかかります。そのかわり、ジオメトリを計算しておいてその値を設定してください(これはレイアウトだけに限った話ではなく、独自の resizeEvent() を実装した時も同様です)。


Copyright © 2005 Trolltech Trademarks
Qt 4.0.0