如何将 Python 和 QML 与 PySide2 连接起来?

2022-01-19 00:00:00 python pyside2 qt qml

问题描述

我想在 Ubuntu 上编写一个简单的桌面应用程序,我认为一个简单的方法是使用 Qt 和 QML 作为 GUI,Python 作为逻辑语言,因为我对 Python 有点熟悉.

I want to write a simple desktop application on Ubuntu and I thought that an easy way was to use Qt with QML as GUI and Python as the language for the logic, since I am somewhat familiar with Python.

现在我尝试了几个小时以某种方式连接 GUI 和逻辑,但它不起作用.我管理了连接 QML --> Python,但不是相反.我有代表我的数据模型的 Python 类,并添加了 JSON 编码和解码函数.因此,目前不涉及 SQL 数据库.但也许 QML 视图和某些数据库之间的直接连接会使事情变得更容易?

Now I am trying for hours to somehow connect the GUI and the logic, but it is not working. I managed the connection QML --> Python but not the other way around. I have Python classes which represent my data model and I added JSON encode and decode functions. So for now there is no SQL database involved. But maybe a direct connection between QML view and some database would make things easier?

现在有一些代码.

QML --> Python

QML 文件:

ApplicationWindow {

// main window
id: mainWindow
title: qsTr("Test")
width: 640
height: 480

signal tmsPrint(string text)

Page {
    id: mainView

    ColumnLayout {
        id: mainLayout

        Button {
            text: qsTr("Say Hello!")
            onClicked: tmsPrint("Hello!")
        }
    }
}    
}

然后我有我的 slot.py:

Then I have my slots.py:

from PySide2.QtCore import Slot

def connect_slots(win):
    win.tmsPrint.connect(say_hello)

@Slot(str)
def say_hello(text):
    print(text)

最后是我的 main.py:

And finally my main.py:

import sys
from controller.slots import connect_slots

from PySide2.QtWidgets import QApplication
from PySide2.QtQml import QQmlApplicationEngine 

if __name__ == '__main__':
    app = QApplication(sys.argv)

    engine = QQmlApplicationEngine()
    engine.load('view/main.qml')

    win = engine.rootObjects()[0]
    connect_slots(win)

    # show the window
    win.show()
    sys.exit(app.exec_())

这很好,我可以打印你好!".但这是最好的方法还是创建一个带插槽的类并使用 setContextProperty 以便能够直接调用它们而不添加额外的信号更好?

This works fine and I can print "Hello!". But is this the best way to do it or is it better to create a class with slots and use setContextProperty to be able to call them directly without adding additional signals?

Python --> QML

我无法完成这项工作.我尝试了不同的方法,但都没有奏效,我也不知道哪一种最好用.我想要做的是例如显示对象列表并提供在应用程序中操作数据的方法等.

I cannot get this done. I tried different approaches, but none worked and I also don't know which one is the best to use. What I want to do is for example show a list of objects and offer means to manipulate data in the application etc.

  1. 包括 Javascript:我添加了一个附加文件 application.js ,其功能只是为了打印一些东西,但它可能用于设置文本字段的上下文等.然后我尝试使用 QMetaObject 和 invokeMethod,但只是得到错误参数等错误.
  1. include Javascript: I added an additional file application.js with a function just to print something, but it could probably be used to set the context of a text field etc. Then I tried to use QMetaObject and invokeMethod, but just got errors with wrong arguments etc.

这种方法有意义吗?其实我不懂javascript,所以如果没有必要,我宁愿不用它.

Does this approach make any sense? Actually I don't know any javascript, so if it is not necessary, I would rather not use it.

  1. ViewModel 方法我创建了一个文件 viewmodel.py

  1. ViewModel approach I created a file viewmodel.py

from PySide2.QtCore import QStringListModel

class ListModel(QStringListModel):

def __init__(self):
     self.textlines = ['hi', 'ho']
     super().__init__()

我在 main.py 中添加了:

And in the main.py I added:

model = ListModel()
engine.rootContext().setContextProperty('myModel', model)

ListView 看起来像这样:

and the ListView looks like this:

ListView {
            width: 180; height: 200

            model: myModel
            delegate: Text {
                text: model.textlines
            }
        }

