PyQt5 多线程绘制曼德勃罗集分形图

  • 2019 年 11 月 23 日
  • 笔记

本篇的代码来自于PyQt4官方demo,其功能是使用多线程,计算每一像素的的RGB,生成一张曼德勃罗集分形图,支持平移与缩放。

代码如下(我已将其改为PyQt5版本):

#!/usr/bin/env python## Copyright (C) 2010 Riverbank Computing Limited.  from PyQt5 import QtCore, QtGui, QtWidgets  DefaultCenterX = -0.647011DefaultCenterY = -0.0395159DefaultScale = 0.00403897ZoomInFactor = 0.8ZoomOutFactor = 1 / ZoomInFactorScrollStep = 20  class RenderThread(QtCore.QThread):#创建一个线程类    ColormapSize = 500       #renderedImage = QtCore.pyqtSignal(QtWidgets.QWidget, float)#only for PyQt4    renderedImage = QtCore.pyqtSignal(QtGui.QImage, float)#for PyQt5       def __init__(self, parent=None):        super().__init__(parent)        self.mutex = QtCore.QMutex() #用于线程锁,使同一时间只有一个线程能访问待保护的对象        self.condition = QtCore.QWaitCondition()        self.centerX = 0.0        self.centerY = 0.0        self.scaleFactor = 0.0        self.resultSize = QtCore.QSize()        self.restart = False        self.abort = False        self.colormap = []        for i in range(RenderThread.ColormapSize):            self.colormap.append(self.rgbFromWaveLength(450 + (i * 400.0 / RenderThread.ColormapSize)))       def __del__(self):        self.mutex.lock()        self.abort = True        self.condition.wakeOne()        self.mutex.unlock()        self.wait()            def render(self, centerX, centerY, scaleFactor, resultSize):        locker = QtCore.QMutexLocker(self.mutex)        self.centerX = centerX        self.centerY = centerY        self.scaleFactor = scaleFactor        self.resultSize = resultSize        if not self.isRunning():            self.start(QtCore.QThread.LowPriority) #以低优先级启动线程,调用run()        else:            self.restart = True                       #用于多线程的同步,一个线程调用QWaitCondition.wait() 阻塞等待,直到另一个线程调用QWaitCondition.wake() 唤醒才继续往下执行            #wakeOne会随机唤醒等待的线程中的一个            self.condition.wakeOne()                def run(self):        while True:            self.mutex.lock() #线程加锁            resultSize = self.resultSize            scaleFactor = self.scaleFactor            centerX = self.centerX            centerY = self.centerY            self.mutex.unlock()#线程解锁            halfWidth = resultSize.width() // 2            halfHeight = resultSize.height() // 2            image = QtGui.QImage(resultSize, QtGui.QImage.Format_RGB32)            NumPasses = 8            curpass = 0            while curpass < NumPasses:                MaxIterations = (1 << (2 * curpass + 6)) + 40                Limit = 4                allBlack = True                for y in range(-halfHeight, halfHeight):                    if self.restart:                        break                    if self.abort:                        return                    ay = 1j * (centerY + (y * scaleFactor))                    for x in range(-halfWidth, halfWidth):                        c0 = centerX + (x * scaleFactor) + ay                        c = c0                        numIterations = 0                        while numIterations < MaxIterations:                            numIterations += 1                            c = c*c + c0                            if abs(c) >= Limit:                                break                            numIterations += 1                            c = c*c + c0                            if abs(c) >= Limit:                                break                            numIterations += 1                            c = c*c + c0                            if abs(c) >= Limit:                                break                            numIterations += 1                            c = c*c + c0                            if abs(c) >= Limit:                                break                        if numIterations < MaxIterations:                            #image.setPixel()设定像素,设定指定坐标处的qRgb                            image.setPixel(x + halfWidth, y + halfHeight,                                           self.colormap[numIterations % RenderThread.ColormapSize])                            allBlack = False                        else:                            #image.setPixel()设定像素,设定指定坐标处的qRgb                            image.setPixel(x + halfWidth, y + halfHeight, QtGui.qRgb(0, 0, 0))                if allBlack and curpass == 0:                    curpass = 4                else:                    if not self.restart:                        self.renderedImage.emit(image, scaleFactor)                    curpass += 1            self.mutex.lock()            if not self.restart:                #用于多线程的同步,一个线程调用QWaitCondition.wait() 阻塞等待,直到另一个线程调用QWaitCondition.wake() 唤醒才继续往下执行                self.condition.wait(self.mutex)            self.restart = False            self.mutex.unlock()                     def rgbFromWaveLength(self, wave):        #根据波长返回一个RGB颜色 对象        r = 0.0        g = 0.0        b = 0.0        if wave >= 380.0 and wave <= 440.0:            r = -1.0 * (wave - 440.0) / (440.0 - 380.0)            b = 1.0        elif wave >= 440.0 and wave <= 490.0:            g = (wave - 440.0) / (490.0 - 440.0)            b = 1.0        elif wave >= 490.0 and wave <= 510.0:            g = 1.0            b = -1.0 * (wave - 510.0) / (510.0 - 490.0)        elif wave >= 510.0 and wave <= 580.0:            r = (wave - 510.0) / (580.0 - 510.0)            g = 1.0        elif wave >= 580.0 and wave <= 645.0:            r = 1.0            g = -1.0 * (wave - 645.0) / (645.0 - 580.0)        elif wave >= 645.0 and wave <= 780.0:            r = 1.0        s = 1.0        if wave > 700.0:            s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0)        elif wave < 420.0:            s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0)        r = pow(r * s, 0.8)        g = pow(g * s, 0.8)        b = pow(b * s, 0.8)        return QtGui.qRgb(r*255, g*255, b*255)
class MandelbrotWidget(QtWidgets.QWidget):    def __init__(self, parent=None):        super().__init__(parent)        self.thread = RenderThread()        self.pixmap = QtGui.QPixmap()        self.pixmapOffset = QtCore.QPoint()        self.lastDragPos = QtCore.QPoint()        self.centerX = DefaultCenterX        self.centerY = DefaultCenterY        self.pixmapScale = DefaultScale        self.curScale = DefaultScale        self.thread.renderedImage.connect(self.updatePixmap)        self.setWindowTitle("Mandelbrot")        self.setCursor(QtCore.Qt.CrossCursor) #光标形状设为十字型        self.resize(550, 400)    def paintEvent(self, event): #屏幕绘制事件 ,self.update()调用时被调用        painter = QtGui.QPainter(self)        painter.fillRect(self.rect(), QtCore.Qt.black)        if self.pixmap.isNull():            painter.setPen(QtCore.Qt.white)            painter.drawText(self.rect(), QtCore.Qt.AlignCenter,                    "Rendering initial image, please wait...")            return               if self.curScale == self.pixmapScale:            painter.drawPixmap(self.pixmapOffset, self.pixmap)        else:            scaleFactor = self.pixmapScale / self.curScale            newWidth = int(self.pixmap.width() * scaleFactor)            newHeight = int(self.pixmap.height() * scaleFactor)            newX = self.pixmapOffset.x() + (self.pixmap.width() - newWidth) / 2            newY = self.pixmapOffset.y() + (self.pixmap.height() - newHeight) / 2            painter.save()            painter.translate(newX, newY)            painter.scale(scaleFactor, scaleFactor)            #exposed, _ = painter.matrix().inverted() #only for PyQt4            exposed, _ = painter.transform().inverted() #变换矩阵的逆矩阵,for PyQt5             exposed = exposed.mapRect(self.rect()).adjusted(-1, -1, 1, 1)            painter.drawPixmap(exposed, self.pixmap, exposed)            painter.restore()        text = "Use mouse wheel or the '+' and '-' keys to zoom. Press and "                 "hold left mouse button to scroll."        metrics = painter.fontMetrics()        textWidth = metrics.width(text)        painter.setPen(QtCore.Qt.NoPen)        painter.setBrush(QtGui.QColor(0, 0, 0, 127))        painter.drawRect((self.width() - textWidth) / 2 - 5, 0, textWidth + 10,                metrics.lineSpacing() + 5)        painter.setPen(QtCore.Qt.white)        painter.drawText((self.width() - textWidth) / 2,                metrics.leading() + metrics.ascent(), text)       def resizeEvent(self, event):        self.thread.render(self.centerX, self.centerY, self.curScale, self.size())    def keyPressEvent(self, event):# 键盘事件的响应        if event.key() == QtCore.Qt.Key_Plus:            self.zoom(ZoomInFactor)        elif event.key() == QtCore.Qt.Key_Minus:            self.zoom(ZoomOutFactor)        elif event.key() == QtCore.Qt.Key_Left:            self.scroll(-ScrollStep, 0)        elif event.key() == QtCore.Qt.Key_Right:            self.scroll(+ScrollStep, 0)        elif event.key() == QtCore.Qt.Key_Down:            self.scroll(0, -ScrollStep)        elif event.key() == QtCore.Qt.Key_Up:            self.scroll(0, +ScrollStep)        else:            super().keyPressEvent(event)                def wheelEvent(self, event):# 鼠标滚轮事件的响应        #numDegrees = event.delta() / 8 # only for PyQt4        numDegrees = event.angleDelta().y() / 8  #for PyQt5        numSteps = numDegrees / 15.0        self.zoom(pow(ZoomInFactor, numSteps))            def mousePressEvent(self, event): #鼠标按下事件的响应        if event.buttons() == QtCore.Qt.LeftButton:            self.lastDragPos = QtCore.QPoint(event.pos())                def mouseMoveEvent(self, event): # 鼠标移动事件的响应        if event.buttons() & QtCore.Qt.LeftButton:            self.pixmapOffset += event.pos() - self.lastDragPos            self.lastDragPos = QtCore.QPoint(event.pos())            self.update()                def mouseReleaseEvent(self, event):# 鼠标释放事件的响应        if event.button() == QtCore.Qt.LeftButton:            self.pixmapOffset += event.pos() - self.lastDragPos            self.lastDragPos = QtCore.QPoint()            deltaX = (self.width() - self.pixmap.width()) / 2 - self.pixmapOffset.x()            deltaY = (self.height() - self.pixmap.height()) / 2 - self.pixmapOffset.y()            self.scroll(deltaX, deltaY)                def updatePixmap(self, image, scaleFactor):        if not self.lastDragPos.isNull():            return        self.pixmap = QtGui.QPixmap.fromImage(image)        self.pixmapOffset = QtCore.QPoint()        self.lastDragPosition = QtCore.QPoint()        self.pixmapScale = scaleFactor        self.update()            def zoom(self, zoomFactor):        self.curScale *= zoomFactor        self.update()        self.thread.render(self.centerX, self.centerY, self.curScale,                self.size())                    def scroll(self, deltaX, deltaY):        self.centerX += deltaX * self.curScale        self.centerY += deltaY * self.curScale        self.update()        self.thread.render(self.centerX, self.centerY, self.curScale, self.size())
if __name__ == '__main__':    import sys    app = QtWidgets.QApplication(sys.argv)    widget = MandelbrotWidget()    widget.show()    sys.exit(app.exec_())