Qt最新事情-QtでWebKitを使ってみよう

第4回Qtの基本プログラミングレイアウトマネージメント

はじめに

前回は、C++のオブジェクトに機能拡張を施したQtのオブジェクトモデルについて説明しました。今回は、ウィジェット作成での基本的機能であるレイアウトマネージメントについて説明します。ただウィジェットを並べてレイアウトする方法を説明するのではなく、レイアウトマネージメントの考え方や、Qtのレイアウトマネージメントがどのような作りになっているかについても触れたいと思います。

レイアウトマネージメントとは

表1のように、ウィジェットは2種類に分類できます。

表1 ウィジェットの種類
種類説明
コンポジットウィジェットウィジェットを配置して作成するウィジェット
グラフィカルウィジェットアナログ時計やVUメーターのようなウィジェット

グラフィカルウィジェットは、2D描画機能を使ってウィジェットの外観を実装し、イベントを処理してユーザ操作を実装します。ボタン、ラベル、スライダーなどもそのようにして実装されています。コンポジットウィジェットは、既存のウィジェットを一般には平面状に配置したウィジェットです。コンポジットウィジェット自体もその構成ウィジェットになります。一般にコンポジットウィジェットでは、ウィジェットの配置方法を2種類持っています。

表2 ウィジェットの配置方法
方法説明
位置とサイズ指定ウィジェットの絶対位置とサイズを指定して並べる機能。どのGUIツールキットも必ず持っているウィジェット配置機能。
レイアウトマネージメントウィジェットの相対関係を指定してウィジェットを並べたり、空き領域にウィジェットを詰込んで行くというように、レイアウトポリシーに従ってウィジェットを配置する機能。

位置とサイズは、setGeometry()、resize()、move()で指定します。しかし、この方法のみでは、以下のような変更に追従する際に、アプリケーション側でそのための処理を書かなければならず、アプリケーション作成の度に何度も煩雑な手間が繰り返されます。

  • 日本語や英語などの言語の変更
  • ウィンドウサイズの変更
  • フォントの種類やフォントサイズの変更

そこで、ウィンドウシステムが現れてから、いろいろなレイアウトポリシーでウィジットを並べる方法が考案されています。

Qtのレイアウトポリシー

Qtでは、表3にあるような並べ方が用意されています。

表3 Qtのレイアウトクラス
レイアウトクラス説明
QBoxLayout縦(上から下、下から上)または横(左から右、右から左)に並べる。
QHBoxLayoutQBoxLayoutを継承し、左から右に横に並べる。
QVBoxLayoutQBoxLayoutを継承し、上から下に縦に並べる。
QGridLayout格子状に並べる。
QFormLayoutn行2列に並べる。ラベル付きで入力ウィジェットを並べることに特化。Qt 4.4で追加された並べ方。
QStackedLayout前後に重ね合わせて、その内の1つを表示中にする。

いろいろありますが、機能を分類して整理するとQHBoxLayoutとQVBoxLayoutはQBoxLayoutの機能を部分的に使うクラスで、実際にはコンストラクタしか実装されていません。QFormLayoutとQStackedLayoutは、補助的なレイアウト機能なので、QBoxLayoutとQGridLayoutの2つのみ、つまり、直線状か格子状に並べるのがQtのレイアウトポリシーです。ただし、実際のコードでは、QHBoxLayout、QVBoxLayout、QGridLayoutの3つが使われ、QBoxLayoutは殆ど使いません。

レイアウトマネージャは、レイアウトポリシーに従って位置(move())やサイズ調整(resize())をしてウィジェットを並べています(つまり、どのようなレイアウトマネージメント機能も最終的にはウィジェットの位置とサイズを指定しています⁠⁠。そうすることで、ウィンドウサイズが変更されたり、フォントが変更されてもウィジェットをきれいに並べられるようになります。

レイアウトの作成方法

ベースのウィジェット上にウィジェットを配置するには、ベースのウィジェットを親オブジェクトにして、レイアウトクラスのインスタンスを作成し、addWidget()でレイアウトに入れます。レイアウトにレイアウトをネストさせるには、addLayout()を使います。リスト1では、トップダウンにウィジェットを並べています。

リスト1 トップダウンなレイアウト
QVBoxLayout* topLayout = new QVBoxLayout(this);

QLabel* messageLabel = new QLabel(this);
topLayout->addWidget(messageLabel);

QPushButton* okButton = new QPushButton("Ok", this);
QPushButton* cancelButton = new QPushButton("Cancel", this);
QHBoxLayout* buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);
topLayout->addLayout(buttonLayout);

リスト2は、Qt 4で追加されたボトムアップなレイアウト記述方法で、ウィジェットのインスタンス生成で親ウィジェットを指定していないので簡潔な記述になっています。setLayout()の呼び出しで、レイアウトがベースのウィジェットに設定され、topLayout 内にある子ウィジェットの親ウィジェットは、QObject::setParent()でベースのウィジェットに設定されます。記述方法はリスト1と異なりますが、オブジェクトツリーとレイアウト内に配置されるオブジェクトの並びは、リスト1と同じです。レイアウトで何が行われているかは掴みにくいですが、慣れると使いやすく修正もしやすい記述方法です。