我收到一个错误myModel is not defined",但我想它无论如何都不能工作,因为委托只接受一个元素而不是一个列表.这种方法是一个好方法吗?如果是,我该如何让它发挥作用?

I get an error "myModel is not defined", but I guess that it can't work anyway, since delegates only take one element and not a list. Is this approach a good one? and if yes, how do I make it work?

  1. 是否有完全不同的方法来处理 QML 视图中的数据?

感谢您的帮助!我知道 Qt 文档,但我对它不满意.所以也许我错过了一些东西.但是 PyQt 似乎比 PySide2 更受欢迎(至少谷歌搜索似乎表明这一点)并且 PySide 引用经常使用 PySide1 或不使用 QML QtQuick 做事方式......

I appreciate your help! I know the Qt documentation but I am not happy with it. So maybe I am missing something. But PyQt seems to be way more popular than PySide2 (at least google searches seem to indicate that) and PySide references often use PySide1 or not the QML QtQuick way of doing things...


解决方案

你的问题有很多方面,所以我会尽量在我的回答中详细一点,并且这个答案会不断更新,因为这种类型的问题经常被问到但他们是针对特定案例的解决方案,因此我将冒昧地给它一个通用方法并在可能的情况下具体说明.

Your question has many aspects so I will try to be detailed in my answer and also this answer will be continuously updated because this type of questions are often asked but they are solutions for a specific case so I am going to take the liberty of giving it a general approach and be specific in the possible scenarios.

QML 到 Python:

您的方法有效,因为 python 中的类型转换是动态的,而在 C++ 中则不会发生.它适用于小任务,但不可维护,逻辑必须与视图分离,因此不应依赖.具体地说,假设打印的文本将被逻辑执行一些处理,那么如果你修改信号的名称,或者如果数据不依赖于 ApplicationWindow 而是依赖于另一个元素等,那么您将不得不更改很多连接代码.

Your method works because the type conversion in python is dynamic, in C++ it does not happen. It works for small tasks but it is not maintainable, the logic must be separated from the view so it should not be dependent. To be concrete, let's say that the printed text will be taken by the logic to perform some processing, then if you modify the name of the signal, or if the data does not depend on ApplicationWindow but on another element, etc. then you will have to change a lot connection code.

如您所说,建议创建一个类,该类负责映射您需要的逻辑数据并将其嵌入到 QML 中,因此如果您更改视图中的某些内容,只需更改连接:

The recommended as you indicate is to create a class that is responsible for mapping the data you need your logic and embed it in QML, so if you change something in the view you just change the connection:

例子:

ma​​in.py

import sys

from PySide2.QtCore import QObject, Signal, Property, QUrl
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

class Backend(QObject):
    textChanged = Signal(str)

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.m_text = ""

    @Property(str, notify=textChanged)
    def text(self):
        return self.m_text

    @text.setter
    def setText(self, text):
        if self.m_text == text:
            return
        self.m_text = text
        self.textChanged.emit(self.m_text)   

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    backend = Backend()

    backend.textChanged.connect(lambda text: print(text))
    engine = QQmlApplicationEngine()
    engine.rootContext().setContextProperty("backend", backend)
    engine.load(QUrl.fromLocalFile('main.qml'))
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec_())

ma​​in.qml

import QtQuick 2.10
import QtQuick.Controls 2.1
import QtQuick.Window 2.2

ApplicationWindow {
    title: qsTr("Test")
    width: 640
    height: 480
    visible: true
    Column{
        TextField{
            id: tf
            text: "Hello"
        }
        Button {
            text: qsTr("Click Me")
            onClicked: backend.text = tf.text
        } 
    }
}

现在,如果您希望文本由另一个元素提供,您只需更改以下行:onClicked: backend.text = tf.text.

Now if you want the text to be provided by another element you just have to change the line: onClicked: backend.text = tf.text.

