このページでは

Qt Quick コントロール - チャットチュートリアル

このチュートリアルでは、Qt Quick コントロールを使用して基本的なチャットアプリケーションを作成する方法を紹介します。また、SQLデータベースをQtアプリケーションに統合する方法についても説明します。

第1章:環境設定

新しいプロジェクトを設定する際は、 Qt Creatorを使用するのが最も簡単です。このプロジェクトでは、「Qt Quick 」アプリケーションテンプレートを選択しました。これにより、以下のファイルを含む基本的な「Hello World」アプリケーションが作成されます:

  • CMakeLists.txt - CMakeに対して、プロジェクトのビルド方法を指定します
  • Main.qml - 空のウィンドウを含むデフォルトのUIを提供します
  • main.cpp - 読み込みmain.qml
  • qtquickcontrols2.conf - アプリケーションに使用するスタイルを指定します

main.cpp

main.cpp 内のデフォルトのコードには、2つのインクルードがあります。

#include <QGuiApplication>
#include <QQmlApplicationEngine>

1つ目は、QGuiApplication へのアクセス権を与えるものです。すべてのQtアプリケーションにはアプリケーションオブジェクトが必要ですが、その具体的な型はアプリケーションの動作によって異なります。非グラフィカルアプリケーションの場合は、QCoreApplication で十分です。QGuiApplication は、 Qt Widgets、一方、QApplication は、QGraphicsViewを使用するグラフィカルアプリケーションには必須です。

2番目のインクルードによりQQmlApplicationEngine が利用可能になり、QML をロードできるようになります。

main() 内で、アプリケーションオブジェクトと QML エンジンを設定します。

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.loadFromModule("chattutorial", "Main");

    return app.exec();
}

QQmlApplicationEngine はQQmlEngine をラップした便利なラッパーであり、loadFromModule 関数を通じてアプリケーション用の QML を簡単に読み込めるようにします。また、ファイル選択機能の利用も容易になります。

C++での設定が完了したら、QMLでのユーザーインターフェースの作成に進むことができます。

Main.qml

デフォルトのQMLコードを、必要に応じて修正しましょう。

import QtQuick
import QtQuick.Controls

すでに Qt Quick モジュールがすでにインポートされていることに気づくでしょう。これにより、Item 、Rectangle 、Text などのグラフィカルプリミティブにアクセスできるようになります。型の完全なリストについては、 Qt Quick QML Types ドキュメントを参照してください。

Qt Quick Controls モジュールのインポートを追加してください。これにより、とりわけApplicationWindow へのアクセスが可能になります。これは、既存のルート型であるWindow を置き換えるものです:

ApplicationWindow {
    width: 540
    height: 960
    visible: true
    ...
}

ApplicationWindow は、header およびfooter を作成するための利便性を追加したWindow です。また、popups の基盤を提供し、背景色color などの基本的なスタイル設定もサポートしています。

ApplicationWindow を使用する際、ほぼ必ず設定されるプロパティが3つあります。それは、width 、height 、およびvisible です。これらを設定すれば、適切なサイズで、コンテンツを配置する準備が整った空のウィンドウが得られます。

注: デフォルトのコードにあった` title ` プロパティは 削除されています。

アプリケーションの最初の「画面」は、連絡先の一覧になります。各画面の上部に、その画面の目的を説明するテキストを表示すると良いでしょう。この状況では、ApplicationWindow のheaderおよびfooterプロパティが役立ちます。これらには、アプリケーションのすべての画面に表示すべき項目に最適な、いくつかの特徴があります:

  • それぞれウィンドウの上部と下部に固定されます。
  • ウィンドウの幅いっぱいに表示されます。

しかし、ヘッダーやフッターの内容が、ユーザーが閲覧している画面によって異なる場合は、Page を使用する方がはるかに簡単です。ここではとりあえず 1 つのページを追加しますが、次の章では、複数のページ間を移動する方法について解説します。

    Page {
        anchors.fill: parent
        header: Label {
            padding: 10
            text: qsTr("Contacts")
            font.pixelSize: 20
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
        }
    }

まず、anchors.fill プロパティを使用して、ウィンドウの全領域を占めるようにサイズ設定されたPageを追加します。

次に、そのheader プロパティにLabel を割り当てます。Labelは、Qt Quick モジュールに含まれるプリミティブなText アイテムを拡張し、スタイリングと font 継承を追加したものです。つまり、Labelは使用されているスタイルに応じて外観が変わり、そのピクセルサイズを子要素に継承させることもできます。

アプリケーションウィンドウの上端とテキストの間に少し間隔を空けたいので、padding プロパティを設定します。これにより、ラベルの両側(境界内)に余分なスペースが確保されます。代わりに、topPadding やbottomPadding プロパティを明示的に設定することも可能です。

ラベルのテキストは、qsTr() 関数を使用して設定しています。これにより、Qtの翻訳システムによってテキストが翻訳可能になります。これは、アプリケーションのエンドユーザーに表示されるテキストについては、従うべきベストプラクティスです。

