merge from dev

This commit is contained in:
Luke Campagnola 2012-08-14 10:22:05 -04:00
commit 0f97ac77e2
74 changed files with 2095 additions and 1401 deletions

View File

@ -1,4 +1,5 @@
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.python2_3 import sortList
#try:
#from PyQt4 import QtOpenGL
#HAVE_OPENGL = True
@ -505,18 +506,19 @@ class GraphicsScene(QtGui.QGraphicsScene):
menusToAdd = []
while item is not self:
item = item.parentItem()
if item is None:
item = self
if not hasattr(item, "getContextMenus"):
continue
subMenus = item.getContextMenus(event)
if subMenus is None:
continue
if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus
subMenus = [subMenus]
for sm in subMenus:
menusToAdd.append(sm)

30
Qt.py
View File

@ -1,15 +1,23 @@
## Do all Qt imports from here to allow easier PyQt / PySide compatibility
#from PySide import QtGui, QtCore, QtOpenGL, QtSvg
from PyQt4 import QtGui, QtCore
try:
from PyQt4 import QtSvg
except ImportError:
pass
try:
from PyQt4 import QtOpenGL
except ImportError:
pass
USE_PYSIDE = False ## If False, import PyQt4. If True, import PySide
## Note that when switching between PyQt and PySide, all template
## files (*.ui) must be rebuilt for the target library.
if USE_PYSIDE:
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
import PySide
VERSION_INFO = 'PySide ' + PySide.__version__
else:
from PyQt4 import QtGui, QtCore
try:
from PyQt4 import QtSvg
except ImportError:
pass
try:
from PyQt4 import QtOpenGL
except ImportError:
pass
if not hasattr(QtCore, 'Signal'):
QtCore.Signal = QtCore.pyqtSignal
VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR

View File

@ -76,7 +76,7 @@ class SRTTransform(QtGui.QTransform):
m = pg.SRTTransform3D(m)
angle, axis = m.getRotation()
if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1):
print angle, axis
print("angle: %s axis: %s" % (str(angle), str(axis)))
raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.")
self._state = {
'pos': Point(m.getTranslation()),

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from Qt import QtCore, QtGui
from Vector import Vector
from SRTTransform import SRTTransform
from .Qt import QtCore, QtGui
from .Vector import Vector
from .SRTTransform import SRTTransform
import pyqtgraph as pg
import numpy as np
import scipy.linalg
@ -136,15 +136,15 @@ class SRTTransform3D(QtGui.QMatrix4x4):
try:
evals, evecs = scipy.linalg.eig(r)
except:
print "Rotation matrix:", r
print "Scale:", scale
print "Original matrix:", m
print("Rotation matrix: %s" % str(r))
print("Scale: %s" % str(scale))
print("Original matrix: %s" % str(m))
raise
eigIndex = np.argwhere(np.abs(evals-1) < 1e-7)
if len(eigIndex) < 1:
print "eigenvalues:", evals
print "eigenvectors:", evecs
print "index:", eigIndex, evals-1
print("eigenvalues: %s" % str(evals))
print("eigenvectors: %s" % str(evecs))
print("index: %s, %s" % (str(eigIndex), str(evals-1)))
raise Exception("Could not determine rotation axis.")
axis = evecs[eigIndex[0,0]].real
axis /= ((axis**2).sum())**0.5
@ -259,23 +259,23 @@ if __name__ == '__main__':
tr3 = QtGui.QTransform()
tr3.translate(20, 0)
tr3.rotate(45)
print "QTransform -> Transform:", SRTTransform(tr3)
print("QTransform -> Transform: %s" % str(SRTTransform(tr3)))
print "tr1:", tr1
print("tr1: %s" % str(tr1))
tr2.translate(20, 0)
tr2.rotate(45)
print "tr2:", tr2
print("tr2: %s" % str(tr2))
dt = tr2/tr1
print "tr2 / tr1 = ", dt
print("tr2 / tr1 = %s" % str(dt))
print "tr2 * tr1 = ", tr2*tr1
print("tr2 * tr1 = %s" % str(tr2*tr1))
tr4 = SRTTransform()
tr4.scale(-1, 1)
tr4.rotate(30)
print "tr1 * tr4 = ", tr1*tr4
print("tr1 * tr4 = %s" % str(tr1*tr4))
w1 = widgets.TestROI((19,19), (22, 22), invertible=True)
#w2 = widgets.TestROI((0,0), (150, 150))

View File

@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from Qt import QtGui, QtCore
from .Qt import QtGui, QtCore
import numpy as np
class Vector(QtGui.QVector3D):

View File

@ -10,6 +10,7 @@ of a large group of widgets.
from .Qt import QtCore, QtGui
import weakref, inspect
from .python2_3 import asUnicode
__all__ = ['WidgetGroup']

View File

@ -1,18 +1,21 @@
# -*- coding: utf-8 -*-
REVISION = None
### import all the goodies and add some helper functions for easy CLI use
## 'Qt' is a local module; it is intended mainly to cover up the differences
## between PyQt4 and PySide.
from .Qt import QtGui
from .Qt import QtGui
## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause)
#if QtGui.QApplication.instance() is None:
#app = QtGui.QApplication([])
import sys
import os, sys
## check python version
if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] != 7):
## Allow anything >= 2.7
if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] < 7):
raise Exception("Pyqtgraph requires Python version 2.7 (this is %d.%d)" % (sys.version_info[0], sys.version_info[1]))
## helpers for 2/3 compatibility
@ -30,13 +33,15 @@ else:
useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff.
CONFIG_OPTIONS = {
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
'foregroundColor': (200,200,200),
'backgroundColor': (0,0,0),
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
'background': (0, 0, 0), ## default background for GraphicsWidget
'antialias': False,
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
}
def setConfigOption(opt, value):
CONFIG_OPTIONS[opt] = value
@ -44,6 +49,23 @@ def getConfigOption(opt):
return CONFIG_OPTIONS[opt]
def systemInfo():
print("sys.platform: %s" % sys.platform)
print("sys.version: %s" % sys.version)
from .Qt import VERSION_INFO
print("qt bindings: %s" % VERSION_INFO)
global REVISION
if REVISION is None: ## this code was probably checked out from bzr; look up the last-revision file
lastRevFile = os.path.join(os.path.dirname(__file__), '.bzr', 'branch', 'last-revision')
if os.path.exists(lastRevFile):
REVISION = open(lastRevFile, 'r').read().strip()
print("pyqtgraph: %s" % REVISION)
print("config:")
import pprint
pprint.pprint(CONFIG_OPTIONS)
## Rename orphaned .pyc files. This is *probably* safe :)
def renamePyc(startDir):
@ -105,7 +127,7 @@ def importAll(path, excludes=()):
globals()[k] = getattr(mod, k)
importAll('graphicsItems')
importAll('widgets', excludes=['MatplotlibWidget'])
importAll('widgets', excludes=['MatplotlibWidget', 'RemoteGraphicsView'])
from .imageview import *
from .WidgetGroup import *

View File

@ -13,6 +13,7 @@ import re, os, sys
from collections import OrderedDict
GLOBAL_PATH = None # so not thread safe.
from . import units
from .python2_3 import asUnicode
class ParseError(Exception):
def __init__(self, message, lineNum, line, fileName=None):

View File

@ -1,4 +1,5 @@
from PyQt4 import QtCore, QtGui
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.python2_3 import asUnicode
class CmdInput(QtGui.QLineEdit):
@ -25,10 +26,10 @@ class CmdInput(QtGui.QLineEdit):
self.execCmd()
else:
QtGui.QLineEdit.keyPressEvent(self, ev)
self.history[0] = unicode(self.text())
self.history[0] = asUnicode(self.text())
def execCmd(self):
cmd = unicode(self.text())
cmd = asUnicode(self.text())
if len(self.history) == 1 or cmd != self.history[1]:
self.history.insert(1, cmd)
#self.lastCmd = cmd

View File

@ -1,13 +1,11 @@
from pyqtgraph.Qt import QtCore, QtGui
import sys, re, os, time, traceback
import sys, re, os, time, traceback, subprocess
import pyqtgraph as pg
import template
from . import template
import pyqtgraph.exceptionHandling as exceptionHandling
import pickle
EDITOR = "pykate {fileName}:{lineNum}"
class ConsoleWidget(QtGui.QWidget):
"""
Widget displaying console output and accepting command input.
@ -24,7 +22,7 @@ class ConsoleWidget(QtGui.QWidget):
be baffling and frustrating to users since it would appear the program has frozen.
- some terminals (eg windows cmd.exe) have notoriously unfriendly interfaces
- ability to add extra features like exception stack introspection
- ability to have multiple interactive prompts for remotely generated processes
- ability to have multiple interactive prompts, including for spawned sub-processes
"""
def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None):
@ -72,8 +70,8 @@ class ConsoleWidget(QtGui.QWidget):
self.ui.historyList.itemDoubleClicked.connect(self.cmdDblClicked)
self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible)
self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllToggled)
self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextToggled)
self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions)
self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextException)
self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked)
self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked)
self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked)
@ -229,15 +227,25 @@ class ConsoleWidget(QtGui.QWidget):
def flush(self):
pass
def catchAllToggled(self, b):
if b:
def catchAllExceptions(self, catch=True):
"""
If True, the console will catch all unhandled exceptions and display the stack
trace. Each exception caught clears the last.
"""
self.ui.catchAllExceptionsBtn.setChecked(catch)
if catch:
self.ui.catchNextExceptionBtn.setChecked(False)
exceptionHandling.register(self.allExceptionsHandler)
else:
exceptionHandling.unregister(self.allExceptionsHandler)
def catchNextToggled(self, b):
if b:
def catchNextException(self, catch=True):
"""
If True, the console will catch the next unhandled exception and display the stack
trace.
"""
self.ui.catchNextExceptionBtn.setChecked(catch)
if catch:
self.ui.catchAllExceptionsBtn.setChecked(False)
exceptionHandling.register(self.nextExceptionHandler)
else:
@ -254,11 +262,15 @@ class ConsoleWidget(QtGui.QWidget):
pass
def stackItemDblClicked(self, item):
global EDITOR
editor = self.editor
if editor is None:
editor = pg.getConfigOption('editorCommand')
if editor is None:
return
tb = self.currentFrame()
lineNum = tb.tb_lineno
fileName = tb.tb_frame.f_code.co_filename
os.system(EDITOR.format(fileName=fileName, lineNum=lineNum))
subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True)
def allExceptionsHandler(self, *args):

View File

@ -1 +1 @@
from Console import ConsoleWidget
from .Console import ConsoleWidget

View File

@ -91,7 +91,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Console", "Console", None, QtGui.QApplication.UnicodeUTF8))
self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8))
@ -101,4 +101,4 @@ class Ui_Form(object):
self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8))
from CmdInput import CmdInput
from .CmdInput import CmdInput

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>Console</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="margin">
@ -153,7 +153,7 @@
<customwidget>
<class>CmdInput</class>
<extends>QLineEdit</extends>
<header>CmdInput</header>
<header>.CmdInput</header>
</customwidget>
</customwidgets>
<resources/>

View File

@ -2,6 +2,7 @@
from pyqtgraph.Qt import QtCore, QtGui
from .Container import *
from .DockDrop import *
from .Dock import Dock
import pyqtgraph.debug as debug
import weakref

View File

@ -1,83 +0,0 @@
import sys
## Make sure pyqtgraph is importable
p = os.path.dirname(os.path.abspath(__file__))
p = os.path.join(p, '..', '..')
sys.path.insert(0, p)
from pyqtgraph.Qt import QtCore, QtGui
from .DockArea import *
from .Dock import *
app = QtGui.QApplication([])
win = QtGui.QMainWindow()
area = DockArea()
win.setCentralWidget(area)
win.resize(800,800)
from .Dock import Dock
d1 = Dock("Dock1", size=(200,200))
d2 = Dock("Dock2", size=(100,100))
d3 = Dock("Dock3", size=(1,1))
d4 = Dock("Dock4", size=(50,50))
d5 = Dock("Dock5", size=(100,100))
d6 = Dock("Dock6", size=(300,300))
area.addDock(d1, 'left')
area.addDock(d2, 'right')
area.addDock(d3, 'bottom')
area.addDock(d4, 'right')
area.addDock(d5, 'left', d1)
area.addDock(d6, 'top', d4)
area.moveDock(d6, 'above', d4)
d3.hideTitleBar()
print("===build complete====")
for d in [d1, d2, d3, d4, d5]:
w = QtGui.QWidget()
l = QtGui.QVBoxLayout()
w.setLayout(l)
btns = []
for i in range(4):
btns.append(QtGui.QPushButton("%s Button %d"%(d.name(), i)))
l.addWidget(btns[-1])
d.w = (w, l, btns)
d.addWidget(w)
import pyqtgraph as pg
p = pg.PlotWidget()
d6.addWidget(p)
print("===widgets added===")
#s = area.saveState()
#print "\n\n-------restore----------\n\n"
#area.restoreState(s)
s = None
def save():
global s
s = area.saveState()
def load():
global s
area.restoreState(s)
#d6.container().setCurrentIndex(0)
#d2.label.setTabPos(40)
#win2 = QtGui.QMainWindow()
#area2 = DockArea()
#win2.setCentralWidget(area2)
#win2.resize(800,800)
win.show()
#win2.show()

View File

@ -18,6 +18,7 @@ import sys, os
# documentation root, use os.path.abspath to make it absolute, like shown here.
path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(path, '..', '..', '..'))
print sys.path
# -- General configuration -----------------------------------------------------

View File

@ -43,5 +43,12 @@ While I consider this approach somewhat lazy, it is often the case that 'lazy' i
Embedding widgets inside PyQt applications
------------------------------------------
For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget <pyqtgraph.PlotWidget>`, :class:`ImageView <pyqtgraph.ImageView>`, :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and :class:`GraphicsView <pyqtgraph.GraphicsView>`. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality.
For the serious application developer, all of the functionality in pyqtgraph is available via :ref:`widgets <api_widgets>` that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget <pyqtgraph.PlotWidget>`, :class:`ImageView <pyqtgraph.ImageView>`, :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and :class:`GraphicsView <pyqtgraph.GraphicsView>`. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality:
#. In Designer, create a QGraphicsView widget ("Graphics View" under the "Display Widgets" category).
#. Right-click on the QGraphicsView and select "Promote To...".
#. Under "Promoted class name", enter the class name you wish to use ("PlotWidget", "GraphicsLayoutWidget", etc).
#. Under "Header file", enter "pyqtgraph".
#. Click "Add", then click "Promote".
See the designer documentation for more information on promoting widgets.

View File

@ -1,22 +1,47 @@
Line, Fill, and Color
=====================
Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color.
Qt relies on its QColor, QPen and QBrush classes for specifying line and fill styles for all of its drawing.
Internally, pyqtgraph uses the same system but also allows many shorthand methods of specifying
the same style options.
For these function arguments, the following values may be used:
Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color.
For most of these function arguments, the following values may be used:
* single-character string representing color (b, g, r, c, m, y, k, w)
* (r, g, b) or (r, g, b, a) tuple
* single greyscale value (0.0 - 1.0)
* (index, maximum) tuple for automatically iterating through colors (see functions.intColor)
* (index, maximum) tuple for automatically iterating through colors (see :func:`intColor <pyqtgraph.intColor>`)
* QColor
* QPen / QBrush where appropriate
Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes::
Notably, more complex pens and brushes can be easily built using the
:func:`mkPen() <pyqtgraph.mkPen>` / :func:`mkBrush() <pyqtgraph.mkBrush>` functions or with Qt's QPen and QBrush classes::
mkPen('y', width=3, style=QtCore.Qt.DashLine) ## Make a dashed yellow line 2px wide
mkPen(0.5) ## solid grey line 1px wide
mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line
See the Qt documentation for 'QPen' and 'PenStyle' for more options.
Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class
See the Qt documentation for 'QPen' and 'PenStyle' for more line-style options and 'QBrush' for more fill options.
Colors can also be built using :func:`mkColor() <pyqtgraph.mkColor>`,
:func:`intColor() <pyqtgraph.intColor>`, :func:`hsvColor() <pyqtgraph.hsvColor>`, or Qt's QColor class.
Default Background and Foreground Colors
----------------------------------------
By default, pyqtgraph uses a black background for its plots and grey for axes, text, and plot lines.
These defaults can be changed using pyqtgraph.setConfigOption()::
import pyqtgraph as pg
## Switch to using white background and black foreground
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
## The following plot has inverted colors
pg.plot([1,4,2,3,5])
(Note that this must be set *before* creating any widgets)

View File

@ -1,3 +1,5 @@
.. _api_widgets:
Pyqtgraph's Widgets
===================

View File

@ -4,41 +4,73 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import user
import numpy as np
app = QtGui.QApplication([])
view = pg.GraphicsView()
l = pg.GraphicsLayout(border=pg.mkPen(0, 0, 255))
l = pg.GraphicsLayout(border=(100,100,100))
view.setCentralItem(l)
view.show()
view.resize(800,600)
## Title at top
text = """
This example demonstrates the use of GraphicsLayout to arrange items in a grid.<br>
The items added to the layout must be subclasses of QGraphicsWidget (this includes <br>
PlotItem, ViewBox, LabelItem, and GrphicsLayout itself).
"""
l.addLabel(text, col=1, colspan=4)
l.nextRow()
## Put vertical label on left side
l.addLabel('Long Vertical Label', angle=-90, rowspan=3)
## Add 3 plots into the first row (automatic position)
p1 = l.addPlot()
p2 = l.addPlot()
p3 = l.addPlot()
p1 = l.addPlot(title="Plot 1")
p2 = l.addPlot(title="Plot 2")
vb = l.addViewBox(lockAspect=True)
img = pg.ImageItem(np.random.normal(size=(100,100)))
vb.addItem(img)
vb.autoRange()
## Add a viewbox into the second row (automatic position)
## Add a sub-layout into the second row (automatic position)
## The added item should avoid the first column, which is already filled
l.nextRow()
vb = l.addViewBox(colspan=3)
l2 = l.addLayout(colspan=3, border=(50,0,0))
l2.setContentsMargins(10, 10, 10, 10)
l2.addLabel("Sub-layout: this layout demonstrates the use of shared axes and axis labels", colspan=3)
l2.nextRow()
l2.addLabel('Vertical Axis Label', angle=-90, rowspan=2)
p21 = l2.addPlot()
p22 = l2.addPlot()
l2.nextRow()
p23 = l2.addPlot()
p24 = l2.addPlot()
l2.nextRow()
l2.addLabel("HorizontalAxisLabel", col=1, colspan=2)
## hide axes on some plots
p21.hideAxis('bottom')
p22.hideAxis('bottom')
p22.hideAxis('left')
p24.hideAxis('left')
p21.hideButtons()
p22.hideButtons()
p23.hideButtons()
p24.hideButtons()
## Add 2 more plots into the third row (manual position)
p4 = l.addPlot(row=2, col=0)
p5 = l.addPlot(row=2, col=1, colspan=2)
p4 = l.addPlot(row=3, col=1)
p5 = l.addPlot(row=3, col=2, colspan=2)
## show some content
## show some content in the plots
p1.plot([1,3,2,4,3,5])
p2.plot([1,3,2,4,3,5])
p3.plot([1,3,2,4,3,5])
p4.plot([1,3,2,4,3,5])
p5.plot([1,3,2,4,3,5])
b = QtGui.QGraphicsRectItem(0, 0, 1, 1)
b.setPen(pg.mkPen(255,255,0))
vb.addItem(b)
vb.setRange(QtCore.QRectF(-1, -1, 3, 3))
## Start Qt event loop unless running in interactive mode.

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import initExample ## Add path to library (just for examples; you do not need this)
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
app = pg.mkQApp()
v = pg.RemoteGraphicsView()
v.show()
plt = v.pg.PlotItem()
v.setCentralItem(plt)
plt.plot([1,4,2,3,6,2,3,4,2,3], pen='g')
## Start Qt event loop unless running in interactive mode or using pyside.
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -11,11 +11,14 @@ examples = OrderedDict([
('Command-line usage', 'CLIexample.py'),
('Basic Plotting', 'Plotting.py'),
('ImageView', 'ImageView.py'),
('ParameterTree', '../parametertree'),
('ParameterTree', 'parametertree.py'),
('Crosshair / Mouse interaction', 'crosshair.py'),
('Video speed test', 'VideoSpeedTest.py'),
('Plot speed test', 'PlotSpeedTest.py'),
('Data Slicing', 'DataSlicing.py'),
('Plot Customization', 'customPlot.py'),
('Dock widgets', 'dockarea.py'),
('Console', 'ConsoleWidget.py'),
('GraphicsItems', OrderedDict([
('Scatter Plot', 'ScatterPlot.py'),
#('PlotItem', 'PlotItem.py'),
@ -46,7 +49,7 @@ examples = OrderedDict([
#('VerticalLabel', '../widgets/VerticalLabel.py'),
('JoystickButton', 'JoystickButton.py'),
])),
('GraphicsScene', 'GraphicsScene.py'),
('Flowcharts', 'Flowchart.py'),
#('Canvas', '../canvas'),
@ -67,9 +70,9 @@ class ExampleLoader(QtGui.QMainWindow):
self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples)
self.ui.exampleTree.expandAll()
self.resize(900,500)
self.resize(1000,500)
self.show()
self.ui.splitter.setSizes([150,750])
self.ui.splitter.setSizes([250,750])
self.ui.loadBtn.clicked.connect(self.loadFile)
self.ui.exampleTree.currentItemChanged.connect(self.showFile)
self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile)

52
examples/customPlot.py Normal file
View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
##
## This example demonstrates the creation of a plot with a customized
## AxisItem and ViewBox.
##
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import time
class DateAxis(pg.AxisItem):
def tickStrings(self, values, scale, spacing):
return [time.strftime('%b %Y', time.localtime(x)) for x in values]
class CustomViewBox(pg.ViewBox):
def __init__(self, *args, **kwds):
pg.ViewBox.__init__(self, *args, **kwds)
self.setMouseMode(self.RectMode)
## reimplement right-click to zoom out
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton:
self.autoRange()
def mouseDragEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton:
ev.ignore()
else:
pg.ViewBox.mouseDragEvent(self, ev)
app = pg.mkQApp()
axis = DateAxis(orientation='bottom')
vb = CustomViewBox()
pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox<br>Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom")
dates = np.arange(8) * (3600*24*356)
pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o')
pw.show()
r = pg.PolyLineROI([(0,0), (10, 10)])
pw.addItem(r)
## Start Qt event loop unless running in interactive mode or using pyside.
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

116
examples/dockarea.py Normal file
View File

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates the use of pyqtgraph's dock widget system.
The dockarea system allows the design of user interfaces which can be rearranged by
the user at runtime. Docks can be moved, resized, stacked, and torn out of the main
window. This is similar in principle to the docking system built into Qt, but
offers a more deterministic dock placement API (in Qt it is very difficult to
programatically generate complex dock arrangements). Additionally, Qt's docks are
designed to be used as small panels around the outer edge of a window. Pyqtgraph's
docks were created with the notion that the entire window (or any portion of it)
would consist of dockable components.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.console
import numpy as np
from pyqtgraph.dockarea import *
app = QtGui.QApplication([])
win = QtGui.QMainWindow()
area = DockArea()
win.setCentralWidget(area)
win.resize(1000,500)
## Create docks, place them into the window one at a time.
## Note that size arguments are only a suggestion; docks will still have to
## fill the entire dock area and obey the limits of their internal widgets.
d1 = Dock("Dock1", size=(1, 1)) ## give this dock the minimum possible size
d2 = Dock("Dock2 - Console", size=(500,300))
d3 = Dock("Dock3", size=(500,400))
d4 = Dock("Dock4 (tabbed) - Plot", size=(500,200))
d5 = Dock("Dock5 - Image", size=(500,200))
d6 = Dock("Dock6 (tabbed) - Plot", size=(500,200))
area.addDock(d1, 'left') ## place d1 at left edge of dock area (it will fill the whole space since there are no other docks yet)
area.addDock(d2, 'right') ## place d2 at right edge of dock area
area.addDock(d3, 'bottom', d1)## place d3 at bottom edge of d1
area.addDock(d4, 'right') ## place d4 at right edge of dock area
area.addDock(d5, 'left', d1) ## place d5 at left edge of d1
area.addDock(d6, 'top', d4) ## place d5 at top edge of d4
## Test ability to move docks programatically after they have been placed
area.moveDock(d4, 'top', d2) ## move d4 to top edge of d2
area.moveDock(d6, 'above', d4) ## move d6 to stack on top of d4
area.moveDock(d5, 'top', d2) ## move d5 to top edge of d2
## Add widgets into each dock
## first dock gets save/restore buttons
w1 = pg.LayoutWidget()
label = QtGui.QLabel(""" -- DockArea Example --
This window has 6 Dock widgets in it. Each dock can be dragged
by its title bar to occupy a different space within the window
but note that one dock has its title bar hidden). Additionally,
the borders between docks may be dragged to resize. Docks that are dragged on top
of one another are stacked in a tabbed layout. Double-click a dock title
bar to place it in its own window.
""")
saveBtn = QtGui.QPushButton('Save dock state')
restoreBtn = QtGui.QPushButton('Restore dock state')
restoreBtn.setEnabled(False)
w1.addWidget(label, row=0, col=0)
w1.addWidget(saveBtn, row=1, col=0)
w1.addWidget(restoreBtn, row=2, col=0)
d1.addWidget(w1)
state = None
def save():
global state
state = area.saveState()
restoreBtn.setEnabled(True)
def load():
global state
area.restoreState(state)
saveBtn.clicked.connect(save)
restoreBtn.clicked.connect(load)
w2 = pg.console.ConsoleWidget()
d2.addWidget(w2)
## Hide title bar on dock 3
d3.hideTitleBar()
w3 = pg.PlotWidget(title="Plot inside dock with no title bar")
w3.plot(np.random.normal(size=100))
d3.addWidget(w3)
w4 = pg.PlotWidget(title="Dock 4 plot")
w4.plot(np.random.normal(size=100))
d4.addWidget(w4)
w5 = pg.ImageView()
w5.setImage(np.random.normal(size=(100,100)))
d5.addWidget(w5)
w6 = pg.PlotWidget(title="Dock 6 plot")
w6.plot(np.random.normal(size=100))
d6.addWidget(w6)
win.show()
## Start Qt event loop unless running in interactive mode or using pyside.
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -1,3 +1,3 @@
## make this version of pyqtgraph importable before any others
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))

