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_())