デフォルトでは、テキストは境界の上端に縦方向で揃えられますが、水平方向の配置はテキストの自然な読み方向によって決まります。たとえば、左から右に読むテキストは左揃えになります。 これらのデフォルト設定を使用すると、テキストはウィンドウの左上隅に配置されてしまいます。これはヘッダーとしては望ましくないため、テキストを境界の中心に、水平方向および垂直方向の両方で揃えます。

プロジェクトファイル

CMakeLists.txt ファイルには、プロジェクトを実行可能な実行ファイルにビルドするためにCMakeが必要とするすべての情報が含まれています。

このファイルの詳細な説明については、「QMLアプリケーションのビルド」を参照してください。

現在、このアプリケーションを実行すると、次のような画面が表示されます:

「連絡先」という見出しが付いた空のアプリケーションウィンドウ

第2章:リスト

この章では、ListView およびItemDelegate を使用して、対話型の項目のリストを作成する方法について説明します。

ListView Qt Quick は モジュールに属し、モデルから取得した項目のリストを表示します。 は Controlsモジュールに属し、 や などのビューやコントロールで使用するための標準的なビューアイテムを提供します。例えば、各 はテキストを表示したり、チェックのオン/オフを切り替えたり、マウスクリックに反応したりすることができます。ItemDelegate Qt Quick ListView ComboBox ItemDelegate

以下は、ListView の例です:

        ...

        ListView {
            id: listView
            anchors.fill: parent
            topMargin: 48
            leftMargin: 48
            bottomMargin: 48
            rightMargin: 48
            spacing: 20
            model: ["Albert Einstein", "Ernest Hemingway", "Hans Gude"]
            delegate: ItemDelegate {
                id: contactDelegate
                text: modelData
                width: listView.width - listView.leftMargin - listView.rightMargin
                height: avatar.implicitHeight
                leftPadding: avatar.implicitWidth + 32

                required property string modelData

                Image {
                    id: avatar
                    source: "images/" + contactDelegate.modelData.replace(" ", "_") + ".png"
                }
            }
        }
        ...

サイズと配置

まず最初に行うのは、ビューのサイズを設定することです。ビューはページ上の利用可能なスペースをすべて埋めるようにするため、anchors.fill を使用します。なお、Pageクラスはヘッダーとフッターに十分なスペースを確保しているため、この場合、ビューは例えばヘッダーの下に配置されます。

次に、ListView の周囲にmargins を設定し、ウィンドウの端との間に一定の距離を確保します。marginプロパティはビューの境界内でのスペースを確保するものであり、これにより、空いている領域はユーザーが「フリック」操作を行うことが可能です。

アイテムはビュー内で適度に間隔を空けて配置されるように、spacing プロパティは20 に設定します。

モデル

ビューに素早くアイテムを配置するために、モデルとして JavaScript の配列を使用しています。QML の最大の強みの 1 つは、アプリケーションのプロトタイピングを極めて迅速に行える点であり、これはその一例です。 また、必要なアイテムの数を指定するために、モデルプロパティに単に数値を割り当てることも可能です。例えば、model プロパティに10 を割り当てると、各アイテムの表示テキストは0 から9 までの番号になります。

しかし、アプリケーションがプロトタイプ段階を過ぎると、すぐに実際のデータを使用する必要が生じます。そのためには、subclassing QAbstractItemModel による適切な C++ モデルを使用するのが最善です。

デリゲート

delegate について説明します。モデルからの対応するテキストを、ItemDelegate のtext プロパティに割り当てます。モデルからのデータが各デリゲートに提供される正確な方法は、使用するモデルの種類によって異なります。詳細については、Qt Quick の「Models and Views」を参照してください。

このアプリケーションでは、ビュー内の各項目の幅をビューの幅と同じにする必要があります。これにより、ユーザーがリストから連絡先を選択するための十分なスペースが確保されます。これは、携帯電話のようなタッチスクリーンの小さいデバイスでは重要な要素となります。 ただし、ビューの幅には48 ピクセルの余白が含まれているため、widthプロパティへの割り当てではこれを考慮する必要があります。

次に、Image を定義します。これにより、ユーザーの連絡先の画像が表示されます。画像の幅は40 ピクセル、高さは40 ピクセルです。縦方向の余白が生じないように、デリゲートの高さを画像の高さに合わせて設定します。

アバター付きの3件の連絡先が表示された連絡先リスト

第3章:ナビゲーション

この章では、StackView を使用してアプリケーション内のページ間を移動する方法について学びます。以下は、修正後のmain.qml です:

import QtQuick.Controls

ApplicationWindow {
    id: window
    width: 540
    height: 960
    visible: true

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: ContactPage {}
    }
}

その名前が示す通り、StackView はスタックベースのナビゲーションを提供します。スタックに「プッシュ」された最後の項目が最初に取り出され、常に最上位の項目が表示されます。

Page の場合と同様に、StackView にアプリケーションウィンドウ全体を埋めるよう指示します。その後、initialItem を通じて表示するアイテムを渡すだけです。StackView は、items 、components 、URLs を受け付けます。

