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

第3回Qtの基本プログラミング~オブジェクトモデル

はじめに

前回は、Qtの中核機能からシグナルとスロットというタイプセーフなコールバックについて説明しました。今回は、シグナルとスロットの実装の基礎となっているQtのオブジェクトモデルについて説明します。

オブジェクトモデル

Qtのオブジェクトモデルでは、C++オブジェクトにシグナルとスロットを含む表1の機能拡張をして、GUIプログラミングに必要とされる柔軟性を高めています。

表1 オブジェクトモデルで拡張された機能
シグナルとスロットタイプセーフコールバック、オブジェクトの疎結合とカプセル化
オブジェクトプロパティ問い合わせ可能なプロパティ
イベントとイベントフィルターイベント処理とイベントフック
国際化用テキスト文字列の翻訳クラスをコンテキストとするテキスト文字列翻訳
インターバルタイマーイベント処理と統合されたタイマー
オブジェクトツリーオブジェクトの階層化と検索
ガーディドポインターQPointer。Qt 4.5でQSharedPointerとQWeakPointerが追加
ダイナミックキャストライブラリ境界制限のないダイナミックキャスト

これらの機能は、オブジェクトクラスQObjectに対して実装されています。この中からオブジェクトプロパティ、オブジェクトツリー、ダイナミックキャストの3つ、それとこれらを支えているメタオブジェクトシステムについて説明します。

オブジェクトツリー

図1 コンポジットウィジェットの例
図1 コンポジットウィジェットの例

図1のように、既存のウィジェットをウィジェットの上に配置して作成するウィジェットをコンポジットウィジェットと呼び、以下の5つがその実装上のポイントです。

  1. ウィジエットがどのウィジェット上に置かれているか
  2. ウィジェットをどのように並べるか
  3. ウィジェットの表示/非表示の制御
  4. ウィジェットの有効化/無効化の制御
  5. ウィジェットのメモリ割り当てと解放の制御

Qtでは、これらの機能の実装にオブジェクトツリーが利用されています。2番目の「ウィジェットの並べ方」は、次回に詳しく説明します。

図1のコンポジットウィジェットのオブジェクトツリーは、レイアウトのためのオブジェクトを省略して、ウィジェットのみに着目すると、図2のようになります。

図2 図1のコンポジットウィジェットのオブジェクトツリー
図2 図1のコンポジットウィジェットのオブジェクトツリー

QFrameとQGroupBoxの親オブジェクトはQWidget、QLabelとQSliderの親オブジェクトはQGroupBoxです。QFrameの親クラスはQWidgetですが、QLabelやQSliderの親クラスはQGroupBoxでないことからわかるように、クラスの継承関係とは異なることに注意しましょう。これらのオブジェクトの親子関係はQObjectの機能で、QObjectを継承したQWidgetでは、この親子関係が、ウィジェットがどのウィジェットの上に乗っているかというウィジェットの上下関係に対応付けられています。

ウィジェットの親子関係に着目したコードはリスト1のようになります。

リスト1 ウィジェットの親子関係
class ColorChooser : public QWidget
{
...
private:
    QFrame* colorFrame;
    QSlider* redSlider;
    QSlider* greenSlider;
    QSlider* blueSlider;
}

ColorChooser::ColorChooser( QWidget* parent )
    : QWidget( parent )
{
    ...
    colorFrame = new QFrame(this);
    QGroupBox* colorGroupBox = new QGroupBox("RGB", this);

    QLabel* redLabel = new QLabel("&Red", groupBox);
    QLabel* greenLabel = new QLabel("&Green", groupBox);
    QLabel* blueLabel = new QLabel("&Blue", groupBox);
    redSlider = new QSlider(Qt::Horizontal, colorGroupBox);
    greenSlider = new QSlider(Qt::Horizontal, colorGroupBox);
    blueSlider = new QSlider(Qt::Horizontal, colorGroupBox);
    ...
}

ColorChooser::~ColorChooser()
{
    // 子ウィジェットのメモリ解放は不要。
}

ウィジェットのインスタンス生成時に親ウィジェットを指定しているのがポイントです。QObject またはその継承クラスのインスタンス生成では、親オブジェクトを指定して、オブジェクトツリーを形成できるようにしています。親オブジェクトは子オブジェクトのリストを持っていて、子オブジェクトは親オブジェクトを指すポインタを持っています。ウィジェットに親子関係を付けることによって、以下のことが可能となっています。

1.子ウィジェットのメモリ解放の自動化

親オウィジェットのメモリ解放時にオブジェクトツリーを辿って、その配下(親子関係の繋がりのあるウィジェット)の全ウィジェットのメモリ解放を自動的に行なっています。

デストラクタでのウィジェットのメモリ解放が不要となり、メモリ解放の確実性が高まります。後から参照するウィジェットの他には、メンバー変数でオブジェクトを抱えておく必要がなくなり、コードがすっきりします。

QTimerクラスなどのQObjectを継承した非GUIクラスも親ウィジェットを指定してインスタンス生成して、メモリ解放を連動させられます。QObjectを継承していないクラスについては、通常のC++のインスタンスの扱いと同じようにしてメモリ解放をします。

