Updated image, SVG, and print exporters. Image export works well; SVG and print still need work.

Added ability to run examples with a specific Qt graphics system
This commit is contained in:
Luke Campagnola 2012-12-26 16:29:29 -05:00
parent a157d9c4fa
commit 8899e8d858
13 changed files with 194 additions and 100 deletions

View File

@ -126,6 +126,9 @@ class ExampleLoader(QtGui.QMainWindow):
extra.append('pyqt')
elif self.ui.pysideCheck.isChecked():
extra.append('pyside')
if self.ui.forceGraphicsCheck.isChecked():
extra.append(str(self.ui.forceGraphicsCombo.currentText()))
if fn is None:
return
@ -163,22 +166,26 @@ def buildFileList(examples, files=None):
buildFileList(val, files)
return files
def testFile(name, f, exe, lib):
def testFile(name, f, exe, lib, graphicsSystem):
global path
fn = os.path.join(path,f)
#print "starting process: ", fn
os.chdir(path)
sys.stdout.write(name)
sys.stdout.flush()
import1 = "import %s" % lib if lib != '' else ''
import2 = os.path.splitext(os.path.split(fn)[1])[0]
graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
code = """
try:
%s
import pyqtgraph as pg
%s
import %s
import sys
print("test complete")
sys.stdout.flush()
import pyqtgraph as pg
import time
while True: ## run a little event loop
pg.QtGui.QApplication.processEvents()
@ -187,7 +194,7 @@ except:
print("test failed")
raise
""" % ("import %s" % lib if lib != '' else "", os.path.splitext(os.path.split(fn)[1])[0])
""" % (import1, import2, graphicsSystem)
#print code
process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
process.stdin.write(code.encode('UTF-8'))

View File

@ -57,6 +57,36 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="forceGraphicsCheck">
<property name="text">
<string>Force Graphics System:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="forceGraphicsCombo">
<item>
<property name="text">
<string>native</string>
</property>
</item>
<item>
<property name="text">
<string>raster</string>
</property>
</item>
<item>
<property name="text">
<string>opengl</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="loadBtn">
<property name="text">

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui'
#
# Created: Fri Oct 26 07:53:55 2012
# Created: Mon Dec 24 00:33:38 2012
# by: PyQt4 UI code generator 4.9.1
#
# WARNING! All changes made in this file will be lost!
@ -44,6 +44,18 @@ class Ui_Form(object):
self.pysideCheck.setObjectName(_fromUtf8("pysideCheck"))
self.horizontalLayout.addWidget(self.pysideCheck)
self.verticalLayout.addLayout(self.horizontalLayout)
self.horizontalLayout_2 = QtGui.QHBoxLayout()
self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2"))
self.forceGraphicsCheck = QtGui.QCheckBox(self.layoutWidget)
self.forceGraphicsCheck.setObjectName(_fromUtf8("forceGraphicsCheck"))
self.horizontalLayout_2.addWidget(self.forceGraphicsCheck)
self.forceGraphicsCombo = QtGui.QComboBox(self.layoutWidget)
self.forceGraphicsCombo.setObjectName(_fromUtf8("forceGraphicsCombo"))
self.forceGraphicsCombo.addItem(_fromUtf8(""))
self.forceGraphicsCombo.addItem(_fromUtf8(""))
self.forceGraphicsCombo.addItem(_fromUtf8(""))
self.horizontalLayout_2.addWidget(self.forceGraphicsCombo)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.loadBtn = QtGui.QPushButton(self.layoutWidget)
self.loadBtn.setObjectName(_fromUtf8("loadBtn"))
self.verticalLayout.addWidget(self.loadBtn)
@ -62,5 +74,9 @@ class Ui_Form(object):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8))
self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCheck.setText(QtGui.QApplication.translate("Form", "Force Graphics System:", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCombo.setItemText(0, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCombo.setItemText(1, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCombo.setItemText(2, QtGui.QApplication.translate("Form", "opengl", None, QtGui.QApplication.UnicodeUTF8))
self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -2,8 +2,8 @@
# Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui'
#
# Created: Fri Oct 26 07:53:57 2012
# by: pyside-uic 0.2.13 running on PySide 1.1.0
# Created: Mon Dec 24 00:33:39 2012
# by: pyside-uic 0.2.13 running on PySide 1.1.2
#
# WARNING! All changes made in this file will be lost!
@ -39,6 +39,18 @@ class Ui_Form(object):
self.pysideCheck.setObjectName("pysideCheck")
self.horizontalLayout.addWidget(self.pysideCheck)
self.verticalLayout.addLayout(self.horizontalLayout)
self.horizontalLayout_2 = QtGui.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.forceGraphicsCheck = QtGui.QCheckBox(self.layoutWidget)
self.forceGraphicsCheck.setObjectName("forceGraphicsCheck")
self.horizontalLayout_2.addWidget(self.forceGraphicsCheck)
self.forceGraphicsCombo = QtGui.QComboBox(self.layoutWidget)
self.forceGraphicsCombo.setObjectName("forceGraphicsCombo")
self.forceGraphicsCombo.addItem("")
self.forceGraphicsCombo.addItem("")
self.forceGraphicsCombo.addItem("")
self.horizontalLayout_2.addWidget(self.forceGraphicsCombo)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.loadBtn = QtGui.QPushButton(self.layoutWidget)
self.loadBtn.setObjectName("loadBtn")
self.verticalLayout.addWidget(self.loadBtn)
@ -57,5 +69,9 @@ class Ui_Form(object):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8))
self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCheck.setText(QtGui.QApplication.translate("Form", "Force Graphics System:", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCombo.setItemText(0, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCombo.setItemText(1, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8))
self.forceGraphicsCombo.setItemText(2, QtGui.QApplication.translate("Form", "opengl", None, QtGui.QApplication.UnicodeUTF8))
self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -3,6 +3,14 @@ import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
if 'pyside' in sys.argv: ## should force example to use PySide instead of PyQt
import PySide
from PySide import QtGui
elif 'pyqt' in sys.argv:
import PyQt4
from PyQt4 import QtGui
else:
from pyqtgraph.Qt import QtGui
for gs in ['raster', 'native', 'opengl']:
if gs in sys.argv:
QtGui.QApplication.setGraphicsSystem(gs)
break

View File

@ -27,12 +27,12 @@ class ImageExporter(Exporter):
def widthChanged(self):
sr = self.getSourceRect()
ar = sr.height() / sr.width()
ar = float(sr.height()) / sr.width()
self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged)
def heightChanged(self):
sr = self.getSourceRect()
ar = sr.width() / sr.height()
ar = float(sr.width()) / sr.height()
self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged)
def parameters(self):
@ -51,6 +51,8 @@ class ImageExporter(Exporter):
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
sourceRect = self.getSourceRect()
#self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32)
#self.png.fill(pyqtgraph.mkColor(self.params['background']))
bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte)
@ -60,11 +62,19 @@ class ImageExporter(Exporter):
bg[:,:,2] = color.red()
bg[:,:,3] = color.alpha()
self.png = pg.makeQImage(bg, alpha=True)
## set resolution of image:
origTargetRect = self.getTargetRect()
resolutionScale = targetRect.width() / origTargetRect.width()
#self.png.setDotsPerMeterX(self.png.dotsPerMeterX() * resolutionScale)
#self.png.setDotsPerMeterY(self.png.dotsPerMeterY() * resolutionScale)
painter = QtGui.QPainter(self.png)
#dtr = painter.deviceTransform()
try:
self.setExportMode(True, {'antialias': self.params['antialias'], 'background': self.params['background']})
self.setExportMode(True, {'antialias': self.params['antialias'], 'background': self.params['background'], 'painter': painter, 'resolutionScale': resolutionScale})
painter.setRenderHint(QtGui.QPainter.Antialiasing, self.params['antialias'])
self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect)
self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect))
finally:
self.setExportMode(False)
painter.end()

View File

@ -3,8 +3,8 @@ from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import re
#__all__ = ['PrintExporter']
__all__ = [] ## Printer is disabled for now--does not work very well.
__all__ = ['PrintExporter']
#__all__ = [] ## Printer is disabled for now--does not work very well.
class PrintExporter(Exporter):
Name = "Printer"
@ -38,9 +38,15 @@ class PrintExporter(Exporter):
if dialog.exec_() != QtGui.QDialog.Accepted:
return;
#dpi = QtGui.QDesktopWidget().physicalDpiX()
#self.svg.setSize(QtCore.QSize(100,100))
#self.svg.setResolution(600)
res = printer.resolution()
#res = printer.resolution()
sr = self.getSourceRect()
#res = sr.width() * .4 / (self.params['width'] * 100 / 2.54)
res = QtGui.QDesktopWidget().physicalDpiX()
printer.setResolution(res)
rect = printer.pageRect()
center = rect.center()
h = self.params['height'] * res * 100. / 2.54
@ -52,8 +58,8 @@ class PrintExporter(Exporter):
sourceRect = self.getSourceRect()
painter = QtGui.QPainter(printer)
try:
self.setExportMode(True)
self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect)
self.setExportMode(True, {'painter': painter})
self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect))
finally:
self.setExportMode(False)
painter.end()

View File

@ -46,7 +46,7 @@ class SVGExporter(Exporter):
painter = QtGui.QPainter(self.svg)
try:
self.setExportMode(True)
self.setExportMode(True, {'painter': painter,})
self.render(painter, QtCore.QRectF(targetRect), sourceRect)
finally:
self.setExportMode(False)

View File

@ -54,3 +54,8 @@ class ArrowItem(QtGui.QGraphicsPathItem):
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
QtGui.QGraphicsPathItem.paint(self, p, *args)
def shape(self):
#if not self.opts['pxMode']:
#return QtGui.QGraphicsPathItem.shape(self)
return self.path

View File

@ -28,10 +28,13 @@ class GraphicsItem(object):
self._viewWidget = None
self._viewBox = None
self._connectedView = None
self._exportOpts = False ## If False, not currently exporting. Otherwise, contains dict of export options.
if register:
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
def getViewWidget(self):
"""
Return the view widget for this item. If the scene has multiple views, only the first view is returned.
@ -82,6 +85,9 @@ class GraphicsItem(object):
Return the transform that converts local item coordinates to device coordinates (usually pixels).
Extends deviceTransform to automatically determine the viewportTransform.
"""
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
return self._exportOpts['painter'].deviceTransform()
if viewportTransform is None:
view = self.getViewWidget()
if view is None:
@ -476,4 +482,16 @@ class GraphicsItem(object):
return tree
def setExportMode(self, export, opts):
"""
This method is called by exporters to inform items that they are being drawn for export
with a specific set of options. Items access these via self._exportOptions.
When exporting is complete, _exportOptions is set to False.
"""
if export:
self._exportOpts = opts
#if 'antialias' not in opts:
#self._exportOpts['antialias'] = True
else:
self._exportOpts = False

View File

@ -52,7 +52,6 @@ class PlotCurveItem(GraphicsObject):
self.clear()
self.path = None
self.fillPath = None
self.exportOpts = False
## this is disastrous for performance.
@ -404,8 +403,8 @@ class PlotCurveItem(GraphicsObject):
path = self.path
prof.mark('generate path')
if self.exportOpts is not False:
aa = self.exportOpts['antialias']
if self._exportOpts is not False:
aa = self._exportOpts.get('antialias', True)
else:
aa = self.opts['antialias']
@ -487,13 +486,6 @@ class PlotCurveItem(GraphicsObject):
ev.accept()
self.sigClicked.emit(self)
def setExportMode(self, export, opts):
if export:
self.exportOpts = opts
if 'antialias' not in opts:
self.exportOpts['antialias'] = True
else:
self.exportOpts = False
class ROIPlotItem(PlotCurveItem):
"""Plot curve that monitors an ROI and image for changes to automatically replot."""

View File

@ -239,7 +239,6 @@ class ScatterPlotItem(GraphicsObject):
'useCache': True, ## If useCache is False, symbols are re-drawn on every paint.
'antialias': pg.getConfigOption('antialias'),
}
self.exportOpts = False
self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False)
@ -546,7 +545,7 @@ class ScatterPlotItem(GraphicsObject):
if invalidate:
self.invalidate()
def getSpotOpts(self, recs):
def getSpotOpts(self, recs, scale=1.0):
if recs.ndim == 0:
rec = recs
symbol = rec['symbol']
@ -561,11 +560,12 @@ class ScatterPlotItem(GraphicsObject):
brush = rec['brush']
if brush is None:
brush = self.opts['brush']
return (symbol, size, fn.mkPen(pen), fn.mkBrush(brush))
return (symbol, size*scale, fn.mkPen(pen), fn.mkBrush(brush))
else:
recs = recs.copy()
recs['symbol'][np.equal(recs['symbol'], None)] = self.opts['symbol']
recs['size'][np.equal(recs['size'], -1)] = self.opts['size']
recs['size'] *= scale
recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen'])
recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush'])
return recs
@ -675,18 +675,20 @@ class ScatterPlotItem(GraphicsObject):
rect = QtCore.QRectF(y, x, h, w)
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
def setExportMode(self, export, opts):
if export:
self.exportOpts = opts
if 'antialias' not in opts:
self.exportOpts['antialias'] = True
else:
self.exportOpts = False
def setExportMode(self, *args, **kwds):
GraphicsObject.setExportMode(self, *args, **kwds)
self.invalidate()
def paint(self, p, *args):
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.boundingRect())
if self._exportOpts is not False:
aa = self._exportOpts.get('antialias', True)
scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed
else:
aa = self.opts['antialias']
scale = 1.0
if self.opts['pxMode'] is True:
atlas = self.fragmentAtlas.getAtlas()
#arr = fn.imageToArray(atlas.toImage(), copy=True)
@ -701,13 +703,9 @@ class ScatterPlotItem(GraphicsObject):
p.resetTransform()
if not USE_PYSIDE and self.opts['useCache'] and self.exportOpts is False:
if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False:
p.drawPixmapFragments(self.fragments, atlas)
else:
if self.exportOpts is not False:
aa = self.exportOpts['antialias']
else:
aa = self.opts['antialias']
p.setRenderHint(p.Antialiasing, aa)
for i in range(len(self.data)):
@ -715,23 +713,20 @@ class ScatterPlotItem(GraphicsObject):
frag = self.fragments[i]
p.resetTransform()
p.translate(frag.x, frag.y)
drawSymbol(p, *self.getSpotOpts(rec))
drawSymbol(p, *self.getSpotOpts(rec, scale))
else:
if self.picture is None:
self.picture = QtGui.QPicture()
p2 = QtGui.QPainter(self.picture)
for rec in self.data:
if scale != 1.0:
rec = rec.copy()
rec['size'] *= scale
p2.resetTransform()
p2.translate(rec['x'], rec['y'])
drawSymbol(p2, *self.getSpotOpts(rec))
drawSymbol(p2, *self.getSpotOpts(rec, scale))
p2.end()
if self.exportOpts is not False:
aa = self.exportOpts['antialias']
else:
aa = self.opts['antialias']
p.setRenderHint(p.Antialiasing, aa)
self.picture.play(p)

View File

@ -27,25 +27,27 @@ class TextItem(UIGraphicsItem):
#*angle* Angle in degrees to rotate text (note that the rotation assigned in this item's
#transformation will be ignored)
self.anchor = pg.Point(anchor)
#self.angle = 0
UIGraphicsItem.__init__(self)
self.textItem = QtGui.QGraphicsTextItem()
self.textItem.setParentItem(self)
self.lastTransform = None
self._bounds = QtCore.QRectF()
if html is None:
self.setText(text, color)
else:
self.setHtml(html)
self.anchor = pg.Point(anchor)
self.fill = pg.mkBrush(fill)
self.border = pg.mkPen(border)
self.angle = angle
#self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
self.rotate(angle)
self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
def setText(self, text, color=(200,200,200)):
color = pg.mkColor(color)
self.textItem.setDefaultTextColor(color)
self.textItem.setPlainText(text)
self.updateText()
#html = '<span style="color: #%s; text-align: center;">%s</span>' % (color, text)
#self.setHtml(html)
@ -70,38 +72,41 @@ class TextItem(UIGraphicsItem):
self.textItem.setFont(*args)
self.updateText()
def updateText(self):
self.viewRangeChanged()
#def getImage(self):
#if self.img is None:
#br = self.textItem.boundingRect()
#img = QtGui.QImage(int(br.width()), int(br.height()), QtGui.QImage.Format_ARGB32)
#p = QtGui.QPainter(img)
#self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
#p.end()
#self.img = img
#return self.img
#def setAngle(self, angle):
#self.angle = angle
#self.updateText()
def textBoundingRect(self):
## return the bounds of the text box in device coordinates
pos = self.mapToDevice(QtCore.QPointF(0,0))
if pos is None:
return None
tbr = self.textItem.boundingRect()
return QtCore.QRectF(pos.x() - tbr.width()*self.anchor.x(), pos.y() - tbr.height()*self.anchor.y(), tbr.width(), tbr.height())
def updateText(self):
## Needed to maintain font size when rendering to image with increased resolution
self.textItem.resetTransform()
#self.textItem.rotate(self.angle)
if self._exportOpts is not False and 'resolutionScale' in self._exportOpts:
s = self._exportOpts['resolutionScale']
self.textItem.scale(s, s)
#br = self.textItem.mapRectToParent(self.textItem.boundingRect())
self.textItem.setPos(0,0)
br = self.textItem.boundingRect()
apos = self.textItem.mapToParent(pg.Point(br.width()*self.anchor.x(), br.height()*self.anchor.y()))
#print br, apos
self.textItem.setPos(-apos.x(), -apos.y())
#def textBoundingRect(self):
### return the bounds of the text box in device coordinates
#pos = self.mapToDevice(QtCore.QPointF(0,0))
#if pos is None:
#return None
#tbr = self.textItem.boundingRect()
#return QtCore.QRectF(pos.x() - tbr.width()*self.anchor.x(), pos.y() - tbr.height()*self.anchor.y(), tbr.width(), tbr.height())
def viewRangeChanged(self):
br = self.textBoundingRect()
if br is None:
return
self.prepareGeometryChange()
self._bounds = fn.invertQTransform(self.deviceTransform()).mapRect(br)
#print self._bounds
self.updateText()
def boundingRect(self):
return self._bounds
return self.textItem.mapToParent(self.textItem.boundingRect()).boundingRect()
def paint(self, p, *args):
tr = p.transform()
@ -110,23 +115,9 @@ class TextItem(UIGraphicsItem):
self.viewRangeChanged()
self.lastTransform = tr
tbr = self.textBoundingRect()
#p.setPen(pg.mkPen('r'))
#p.drawRect(self.boundingRect())
p.setPen(self.border)
p.setBrush(self.fill)
p.setRenderHint(p.Antialiasing, True)
p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect()))
#p.fillRect(tbr)
p.resetTransform()
#p.drawRect(tbr)
p.translate(tbr.left(), tbr.top())
p.rotate(self.angle)
p.drawRect(QtCore.QRectF(0, 0, tbr.width(), tbr.height()))
self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None)