Python 到 QML:

  1. 我不能告诉你你用这个方法做错了什么,因为你没有显示任何代码,但我确实指出了缺点.主要缺点是要使用此方法,您必须有权访问该方法,并且有两种可能性,第一种是它是一个 rootObjects,如您的第一个示例中所示或通过 objectName 搜索,但它发生了你最初寻找对象,你得到它,然后从 QML 中删除它,例如,每次更改页面时都会创建和删除 StackView 的页面,因此此方法不正确.

  1. I can not tell you what you did wrong with this method because you do not show any code, but I do indicate the disadvantages. The main disadvantage is that to use this method you must have access to the method and for that there are 2 possibilities, the first one is that it is a rootObjects as it is shown in your first example or searching through the objectName, but it happens that you initially look for the object, you get it and this is removed from QML, for example the Pages of a StackView are created and deleted every time you change pages so this method would not be correct.

第二种方法对我来说是正确的,但你没有正确使用它,不像 QtWidgets 专注于 QML 中的行和列,使用了角色.首先让我们正确实现您的代码.

The second method for me is the correct one but you have not used it correctly, unlike the QtWidgets that focus on the row and the column in QML the roles are used. First let's implement your code correctly.

First textlines 不能从 QML 访问,因为它不是 qproperty.正如我所说,您必须通过角色访问,要查看模型的角色,您可以打印 roleNames() 的结果:

First textlines is not accessible from QML since it is not a qproperty. As I said you must access through the roles, to see the roles of a model you can print the result of roleNames():

model = QStringListModel()
model.setStringList(["hi", "ho"])
print(model.roleNames())

输出:

{
    0: PySide2.QtCore.QByteArray('display'),
    1: PySide2.QtCore.QByteArray('decoration'),
    2: PySide2.QtCore.QByteArray('edit'),
    3: PySide2.QtCore.QByteArray('toolTip'),
    4: PySide2.QtCore.QByteArray('statusTip'),
    5: PySide2.QtCore.QByteArray('whatsThis')
}

如果要获取文本必须使用角色Qt::DisplayRole,其数值根据docs 是:

In the case that you want to obtain the text you must use the role Qt::DisplayRole, whose numerical value according to the docs is:

Qt::DisplayRole 0   The key data to be rendered in the form of text. (QString)

所以在 QML 中你应该使用 model.display(或只使用 display).所以正确的代码如下:

so in QML you should use model.display(or only display). so the correct code is as follows:

ma​​in.py

import sys

from PySide2.QtCore import QUrl, QStringListModel
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine  

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    model = QStringListModel()
    model.setStringList(["hi", "ho"])

    engine = QQmlApplicationEngine()
    engine.rootContext().setContextProperty("myModel", model)
    engine.load(QUrl.fromLocalFile('main.qml'))
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec_())

ma​​in.qml

import QtQuick 2.10
import QtQuick.Controls 2.1
import QtQuick.Window 2.2

ApplicationWindow {
    title: qsTr("Test")
    width: 640
    height: 480
    visible: true
    ListView{
        model: myModel
        anchors.fill: parent
        delegate: Text { text: model.display }
    }
}

如果你希望它是可编辑的,你必须使用 model.display = foo:

If you want it to be editable you must use the model.display = foo:

import QtQuick 2.10
import QtQuick.Controls 2.1
import QtQuick.Window 2.2

ApplicationWindow {
    title: qsTr("Test")
    width: 640
    height: 480
    visible: true
    ListView{
        model: myModel
        anchors.fill: parent
        delegate: 
        Column{
            Text{ 
                text: model.display 
            }
            TextField{
                onTextChanged: {
                    model.display = text
                }
            }
        }
    }
}

<小时>

还有许多其他方法可以通过 QML 与 Python/C++ 交互,但最好的方法是通过 setContextProperty 嵌入在 Python/C++ 中创建的对象.


There are many other methods to interact with Python/C++ with QML but the best methods involve embedding the objects created in Python/C++ through setContextProperty.

正如你所说的 PySide2 的文档不多,它正在实现中,你可以通过下面的 链接.存在的最多的是 PyQt5 的许多示例,因此我建议您了解两者之间的等价性并进行翻译,这种翻译并不难,因为它们是最小的更改.

As you indicate the docs of PySide2 is not much, it is being implemented and you can see it through the following link. What exists most are many examples of PyQt5 so I recommend you understand what are the equivalences between both and make a translation, this translation is not hard since they are minimal changes.

相关文章