Qt Quick Controls - チャットチュートリアル

このチュートリアルでは、Qt Quick Controls を使って基本的なチャットアプリケーションを書く方法を説明します。また、SQL データベースを Qt アプリケーションに統合する方法も説明します。

第1章 セットアップ

新しいプロジェクトをセットアップするときは、Qt Creator を使うのが一番簡単です。このプロジェクトでは、Qt Quick アプリケーションテンプレートを選びました。このテンプレートは、基本的な "Hello World" アプリケーションを以下のファイルとともに作成します:

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

main.cpp

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

#include <QGuiApplication>
#include <QQmlApplicationEngine>

1つ目はQGuiApplication にアクセスするためのものです。すべてのQtアプリケーションはアプリケーション・オブジェクトを必要としますが、正確なタイプはアプリケーションの動作に依存します。非グラフィカル・アプリケーションではQCoreApplication で十分です。Qtウィジェットを使用しないグラフィカル・アプリケーションではQGuiApplication で十分ですが、Qtウィジェットを使用するアプリケーションではQApplication が必要です。

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 は の便利なラッパーで、アプリケーションのQMLを簡単にロードするための 関数を提供します。また、QQmlEngine loadFromModule ファイルセレクタを利用するための便利な機能も追加されています。

C++でのセットアップが完了したら、次はQMLでのユーザーインターフェイスに移 りましょう。

Main.qml

デフォルトのQMLコードを私たちのニーズに合うように変更してみましょう。

import QtQuick
import QtQuick.Controls

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

Qt Quick Controls モジュールのインポートを追加します。これは特に、既存のルート型Window を置き換えるApplicationWindow へのアクセスを提供します:

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

ApplicationWindow は で、 と を作成するための利便性が追加されています。また、 の基盤も提供し、背景 などの基本的なスタイリングもサポートしています。Window header footer popups color

ApplicationWindow を使用する場合、ほとんど常に設定される3つのプロパティがあります:widthheightvisible 。これらを設定すると、適切なサイズの空のウィンドウができ、コンテンツで満たされる準備が整います。

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

このアプリケーションの最初の「スクリーン」は、連絡先のリストになります。各画面の上部に、その目的を説明するテキストがあると良いでしょう。このような状況では、ApplicationWindow のヘッダーとフッター・プロパティが有効です。これらのプロパティには、アプリケーションの各画面に表示されるべき項目に理想的な特徴があります:

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

しかし、ヘッダーとフッターの内容が、ユーザーがどの画面を見ているかによって異なる場合は、Page を使う方がはるかに簡単です。次の章では、複数のページの間を移動する方法を説明します。

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

まず、anchors.fill プロパティを使用して、ウィンドウ上のすべてのスペースを占める大きさのページを追加します。

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

アプリケーション・ウィンドウの上部とテキストの間に距離が欲しいので、padding プロパティを設定します。これにより、ラベルの両側(境界内)に余分なスペースが割り当てられます。代わりに、topPaddingbottomPadding プロパティを明示的に設定することもできます。

ラベルのテキストは、qsTr() 関数を使って設定します。これは、Qt の翻訳システムでテキストが翻訳できるようにするためです。これは、アプリケーションのエンドユーザーから見えるテキストに適用する良い方法です。

デフォルトでは、テキストはその境界の上端に垂直に配置され、水平方向の配置はテキストの自然な方向に依存します;例えば、左から右に読まれるテキストは左に配置されます。例えば、左から右に読まれるテキストは左に揃えられる。これらのデフォルトを使用すると、テキストはウィンドウの左上隅に配置される。これはヘッダーとしては望ましくないので、水平方向と垂直方向の両方で、テキストを境界の中心に揃える。

プロジェクトファイル

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

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

現在、私たちのアプリケーションを実行すると以下のようになります:

第2章 リスト

この章では、ListViewItemDelegate を使って、インタラクティブなアイテムのリストを作成する方法を説明します。

ListView ItemDelegate は Qt Quick Controls モジュールのもので、 や のようなビューやコントロールで使用するための標準ビューアイテムを提供します。例えば、 はそれぞれ、テキストを表示したり、オン・オフをチェックしたり、マウスのクリックに反応したりすることができます。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 を設定し、ウィンドウの端との間に距離を置きます。マージンプロパティは、ビューの境界内にスペースを確保します。これは、空の領域がまだユーザーによって「フリック」できることを意味します。

アイテムはビュー内できれいに間隔を空ける必要があるため、spacing プロパティは20 に設定されています。

モデル

ビューに素早くアイテムを入力するために、JavaScriptの配列をモデルとして使用しています。QMLの最大の長所の一つは、アプリケーションのプロトタイピングを非常に素早く行えることですが、これはその一例です。また、単純にモデルプロパティに数値を代入して、必要なアイテムの数を示すことも可能です。例えば、model プロパティに10 を割り当てると、各項目の表示テキストは0 から9 までの数字になります。

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

デリゲート

delegate text ItemDelegateモデルからのデータを各デリゲートで利用できるようにする正確な方法は、使用するモデルのタイプに依存します。詳細については、「Qt Quick のモデルとビュー」を参照してください。