連絡先リストのコードを `ContactPage.qml` に移動したことに気づくでしょう。アプリケーションにどの画面が含まれるか大まかな見当がついたら、早めにこの作業を行うことをお勧めします。そうすることで、コードの可読性が向上するだけでなく、特定のコンポーネントからアイテムがインスタンス化されるのは本当に必要な場合のみとなり、メモリ使用量を削減できます。

注: Qt Creator には 、QML用の便利なクイックフィックスがいくつか用意されており、その一つとして、コードブロックを別のファイルに移動する機能があります(Alt + Enter > Move Component into Separate File )。

ListView を使用する際に考慮すべきもう1つの点は、id で参照するか、それともアタッチドプロパティListView.view を使用するかです。最適なアプローチは、いくつかの要因によって異なります。ビューにIDを付与すると、バインディング式がより短く効率的になります。これは、アタッチドプロパティのオーバーヘッドが非常に少ないためです。 しかし、そのデリゲートを他のビューでも再利用する予定がある場合は、デリゲートを特定のビューに縛り付けないよう、アタッチドプロパティを使用する方が望ましいでしょう。たとえば、アタッチドプロパティを使用すると、デリゲート内の `width ` という代入は次のようになります。

width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin

第2章では、ヘッダーの下にListView を追加しました。その章のアプリケーションを実行すると、ビューの内容がヘッダーの上をスクロールして表示されることがわかります:

これはあまり見栄えが良くありません。特に、デリゲート内のテキストがヘッダーのテキストに届くほど長い場合はなおさらです。理想としては、ヘッダーテキストの下、かつビューの上部に、単色のブロックを表示させたいところです。これにより、リストビューの内容がヘッダーの内容と視覚的に干渉することを防げます。 なお、ビューのclip プロパティをtrue に設定することでこれを実現することも可能ですが、そうするとcan affect performance 。