2.オブジェクトツリー内のオブジェクト検索

以下のようにして、オブジェクトツリー内のオブジェクトを検索し、ウィジットを操作できます。

QLabel* label = colorChooser->findChild<QLabel*>("redLabel");
QList<QLabel*> labels = colorChooser->findChildren<QLabel*>();
// ここでラベルのプロパティを変更する

ここで、QLabelのインスタンスには、以下のようにてオブジェクト名が付けられているものとしています。

redLabel->setObjectName("redLabel");

3 親ウィジェットの表示/非表示に応じて、配下のウィジェットを表示/非表示する。

show()を呼び出して親ウィジェットを表示状態にしたときに、その配下のウィジェットを自動的に表示します。hide()を呼び出すと非表示にします。

4 親ウィジェットの有効化/無効化に応じて、配下のウィジェットを有効化/無効化する。

setEnabled(true)を呼び出して親ウィジェットを有効化したときに、その配下のウィジェットを自動的に有効化します。setEnabled(false)を呼び出すと無効化します。

表示/非表示と有効化/無効化は、それぞれ三値状態で管理されているので、子ウィジェットが明示的にhide()やsetEnabled(false)を呼び出していると、親ウィジェットのshow()やsetEnabled(true)の呼び出しに、子ウィジェットは追従しなくなります。

子ウィジェットの自動メモリ解放に関連して、暗黙の共有クラスについても触れておきます。Qtでは、文字列、フォント、ピックスマップなどのリソースやコレクションクラスは、リファレンスカウント付きのクラスとして実装されていて、値ベースでデータを扱えるようにしています。関数の戻り値としてQStringやQPixmapのインスタンスを返しても文字列やイメージ領域の本体はコピーされません。コレクションクラスについても同様です。共有リソースに変更が行われようとするときに、データが実際にコピーされて変更が行われ、リファレンスカウントも変更されます。そして、リファレンスカウントが0になったときに、Qtライブラリーが自動的にメモリ領域を解放します。一般に、値ベースでデータを扱えるとコードが簡潔で明瞭になり、メモリ解放も確実にできて安定性が向上します。

表2 暗黙の共有クラス
QBitArray               QKeySequence            QSet
QBitmap                 QLinkedList             QSqlField
QBrush                  QList                   QSqlQuery
QByteArray              QLocale                 QSqlRecord
QCache                  QMap                    QStack
QCursor                 QMultiHash              QString
QDir                    QMultiMap               QStringList
QFileInfo               QPainterPath            QTextBoundaryFinder
QFont                   QPalette                QTextCursor
QFontInfo               QPen                    QTextDocumentFragment
QFontMetrics            QPicture                QTextFormat
QFontMetricsF           QPixmap                 QUrl
QGLColormap             QPolygon                QVariant
QGradient               QPolygonF               QVector
QHash                   QQueue                  QX11Info
QIcon                   QRegExp
QImage                  QRegion

オブジェクトプロパティ

QObjectの継承クラスに任意のプロパティを持たせることができ、オブジェクトにどのようなプロパティがあるかを検索して、プロパティの参照と変更ができます。

リスト2 ウィジェットのプロパティ定義
class Person : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QString name  READ name WRITE setName);
    Q_PROPERTY(int age READ age WRITE setAge);

public:
    explicit Person(QObject* parent = 0);

    QString name() const;
    int age() const;

public slots:
    void setName(const QString& name);
    void setAge(int age);
...
};
リスト3 プロパティ宣言
Q_PROPERTY(type name
           READ getFunction
           [WRITE setFunction]
           [RESET resetFunction]
           [DESIGNABLE bool]
           [SCRIPTABLE bool]
           [STORED bool]
           [USER bool])

リスト2では、nameとageの2つのプロパティを宣言しています。アクセッサに対してQ_PROPERTYマクロでプロパティ宣言を記述すると、property()メソッドを使ってプロパティ名でプロパティにアクセスできるようになります。READアクセッサは必須で、プロパティ名とREADアクセッサ名は異なっても構いません。実際にプロパティにアクセスするコードは次のようになります。

リスト4 プロパティにアクセスする例

QListIterator<PersonPtr> it(personList);
while (it.hasNext()) {
    PersonPtr p = it.next();
    QVariant name = p->property("name");
    QVariant age = p->property("age");
    QVariant weight = p->property("weight");

    qDebug() << (name.isValid() ? name.toString() : "----")
             << (age.isValid() ? age.toInt() : -1 )
             << (weight.isValid() ? weight.toInt() : -1 );
    if ( age.isValid() )
        p->setProperty("age", p->property("age").toInt() * 2 );
}

weightプロパティは宣言されていないので、通常は未定義になりますが、実行中にsetProperty()で未定義のプロパティに値を設定するとその時点でプロパティが定義されます。これをダイナミックプロパティと呼びます。

QVariantは、コピー可能な任意のオブジェクトをひとつ抱える機能を持つクラスです。C++の組込み型とQtのクラスは扱えるようになっていて、Qtのタイプシステムによってユーザ定義型も扱えます。

ダイナミックキャスト