リスト2 ボトムアップなレイアウト
QLabel* messageLabel = new QLabel(this);

QPushButton* okButton = new QPushButton("Ok");
QPushButton* cancelButton = new QPushButton("Cancel");
QHBoxLayout* buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);

QVBoxLayout* topLayout = new QVBoxLayout;
topLayout->addWidget(messageLabel);
topLayout->addLayout(buttonLayout);

setLayout(topLayout);

レイアウト内に配置されるオブジェクト

インスタンス生成に時間がかかったり再生成が困難なウィジェットがある場合には、そのウィジェットを1つ用意して、複数のウィンドウで使い回したくなります。そのためには、オブジェクトツリーとレイアウト構造の把握が必要です。

レイアウト内には、3種類のオブジェクトが存在します。

  • ウィジェット
    ラベルやボタンなどのウィジェット。addWidget()でレイアウトに入れる。
  • レイアウト
    レイアウト自体も並べられる、つまりネストしたレイアウト。addLayout()でレイアウトに入れる。
  • スペーサー
    QSpacerItemのインスタンス。縦横の広がりを持つ空白で表示はされない。固定サイズのスペーサーはaddSpacing()、伸縮するスペーサーはaddStretch()でレイアウトに入れる。

リスト3は固定サイズのボタンと伸縮するスペースに合わせて伸縮するボタンを並べたレイアウト記述で、図1が実行結果となります。

リスト3 レイアウト内オブジェクトの確認コード
QPushButton* button11 = new QPushButton("Button 11");
QPushButton* button12 = new QPushButton("Button 12");
QPushButton* button13 = new QPushButton("Button 13");
QPushButton* button14 = new QPushButton("Button 14");

QHBoxLayout* buttonLayout1 = new QHBoxLayout;
buttonLayout1->addWidget(button11);
buttonLayout1->addSpacing(12);        // 固定サイズのスペース
buttonLayout1->addWidget(button12);
buttonLayout1->addWidget(button13);
buttonLayout1->addStretch();            // 伸縮するスペース
buttonLayout1->addWidget(button14);

QPushButton* button21 = new QPushButton("Button 21");
QPushButton* button22 = new QPushButton("Button 21");

QHBoxLayout* buttonLayout2 = new QHBoxLayout;
buttonLayout2->addWidget(button21);
buttonLayout2->addWidget(button22);

QVBoxLayout* topLayout = new QVBoxLayout;
topLayout->addLayout(buttonLayout1);
topLayout->addLayout(buttonLayout2);

QWidget top;
top.setLayout(topLayout);
図1 レイアウト確認サンプル
図1 レイアウト確認サンプル

オブジェクトツリーは図2のようになり、レイアウト内のオブジェクトのリンク構造は図3のようになります。図3ではマージンによる空白は省略しています。

図2 リスト3の記述で作成されるオブジェクトツリー
図2 リスト3の記述で作成されるオブジェクトツリー
図3 リスト3の記述で作成されるレイアウト内のリンク構造
図3 リスト3の記述で作成されるレイアウト内のリンク構造

レイアウト内にどのようにオブジェクトが存在しているかを把握するのは、一段上のステップに上がるために必要な知識です。QLayout::itemAt()、QLayoutItem::widget()、QLayoutItem::layout()、QLayoutItem::spacerItem()を使うと、図2や図3のようになっているのを確かめられます。

いろいろなスペースとサイズ制約

いろいろなスペースの取り方やウィジェットのサイズ制約方法によって、レイアウト機能が柔軟になり、複雑なレイアウト要求に答えられるようになります。

いろいろなスペース

スペースには、属性としてのスペースとオブジェクトとして存在するものの2つがあります。

表4 スペースの種類
種類説明
マージンレイアウトに並べたウィジェットの上下左右に取られるスペース。QLayout::getContentsMargins()/setContentsMargins()で参照と設定。
スペーシングレイアウトに並べるウィジェットの間隔。レイアウトオブジェクトのspacingプロパティで、すべてのウィジェット間に適用される。
可変スペースレイアウトに配置された可変サイズのQSpacerItemオブジェクト。
固定スペースレイアウトに配置された固定サイズのQSpacerItemオブジェクト。

サイズヒントとサイズポリシー

サイズヒントは、QWidget::sizeHint()が返すウィジェットの最適なサイズです。最適というのは、たとえばボタンであれば、ボタンのテキストが欠けることなく表示され、そのテキストの周りに適度なスペースが取られたサイズです。

