QML、SQL和PySide集成教程
本教程与Qt聊天教程非常相似,但它专注于解释如何使用QML解释器将SQL数据库集成到PySide6应用程序中。
sqlDialog.py
我们向程序中导入相关的库,定义一个全局变量来保存我们表的名字,并定义一个全局函数 createTable()
,该函数在表不存在时创建一个新的表。数据库包含一个模拟对话开始的单行。
1import datetime
2import logging
3
4from PySide6.QtCore import Qt, Slot
5from PySide6.QtSql import QSqlDatabase, QSqlQuery, QSqlRecord, QSqlTableModel
6from PySide6.QtQml import QmlElement
7
8table_name = "Conversations"
9QML_IMPORT_NAME = "ChatModel"
10QML_IMPORT_MAJOR_VERSION = 1
11
12
13def createTable():
14 if table_name in QSqlDatabase.database().tables():
15 return
16
17 query = QSqlQuery()
18 if not query.exec_(
19 """
20 CREATE TABLE IF NOT EXISTS 'Conversations' (
21 'author' TEXT NOT NULL,
22 'recipient' TEXT NOT NULL,
23 'timestamp' TEXT NOT NULL,
24 'message' TEXT NOT NULL,
25 FOREIGN KEY('author') REFERENCES Contacts ( name ),
26 FOREIGN KEY('recipient') REFERENCES Contacts ( name )
27 )
28 """
29 ):
30 logging.error("Failed to query database")
31
32 # This adds the first message from the Bot
33 # and further development is required to make it interactive.
34 query.exec_(
35 """
36 INSERT INTO Conversations VALUES(
37 'machine', 'Me', '2019-01-07T14:36:06', 'Hello!'
38 )
39 """
40 )
SqlConversationModel
类提供了必需的只读数据模型,该模型用于不可编辑的联系人列表。它派生于 QSqlQueryModel 类,这是此用例的合理选择。然后,我们创建表格,将名称设置为在前面定义的名称,使用 setTable()
方法。我们给表中添加必要的属性,以便程序能够反映出聊天应用的概念。
1@QmlElement
2class SqlConversationModel(QSqlTableModel):
3 def __init__(self, parent=None):
4 super(SqlConversationModel, self).__init__(parent)
5
6 createTable()
7 self.setTable(table_name)
8 self.setSort(2, Qt.DescendingOrder)
9 self.setEditStrategy(QSqlTableModel.OnManualSubmit)
10 self.recipient = ""
11
12 self.select()
13 logging.debug("Table was loaded successfully.")
在 setRecipient()
中,你对数据库返回的结果设置一个过滤器,并在消息接收者改变时发出信号。
1 def setRecipient(self, recipient):
2 if recipient == self.recipient:
3 pass
4
5 self.recipient = recipient
6
7 filter_str = (f"(recipient = '{self.recipient}' AND author = 'Me') OR "
8 f"(recipient = 'Me' AND author='{self.recipient}')")
9 self.setFilter(filter_str)
10 self.select()
如果角色不是自定义用户角色,data()
函数将回退到 QSqlTableModel
的实现。如果你得到一个用户角色,我们可以从它减去 UserRole()
来获得该字段的索引,然后使用该索引找到要返回的值。
1 def data(self, index, role):
2 if role < Qt.UserRole:
3 return QSqlTableModel.data(self, index, role)
4
5 sql_record = QSqlRecord()
6 sql_record = self.record(index.row())
7
8 return sql_record.value(role - Qt.UserRole)
在 roleNames()
函数中,我们返回一个包含我们自定义角色和角色名称的 Python 字典,作为键值对,以便我们在 QML 中使用这些角色。或者,声明一个枚举来存储所有角色值可能很有用。请注意,names
必须是一个哈希以便用作字典键,这就是我们使用 hash
函数的原因。
1 def roleNames(self):
2 """Converts dict to hash because that's the result expected
3 by QSqlTableModel"""
4 names = {}
5 author = "author".encode()
6 recipient = "recipient".encode()
7 timestamp = "timestamp".encode()
8 message = "message".encode()
9
10 names[hash(Qt.UserRole)] = author
11 names[hash(Qt.UserRole + 1)] = recipient
12 names[hash(Qt.UserRole + 2)] = timestamp
13 names[hash(Qt.UserRole + 3)] = message
14
15 return names
send_message()
函数使用给定的接收者和消息在数据库中插入一条新记录。使用 OnManualSubmit()
的情况下,您还需要调用 submitAll()
,因为所有更改都将被缓存到模型中,直到您这样做。
1 # This is a workaround because PySide doesn't provide Q_INVOKABLE
2 # So we declare this as a Slot to be able to call it from QML
3 @Slot(str, str, str)
4 def send_message(self, recipient, message, author):
5 timestamp = datetime.datetime.now()
6
7 new_record = self.record()
8 new_record.setValue("author", author)
9 new_record.setValue("recipient", recipient)
10 new_record.setValue("timestamp", str(timestamp))
11 new_record.setValue("message", message)
12
13 logging.debug(f'Message: "{message}" \n Received by: "{recipient}"')
14
15 if not self.insertRecord(self.rowCount(), new_record):
16 logging.error("Failed to send message: {self.lastError().text()}")
17 return
18
19 self.submitAll()
20 self.select()
chat.qml#
让我们看看 chat.qml
文件。
1import QtQuick
2import QtQuick.Layouts
3import QtQuick.Controls
首先,导入 Qt Quick 模块。这将为我们提供访问 Item、Rectangle、Text 等图形原语的方式。有关类型列表的完整信息,请参阅 Qt Quick QML Types 文档。然后,我们添加 QtQuick.Layouts 的导入,我们将很快介绍它。
接下来,导入 Qt Quick Controls 模块。除其他外,这提供了对 ApplicationWindow
的访问,它取代了现有的根类型 Window
现在,让我们一步一步地分析 chat.qml
文件。
1ApplicationWindow {
2 id: window
3 title: qsTr("Chat")
4 width: 640
5 height: 960
6 visible: true
ApplicationWindow
是一个带有创建页眉和页脚等附加便利的 Window。它还为弹出窗口提供基础并支持一些基本样式,如背景颜色。
当使用 ApplicationWindow 时,通常都会设置三个属性:width
、height
和 visible
。一旦我们设置了这些值,我们就有了一个合适尺寸、空白的窗口,准备好填充内容。
因为我们正在将 SqlConversationModel
类公开到 QML 中,所以我们将声明一个组件来访问它
1 SqlConversationModel {
2 id: chat_model
3 }
在 QML 中有两种方式来布局项:Item Positioners 和 Qt Quick Layouts。
Item Positioners(例如 Row、Column 等)对于项大小已知或固定的情况很有用,并且只需将它们整齐地排列成一定形状。
Qt Quick Layouts 中的布局可以同时定位和调整项大小,因此非常适合可调整大小的用户界面。下面,我们使用 ColumnLayout 垂直布局一个 ListView 和一个 Pane。
1 ColumnLayout { 2 anchors.fill: window 3 4 ListView {
1 Pane { 2 id: pane 3 Layout.fillWidth: true
Pane 是一个基本矩形,其颜色来自应用程序的样式。它与 Frame 类似,但它的边框没有线条。
布局的直接子项有多种可用的附加属性。我们使用Layout.fillWidth和Layout.fillHeight在ListView上,确保它能够尽可能多地占据ColumnLayout中的空间,同样的做法也应用于面板。由于ColumnLayout是垂直布局,因此每个子项的左右两侧没有其他项目,结果是每个项目占据了布局的全部宽度。
另一方面,在ListView中使用的Layout.fillHeight语句,允许它填充在面板之后剩余的空间。
让我们详细看看Listview
。
1 ListView {
2 id: listView
3 Layout.fillWidth: true
4 Layout.fillHeight: true
5 Layout.margins: pane.leftPadding + messageField.leftPadding
6 displayMarginBeginning: 40
7 displayMarginEnd: 40
8 verticalLayoutDirection: ListView.BottomToTop
9 spacing: 12
10 model: chat_model
11 delegate: Column {
12 anchors.right: sentByMe ? listView.contentItem.right : undefined
13 spacing: 6
14
15 readonly property bool sentByMe: model.recipient !== "Me"
16 Row {
17 id: messageRow
18 spacing: 6
19 anchors.right: sentByMe ? parent.right : undefined
20
21 Rectangle {
22 width: Math.min(messageText.implicitWidth + 24,
23 listView.width - (!sentByMe ? messageRow.spacing : 0))
24 height: messageText.implicitHeight + 24
25 radius: 15
26 color: sentByMe ? "lightgrey" : "steelblue"
27
28 Label {
29 id: messageText
30 text: model.message
31 color: sentByMe ? "black" : "white"
32 anchors.fill: parent
33 anchors.margins: 12
34 wrapMode: Label.Wrap
35 }
36 }
37 }
38
39 Label {
40 id: timestampText
41 text: Qt.formatDateTime(model.timestamp, "d MMM hh:mm")
42 color: "lightgrey"
43 anchors.right: sentByMe ? parent.right : undefined
44 }
45 }
46
47 ScrollBar.vertical: ScrollBar {}
48 }
在填充其父项的宽度和高度之后,我们还对该视图设置了一些边距。
接下来,我们设置了displayMarginBeginning和displayMarginEnd。这些属性确保在滚动到视图边缘时,视图外的代理不会消失。为了更好地理解,请取消注释属性,然后重新运行您的代码。现在观察在滚动视图时的变化。
然后我们将视图的垂直方向翻转,使得第一个项目在底部。
此外,需要区分联系人和联系人发送的消息。目前,当消息由您发送时,我们设置了一个sentByMe
属性,在不同的联系人之间交替。使用此属性,我们以两种方式区分不同的联系人
通过将
anchors.right
设置为parent.right
,将联系人发送的消息对齐到屏幕的右侧。我们根据联系人的不同来改变矩形的颜色。由于我们不想在暗背景上显示深色文本,反之亦然,我们还根据联系人的身份设置文本颜色。
在屏幕底部,我们放置一个TextArea项目以允许多行文本输入,并放置一个按钮以发送消息。我们使用面板来覆盖这两个项目下面的区域
1 Pane {
2 id: pane
3 Layout.fillWidth: true
4
5 RowLayout {
6 width: parent.width
7
8 TextArea {
9 id: messageField
10 Layout.fillWidth: true
11 placeholderText: qsTr("Compose message")
12 wrapMode: TextArea.Wrap
13 }
14
15 Button {
16 id: sendButton
17 text: qsTr("Send")
18 enabled: messageField.length > 0
19 onClicked: {
20 listView.model.send_message("machine", messageField.text, "Me");
21 messageField.text = "";
22 }
23 }
24 }
25 }
TextArea应填充屏幕的可用宽度。我们分配一些占位文本,以便向联系人提供视觉提示,告诉他们在哪里开始输入。输入区域内的文本会被换行,以确保它不会超出屏幕。
最后,我们有一个按钮,允许我们调用在sqlDialog.py
上定义的send_message
方法,因为这里只是一个模拟示例,并且我们只使用字符串来使用一个可能的接收者和一个可能的发送者。
main.py#
我们使用 logging
而不是 Python 的 print()
,因为这样可以更好地控制应用程序生成的消息级别(错误、警告和信息消息)。
1import sys
2import logging
3
4from PySide6.QtCore import QDir, QFile, QUrl
5from PySide6.QtGui import QGuiApplication
6from PySide6.QtQml import QQmlApplicationEngine
7from PySide6.QtSql import QSqlDatabase
8
9# We import the file just to trigger the QmlElement type registration.
10import sqlDialog
11
12logging.basicConfig(filename="chat.log", level=logging.DEBUG)
13logger = logging.getLogger("logger")
connectToDatabase()
函数创建与 SQLite 数据库的连接,如果文件不存在,则创建该文件。
1def connectToDatabase():
2 database = QSqlDatabase.database()
3 if not database.isValid():
4 database = QSqlDatabase.addDatabase("QSQLITE")
5 if not database.isValid():
6 logger.error("Cannot add database")
7
8 write_dir = QDir("")
9 if not write_dir.mkpath("."):
10 logger.error("Failed to create writable directory")
11
12 # Ensure that we have a writable location on all devices.
13 abs_path = write_dir.absolutePath()
14 filename = f"{abs_path}/chat-database.sqlite3"
15
16 # When using the SQLite driver, open() will create the SQLite
17 # database if it doesn't exist.
18 database.setDatabaseName(filename)
19 if not database.open():
20 logger.error("Cannot open database")
21 QFile.remove(filename)
在 main
函数中发生了几件有趣的事情
声明一个 QGuiApplication。您应该使用 QGuiApplication 而不是 QApplication,因为我们不使用 QtWidgets 模块。
连接到数据库
声明一个 QQmlApplicationEngine。这允许您访问 QML 元素以从我们在
sqlDialog.py
上构建的对话模型中连接 Python 和 QML。加载定义 UI 的
.qml
文件。
最后,Qt 应用程序运行,您的程序开始。
1if __name__ == "__main__":
2 app = QGuiApplication()
3 connectToDatabase()
4
5 engine = QQmlApplicationEngine()
6 engine.load(QUrl("chat.qml"))
7
8 if not engine.rootObjects():
9 sys.exit(-1)
10
11 app.exec()