QObjectの継承クラスに対して、RTTI(Run Time Type Information)を使わずにダイナミックキャストを可能にしています。dynamic_castと異なり、共有ライブラリーを跨いで使用できるが利点です。以下のように扱い方は dynamic_castと同じです。

QObject* object = new QLabel;
if (QLabel* label = qobject_cast<QLabel*>(object)) {
    label->setText("Hi!");
}

メタオブジェクトシステム

これまで説明した機能の実現を支えているのがメタオブジェクトシステムです。QObject::metaObject()メソッドはメタオブジェクトを返し、クラス名、シグナルとスロットのメソッド、プロパティ、列挙型と集合型、スーパークラスの情報などのオブジェクトのクラスのメタ情報にアクセスできるようにしています。

metaObject()が返すのは変数staticMetaObjectのアドレスで、metaObject()の実行コードとstaticMetaObjectの初期コードは、moc (Meta Object Compiler) がクラス宣言をパーズして生成されます。この他にmocは、ダイナミックキャストのためのqt_metacast()、シグナルとスロットのメソッド呼び出しのためのqt_metacall()、シグナルの実装コードなども生成します。

リスト5は、列挙型、シグナルとスロットのメソッド、プロパティに、メタオブジェクトを使ってアクセスをするコードの例です。

リスト5 メタ情報へのアクセスコード
const QMetaObject* metaObject = &Class::staticMetaObject;

int count;

qDebug() << endl << "Enumerators:";

qDebug() << "enumeratorCount() =" << metaObject->enumeratorCount();
qDebug() << "enumeratorOffset() =" << metaObject->enumeratorOffset();

count = metaObject->enumeratorCount();
for (int i = 0; i < count; i++) {
    QMetaEnum enumerator = metaObject->enumerator(i);
    qDebug() << "i =" << i;
    qDebug() << enumerator.name();
    for (int k = 0; k <  enumerator.keyCount(); ++k) {
        qDebug() << "\t" << enumerator.key(k);
    }
}

qDebug() << endl << "Methods:";

qDebug() << "methodCount() =" << metaObject->methodCount();
qDebug() << "methodOffset() =" << metaObject->methodOffset();

count = metaObject->methodCount();
for (int i = 0; i < count; i++) {
    QMetaMethod method = metaObject->method(i);
    qDebug() << "i =" << i;
    qDebug() << method.signature() << "," << method.typeName();
    qDebug() << method.parameterNames();
    qDebug() << method.parameterTypes();
}

qDebug() << endl << "Properties:";

qDebug() << "propertyCount() =" << metaObject->propertyCount();
qDebug() << "propertyOffset() =" << metaObject->propertyOffset();

count = metaObject->propertyCount();
for (int i = 0; i < count; i++) {
    QMetaProperty property = metaObject->property(i);
    qDebug() << "i =" << i;
    qDebug() << property.name();
    qDebug() << property.typeName();
}

リスト6のクラス宣言に対しての実行結果はリスト7のようになります。

リスト6 クラス宣言
    Q_ENUMS( Enum1 )
    ...
    Q_PROPERTY( int prop1 READ prop1 WRITE setProp1 )
    ...
public slots:
    void setProp1( int value );
    ...
signals:    
    ...
    void sigC( const QString& );
    ...
リスト7 メタ情報へのアクセス結果
Enumerators:
enumeratorCount() = 3 
enumeratorOffset() = 0 
==== i = 0 
Enum1 
         Enum1A 
         Enum1B 
         Enum1C 
...

Methods:
methodCount() = 16 
methodOffset() = 4 
i = 0 
destroyed(QObject*) ,  
("") 
...
==== i = 6 
sigC(QString) ,  
("") 
("QString") 
==== i = 7 
setProp1(int) ,  
("value") 
("int") 
...

Properties:
propertyCount() = 7 
propertyOffset() = 1 
i = 0 
objectName 
QString 
i = 1 
prop1 
int 
...

アクセス結果にシグナルとスロットのメソッドがあることに注目しましょう。シグナルとスロットのconnect()での接続時には、このメタ情報によってオブジェクト間を接続し、その情報をオブジェクトに設定しています。そして、シグナルを送信するというのは、シグナルメソッドが呼び出され(スレッド間の場合にはイベントループを介します⁠⁠、接続先のオブジェクトのスロットを呼び出すことなのです。

QMetaObject::invokeMethod()は、シグナルとスロットを呼び出すために公開されているAPIです。

QMetaObject::invokeMethod(slider, "valueChanged", Q_ARG(int, value));

このようにするとQSliderのシグナルvalueChanged(int)が送信され、接続されたスロットでシグナルが受け取られます。強制的にシグナルを送信しているので、sliderの実際の値とは異なる値でシグナルが送信されることに注意しましょう。同様にしてスロットの呼び出しもできます。

まとめと次回の予告

今回はQtのアーキテクチャの基礎となっているオブジェクトモデルについて説明しました。次回は、Qtの基本プログラミングの最後として、ウィジェットを配置するレイアウトマネージメントを普通とは違う視点で説明します。

おすすめ記事

記事・ニュース一覧