グラフィカルなウィジェットには、最適なサイズを決められるものと決められないものがあります。アナログ時計ウィジェットのように、最適サイズのないウィジェットは、適度なサイズを返すようにsizeHint()を実装して、レイアウトマネージャにそのサイズがどれくらいになるかわかるように知らせないと、サイズが決められず、表示されないことがあります。

コンポジットウィジェットは、レイアウトを使用していれば、レイアウトが配下のウィジェットのサイズヒントを調べてコンポジットウィジェットのサイズヒントを自動的に求めます。レイアウトを使用していないコンポジットウィジェットの場合には、グラフィカルなウィジェットと同じようにsizeHint()を実装します。

サイズポリシーは、QWidget::sizePolicy()が返すQSizePolicyオブジェクトで、ウィジェットが縦または横にどのように伸縮できるかというポリシーです。ボタンは縦には伸縮させないものなので、縦のサイズポリシーはFixedで、横には伸ばしても良いけれど、ボタンのテキストは表示されなければならないので、横のサイズポリシーはMinimumに設定されています。

表5 よく使われるサイズポリシー
種類説明
QSizePolicy::Fixedサイズヒントに常に固定。
QSizePolicy::Minimumサイズヒントが最小サイズで、サイズヒントよりも拡大可能。
QSizePolicy::Maximumサイズヒントが最大サイズで、最小サイズ迄縮小可能。
QSizePolicy::Preferredサイズヒントが最適サイズで、伸縮可能。
QSizePolicy::Expanding伸縮できるが拡大の方が適している。

スペーサーにもサイズヒントとサイズポリシーがあり、最小幅が12ピクセルで、横方向に延ばせるというようなスペースも作れます。

サイズ制約

QWidgetのminimumSizeとmaximumSizeのプロパティが最小サイズと最大サイズで、setFixedSize()で最小サイズと最大サイズを同一にすることで固定サイズとなります。

ストレッチ

サイズポリシーには、ベースウィジェットの横幅または高さをどのような割合で子ウィジェットに配分するかを指定するストレッチファクターがあり、QLayout::addWidget(widget, factor)の2番目の引数でストレッチファクターを指定すると、ウィジェットのサイズポリシーに設定されます。例えば、2つの横方向に並ぶウィジェットに2と3のストレッチファクターを指定すると、ベースウィジェットの横幅を2:3で分割した幅が各ウィジェットに配分されます。ただし、ウィジェットは最小サイズよりは小さくなりません。デフォルトは0でストレッチファクターは影響しません。スペーサーにもストレッチファクターがあり、ウィジェットと同様に幅と高さが割り当てられます。

ストラット

レイアウトストラットとグローバルストラットの2種類のストラットがあります。レイアウトストラットはaddStrut(int)でで設定し、QHBoxLayoutの場合には、レイアウト領域の高さは指定した値より小さくなりません。逆に、QVBoxLayout の場合には、幅が指定した値より小さくなりません。

グローバルストラットは、QApplication::setGlobalStrut(QSize)で指定する矩形で、マウスで操作するボタンやスライダーなどの大きさは、指定した矩形よりも小さくならないようになります。タッチパネルなどの操作に適するようにウィジェットのサイズを調整できます。

heightForWidth()

ウィジェットやレイアウトの幅に対応して高さが定まるような場合に使われます。たとえば、QLabelのテキストが折り返されるときにその高さを求めて返しています。

レイアウトマネージメントの補足事項

  • 必ずしもすべてのウィジェットをレイアウトマネージメント機能の配下に置く必要はありません。インジケータのようなウィジェットを浮かせて、他のウィジェットの上に部分的に重ねることもあります。そのようなウィジェットは、レイアウトには入れずに使います。
  • Qt Designerのようにグラフィカルにウィジェットを編集するツールは、レイアウトコードを使いこなせるようになってから使ってこそ効果的です。レイアウトがどのようなことをしているかを本質的に掴めるようになるには、コードで記述できることが必須です。GUI操作では使えるレイアウト機能が部分的であったり、GUI操作のために便宜的な操作方法を取っていたりしているため、本質的なことを掴みにくいからです。
  • レイアウト機能で並べているウィジェットに、位置やサイズの直接指定はできません。たとえば、横に並んでいるウィジェットの片方の端のウィジェットをmove()で呼び出して、もう一方の端に位置を変えようとすれば、レイアウトマネージャが並べようとしていることと矛盾してしまいます。
  • 矛盾するレイアウト要求もあります。レイアウトマネージャが常に全ウィジェットの要求を満たせるとは限りません。たとえば、80×200の固定サイズのベースウィジェット上に、60×50の固定サイズの子ウィジェットを縦に5つは並べられません。子ウィジェットの高さは50よりも小さくなった状態で並べられるでしょう。

まとめと次回の予告

Qtのアーキテクチャの中から、Qtを特徴付ける基本的な機能を普通とは少し違う視点で説明してきました。次回は、いよいよ本連載のテーマであるQt 4.4で追加された大きな機能Qt WebKitについて説明です。

おすすめ記事

記事・ニュース一覧