ToolBar が最適なツールです。これは、ナビゲーションボタンや検索フィールドなど、アプリケーション全体およびコンテキストに応じたアクションやコントロールを収めるコンテナです。何より、背景色はいつものようにアプリケーションスタイルから取得されます。実際の動作は以下の通りです:

    header: ToolBar {
        Label {
            text: qsTr("Contacts")
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

このコンテナ自体にはレイアウトがないため、ラベルを中央に配置するのは私たち自身で行います。

残りのコードは第2章と同じですが、clicked シグナルを利用して次のページをスタックビューにプッシュしている点が異なります:

            onClicked: root.StackView.view.push("ConversationPage.qml", { inConversationWith: modelData })

`Component ` や `url ` を `StackView` にプッシュする際、(最終的に) インスタンス化されるアイテムをいくつかの変数で初期化する必要があることがよくあります。StackView の `push()` 関数は、2番目の引数として JavaScript オブジェクトを受け取ることで、この処理に対応しています。 これを利用して、次のページに連絡先の名前を渡し、そのページではその情報を使って関連する会話を表示します。root.StackView.view.push という構文に注意してください。これは、添付プロパティの動作の都合上、必要なものです。

ConversationPage.qml を順を追って見ていきましょう。まずはインポートから始めます:

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

これらは以前のものと同じですが、QtQuick.Layouts のインポートが追加されています。これについては後ほど説明します。

Page {
    id: root

    property string inConversationWith

    header: ToolBar {
        ToolButton {
            text: qsTr("Back")
            anchors.left: parent.left
            anchors.leftMargin: 10
            anchors.verticalCenter: parent.verticalCenter
            onClicked: root.StackView.view.pop()
        }

        Label {
            id: pageTitle
            text: root.inConversationWith
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }
    ...

このコンポーネントのルートアイテムは別の Page であり、inConversationWith というカスタムプロパティを持っています。現時点では、このプロパティはヘッダーのラベルに何を表示するかを決定するだけです。後で、会話内のメッセージ一覧を格納する SQL クエリでこれを使用することになります。

ユーザーが「連絡先」ページに戻れるようにするため、クリック時に `pop()` を呼び出す `ToolButton ` を追加します。`ToolButton ` は機能的には `Button` と似ていますが、ToolBar 内でより適した外観を提供します。

QMLでは、アイテムのレイアウトには「Item Positioners」と「Qt Quick 」の2つの方法があります。 アイテムポジショナー(Row 、Column など)は、アイテムのサイズが既知または固定されており、特定の配置で整然と配置するだけでよい場合に役立ちます。Qt Quick のレイアウトは、アイテムの位置決めとサイズ変更の両方が可能なため、サイズ変更可能なユーザーインターフェースに最適です。以下では、ColumnLayout を使用して、ListView とPane を縦方向に配置します:

    ColumnLayout {
        anchors.fill: parent

        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            ...

        }
        ...

        Pane {
            id: pane
            Layout.fillWidth: true
            ...
    }

Paneは基本的に、アプリケーションのスタイルに基づいて色が決定される長方形です。Frame と似ていますが、境界線にストロークがない点が唯一の違いです。

レイアウトの直接の子要素であるアイテムには、さまざまなattached properties が利用可能です。ListView では、Layout.fillWidth およびLayout.fillHeight を使用し、ColumnLayout 内で可能な限り多くのスペースを確保するようにしています。Pane についても同様の処理を行っています。ColumnLayout は垂直レイアウトであるため、各子要素の左右には他の要素が存在せず、その結果、各要素がレイアウトの幅全体を占めることになります。

一方、ListView 内のLayout.fillHeight という記述により、Paneを配置した後に残ったスペースをこの要素が占有できるようになります。

リストビューを詳しく見てみましょう:

        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            Layout.margins: pane.leftPadding + messageField.leftPadding
            displayMarginBeginning: 40
            displayMarginEnd: 40
            verticalLayoutDirection: ListView.BottomToTop
            spacing: 12
            model: 10
            delegate: Row {
                id: messageDelegate
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                required property int index
                readonly property bool sentByMe: index % 2 == 0

                Rectangle {
                    id: avatar
                    width: height
                    height: parent.height
                    color: "grey"
                    visible: !messageDelegate.sentByMe
                }

                Rectangle {
                    width: 80
                    height: 40
                    color: messageDelegate.sentByMe ? "lightgrey" : "steelblue"

                    Label {
                        anchors.centerIn: parent
                        text: messageDelegate.index
                        color: messageDelegate.sentByMe ? "black" : "white"
                    }
                }
            }

            ScrollBar.vertical: ScrollBar {}
        }

親要素の幅と高さに合わせてから、ビューにマージンも設定しています。これにより、「メッセージ作成」フィールドのプレースホルダーテキストときれいに揃うようになります:

リスト表示の余白を、その下にあるメッセージ作成フィールドに合わせて配置する

次に、displayMarginBeginning とdisplayMarginEnd を設定します。これらのプロパティを設定することで、ビューの端でスクロールしても、ビューの境界外にあるデリゲートが消えてしまうのを防ぐことができます。この仕組みを理解するには、これらのプロパティをコメントアウトして、ビューをスクロールしたときに何が起こるかを確認するのが最もわかりやすいでしょう。

その後、ビューの垂直方向を反転させ、最初の項目が下部にくるようにします。デリゲート間の間隔は12ピクセルに設定し、第4章で実際のモデルを実装するまでの間、テスト用に「ダミー」モデルが割り当てられます。

デリゲート内では、上の画像に示すように、アバターの後にメッセージの内容が続くようにするため、Row をルートアイテムとして宣言します。

ユーザーが送信したメッセージと、連絡先が送信したメッセージを区別する必要があります。現時点では、ダミーのプロパティ `sentByMe` を設定しています。これは、デリゲートのインデックスを使用して、送信者を切り替えるだけのものです。このプロパティを使用して、以下の3つの方法で送信者を区別します:

  • ユーザーが送信したメッセージは、anchors.right をlistView.contentItem.right に設定することで、画面の右側に揃えます。
  • アバター(現時点では単なる Rectangle です)のvisible プロパティをsentByMe に基づいて設定することで、メッセージが連絡先から送信された場合にのみアバターを表示します。
  • 送信者に応じて矩形の色を変更します。暗い背景に暗い色のテキストを表示したり、その逆になったりするのは避けたいので、送信者に応じてテキストの色も設定します。第5章では、このような処理をスタイル設定がどのように処理してくれるかについて見ていきます。

画面の下部には、複数行のテキスト入力ができるようにTextArea を配置し、メッセージを送信するためのボタンも配置します。リストビューの内容がページヘッダーと重ならないようにToolBar を使用するのと同じように、これら2つの項目の下の領域を覆うためにPaneを使用します:

        Pane {
            id: pane
            Layout.fillWidth: true
            Layout.fillHeight: false

            RowLayout {
                width: parent.width

                TextArea {
                    id: messageField
                    Layout.fillWidth: true
                    placeholderText: qsTr("Compose message")
                    wrapMode: TextArea.Wrap
                }

                Button {
                    id: sendButton
                    text: qsTr("Send")
                    enabled: messageField.length > 0
                    Layout.fillWidth: false
                }
            }
        }

TextArea は、画面の利用可能な幅いっぱいに表示されるようにします。ユーザーが入力を開始すべき場所を視覚的に示すため、プレースホルダーテキストを設定します。入力領域内のテキストは、画面からはみ出さないよう改行処理が行われます。

最後に、実際に送信すべきメッセージがある場合にのみ、ボタンが有効になります。

第4章:モデル

第4章では、C++で読み取り専用および読み書き可能なSQLモデルを作成し、それらをQMLに公開してビューにデータを反映させる手順について解説します。

QSqlQueryModel

チュートリアルを簡潔にするため、ユーザーの連絡先リストは編集不可にすることにしました。QSqlQueryModelは、SQLの結果セットに対して読み取り専用のデータモデルを提供するため、この目的には最適な選択肢です。

QSqlQueryModel を継承するSqlContactModel クラスを確認してみましょう:

#include <QQmlEngine>
#include <QSqlQueryModel>

class SqlContactModel : public QSqlQueryModel
{
    Q_OBJECT
    QML_ELEMENT

public:
    SqlContactModel(QObject *parent = nullptr);
};

ここには特に複雑な処理はないので、.cpp ファイルに進みましょう:

#include "sqlcontactmodel.h"

#include <QDebug>
#include <QSqlError>
#include <QSqlQuery>

static voidcreateTable()
{
    if(QSqlDatabase::database().tables().contains(QStringLiteral("Contacts"))) {
        // テーブルはすでに存在するため、何もする必要はありません。
        return;
    }

    QSqlQuery query;
   if(!query.exec(
        "CREATE TABLE IF NOT EXISTS 'Contacts' ("
        "   'name' TEXT NOT NULL,"
        "   PRIMARY KEY(name)"
        ")")) {
        qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
    }

    query.exec("INSERT INTO Contacts VALUES('Albert Einstein')");
    query.exec("INSERT INTO Contacts VALUES('Ernest Hemingway')");
    query.exec("INSERT INTO Contacts VALUES('Hans Gude')");
}

クラスのヘッダーファイルと、Qt XML から必要なヘッダーファイルをインクルードします。次に、createTable() という名前の静的関数を定義します。この関数を使用して、SQL テーブルがまだ存在しない場合は作成し、その後、ダミーの連絡先データをいくつか挿入します。

database() の呼び出しは、まだ特定のデータベースを設定していないため、少し分かりにくいかもしれません。この関数に接続名が渡されない場合、関数は「デフォルトの接続」を返します。その作成方法については、後ほど説明します。

SqlContactModel::SqlContactModel(QObject*parent) :
    QSqlQueryModel(parent)
{
    createTable();

    QSqlQuery query;
    if(!query.exec("SELECT * FROM Contacts"))
        qFatal("Contacts SELECT query failed: %s", qPrintable(query.lastError().text()));

    setQuery(std::move(query));
    if(lastError().isValid())
        qFatal("Cannot set query on SqlContactModel: %s", qPrintable(lastError().text()));
}

コンストラクタ内で、createTable() を呼び出します。その後、モデルにデータを格納するために使用するクエリを構築します。この例では、Contacts テーブルのすべての行を取得します。

QSqlTableModel

SqlConversationModel はより複雑です:

#include <QQmlEngine>
#include <QSqlTableModel>

class SqlConversationModel : public QSqlTableModel
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(QString recipient READ recipient WRITE setRecipient NOTIFY recipientChanged)

public:
    SqlConversationModel(QObject *parent = nullptr);

    QString recipient() const;
    void setRecipient(const QString &recipient);

    QVariant data(const QModelIndex &index, int role) const override;
    QHash<int, QByteArray> roleNames() const override;

    Q_INVOKABLE void sendMessage(const QString &recipient, const QString &message);

signals:
    void recipientChanged();

private:
    QString m_recipient;
};

Q_PROPERTY とQ_INVOKABLE の両方のマクロを使用しているため、Q_OBJECT マクロを使用してmocにその旨を通知する必要があります。

recipient プロパティは、QML側から設定され、モデルに対してどの会話のメッセージを取得すべきかを通知します。

QMLで独自のロールを使用できるようにするため、data() およびroleNames() 関数をオーバーライドします。

また、QML から呼び出したい `sendMessage() ` 関数を定義するため、`Q_INVOKABLE ` マクロを使用しています。

.cpp ファイルを見てみましょう:

#include "sqlconversationmodel.h"

#include <QDateTime>
#include <QDebug>
#include <QSqlError>
#include <QSqlRecord>
#include <QSqlQuery>

static const char *conversationsTableName = "Conversations";

static voidcreateTable()
{
    if(QSqlDatabase::database().tables().contains(conversationsTableName)) {
        // テーブルはすでに存在するため、何もする必要はありません。
        return;
    }

    QSqlQuery query;
    if(!query.exec(
        "CREATE TABLE IF NOT EXISTS 'Conversations' ("
        "'author' TEXT NOT NULL,"
        "'recipient' TEXT NOT NULL,"
       "'timestamp' TEXT NOT NULL,"
        "'message' TEXT NOT NULL,"
        "FOREIGN KEY('author') REFERENCES Contacts ( name ),"
       "FOREIGN KEY('recipient') REFERENCES Contacts ( name )"
        ")")) {
        qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
    }

    query.exec("INSERT INTO Conversations VALUES('Me', 'Ernest Hemingway', '2016-01-07T14:36:06', 'Hello!')");
    query.exec("INSERT INTO Conversations VALUES('アーネスト・ヘミングウェイ', '私', '2016-01-07T14:36:16', 'こんにちは。')");
    query.exec("INSERT INTO Conversations VALUES('Me', 'Albert Einstein', '2016-01-01T11:24:53', 'Hi!')");
    query.exec("INSERT INTO Conversations VALUES('Albert Einstein', 'Me', '2016-01-07T14:36:16', 'Good morning.')");
    query.exec("INSERT INTO Conversations VALUES('ハンス・グード', '私', '2015-11-20T06:30:02', 'おはよう。私の絵は届きましたか?')");
    query.exec("INSERT INTO Conversations VALUES('Me', 'Hans Gude', '2015-11-20T08:21:03', 'おはよう、ハンス。ええ、とても素敵ですね。 本当にありがとう! "
               "それには何時間かかったの?')");
}

これは `sqlcontactmodel.cpp` と非常に似ていますが、ここでは `Conversations ` テーブルを操作している点が異なります。また、ファイル内の数か所で `conversationsTableName ` を使用するため、これを静的な定数変数として定義しています。

SqlConversationModel::SqlConversationModel(QObject *parent) :
    QSqlTableModel(parent)
{
    createTable();
    setTable(conversationsTableName);
    setSort(2, Qt::DescendingOrder);
    // Ensures that the model is sorted correctly after submitting a new row.
    setEditStrategy(QSqlTableModel::OnManualSubmit);
}

SqlContactModel と同様に、コンストラクタで最初に行うのはテーブルの作成です。setTable() 関数を通じて、QSqlTableModel に使用するテーブル名を指定します。会話内の最新のメッセージが最初に表示されるようにするため、クエリ結果をtimestamp フィールドで降順にソートします。これは、ListView のverticalLayoutDirection プロパティをListView.BottomToTop に設定することと密接に関連しています(これについては第3章で解説しました)。

QString SqlConversationModel::recipient() const
{
    return m_recipient;
}

void SqlConversationModel::setRecipient(const QString &recipient)
{
    if (recipient == m_recipient)
        return;

    m_recipient = recipient;

    const QString filterString = QString::fromLatin1(
        "(recipient = '%1' AND author = 'Me') OR (recipient = 'Me' AND author='%1')").arg(m_recipient);
    setFilter(filterString);
    select();

    emit recipientChanged();
}

setRecipient() では、データベースから返された結果に対してフィルタを設定します。

QVariant SqlConversationModel::data(const QModelIndex &index, int role) const
{
    if (role < Qt::UserRole)
        return QSqlTableModel::data(index, role);

    const QSqlRecord sqlRecord = record(index.row());
    return sqlRecord.value(role - Qt::UserRole);
}

data() 関数は、ロールがカスタムユーザーロールでない場合、QSqlTableModel の実装にフォールバックします。ロールがユーザーロールである場合は、Qt::UserRole をそのロールから差し引いてそのフィールドのインデックスを取得し、それを用いて返す必要のある値を特定することができます。

QHash<int, QByteArray> SqlConversationModel::roleNames() const
{
    QHash<int, QByteArray> names;
    names[Qt::UserRole] = "author";
    names[Qt::UserRole + 1] = "recipient";
    names[Qt::UserRole + 2] = "timestamp";
    names[Qt::UserRole + 3] = "message";
    return names;
}

roleNames() では、カスタムロールの値とロール名の対応関係を返します。これにより、QML でこれらのロールを使用できるようになります。すべてのロール値を保持するための列挙型を宣言しておくと便利ですが、この関数以外のコードでは特定の値を参照することはないため、ここでは宣言しません。

voidSqlConversationModel::sendMessage(constQString&recipient, constQString&message)
{
    constQString timestamp=QDateTime::currentDateTime().toString(Qt::ISODate);

    QSqlRecord newRecord=record();
    newRecord.setValue("author", "Me");
    newRecord.setValue("recipient",recipient);
    newRecord.setValue("timestamp",timestamp);
    newRecord.setValue("message",message);
   if(!insertRecord(rowCount(),newRecord)) {
        qWarning() << "Failed to send message:" << lastError().text();
       return;
    }

sendMessage() 関数は、指定されたrecipient とmessage を使用して、データベースに新しいレコードを挿入します。QSqlTableModel::OnManualSubmit を使用しているため、submitAll()を手動で呼び出す必要があります。

データベースへの接続とQMLでの型の登録

モデルクラスの定義が完了したので、main.cpp を見てみましょう:

#include <QtCore>
#include <QGuiApplication>
#include <QSqlDatabase>
#include <QSqlError>
#include <QtQml>

static voidconnectToDatabase()
{
    QSqlDatabase database=QSqlDatabase::database();
    if(!database.isValid()) {
        database=QSqlDatabase::addDatabase("QSQLITE");
        if(!database.isValid())
            qFatal("Cannot add database: %s", qPrintable(database.lastError().text()));
    }

    constQDir writeDir=QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    if(!writeDir.mkpath("."))
        qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath()));

   // すべてのデバイスに書き込み可能な場所があることを確認する。
    constQString fileName=writeDir.absolutePath()+ "/chat-database.sqlite3";
   // SQLite ドライバを使用する場合、open() は SQLite データベースが存在しない場合にそれを作成します。
   database.setDatabaseName(fileName);
    if(!database.open()) {
        qFatal("Cannot open database: %s", qPrintable(database.lastError().text()));
        QFile::remove(fileName);
    }
}

intmain(intargc, char *argv[])
{
    QGuiApplication app(argc,argv);

    connectToDatabase();

    QQmlApplicationEngine engine;
    engine.loadFromModule("chattutorial", "Main");
    if(engine.rootObjects().isEmpty())
        return-1;

    returnapp.exec();
}

connectToDatabase() SQLiteデータベースへの接続を確立し、ファイルが存在しない場合は実際にファイルを作成します。

main() 内では、qmlRegisterType()を呼び出し、QML内でモデルを型として登録します。

QMLでのモデルの使用

モデルが QML タイプとして利用可能になったため、ContactPage.qml に若干の変更を加える必要があります。これらのタイプを使用するには、まずmain.cpp で設定した URI を使用して、それらをインポートする必要があります:

import chattutorial

次に、ダミーのモデルを適切なモデルに置き換えます:

        model: SqlContactModel {}

デリゲート内では、モデルデータにアクセスするために別の構文を使用します:

            text: model.display

ConversationPage.qml では、同じchattutorial のインポートを追加し、ダミーモデルを置き換えます:

            model: SqlConversationModel {
                recipient: root.inConversationWith
            }

モデル内では、recipient プロパティを、ページが表示されている連絡先の名前に設定します。

各メッセージの下に表示したいタイムスタンプに対応するため、ルートデリゲート項目を「Row」から「Column」に変更します:

            delegate: Column {
                id: conversationDelegate
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                required property string author
                required property string recipient
                required property date timestamp
                required property string message
                readonly property bool sentByMe: recipient !== "Me"

                Row {
                    id: messageRow
                    spacing: 6
                    anchors.right: conversationDelegate.sentByMe ? parent.right : undefined

                    Image {
                        id: avatar
                        source: !conversationDelegate.sentByMe
                            ? "images/" + conversationDelegate.author.replace(" ", "_") + ".png" : ""
                    }

                    Rectangle {
                        width: Math.min(messageText.implicitWidth + 24,
                            listView.width - (!conversationDelegate.sentByMe ? avatar.width + messageRow.spacing : 0))
                        height: messageText.implicitHeight + 24
                        color: conversationDelegate.sentByMe ? "lightgrey" : "steelblue"

                        Label {
                            id: messageText
                            text: conversationDelegate.message
                            color: conversationDelegate.sentByMe ? "black" : "white"
                            anchors.fill: parent
                            anchors.margins: 12
                            wrapMode: Label.Wrap
                        }
                    }
                }

                Label {
                    id: timestampText
                    text: Qt.formatDateTime(conversationDelegate.timestamp, "d MMM hh:mm")
                    color: "lightgrey"
                    anchors.right: conversationDelegate.sentByMe ? parent.right : undefined
                }
            }

メッセージ本文の下にタイムスタンプが表示されるチャットメッセージ

適切なモデルが準備できたので、sentByMe プロパティの式内で、そのrecipient ロールを使用できるようになりました。

アバターに使用されていたRectangleはImageに変換されました。画像には固有の暗黙的なサイズがあるため、明示的に指定する必要はありません。以前と同様に、作成者がユーザー本人でない場合にのみアバターを表示しますが、今回はvisible プロパティを使用する代わりに、画像のsource を空のURLに設定しています。

各メッセージの背景は、テキストよりも両端を12ピクセルずつ広げたいと考えています。 ただし、メッセージが長すぎる場合は、その幅をリストビューの端まで制限したいので、Math.min() を使用しています。メッセージが自分から送信されたものではない場合、その前に必ずアバターが表示されるため、アバターの幅と行間を差し引くことで、その分を考慮しています。

例えば、上の画像では、メッセージテキストの暗黙的な幅が小さい方の値となっています。一方、下の画像ではメッセージテキストがかなり長いため、小さい方の値(ビューの幅)が選択され、テキストが画面の反対側の端で止まるようになっています:

長いメッセージのテキストを、表示領域の幅に合わせて折り返す

前述したように、各メッセージのタイムスタンプを表示するために、Labelを使用しています。日付と時刻は、Qt.formatDateTime() を使用して、カスタムフォーマットでフォーマットされています。

次に、「送信」ボタンがクリックされた際の動作を実装する必要があります:

                Button {
                    id: sendButton
                    text: qsTr("Send")
                    enabled: messageField.length > 0
                    Layout.fillWidth: false
                    onClicked: {
                        listView.model.sendMessage(root.inConversationWith, messageField.text)
                        messageField.text = ""
                    }
                }

まず、モデルの呼び出し可能な関数 `sendMessage() ()` を呼び出し、Conversations データベーステーブルに新しい行を挿入します。次に、テキストフィールドをクリアして、今後の入力に備えます。

第5章:スタイル設定

Qt Quick コントロールのスタイルは、どのプラットフォームでも動作するように設計されています。この章では、Basic、Material、Universalの各スタイルでアプリケーションを実行した際に見栄えが良くなるよう、外観に若干の調整を加えます。

これまで、アプリケーションは「Basic」スタイルでのみテストしてきました。例えば、「Material」スタイルで実行すると、すぐにいくつかの問題が見つかります。以下は「連絡先」ページです:

Materialスタイルの「連絡先」ページで、テキストのコントラストが不十分

ヘッダーのテキストが濃い青の背景に黒で表示されており、非常に読みにくくなっています。同じ現象は「会話」ページでも発生しています:

Materialスタイルの会話ページで、テキストのコントラストが不十分である

解決策は、ツールバーに「Dark」テーマを使用するよう指定し、その設定が子要素に反映されるようにして、子要素のテキスト色を明るい色に変更できるようにすることです。これを行う最も簡単な方法は、Materialスタイルを直接インポートし、Materialの添付プロパティを使用することです:

import QtQuick.Controls.Material 2.12

// ...

header: ToolBar {
    Material.theme: Material.Dark

    // ...
}

ただし、これにはMaterialスタイルへの強固な依存関係が生じます。ターゲットデバイスがMaterialスタイルを使用していない場合でも、Materialスタイルプラグインをアプリケーションとともにデプロイする必要があります。そうしないと、QMLエンジンがインポート先を見つけられなくなります。

その代わりに、Qt Quick Controlsが備えているスタイルベースのファイルセレクタ機能を利用するのが望ましいです。これを行うには、ToolBar を独立したファイルに移動する必要があります。 このファイルをChatToolBar.qml と名付けます。これはファイルの「デフォルト」バージョンとなり、つまり、Basic スタイル(スタイルが指定されていない場合に使用されるスタイル)が使用されている際に適用されます。新しいファイルの内容は次のとおりです:

import QtQuick.Controls

ToolBar {
}

このファイル内ではToolBar 型のみを使用しているため、Qt Quick Controlsのインポートだけで十分です。コード自体はContactPage.qml にあったものから変更されていませんが、これは正しい動作です。ファイルのデフォルト版については、変更する必要はありません。

ContactPage.qml に戻り、新しい型を使用するようにコードを更新します:

    header: ChatToolBar {
        Label {
            text: qsTr("Contacts")
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

次に、ツールバーの Material バージョンを追加する必要があります。ファイルセレクタは、ファイルのバリエーションが、そのファイルのデフォルトバージョンと同じディレクトリ内に、適切な名前が付けられたディレクトリに存在することを想定しています。つまり、ChatToolBar.qml が存在するディレクトリ(ルートフォルダ)に、「+Material」という名前のフォルダを追加する必要があります。 「+」は、選択機能が誤ってトリガーされないようにするため、QFileSelector で必須となっています。

以下に+Material/ChatToolBar.qml を示します:

import QtQuick.Controls
import QtQuick.Controls.Material

ToolBar {
    Material.theme: Material.Dark
}

ConversationPage.qml に対しても同様の変更を加えます:

    header: ChatToolBar {
        ToolButton {
            text: qsTr("Back")
            anchors.left: parent.left
            anchors.leftMargin: 10
            anchors.verticalCenter: parent.verticalCenter
            onClicked: root.StackView.view.pop()
        }

        Label {
            id: pageTitle
            text: root.inConversationWith
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

これで、両方のページが正しく表示されるようになりました:

「Material」スタイルを採用し、ヘッダーのコントラストを修正した「お問い合わせ」ページ

Materialスタイルが適用され、ヘッダーのコントラストが修正されたトークページ

ユニバーサルスタイルを試してみましょう:

ユニバーサルスタイルの「お問い合わせ」ページ

ユニバーサルスタイルのトークページ

問題はありません。このような比較的シンプルなアプリケーションの場合、スタイルを切り替える際に必要な調整はごくわずかです。

それでは、各スタイルのダークテーマを試してみましょう。「Basic」スタイルにはダークテーマがありません。これは、可能な限り高いパフォーマンスを実現するように設計されているため、ダークテーマを追加するとわずかなオーバーヘッドが生じるからです。まずは「Material」スタイルをテストしますので、qtquickcontrols2.conf に、ダークテーマを使用するように指定するエントリを追加してください:

[Material]
Primary=Indigo
Accent=Indigo
Theme=Dark

設定が完了したら、アプリケーションをビルドして実行してください。次のような画面が表示されるはずです:

ダークテーマの「マテリアル」スタイルを採用した「お問い合わせ」ページ

ダークテーマでMaterialスタイルが適用されたトークページ

どちらのページも問題なく表示されています。次に、Universal スタイルのエントリを追加しましょう:

[universal]
Theme=Dark

アプリケーションをビルドして実行すると、次のような結果が表示されるはずです:

ダークテーマのユニバーサルスタイルを採用した「お問い合わせ」ページ

ダークテーマのユニバーサルスタイルが適用されたトークページ

まとめ

このチュートリアルでは、Qt Quick コントロールを使用して基本的なアプリケーションを作成する以下の手順を解説しました:

  • Qt Creator を使用して新しいプロジェクトを作成する。
  • ApplicationWindow の基本的な設定を行う。
  • Page を使用してヘッダーとフッターを定義する。
  • ListView にコンテンツを表示する。
  • コンポーネントを個別のファイルにリファクタリングする。
  • StackView を使用した画面間の移動。
  • レイアウトを使用して、アプリケーションのサイズ変更をスムーズに行う。
  • SQL データベースをアプリケーションに統合する、読み取り専用および書き込み可能なカスタムモデルの両方を実装する。
  • Q_PROPERTY 、Q_INVOKABLE 、およびqmlRegisterType() を使用して、C++ を QML と統合する。
  • 複数のスタイルのテストと設定。

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.