このアプリケーションでは、ビュー内の各項目の幅は、ビューの幅と同じでなければなりません。これは、携帯電話のようなタッチスクリーンが小さいデバイスでは重要な要素です。しかし、ビューの幅には、48 ピクセルの余白が含まれるため、width プロパティへの割り当てでそれを考慮する必要があります。

次に、Image を定義します。これは、ユーザーのコンタクトの画像を表示します。画像の幅は40 ピクセル、高さは40 ピクセルです。デリゲートの高さは画像の高さを基準にして、縦の空きがないようにします。

第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 StackViewitemscomponentsURLs を受け入れます。

連絡先リストのコードをContactPage.qml に移したことにお気づきでしょう。アプリケーションに含まれる画面の大まかなイメージがつかめたら、すぐにこれを実行するのがよいでしょう。そうすることで、コードが読みやすくなるだけでなく、完全に必要なときだけ、指定されたコンポーネントからアイテムがインスタンス化されるようになり、メモリ使用量が減ります。

注意: Qt Creatorには、QMLの便利なリファクタリングオプションがいくつか用意されており、そのうちの1つで、コードのブロックを別のファイル(Alt + Enter > Move Component into Separate File)に移動することができます。

ListView を使用する際に考慮すべきもう1つの点は、id で参照するか、付属のListView.view プロパティを使用するかということです。最適な方法は、いくつかの異なる要因に依存します。ビューに id を指定すると、attached プロパティのオーバーヘッドが非常に小さくなるため、バインディング式がより短く効率的になります。しかし、他のビューでデリゲートを再利用する予定がある場合は、attachedプロパティを使用して、デリゲートを特定のビューに結び付けないようにする方が良いでしょう。例えば、attachedプロパティを使用すると、デリゲートの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 })

ComponenturlStackView にプッシュするとき、(最終的に)インスタンス化されたアイテムをいくつかの変数で初期化する必要があることがよくあります。StackView push() 関数は、JavaScript オブジェクトを第 2 引数として受け取ることで、これを考慮します。これを使用して、次のページにコンタクトの名前を提供し、それを使用して関連する会話を表示します。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
        }
    }
    ...

このコンポーネントのルートアイテムは、inConversationWith というカスタムプロパティを持つ別のPageです。今のところ、このプロパティは単にヘッダーのラベルが何を表示するかを決定します。後ほど、このプロパティを会話内のメッセージのリストを生成するSQLクエリで使用します。

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

QMLではアイテムをレイアウトする方法として、アイテムポジショナーと Qtクイックレイアウトの2つがあります。アイテムポジショナー(RowColumn など)は、アイテムのサイズが既知または固定で、特定のフォーメーションに整然と配置するだけでよい場合に便利です。Qtクイックレイアウトのレイアウトは、アイテムの配置とサイズ変更の両方が可能なので、サイズ変更可能なユーザーインターフェイスに適しています。以下では、ColumnLayout を使って、ListViewPane を垂直にレイアウトしています:

    ColumnLayout {
        anchors.fill: parent

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

        }
        ...

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

ペインは基本的に長方形で、その色はアプリケーションのスタイルに由来します。Frame と似ていますが、唯一の違いは、境界線にストロークがないことです。

レイアウトの直接の子であるアイテムには、さまざまなattached propertiesListView にはLayout.fillWidthLayout.fillHeight を使用し、ColumnLayout 内のスペースをできるだけ広く取るようにしています。同じことがペインにも行われます。ColumnLayout は縦長レイアウトなので、各子アイテムの左右にはアイテムがなく、各アイテムがレイアウトの幅全体を消費することになります。

一方、ListViewLayout.fillHeight ステートメントを使用すると、ペインを収容した後の残りのスペースを占有することができます。

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

        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 {}
        }

親の幅と高さを埋めた後、ビューにマージンを設定します。これにより、"compose message "フィールドのプレースホルダー・テキストがきれいに整列されます:

次に、displayMarginBeginningdisplayMarginEnd を設定します。これらのプロパティは、ビューの端でスクロールしている間、ビューの境界の外にあるデリゲートが消えないようにするものです。プロパティをコメントアウトして、ビューをスクロールしたときに何が起こるかを見るのが、これを理解する最も簡単な方法です。

次に、ビューの垂直方向を反転させ、最初のアイテムが一番下に来るようにします。デリゲートは12ピクセル間隔で配置し、第4章で実際のモデルを実装するまでの間、テスト用に「ダミー」モデルを割り当てます。

上の画像のように、アバターの後にメッセージの内容が続くようにしたいので、デリゲートの中で、ルート・アイテムとしてRow

ユーザーから送信されたメッセージは、コンタクトから送信されたメッセージと区別する必要があります。とりあえず、sentByMe というダミーのプロパティを設定します。このプロパティは、単にデリゲートのインデックスを使用して、異なる著者を交互に表示します。このプロパティを使って、3つの方法で異なる著者を区別します:

  • ユーザが送信したメッセージは、anchors.rightlistView.contentItem.right に設定することで、画面の右側に整列されます。
  • アバター(今のところ単に長方形です)のvisible プロパティをsentByMe に基づいて設定することで、メッセージがコンタクトによって送信された場合のみ表示します。
  • 矩形の色は作者によって変えます。暗い背景には暗いテキストを表示したくないので、またその逆も同様です。第5章では、スタイリングがこのような問題をどのように処理してくれるかを見ていくことにしよう。

