【问题标题】:How to update QML ListView from a python thread?如何从 python 线程更新 QML ListView?
【发布时间】:2018-07-25 09:00:18
【问题描述】:

我正在编写一个从 Reddit 获取提交的小程序。 到目前为止,我的目标只是获取提交并将它们显示在 QML 的 ListView 中。我创建了一个基本的 QML 文件,并创建了一个名为“SubmissionModel”的类,它扩展了“QAbstractListModel”。我使用 PRAW 来获取 reddit 提交,效果很好。

我使用了一个名为 'fetch' 的函数,它使用从 reddit 获得的新提交填充 SubmissionModel 类。然而,这会阻止 QML 视图并使其挂起,直到 'fetch' 函数退出。

我尝试在另一个 Python 线程中运行 'fetch' 函数,这释放了 QML 视图,但遗憾的是 ListView 不再更新。我正在寻找一种可以在 QML 端更新 ListView 的方法,同时从另一个线程运行 fetch 函数。

获取函数:

def fetch():

    reddit = init_reddit()

    subreddit = reddit.subreddit('LandscapePhotography')
    counter = 0
    for submission in subreddit.submissions(None, time.time()):
        counter += 1
        print(
            "Counter: {} Submission title: {} , Submission URL: {} ,Created at: {}".format(
                counter, submission.title,
                submission.url,
                datetime.datetime.fromtimestamp(int(submission.created)).strftime('%Y-%m-%d %H:%M:%S')))

        model.addSubmission(Submission(submission.title, submission.url,
                                       datetime.datetime.fromtimestamp(int(submission.created)).strftime(
                                           '%Y-%m-%d %H:%M:%S')))
        if counter == 400:
            break

“提交模型”类:

class SubmissionModel(QAbstractListModel):
    NameRole = Qt.UserRole + 1
    LinkRole = Qt.UserRole + 2
    TimeRole = Qt.UserRole + 3

    _roles = {NameRole: b"name", LinkRole: b"link", TimeRole: b"time"}

    def __init__(self, parent=None):
        super(SubmissionModel, self).__init__(parent)

        self._submissions = []

    def addSubmission(self, submission):
        self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
        self._submissions.append(submission)
        self.endInsertRows()

    def rowCount(self, parent=QModelIndex()):
        return len(self._submissions)

    def data(self, index, role=Qt.DisplayRole):
        try:
            submission = self._submissions[index.row()]
        except IndexError:
            return QVariant()

        if role == self.NameRole:
            return submission.name()

        if role == self.LinkRole:
            return submission.link()

        if role == self.CreateTimeRole:
            return submission.time()

        return QVariant()

    def roleNames(self):
        return self._roles

没有线程的主函数

if __name__ == '__main__':
    import sys

    app = QGuiApplication(sys.argv)

    model = SubmissionModel()

    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)
    ctxt = view.rootContext()
    ctxt.setContextProperty('myModel', model)

    view.setSource(QUrl('main.qml'))
    view.show()

    fetch()

    sys.exit(app.exec_())

当应用程序像这样运行时 ^^ 它可以工作。 UI 挂起直到 fetch() 完成,当它完成时,UI 将更新为 SubmissionModel 中的所有提交

主线程

if __name__ == '__main__':
    import sys

    app = QGuiApplication(sys.argv)

    model = SubmissionModel()

    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)
    ctxt = view.rootContext()
    ctxt.setContextProperty('myModel', model)

    view.setSource(QUrl('main.qml'))
    view.show()

    **thread = threading.Thread(target=fetch)**
    **thread.start()**

    sys.exit(app.exec_())

这使得 'fetch' 函数在另一个线程上运行。虽然它可以正常工作,但每次添加提交时它都无法更新 UI。事实上,用户界面永远不会随着新提交而更新。

我的理解是 addSubmission 函数中的 self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())self.endInsertRows() 方法是为了让 UI 中的 ListView 知道模型已更新。但是,当运行这些方法的函数从另一个线程运行时,这不起作用。有人可以指出我如何从不同的线程更新 ListView 的正确方向吗?

