14、QML, SQL and PySide Integration Tutorial

本教程与 Qt Chat 教程非常相似,但它侧重于解释如何使用 QML 将 SQL 数据库集成到 PySide6 应用程序中。

sqlDialog.py

我们将相关库导入我们的程序,定义一个保存表名的全局变量,并定义全局函数 createTable(),如果新表不存在则创建一个新表。数据库包含一行来模拟对话的开始。

import datetime
import logging

from PySide6.QtCore import Qt, Slot
from PySide6.QtSql import QSqlDatabase, QSqlQuery, QSqlRecord, QSqlTableModel
from PySide6.QtQml import QmlElement

table_name = "Conversations"
QML_IMPORT_NAME = "ChatModel"
QML_IMPORT_MAJOR_VERSION = 1


def createTable():
    if table_name in QSqlDatabase.database().tables():
        return

    query = QSqlQuery()
    if not 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 )
        )
        """
    ):
        logging.error("Failed to query database")

    # This adds the first message from the Bot
    # and further development is required to make it interactive.
    query.exec_(
        """
        INSERT INTO Conversations VALUES(
            'machine', 'Me', '2019-01-07T14:36:06', 'Hello!'
        )
        """
    )

SqlConversationModel 类提供了不可编辑的联系人列表所需的只读数据模型。它派生自 QSqlQueryModel 类,这是此用例的逻辑选择。然后,我们继续创建表,将其名称设置为之前使用 setTable() 方法定义的名称。我们将必要的属性添加到表中,以拥有一个反映聊天应用程序理念的程序。

@QmlElement
class SqlConversationModel(QSqlTableModel):
    def __init__(self, parent=None):
        super(SqlConversationModel, self).__init__(parent)

        createTable()
        self.setTable(table_name)
        self.setSort(2, Qt.DescendingOrder)
        self.setEditStrategy(QSqlTableModel.OnManualSubmit)
        self.recipient = ""

        self.select()
        logging.debug("Table was loaded successfully.")

在 setRecipient() 中,您对从数据库返回的结果设置一个过滤器,并在每次消息接收者更改时发出一个信号。

    def setRecipient(self, recipient):
        if recipient == self.recipient:
            pass

        self.recipient = recipient

        filter_str = (f"(recipient = '{self.recipient}' AND author = 'Me') OR "
                      f"(recipient = 'Me' AND author='{self.recipient}')")
        self.setFilter(filter_str)
        self.select()

如果角色不是自定义用户角色,则 data() 函数会退回到 QSqlTableModel 的实现。如果你得到一个用户角色,我们可以从中减去 UserRole() 来得到那个字段的索引,然后使用那个索引来找到要返回的值。

    def data(self, index, role):
        if role < Qt.UserRole:
            return QSqlTableModel.data(self, index, role)

        sql_record = QSqlRecord()
        sql_record = self.record(index.row())

        return sql_record.value(role - Qt.UserRole)

在 roleNames() 中,我们返回一个 Python 字典,其中包含我们的自定义角色和角色名称作为键值对,因此我们可以在 QML 中使用这些角色。或者,声明一个 Enum 来保存所有角色值可能很有用。请注意,名称必须是哈希才能用作字典键,这就是我们使用哈希函数的原因。

    def roleNames(self):
        """Converts dict to hash because that's the result expected
        by QSqlTableModel"""
        names = {}
        author = "author".encode()
        recipient = "recipient".encode()
        timestamp = "timestamp".encode()
        message = "message".encode()

        names[hash(Qt.UserRole)] = author
        names[hash(Qt.UserRole + 1)] = recipient
        names[hash(Qt.UserRole + 2)] = timestamp
        names[hash(Qt.UserRole + 3)] = message

        return names

send_message() 函数使用给定的收件人和消息将新记录插入数据库。使用 OnManualSubmit() 要求您还调用 submitAll(),因为所有更改都将缓存在模型中,直到您这样做。

    # This is a workaround because PySide doesn't provide Q_INVOKABLE
    # So we declare this as a Slot to be able to call it  from QML
    @Slot(str, str, str)
    def send_message(self, recipient, message, author):
        timestamp = datetime.datetime.now()

        new_record = self.record()
        new_record.setValue("author", author)
        new_record.setValue("recipient", recipient)
        new_record.setValue("timestamp", str(timestamp))
        new_record.setValue("message", message)

        logging.debug(f'Message: "{message}" \n Received by: "{recipient}"')

        if not self.insertRecord(self.rowCount(), new_record):
            logging.error("Failed to send message: {self.lastError().text()}")
            return

        self.submitAll()
        self.select()

chat.qml

让我们看一下chat.qml 文件。

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

首先,导入 Qt Quick 模块。这使我们能够访问图形基元,例如 Item、Rectangle、Text 等。有关类型的完整列表,请参阅 Qt Quick QML 类型文档。然后我们添加 QtQuick.Layouts 导入,我们稍后会介绍。

接下来,导入 Qt Quick Controls 模块。除其他外,这提供了对 ApplicationWindow 的访问,它替换了现有的根类型 Window:

让我们逐步浏览 chat.qml 文件。

ApplicationWindow {
    id: window
    title: qsTr("Chat")
    width: 640
    height: 960
    visible: true

在 QML 中有两种布局项目的方法:项目定位器和 Qt 快速布局。

项目定位器(行、列等)对于项目大小已知或固定的情况很有用,并且只需将它们整齐地定位在特定的格式中。

Qt Quick Layouts 中的布局可以定位和调整项目大小,使其非常适合可调整大小的用户界面。下面,我们使用 ColumnLayout 来垂直布局一个 ListView 和一个 Pane。

    ColumnLayout {
        anchors.fill: window

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

窗格基本上是一个矩形,其颜色来自应用程序的样式。它类似于 Frame,但它的边框没有笔划。 作为布局的直接子项的项目具有各种可用的附加属性。我们在 ListView 上使用 Layout.fillWidth 和 Layout.fillHeight 来确保它在 ColumnLayout 中占用尽可能多的空间,对于 Pane 也是如此。由于 ColumnLayout 是垂直布局,每个子项的左侧或右侧都没有任何项目,因此这导致每个项目占用布局的整个宽度。

另一方面,ListView 中的 Layout.fillHeight 语句使其能够占据容纳 Pane 后剩余的空间。 让我们详细看一下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: chat_model
            delegate: Column {
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                readonly property bool sentByMe: model.recipient !== "Me"
                Row {
                    id: messageRow
                    spacing: 6
                    anchors.right: sentByMe ? parent.right : undefined

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

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

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

            ScrollBar.vertical: ScrollBar {}
        }

在填充了其父级的宽度和高度后,我们还在视图上设置了一些边距。

接下来,我们设置 displayMarginBeginning 和 displayMarginEnd。这些属性确保当您在视图边缘滚动时,视图外的代理不会消失。为了更好地理解,请考虑注释掉这些属性,然后重新运行您的代码。现在观察滚动视图时会发生什么。

然后我们翻转视图的垂直方向,使第一个项目位于底部。

此外,联系人发送的消息应与联系人发送的消息区分开来。目前,当您发送消息时,我们设置了一个 sentByMe 属性,以在不同联系人之间交替。使用此属性,我们以两种方式区分不同的联系人: 通过将 anchors.right 设置为 parent.right,联系人发送的消息将与屏幕右侧对齐。

我们根据接触改变矩形的颜色。由于我们不想在深色背景上显示深色文本,反之亦然,我们还根据联系人的身份设置文本颜色。

在屏幕底部,我们放置了一个 TextArea 项以允许多行文本输入,以及一个用于发送消息的按钮。我们使用 Pane 来覆盖这两个项目下的区域:

        Pane {
            id: pane
            Layout.fillWidth: true

            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
                    onClicked: {
                        listView.model.send_message("machine", messageField.text, "Me");
                        messageField.text = "";
                    }
                }
            }
        }

TextArea 应该填满屏幕的可用宽度。我们分配一些占位符文本来为联系人提供关于他们应该从哪里开始输入的视觉提示。输入区域内的文本被换行以确保它不会超出屏幕。

最后,我们有一个按钮,允许我们调用我们在 sqlDialog.py 上定义的 send_message 方法,因为我们在这里只是有一个模拟示例,并且这个对话只有一个可能的收件人和一个可能的发件人,我们只是在这里使用字符串。

main.py

我们使用日志而不是 Python 的 print(),因为它提供了一种更好的方法来控制我们的应用程序将生成的消息级别(错误、警告和信息消息)。

import sys
import logging

from PySide6.QtCore import QDir, QFile, QUrl
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtSql import QSqlDatabase

# We import the file just to trigger the QmlElement type registration.
import sqlDialog

logging.basicConfig(filename="chat.log", level=logging.DEBUG)
logger = logging.getLogger("logger")

connectToDatabase() 创建与 SQLite 数据库的连接,如果实际文件不存在,则创建实际文件。

def connectToDatabase():
    database = QSqlDatabase.database()
    if not database.isValid():
        database = QSqlDatabase.addDatabase("QSQLITE")
        if not database.isValid():
            logger.error("Cannot add database")

    write_dir = QDir("")
    if not write_dir.mkpath("."):
        logger.error("Failed to create writable directory")

    # Ensure that we have a writable location on all devices.
    abs_path = write_dir.absolutePath()
    filename = f"{abs_path}/chat-database.sqlite3"

    # When using the SQLite driver, open() will create the SQLite
    # database if it doesn't exist.
    database.setDatabaseName(filename)
    if not database.open():
        logger.error("Cannot open database")
        QFile.remove(filename)

在 main 函数中发生了一些有趣的事情:

声明一个 QGuiApplication。你应该使用 QGuiApplication 而不是 QApplication 因为我们没有使用 QtWidgets 模块。

连接数据库,

声明一个 QQmlApplicationEngine。这允许您访问 QML 元素以从我们在 sqlDialog.py 上构建的对话模型连接 Python 和 QML。

加载定义 UI 的 .qml 文件。

最后,Qt 应用程序运行,您的程序启动。

if __name__ == "__main__":
    app = QGuiApplication()
    connectToDatabase()

    engine = QQmlApplicationEngine()
    engine.load(QUrl("chat.qml"))

    if not engine.rootObjects():
        sys.exit(-1)

    app.exec()

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值