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モジュールがすでにインポートされていることにお気づきでしょう。これにより、Item 、Rectangle 、Text などのグラフィカルプリミティブにアクセスできるようになります。全ての型のリストについては Qt Quick QML Typesドキュメントを参照のこと。
Qt Quick Controls モジュールのインポートを追加する。これは特に、既存のルート型Window
を置き換えるApplicationWindow へのアクセスを提供します:
ApplicationWindow { width: 540 height: 960 visible: true ... }
ApplicationWindow は、 に、 と を作成するための利便性を追加したものです。また、 の基礎を提供し、背景 のような基本的なスタイリングもサポートします。Window header footer popups color
ApplicationWindow を使用する場合、ほとんど常に設定される3つのプロパティがあります:width 、height 、visible 。これらを設定すると、適切なサイズの空のウィンドウができ、コンテンツで満たされる準備が整います。
注: デフォルト・コードの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 プロパティを設定します。これにより、ラベルの両側(境界内)に余分なスペースが割り当てられます。代わりに、topPadding とbottomPadding プロパティを明示的に設定することもできます。
qsTr()
関数を使用してラベルのテキストを設定します。これは、Qt の翻訳システムでテキストが翻訳できるようにするためです。これは、アプリケーションのエンドユーザーが目にするテキストに適用する良い方法です。
デフォルトでは、テキストはその境界の上端に垂直に配置され、水平方向の配置はテキストの自然な方向に依存します;例えば、左から右に読まれるテキストは左に配置されます。例えば、左から右に読まれるテキストは左に揃えられる。これらのデフォルトを使用すると、テキストはウィンドウの左上隅に配置される。これはヘッダーとしては望ましくないので、水平方向と垂直方向の両方で、テキストを境界の中央に揃えます。
プロジェクトファイル
CMakeLists.txt
ファイルには、CMakeがプロジェクトをビルドして実行可能ファイルにするために必要な情報がすべて含まれています。
このファイルについての詳しい説明は、QMLアプリケーションのビルドを参照してください。
現在、私たちのアプリケーションを実行すると以下のようになります:
第2章 リスト
この章では、ListView とItemDelegate を使って、対話型アイテムのリストを作成する方法を説明します。
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 。モデルの対応するテキストを、ItemDelegate のtext プロパティに代入します。モデルのデータを各デリゲートで利用できるようにする正確な方法は、使用するモデルのタイプによって異なります。詳しくは 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を使ったナビゲーション
その名前が示すように、StackView はスタックベースのナビゲーションを提供します。スタックに"プッシュ "された最後のアイテムが最初に削除され、一番上のアイテムが常に表示されます。
Pageのときと同じように、StackView 、アプリケーション・ウィンドウを埋めるように指示する。initialItem StackView はitems 、components 、URLs を受け入れます。
連絡先リストのコードをContactPage.qml
に移したことにお気づきでしょう。アプリケーションに含まれる画面の大まかなイメージがつかめたら、すぐにこれを実行するのがよいでしょう。そうすることで、コードが読みやすくなるだけでなく、アイテムが完全に必要なときだけ与えられたコンポーネントからインスタンス化されるようになり、メモリ使用量が減ります。
注: Qt Creator はQMLの便利なリファクタリングオプションをいくつか提供しています。そのうちの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 })
Component やurl をStackView にプッシュするとき、(最終的に)インスタンス化されたアイテムをいくつかの変数で初期化する必要があることがよくあります。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 を使って、ListView とPane を垂直にレイアウトしています:
ColumnLayout { anchors.fill: parent ListView { id: listView Layout.fillWidth: true Layout.fillHeight: true ... } ... Pane { id: pane Layout.fillWidth: true ... }
ペインは基本的に長方形で、その色はアプリケーションのスタイルに由来します。Frame と似ていますが、唯一の違いは、境界線にストロークがないことです。
レイアウトの直接の子であるアイテムには、さまざまなattached properties 。ListView にはLayout.fillWidth とLayout.fillHeight を使用し、ColumnLayout 内のスペースをできるだけ広く取るようにしています。同じことがペインにも行われます。ColumnLayout は縦型レイアウトなので、各子要素の左右にはアイテムがなく、各アイテムがレイアウトの幅全体を占めることになります。
一方、ListView のLayout.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 "フィールドのプレースホルダーテキストがきれいに整列されます:
次に、displayMarginBeginning とdisplayMarginEnd を設定します。これらのプロパティは、ビューの端でスクロールしている間に、ビューの境界の外にあるデリゲートが消えないようにするものです。プロパティをコメントアウトして、ビューをスクロールしたときに何が起こるかを見るのが、これを理解する最も簡単な方法です。
次に、ビューの垂直方向を反転させ、最初のアイテムが一番下に来るようにします。デリゲートは12ピクセル間隔で配置され、第4章で本物のモデルを実装するまでの間、テスト用に「ダミー」モデルが割り当てられます。
上の画像のように、アバターに続いてメッセージの内容を表示したいので、デリゲート内でRow をルート・アイテムとして宣言します。
ユーザーから送信されたメッセージは、コンタクトから送信されたメッセージと区別する必要があります。とりあえず、sentByMe
というダミーのプロパティを設定します。このプロパティは、単にデリゲートのインデックスを使用して、異なる著者を交互に表示します。このプロパティを使用して、3つの方法で異なる著者を区別します:
- ユーザが送信したメッセージは、
anchors.right
をlistView.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()
関数は、与えられたrecipient
とmessage
を使って、新しいレコードをデータベースに挿入する。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 のスタイルはどのプラットフォームでも動作するように設計されています。この章では、Basic、Material、Universalの各スタイルでアプリケーションを実行したときに見栄えがよくなるように、ビジュアルを少し調整します。
これまでは、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_PROPERTY 、Q_INVOKABLE 、qmlRegisterType ()を使って、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.