关于qt:如何通过PySide2连接Python和QML?

How to connect Python and QML with PySide2?

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

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

所以现在有一些代码。

QML-> Python

QML文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:

1
2
3
4
5
6
7
8
from PySide2.QtCore import Slot

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

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

最后是我的main.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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_())

这样工作正常,我可以打印" Hello!"。但这是最好的方法吗?还是创建一个带有插槽的类并使用setContextProperty能够直接调用它们而不添加其他信号更好?

Python-> QML

我做不到。我尝试了不同的方法,但是没有一种有效,而且我也不知道哪种方法最适合使用。我想做的是例如显示对象列表并提供在应用程序中操纵数据的方法等。

  • 包含Javascript:
    我添加了一个带有功能的附加文件application.js,用于打印某些内容,但它可能可以用于设置文本字段的上下文等。
    然后我尝试使用QMetaObject和invokeMethod,但是参数错误等导致错误。
  • 这种方法有意义吗?实际上我不知道任何JavaScript,因此,如果没有必要,我宁愿不使用它。

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

    1
    2
    3
    4
    5
    6
    7
    from PySide2.QtCore import QStringListModel

    class ListModel(QStringListModel):

    def __init__(self):
         self.textlines = ['hi', 'ho']
         super().__init__()
  • 在main.py中,我添加了:

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

    和ListView看起来像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    ListView {
                width: 180; height: 200

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

    我收到一个错误" myModel未定义",但是我猜想它仍然无法工作,因为委托仅接受一个元素而不是一个列表。
    这是一个好方法吗?如果是的话,我该如何运作?

  • 在QML视图中是否存在完全不同的方法来处理数据?
  • 我感谢您的帮助!
    我知道Qt文档,但对此不满意。所以也许我缺少了一些东西。但是PyQt似乎比PySide2更受欢迎(至少Google搜索似乎表明了这一点),而且PySide引用经常使用PySide1或不使用QML QtQuick的做事方式...


    您的问题涉及很多方面,因此我将尝试在其答案中进行详细说明,并且此答案将不断更新,因为此类问题经常被问到,但它们是针对特定情况的解决方案,因此,我将自由给出一种通用方法,并在可能的情况下具体说明。

    QML到Python:

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

    所建议的建议是创建一个类,该类负责映射所需逻辑的数据并将其嵌入到QML中,因此,如果您在视图中进行了某些更改,则只需更改连接即可:

    例:

    main.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    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_())

    main.qml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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

    从Python到QML:

  • 我无法告诉您此方法的错误之处,因为您没有显示任何代码,但我确实指出了缺点。主要缺点是要使用此方法,您必须有权使用该方法,并且存在两种可能性,第一种是它是您的第一个示例中所示的rootObjects或在objectName中进行搜索,但是这种情况会发生最初是寻找对象的,然后获取它,并将其从QML中删除,例如,每次更改页面时都会创建并删除StackView的页面,因此此方法将不正确。

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

  • 无法从QML访问第一个textlines,因为它不是qproperty。就像我说过的那样,您必须访问角色,才能查看模型的角色,您可以打印roleNames()的结果:

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

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    {
        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,根据文档,其角色的数值为:

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

    因此在QML中,您应该使用model.display(或仅使用display)。因此正确的代码如下:

    main.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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_())

    main.qml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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 ++中创建的对象。

    正如您所指出的,PySide2的文档并不多,它正在实现中,您可以通过以下链接查看它。目前最多的是PyQt5的许多示例,所以我建议您了解两者之间的等效之处并进行翻译,因为它们是很小的更改,所以翻译并不难。