从线程修改 QGraphicsItem 的正确方法?

Posted

技术标签:

【中文标题】从线程修改 QGraphicsItem 的正确方法?【英文标题】:proper way of modifying a QGraphicsItem from a thread? 【发布时间】:2014-02-26 03:21:50 【问题描述】:

我正在制作吉他指法谱软件,当开始播放时,图形每 16 个音符更新一次(例如,通过将光标向右移动一个空格)。我无法弄清楚如何从线程更新 QGraphicsItems 而没有问题。在下面的示例中(从原始程序简化),我有一个 QThread 进行播放并每隔 0.02 秒在右侧重绘一个 QGraphicsRectItem。问题是矩形经常冻结并且即使在播放停止后仍然保持冻结状态。

谁能告诉我从线程更新 QGraphicsView 的更好方法是什么?

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import random
import sys
import threading

from PyQt4 import QtGui, QtCore

class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        # add TablatureWindow
        self.tabWidget = QtGui.QTabWidget()
        self.setCentralWidget(self.tabWidget)
        self.setWindowTitle('Tablature Editor')    

        self.tablatureWindow = TablatureWindow(self)
        self.tabWidget.removeTab(0)
        self.tabWidget.addTab(self.tablatureWindow, 'Untitled')
        self.tablatureWindow.setFocus()

        self.positionWindow()       # center and enlargen window

        self.show()

    def positionWindow(self):
        qr = self.frameGeometry()
        cp = QtGui.QDesktopWidget().availableGeometry().center()
        width = QtGui.QDesktopWidget().availableGeometry().width() - 100
        height = QtGui.QDesktopWidget().availableGeometry().height() - 100
        self.resize(width, height)
        qr = self.frameGeometry()
        cp = QtGui.QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())  


class TablatureWindow(QtGui.QGraphicsView):    
    def __init__(*args, **kwargs):
        start_time = time.time()

        self = args[0] 
        self._parent = args[1]

        QtGui.QGraphicsView.__init__(self)    
        self.scene = QtGui.QGraphicsScene(self)
        self.setScene(self.scene)

        self.scene.setSceneRect(QtCore.QRectF(0, 0, 20000, 2000))

        self.cursorItem = QtGui.QGraphicsRectItem(100, 100, 20, 20)
        self.cursorItem.setBrush(QtCore.Qt.black)
        self.scene.addItem(self.cursorItem)
        self.centerOn(0,0)

        self.isPlaying = False


    def keyPressEvent(self, e):
        key = e.key()

        # "p" starts or stops playback
        if key == QtCore.Qt.Key_P:
            if self.isPlaying == False:
                self.beginPlayback()
            else:
                self.stopPlayback()

    def beginPlayback(self):
        print('begin')
        self.isPlaying = True

        self.playbackThread = PlaybackThread(self)
        self.playbackThread.start()

    def stopPlayback(self):
        print('stop')
        self.isPlaying = False

        self.playbackThread.stopPlayback()


class PlaybackThread(QtCore.QThread):
    def __init__(self, parent):
        QtCore.QThread.__init__(self)
        self._parent = parent

        self.doStopThread = False

    def run(self):
        self.startTime = time.time()

        dt = 0.02    # move every dt seconds

        for i in range(0, 2000):            # keep going right for 1000 spaces
            if not self.doStopThread:

                x = self._parent.cursorItem.rect().x()
                y = self._parent.cursorItem.rect().y()
                w = self._parent.cursorItem.rect().width()
                h = self._parent.cursorItem.rect().height()

                self._parent.cursorItem.setRect(x+10, y, w, h)

                ideal_dt = (i+1) * dt + self.startTime
                dt2 = ideal_dt-time.time()
                time.sleep(dt2)             # sleep for remaining time


    def stopPlayback(self):
        self.doStopThread = True

    def __del__(self):
        self.wait()


def main():

    app = QtGui.QApplication(sys.argv)
    ex = MainWindow()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main() 

【问题讨论】:

【参考方案1】:

我想我已经通过用 QTimer 替换线程找到了部分答案:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import random
import sys
import threading

from PyQt4 import QtGui, QtCore

class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        # add TablatureWindow
        self.tabWidget = QtGui.QTabWidget()
        self.setCentralWidget(self.tabWidget)
        self.setWindowTitle('Tablature Editor')    

        self.tablatureWindow = TablatureWindow(self)
        self.tabWidget.removeTab(0)
        self.tabWidget.addTab(self.tablatureWindow, 'Untitled')
        self.tablatureWindow.setFocus()

        self.positionWindow()       # center and enlargen window

        self.show()

    def positionWindow(self):
        qr = self.frameGeometry()
        cp = QtGui.QDesktopWidget().availableGeometry().center()
        width = QtGui.QDesktopWidget().availableGeometry().width() - 100
        height = QtGui.QDesktopWidget().availableGeometry().height() - 100
        self.resize(width, height)
        qr = self.frameGeometry()
        cp = QtGui.QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())  


class TablatureWindow(QtGui.QGraphicsView):    
    def __init__(*args, **kwargs):
        start_time = time.time()

        self = args[0] 
        self._parent = args[1]

        QtGui.QGraphicsView.__init__(self)    
        self.scene = QtGui.QGraphicsScene(self)
        self.setScene(self.scene)

        self.scene.setSceneRect(QtCore.QRectF(0, 0, 20000, 2000))

        self.cursorItem = QtGui.QGraphicsRectItem(100, 100, 20, 20)
        self.cursorItem.setBrush(QtCore.Qt.black)
        self.scene.addItem(self.cursorItem)
        self.centerOn(0,0)

        self.isPlaying = False


    def keyPressEvent(self, e):
        key = e.key()

        # "p" starts or stops playback
        if key == QtCore.Qt.Key_P:
            if self.isPlaying == False:
                self.beginPlayback()
            else:
                self.stopPlayback()

    def beginPlayback(self):
        print('begin')
        self.isPlaying = True

        self.playback = Playback(self)

    def stopPlayback(self):
        print('stop')
        self.isPlaying = False

        self.playback.stopPlayback()


class Playback:
    def __init__(self, parent):
        self._parent = parent

        self.timer = QtCore.QTimer()
        QtCore.QObject.connect(
            self.timer,
            QtCore.SIGNAL("timeout()"),
            self.doStuff)

        self.timer.start(20)        # 20 ms

    def doStuff(self):
        x = self._parent.cursorItem.rect().x()
        y = self._parent.cursorItem.rect().y()
        w = self._parent.cursorItem.rect().width()
        h = self._parent.cursorItem.rect().height()

        self._parent.cursorItem.setRect(x+10, y, w, h)

        self._parent.update()

    def stopPlayback(self):
        self.timer.stop()

def main():

    app = QtGui.QApplication(sys.argv)
    ex = MainWindow()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main() 

我不再看到冻结(无论如何),但我确实看到了一些图形故障,例如当我滚动窗口时移动矩形留下的小矩形。

【讨论】:

【参考方案2】:

您不能使用非主线程中的 GUI 对象。我对Cannot send posted events for objects in another thread 的回答完全适用于这里。

【讨论】:

以上是关于从线程修改 QGraphicsItem 的正确方法?的主要内容,如果未能解决你的问题,请参考以下文章

PySide/PyQT5:如何从 QGraphicsItem 发出信号?

PyQt4 - 拖动后 QGraphicsItem 位置未正确映射到场景中

QGraphicsItem - 项目转换失败

使用 mouseMoveEvent 约束 QGraphicsItem 移动

从 QGraphicsItem 上的上下文菜单操作中获取事件

将鼠标事件从 QGraphicsItem 传递给 QGraphicsScene