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 にアクセスするためのものです。QGuiApplication すべてのQtアプリケーションはアプリケーション・オブジェクトを必要としますが、その正確な型はアプリケーションの動作に依存します。グラフィカルでないアプリケーションはQCoreApplication で十分です。 Qt Widgetsを使用しないグラフィカル・アプリケーションには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 Qt Quick 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 。モデルの対応するテキストを、ItemDelegatetext プロパティに代入します。モデルのデータを各デリゲートで利用できるようにする正確な方法は、使用するモデルのタイプによって異なります。詳しくは 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 CreatorQMLの便利なリファクタリングオプションをいくつか提供しています。そのうちの1つは、コードのブロックを別のファイル(Alt + Enter > Move Component into Separate File )に移動することができるものです。

ListView を使用する際に考慮しなければならないもう一つの点は、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 Quick レイアウトの2つがあります。アイテムポジショナー(Row,Column など)は、アイテムの大きさが既知か固定で、ある一定のフォーメーションに整然と配置するだけでよい場合に便利です。Qt Quick のレイアウトは、アイテムの配置とサイズ変更の両方が可能なので、サイズ変更可能なユーザー・インターフェースに適しています。以下では、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章では、読み出し専用と読み出し書き込み専用の SQL モデルを C++ で作成し、それを 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")){// テーブルは既に存在します。   QSqlQueryquery;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*親) :   QSqlQueryModel(parent) { createTable();    QSqlQueryquery;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 ファイルを見てみましょう:

#define "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)) {// テーブルは既に存在する。   QSqlQueryquery;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', 'こんにちは!')"); query.exec("INSERT INTO Conversations VALUES('Ernest Hemingway', 'Me', '2016-01-07T14:36:16', 'こんにちは。')"); query.exec("INSERT INTO Conversations VALUES('Me', 'Albert Einstein', '2016-01-01T11:24:53', 'こんにちは!')"); query.exec("INSERT INTO Conversations VALUES('Albert Einstein', 'Me', '2016-01-07T14:36:16', 'おはようございます。')"); 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.はい、素晴らしいです。ありがとう!" "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 フィールドでクエリ結果を降順にソートします。これは、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で使用することができます。すべてのロール値を保持するenumを宣言すると便利ですが、この関数以外のコードで特定の値を参照することはないので、ここでは省略します。

voidSqlConversationModel::sendMessage(constQString受信者, constQStringメッセージ) {constQStringtimestamp=QDateTime::currentDateTime().toString(Qt::ISODate);    QSqlRecordnewRecord=record(); newRecord.setValue("author", "Me"); newRecord.setValue("recipient",recipient); newRecord.setValue("timestamp",timestamp); newRecord.setValue("message",message);if(!insertRecord(rowCount(),newRecord)) {if(!        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 voidconnectToDatabase() { 次のようにします。    QSqlDatabaseデータベース=QSqlDatabase::database();if(!database.isValid()) { database=QSqlDatabase::addDatabase("QSQLITE");if(!database.isValid())            qFatal("Cannot add database: %s", qPrintable(database.lastError().text()));
    }constQDirwriteDir=QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);if(!writeDir.mkpath("."))        qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath()));

   // すべてのデバイスに書き込み可能な場所があることを確認する。QStringfileName=writeDir.absolutePath()+ "/chat-database.sqlite3";// SQLiteドライバを使用する場合、SQLiteデータベースが存在しなければopen()で作成します。        qFatal("Cannot open database: %s", qPrintable(database.lastError().text()));
        QFile::remove(fileName); } } }intmain(intargc, char *argv[]) { QGuiApplicationapp(argc,argv); connectToDatabase();    QQmlApplicationEngineengine; 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 プロパティを、ページが表示されているコンタクトの名前に設定します。

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

            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」テーマを使用するよう指示し、この情報が子テーマに伝わり、子テーマのテキスト色を明るい色に切り替えることができるようにすることです。これを行う最も簡単な方法は、マテリアル・スタイルを直接インポートして、Material attachedプロパティを使用することです:

import QtQuick.Controls.Material 2.12

// ...

header: ToolBar {
    Material.theme: Material.Dark

    // ...
}

ただし、この場合、Material スタイルに依存することになります。Material スタイルのプラグインは、ターゲット デバイスが使用していなくても、アプリケーションと一緒にデプロイする必要があります

そうしないと、QML エンジンはインポートを見つけることができません。代わりに、Qt Quick Controlsスタイルベースのファイル セレクタのビルトイン サポートを利用することをお勧めします。そのためには、ToolBar を独自のファイルに移動する必要があります。これをChatToolBar.qml と呼ぶことにする。これはファイルの「デフォルト」バージョンとなり、ベーシック・スタイル(何も指定されていないときに使用されるスタイル)が使用されているときに使用されます。これが新しいファイルです:

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を統合する。
  • 複数のスタイルのテストと設定

© 2025 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.