Qt Quick Controls - 聊天教程

本教程介绍如何使用Qt Quick Controls 编写一个基本的聊天应用程序。它还将讲解如何将 SQL 数据库集成到 Qt 应用程序中。

第 1 章:设置

设置新项目时,最简单的方法是使用 Qt Creator.在本项目中,我们选择了Qt Quick 应用程序模板,它创建了一个包含以下文件的基本 "Hello World "应用程序:

  • CMakeLists.txt - 指导 CMake 如何构建我们的项目
  • Main.qml - 提供一个包含空窗口的默认用户界面
  • main.cpp - 加载main.qml
  • qtquickcontrols2.conf - 告诉应用程序应使用哪种样式

main.cpp

main.cpp 中的默认代码有两个包含:

#include <QGuiApplication>
#include <QQmlApplicationEngine>

第一个让我们访问QGuiApplication 。所有 Qt 应用程序都需要一个应用程序对象,但具体类型取决于应用程序的功能。对于非图形应用程序,QCoreApplication 即可。对于不使用 Qt XML 的图形应用程序,QGuiApplication 即可。 Qt Widgets的图形应用程序,而QApplication 则是必须的。

第二个 include 使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 模块的导入。除其他外,这将提供对ApplicationWindow 的访问权限,它将取代现有的根类型Window

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

ApplicationWindow 它是一个 ,并增加了创建 和 的便利性。它还为 提供了基础,并支持一些基本样式,如背景 。Window header footer popups color

使用ApplicationWindow 时,有三个属性几乎总是要设置的: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 通过添加样式font 继承,扩展了Qt Quick 模块中的原始Text 项目。这意味着标签可以根据使用的样式呈现不同的外观,还可以将像素大小传播给其子项。

我们希望在应用程序窗口顶部和文本之间留出一定距离,因此我们设置了padding 属性。这将在标签两侧(在其范围内)分配额外空间。我们也可以明确设置topPaddingbottomPadding 属性。

我们使用qsTr() 函数设置标签的文本,这样可以确保Qt XML 的翻译系统能翻译文本。对于应用程序最终用户可见的文本,这是一个很好的做法。

默认情况下,文本垂直对齐到其边界的顶部,而水平对齐取决于文本的自然方向;例如,从左到右阅读的文本将向左对齐。如果使用这些默认值,我们的文本将位于窗口的左上角。这对于标题来说并不可取,因此我们将文本水平和垂直对齐到其边界的中心。

项目文件

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 的最大优势之一就是它能让应用程序的原型设计变得极其快速,这就是一个很好的例子。我们也可以简单地为模型属性指定一个数字,以表示需要多少个项目。例如,如果将10 赋值给model 属性,则每个项目的显示文本将是一个从09 的数字。

不过,一旦应用程序过了原型阶段,很快就需要使用一些真实数据。为此,最好通过subclassing QAbstractItemModel 使用适当的 C++ 模型。

委托

delegate我们将模型中的相应文本分配给ItemDelegatetext 属性。将模型中的数据提供给每个委托的具体方式取决于所使用的模型类型。更多信息请参见 Qt Quick 中的模型和视图

在我们的应用程序中,视图中每个项的宽度应与视图的宽度相同。这样可以确保用户有足够的空间从列表中选择联系人,这对于触摸屏较小的设备(如手机)来说是一个重要因素。不过,视图的宽度包括48 像素的边距,因此我们在分配宽度属性时必须考虑到这一点。

接下来,我们定义一个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 给它一个要显示的项目。StackView 接受items,componentsURLs

您会注意到,我们将联系人列表的代码移到了ContactPage.qml 中。一旦您对应用程序将包含哪些屏幕有了大致的了解,最好就立即这样做。这样做不仅能使代码更易于阅读,还能确保只有在完全必要时才从给定组件中实例化项目,从而减少内存使用量。

注: Qt Creator 为 QML 提供了几种方便的快速修复方法,其中之一是允许您将代码块移动到一个单独的文件中 (Alt + Enter > Move Component into Separate File)。

使用ListView 时需要考虑的另一件事是,是用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 })

在将Componenturl 推送到StackView 时,通常需要用一些变量来初始化(最终)实例化的项目。StackView push() 函数将一个 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
        }
    }
    ...

该组件的根项是另一个页面,它有一个名为inConversationWith 的自定义属性。目前,该属性将简单地决定标题中的标签显示内容。稍后,我们将在 SQL 查询中使用它来填充对话中的消息列表。

为了让用户返回到 "联系 "页面,我们添加了一个ToolButton ,点击后会调用pop() 。ToolButton 在功能上类似于Button ,但其外观更适合ToolBar

QML 有两种布局项目的方法:项目定位器(Item Positioners)和Qt Quick 布局。项目定位器(Row,Column, 等等)适用于项目大小已知或固定的情况,只需将它们整齐地摆放成一定的队形即可。Qt Quick Layouts 中的布局既可以定位项目,也可以调整项目大小,因此非常适合可调整大小的用户界面。下面,我们使用ColumnLayout 来垂直布局ListViewPane

    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.fillWidthLayout.fillHeight ,以确保它在ColumnLayout 中占据尽可能多的空间。窗格也是如此。由于ColumnLayout 是垂直布局,每个子项的左右两侧都没有任何项目,因此每个项目都将占用整个布局的宽度。