【问题讨论】:

    标签: python multithreading pyqt qml pyqt5


    【解决方案1】:

    建议使用 PyQt 提供的工具来处理线程:

    • QThreadPoolQRunnable:
    def fetch():
        reddit = init_reddit()
    
        subreddit = reddit.subreddit("LandscapePhotography")
        counter = 0
        for submission in subreddit.submissions(None, time.time()):
            counter += 1
            print(
                "Counter: {} Submission title: {} , Submission URL: {} ,Created at: {}".format(
                    counter,
                    submission.title,
                    submission.url,
                    datetime.datetime.fromtimestamp(
                        int(submission.created)
                    ).strftime("%Y-%m-%d %H:%M:%S"),
                )
            )
    
            submission = Submission(
                submission.title,
                submission.url,
                datetime.datetime.fromtimestamp(int(submission.created)).strftime(
                    "%Y-%m-%d %H:%M:%S"
                ),
            )
            QMetaObject.invokeMethod(
                model,
                "addSubmission",
                Qt.QueuedConnection,
                Q_ARG(Submission, submission),
            )
            QThread.msleep(10)
    
            if counter == 400:
                break
    
    
    class RedditRunnable(QRunnable):
        def run(self):
            fetch()
    

    然后在main中调用:

    if __name__ == '__main__':
        import sys
    
        app = QGuiApplication(sys.argv)
    
        model = SubmissionModel()
    
        view = QQuickView()
        view.setResizeMode(QQuickView.SizeRootObjectToView)
        ctxt = view.rootContext()
        ctxt.setContextProperty('myModel', model)
    
        view.setSource(QUrl('main.qml'))
        view.show()
    
        runnable = RedditRunnable()
        QThreadPool.globalInstance().start(runnable)
    
        sys.exit(app.exec_())
    
    • 或使用信号:
    def fetch():
        reddit = init_reddit()
    
        subreddit = reddit.subreddit("LandscapePhotography")
        counter = 0
        message = Message()
        message.submissionSignal.connect(model.addSubmission, Qt.QueuedConnection)
        for submission in subreddit.submissions(None, time.time()):
            counter += 1
            print(
                "Counter: {} Submission title: {} , Submission URL: {} ,Created at: {}".format(
                    counter,
                    submission.title,
                    submission.url,
                    datetime.datetime.fromtimestamp(
                        int(submission.created)
                    ).strftime("%Y-%m-%d %H:%M:%S"),
                )
            )
    
            submission = Submission(
                submission.title,
                submission.url,
                datetime.datetime.fromtimestamp(int(submission.created)).strftime(
                    "%Y-%m-%d %H:%M:%S"
                ),
            )
            message.submissionSignal.emit(submission)
            QThread.msleep(10)
    
            if counter == 400:
                break
    
    class Message(QObject):
        submissionSignal = pyqtSignal(Submission)
    
    if __name__ == '__main__':
        import sys
    
        app = QGuiApplication(sys.argv)
    
        model = SubmissionModel()
    
        view = QQuickView()
        view.setResizeMode(QQuickView.SizeRootObjectToView)
        ctxt = view.rootContext()
        ctxt.setContextProperty('myModel', model)
    
        view.setSource(QUrl('main.qml'))
        view.show()
        thread = threading.Thread(target=fetch)
        thread.start()
    
        sys.exit(app.exec_())
    

    两种方法都可以在以下link获取。

    【讨论】:

    • 谢谢!它完全按预期工作!请问为什么在发送信号后加QThread.msleep(10)?是为了防止一次发射太多信号吗?
    • 给你时间让GUI更新数据,如果你不这样做,CPU使用率会不必要地增加太多。
    猜你喜欢
    • 1970-01-01
    • 2023-03-08
    • 2020-04-09
    • 1970-01-01
    • 2018-07-01
    • 1970-01-01
    • 2017-01-21
    • 2018-10-10
    • 1970-01-01
    相关资源
    最近更新 更多