0
点赞
收藏
分享

微信扫一扫

PySide6: QML, SQL and PySide Integration Tutorial

本教程与Qt聊天教程非常相似,但它着重于解释如何使用QML作为UI将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()

在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)

如果角色不是自定义用户角色,则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())

在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

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()

chat.qml

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

import QtQuick
import QtQuick.Layouts

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

接下来,导入Qt快速控制模块。除其他外,这还提供了对ApplicationWindow的访问,ApplicationWindow取代了现有的根类型Window:

让我们开始chat.qml文件。 

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

ApplicationWindow是一个为创建页眉和页脚添加了一些便利的窗口。它还为弹出窗口提供了基础,并支持一些基本样式,例如背景色。

在使用ApplicationWindow时,几乎总是设置三个属性:宽度、高度和可见。一旦设置好这些,我们就有了一个大小合适的空窗口,可以填充内容。

因为我们将SqlConversationModel类公开给QML,所以我们将声明一个组件来访问它:

SqlConversationModel {
id: chat_model

QML中有两种布局项目的方式:Item Positioners和Qt Quick Layouts。

  • Item Positioners(行、列等)在项目大小已知或固定的情况下非常有用,所需的只是将它们整齐地放置在特定的格式中。
  • Qt Quick Layouts.中的布局既可以定位项目,也可以调整项目大小,使其非常适合可调整大小的用户界面。下面,我们使用ColumnLayout垂直布局ListView和Pane。

    ColumnLayout {
anchors.fill: window

        Pane {
id: pane

窗格基本上是一个矩形,其颜色来自应用程序的样式。它类似于“帧”,但其边界周围没有笔划。

作为布局的直接子项的项目具有各种可用的附加属性。我们在ListView上使用Layout.fillWidth和Layout.fillHeight,以确保它在ColumnLayout中占用尽可能多的空间,对Pane也是如此。由于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: 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和displayMargineEnd。这些属性确保在视图边缘滚动时,视图外部的代理不会消失。为了更好地理解,请考虑注释掉属性,然后重新运行代码。现在,查看滚动视图时发生的情况。

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

此外,联系人发送的消息应与联系人发送的信息区分开来。目前,当您发送消息时,我们设置了sentByMe属性,以在不同联系人之间切换。使用此属性,我们可以通过两种方式区分不同的联系人:

联系人发送的消息通过设置锚点与屏幕右侧对齐。right to parent.right。

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

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

        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

我们使用logging而不是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)

​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")

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

声明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)

PySide6: QML, SQL and PySide Integration Tutorial_ide





举报

相关推荐

0 条评论