View File

@ -1,38 +1,10 @@
# -*- coding: utf-8 -*-
import initExample ## Add path to library (just for examples; you do not need this)
import numpy as np
import pyqtgraph.multiprocess as mp
from pyqtgraph.multiprocess.parallelizer import Parallelize #, Parallelizer
import pyqtgraph as pg
import time
print "\n=================\nParallelize"
tasks = [1,2,4,8]
results = [None] * len(tasks)
size = 2000000
start = time.time()
with Parallelize(enumerate(tasks), results=results, workers=1) as tasker:
for i, x in tasker:
print i, x
tot = 0
for j in xrange(size):
tot += j * x
results[i] = tot
print results
print "serial:", time.time() - start
start = time.time()
with Parallelize(enumerate(tasks), results=results) as tasker:
for i, x in tasker:
print i, x
tot = 0
for j in xrange(size):
tot += j * x
results[i] = tot
print results
print "parallel:", time.time() - start

63
examples/parallelize.py Normal file
View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
import initExample ## Add path to library (just for examples; you do not need this)
import numpy as np
import pyqtgraph.multiprocess as mp
import pyqtgraph as pg
import time
print "\n=================\nParallelize"
## Do a simple task:
## for x in range(N):
## sum([x*i for i in range(M)])
##
## We'll do this three times
## - once without Parallelize
## - once with Parallelize, but forced to use a single worker
## - once with Parallelize automatically determining how many workers to use
##
tasks = range(10)
results = [None] * len(tasks)
results2 = results[:]
results3 = results[:]
size = 2000000
pg.mkQApp()
### Purely serial processing
start = time.time()
with pg.ProgressDialog('processing serially..', maximum=len(tasks)) as dlg:
for i, x in enumerate(tasks):
tot = 0
for j in xrange(size):
tot += j * x
results[i] = tot
dlg += 1
if dlg.wasCanceled():
raise Exception('processing canceled')
print "Serial time: %0.2f" % (time.time() - start)
### Use parallelize, but force a single worker
### (this simulates the behavior seen on windows, which lacks os.fork)
start = time.time()
with mp.Parallelize(enumerate(tasks), results=results2, workers=1, progressDialog='processing serially (using Parallelizer)..') as tasker:
for i, x in tasker:
tot = 0
for j in xrange(size):
tot += j * x
tasker.results[i] = tot
print "\nParallel time, 1 worker: %0.2f" % (time.time() - start)
print "Results match serial: ", results2 == results
### Use parallelize with multiple workers
start = time.time()
with mp.Parallelize(enumerate(tasks), results=results3, progressDialog='processing in parallel..') as tasker:
for i, x in tasker:
tot = 0
for j in xrange(size):
tot += j * x
tasker.results[i] = tot
print "\nParallel time, %d workers: %0.2f" % (mp.Parallelize.suggestedWorkerCount(), time.time() - start)
print "Results match serial: ", results3 == results

160
examples/parametertree.py Normal file
View File