另一方面,ListView 中的Layout.fillHeight 语句将使其能够占据容纳窗格后的剩余空间。

让我们详细看看 listview:

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

在填满父对象的宽度和高度后,我们还为视图设置了一些边距。这样,我们就能很好地与 "撰写信息 "字段中的占位符文本对齐:

接下来,我们设置displayMarginBeginningdisplayMarginEnd 。这些属性可确保在视图边缘滚动时,视图边界外的委托不会消失。要理解这一点,最简单的方法是注释掉这些属性,然后看看在滚动视图时会发生什么。

然后,我们翻转视图的垂直方向,使第一个项目位于底部。代表的间距为 12 像素,并指定一个"虚拟 "模型用于测试,直到我们在第 4 章中实现真正的模型。

在委托中,我们将Row 声明为根项目,因为我们希望头像之后是信息内容,如上图所示。

用户发送的信息应与联系人发送的信息区分开来。目前,我们设置了一个虚拟属性sentByMe ,它只是使用委托的索引来交替显示不同的作者。使用该属性,我们可以通过三种方式区分不同的作者:

  • 通过将anchors.right 设置为listView.contentItem.right ,用户发送的信息会被对齐到屏幕右侧。
  • 通过根据sentByMe 设置头像(目前只是一个矩形)的visible 属性,我们只在信息由联系人发送时才显示头像。
  • 我们会根据作者来改变矩形的颜色。由于我们不想在深色背景上显示深色文字,反之亦然,因此我们也会根据作者设置文字颜色。在第 5 章中,我们将看到样式设计是如何为我们解决类似问题的。

在屏幕底部,我们放置了一个TextArea 项目,允许多行文本输入,并放置了一个按钮用于发送信息。我们使用 Pane 来覆盖这两个项目下的区域,就像我们使用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 结果集提供了只读数据模型。

让我们看看源于QSqlQueryModelSqlContactModel 类:

#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*parent)    QSqlQueryModel(parent) { createTable();    QSqlQuerySqlContactModel::SqlContactModel( *parent) { createTable(); 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_PROPERTYQ_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 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', '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!exec("INSERT INTO Conversations VALUES('我''阿尔伯特-爱因斯坦''2016-01-01T11:24:53''嗨!')"); query.exec("INSERT INTO Conversations VALUES('阿尔伯特-爱因斯坦''我''2016-01-07T14:36:16''早上好。')"); query.exec("INSERT INTO Conversations VALUES('Hans Gude''我''2015-11-20T06:30:02''早上好。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.请稍等!" "Hvor mange timer har du brukt på den?')"); }

这与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 字段以降序对查询结果进行排序。这与将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 中使用这些角色。声明一个枚举来保存所有的角色值可能是有用的,但由于我们不会在此函数之外的代码中引用任何特定的值,所以我们就不麻烦了。

voidSqlConversationModel::sendMessage(constQString收件人 常量QString信息 {常量QStringtimestamp=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)) {        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() 将创建它。database.setDatabaseName(fileName);if(!database.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 角色。

用于头像的矩形已转换为图像。图像有自己的隐式尺寸,因此我们不需要明确指定。与之前一样,我们只在作者不是用户时显示头像,只不过这次我们将图片的source 设置为空 URL,而不是使用visible 属性。

我们希望每条信息的背景都比文字略宽(每边 12 像素)。但是,如果信息太长,我们希望将其宽度限制在列表视图的边缘,因此使用了Math.min() 。如果信息不是由我们发送的,头像总是会出现在信息之前,因此我们通过减去头像的宽度和行间距来解决这个问题。

例如,在上图中,信息文本的隐含宽度是较小的值。然而,在下图中,信息文本很长,因此我们选择了较小的值(视图宽度),以确保文本停在屏幕的对边:

为了显示我们之前讨论过的每条信息的时间戳,我们使用了标签。日期和时间的格式是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() 函数,该函数会在对话数据库表中插入一条新记录。然后,我们清除文本字段,为将来的输入让路。

第 5 章:样式

Qt Quick Controls 中的样式设计适用于任何平台。在本章中,我们将进行一些细微的视觉调整,以确保我们的应用程序在使用基本材质通用样式运行时外观良好。

到目前为止,我们只是在使用 Basic 样式测试应用程序。例如,如果我们使用Material 样式运行应用程序,我们会立即发现一些问题。下面是联系人页面:

标题文字是深蓝色背景上的黑色,非常难以阅读。对话页面也出现了同样的问题:

解决方法是告诉工具栏应使用"深色 "主题,以便将此信息传播给其子栏,让它们将文字颜色切换为浅色。最简单的方法是直接导入 Material 样式并使用 Material attached 属性:

import QtQuick.Controls.Material 2.12

// ...

header: ToolBar {
    Material.theme: Material.Dark

    // ...
}

不过,这也带来了对 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
        }
    }

现在我们需要添加材料版本的工具栏。文件选择器希望文件的变体与默认版本的文件一起存在于适当命名的目录中。这意味着我们需要在 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 风格,因此请在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.