画面の下部には、複数行のテキスト入力を可能にするTextArea 、メッセージを送信するボタンを配置します。リストビューのコンテンツがページヘッダーと干渉しないようにToolBar

        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 void createTable()
{
    if (QSqlDatabase::database().tables().contains(QStringLiteral("Contacts"))) {
        // The table already exists; we don't need to do anything.
        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から必要なヘッダーファイルをインクルードします。そして、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 から設定され、モデルにどの会話のメッセージを取得すべきかを知らせます。

data() とroleNames() 関数をオーバーライドして、QML でカスタムロールを使用できるようにします。

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

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

#include "sqlconversationmodel.h"

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

static const char *conversationsTableName = "Conversations";

static void createTable()
{
    if (QSqlDatabase::database().tables().contains(conversationsTableName)) {
        // The table already exists; we don't need to do anything.
        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('Ernest Hemingway', 'Me', '2016-01-07T14:36:16', 'Good afternoon.')");
    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('Hans Gude', 'Me', '2015-11-20T06:30:02', 'God morgen. Har du fått mitt maleri?')");
    query.exec("INSERT INTO Conversations VALUES('Me', 'Hans Gude', '2015-11-20T08:21:03', 'God morgen, Hans. Ja, det er veldig fint. Tusen takk! "
               "Hvor mange timer har du brukt på den?')");
}

sqlcontactmodel.cpp とよく似ていますが、Conversations テーブルを操作している点が異なります。また、conversationsTableName を static const 変数として定義しています。

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 フィールドでクエリ結果を降順にソートします。これは、ListViewverticalLayoutDirection プロパティを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で使用することができます。すべてのロール値を保持するenumを宣言すると便利ですが、この関数以外のコードでは特定の値を参照しないので、ここでは省略します。

void SqlConversationModel::sendMessage(const QString &recipient, const QString &message)
{
    const QString 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() 関数は、与えられたrecipientmessage を使用して、新しいレコードをデータベースに挿入します。QSqlTableModel::OnManualSubmit を使用しているため、手動でsubmitAll() を呼び出す必要があります。

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

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

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

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

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

    // Ensure that we have a writable location on all devices.
    const QString fileName = writeDir.absolutePath() + "/chat-database.sqlite3";
    // When using the SQLite driver, open() will create the SQLite database if it doesn't exist.
    database.setDatabaseName(fileName);
    if (!database.open()) {
        qFatal("Cannot open database: %s", qPrintable(database.lastError().text()));
        QFile::remove(fileName);
    }
}

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

    connectToDatabase();

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

    return app.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 プロパティを、ページが表示されているコンタクトの名前に設定します。

ルートデリゲート項目は、すべてのメッセージの下に表示したいタイムスタンプに対応するために、行から列に変更します:

            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 の役割を使用することができます。

アバターに使われていた矩形は画像に変換されました。画像には暗黙のサイズがあるので、明示的に指定する必要はありません。前と同じように、作者がユーザーでないときだけアバターを表示しますが、今回は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 Controls のスタイルは、どのプラットフォームでも動作するように設計されています。この章では、BasicMaterialUniversalの各スタイルでアプリケーションを実行したときに見栄えが良くなるように、ちょっとした見た目の調整を行います。

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

ヘッダーのテキストは紺色の背景に黒で、とても読みにくいです。同じことが会話のページでも起こります:

解決策は、ツールバーに"Dark "テーマを使用するように指示することで、この情報が子テーマに伝わり、子テーマのテキスト色を明るいものに切り替えることができます。これを行う最も簡単な方法は、マテリアル・スタイルを直接インポートし、マテリアルの添付プロパティを使用することです:

import QtQuick.Controls.Material 2.12

// ...

header: ToolBar {
    Material.theme: Material.Dark

    // ...
}

しかし、これにはマテリアル・スタイルへのハードな依存が伴います。マテリアル・スタイル・プラグインは、ターゲット・デバイスがマテリアル・スタイルを使用していなくても、アプリケーションと共にデプロイする必要があります

そうしないと、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
        }
    }

次に、マテリアル・バージョンのツールバーを追加する必要があります。ファイルセレクタは、ファイルのデフォルトバージョンと一緒に存在する、適切な名前のディレクトリにファイルのバリアントがあることを期待します。これは、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
        }
    }

これで両方のページが正しく見えるようになりました:

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

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

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

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

これが完了したら、アプリケーションをビルドして実行してください。これが表示されるはずです:

どちらのページも問題なく見えます。次にユニバーサルスタイルのエントリーを追加します:

[universal]
Theme=Dark

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

まとめ

このチュートリアルでは、Qt Quick Controls を使って基本的なアプリケーションを書くための以下のステップを説明しました:

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

本ドキュメントに含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。