@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates the use of pyqtgraph's parametertree system. This provides
a simple way to generate user interfaces that control sets of parameters. The example
demonstrates a variety of different parameter types (int, float, list, etc.)
as well as some customized parameter types
"""
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import collections
app = QtGui.QApplication([])
import pyqtgraph.parametertree.parameterTypes as pTypes
from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType
## test subclassing parameters
## This parameter automatically generates two child parameters which are always reciprocals of each other
class ComplexParameter(pTypes.GroupParameter):
def __init__(self, **opts):
opts['type'] = 'bool'
opts['value'] = True
pTypes.GroupParameter.__init__(self, **opts)
self.addChild({'name': 'A = 1/B', 'type': 'float', 'value': 7, 'suffix': 'Hz', 'siPrefix': True})
self.addChild({'name': 'B = 1/A', 'type': 'float', 'value': 1/7., 'suffix': 's', 'siPrefix': True})
self.a = self.param('A = 1/B')
self.b = self.param('B = 1/A')
self.a.sigValueChanged.connect(self.aChanged)
self.b.sigValueChanged.connect(self.bChanged)
def aChanged(self):
self.b.setValue(1.0 / self.a.value(), blockSignal=self.bChanged)
def bChanged(self):
self.a.setValue(1.0 / self.b.value(), blockSignal=self.aChanged)
## test add/remove
## this group includes a menu allowing the user to add new parameters into its child list
class ScalableGroup(pTypes.GroupParameter):
def __init__(self, **opts):
opts['type'] = 'group'
opts['addText'] = "Add"
opts['addList'] = ['str', 'float', 'int']
pTypes.GroupParameter.__init__(self, **opts)
def addNew(self, typ):
val = {
'str': '',
'float': 0.0,
'int': 0
}[typ]
self.addChild(dict(name="ScalableParam %d" % (len(self.childs)+1), type=typ, value=val, removable=True, renamable=True))
## test column spanning (widget sub-item that spans all columns)
class TextParameterItem(pTypes.WidgetParameterItem):
def __init__(self, param, depth):
pTypes.WidgetParameterItem.__init__(self, param, depth)
self.subItem = QtGui.QTreeWidgetItem()
self.addChild(self.subItem)
def treeWidgetChanged(self):
self.treeWidget().setFirstItemColumnSpanned(self.subItem, True)
self.treeWidget().setItemWidget(self.subItem, 0, self.textBox)
self.setExpanded(True)
def makeWidget(self):
self.textBox = QtGui.QTextEdit()
self.textBox.setMaximumHeight(100)
self.textBox.value = lambda: str(self.textBox.toPlainText())
self.textBox.setValue = self.textBox.setPlainText
self.textBox.sigChanged = self.textBox.textChanged
return self.textBox
class TextParameter(Parameter):
type = 'text'
itemClass = TextParameterItem
registerParameterType('text', TextParameter)
params = [
{'name': 'Basic parameter data types', 'type': 'group', 'children': [
{'name': 'Integer', 'type': 'int', 'value': 10},
{'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1},
{'name': 'String', 'type': 'str', 'value': "hi"},
{'name': 'List', 'type': 'list', 'values': [1,2,3], 'value': 2},
{'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2},
{'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"},
{'name': 'Color', 'type': 'color', 'value': "FF0", 'tip': "This is a color button"},
{'name': 'Subgroup', 'type': 'group', 'children': [
{'name': 'Sub-param 1', 'type': 'int', 'value': 10},
{'name': 'Sub-param 2', 'type': 'float', 'value': 1.2e6},
]},
]},
{'name': 'Numerical Parameter Options', 'type': 'group', 'children': [
{'name': 'Units + SI prefix', 'type': 'float', 'value': 1.2e-6, 'step': 1e-6, 'siPrefix': True, 'suffix': 'V'},
{'name': 'Limits (min=7;max=15)', 'type': 'int', 'value': 11, 'limits': (7, 15), 'default': -6},
{'name': 'DEC stepping', 'type': 'float', 'value': 1.2e6, 'dec': True, 'step': 1, 'siPrefix': True, 'suffix': 'Hz'},
]},
{'name': 'Extra Parameter Options', 'type': 'group', 'children': [
{'name': 'Read-only', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'readonly': True},
{'name': 'Renamable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'renamable': True},
{'name': 'Removable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'removable': True},
]},
ComplexParameter(name='Custom parameter group (reciprocal values)'),
ScalableGroup(name="Expandable Parameter Group", children=[
{'name': 'ScalableParam 1', 'type': 'str', 'value': "default param 1"},
{'name': 'ScalableParam 2', 'type': 'str', 'value': "default param 2"},
]),
{'name': 'Custom parameter class (text box)', 'type': 'text', 'value': 'Some text...'},
]
## Create tree of Parameter objects
p = Parameter(name='params', type='group', children=params)
## If anything changes in the tree, print a message
def change(param, changes):
print("tree changes:")
for param, change, data in changes:
path = p.childPath(param)
if path is not None:
childName = '.'.join(path)
else:
childName = param.name()
print(' parameter: %s'% childName)
print(' change: %s'% change)
print(' data: %s'% str(data))
print(' ----------')
p.sigTreeStateChanged.connect(change)
## Create two ParameterTree widgets, both accessing the same data
t = ParameterTree()
t.setParameters(p, showTop=False)
t.show()
t.resize(400,600)
t2 = ParameterTree()
t2.setParameters(p, showTop=False)
t2.show()
t2.resize(400,600)
## Start Qt event loop unless running in interactive mode or using pyside.
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -51,16 +51,16 @@ class ExceptionHandler:
def __call__(self, *args):
## call original exception handler first (prints exception)
global original_excepthook, callbacks, clear_tracebacks
print "=====", time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())), "====="
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
ret = original_excepthook(*args)
for cb in callbacks:
try:
cb(*args)
except:
print " --------------------------------------------------------------"
print " Error occurred during exception callback", cb
print " --------------------------------------------------------------"
print(" --------------------------------------------------------------")
print(" Error occurred during exception callback %s" % str(cb))
print(" --------------------------------------------------------------")
traceback.print_exception(*sys.exc_info())

View File

@ -71,7 +71,8 @@ class Flowchart(Node):
if terminals is None:
terminals = {}
self.filePath = filePath
Node.__init__(self, name) ## create node without terminals; we'll add these later
Node.__init__(self, name, allowAddInput=True, allowAddOutput=True) ## create node without terminals; we'll add these later
self.inputWasSet = False ## flag allows detection of changes in the absence of input change.
self._nodes = {}
@ -457,7 +458,7 @@ class Flowchart(Node):
state = Node.saveState(self)
state['nodes'] = []
state['connects'] = []
state['terminals'] = self.saveTerminals()
#state['terminals'] = self.saveTerminals()
for name, node in self._nodes.items():
cls = type(node)
@ -470,7 +471,7 @@ class Flowchart(Node):
conn = self.listConnections()
for a, b in conn:
state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name()))
state['inputNode'] = self.inputNode.saveState()
state['outputNode'] = self.outputNode.saveState()
@ -486,7 +487,8 @@ class Flowchart(Node):
nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
for n in nodes:
if n['name'] in self._nodes:
self._nodes[n['name']].moveBy(*n['pos'])
#self._nodes[n['name']].graphicsItem().moveBy(*n['pos'])
self._nodes[n['name']].restoreState(n['state'])
continue
try:
node = self.createNode(n['class'], name=n['name'])
@ -498,7 +500,7 @@ class Flowchart(Node):
self.inputNode.restoreState(state.get('inputNode', {}))
self.outputNode.restoreState(state.get('outputNode', {}))
self.restoreTerminals(state['terminals'])
#self.restoreTerminals(state['terminals'])
for n1, t1, n2, t2 in state['connects']:
try:
self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2])

View File

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
from pyqtgraph.Qt import QtCore, QtGui
#from PySide import QtCore, QtGui
from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject
import pyqtgraph.functions as fn
from .Terminal import *
from collections import OrderedDict
from pyqtgraph.debug import *
import numpy as np
#from pyqtgraph.ObjectWorkaround import QObjectWorkaround
from .eq import *
#TETRACYCLINE = True
def strDict(d):
return dict([(str(k), v) for k, v in d.items()])
@ -32,8 +29,8 @@ class Node(QtCore.QObject):
self.bypassButton = None ## this will be set by the flowchart ctrl widget..
self._graphicsItem = None
self.terminals = OrderedDict()
self._inputs = {}
self._outputs = {}
self._inputs = OrderedDict()
self._outputs = OrderedDict()
self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals
self._allowAddOutput = allowAddOutput
self._allowRemove = allowRemove
@ -85,24 +82,16 @@ class Node(QtCore.QObject):
def terminalRenamed(self, term, oldName):
"""Called after a terminal has been renamed"""
newName = term.name()
#print "node", self, "handling rename..", newName, oldName
for d in [self.terminals, self._inputs, self._outputs]:
if oldName not in d:
continue
#print " got one"
d[newName] = d[oldName]
del d[oldName]
self.graphicsItem().updateTerminals()
#self.emit(QtCore.SIGNAL('terminalRenamed'), term, oldName)
self.sigTerminalRenamed.emit(term, oldName)
def addTerminal(self, name, **opts):
#print "Node.addTerminal called. name:", name, "opts:", opts
#global TETRACYCLINE
#print "TETRACYCLINE: ", TETRACYCLINE
#if TETRACYCLINE:
#print "Creating Terminal..."
name = self.nextTerminalName(name)
term = Terminal(self, name, **opts)
self.terminals[name] = term
@ -278,12 +267,20 @@ class Node(QtCore.QObject):
def saveState(self):
pos = self.graphicsItem().pos()
return {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
termsEditable = self._allowAddInput | self._allowAddOutput
for term in self._inputs.values() + self._outputs.values():
termsEditable |= term._renamable | term._removable | term._multiable
if termsEditable:
state['terminals'] = self.saveTerminals()
return state
def restoreState(self, state):
pos = state.get('pos', (0,0))
self.graphicsItem().setPos(*pos)
self.bypass(state.get('bypass', False))
if 'terminals' in state:
self.restoreTerminals(state['terminals'])
def saveTerminals(self):
terms = OrderedDict()
@ -309,8 +306,8 @@ class Node(QtCore.QObject):
for t in self.terminals.values():
t.close()
self.terminals = OrderedDict()
self._inputs = {}
self._outputs = {}
self._inputs = OrderedDict()
self._outputs = OrderedDict()
def close(self):
"""Cleans up after the node--removes terminals, graphicsItem, widget"""
@ -493,10 +490,6 @@ class NodeGraphicsItem(GraphicsObject):
self.hovered = False
self.update()
#def mouseReleaseEvent(self, ev):
#ret = QtGui.QGraphicsItem.mouseReleaseEvent(self, ev)
#return ret
def keyPressEvent(self, ev):
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
ev.accept()
@ -513,13 +506,8 @@ class NodeGraphicsItem(GraphicsObject):
return GraphicsObject.itemChange(self, change, val)
#def contextMenuEvent(self, ev):
#ev.accept()
#self.menu.popup(ev.screenPos())
def getMenu(self):
return self.menu
def getContextMenus(self, event):
return [self.menu]
@ -548,25 +536,3 @@ class NodeGraphicsItem(GraphicsObject):
def addOutputFromMenu(self): ## called when add output is clicked in context menu
self.node.addOutput(renamable=True, removable=True, multiable=False)
#def menuTriggered(self, action):
##print "node.menuTriggered called. action:", action
#act = str(action.text())
#if act == "Add input":
#self.node.addInput()
#self.updateActionMenu()
#elif act == "Add output":
#self.node.addOutput()
#self.updateActionMenu()
#elif act == "Remove node":
#self.node.close()
#else: ## only other option is to remove a terminal
#self.node.removeTerminal(act)
#self.terminalMenu.removeAction(action)
#def updateActionMenu(self):
#for t in self.node.terminals:
#if t not in [str(a.text()) for a in self.terminalMenu.actions()]:
#self.terminalMenu.addAction(t)
#for a in self.terminalMenu.actions():
#if str(a.text()) not in self.node.terminals:
#self.terminalMenu.removeAction(a)

View File

@ -45,7 +45,7 @@ class Terminal:
self._value = {} ## dictionary of terminal:value pairs.
else:
self._value = None
self.valueOk = None
self.recolor()
@ -70,6 +70,8 @@ class Terminal:
return
self._value = val
else:
if not isinstance(self._value, dict):
self._value = {}
if val is not None:
self._value.update(val)
@ -132,9 +134,14 @@ class Terminal:
def isMultiValue(self):
return self._multi
def setMultiValue(self, b):
def setMultiValue(self, multi):
"""Set whether this is a multi-value terminal."""
self._multi = b
self._multi = multi
if not multi and len(self.inputTerminals()) > 1:
self.disconnectAll()
for term in self.inputTerminals():
self.inputChanged(term)
def isOutput(self):
return self._io == 'out'
@ -407,6 +414,8 @@ class TerminalGraphicsItem(GraphicsObject):
multiAct = QtGui.QAction("Multi-value", self.menu)
multiAct.setCheckable(True)
multiAct.setChecked(self.term.isMultiValue())
multiAct.setEnabled(self.term.isMultiable())
multiAct.triggered.connect(self.toggleMulti)
self.menu.addAction(multiAct)
self.menu.multiAct = multiAct

View File

@ -240,7 +240,7 @@ class EvalNode(Node):
def saveState(self):
state = Node.saveState(self)
state['text'] = str(self.text.toPlainText())
state['terminals'] = self.saveTerminals()
#state['terminals'] = self.saveTerminals()
return state
def restoreState(self, state):
@ -282,7 +282,7 @@ class ColumnJoinNode(Node):
def addInput(self):
#print "ColumnJoinNode.addInput called."
term = Node.addInput(self, 'input', renamable=True)
term = Node.addInput(self, 'input', renamable=True, removable=True)
#print "Node.addInput returned. term:", term
item = QtGui.QTreeWidgetItem([term.name()])
item.term = term
@ -322,16 +322,14 @@ class ColumnJoinNode(Node):
def restoreState(self, state):
Node.restoreState(self, state)
inputs = [inp.name() for inp in self.inputs()]
inputs = self.inputs()
order = [name for name in state['order'] if name in inputs]
for name in inputs:
if name not in state['order']:
self.removeTerminal(name)
for name in state['order']:
if name not in inputs:
Node.addInput(self, name, renamable=True)
if name not in order:
order.append(name)
self.tree.clear()
for name in state['order']:
for name in order:
term = self[name]
item = QtGui.QTreeWidgetItem([name])
item.term = term

View File

@ -153,7 +153,7 @@ def denoise(data, radius=2, threshold=4):
r2 = radius * 2
d1 = data.view(np.ndarray)
d2 = data[radius:] - data[:-radius] #a derivative
d2 = d1[radius:] - d1[:-radius] #a derivative
#d3 = data[r2:] - data[:-r2]
#d4 = d2 - d3
stdev = d2.std()

View File

@ -5,6 +5,7 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from .python2_3 import asUnicode
Colors = {
'b': (0,0,255,255),
'g': (0,255,0,255),
@ -357,28 +358,36 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0)
def affineSlice(data, shape, origin, vectors, axes, **kargs):
def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs):
"""
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets.
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this).
For a graphical interface to this function, see :func:`ROI.getArrayRegion`
For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>`
============== ====================================================================================================
Arguments:
*data* (ndarray) the original dataset
*shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape))
*origin* the location in the original dataset that will become the origin of the sliced data.
*vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same
length as *axes*. If the vectors are not unit length, the result will be scaled relative to the
original data. If the vectors are not orthogonal, the result will be sheared relative to the
original data.
*axes* The axes in the original dataset which correspond to the slice *vectors*
*order* The order of spline interpolation. Default is 1 (linear). See scipy.ndimage.map_coordinates
for more information.
*returnCoords* If True, return a tuple (result, coords) where coords is the array of coordinates used to select
values from the original dataset.
*All extra keyword arguments are passed to scipy.ndimage.map_coordinates.*
--------------------------------------------------------------------------------------------------------------------
============== ====================================================================================================
| *data* (ndarray): the original dataset
| *shape*: the shape of the slice to take (Note the return value may have more dimensions than len(shape))
| *origin*: the location in the original dataset that will become the origin in the sliced data.
| *vectors*: list of unit vectors which point in the direction of the slice axes
Note the following must be true:
* each vector must have the same length as *axes*
* If the vectors are not unit length, the result will be scaled.
* If the vectors are not orthogonal, the result will be sheared.
*axes*: the axes in the original dataset which correspond to the slice *vectors*
All extra keyword arguments are passed to scipy.ndimage.map_coordinates
| len(shape) == len(vectors)
| len(origin) == len(axes) == len(vectors[i])
Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes
@ -391,10 +400,6 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
Note the following must be true:
| len(shape) == len(vectors)
| len(origin) == len(axes) == len(vectors[0])
"""
# sanity check
@ -436,7 +441,7 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
for inds in np.ndindex(*extraShape):
ind = (Ellipsis,) + inds
#print data[ind].shape, x.shape, output[ind].shape, output.shape
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, **kargs)
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs)
tr = list(range(output.ndim))
trb = []
@ -447,9 +452,18 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
tr2 = tuple(trb+tr)
## Untranspose array before returning
return output.transpose(tr2)
output = output.transpose(tr2)
if returnCoords:
return (output, x)
else:
return output
def transformToArray(tr):
"""
Given a QTransform, return a 3x3 numpy array.
"""
return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]])
def solve3DTransform(points1, points2):
"""
Find a 3D transformation matrix that maps points1 onto points2
@ -1275,3 +1289,19 @@ def isosurface(data, level):
return facets
def invertQTransform(tr):
"""Return a QTransform that is the inverse of *tr*.
Rasises an exception if tr is not invertible.
Note that this function is preferred over QTransform.inverted() due to
bugs in that method. (specifically, Qt has floating-point precision issues
when determining whether a matrix is invertible)
"""
#return tr.inverted()[0]
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
inv = scipy.linalg.inv(arr)
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])

View File

@ -1,9 +1,11 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.python2_3 import asUnicode
import numpy as np
from pyqtgraph.Point import Point
import pyqtgraph.debug as debug
import weakref
import pyqtgraph.functions as fn
import pyqtgraph as pg
from .GraphicsWidget import GraphicsWidget
__all__ = ['AxisItem']
@ -65,8 +67,6 @@ class AxisItem(GraphicsWidget):
self.setRange(0, 1)
if pen is None:
pen = QtGui.QPen(QtGui.QColor(100, 100, 100))
self.setPen(pen)
self._linkedView = None
@ -189,23 +189,31 @@ class AxisItem(GraphicsWidget):
self.setMaximumWidth(w)
self.setMinimumWidth(w)
def pen(self):
if self._pen is None:
return fn.mkPen(pg.getConfigOption('foreground'))
return self._pen
def setPen(self, pen):
self.pen = pen
"""
Set the pen used for drawing text, axes, ticks, and grid lines.
if pen == None, the default will be used (see :func:`setConfigOption
<pyqtgraph.setConfigOption>`)
"""
self._pen = pen
self.picture = None
self.update()
def setScale(self, scale=None):
"""
Set the value scaling for this axis.
The scaling value 1) multiplies the values displayed along the axis
and 2) changes the way units are displayed in the label.
Set the value scaling for this axis. Values on the axis are multiplied
by this scale factor before being displayed as text. By default,
this scaling value is automatically determined based on the visible range
and the axis units are updated to reflect the chosen scale factor.
For example: If the axis spans values from -0.1 to 0.1 and has units set
to 'V' then a scale of 1000 would cause the axis to display values -100 to 100
and the units would appear as 'mV'
If scale is None, then it will be determined automatically based on the current
range displayed by the axis.
"""
if scale is None:
#if self.drawLabel: ## If there is a label, then we are free to rescale the values
@ -219,8 +227,10 @@ class AxisItem(GraphicsWidget):
self.setLabel(unitPrefix=prefix)
else:
scale = 1.0
else:
self.setLabel(unitPrefix='')
self.autoScale = False
if scale != self.scale:
self.scale = scale
self.setLabel()
@ -354,6 +364,29 @@ class AxisItem(GraphicsWidget):
(intervals[minorIndex], 0)
]
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
### Determine major/minor tick spacings which flank the optimal spacing.
#intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit
#minorIndex = 0
#while intervals[minorIndex+1] <= optimalSpacing:
#minorIndex += 1
### make sure we never see 5 and 2 at the same time
#intIndexes = [
#[0,1,3],
#[0,2,3],
#[2,3,4],
#[3,4,6],
#[3,5,6],
#][minorIndex]
#return [
#(intervals[intIndexes[2]], 0),
#(intervals[intIndexes[1]], 0),
#(intervals[intIndexes[0]], 0)
#]
def tickValues(self, minVal, maxVal, size):
"""
@ -370,8 +403,6 @@ class AxisItem(GraphicsWidget):
"""
minVal, maxVal = sorted((minVal, maxVal))
if self.logMode:
return self.logTickValues(minVal, maxVal, size)
ticks = []
tickLevels = self.tickSpacing(minVal, maxVal, size)
@ -388,21 +419,36 @@ class AxisItem(GraphicsWidget):
## remove any ticks that were present in higher levels
## we assume here that if the difference between a tick value and a previously seen tick value
## is less than spacing/100, then they are 'equal' and we can ignore the new tick.
values = filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values)
values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
allValues = np.concatenate([allValues, values])
ticks.append((spacing, values))
if self.logMode:
return self.logTickValues(minVal, maxVal, size, ticks)
return ticks
def logTickValues(self, minVal, maxVal, size):
v1 = int(np.floor(minVal))
v2 = int(np.ceil(maxVal))
major = list(range(v1+1, v2))
def logTickValues(self, minVal, maxVal, size, stdTicks):
minor = []
for v in range(v1, v2):
minor.extend(v + np.log10(np.arange(1, 10)))
minor = [x for x in minor if x>minVal and x<maxVal]
return [(1.0, major), (None, minor)]
## start with the tick spacing given by tickValues().
## Any level whose spacing is < 1 needs to be converted to log scale
ticks = []
for (spacing, t) in stdTicks:
if spacing >= 1.0:
ticks.append((spacing, t))
if len(ticks) < 3:
v1 = int(np.floor(minVal))
v2 = int(np.ceil(maxVal))
#major = list(range(v1+1, v2))
minor = []
for v in range(v1, v2):
minor.extend(v + np.log10(np.arange(1, 10)))
minor = [x for x in minor if x>minVal and x<maxVal]
ticks.append((None, minor))
return ticks
def tickStrings(self, values, scale, spacing):
"""Return the strings that should be placed next to ticks. This method is called
@ -477,12 +523,14 @@ class AxisItem(GraphicsWidget):
#print tickStart, tickStop, span
## draw long line along axis
p.setPen(self.pen)
p.setPen(self.pen())
p.drawLine(*span)
p.translate(0.5,0) ## resolves some damn pixel ambiguity
## determine size of this item in pixels
points = list(map(self.mapToDevice, span))
if None in points:
return
lengthInPixels = Point(points[1] - points[0]).length()
if lengthInPixels == 0:
return
@ -513,6 +561,10 @@ class AxisItem(GraphicsWidget):
else:
xScale = bounds.width() / dif
offset = self.range[0] * xScale
xRange = [x * xScale - offset for x in self.range]
xMin = min(xRange)
xMax = max(xRange)
prof.mark('init')
@ -521,6 +573,7 @@ class AxisItem(GraphicsWidget):
## draw ticks
## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
## draw three different intervals, long ticks first
for i in range(len(tickLevels)):
tickPositions.append([])
ticks = tickLevels[i][1]
@ -530,19 +583,28 @@ class AxisItem(GraphicsWidget):
lineAlpha = 255 / (i+1)
if self.grid is not False:
lineAlpha = self.grid
lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.)
for v in ticks:
## determine actual position to draw this tick
x = (v * xScale) - offset
if x < xMin or x > xMax: ## last check to make sure no out-of-bounds ticks are drawn
tickPositions[i].append(None)
continue
tickPositions[i].append(x)
p1 = [x, x]
p2 = [x, x]
p1[axis] = tickStart
p2[axis] = tickStop
if self.grid is False:
p2[axis] += tickLength*tickDir
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha)))
tickPen = self.pen()
color = tickPen.color()
color.setAlpha(lineAlpha)
tickPen.setColor(color)
p.setPen(tickPen)
p.drawLine(Point(p1), Point(p2))
tickPositions[i].append(x)
prof.mark('draw ticks')
## Draw text until there is no more room (or no more text)
@ -557,10 +619,15 @@ class AxisItem(GraphicsWidget):
if len(strings) == 0:
continue
## ignore strings belonging to ticks that were previously ignored
for j in range(len(strings)):
if tickPositions[i][j] is None:
strings[j] = None
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None])
if i > 0: ## always draw top level
## measure all text, make sure there's enough room
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings])
if axis == 0:
textSize = np.sum([r.height() for r in textRects])
else:
@ -570,11 +637,12 @@ class AxisItem(GraphicsWidget):
textFillRatio = float(textSize) / lengthInPixels
if textFillRatio > 0.7:
break
#spacing, values = tickLevels[best]
#strings = self.tickStrings(values, self.scale, spacing)
for j in range(len(strings)):
vstr = strings[j]
if vstr is None:## this tick was ignored because it is out of bounds
continue
x = tickPositions[i][j]
textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
height = textRect.height()
@ -592,7 +660,7 @@ class AxisItem(GraphicsWidget):
textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height)
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150)))
p.setPen(self.pen())
p.drawText(rect, textFlags, vstr)
prof.mark('draw text')
prof.finish()

View File

@ -1,4 +1,5 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.python2_3 import sortList
import pyqtgraph.functions as fn
from .GraphicsObject import GraphicsObject
from .GraphicsWidget import GraphicsWidget
@ -175,7 +176,7 @@ class TickSliderItem(GraphicsWidget):
def resizeEvent(self, ev):
wlen = max(40, self.widgetLength())
self.setLength(wlen-self.tickSize)
self.setLength(wlen-self.tickSize-2)
self.setOrientation(self.orientation)
#bounds = self.scene().itemsBoundingRect()
#bounds.setLeft(min(-self.tickSize*0.5, bounds.left()))
@ -186,7 +187,7 @@ class TickSliderItem(GraphicsWidget):
def setLength(self, newLen):
#private
for t, x in list(self.ticks.items()):
t.setPos(x * newLen, t.pos().y())
t.setPos(x * newLen + 1, t.pos().y())
self.length = float(newLen)
#def mousePressEvent(self, ev):
@ -491,8 +492,8 @@ class GradientEditorItem(TickSliderItem):
def setLength(self, newLen):
#private (but maybe public)
TickSliderItem.setLength(self, newLen)
self.backgroundRect.setRect(0, -self.rectSize, newLen, self.rectSize)
self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize)
self.backgroundRect.setRect(1, -self.rectSize, newLen, self.rectSize)
self.gradRect.setRect(1, -self.rectSize, newLen, self.rectSize)
self.updateGradient()
def currentColorChanged(self, color):

View File

@ -1,6 +1,7 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.GraphicsScene import GraphicsScene
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
import weakref
class GraphicsItem(object):
@ -149,16 +150,32 @@ class GraphicsItem(object):
"""Return vectors in local coordinates representing the width and height of a view pixel.
If direction is specified, then return vectors parallel and orthogonal to it.
Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)."""
Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)
or if pixel size is below floating-point precision limit.
"""
dt = self.deviceTransform()
if dt is None:
return None, None
if direction is None:
direction = Point(1, 0)
direction = Point(1, 0)
if direction.manhattanLength() == 0:
raise Exception("Cannot compute pixel length for 0-length vector.")
## attempt to re-scale direction vector to fit within the precision of the coordinate system
if direction.x() == 0:
r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))
elif direction.y() == 0:
r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))
else:
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
direction = direction * r
viewDir = Point(dt.map(direction) - dt.map(Point(0,0)))
if viewDir.manhattanLength() == 0:
return None, None ## pixel size cannot be represented on this scale
orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
try:
@ -168,7 +185,7 @@ class GraphicsItem(object):
raise Exception("Invalid direction %s" %direction)
dti = dt.inverted()[0]
dti = fn.invertQTransform(dt)
return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
#vt = self.deviceTransform()
@ -194,23 +211,26 @@ class GraphicsItem(object):
def pixelSize(self):
## deprecated
v = self.pixelVectors()
if v == (None, None):
return None, None
return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5
def pixelWidth(self):
## deprecated
vt = self.deviceTransform()
if vt is None:
return 0
vt = vt.inverted()[0]
vt = fn.invertQTransform(vt)
return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length()
def pixelHeight(self):
## deprecated
vt = self.deviceTransform()
if vt is None:
return 0
vt = vt.inverted()[0]
vt = fn.invertQTransform(vt)
return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
@ -232,7 +252,7 @@ class GraphicsItem(object):
vt = self.deviceTransform()
if vt is None:
return None
vt = vt.inverted()[0]
vt = fn.invertQTransform(vt)
return vt.map(obj)
def mapRectToDevice(self, rect):
@ -253,7 +273,7 @@ class GraphicsItem(object):
vt = self.deviceTransform()
if vt is None:
return None
vt = vt.inverted()[0]
vt = fn.invertQTransform(vt)
return vt.mapRect(rect)
def mapToView(self, obj):
@ -272,14 +292,14 @@ class GraphicsItem(object):
vt = self.viewTransform()
if vt is None:
return None
vt = vt.inverted()[0]
vt = fn.invertQTransform(vt)
return vt.map(obj)
def mapRectFromView(self, obj):
vt = self.viewTransform()
if vt is None:
return None
vt = vt.inverted()[0]
vt = fn.invertQTransform(vt)
return vt.mapRect(obj)
def pos(self):

View File

@ -21,21 +21,29 @@ class GraphicsLayout(GraphicsWidget):
self.border = border
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
self.items = {}
self.rows = {}
self.items = {} ## item: [(row, col), (row, col), ...] lists all cells occupied by the item
self.rows = {} ## row: {col1: item1, col2: item2, ...} maps cell location to item
self.currentRow = 0
self.currentCol = 0
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
#def resizeEvent(self, ev):
#ret = GraphicsWidget.resizeEvent(self, ev)
#print self.pos(), self.mapToDevice(self.rect().topLeft())
#return ret
def nextRow(self):
"""Advance to next row for automatic item placement"""
self.currentRow += 1
self.currentCol = 0
self.currentCol = -1
self.nextColumn()
def nextColumn(self, colspan=1):
"""Advance to next column, while returning the current column number
def nextColumn(self):
"""Advance to next available column
(generally only for internal use--called by addItem)"""
self.currentCol += colspan
return self.currentCol-colspan
self.currentCol += 1
while self.getItem(self.currentRow, self.currentCol) is not None:
self.currentCol += 1
def nextCol(self, *args, **kargs):
"""Alias of nextColumn"""
@ -66,6 +74,8 @@ class GraphicsLayout(GraphicsWidget):
Create a LabelItem with *text* and place it in the next available cell (or in the cell specified)
All extra keyword arguments are passed to :func:`LabelItem.__init__ <pyqtgraph.LabelItem.__init__>`
Returns the created item.
To create a vertical label, use *angle*=-90
"""
text = LabelItem(text, **kargs)
self.addItem(text, row, col, rowspan, colspan)
@ -89,18 +99,24 @@ class GraphicsLayout(GraphicsWidget):
if row is None:
row = self.currentRow
if col is None:
col = self.nextCol(colspan)
col = self.currentCol
if row not in self.rows:
self.rows[row] = {}
self.rows[row][col] = item
self.items[item] = (row, col)
self.items[item] = []
for i in range(rowspan):
for j in range(colspan):
row2 = row + i
col2 = col + j
if row2 not in self.rows:
self.rows[row2] = {}
self.rows[row2][col2] = item
self.items[item].append((row2, col2))
self.layout.addItem(item, row, col, rowspan, colspan)
self.nextColumn()
def getItem(self, row, col):
"""Return the item in (*row*, *col*)"""
return self.row[row][col]
"""Return the item in (*row*, *col*). If the cell is empty, return None."""
return self.rows.get(row, {}).get(col, None)
def boundingRect(self):
return self.rect()
@ -124,9 +140,10 @@ class GraphicsLayout(GraphicsWidget):
ind = self.itemIndex(item)
self.layout.removeAt(ind)
self.scene().removeItem(item)
r,c = self.items[item]
for r,c in self.items[item]:
del self.rows[r][c]
del self.items[item]
del self.rows[r][c]
self.update()
def clear(self):

View File

@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore
from .UIGraphicsItem import *
import numpy as np
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
__all__ = ['GridItem']
class GridItem(UIGraphicsItem):
@ -47,7 +48,7 @@ class GridItem(UIGraphicsItem):
p = QtGui.QPainter()
p.begin(self.picture)
dt = self.viewTransform().inverted()[0]
dt = fn.invertQTransform(self.viewTransform())
vr = self.getViewWidget().rect()
unit = self.pixelWidth(), self.pixelHeight()
dim = [vr.width(), vr.height()]
@ -112,7 +113,7 @@ class GridItem(UIGraphicsItem):
texts.append((QtCore.QPointF(x, y), "%g"%p1[ax]))
tr = self.deviceTransform()
#tr.scale(1.5, 1.5)
p.setWorldTransform(tr.inverted()[0])
p.setWorldTransform(fn.invertQTransform(tr))
for t in texts:
x = tr.map(t[0]) + Point(0.5, 0.5)
p.drawText(x, t[1])

View File

@ -50,7 +50,7 @@ class HistogramLUTItem(GraphicsWidget):
self.layout.setSpacing(0)
self.vb = ViewBox()
self.vb.setMaximumWidth(152)
self.vb.setMinimumWidth(52)
self.vb.setMinimumWidth(45)
self.vb.setMouseEnabled(x=False, y=True)
self.gradient = GradientEditorItem()
self.gradient.setOrientation('right')

View File

@ -1,5 +1,6 @@
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.functions as fn
import pyqtgraph as pg
from .GraphicsWidget import GraphicsWidget
@ -18,14 +19,13 @@ class LabelItem(GraphicsWidget):
GraphicsWidget.__init__(self, parent)
self.item = QtGui.QGraphicsTextItem(self)
self.opts = {
'color': 'CCC',
'color': None,
'justify': 'center'
}
self.opts.update(args)
self.sizeHint = {}
self._sizeHint = {}
self.setText(text)
self.setAngle(angle)
def setAttr(self, attr, value):
"""Set default text properties. See setText() for accepted parameters."""
@ -44,15 +44,17 @@ class LabelItem(GraphicsWidget):
==================== ==============================
"""
self.text = text
opts = self.opts.copy()
opts = self.opts
for k in args:
opts[k] = args[k]
optlist = []
if 'color' in opts:
if isinstance(opts['color'], QtGui.QColor):
opts['color'] = fn.colorStr(opts['color'])[:6]
optlist.append('color: #' + opts['color'])
color = self.opts['color']
if color is None:
color = pg.getConfigOption('foreground')
color = fn.mkColor(color)
optlist.append('color: #' + fn.colorStr(color)[:6])
if 'size' in opts:
optlist.append('font-size: ' + opts['size'])
if 'bold' in opts and opts['bold'] in [True, False]:
@ -64,7 +66,7 @@ class LabelItem(GraphicsWidget):
self.item.setHtml(full)
self.updateMin()
self.resizeEvent(None)
self.update()
self.updateGeometry()
def resizeEvent(self, ev):
#c1 = self.boundingRect().center()
@ -72,16 +74,35 @@ class LabelItem(GraphicsWidget):
#dif = c1 - c2
#self.item.moveBy(dif.x(), dif.y())
#print c1, c2, dif, self.item.pos()
self.item.setPos(0,0)
bounds = self.itemRect()
left = self.mapFromItem(self.item, QtCore.QPointF(0,0)) - self.mapFromItem(self.item, QtCore.QPointF(1,0))
rect = self.rect()
if self.opts['justify'] == 'left':
self.item.setPos(0,0)
if left.x() != 0:
bounds.moveLeft(rect.left())
if left.y() < 0:
bounds.moveTop(rect.top())
elif left.y() > 0:
bounds.moveBottom(rect.bottom())
elif self.opts['justify'] == 'center':
bounds = self.item.mapRectToParent(self.item.boundingRect())
self.item.setPos(self.width()/2. - bounds.width()/2., 0)
bounds.moveCenter(rect.center())
#bounds = self.itemRect()
#self.item.setPos(self.width()/2. - bounds.width()/2., 0)
elif self.opts['justify'] == 'right':
bounds = self.item.mapRectToParent(self.item.boundingRect())
self.item.setPos(self.width() - bounds.width(), 0)
#if self.width() > 0:
#self.item.setTextWidth(self.width())
if left.x() != 0:
bounds.moveRight(rect.right())
if left.y() < 0:
bounds.moveBottom(rect.bottom())
elif left.y() > 0:
bounds.moveTop(rect.top())
#bounds = self.itemRect()
#self.item.setPos(self.width() - bounds.width(), 0)
self.item.setPos(bounds.topLeft() - self.itemRect().topLeft())
self.updateMin()
def setAngle(self, angle):
self.angle = angle
@ -89,27 +110,31 @@ class LabelItem(GraphicsWidget):
self.item.rotate(angle)
self.updateMin()
def updateMin(self):
bounds = self.item.mapRectToParent(self.item.boundingRect())
bounds = self.itemRect()
self.setMinimumWidth(bounds.width())
self.setMinimumHeight(bounds.height())
self.sizeHint = {
self._sizeHint = {
QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()),
QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()),
QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2),
QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this?
}
self.update()
self.updateGeometry()
def sizeHint(self, hint, constraint):
if hint not in self.sizeHint:
if hint not in self._sizeHint:
return QtCore.QSizeF(0, 0)
return QtCore.QSizeF(*self.sizeHint[hint])
return QtCore.QSizeF(*self._sizeHint[hint])
def itemRect(self):
return self.item.mapRectToParent(self.item.boundingRect())
#def paint(self, p, *args):
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.rect())
#p.drawRect(self.item.boundingRect())
#p.setPen(fn.mkPen('g'))
#p.drawRect(self.itemRect())

View File

@ -33,17 +33,12 @@ from .. AxisItem import AxisItem
from .. LabelItem import LabelItem
from .. GraphicsWidget import GraphicsWidget
from .. ButtonItem import ButtonItem
#from .. GraphicsLayout import GraphicsLayout
from pyqtgraph.WidgetGroup import WidgetGroup
import collections
__all__ = ['PlotItem']
#try:
#from WidgetGroup import *
#HAVE_WIDGETGROUP = True
#except:
#HAVE_WIDGETGROUP = False
try:
from metaarray import *
HAVE_METAARRAY = True
@ -78,6 +73,7 @@ class PlotItem(GraphicsWidget):
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
:func:`invertY <pyqtgraph.ViewBox.invertY>`,
:func:`register <pyqtgraph.ViewBox.register>`,
:func:`unregister <pyqtgraph.ViewBox.unregister>`
@ -99,26 +95,28 @@ class PlotItem(GraphicsWidget):
lastFileDir = None
managers = {}
def __init__(self, parent=None, name=None, labels=None, title=None, **kargs):
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs):
"""
Create a new PlotItem. All arguments are optional.
Any extra keyword arguments are passed to PlotItem.plot().
============= ==========================================================================================
============== ==========================================================================================
**Arguments**
*title* Title to display at the top of the item. Html is allowed.
*labels* A dictionary specifying the axis labels to display::
*title* Title to display at the top of the item. Html is allowed.
*labels* A dictionary specifying the axis labels to display::
{'left': (args), 'bottom': (args), ...}
{'left': (args), 'bottom': (args), ...}
The name of each axis and the corresponding arguments are passed to
:func:`PlotItem.setLabel() <pyqtgraph.PlotItem.setLabel>`
Optionally, PlotItem my also be initialized with the keyword arguments left,
right, top, or bottom to achieve the same effect.
*name* Registers a name for this view so that others may link to it
============= ==========================================================================================
The name of each axis and the corresponding arguments are passed to
:func:`PlotItem.setLabel() <pyqtgraph.PlotItem.setLabel>`
Optionally, PlotItem my also be initialized with the keyword arguments left,
right, top, or bottom to achieve the same effect.
*name* Registers a name for this view so that others may link to it
*viewBox* If specified, the PlotItem will be constructed with this as its ViewBox.
*axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items
for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top')
and the values must be instances of AxisItem (or at least compatible with AxisItem).
============== ==========================================================================================
"""
GraphicsWidget.__init__(self, parent)
@ -127,8 +125,6 @@ class PlotItem(GraphicsWidget):
## Set up control buttons
path = os.path.dirname(__file__)
#self.ctrlBtn = ButtonItem(os.path.join(path, 'ctrl.png'), 14, self)
#self.ctrlBtn.clicked.connect(self.ctrlBtnClicked)
self.autoImageFile = os.path.join(path, 'auto.png')
self.lockImageFile = os.path.join(path, 'lock.png')
self.autoBtn = ButtonItem(self.autoImageFile, 14, self)
@ -141,32 +137,33 @@ class PlotItem(GraphicsWidget):
self.layout.setHorizontalSpacing(0)
self.layout.setVerticalSpacing(0)
self.vb = ViewBox(name=name)
if viewBox is None:
viewBox = ViewBox()
self.vb = viewBox
self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus
if name is not None:
self.vb.register(name)
self.vb.sigRangeChanged.connect(self.sigRangeChanged)
self.vb.sigXRangeChanged.connect(self.sigXRangeChanged)
self.vb.sigYRangeChanged.connect(self.sigYRangeChanged)
#self.vb.sigRangeChangedManually.connect(self.enableManualScale)
#self.vb.sigRangeChanged.connect(self.viewRangeChanged)
self.layout.addItem(self.vb, 2, 1)
self.alpha = 1.0
self.autoAlpha = True
self.spectrumMode = False
#self.autoScale = [True, True]
## Create and place scale items
self.scales = {
'top': {'item': AxisItem(orientation='top', linkView=self.vb), 'pos': (1, 1)},
'bottom': {'item': AxisItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)},
'left': {'item': AxisItem(orientation='left', linkView=self.vb), 'pos': (2, 0)},
'right': {'item': AxisItem(orientation='right', linkView=self.vb), 'pos': (2, 2)}
}
for k in self.scales:
item = self.scales[k]['item']
self.layout.addItem(item, *self.scales[k]['pos'])
item.setZValue(-1000)
item.setFlag(item.ItemNegativeZStacksBehindParent)
## Create and place axis items
if axisItems is None:
axisItems = {}
self.axes = {}
for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
axis = axisItems.get(k, AxisItem(orientation=k))
axis.linkToView(self.vb)
self.axes[k] = {'item': axis, 'pos': pos}
self.layout.addItem(axis, *pos)
axis.setZValue(-1000)
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
self.titleLabel = LabelItem('', size='11pt')
self.layout.addItem(self.titleLabel, 0, 1)
@ -192,8 +189,7 @@ class PlotItem(GraphicsWidget):
for m in [
'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible',
'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled',
'enableAutoRange', 'disableAutoRange', 'setAspectLocked',
'setMenuEnabled', 'menuEnabled',
'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY',
'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well.
setattr(self, m, getattr(self.vb, m))
@ -233,45 +229,12 @@ class PlotItem(GraphicsWidget):
self.subMenus.append(sm)
self.ctrlMenu.addMenu(sm)
## exporting is handled by GraphicsScene now
#exportOpts = collections.OrderedDict([
#('SVG - Full Plot', self.saveSvgClicked),
#('SVG - Curves Only', self.saveSvgCurvesClicked),
#('Image', self.saveImgClicked),
#('CSV', self.saveCsvClicked),
#])
#self.vb.menu.setExportMethods(exportOpts)
#if HAVE_WIDGETGROUP:
self.stateGroup = WidgetGroup()
for name, w in menuItems:
self.stateGroup.autoAdd(w)
self.fileDialog = None
#self.xLinkPlot = None
#self.yLinkPlot = None
#self.linksBlocked = False
#self.setAcceptHoverEvents(True)
## Connect control widgets
#c.xMinText.editingFinished.connect(self.setManualXScale)
#c.xMaxText.editingFinished.connect(self.setManualXScale)
#c.yMinText.editingFinished.connect(self.setManualYScale)
#c.yMaxText.editingFinished.connect(self.setManualYScale)
#c.xManualRadio.clicked.connect(lambda: self.updateXScale())
#c.yManualRadio.clicked.connect(lambda: self.updateYScale())
#c.xAutoRadio.clicked.connect(self.updateXScale)
#c.yAutoRadio.clicked.connect(self.updateYScale)
#c.xAutoPercentSpin.valueChanged.connect(self.replot)
#c.yAutoPercentSpin.valueChanged.connect(self.replot)
c.alphaGroup.toggled.connect(self.updateAlpha)
c.alphaSlider.valueChanged.connect(self.updateAlpha)
c.autoAlphaCheck.toggled.connect(self.updateAlpha)
@ -283,13 +246,6 @@ class PlotItem(GraphicsWidget):
c.fftCheck.toggled.connect(self.updateSpectrumMode)
c.logXCheck.toggled.connect(self.updateLogMode)
c.logYCheck.toggled.connect(self.updateLogMode)
#c.saveSvgBtn.clicked.connect(self.saveSvgClicked)
#c.saveSvgCurvesBtn.clicked.connect(self.saveSvgCurvesClicked)
#c.saveImgBtn.clicked.connect(self.saveImgClicked)
#c.saveCsvBtn.clicked.connect(self.saveCsvClicked)
#self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged)
#self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged)
c.downsampleSpin.valueChanged.connect(self.updateDownsampling)
@ -298,24 +254,15 @@ class PlotItem(GraphicsWidget):
self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation)
self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation)
#c.xMouseCheck.toggled.connect(self.mouseCheckChanged)
#c.yMouseCheck.toggled.connect(self.mouseCheckChanged)
#self.xLinkPlot = None
#self.yLinkPlot = None
#self.linksBlocked = False
self.manager = None
self.hideAxis('right')
self.hideAxis('top')
self.showAxis('left')
self.showAxis('bottom')
#if name is not None:
#self.registerPlot(name)
if labels is None:
labels = {}
for label in list(self.scales.keys()):
for label in list(self.axes.keys()):
if label in kargs:
labels[label] = kargs[label]
del kargs[label]
@ -330,15 +277,16 @@ class PlotItem(GraphicsWidget):
if len(kargs) > 0:
self.plot(**kargs)
#self.enableAutoRange()
def implements(self, interface=None):
return interface in ['ViewBoxWrapper']
def getViewBox(self):
"""Return the ViewBox within."""
"""Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within."""
return self.vb
def setLogMode(self, x, y):
"""
Set log scaling for x and y axes.
@ -399,11 +347,11 @@ class PlotItem(GraphicsWidget):
#self.autoBtn.setParent(None)
#self.autoBtn = None
for k in self.scales:
i = self.scales[k]['item']
for k in self.axes:
i = self.axes[k]['item']
i.close()
self.scales = None
self.axes = None
self.scene().removeItem(self.vb)
self.vb = None
@ -431,47 +379,6 @@ class PlotItem(GraphicsWidget):
def registerPlot(self, name): ## for backward compatibility
self.vb.register(name)
#self.name = name
#win = str(self.window())
##print "register", name, win
#if win not in PlotItem.managers:
#PlotItem.managers[win] = PlotWidgetManager()
#self.manager = PlotItem.managers[win]
#self.manager.addWidget(self, name)
##QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList)
#self.manager.sigWidgetListChanged.connect(self.updatePlotList)
#self.updatePlotList()
#def updatePlotList(self):
#"""Update the list of all plotWidgets in the "link" combos"""
##print "update plot list", self
#try:
#for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]:
#current = unicode(sc.currentText())
#sc.blockSignals(True)
#try:
#sc.clear()
#sc.addItem("")
#if self.manager is not None:
#for w in self.manager.listWidgets():
##print w
#if w == self.name:
#continue
#sc.addItem(w)
#if w == current:
#sc.setCurrentIndex(sc.count()-1)
#finally:
#sc.blockSignals(False)
#if unicode(sc.currentText()) != current:
#sc.currentItemChanged.emit()
#except:
#import gc
#refs= gc.get_referrers(self)
#print " error during update of", self
#print " Referrers are:", refs
#raise
def updateGrid(self, *args):
alpha = self.ctrl.gridAlphaSlider.value()
@ -492,91 +399,6 @@ class PlotItem(GraphicsWidget):
return wr
#def viewRangeChanged(self, vb, range):
##self.emit(QtCore.SIGNAL('viewChanged'), *args)
#self.sigRangeChanged.emit(self, range)
#def blockLink(self, b):
#self.linksBlocked = b
#def xLinkComboChanged(self):
#self.setXLink(str(self.ctrl.xLinkCombo.currentText()))
#def yLinkComboChanged(self):
#self.setYLink(str(self.ctrl.yLinkCombo.currentText()))
#def setXLink(self, plot=None):
#"""Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)"""
#if isinstance(plot, basestring):
#if self.manager is None:
#return
#if self.xLinkPlot is not None:
#self.manager.unlinkX(self, self.xLinkPlot)
#plot = self.manager.getWidget(plot)
#if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'):
#plot = plot.getPlotItem()
#self.xLinkPlot = plot
#if plot is not None:
#self.setManualXScale()
#self.manager.linkX(self, plot)
#def setYLink(self, plot=None):
#"""Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)"""
#if isinstance(plot, basestring):
#if self.manager is None:
#return
#if self.yLinkPlot is not None:
#self.manager.unlinkY(self, self.yLinkPlot)
#plot = self.manager.getWidget(plot)
#if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'):
#plot = plot.getPlotItem()
#self.yLinkPlot = plot
#if plot is not None:
#self.setManualYScale()
#self.manager.linkY(self, plot)
#def linkXChanged(self, plot):
#"""Called when a linked plot has changed its X scale"""
##print "update from", plot
#if self.linksBlocked:
#return
#pr = plot.vb.viewRect()
#pg = plot.viewGeometry()
#if pg is None:
##print " return early"
#return
#sg = self.viewGeometry()
#upp = float(pr.width()) / pg.width()
#x1 = pr.left() + (sg.x()-pg.x()) * upp
#x2 = x1 + sg.width() * upp
#plot.blockLink(True)
#self.setManualXScale()
#self.setXRange(x1, x2, padding=0)
#plot.blockLink(False)
#self.replot()
#def linkYChanged(self, plot):
#"""Called when a linked plot has changed its Y scale"""
#if self.linksBlocked:
#return
#pr = plot.vb.viewRect()
#pg = plot.vb.boundingRect()
#sg = self.vb.boundingRect()
#upp = float(pr.height()) / pg.height()
#y1 = pr.bottom() + (sg.y()-pg.y()) * upp
#y2 = y1 + sg.height() * upp
#plot.blockLink(True)
#self.setManualYScale()
#self.setYRange(y1, y2, padding=0)
#plot.blockLink(False)
#self.replot()
def avgToggled(self, b):
if b:
self.recomputeAverages()
@ -650,50 +472,6 @@ class PlotItem(GraphicsWidget):
else:
plot.setData(x, y)
#def mouseCheckChanged(self):
#state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()]
#self.vb.setMouseEnabled(*state)
#def xRangeChanged(self, _, range):
#if any(np.isnan(range)) or any(np.isinf(range)):
#raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender())))
#self.ctrl.xMinText.setText('%0.5g' % range[0])
#self.ctrl.xMaxText.setText('%0.5g' % range[1])
### automatically change unit scale
#maxVal = max(abs(range[0]), abs(range[1]))
#(scale, prefix) = fn.siScale(maxVal)
##for l in ['top', 'bottom']:
##if self.getLabel(l).isVisible():
##self.setLabel(l, unitPrefix=prefix)
##self.getScale(l).setScale(scale)
##else:
##self.setLabel(l, unitPrefix='')
##self.getScale(l).setScale(1.0)
##self.emit(QtCore.SIGNAL('xRangeChanged'), self, range)
#self.sigXRangeChanged.emit(self, range)
#def yRangeChanged(self, _, range):
#if any(np.isnan(range)) or any(np.isinf(range)):
#raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender())))
#self.ctrl.yMinText.setText('%0.5g' % range[0])
#self.ctrl.yMaxText.setText('%0.5g' % range[1])
### automatically change unit scale
#maxVal = max(abs(range[0]), abs(range[1]))
#(scale, prefix) = fn.siScale(maxVal)
##for l in ['left', 'right']:
##if self.getLabel(l).isVisible():
##self.setLabel(l, unitPrefix=prefix)
##self.getScale(l).setScale(scale)
##else:
##self.setLabel(l, unitPrefix='')
##self.getScale(l).setScale(1.0)
##self.emit(QtCore.SIGNAL('yRangeChanged'), self, range)
#self.sigYRangeChanged.emit(self, range)
def autoBtnClicked(self):
if self.autoBtn.mode == 'auto':
self.enableAutoRange()
@ -706,72 +484,6 @@ class PlotItem(GraphicsWidget):
"""
print("Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead.")
self.vb.enableAutoRange(self.vb.XYAxes)
#self.ctrl.xAutoRadio.setChecked(True)
#self.ctrl.yAutoRadio.setChecked(True)
#self.autoBtn.setImageFile(self.lockImageFile)
#self.autoBtn.mode = 'lock'
#self.updateXScale()
#self.updateYScale()
#self.replot()
#def updateXScale(self):
#"""Set plot to autoscale or not depending on state of radio buttons"""
#if self.ctrl.xManualRadio.isChecked():
#self.setManualXScale()
#else:
#self.setAutoXScale()
#self.replot()
#def updateYScale(self, b=False):
#"""Set plot to autoscale or not depending on state of radio buttons"""
#if self.ctrl.yManualRadio.isChecked():
#self.setManualYScale()
#else:
#self.setAutoYScale()
#self.replot()
#def enableManualScale(self, v=[True, True]):
#if v[0]:
#self.autoScale[0] = False
#self.ctrl.xManualRadio.setChecked(True)
##self.setManualXScale()
#if v[1]:
#self.autoScale[1] = False
#self.ctrl.yManualRadio.setChecked(True)
##self.setManualYScale()
##self.autoBtn.enable()
#self.autoBtn.setImageFile(self.autoImageFile)
#self.autoBtn.mode = 'auto'
##self.replot()
#def setManualXScale(self):
#self.autoScale[0] = False
#x1 = float(self.ctrl.xMinText.text())
#x2 = float(self.ctrl.xMaxText.text())
#self.ctrl.xManualRadio.setChecked(True)
#self.setXRange(x1, x2, padding=0)
#self.autoBtn.show()
##self.replot()
#def setManualYScale(self):
#self.autoScale[1] = False
#y1 = float(self.ctrl.yMinText.text())
#y2 = float(self.ctrl.yMaxText.text())
#self.ctrl.yManualRadio.setChecked(True)
#self.setYRange(y1, y2, padding=0)
#self.autoBtn.show()
##self.replot()
#def setAutoXScale(self):
#self.autoScale[0] = True
#self.ctrl.xAutoRadio.setChecked(True)
##self.replot()
#def setAutoYScale(self):
#self.autoScale[1] = True
#self.ctrl.yAutoRadio.setChecked(True)
##self.replot()
def addItem(self, item, *args, **kargs):
"""
@ -867,17 +579,6 @@ class PlotItem(GraphicsWidget):
"""
#if y is not None:
#data = y
#if data2 is not None:
#x = data
#data = data2
#if decimate is not None and decimate > 1:
#data = data[::decimate]
#if x is not None:
#x = x[::decimate]
## print 'plot with decimate = %d' % (decimate)
clear = kargs.get('clear', False)
params = kargs.get('params', None)
@ -888,23 +589,7 @@ class PlotItem(GraphicsWidget):
if params is None:
params = {}
#if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
#curve = self._plotMetaArray(data, x=x, **kargs)
#elif isinstance(data, np.ndarray):
#curve = self._plotArray(data, x=x, **kargs)
#elif isinstance(data, list):
#if x is not None:
#x = np.array(x)
#curve = self._plotArray(np.array(data), x=x, **kargs)
#elif data is None:
#curve = PlotCurveItem(**kargs)
#else:
#raise Exception('Not sure how to plot object of type %s' % type(data))
#print data, curve
self.addItem(item, params=params)
#if pen is not None:
#curve.setPen(fn.mkPen(pen))
return item
@ -922,80 +607,34 @@ class PlotItem(GraphicsWidget):
del kargs['size']
return self.plot(*args, **kargs)
#sp = ScatterPlotItem(*args, **kargs)
#self.addItem(sp)
#return sp
#def plotChanged(self, curve=None):
## Recompute auto range if needed
#args = {}
#for ax in [0, 1]:
#print "range", ax
#if self.autoScale[ax]:
#percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01
#mn = None
#mx = None
#for c in self.curves + [c[1] for c in self.avgCurves.values()] + self.dataItems:
#if not c.isVisible():
#continue
#cmn, cmx = c.getRange(ax, percentScale)
##print " ", c, cmn, cmx
#if mn is None or cmn < mn:
#mn = cmn
#if mx is None or cmx > mx:
#mx = cmx
#if mn is None or mx is None or any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])):
#continue
#if mn == mx:
#mn -= 1
#mx += 1
#if ax == 0:
#args['xRange'] = [mn, mx]
#else:
#args['yRange'] = [mn, mx]
#if len(args) > 0:
##print args
#self.setRange(**args)
def replot(self):
#self.plotChanged()
self.update()
def updateParamList(self):
self.ctrl.avgParamList.clear()
## Check to see that each parameter for each curve is present in the list
#print "\nUpdate param list", self
#print "paramList:", self.paramList
for c in self.curves:
#print " curve:", c
for p in list(self.itemMeta.get(c, {}).keys()):
#print " param:", p
if type(p) is tuple:
p = '.'.join(p)
## If the parameter is not in the list, add it.
matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly)
#print " matches:", matches
if len(matches) == 0:
i = QtGui.QListWidgetItem(p)
if p in self.paramList and self.paramList[p] is True:
#print " set checked"
i.setCheckState(QtCore.Qt.Checked)
else:
#print " set unchecked"
i.setCheckState(QtCore.Qt.Unchecked)
self.ctrl.avgParamList.addItem(i)
else:
i = matches[0]
self.paramList[p] = (i.checkState() == QtCore.Qt.Checked)
#print "paramList:", self.paramList
## This is bullshit.
## Qt's SVG-writing capabilities are pretty terrible.
def writeSvgCurves(self, fileName=None):
if fileName is None:
self.fileDialog = FileDialog()
@ -1190,18 +829,12 @@ class PlotItem(GraphicsWidget):
def saveState(self):
#if not HAVE_WIDGETGROUP:
#raise Exception("State save/restore requires WidgetGroup class.")
state = self.stateGroup.state()
state['paramList'] = self.paramList.copy()
state['view'] = self.vb.getState()
#print "\nSAVE %s:\n" % str(self.name), state
#print "Saving state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup']))
return state
def restoreState(self, state):
#if not HAVE_WIDGETGROUP:
#raise Exception("State save/restore requires WidgetGroup class.")
if 'paramList' in state:
self.paramList = state['paramList'].copy()
@ -1218,8 +851,6 @@ class PlotItem(GraphicsWidget):
state['yGridCheck'] = state['gridGroup']
self.stateGroup.setState(state)
#self.updateXScale()
#self.updateYScale()
self.updateParamList()
if 'view' not in state:
@ -1232,13 +863,6 @@ class PlotItem(GraphicsWidget):
}
self.vb.setState(state['view'])
#print "\nRESTORE %s:\n" % str(self.name), state
#print "Restoring state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup']))
#avg = self.ctrl.averageGroup.isChecked()
#if avg != state['averageGroup']:
#print " WARNING: avgGroup is %s, should be %s" % (str(avg), str(state['averageGroup']))
def widgetGroupInterface(self):
return (None, PlotItem.saveState, PlotItem.restoreState)
@ -1269,8 +893,6 @@ class PlotItem(GraphicsWidget):
for c in self.curves:
c.setDownsampling(ds)
self.recomputeAverages()
#for c in self.avgCurves.values():
#c[1].setDownsampling(ds)
def downsampleMode(self):
@ -1306,8 +928,6 @@ class PlotItem(GraphicsWidget):
(alpha, auto) = self.alphaState()
for c in self.curves:
c.setAlpha(alpha**2, auto)
#self.replot(autoRange=False)
def alphaState(self):
enabled = self.ctrl.alphaGroup.isChecked()
@ -1330,9 +950,6 @@ class PlotItem(GraphicsWidget):
mode = False
return mode
#def wheelEvent(self, ev):
## disables default panning the whole scene by mousewheel
#ev.accept()
def resizeEvent(self, ev):
if self.autoBtn is None: ## already closed down
@ -1340,29 +957,42 @@ class PlotItem(GraphicsWidget):
btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect())
y = self.size().height() - btnRect.height()
self.autoBtn.setPos(0, y)
#def hoverMoveEvent(self, ev):
#self.mousePos = ev.pos()
#self.mouseScreenPos = ev.screenPos()
#def ctrlBtnClicked(self):
#self.ctrlMenu.popup(self.mouseScreenPos)
def getMenu(self):
return self.ctrlMenu
def getContextMenus(self, event):
## called when another item is displaying its context menu; we get to add extras to the end of the menu.
return self.ctrlMenu
if self.menuEnabled():
return self.ctrlMenu
else:
return None
def setMenuEnabled(self, enableMenu=True, enableViewBoxMenu='same'):
"""
Enable or disable the context menu for this PlotItem.
By default, the ViewBox's context menu will also be affected.
(use enableViewBoxMenu=None to leave the ViewBox unchanged)
"""
self._menuEnabled = enableMenu
if enableViewBoxMenu is None:
return
if enableViewBoxMenu is 'same':
enableViewBoxMenu = enableMenu
self.vb.setMenuEnabled(enableViewBoxMenu)
def menuEnabled(self):
return self._menuEnabled
def getLabel(self, key):
pass
def _checkScaleKey(self, key):
if key not in self.scales:
raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.scales.keys()))))
if key not in self.axes:
raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.axes.keys()))))
def getScale(self, key):
return self.getAxis(key)
@ -1371,7 +1001,7 @@ class PlotItem(GraphicsWidget):
"""Return the specified AxisItem.
*name* should be 'left', 'bottom', 'top', or 'right'."""
self._checkScaleKey(name)
return self.scales[name]['item']
return self.axes[name]['item']
def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args):
"""
@ -1417,13 +1047,14 @@ class PlotItem(GraphicsWidget):
axis must be one of 'left', 'bottom', 'right', or 'top'
"""
s = self.getScale(axis)
p = self.scales[axis]['pos']
p = self.axes[axis]['pos']
if show:
s.show()
else:
s.hide()
def hideAxis(self, axis):
"""Hide one of the PlotItem's axes. ('left', 'bottom', 'right', or 'top')"""
self.showAxis(axis, False)
def showScale(self, *args, **kargs):
@ -1431,6 +1062,7 @@ class PlotItem(GraphicsWidget):
return self.showAxis(*args, **kargs)
def hideButtons(self):
"""Causes auto-scale button ('A' in lower-left corner) to be hidden for this PlotItem"""
#self.ctrlBtn.hide()
self.autoBtn.hide()
@ -1454,7 +1086,6 @@ class PlotItem(GraphicsWidget):
## create curve
try:
xv = arr.xvals(0)
#print 'xvals:', xv
except:
if x is None:
xv = np.arange(arr.shape[0])
@ -1474,17 +1105,6 @@ class PlotItem(GraphicsWidget):
return c
#def saveSvgClicked(self):
#self.writeSvg()
#def saveSvgCurvesClicked(self):
#self.writeSvgCurves()
#def saveImgClicked(self):
#self.writeImage()
#def saveCsvClicked(self):
#self.writeCsv()
def setExportMode(self, export, opts):
if export:
@ -1492,63 +1112,3 @@ class PlotItem(GraphicsWidget):
else:
self.autoBtn.show()
#class PlotWidgetManager(QtCore.QObject):
#sigWidgetListChanged = QtCore.Signal(object)
#"""Used for managing communication between PlotWidgets"""
#def __init__(self):
#QtCore.QObject.__init__(self)
#self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here
#def addWidget(self, w, name):
#self.widgets[name] = w
##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys())
#self.sigWidgetListChanged.emit(self.widgets.keys())
#def removeWidget(self, name):
#if name in self.widgets:
#del self.widgets[name]
##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys())
#self.sigWidgetListChanged.emit(self.widgets.keys())
#else:
#print "plot %s not managed" % name
#def listWidgets(self):
#return self.widgets.keys()
#def getWidget(self, name):
#if name not in self.widgets:
#return None
#else:
#return self.widgets[name]
#def linkX(self, p1, p2):
##QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged)
#p1.sigXRangeChanged.connect(p2.linkXChanged)
##QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged)
#p2.sigXRangeChanged.connect(p1.linkXChanged)
#p1.linkXChanged(p2)
##p2.setManualXScale()
#def unlinkX(self, p1, p2):
##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged)
#p1.sigXRangeChanged.disconnect(p2.linkXChanged)
##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged)
#p2.sigXRangeChanged.disconnect(p1.linkXChanged)
#def linkY(self, p1, p2):
##QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged)
#p1.sigYRangeChanged.connect(p2.linkYChanged)
##QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged)
#p2.sigYRangeChanged.connect(p1.linkYChanged)
#p1.linkYChanged(p2)
##p2.setManualYScale()
#def unlinkY(self, p1, p2):
##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged)
#p1.sigYRangeChanged.disconnect(p2.linkYChanged)
##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged)
#p2.sigYRangeChanged.disconnect(p1.linkYChanged)

View File

@ -116,7 +116,7 @@ class Ui_Form(object):
self.gridLayout_2.addWidget(self.yGridCheck, 1, 0, 1, 2)
self.gridAlphaSlider = QtGui.QSlider(self.gridGroup)
self.gridAlphaSlider.setMaximum(255)
self.gridAlphaSlider.setProperty("value", 70)
self.gridAlphaSlider.setProperty("value", 128)
self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal)
self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider"))
self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1)

View File

@ -221,7 +221,7 @@
<number>255</number>
</property>
<property name="value">
<number>70</number>
<number>128</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>

View File

@ -800,7 +800,7 @@ class ROI(GraphicsObject):
#print " dshape", dShape
## Determine transform that maps ROI bounding box to image coordinates
tr = self.sceneTransform() * img.sceneTransform().inverted()[0]
tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform())
## Modify transform to scale from image coords to data coords
#m = QtGui.QTransform()
@ -832,35 +832,34 @@ class ROI(GraphicsObject):
else:
return bounds, tr
def getArrayRegion(self, data, img, axes=(0,1)):
"""Use the position of this ROI relative to an imageItem to pull a slice from an array."""
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
"""Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array.
This method uses :func:`affineSlice <pyqtgraph.affineSlice>` to generate
the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to
pass to :func:`affineSlice <pyqtgraph.affineSlice>`.
shape = self.state['size']
If *returnMappedCoords* is True, then the method returns a tuple (result, coords)
such that coords is the set of coordinates used to interpolate values from the original
data, mapped into the parent coordinate system of the image. This is useful, when slicing
data from images that have been transformed, for determining the location of each value
in the sliced data.
origin = self.mapToItem(img, QtCore.QPointF(0, 0))
## vx and vy point in the directions of the slice axes, but must be scaled properly
vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin
vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin
lvx = np.sqrt(vx.x()**2 + vx.y()**2)
lvy = np.sqrt(vy.x()**2 + vy.y()**2)
pxLen = img.width() / float(data.shape[axes[0]])
sx = pxLen / lvx
sy = pxLen / lvy
vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy))
shape = self.state['size']
shape = [abs(shape[0]/sx), abs(shape[1]/sy)]
origin = (origin.x(), origin.y())
#print "shape", shape, "vectors", vectors, "origin", origin
return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, order=1)
All extra keyword arguments are passed to :func:`affineSlice <pyqtgraph.affineSlice>`.
"""
shape, vectors, origin = self.getAffineSliceParams(data, img, axes)
if not returnMappedCoords:
return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds)
else:
kwds['returnCoords'] = True
result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds)
tr = fn.transformToArray(img.transform())[:,:2].reshape((3, 2) + (1,)*(coords.ndim-1))
coords = coords[np.newaxis, ...]
mapped = (tr*coords).sum(axis=0)
return result, mapped
### transpose data so x and y are the first 2 axes
#trAx = range(0, data.ndim)
#trAx.remove(axes[0])
@ -959,6 +958,37 @@ class ROI(GraphicsObject):
### Untranspose array before returning
#return arr5.transpose(tr2)
def getAffineSliceParams(self, data, img, axes=(0.1)):
"""
Returns the parameters needed to use :func:`affineSlice <pyqtgraph.affineSlice>` to
extract a subset of *data* using this ROI and *img* to specify the subset.
See :func:`getArrayRegion <pyqtgraph.ROI.getArrayRegion>` for more information.
"""
shape = self.state['size']
origin = self.mapToItem(img, QtCore.QPointF(0, 0))
## vx and vy point in the directions of the slice axes, but must be scaled properly
vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin
vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin
lvx = np.sqrt(vx.x()**2 + vx.y()**2)
lvy = np.sqrt(vy.x()**2 + vy.y()**2)
pxLen = img.width() / float(data.shape[axes[0]])
#img.width is number of pixels or width of item?
#need pxWidth and pxHeight instead of pxLen ?
sx = pxLen / lvx
sy = pxLen / lvy
vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy))
shape = self.state['size']
shape = [abs(shape[0]/sx), abs(shape[1]/sy)]
origin = (origin.x(), origin.y())
return shape, vectors, origin
def getGlobalTransform(self, relativeTo=None):
"""Return global transformation (rotation angle+translation) required to move
from relative state to current state. If relative state isn't specified,
@ -1251,7 +1281,7 @@ class Handle(UIGraphicsItem):
v = dt.map(QtCore.QPointF(1, 0)) - dt.map(QtCore.QPointF(0, 0))
va = np.arctan2(v.y(), v.x())
dti = dt.inverted()[0]
dti = fn.invertQTransform(dt)
devPos = dt.map(QtCore.QPointF(0,0))
tr = QtGui.QTransform()
tr.translate(devPos.x(), devPos.y())

View File

@ -79,7 +79,7 @@ class ScatterPlotItem(GraphicsObject):
prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
GraphicsObject.__init__(self)
self.setFlag(self.ItemHasNoContents, True)
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)])
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('item', object), ('data', object)])
self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
@ -226,6 +226,7 @@ class ScatterPlotItem(GraphicsObject):
self.setPointData(kargs['data'], dataSet=newData)
#self.updateSpots()
self.prepareGeometryChange()
self.bounds = [None, None]
self.generateSpotItems()
self.sigPlotChanged.emit(self)
@ -396,7 +397,7 @@ class ScatterPlotItem(GraphicsObject):
if frac >= 1.0 and self.bounds[ax] is not None:
return self.bounds[ax]
self.prepareGeometryChange()
#self.prepareGeometryChange()
if self.data is None or len(self.data) == 0:
return (None, None)
@ -464,6 +465,7 @@ class ScatterPlotItem(GraphicsObject):
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
def viewRangeChanged(self):
self.prepareGeometryChange()
GraphicsObject.viewRangeChanged(self)
self.bounds = [None, None]
@ -557,7 +559,7 @@ class SpotItem(GraphicsItem):
If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead.
"""
symbol = self._data['symbol']
if symbol == '':
if symbol is None:
symbol = self._plot.opts['symbol']
try:
n = int(symbol)

View File

@ -1,6 +1,7 @@
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
from .UIGraphicsItem import *
import pyqtgraph.functions as fn
class TextItem(UIGraphicsItem):
"""
@ -87,7 +88,7 @@ class TextItem(UIGraphicsItem):
if br is None:
return
self.prepareGeometryChange()
self._bounds = self.deviceTransform().inverted()[0].mapRect(br)
self._bounds = fn.invertQTransform(self.deviceTransform()).mapRect(br)
#print self._bounds
def boundingRect(self):

View File

@ -1,4 +1,5 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.python2_3 import sortList
import numpy as np
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
@ -62,7 +63,7 @@ class ViewBox(GraphicsWidget):
NamedViews = weakref.WeakValueDictionary() # name: ViewBox
AllViews = weakref.WeakKeyDictionary() # ViewBox: None
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu = True, name=None):
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
"""
============= =============================================================
**Arguments**
@ -105,6 +106,8 @@ class ViewBox(GraphicsWidget):
'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode,
'enableMenu': enableMenu,
'wheelScaleFactor': -1.0 / 8.0,
'background': None,
}
@ -118,17 +121,23 @@ class ViewBox(GraphicsWidget):
self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses
## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
## this is a workaround for a Qt + OpenGL but that causes improper clipping
## this is a workaround for a Qt + OpenGL bug that causes improper clipping
## https://bugreports.qt.nokia.com/browse/QTBUG-23723
self.childGroup = ChildGroup(self)
self.childGroup.sigItemsChanged.connect(self.itemsChanged)
self.background = QtGui.QGraphicsRectItem(self.rect())
self.background.setParentItem(self)
self.background.setZValue(-1e6)
self.background.setPen(fn.mkPen(None))
self.updateBackground()
#self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
# this also enables capture of keyPressEvents.
## Make scale box that is shown when dragging on the view
self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
self.rbScaleBox.setPen(fn.mkPen((255,0,0), width=1))
self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1))
self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
self.rbScaleBox.hide()
self.addItem(self.rbScaleBox)
@ -286,6 +295,7 @@ class ViewBox(GraphicsWidget):
#self.updateAutoRange()
self.updateMatrix()
self.sigStateChanged.emit(self)
self.background.setRect(self.rect())
#self.linkedXChanged()
#self.linkedYChanged()
@ -349,7 +359,7 @@ class ViewBox(GraphicsWidget):
changes[1] = yRange
if len(changes) == 0:
print rect
print(rect)
raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect)))
changed = [False, False]
@ -442,10 +452,8 @@ class ViewBox(GraphicsWidget):
center = Point(vr.center())
else:
center = Point(center)
tl = center + (vr.topLeft()-center) * scale
br = center + (vr.bottomRight()-center) * scale
self.setRange(QtCore.QRectF(tl, br), padding=0)
def translateBy(self, t):
@ -755,7 +763,7 @@ class ViewBox(GraphicsWidget):
def mapToView(self, obj):
"""Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox"""
m = self.childTransform().inverted()[0]
m = fn.invertQTransform(self.childTransform())
return m.map(obj)
def mapFromView(self, obj):
@ -821,7 +829,7 @@ class ViewBox(GraphicsWidget):
mask[axis] = mv
s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor
center = Point(self.childGroup.transform().inverted()[0].map(ev.pos()))
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
#center = ev.pos()
self.scaleBy(s, center)
@ -854,7 +862,10 @@ class ViewBox(GraphicsWidget):
return self._menuCopy
def getContextMenus(self, event):
return self.menu.subMenus()
if self.menuEnabled():
return self.menu.subMenus()
else:
return None
#return [self.getMenu(event)]
@ -901,8 +912,11 @@ class ViewBox(GraphicsWidget):
dif = np.array([dif.x(), dif.y()])
dif[0] *= -1
s = ((mask * 0.02) + 1) ** dif
center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton)))
#center = Point(ev.buttonDownPos(QtCore.Qt.RightButton))
tr = self.childGroup.transform()
tr = fn.invertQTransform(tr)
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
self.scaleBy(s, center)
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
@ -1155,6 +1169,15 @@ class ViewBox(GraphicsWidget):
#self.scene().render(p)
#p.end()
def updateBackground(self):
bg = self.state['background']
if bg is None:
self.background.hide()
else:
self.background.show()
self.background.setBrush(fn.mkBrush(bg))
def updateViewLists(self):
def cmpViews(a, b):
wins = 100 * cmp(a.window() is self.window(), b.window() is self.window())

View File

@ -1,4 +1,5 @@
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.WidgetGroup import WidgetGroup
from .axisCtrlTemplate import Ui_Form as AxisCtrlTemplate
import weakref

View File

@ -38,7 +38,7 @@ from pyqtgraph.SignalProxy import SignalProxy
class PlotROI(ROI):
def __init__(self, size):
ROI.__init__(self, pos=[0,0], size=size, scaleSnap=True, translateSnap=True)
ROI.__init__(self, pos=[0,0], size=size) #, scaleSnap=True, translateSnap=True)
self.addScaleHandle([1, 1], [0, 0])
self.addRotateHandle([0, 0], [0.5, 0.5])
@ -67,7 +67,12 @@ class ImageView(QtGui.QWidget):
sigTimeChanged = QtCore.Signal(object, object)
sigProcessingChanged = QtCore.Signal(object)
def __init__(self, parent=None, name="ImageView", *args):
def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args):
"""
By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem. Custom items may be given instead
by specifying the *view* and/or *imageItem* arguments.
"""
QtGui.QWidget.__init__(self, parent, *args)
self.levelMax = 4096
self.levelMin = 0
@ -89,7 +94,10 @@ class ImageView(QtGui.QWidget):
#self.ui.graphicsView.setAspectLocked(True)
#self.ui.graphicsView.invertY()
#self.ui.graphicsView.enableMouse()
self.view = ViewBox()
if view is None:
self.view = ViewBox()
else:
self.view = view
self.ui.graphicsView.setCentralItem(self.view)
self.view.setAspectLocked(True)
self.view.invertY()
@ -101,7 +109,10 @@ class ImageView(QtGui.QWidget):
#self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255))
#self.ui.gradientWidget.setOrientation('right')
self.imageItem = ImageItem()
if imageItem is None:
self.imageItem = ImageItem()
else:
self.imageItem = imageItem
self.view.addItem(self.imageItem)
self.currentIndex = 0
@ -531,14 +542,18 @@ class ImageView(QtGui.QWidget):
axes = (1, 2)
else:
return
data = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes)
data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True)
if data is not None:
while data.ndim > 1:
data = data.mean(axis=1)
if image.ndim == 3:
self.roiCurve.setData(y=data, x=self.tVals)
else:
self.roiCurve.setData(y=data, x=list(range(len(data))))
while coords.ndim > 2:
coords = coords[:,:,0]
coords = coords - coords[:,0,np.newaxis]
xvals = (coords**2).sum(axis=0) ** 0.5
self.roiCurve.setData(y=data, x=xvals)
#self.ui.roiPlot.replot()
@ -664,4 +679,18 @@ class ImageView(QtGui.QWidget):
#return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0])
##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value()
def getView(self):
"""Return the ViewBox (or other compatible object) which displays the ImageItem"""
return self.view
def getImageItem(self):
"""Return the ImageItem for this ImageView."""
return self.imageItem
def getRoiPlot(self):
"""Return the ROI PlotWidget for this ImageView"""
return self.ui.roiPlot
def getHistogramWidget(self):
"""Return the HistogramLUTWidget for this ImageView"""
return self.ui.histogram

View File

@ -19,4 +19,6 @@ TODO:
(RemoteGraphicsView class)
"""
from processes import *
from .processes import *
from .parallelizer import Parallelize, CanceledError
from .remoteproxy import proxy

15
multiprocess/bootstrap.py Normal file
View File

@ -0,0 +1,15 @@
"""For starting up remote processes"""
import sys, pickle
if __name__ == '__main__':
name, port, authkey, targetStr, path = pickle.load(sys.stdin)
if path is not None:
## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list.
while len(sys.path) > 0:
sys.path.pop()
sys.path.extend(path)
#import pyqtgraph
#import pyqtgraph.multiprocess.processes
target = pickle.loads(targetStr) ## unpickling the target should import everything we need
target(name, port, authkey)
sys.exit(0)

View File

@ -2,6 +2,10 @@ import os, sys, time, multiprocessing
from processes import ForkedProcess
from remoteproxy import ExitError
class CanceledError(Exception):
"""Raised when the progress dialog is canceled during a processing operation."""
pass
class Parallelize:
"""
Class for ultra-simple inline parallelization on multi-core CPUs
@ -29,35 +33,82 @@ class Parallelize:
print results
The only major caveat is that *result* in the example above must be picklable.
The only major caveat is that *result* in the example above must be picklable,
since it is automatically sent via pipe back to the parent process.
"""
def __init__(self, tasks, workers=None, block=True, **kwds):
def __init__(self, tasks, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds):
"""
Args:
tasks - list of objects to be processed (Parallelize will determine how to distribute the tasks)
workers - number of worker processes or None to use number of CPUs in the system
kwds - objects to be shared by proxy with child processes
=============== ===================================================================
Arguments:
tasks list of objects to be processed (Parallelize will determine how to
distribute the tasks)
workers number of worker processes or None to use number of CPUs in the
system
progressDialog optional dict of arguments for ProgressDialog
to update while tasks are processed
randomReseed If True, each forked process will reseed its random number generator
to ensure independent results. Works with the built-in random
and numpy.random.
kwds objects to be shared by proxy with child processes (they will
appear as attributes of the tasker)
=============== ===================================================================
"""
self.block = block
## Generate progress dialog.
## Note that we want to avoid letting forked child processes play with progress dialogs..
self.showProgress = False
if progressDialog is not None:
self.showProgress = True
if isinstance(progressDialog, basestring):
progressDialog = {'labelText': progressDialog}
import pyqtgraph as pg
self.progressDlg = pg.ProgressDialog(**progressDialog)
if workers is None:
workers = multiprocessing.cpu_count()
workers = self.suggestedWorkerCount()
if not hasattr(os, 'fork'):
workers = 1
self.workers = workers
self.tasks = list(tasks)
self.kwds = kwds
self.reseed = randomReseed
self.kwds = kwds.copy()
self.kwds['_taskStarted'] = self._taskStarted
def __enter__(self):
self.proc = None
workers = self.workers
if workers == 1:
return Tasker(None, self.tasks, self.kwds)
if self.workers == 1:
return self.runSerial()
else:
return self.runParallel()
def __exit__(self, *exc_info):
if self.proc is not None: ## worker
try:
if exc_info[0] is not None:
sys.excepthook(*exc_info)
finally:
#print os.getpid(), 'exit'
os._exit(0)
else: ## parent
if self.showProgress:
self.progressDlg.__exit__(None, None, None)
def runSerial(self):
if self.showProgress:
self.progressDlg.__enter__()
self.progressDlg.setMaximum(len(self.tasks))
self.progress = {os.getpid(): []}
return Tasker(None, self.tasks, self.kwds)
def runParallel(self):
self.childs = []
## break up tasks into one set per worker
workers = self.workers
chunks = [[] for i in xrange(workers)]
i = 0
for i in range(len(self.tasks)):
@ -65,37 +116,91 @@ class Parallelize:
## fork and assign tasks to each worker
for i in range(workers):
proc = ForkedProcess(target=None, preProxy=self.kwds)
proc = ForkedProcess(target=None, preProxy=self.kwds, randomReseed=self.reseed)
if not proc.isParent:
self.proc = proc
return Tasker(proc, chunks[i], proc.forkedProxies)
else:
self.childs.append(proc)
## process events from workers until all have exited.
activeChilds = self.childs[:]
while len(activeChilds) > 0:
for ch in activeChilds:
## Keep track of the progress of each worker independently.
self.progress = {ch.childPid: [] for ch in self.childs}
## for each child process, self.progress[pid] is a list
## of task indexes. The last index is the task currently being
## processed; all others are finished.
try:
if self.showProgress:
self.progressDlg.__enter__()
self.progressDlg.setMaximum(len(self.tasks))
## process events from workers until all have exited.
activeChilds = self.childs[:]
pollInterval = 0.01
while len(activeChilds) > 0:
waitingChildren = 0
rem = []
try:
ch.processRequests()
except ExitError:
rem.append(ch)
for ch in rem:
activeChilds.remove(ch)
time.sleep(0.1)
for ch in activeChilds:
try:
n = ch.processRequests()
if n > 0:
waitingChildren += 1
except ExitError:
#print ch.childPid, 'process finished'
rem.append(ch)
if self.showProgress:
self.progressDlg += 1
#print "remove:", [ch.childPid for ch in rem]
for ch in rem:
activeChilds.remove(ch)
while True:
try:
os.waitpid(ch.childPid, 0)
break
except OSError as ex:
if ex.errno == 4: ## If we get this error, just try again
continue
#print "Ignored system call interruption"
else:
raise
#print [ch.childPid for ch in activeChilds]
if self.showProgress and self.progressDlg.wasCanceled():
for ch in activeChilds:
ch.kill()
raise CanceledError()
## adjust polling interval--prefer to get exactly 1 event per poll cycle.
if waitingChildren > 1:
pollInterval *= 0.7
elif waitingChildren == 0:
pollInterval /= 0.7
pollInterval = max(min(pollInterval, 0.5), 0.0005) ## but keep it within reasonable limits
time.sleep(pollInterval)
finally:
if self.showProgress:
self.progressDlg.__exit__(None, None, None)
return [] ## no tasks for parent process.
def __exit__(self, *exc_info):
if exc_info[0] is not None:
sys.excepthook(*exc_info)
if self.proc is not None:
os._exit(0)
def wait(self):
## wait for all child processes to finish
pass
@staticmethod
def suggestedWorkerCount():
return multiprocessing.cpu_count() ## is this really the best option?
def _taskStarted(self, pid, i, **kwds):
## called remotely by tasker to indicate it has started working on task i
#print pid, 'reported starting task', i
if self.showProgress:
if len(self.progress[pid]) > 0:
self.progressDlg += 1
if pid == os.getpid(): ## single-worker process
if self.progressDlg.wasCanceled():
raise CanceledError()
self.progress[pid].append(i)
class Tasker:
def __init__(self, proc, tasks, kwds):
@ -106,9 +211,13 @@ class Tasker:
def __iter__(self):
## we could fix this up such that tasks are retrieved from the parent process one at a time..
for task in self.tasks:
for i, task in enumerate(self.tasks):
self.index = i
#print os.getpid(), 'starting task', i
self._taskStarted(os.getpid(), i, _callSync='off')
yield task
if self.proc is not None:
#print os.getpid(), 'no more tasks'
self.proc.close()

View File

@ -1,10 +1,51 @@
from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy
import subprocess, atexit, os, sys, time, random, socket
import subprocess, atexit, os, sys, time, random, socket, signal
import cPickle as pickle
import multiprocessing.connection
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError']
class Process(RemoteEventHandler):
def __init__(self, name=None, target=None):
"""
Bases: RemoteEventHandler
This class is used to spawn and control a new python interpreter.
It uses subprocess.Popen to start the new process and communicates with it
using multiprocessing.Connection objects over a network socket.
By default, the remote process will immediately enter an event-processing
loop that carries out requests send from the parent process.
Remote control works mainly through proxy objects::
proc = Process() ## starts process, returns handle
rsys = proc._import('sys') ## asks remote process to import 'sys', returns
## a proxy which references the imported module
rsys.stdout.write('hello\n') ## This message will be printed from the remote
## process. Proxy objects can usually be used
## exactly as regular objects are.
proc.close() ## Request the remote process shut down
Requests made via proxy objects may be synchronous or asynchronous and may
return objects either by proxy or by value (if they are picklable). See
ProxyObject for more information.
"""
def __init__(self, name=None, target=None, copySysPath=True):
"""
============ =============================================================
Arguments:
name Optional name for this process used when printing messages
from the remote process.
target Optional function to call after starting remote process.
By default, this is startEventLoop(), which causes the remote
process to process requests from the parent process until it
is asked to quit. If you wish to specify a different target,
it must be picklable (bound methods are not).
copySysPath If true, copy the contents of sys.path to the remote process
============ =============================================================
"""
if target is None:
target = startEventLoop
if name is None:
@ -25,8 +66,12 @@ class Process(RemoteEventHandler):
port += 1
## start remote process, instruct it to run target function
self.proc = subprocess.Popen((sys.executable, __file__, 'remote'), stdin=subprocess.PIPE)
pickle.dump((name+'_child', port, authkey, target), self.proc.stdin)
sysPath = sys.path if copySysPath else None
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
self.proc = subprocess.Popen((sys.executable, bootstrap), stdin=subprocess.PIPE)
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
## set its sys.path properly before unpickling the target
pickle.dump((name+'_child', port, authkey, targetStr, sysPath), self.proc.stdin)
self.proc.stdin.close()
## open connection for remote process
@ -60,19 +105,32 @@ def startEventLoop(name, port, authkey):
class ForkedProcess(RemoteEventHandler):
"""
ForkedProcess is a substitute for Process that uses os.fork() to generate a new process.
This is much faster than starting a completely new interpreter, but carries some caveats
and limitations:
- open file handles are shared with the parent process, which is potentially dangerous
- it is not possible to have a QApplication in both parent and child process
(unless both QApplications are created _after_ the call to fork())
- generally not thread-safe. Also, threads are not copied by fork(); the new process
will have only one thread that starts wherever fork() was called in the parent process.
- forked processes are unceremoniously terminated when join() is called; they are not
given any opportunity to clean up. (This prevents them calling any cleanup code that
was only intended to be used by the parent process)
This is much faster than starting a completely new interpreter and child processes
automatically have a copy of the entire program state from before the fork. This
makes it an appealing approach when parallelizing expensive computations. (see
also Parallelizer)
However, fork() comes with some caveats and limitations:
- fork() is not available on Windows.
- It is not possible to have a QApplication in both parent and child process
(unless both QApplications are created _after_ the call to fork())
Attempts by the forked process to access Qt GUI elements created by the parent
will most likely cause the child to crash.
- Likewise, database connections are unlikely to function correctly in a forked child.
- Threads are not copied by fork(); the new process
will have only one thread that starts wherever fork() was called in the parent process.
- Forked processes are unceremoniously terminated when join() is called; they are not
given any opportunity to clean up. (This prevents them calling any cleanup code that
was only intended to be used by the parent process)
- Normally when fork()ing, open file handles are shared with the parent process,
which is potentially dangerous. ForkedProcess is careful to close all file handles
that are not explicitly needed--stdout, stderr, and a single pipe to the parent
process.
"""
def __init__(self, name=None, target=0, preProxy=None):
def __init__(self, name=None, target=0, preProxy=None, randomReseed=True):
"""
When initializing, an optional target may be given.
If no target is specified, self.eventLoop will be used.
@ -83,6 +141,9 @@ class ForkedProcess(RemoteEventHandler):
in the remote process (but do not need to be sent explicitly since
they are available immediately before the call to fork().
Proxies will be availabe as self.proxies[name].
If randomReseed is True, the built-in random and numpy.random generators
will be reseeded in the child process.
"""
self.hasJoined = False
if target == 0:
@ -101,16 +162,51 @@ class ForkedProcess(RemoteEventHandler):
pid = os.fork()
if pid == 0:
self.isParent = False
## We are now in the forked process; need to be extra careful what we touch while here.
## - no reading/writing file handles/sockets owned by parent process (stdout is ok)
## - don't touch QtGui or QApplication at all; these are landmines.
## - don't let the process call exit handlers
## -
## close all file handles we do not want shared with parent
conn.close()
sys.stdin.close() ## otherwise we screw with interactive prompts.
fid = remoteConn.fileno()
os.closerange(3, fid)
os.closerange(fid+1, 4096) ## just guessing on the maximum descriptor count..
## Override any custom exception hooks
def excepthook(*args):
import traceback
traceback.print_exception(*args)
sys.excepthook = excepthook
## Make it harder to access QApplication instance
if 'PyQt4.QtGui' in sys.modules:
sys.modules['PyQt4.QtGui'].QApplication = None
sys.modules.pop('PyQt4.QtGui', None)
sys.modules.pop('PyQt4.QtCore', None)
## sabotage atexit callbacks
atexit._exithandlers = []
atexit.register(lambda: os._exit(0))
if randomReseed:
if 'numpy.random' in sys.modules:
sys.modules['numpy.random'].seed(os.getpid() ^ int(time.time()*10000%10000))
if 'random' in sys.modules:
sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000))
RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid())
if target is not None:
target()
ppid = os.getppid()
self.forkedProxies = {}
for name, proxyId in proxyIDs.iteritems():
self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name]))
if target is not None:
target()
else:
self.isParent = True
self.childPid = pid
@ -127,10 +223,11 @@ class ForkedProcess(RemoteEventHandler):
self.processRequests() # exception raised when the loop should exit
time.sleep(0.01)
except ExitError:
sys.exit(0)
break
except:
print "Error occurred in forked event loop:"
sys.excepthook(*sys.exc_info())
sys.exit(0)
def join(self, timeout=10):
if self.hasJoined:
@ -138,10 +235,19 @@ class ForkedProcess(RemoteEventHandler):
#os.kill(pid, 9)
try:
self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation.
os.waitpid(self.childPid, 0)
except IOError: ## probably remote process has already quit
pass
self.hasJoined = True
def kill(self):
"""Immediately kill the forked remote process.
This is generally safe because forked processes are already
expected to _avoid_ any cleanup at exit."""
os.kill(self.childPid, signal.SIGKILL)
self.hasJoined = True
##Special set of subclasses that implement a Qt event loop instead.
@ -165,8 +271,33 @@ class RemoteQtEventHandler(RemoteEventHandler):
#raise
class QtProcess(Process):
def __init__(self, name=None):
Process.__init__(self, name, target=startQtEventLoop)
"""
QtProcess is essentially the same as Process, with two major differences:
- The remote process starts by running startQtEventLoop() which creates a
QApplication in the remote process and uses a QTimer to trigger
remote event processing. This allows the remote process to have its own
GUI.
- A QTimer is also started on the parent process which polls for requests
from the child process. This allows Qt signals emitted within the child
process to invoke slots on the parent process and vice-versa.
Example::
proc = QtProcess()
rQtGui = proc._import('PyQt4.QtGui')
btn = rQtGui.QPushButton('button on child process')
btn.show()
def slot():
print 'slot invoked on parent process'
btn.clicked.connect(proxy(slot)) # be sure to send a proxy of the slot
"""
def __init__(self, **kwds):
if 'target' not in kwds:
kwds['target'] = startQtEventLoop
Process.__init__(self, **kwds)
self.startEventTimer()
def startEventTimer(self):
@ -201,8 +332,3 @@ def startQtEventLoop(name, port, authkey):
app.exec_()
if __name__ == '__main__':
if len(sys.argv) == 2 and sys.argv[1] == 'remote': ## module has been invoked as script in new python interpreter.
name, port, authkey, target = pickle.load(sys.stdin)
target(name, port, authkey)
sys.exit(0)

View File

@ -9,7 +9,26 @@ class NoResultError(Exception):
class RemoteEventHandler(object):
"""
This class handles communication between two processes. One instance is present on
each process and listens for communication from the other process. This enables
(amongst other things) ObjectProxy instances to look up their attributes and call
their methods.
This class is responsible for carrying out actions on behalf of the remote process.
Each instance holds one end of a Connection which allows python
objects to be passed between processes.
For the most common operations, see _import(), close(), and transfer()
To handle and respond to incoming requests, RemoteEventHandler requires that its
processRequests method is called repeatedly (this is usually handled by the Process
classes defined in multiprocess.processes).
"""
handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process
## an object proxy belongs to
@ -55,19 +74,25 @@ class RemoteEventHandler(object):
def processRequests(self):
"""Process all pending requests from the pipe, return
after no more events are immediately available. (non-blocking)"""
after no more events are immediately available. (non-blocking)
Returns the number of events processed.
"""
if self.exited:
raise ExitError()
numProcessed = 0
while self.conn.poll():
try:
self.handleRequest()
numProcessed += 1
except ExitError:
self.exited = True
raise
except:
print "Error in process %s" % self.name
sys.excepthook(*sys.exc_info())
return numProcessed
def handleRequest(self):
"""Handle a single request from the remote process.
@ -175,6 +200,7 @@ class RemoteEventHandler(object):
self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result))
def replyError(self, reqId, *exc):
print "error:", self.name, reqId, exc[1]
excStr = traceback.format_exception(*exc)
try:
self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr))
@ -282,7 +308,9 @@ class RemoteEventHandler(object):
try:
optStr = pickle.dumps(opts)
except:
print "Error pickling:", opts
print "==== Error pickling this object: ===="
print opts
print "======================================="
raise
request = (request, reqId, optStr)
@ -381,8 +409,8 @@ class RemoteEventHandler(object):
def transfer(self, obj, **kwds):
"""
Transfer an object to the remote host (the object must be picklable) and return
a proxy for the new remote object.
Transfer an object by value to the remote host (the object must be picklable)
and return a proxy for the new remote object.
"""
return self.send(request='transfer', opts=dict(obj=obj), **kwds)
@ -395,7 +423,12 @@ class RemoteEventHandler(object):
class Request:
## used internally for tracking asynchronous requests and returning results
"""
Request objects are returned when calling an ObjectProxy in asynchronous mode
or if a synchronous call has timed out. Use hasResult() to ask whether
the result of the call has been returned yet. Use result() to get
the returned value.
"""
def __init__(self, process, reqId, description=None, timeout=10):
self.proc = process
self.description = description
@ -405,10 +438,13 @@ class Request:
self.timeout = timeout
def result(self, block=True, timeout=None):
"""Return the result for this request.
"""
Return the result for this request.
If block is True, wait until the result has arrived or *timeout* seconds passes.
If the timeout is reached, raise an exception. (use timeout=None to disable)
If block is False, raises an exception if the result has not arrived yet."""
If the timeout is reached, raise NoResultError. (use timeout=None to disable)
If block is False, raise NoResultError immediately if the result has not arrived yet.
"""
if self.gotResult:
return self._result
@ -434,16 +470,24 @@ class Request:
def hasResult(self):
"""Returns True if the result for this request has arrived."""
try:
#print "check result", self.description
self.result(block=False)
except NoResultError:
#print " -> not yet"
pass
return self.gotResult
class LocalObjectProxy(object):
"""Used for wrapping local objects to ensure that they are send by proxy to a remote host."""
"""
Used for wrapping local objects to ensure that they are send by proxy to a remote host.
Note that 'proxy' is just a shorter alias for LocalObjectProxy.
For example::
data = [1,2,3,4,5]
remotePlot.plot(data) ## by default, lists are pickled and sent by value
remotePlot.plot(proxy(data)) ## force the object to be sent by proxy
"""
nextProxyId = 0
proxiedObjects = {} ## maps {proxyId: object}
@ -467,24 +511,31 @@ class LocalObjectProxy(object):
del cls.proxiedObjects[pid]
#print "release:", cls.proxiedObjects
def __init__(self, obj):
def __init__(self, obj, **opts):
"""
Create a 'local' proxy object that, when sent to a remote host,
will appear as a normal ObjectProxy to *obj*.
Any extra keyword arguments are passed to proxy._setProxyOptions()
on the remote side.
"""
self.processId = os.getpid()
#self.objectId = id(obj)
self.typeStr = repr(obj)
#self.handler = handler
self.obj = obj
self.opts = opts
def __reduce__(self):
## a proxy is being pickled and sent to a remote process.
## every time this happens, a new proxy will be generated in the remote process,
## so we keep a new ID so we can track when each is released.
pid = LocalObjectProxy.registerObject(self.obj)
return (unpickleObjectProxy, (self.processId, pid, self.typeStr))
return (unpickleObjectProxy, (self.processId, pid, self.typeStr, None, self.opts))
## alias
proxy = LocalObjectProxy
def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None):
def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None, opts=None):
if processId == os.getpid():
obj = LocalObjectProxy.lookupProxyId(proxyId)
if attributes is not None:
@ -492,7 +543,10 @@ def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None):
obj = getattr(obj, attr)
return obj
else:
return ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr)
proxy = ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr)
if opts is not None:
proxy._setProxyOptions(**opts)
return proxy
class ObjectProxy(object):
"""
@ -501,7 +555,44 @@ class ObjectProxy(object):
attributes on existing proxy objects.
For the most part, this object can be used exactly as if it
were a local object.
were a local object::
rsys = proc._import('sys') # returns proxy to sys module on remote process
rsys.stdout # proxy to remote sys.stdout
rsys.stdout.write # proxy to remote sys.stdout.write
rsys.stdout.write('hello') # calls sys.stdout.write('hello') on remote machine
# and returns the result (None)
When calling a proxy to a remote function, the call can be made synchronous
(result of call is returned immediately), asynchronous (result is returned later),
or return can be disabled entirely::
ros = proc._import('os')
## synchronous call; result is returned immediately
pid = ros.getpid()
## asynchronous call
request = ros.getpid(_callSync='async')
while not request.hasResult():
time.sleep(0.01)
pid = request.result()
## disable return when we know it isn't needed
rsys.stdout.write('hello', _callSync='off')
Additionally, values returned from a remote function call are automatically
returned either by value (must be picklable) or by proxy.
This behavior can be forced::
rnp = proc._import('numpy')
arrProxy = rnp.array([1,2,3,4], _returnType='proxy')
arrValue = rnp.array([1,2,3,4], _returnType='value')
The default callSync and returnType behaviors (as well as others) can be set
for each proxy individually using ObjectProxy._setProxyOptions() or globally using
proc.setProxyOptions().
"""
def __init__(self, processId, proxyId, typeStr='', parent=None):
object.__init__(self)
@ -574,6 +665,13 @@ class ObjectProxy(object):
"""
self._proxyOptions.update(kwds)
def _getValue(self):
"""
Return the value of the proxied object
(the remote object must be picklable)
"""
return self._handler.getObjValue(self)
def _getProxyOption(self, opt):
val = self._proxyOptions[opt]
if val is None:
@ -591,20 +689,31 @@ class ObjectProxy(object):
return "<ObjectProxy for process %d, object 0x%x: %s >" % (self._processId, self._proxyId, self._typeStr)
def __getattr__(self, attr):
#if '_processId' not in self.__dict__:
#raise Exception("ObjectProxy has no processId")
#proc = Process._processes[self._processId]
deferred = self._getProxyOption('deferGetattr')
if deferred is True:
def __getattr__(self, attr, **kwds):
"""
Calls __getattr__ on the remote object and returns the attribute
by value or by proxy depending on the options set (see
ObjectProxy._setProxyOptions and RemoteEventHandler.setProxyOptions)
If the option 'deferGetattr' is True for this proxy, then a new proxy object
is returned _without_ asking the remote object whether the named attribute exists.
This can save time when making multiple chained attribute requests,
but may also defer a possible AttributeError until later, making
them more difficult to debug.
"""
opts = self._getProxyOptions()
for k in opts:
if '_'+k in kwds:
opts[k] = kwds.pop('_'+k)
if opts['deferGetattr'] is True:
return self._deferredAttr(attr)
else:
opts = self._getProxyOptions()
#opts = self._getProxyOptions()
return self._handler.getObjAttr(self, attr, **opts)
def _deferredAttr(self, attr):
return DeferredObjectProxy(self, attr)
def __call__(self, *args, **kwds):
"""
Attempts to call the proxied object from the remote process.
@ -613,44 +722,34 @@ class ObjectProxy(object):
_callSync 'off', 'sync', or 'async'
_returnType 'value', 'proxy', or 'auto'
If the remote call raises an exception on the remote process,
it will be re-raised on the local process.
"""
#opts = {}
#callSync = kwds.pop('_callSync', self.)
#if callSync is not None:
#opts['callSync'] = callSync
#returnType = kwds.pop('_returnType', self._defaultReturnValue)
#if returnType is not None:
#opts['returnType'] = returnType
opts = self._getProxyOptions()
for k in opts:
if '_'+k in kwds:
opts[k] = kwds.pop('_'+k)
#print "call", opts
return self._handler.callObj(obj=self, args=args, kwds=kwds, **opts)
def _getValue(self):
## this just gives us an easy way to change the behavior of the special methods
#proc = Process._processes[self._processId]
return self._handler.getObjValue(self)
## Explicitly proxy special methods. Is there a better way to do this??
def _getSpecialAttr(self, attr):
#return self.__getattr__(attr)
## this just gives us an easy way to change the behavior of the special methods
return self._deferredAttr(attr)
def __getitem__(self, *args):
return self._getSpecialAttr('__getitem__')(*args)
def __setitem__(self, *args):
return self._getSpecialAttr('__setitem__')(*args)
return self._getSpecialAttr('__setitem__')(*args, _callSync='off')
def __setattr__(self, *args):
return self._getSpecialAttr('__setattr__')(*args)
return self._getSpecialAttr('__setattr__')(*args, _callSync='off')
def __str__(self, *args):
return self._getSpecialAttr('__str__')(*args, _returnType=True)
return self._getSpecialAttr('__str__')(*args, _returnType='value')
def __len__(self, *args):
return self._getSpecialAttr('__len__')(*args)
@ -670,6 +769,21 @@ class ObjectProxy(object):
def __pow__(self, *args):
return self._getSpecialAttr('__pow__')(*args)
def __iadd__(self, *args):
return self._getSpecialAttr('__iadd__')(*args, _callSync='off')
def __isub__(self, *args):
return self._getSpecialAttr('__isub__')(*args, _callSync='off')
def __idiv__(self, *args):
return self._getSpecialAttr('__idiv__')(*args, _callSync='off')
def __imul__(self, *args):
return self._getSpecialAttr('__imul__')(*args, _callSync='off')
def __ipow__(self, *args):
return self._getSpecialAttr('__ipow__')(*args, _callSync='off')
def __rshift__(self, *args):
return self._getSpecialAttr('__rshift__')(*args)
@ -679,6 +793,15 @@ class ObjectProxy(object):
def __floordiv__(self, *args):
return self._getSpecialAttr('__pow__')(*args)
def __irshift__(self, *args):
return self._getSpecialAttr('__rshift__')(*args, _callSync='off')
def __ilshift__(self, *args):
return self._getSpecialAttr('__lshift__')(*args, _callSync='off')
def __ifloordiv__(self, *args):
return self._getSpecialAttr('__pow__')(*args, _callSync='off')
def __eq__(self, *args):
return self._getSpecialAttr('__eq__')(*args)
@ -704,7 +827,16 @@ class ObjectProxy(object):
return self._getSpecialAttr('__or__')(*args)
def __xor__(self, *args):
return self._getSpecialAttr('__or__')(*args)
return self._getSpecialAttr('__xor__')(*args)
def __iand__(self, *args):
return self._getSpecialAttr('__iand__')(*args, _callSync='off')
def __ior__(self, *args):
return self._getSpecialAttr('__ior__')(*args, _callSync='off')
def __ixor__(self, *args):
return self._getSpecialAttr('__ixor__')(*args, _callSync='off')
def __mod__(self, *args):
return self._getSpecialAttr('__mod__')(*args)
@ -746,6 +878,37 @@ class ObjectProxy(object):
return self._getSpecialAttr('__rmod__')(*args)
class DeferredObjectProxy(ObjectProxy):
"""
This class represents an attribute (or sub-attribute) of a proxied object.
It is used to speed up attribute requests. Take the following scenario::
rsys = proc._import('sys')
rsys.stdout.write('hello')
For this simple example, a total of 4 synchronous requests are made to
the remote process:
1) import sys
2) getattr(sys, 'stdout')
3) getattr(stdout, 'write')
4) write('hello')
This takes a lot longer than running the equivalent code locally. To
speed things up, we can 'defer' the two attribute lookups so they are
only carried out when neccessary::
rsys = proc._import('sys')
rsys._setProxyOptions(deferGetattr=True)
rsys.stdout.write('hello')
This example only makes two requests to the remote process; the two
attribute lookups immediately return DeferredObjectProxy instances
immediately without contacting the remote process. When the call
to write() is made, all attribute requests are processed at the same time.
Note that if the attributes requested do not exist on the remote object,
making the call to write() will raise an AttributeError.
"""
def __init__(self, parentProxy, attribute):
## can't set attributes directly because setattr is overridden.
for k in ['_processId', '_typeStr', '_proxyId', '_handler']:
@ -756,4 +919,10 @@ class DeferredObjectProxy(ObjectProxy):
def __repr__(self):
return ObjectProxy.__repr__(self) + '.' + '.'.join(self._attributes)
def _undefer(self):
"""
Return a non-deferred ObjectProxy referencing the same object
"""
return self._parent.__getattr__(self._attributes[-1], _deferGetattr=False)

View File

@ -29,12 +29,18 @@ class GLViewWidget(QtOpenGL.QGLWidget):
self.keysPressed = {}
self.keyTimer = QtCore.QTimer()
self.keyTimer.timeout.connect(self.evalKeyState)
self.makeCurrent()
def addItem(self, item):
self.items.append(item)
if hasattr(item, 'initializeGL'):
self.makeCurrent()
item.initializeGL()
try:
item.initializeGL()
except:
self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item))
item._setView(self)
#print "set view", item, self, item.view()
self.update()
@ -100,20 +106,28 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glPushAttrib(GL_ALL_ATTRIB_BITS)
i.paint()
except:
import sys
sys.excepthook(*sys.exc_info())
print("Error while drawing item", i)
import pyqtgraph.debug
pyqtgraph.debug.printExc()
msg = "Error while drawing item %s." % str(item)
ver = glGetString(GL_VERSION).split()[0]
if int(ver.split('.')[0]) < 2:
print(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
else:
print(msg)
finally:
glPopAttrib(GL_ALL_ATTRIB_BITS)
glPopAttrib()
else:
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
tr = i.transform()
a = np.array(tr.copyDataTo()).reshape((4,4))
glMultMatrixf(a.transpose())
self.drawItemTree(i)
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
try:
tr = i.transform()
a = np.array(tr.copyDataTo()).reshape((4,4))
glMultMatrixf(a.transpose())
self.drawItemTree(i)
finally:
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
def cameraPosition(self):
@ -237,4 +251,15 @@ class GLViewWidget(QtOpenGL.QGLWidget):
else:
self.keyTimer.stop()
def checkOpenGLVersion(self, msg):
## Only to be called from within exception handler.
ver = glGetString(GL_VERSION).split()[0]
if int(ver.split('.')[0]) < 2:
import pyqtgraph.debug
pyqtgraph.debug.printExc()
raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
else:
raise

16
opengl/glInfo.py Normal file
View File

@ -0,0 +1,16 @@
from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL
from OpenGL.GL import *
app = QtGui.QApplication([])
class GLTest(QtOpenGL.QGLWidget):
def __init__(self):
QtOpenGL.QGLWidget.__init__(self)
self.makeCurrent()
print "GL version:", glGetString(GL_VERSION)
print "MAX_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_TEXTURE_SIZE)
print "MAX_3D_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE)
print "Extensions:", glGetString(GL_EXTENSIONS)
GLTest()

View File

@ -43,6 +43,11 @@ class GLVolumeItem(GLGraphicsItem):
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER)
shape = self.data.shape
## Test texture dimensions first
glTexImage3D(GL_PROXY_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, 0, GL_TEXTURE_WIDTH) == 0:
raise Exception("OpenGL failed to create 3D texture (%dx%dx%d); too large for this hardware." % shape[:3])
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data.transpose((2,1,0,3)))
glDisable(GL_TEXTURE_3D)

View File

@ -131,11 +131,16 @@ class Parameter(QtCore.QObject):
return name
def childPath(self, child):
"""Return the path of parameter names from self to child."""
"""
Return the path of parameter names from self to child.
If child is not a (grand)child of self, return None.
"""
path = []
while child is not self:
path.insert(0, child.name())
child = child.parent()
if child is None:
return None
return path
def setValue(self, value, blockSignal=None):

View File

@ -113,4 +113,6 @@ class ParameterTree(TreeWidget):
sel[0].selected(True)
return TreeWidget.selectionChanged(self, *args)
def wheelEvent(self, ev):
self.clearSelection()
return TreeWidget.wheelEvent(self, ev)

View File

@ -1,132 +0,0 @@
## tests for ParameterTree
## make sure pyqtgraph is in path
import sys,os
md = os.path.abspath(os.path.dirname(__file__))
sys.path.append(os.path.join(md, '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui
import collections, user
app = QtGui.QApplication([])
import pyqtgraph.parametertree.parameterTypes as pTypes
from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType
## test subclassing parameters
## This parameter automatically generates two child parameters which are always reciprocals of each other
class ComplexParameter(Parameter):
def __init__(self, **opts):
opts['type'] = 'bool'
opts['value'] = True
Parameter.__init__(self, **opts)
self.addChild({'name': 'A = 1/B', 'type': 'float', 'value': 7, 'suffix': 'Hz', 'siPrefix': True})
self.addChild({'name': 'B = 1/A', 'type': 'float', 'value': 1/7., 'suffix': 's', 'siPrefix': True})
self.a = self.param('A = 1/B')
self.b = self.param('B = 1/A')
self.a.sigValueChanged.connect(self.aChanged)
self.b.sigValueChanged.connect(self.bChanged)
def aChanged(self):
self.b.setValue(1.0 / self.a.value(), blockSignal=self.bChanged)
def bChanged(self):
self.a.setValue(1.0 / self.b.value(), blockSignal=self.aChanged)
## test add/remove
## this group includes a menu allowing the user to add new parameters into its child list
class ScalableGroup(pTypes.GroupParameter):
def __init__(self, **opts):
opts['type'] = 'group'
opts['addText'] = "Add"
opts['addList'] = ['str', 'float', 'int']
pTypes.GroupParameter.__init__(self, **opts)
def addNew(self, typ):
val = {
'str': '',
'float': 0.0,
'int': 0
}[typ]
self.addChild(dict(name="ScalableParam %d" % (len(self.childs)+1), type=typ, value=val, removable=True, renamable=True))
## test column spanning (widget sub-item that spans all columns)
class TextParameterItem(pTypes.WidgetParameterItem):
def __init__(self, param, depth):
pTypes.WidgetParameterItem.__init__(self, param, depth)
self.subItem = QtGui.QTreeWidgetItem()
self.addChild(self.subItem)
def treeWidgetChanged(self):
self.treeWidget().setFirstItemColumnSpanned(self.subItem, True)
self.treeWidget().setItemWidget(self.subItem, 0, self.textBox)
self.setExpanded(True)
def makeWidget(self):
self.textBox = QtGui.QTextEdit()
self.textBox.setMaximumHeight(100)
self.textBox.value = lambda: str(self.textBox.toPlainText())
self.textBox.setValue = self.textBox.setPlainText
self.textBox.sigChanged = self.textBox.textChanged
return self.textBox
class TextParameter(Parameter):
type = 'text'
itemClass = TextParameterItem
registerParameterType('text', TextParameter)
params = [
{'name': 'Group 0', 'type': 'group', 'children': [
{'name': 'Param 1', 'type': 'int', 'value': 10},
{'name': 'Param 2', 'type': 'float', 'value': 10},
]},
{'name': 'Group 1', 'type': 'group', 'children': [
{'name': 'Param 1.1', 'type': 'float', 'value': 1.2e-6, 'dec': True, 'siPrefix': True, 'suffix': 'V'},
{'name': 'Param 1.2', 'type': 'float', 'value': 1.2e6, 'dec': True, 'siPrefix': True, 'suffix': 'Hz'},
{'name': 'Group 1.3', 'type': 'group', 'children': [
{'name': 'Param 1.3.1', 'type': 'int', 'value': 11, 'limits': (-7, 15), 'default': -6},
{'name': 'Param 1.3.2', 'type': 'float', 'value': 1.2e6, 'dec': True, 'siPrefix': True, 'suffix': 'Hz', 'readonly': True},
]},
{'name': 'Param 1.4', 'type': 'str', 'value': "hi"},
{'name': 'Param 1.5', 'type': 'list', 'values': [1,2,3], 'value': 2},
{'name': 'Param 1.6', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2},
ComplexParameter(name='ComplexParam'),
ScalableGroup(name="ScalableGroup", children=[
{'name': 'ScalableParam 1', 'type': 'str', 'value': "hi"},
{'name': 'ScalableParam 2', 'type': 'str', 'value': "hi"},
])
]},
{'name': 'Param 5', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"},
{'name': 'Param 6', 'type': 'color', 'value': "FF0", 'tip': "This is a color button. It cam be renamed.", 'renamable': True},
{'name': 'TextParam', 'type': 'text', 'value': 'Some text...'},
]
#p = pTypes.ParameterSet("params", params)
p = Parameter(name='params', type='group', children=params)
def change(param, changes):
print("tree changes:")
for param, change, data in changes:
print(" [" + '.'.join(p.childPath(param))+ "] ", change, data)
p.sigTreeStateChanged.connect(change)
t = ParameterTree()
t.setParameters(p, showTop=False)
t.show()
t.resize(400,600)
t2 = ParameterTree()
t2.setParameters(p, showTop=False)
t2.show()
t2.resize(400,600)
import sys
if sys.flags.interactive == 0:
app.exec_()

View File

@ -1,4 +1,5 @@
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.python2_3 import asUnicode
from .Parameter import Parameter, registerParameterType
from .ParameterItem import ParameterItem
from pyqtgraph.widgets.SpinBox import SpinBox
@ -348,6 +349,7 @@ class GroupParameterItem(ParameterItem):
def treeWidgetChanged(self):
ParameterItem.treeWidgetChanged(self)
self.treeWidget().setFirstItemColumnSpanned(self, True)
if self.addItem is not None:
self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox)
self.treeWidget().setFirstItemColumnSpanned(self.addItem, True)

View File

@ -42,8 +42,9 @@ def sortList(l, cmpFunc):
if sys.version_info[0] == 3:
import builtins
builtins.basestring = str
builtins.asUnicode = asUnicode
builtins.sortList = sortList
#builtins.asUnicode = asUnicode
#builtins.sortList = sortList
basestring = str
def cmp(a,b):
if a>b:
return 1
@ -52,7 +53,7 @@ if sys.version_info[0] == 3:
else:
return 0
builtins.cmp = cmp
else:
import __builtin__
__builtin__.asUnicode = asUnicode
__builtin__.sortList = sortList
#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe
#import __builtin__
#__builtin__.asUnicode = asUnicode
#__builtin__.sortList = sortList

View File

@ -34,6 +34,7 @@ def reloadAll(prefix=None, debug=False):
- Skips reload if the file has not been updated (if .pyc is newer than .py)
- if prefix is None, checks all loaded modules
"""
failed = []
for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload
if not inspect.ismodule(mod):
continue
@ -58,7 +59,10 @@ def reloadAll(prefix=None, debug=False):
reload(mod, debug=debug)
except:
printExc("Error while reloading module %s, skipping\n" % mod)
failed.append(mod.__name__)
if len(failed) > 0:
raise Exception("Some modules failed to reload: %s" % ', '.join(failed))
def reload(module, debug=False, lists=False, dicts=False):
"""Replacement for the builtin reload function:

View File

@ -15,7 +15,7 @@ class GradientWidget(GraphicsView):
def __init__(self, parent=None, orientation='bottom', *args, **kargs):
GraphicsView.__init__(self, parent, useOpenGL=False, background=None)
self.maxDim = 27
self.maxDim = 31
kargs['tickPen'] = 'k'
self.item = GradientEditorItem(*args, **kargs)
self.item.sigGradientChanged.connect(self.sigGradientChanged)
@ -24,7 +24,7 @@ class GradientWidget(GraphicsView):
self.setCacheMode(self.CacheNone)
self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing)
self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain)
self.setBackgroundRole(QtGui.QPalette.NoRole)
#self.setBackgroundRole(QtGui.QPalette.NoRole)
#self.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
#self.setAutoFillBackground(False)
#self.setAttribute(QtCore.Qt.WA_PaintOnScreen, False)

View File

@ -6,6 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
try:
from pyqtgraph.Qt import QtOpenGL
@ -13,12 +14,8 @@ try:
except ImportError:
HAVE_OPENGL = False
#from numpy import vstack
#import time
from pyqtgraph.Point import Point
#from vector import *
import sys, os
#import debug
from .FileDialog import FileDialog
from pyqtgraph.GraphicsScene import GraphicsScene
import numpy as np
@ -29,6 +26,20 @@ import pyqtgraph
__all__ = ['GraphicsView']
class GraphicsView(QtGui.QGraphicsView):
"""Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the
viewed coordinate range. Also automatically creates a GraphicsScene and a central QGraphicsWidget
that is automatically scaled to the full view geometry.
This widget is the basis for :class:`PlotWidget <pyqtgraph.PlotWidget>`,
:class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and the view widget in
:class:`ImageView <pyqtgraph.ImageView>`.
By default, the view coordinate system matches the widget's pixel coordinates and
automatically updates when the view is resized. This can be overridden by setting
autoPixelRange=False. The exact visible range can be set with setRange().
The view can be panned using the middle mouse button and scaled using the right mouse button if
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
sigRangeChanged = QtCore.Signal(object, object)
sigMouseReleased = QtCore.Signal(object)
@ -37,17 +48,25 @@ class GraphicsView(QtGui.QGraphicsView):
sigScaleChanged = QtCore.Signal(object)
lastFileDir = None
def __init__(self, parent=None, useOpenGL=None, background='k'):
"""Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the
viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget
that is automatically scaled to the full view geometry.
def __init__(self, parent=None, useOpenGL=None, background='default'):
"""
============ ============================================================
Arguments:
parent Optional parent widget
useOpenGL If True, the GraphicsView will use OpenGL to do all of its
rendering. This can improve performance on some systems,
but may also introduce bugs (the combination of
QGraphicsView and QGLWidget is still an 'experimental'
feature of Qt)
background Set the background color of the GraphicsView. Accepts any
single argument accepted by
:func:`mkColor <pyqtgraph.mkColor>`. By
default, the background color is determined using the
'backgroundColor' configuration option (see
:func:`setConfigOption <pyqtgraph.setConfigOption>`.
============ ============================================================
"""
By default, the view coordinate system matches the widget's pixel coordinates and
automatically updates when the view is resized. This can be overridden by setting
autoPixelRange=False. The exact visible range can be set with setRange().
The view can be panned using the middle mouse button and scaled using the right mouse button if
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
self.closed = False
QtGui.QGraphicsView.__init__(self, parent)
@ -62,9 +81,7 @@ class GraphicsView(QtGui.QGraphicsView):
## This might help, but it's probably dangerous in the general case..
#self.setOptimizationFlag(self.DontSavePainterState, True)
if background is not None:
brush = fn.mkBrush(background)
self.setBackgroundBrush(brush)
self.setBackground(background)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setFrameShape(QtGui.QFrame.NoFrame)
@ -75,13 +92,10 @@ class GraphicsView(QtGui.QGraphicsView):
self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate)
#self.setSceneRect(QtCore.QRectF(-1e10, -1e10, 2e10, 2e10))
self.lockedViewports = []
self.lastMousePos = None
self.setMouseTracking(True)
self.aspectLocked = False
#self.yInverted = True
self.range = QtCore.QRectF(0, 0, 1, 1)
self.autoPixelRange = True
self.currentItem = None
@ -90,6 +104,11 @@ class GraphicsView(QtGui.QGraphicsView):
self.sceneObj = GraphicsScene()
self.setScene(self.sceneObj)
## Workaround for PySide crash
## This ensures that the scene will outlive the view.
if pyqtgraph.Qt.USE_PYSIDE:
self.sceneObj._view_ref_workaround = self
## by default we set up a central widget with a grid layout.
## this can be replaced if needed.
self.centralWidget = None
@ -101,15 +120,25 @@ class GraphicsView(QtGui.QGraphicsView):
self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False)
self.clickAccepted = False
#def paintEvent(self, *args):
#prof = debug.Profiler('GraphicsView.paintEvent '+str(id(self)), disabled=False)
#QtGui.QGraphicsView.paintEvent(self, *args)
#prof.finish()
def setBackground(self, background):
"""
Set the background color of the GraphicsView.
To use the defaults specified py pyqtgraph.setConfigOption, use background='default'.
To make the background transparent, use background=None.
"""
self._background = background
if background == 'default':
background = pyqtgraph.getConfigOption('background')
if background is None:
self.setBackgroundRole(QtGui.QPalette.NoRole)
else:
brush = fn.mkBrush(background)
self.setBackgroundBrush(brush)
def close(self):
self.centralWidget = None
self.scene().clear()
#print " ", self.scene().itemCount()
self.currentItem = None
self.sceneObj = None
self.closed = True
@ -123,11 +152,9 @@ class GraphicsView(QtGui.QGraphicsView):
else:
v = QtGui.QWidget()
#v.setStyleSheet("background-color: #000000;")
self.setViewport(v)
def keyPressEvent(self, ev):
#QtGui.QGraphicsView.keyPressEvent(self, ev)
self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene
## (view likes to eat arrow key events)
@ -136,7 +163,8 @@ class GraphicsView(QtGui.QGraphicsView):
return self.setCentralWidget(item)
def setCentralWidget(self, item):
"""Sets a QGraphicsWidget to automatically fill the entire view."""
"""Sets a QGraphicsWidget to automatically fill the entire view (the item will be automatically
resize whenever the GraphicsView is resized)."""
if self.centralWidget is not None:
self.scene().removeItem(self.centralWidget)
self.centralWidget = item
@ -162,15 +190,18 @@ class GraphicsView(QtGui.QGraphicsView):
return
if self.autoPixelRange:
self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height())
GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False)
GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way.
self.updateMatrix()
def updateMatrix(self, propagate=True):
self.setSceneRect(self.range)
if self.aspectLocked:
self.fitInView(self.range, QtCore.Qt.KeepAspectRatio)
if self.autoPixelRange:
self.resetTransform()
else:
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
if self.aspectLocked:
self.fitInView(self.range, QtCore.Qt.KeepAspectRatio)
else:
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
self.sigRangeChanged.emit(self, self.range)
@ -196,11 +227,6 @@ class GraphicsView(QtGui.QGraphicsView):
scale = [sx, sy]
if self.aspectLocked:
scale[0] = scale[1]
#adj = (self.range.width()*0.5*(1.0-(1.0/scale[0])), self.range.height()*0.5*(1.0-(1.0/scale[1])))
#print "======\n", scale, adj
#print self.range
#self.range.adjust(adj[0], adj[1], -adj[0], -adj[1])
#print self.range
if self.scaleCenter:
center = None
@ -270,13 +296,6 @@ class GraphicsView(QtGui.QGraphicsView):
r1.setBottom(r.bottom())
GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False)
#def invertY(self, invert=True):
##if self.yInverted != invert:
##self.scale[1] *= -1.
#self.yInverted = invert
#self.updateMatrix()
def wheelEvent(self, ev):
QtGui.QGraphicsView.wheelEvent(self, ev)
if not self.mouseEnabled:
@ -289,39 +308,11 @@ class GraphicsView(QtGui.QGraphicsView):
def setAspectLocked(self, s):
self.aspectLocked = s
#def mouseDoubleClickEvent(self, ev):
#QtGui.QGraphicsView.mouseDoubleClickEvent(self, ev)
#pass
### This function is here because interactive mode is disabled due to bugs.
#def graphicsSceneEvent(self, ev, pev=None, fev=None):
#ev1 = GraphicsSceneMouseEvent()
#ev1.setPos(QtCore.QPointF(ev.pos().x(), ev.pos().y()))
#ev1.setButtons(ev.buttons())
#ev1.setButton(ev.button())
#ev1.setModifiers(ev.modifiers())
#ev1.setScenePos(self.mapToScene(QtCore.QPoint(ev.pos())))
#if pev is not None:
#ev1.setLastPos(pev.pos())
#ev1.setLastScenePos(pev.scenePos())
#ev1.setLastScreenPos(pev.screenPos())
#if fev is not None:
#ev1.setButtonDownPos(fev.pos())
#ev1.setButtonDownScenePos(fev.scenePos())
#ev1.setButtonDownScreenPos(fev.screenPos())
#return ev1
def leaveEvent(self, ev):
self.scene().leaveEvent(ev) ## inform scene when mouse leaves
def mousePressEvent(self, ev):
QtGui.QGraphicsView.mousePressEvent(self, ev)
#print "Press over:"
#for i in self.items(ev.pos()):
# print i.zValue(), int(i.acceptedMouseButtons()), i, i.scenePos()
#print "Event accepted:", ev.isAccepted()
#print "Grabber:", self.scene().mouseGrabberItem()
if not self.mouseEnabled:
@ -333,39 +324,14 @@ class GraphicsView(QtGui.QGraphicsView):
self.scene().clearSelection()
return ## Everything below disabled for now..
#self.currentItem = None
#maxZ = None
#for i in self.items(ev.pos()):
#if maxZ is None or maxZ < i.zValue():
#self.currentItem = i
#maxZ = i.zValue()
#print "make event"
#self.pev = self.graphicsSceneEvent(ev)
#self.fev = self.pev
#if self.currentItem is not None:
#self.currentItem.mousePressEvent(self.pev)
##self.clearMouse()
##self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
#self.emit(QtCore.SIGNAL("mousePressed(PyQt_PyObject)"), self.mouseTrail)
def mouseReleaseEvent(self, ev):
QtGui.QGraphicsView.mouseReleaseEvent(self, ev)
if not self.mouseEnabled:
return
#self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
#self.emit(QtCore.SIGNAL("mouseReleased"), ev)
self.sigMouseReleased.emit(ev)
self.lastButtonReleased = ev.button()
return ## Everything below disabled for now..
##self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
#self.emit(QtCore.SIGNAL("mouseReleased(PyQt_PyObject)"), self.mouseTrail)
#if self.currentItem is not None:
#pev = self.graphicsSceneEvent(ev, self.pev, self.fev)
#self.pev = pev
#self.currentItem.mouseReleaseEvent(pev)
#self.currentItem = None
def mouseMoveEvent(self, ev):
if self.lastMousePos is None:
self.lastMousePos = Point(ev.pos())
@ -375,10 +341,7 @@ class GraphicsView(QtGui.QGraphicsView):
QtGui.QGraphicsView.mouseMoveEvent(self, ev)
if not self.mouseEnabled:
return
#self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos()))
self.sigSceneMouseMoved.emit(self.mapToScene(ev.pos()))
#print "moved. Grabber:", self.scene().mouseGrabberItem()
if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it.
return
@ -386,10 +349,7 @@ class GraphicsView(QtGui.QGraphicsView):
if ev.buttons() == QtCore.Qt.RightButton:
delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50))
scale = 1.01 ** delta
#if self.yInverted:
#scale[0] = 1. / scale[0]
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
#self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range)
self.sigRangeChanged.emit(self, self.range)
elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button.
@ -397,23 +357,8 @@ class GraphicsView(QtGui.QGraphicsView):
tr = -delta * px
self.translate(tr[0], tr[1])
#self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range)
self.sigRangeChanged.emit(self, self.range)
#return ## Everything below disabled for now..
##self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
#if self.currentItem is not None:
#pev = self.graphicsSceneEvent(ev, self.pev, self.fev)
#self.pev = pev
#self.currentItem.mouseMoveEvent(pev)
#def paintEvent(self, ev):
#prof = debug.Profiler('GraphicsView.paintEvent (0x%x)' % id(self))
#QtGui.QGraphicsView.paintEvent(self, ev)
#prof.finish()
def pixelSize(self):
"""Return vector with the length and width of one view pixel in scene coordinates"""
p0 = Point(0,0)
@ -423,80 +368,7 @@ class GraphicsView(QtGui.QGraphicsView):
p11 = tr.map(p1)
return Point(p11 - p01)
#def writeSvg(self, fileName=None):
#if fileName is None:
#self.fileDialog = FileDialog()
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
#if GraphicsView.lastFileDir is not None:
#self.fileDialog.setDirectory(GraphicsView.lastFileDir)
#self.fileDialog.show()
#self.fileDialog.fileSelected.connect(self.writeSvg)
#return
#fileName = str(fileName)
#GraphicsView.lastFileDir = os.path.split(fileName)[0]
#self.svg = QtSvg.QSvgGenerator()
#self.svg.setFileName(fileName)
#self.svg.setSize(self.size())
#self.svg.setResolution(600)
#painter = QtGui.QPainter(self.svg)
#self.render(painter)
#def writeImage(self, fileName=None):
#if fileName is None:
#self.fileDialog = FileDialog()
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) ## this is the line that makes the fileDialog not show on mac
#if GraphicsView.lastFileDir is not None:
#self.fileDialog.setDirectory(GraphicsView.lastFileDir)
#self.fileDialog.show()
#self.fileDialog.fileSelected.connect(self.writeImage)
#return
#fileName = str(fileName)
#GraphicsView.lastFileDir = os.path.split(fileName)[0]
#self.png = QtGui.QImage(self.size(), QtGui.QImage.Format_ARGB32)
#painter = QtGui.QPainter(self.png)
#rh = self.renderHints()
#self.setRenderHints(QtGui.QPainter.Antialiasing)
#self.render(painter)
#self.setRenderHints(rh)
#self.png.save(fileName)
#def writePs(self, fileName=None):
#if fileName is None:
#self.fileDialog = FileDialog()
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
#self.fileDialog.show()
#self.fileDialog.fileSelected.connect(self.writePs)
#return
##if fileName is None:
## fileName = str(QtGui.QFileDialog.getSaveFileName())
#printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution)
#printer.setOutputFileName(fileName)
#painter = QtGui.QPainter(printer)
#self.render(painter)
#painter.end()
def dragEnterEvent(self, ev):
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events
#def getFreehandLine(self):
## Wait for click
#self.clearMouse()
#while self.lastButtonReleased != QtCore.Qt.LeftButton:
#QtGui.qApp.sendPostedEvents()
#QtGui.qApp.processEvents()
#time.sleep(0.01)
#fl = vstack(self.mouseTrail)
#return fl
#def getClick(self):
#fl = self.getFreehandLine()
#return fl[-1]

View File

@ -18,7 +18,7 @@ class HistogramLUTWidget(GraphicsView):
self.item = HistogramLUTItem(*args, **kargs)
self.setCentralItem(self.item)
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
self.setMinimumWidth(92)
self.setMinimumWidth(95)
def sizeHint(self):

96
widgets/LayoutWidget.py Normal file
View File

@ -0,0 +1,96 @@
from pyqtgraph.Qt import QtGui, QtCore
__all__ = ['LayoutWidget']
class LayoutWidget(QtGui.QWidget):
"""
Convenience class used for laying out QWidgets in a grid.
(It's just a little less effort to use than QGridLayout)
"""
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.layout = QtGui.QGridLayout()
self.setLayout(self.layout)
self.items = {}
self.rows = {}
self.currentRow = 0
self.currentCol = 0
def nextRow(self):
"""Advance to next row for automatic widget placement"""
self.currentRow += 1
self.currentCol = 0
def nextColumn(self, colspan=1):
"""Advance to next column, while returning the current column number
(generally only for internal use--called by addWidget)"""
self.currentCol += colspan
return self.currentCol-colspan
def nextCol(self, *args, **kargs):
"""Alias of nextColumn"""
return self.nextColumn(*args, **kargs)
def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs):
"""
Create a QLabel with *text* and place it in the next available cell (or in the cell specified)
All extra keyword arguments are passed to QLabel().
Returns the created widget.
"""
text = QtGui.QLabel(text, **kargs)
self.addItem(text, row, col, rowspan, colspan)
return text
def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
"""
Create an empty LayoutWidget and place it in the next available cell (or in the cell specified)
All extra keyword arguments are passed to :func:`LayoutWidget.__init__ <pyqtgraph.LayoutWidget.__init__>`
Returns the created widget.
"""
layout = LayoutWidget(**kargs)
self.addItem(layout, row, col, rowspan, colspan)
return layout
def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1):
"""
Add a widget to the layout and place it in the next available cell (or in the cell specified).
"""
if row is None:
row = self.currentRow
if col is None:
col = self.nextCol(colspan)
if row not in self.rows:
self.rows[row] = {}
self.rows[row][col] = item
self.items[item] = (row, col)
self.layout.addWidget(item, row, col, rowspan, colspan)
def getWidget(self, row, col):
"""Return the widget in (*row*, *col*)"""
return self.row[row][col]
#def itemIndex(self, item):
#for i in range(self.layout.count()):
#if self.layout.itemAt(i).graphicsItem() is item:
#return i
#raise Exception("Could not determine index of item " + str(item))
#def removeItem(self, item):
#"""Remove *item* from the layout."""
#ind = self.itemIndex(item)
#self.layout.removeAt(ind)
#self.scene().removeItem(item)
#r,c = self.items[item]
#del self.items[item]
#del self.rows[r][c]
#self.update()
#def clear(self):
#items = []
#for i in list(self.items.keys()):
#self.removeItem(i)

View File

@ -41,6 +41,8 @@ class PlotWidget(GraphicsView):
other methods, use :func:`getPlotItem <pyqtgraph.PlotWidget.getPlotItem>`.
"""
def __init__(self, parent=None, **kargs):
"""When initializing PlotWidget, all keyword arguments except *parent* are passed
to :func:`PlotItem.__init__() <pyqtgraph.PlotItem.__init__>`."""
GraphicsView.__init__(self, parent)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.enableMouse(False)

View File

@ -14,7 +14,7 @@ class ProgressDialog(QtGui.QProgressDialog):
if dlg.wasCanceled():
raise Exception("Processing canceled by user")
"""
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False):
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False):
"""
============== ================================================================
**Arguments:**
@ -25,15 +25,16 @@ class ProgressDialog(QtGui.QProgressDialog):
parent
wait Length of time (im ms) to wait before displaying dialog
busyCursor If True, show busy cursor until dialog finishes
disable If True, the progress dialog will not be displayed
and calls to wasCanceled() will always return False.
If ProgressDialog is entered from a non-gui thread, it will
always be disabled.
============== ================================================================
"""
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
if not isGuiThread:
self.disabled = True
self.disabled = disable or (not isGuiThread)
if self.disabled:
return
self.disabled = False
noCancel = False
if cancelText is None:

View File

@ -0,0 +1,172 @@
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.multiprocess as mp
import pyqtgraph as pg
from .GraphicsView import GraphicsView
import numpy as np
import mmap, tempfile, ctypes, atexit
__all__ = ['RemoteGraphicsView']
class RemoteGraphicsView(QtGui.QWidget):
"""
Replacement for GraphicsView that does all scene management and rendering on a remote process,
while displaying on the local widget.
GraphicsItems must be created by proxy to the remote process.
"""
def __init__(self, parent=None, *args, **kwds):
self._img = None
self._imgReq = None
QtGui.QWidget.__init__(self)
self._proc = mp.QtProcess()
self.pg = self._proc._import('pyqtgraph')
self.pg.setConfigOptions(self.pg.CONFIG_OPTIONS)
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
self._view = rpgRemote.Renderer(*args, **kwds)
self._view._setProxyOptions(deferGetattr=True)
self.setFocusPolicy(self._view.focusPolicy())
shmFileName = self._view.shmFileName()
self.shmFile = open(shmFileName, 'r')
self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ)
self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged))
for method in ['scene', 'setCentralItem']:
setattr(self, method, getattr(self._view, method))
def resizeEvent(self, ev):
ret = QtGui.QWidget.resizeEvent(self, ev)
self._view.resize(self.size(), _callSync='off')
return ret
def remoteSceneChanged(self, data):
w, h, size = data
if self.shm.size != size:
self.shm.close()
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
self.shm.seek(0)
self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32)
self.update()
def paintEvent(self, ev):
if self._img is None:
return
p = QtGui.QPainter(self)
p.drawImage(self.rect(), self._img, QtCore.QRect(0, 0, self._img.width(), self._img.height()))
p.end()
def mousePressEvent(self, ev):
self._view.mousePressEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
ev.accept()
return QtGui.QWidget.mousePressEvent(self, ev)
def mouseReleaseEvent(self, ev):
self._view.mouseReleaseEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
ev.accept()
return QtGui.QWidget.mouseReleaseEvent(self, ev)
def mouseMoveEvent(self, ev):
self._view.mouseMoveEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
ev.accept()
return QtGui.QWidget.mouseMoveEvent(self, ev)
def wheelEvent(self, ev):
self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), ev.orientation(), _callSync='off')
ev.accept()
return QtGui.QWidget.wheelEvent(self, ev)
def keyEvent(self, ev):
if self._view.keyEvent(ev.type(), int(ev.modifiers()), text, autorep, count):
ev.accept()
return QtGui.QWidget.keyEvent(self, ev)
class Renderer(GraphicsView):
sceneRendered = QtCore.Signal(object)
def __init__(self, *args, **kwds):
## Create shared memory for rendered image
#fd = os.open('/tmp/mmaptest', os.O_CREAT | os.O_TRUNC | os.O_RDWR)
#os.write(fd, '\x00' * mmap.PAGESIZE)
self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_')
self.shmFile.write('\x00' * mmap.PAGESIZE)
#fh.flush()
fd = self.shmFile.fileno()
self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE)
atexit.register(self.close)
GraphicsView.__init__(self, *args, **kwds)
self.scene().changed.connect(self.update)
self.img = None
self.renderTimer = QtCore.QTimer()
self.renderTimer.timeout.connect(self.renderView)
self.renderTimer.start(16)
def close(self):
self.shm.close()
self.shmFile.close()
def shmFileName(self):
return self.shmFile.name
def update(self):
self.img = None
return GraphicsView.update(self)
def resize(self, size):
oldSize = self.size()
GraphicsView.resize(self, size)
self.resizeEvent(QtGui.QResizeEvent(size, oldSize))
self.update()
def renderView(self):
if self.img is None:
## make sure shm is large enough and get its address
size = self.width() * self.height() * 4
if size > self.shm.size():
self.shm.resize(size)
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
## render the scene directly to shared memory
self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
self.img.fill(0xffffffff)
p = QtGui.QPainter(self.img)
self.render(p, self.viewRect(), self.rect())
p.end()
self.sceneRendered.emit((self.width(), self.height(), self.shm.size()))
def mousePressEvent(self, typ, pos, gpos, btn, btns, mods):
typ = QtCore.QEvent.Type(typ)
btns = QtCore.Qt.MouseButtons(btns)
mods = QtCore.Qt.KeyboardModifiers(mods)
return GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods))
def mouseMoveEvent(self, typ, pos, gpos, btn, btns, mods):
typ = QtCore.QEvent.Type(typ)
btns = QtCore.Qt.MouseButtons(btns)
mods = QtCore.Qt.KeyboardModifiers(mods)
return GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods))
def mouseReleaseEvent(self, typ, pos, gpos, btn, btns, mods):
typ = QtCore.QEvent.Type(typ)
btns = QtCore.Qt.MouseButtons(btns)
mods = QtCore.Qt.KeyboardModifiers(mods)
return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods))
def wheelEvent(self, pos, gpos, d, btns, mods, ori):
btns = QtCore.Qt.MouseButtons(btns)
mods = QtCore.Qt.KeyboardModifiers(mods)
return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori))
def keyEvent(self, typ, mods, text, autorep, count):
typ = QtCore.QEvent.Type(typ)
mods = QtCore.Qt.KeyboardModifiers(mods)
GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count))
return ev.accepted()

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.SignalProxy import SignalProxy
import pyqtgraph.functions as fn

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.python2_3 import asUnicode
import numpy as np
try: