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

30
Qt.py
View File

@ -1,15 +1,23 @@
## Do all Qt imports from here to allow easier PyQt / PySide compatibility ## Do all Qt imports from here to allow easier PyQt / PySide compatibility
#from PySide import QtGui, QtCore, QtOpenGL, QtSvg USE_PYSIDE = False ## If False, import PyQt4. If True, import PySide
from PyQt4 import QtGui, QtCore ## Note that when switching between PyQt and PySide, all template
try: ## files (*.ui) must be rebuilt for the target library.
from PyQt4 import QtSvg
except ImportError: if USE_PYSIDE:
pass from PySide import QtGui, QtCore, QtOpenGL, QtSvg
try: import PySide
from PyQt4 import QtOpenGL VERSION_INFO = 'PySide ' + PySide.__version__
except ImportError: else:
pass 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 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) m = pg.SRTTransform3D(m)
angle, axis = m.getRotation() angle, axis = m.getRotation()
if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1): 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.") raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.")
self._state = { self._state = {
'pos': Point(m.getTranslation()), 'pos': Point(m.getTranslation()),

View File

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

View File

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

View File

@ -1,18 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
REVISION = None
### import all the goodies and add some helper functions for easy CLI use ### 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 ## 'Qt' is a local module; it is intended mainly to cover up the differences
## between PyQt4 and PySide. ## 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) ## 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: #if QtGui.QApplication.instance() is None:
#app = QtGui.QApplication([]) #app = QtGui.QApplication([])
import sys import os, sys
## check python version ## 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])) 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 ## helpers for 2/3 compatibility
@ -30,13 +33,15 @@ else:
useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff.
CONFIG_OPTIONS = { 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 'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
'foregroundColor': (200,200,200), 'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
'backgroundColor': (0,0,0), 'background': (0, 0, 0), ## default background for GraphicsWidget
'antialias': False, 'antialias': False,
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
} }
def setConfigOption(opt, value): def setConfigOption(opt, value):
CONFIG_OPTIONS[opt] = value CONFIG_OPTIONS[opt] = value
@ -44,6 +49,23 @@ def getConfigOption(opt):
return CONFIG_OPTIONS[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 :) ## Rename orphaned .pyc files. This is *probably* safe :)
def renamePyc(startDir): def renamePyc(startDir):
@ -105,7 +127,7 @@ def importAll(path, excludes=()):
globals()[k] = getattr(mod, k) globals()[k] = getattr(mod, k)
importAll('graphicsItems') importAll('graphicsItems')
importAll('widgets', excludes=['MatplotlibWidget']) importAll('widgets', excludes=['MatplotlibWidget', 'RemoteGraphicsView'])
from .imageview import * from .imageview import *
from .WidgetGroup import * from .WidgetGroup import *

View File

@ -13,6 +13,7 @@ import re, os, sys
from collections import OrderedDict from collections import OrderedDict
GLOBAL_PATH = None # so not thread safe. GLOBAL_PATH = None # so not thread safe.
from . import units from . import units
from .python2_3 import asUnicode
class ParseError(Exception): class ParseError(Exception):
def __init__(self, message, lineNum, line, fileName=None): 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): class CmdInput(QtGui.QLineEdit):
@ -25,10 +26,10 @@ class CmdInput(QtGui.QLineEdit):
self.execCmd() self.execCmd()
else: else:
QtGui.QLineEdit.keyPressEvent(self, ev) QtGui.QLineEdit.keyPressEvent(self, ev)
self.history[0] = unicode(self.text()) self.history[0] = asUnicode(self.text())
def execCmd(self): def execCmd(self):
cmd = unicode(self.text()) cmd = asUnicode(self.text())
if len(self.history) == 1 or cmd != self.history[1]: if len(self.history) == 1 or cmd != self.history[1]:
self.history.insert(1, cmd) self.history.insert(1, cmd)
#self.lastCmd = cmd #self.lastCmd = cmd

View File

@ -1,13 +1,11 @@
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import sys, re, os, time, traceback import sys, re, os, time, traceback, subprocess
import pyqtgraph as pg import pyqtgraph as pg
import template from . import template
import pyqtgraph.exceptionHandling as exceptionHandling import pyqtgraph.exceptionHandling as exceptionHandling
import pickle import pickle
EDITOR = "pykate {fileName}:{lineNum}"
class ConsoleWidget(QtGui.QWidget): class ConsoleWidget(QtGui.QWidget):
""" """
Widget displaying console output and accepting command input. 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. be baffling and frustrating to users since it would appear the program has frozen.
- some terminals (eg windows cmd.exe) have notoriously unfriendly interfaces - some terminals (eg windows cmd.exe) have notoriously unfriendly interfaces
- ability to add extra features like exception stack introspection - 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): 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.historyList.itemDoubleClicked.connect(self.cmdDblClicked)
self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible) self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible)
self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllToggled) self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions)
self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextToggled) self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextException)
self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked) self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked)
self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked) self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked)
self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked) self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked)
@ -229,15 +227,25 @@ class ConsoleWidget(QtGui.QWidget):
def flush(self): def flush(self):
pass pass
def catchAllToggled(self, b): def catchAllExceptions(self, catch=True):
if b: """
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) self.ui.catchNextExceptionBtn.setChecked(False)
exceptionHandling.register(self.allExceptionsHandler) exceptionHandling.register(self.allExceptionsHandler)
else: else:
exceptionHandling.unregister(self.allExceptionsHandler) exceptionHandling.unregister(self.allExceptionsHandler)
def catchNextToggled(self, b): def catchNextException(self, catch=True):
if b: """
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) self.ui.catchAllExceptionsBtn.setChecked(False)
exceptionHandling.register(self.nextExceptionHandler) exceptionHandling.register(self.nextExceptionHandler)
else: else:
@ -254,11 +262,15 @@ class ConsoleWidget(QtGui.QWidget):
pass pass
def stackItemDblClicked(self, item): 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() tb = self.currentFrame()
lineNum = tb.tb_lineno lineNum = tb.tb_lineno
fileName = tb.tb_frame.f_code.co_filename 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): 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) QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, 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.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", 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)) 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.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)) 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> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Console</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<property name="margin"> <property name="margin">
@ -153,7 +153,7 @@
<customwidget> <customwidget>
<class>CmdInput</class> <class>CmdInput</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>CmdInput</header> <header>.CmdInput</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources/> <resources/>

View File

@ -2,6 +2,7 @@
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
from .Container import * from .Container import *
from .DockDrop import * from .DockDrop import *
from .Dock import Dock
import pyqtgraph.debug as debug import pyqtgraph.debug as debug
import weakref 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. # documentation root, use os.path.abspath to make it absolute, like shown here.
path = os.path.dirname(os.path.abspath(__file__)) path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(path, '..', '..', '..')) sys.path.insert(0, os.path.join(path, '..', '..', '..'))
print sys.path
# -- General configuration ----------------------------------------------------- # -- 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 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 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) * single-character string representing color (b, g, r, c, m, y, k, w)
* (r, g, b) or (r, g, b, a) tuple * (r, g, b) or (r, g, b, a) tuple
* single greyscale value (0.0 - 1.0) * 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 * QColor
* QPen / QBrush where appropriate * 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('y', width=3, style=QtCore.Qt.DashLine) ## Make a dashed yellow line 2px wide
mkPen(0.5) ## solid grey line 1px wide mkPen(0.5) ## solid grey line 1px wide
mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line
See the Qt documentation for 'QPen' and 'PenStyle' for more options. 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 mkColor(), intColor(), hsvColor(), or Qt's QColor class 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 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 from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg
import user import numpy as np
app = QtGui.QApplication([]) app = QtGui.QApplication([])
view = pg.GraphicsView() view = pg.GraphicsView()
l = pg.GraphicsLayout(border=pg.mkPen(0, 0, 255)) l = pg.GraphicsLayout(border=(100,100,100))
view.setCentralItem(l) view.setCentralItem(l)
view.show() 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) ## Add 3 plots into the first row (automatic position)
p1 = l.addPlot() p1 = l.addPlot(title="Plot 1")
p2 = l.addPlot() p2 = l.addPlot(title="Plot 2")
p3 = l.addPlot() 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() 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) ## Add 2 more plots into the third row (manual position)
p4 = l.addPlot(row=2, col=0) p4 = l.addPlot(row=3, col=1)
p5 = l.addPlot(row=2, col=1, colspan=2) p5 = l.addPlot(row=3, col=2, colspan=2)
## show some content in the plots
## show some content
p1.plot([1,3,2,4,3,5]) p1.plot([1,3,2,4,3,5])
p2.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]) p4.plot([1,3,2,4,3,5])
p5.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. ## 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'), ('Command-line usage', 'CLIexample.py'),
('Basic Plotting', 'Plotting.py'), ('Basic Plotting', 'Plotting.py'),
('ImageView', 'ImageView.py'), ('ImageView', 'ImageView.py'),
('ParameterTree', '../parametertree'), ('ParameterTree', 'parametertree.py'),
('Crosshair / Mouse interaction', 'crosshair.py'), ('Crosshair / Mouse interaction', 'crosshair.py'),
('Video speed test', 'VideoSpeedTest.py'), ('Video speed test', 'VideoSpeedTest.py'),
('Plot speed test', 'PlotSpeedTest.py'), ('Plot speed test', 'PlotSpeedTest.py'),
('Data Slicing', 'DataSlicing.py'), ('Data Slicing', 'DataSlicing.py'),
('Plot Customization', 'customPlot.py'),
('Dock widgets', 'dockarea.py'),
('Console', 'ConsoleWidget.py'),
('GraphicsItems', OrderedDict([ ('GraphicsItems', OrderedDict([
('Scatter Plot', 'ScatterPlot.py'), ('Scatter Plot', 'ScatterPlot.py'),
#('PlotItem', 'PlotItem.py'), #('PlotItem', 'PlotItem.py'),
@ -46,7 +49,7 @@ examples = OrderedDict([
#('VerticalLabel', '../widgets/VerticalLabel.py'), #('VerticalLabel', '../widgets/VerticalLabel.py'),
('JoystickButton', 'JoystickButton.py'), ('JoystickButton', 'JoystickButton.py'),
])), ])),
('GraphicsScene', 'GraphicsScene.py'), ('GraphicsScene', 'GraphicsScene.py'),
('Flowcharts', 'Flowchart.py'), ('Flowcharts', 'Flowchart.py'),
#('Canvas', '../canvas'), #('Canvas', '../canvas'),
@ -67,9 +70,9 @@ class ExampleLoader(QtGui.QMainWindow):
self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples) self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples)
self.ui.exampleTree.expandAll() self.ui.exampleTree.expandAll()
self.resize(900,500) self.resize(1000,500)
self.show() self.show()
self.ui.splitter.setSizes([150,750]) self.ui.splitter.setSizes([250,750])
self.ui.loadBtn.clicked.connect(self.loadFile) self.ui.loadBtn.clicked.connect(self.loadFile)
self.ui.exampleTree.currentItemChanged.connect(self.showFile) self.ui.exampleTree.currentItemChanged.connect(self.showFile)
self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile) 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 ## make this version of pyqtgraph importable before any others
import sys, os 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 -*- # -*- coding: utf-8 -*-
import initExample ## Add path to library (just for examples; you do not need this) import initExample ## Add path to library (just for examples; you do not need this)
import numpy as np import numpy as np
import pyqtgraph.multiprocess as mp import pyqtgraph.multiprocess as mp
from pyqtgraph.multiprocess.parallelizer import Parallelize #, Parallelizer import pyqtgraph as pg
import time 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): def __call__(self, *args):
## call original exception handler first (prints exception) ## call original exception handler first (prints exception)
global original_excepthook, callbacks, clear_tracebacks 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) ret = original_excepthook(*args)
for cb in callbacks: for cb in callbacks:
try: try:
cb(*args) cb(*args)
except: except:
print " --------------------------------------------------------------" print(" --------------------------------------------------------------")
print " Error occurred during exception callback", cb print(" Error occurred during exception callback %s" % str(cb))
print " --------------------------------------------------------------" print(" --------------------------------------------------------------")
traceback.print_exception(*sys.exc_info()) traceback.print_exception(*sys.exc_info())

View File

@ -71,7 +71,8 @@ class Flowchart(Node):
if terminals is None: if terminals is None:
terminals = {} terminals = {}
self.filePath = filePath 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.inputWasSet = False ## flag allows detection of changes in the absence of input change.
self._nodes = {} self._nodes = {}
@ -457,7 +458,7 @@ class Flowchart(Node):
state = Node.saveState(self) state = Node.saveState(self)
state['nodes'] = [] state['nodes'] = []
state['connects'] = [] state['connects'] = []
state['terminals'] = self.saveTerminals() #state['terminals'] = self.saveTerminals()
for name, node in self._nodes.items(): for name, node in self._nodes.items():
cls = type(node) cls = type(node)
@ -470,7 +471,7 @@ class Flowchart(Node):
conn = self.listConnections() conn = self.listConnections()
for a, b in conn: for a, b in conn:
state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name())) state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name()))
state['inputNode'] = self.inputNode.saveState() state['inputNode'] = self.inputNode.saveState()
state['outputNode'] = self.outputNode.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])) nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
for n in nodes: for n in nodes:
if n['name'] in self._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 continue
try: try:
node = self.createNode(n['class'], name=n['name']) node = self.createNode(n['class'], name=n['name'])
@ -498,7 +500,7 @@ class Flowchart(Node):
self.inputNode.restoreState(state.get('inputNode', {})) self.inputNode.restoreState(state.get('inputNode', {}))
self.outputNode.restoreState(state.get('outputNode', {})) self.outputNode.restoreState(state.get('outputNode', {}))
self.restoreTerminals(state['terminals']) #self.restoreTerminals(state['terminals'])
for n1, t1, n2, t2 in state['connects']: for n1, t1, n2, t2 in state['connects']:
try: try:
self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2]) self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2])

View File

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
#from PySide import QtCore, QtGui
from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
from .Terminal import * from .Terminal import *
from collections import OrderedDict from collections import OrderedDict
from pyqtgraph.debug import * from pyqtgraph.debug import *
import numpy as np import numpy as np
#from pyqtgraph.ObjectWorkaround import QObjectWorkaround
from .eq import * from .eq import *
#TETRACYCLINE = True
def strDict(d): def strDict(d):
return dict([(str(k), v) for k, v in d.items()]) 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.bypassButton = None ## this will be set by the flowchart ctrl widget..
self._graphicsItem = None self._graphicsItem = None
self.terminals = OrderedDict() self.terminals = OrderedDict()
self._inputs = {} self._inputs = OrderedDict()
self._outputs = {} self._outputs = OrderedDict()
self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals
self._allowAddOutput = allowAddOutput self._allowAddOutput = allowAddOutput
self._allowRemove = allowRemove self._allowRemove = allowRemove
@ -85,24 +82,16 @@ class Node(QtCore.QObject):
def terminalRenamed(self, term, oldName): def terminalRenamed(self, term, oldName):
"""Called after a terminal has been renamed""" """Called after a terminal has been renamed"""
newName = term.name() newName = term.name()
#print "node", self, "handling rename..", newName, oldName
for d in [self.terminals, self._inputs, self._outputs]: for d in [self.terminals, self._inputs, self._outputs]:
if oldName not in d: if oldName not in d:
continue continue
#print " got one"
d[newName] = d[oldName] d[newName] = d[oldName]
del d[oldName] del d[oldName]
self.graphicsItem().updateTerminals() self.graphicsItem().updateTerminals()
#self.emit(QtCore.SIGNAL('terminalRenamed'), term, oldName)
self.sigTerminalRenamed.emit(term, oldName) self.sigTerminalRenamed.emit(term, oldName)
def addTerminal(self, name, **opts): 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) name = self.nextTerminalName(name)
term = Terminal(self, name, **opts) term = Terminal(self, name, **opts)
self.terminals[name] = term self.terminals[name] = term
@ -278,12 +267,20 @@ class Node(QtCore.QObject):
def saveState(self): def saveState(self):
pos = self.graphicsItem().pos() 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): def restoreState(self, state):
pos = state.get('pos', (0,0)) pos = state.get('pos', (0,0))
self.graphicsItem().setPos(*pos) self.graphicsItem().setPos(*pos)
self.bypass(state.get('bypass', False)) self.bypass(state.get('bypass', False))
if 'terminals' in state:
self.restoreTerminals(state['terminals'])
def saveTerminals(self): def saveTerminals(self):
terms = OrderedDict() terms = OrderedDict()
@ -309,8 +306,8 @@ class Node(QtCore.QObject):
for t in self.terminals.values(): for t in self.terminals.values():
t.close() t.close()
self.terminals = OrderedDict() self.terminals = OrderedDict()
self._inputs = {} self._inputs = OrderedDict()
self._outputs = {} self._outputs = OrderedDict()
def close(self): def close(self):
"""Cleans up after the node--removes terminals, graphicsItem, widget""" """Cleans up after the node--removes terminals, graphicsItem, widget"""
@ -493,10 +490,6 @@ class NodeGraphicsItem(GraphicsObject):
self.hovered = False self.hovered = False
self.update() self.update()
#def mouseReleaseEvent(self, ev):
#ret = QtGui.QGraphicsItem.mouseReleaseEvent(self, ev)
#return ret
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
ev.accept() ev.accept()
@ -513,13 +506,8 @@ class NodeGraphicsItem(GraphicsObject):
return GraphicsObject.itemChange(self, change, val) return GraphicsObject.itemChange(self, change, val)
#def contextMenuEvent(self, ev):
#ev.accept()
#self.menu.popup(ev.screenPos())
def getMenu(self): def getMenu(self):
return self.menu return self.menu
def getContextMenus(self, event): def getContextMenus(self, event):
return [self.menu] return [self.menu]
@ -548,25 +536,3 @@ class NodeGraphicsItem(GraphicsObject):
def addOutputFromMenu(self): ## called when add output is clicked in context menu def addOutputFromMenu(self): ## called when add output is clicked in context menu
self.node.addOutput(renamable=True, removable=True, multiable=False) 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. self._value = {} ## dictionary of terminal:value pairs.
else: else:
self._value = None self._value = None
self.valueOk = None self.valueOk = None
self.recolor() self.recolor()
@ -70,6 +70,8 @@ class Terminal:
return return
self._value = val self._value = val
else: else:
if not isinstance(self._value, dict):
self._value = {}
if val is not None: if val is not None:
self._value.update(val) self._value.update(val)
@ -132,9 +134,14 @@ class Terminal:
def isMultiValue(self): def isMultiValue(self):
return self._multi return self._multi
def setMultiValue(self, b): def setMultiValue(self, multi):
"""Set whether this is a multi-value terminal.""" """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): def isOutput(self):
return self._io == 'out' return self._io == 'out'
@ -407,6 +414,8 @@ class TerminalGraphicsItem(GraphicsObject):
multiAct = QtGui.QAction("Multi-value", self.menu) multiAct = QtGui.QAction("Multi-value", self.menu)
multiAct.setCheckable(True) multiAct.setCheckable(True)
multiAct.setChecked(self.term.isMultiValue()) multiAct.setChecked(self.term.isMultiValue())
multiAct.setEnabled(self.term.isMultiable())
multiAct.triggered.connect(self.toggleMulti) multiAct.triggered.connect(self.toggleMulti)
self.menu.addAction(multiAct) self.menu.addAction(multiAct)
self.menu.multiAct = multiAct self.menu.multiAct = multiAct

View File

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

View File

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

View File

@ -5,6 +5,7 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation. Distributed under MIT/X11 license. See license.txt for more infomation.
""" """
from .python2_3 import asUnicode
Colors = { Colors = {
'b': (0,0,255,255), 'b': (0,0,255,255),
'g': (0,255,0,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. 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: 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 Note the following must be true:
| *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
* each vector must have the same length as *axes* | len(shape) == len(vectors)
* If the vectors are not unit length, the result will be scaled. | len(origin) == len(axes) == len(vectors[i])
* 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
Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes 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)) 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 # sanity check
@ -436,7 +441,7 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
for inds in np.ndindex(*extraShape): for inds in np.ndindex(*extraShape):
ind = (Ellipsis,) + inds ind = (Ellipsis,) + inds
#print data[ind].shape, x.shape, output[ind].shape, output.shape #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)) tr = list(range(output.ndim))
trb = [] trb = []
@ -447,9 +452,18 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
tr2 = tuple(trb+tr) tr2 = tuple(trb+tr)
## Untranspose array before returning ## 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): def solve3DTransform(points1, points2):
""" """
Find a 3D transformation matrix that maps points1 onto points2 Find a 3D transformation matrix that maps points1 onto points2
@ -1275,3 +1289,19 @@ def isosurface(data, level):
return facets 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.Qt import QtGui, QtCore
from pyqtgraph.python2_3 import asUnicode
import numpy as np import numpy as np
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
import pyqtgraph.debug as debug import pyqtgraph.debug as debug
import weakref import weakref
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import pyqtgraph as pg
from .GraphicsWidget import GraphicsWidget from .GraphicsWidget import GraphicsWidget
__all__ = ['AxisItem'] __all__ = ['AxisItem']
@ -65,8 +67,6 @@ class AxisItem(GraphicsWidget):
self.setRange(0, 1) self.setRange(0, 1)
if pen is None:
pen = QtGui.QPen(QtGui.QColor(100, 100, 100))
self.setPen(pen) self.setPen(pen)
self._linkedView = None self._linkedView = None
@ -189,23 +189,31 @@ class AxisItem(GraphicsWidget):
self.setMaximumWidth(w) self.setMaximumWidth(w)
self.setMinimumWidth(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): 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.picture = None
self.update() self.update()
def setScale(self, scale=None): def setScale(self, scale=None):
""" """
Set the value scaling for this axis. Set the value scaling for this axis. Values on the axis are multiplied
The scaling value 1) multiplies the values displayed along the axis by this scale factor before being displayed as text. By default,
and 2) changes the way units are displayed in the label. 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 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 to 'V' then a scale of 1000 would cause the axis to display values -100 to 100
and the units would appear as 'mV' 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 scale is None:
#if self.drawLabel: ## If there is a label, then we are free to rescale the values #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) self.setLabel(unitPrefix=prefix)
else: else:
scale = 1.0 scale = 1.0
else:
self.setLabel(unitPrefix='')
self.autoScale = False
if scale != self.scale: if scale != self.scale:
self.scale = scale self.scale = scale
self.setLabel() self.setLabel()
@ -354,6 +364,29 @@ class AxisItem(GraphicsWidget):
(intervals[minorIndex], 0) (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): def tickValues(self, minVal, maxVal, size):
""" """
@ -370,8 +403,6 @@ class AxisItem(GraphicsWidget):
""" """
minVal, maxVal = sorted((minVal, maxVal)) minVal, maxVal = sorted((minVal, maxVal))
if self.logMode:
return self.logTickValues(minVal, maxVal, size)
ticks = [] ticks = []
tickLevels = self.tickSpacing(minVal, maxVal, size) tickLevels = self.tickSpacing(minVal, maxVal, size)
@ -388,21 +419,36 @@ class AxisItem(GraphicsWidget):
## remove any ticks that were present in higher levels ## 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 ## 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. ## 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]) allValues = np.concatenate([allValues, values])
ticks.append((spacing, values)) ticks.append((spacing, values))
if self.logMode:
return self.logTickValues(minVal, maxVal, size, ticks)
return ticks return ticks
def logTickValues(self, minVal, maxVal, size): def logTickValues(self, minVal, maxVal, size, stdTicks):
v1 = int(np.floor(minVal))
v2 = int(np.ceil(maxVal))
major = list(range(v1+1, v2))
minor = [] ## start with the tick spacing given by tickValues().
for v in range(v1, v2): ## Any level whose spacing is < 1 needs to be converted to log scale
minor.extend(v + np.log10(np.arange(1, 10)))
minor = [x for x in minor if x>minVal and x<maxVal] ticks = []
return [(1.0, major), (None, minor)] 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): def tickStrings(self, values, scale, spacing):
"""Return the strings that should be placed next to ticks. This method is called """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 #print tickStart, tickStop, span
## draw long line along axis ## draw long line along axis
p.setPen(self.pen) p.setPen(self.pen())
p.drawLine(*span) p.drawLine(*span)
p.translate(0.5,0) ## resolves some damn pixel ambiguity p.translate(0.5,0) ## resolves some damn pixel ambiguity
## determine size of this item in pixels ## determine size of this item in pixels
points = list(map(self.mapToDevice, span)) points = list(map(self.mapToDevice, span))
if None in points:
return
lengthInPixels = Point(points[1] - points[0]).length() lengthInPixels = Point(points[1] - points[0]).length()
if lengthInPixels == 0: if lengthInPixels == 0:
return return
@ -513,6 +561,10 @@ class AxisItem(GraphicsWidget):
else: else:
xScale = bounds.width() / dif xScale = bounds.width() / dif
offset = self.range[0] * xScale offset = self.range[0] * xScale
xRange = [x * xScale - offset for x in self.range]
xMin = min(xRange)
xMax = max(xRange)
prof.mark('init') prof.mark('init')
@ -521,6 +573,7 @@ class AxisItem(GraphicsWidget):
## draw ticks ## draw ticks
## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
## draw three different intervals, long ticks first ## draw three different intervals, long ticks first
for i in range(len(tickLevels)): for i in range(len(tickLevels)):
tickPositions.append([]) tickPositions.append([])
ticks = tickLevels[i][1] ticks = tickLevels[i][1]
@ -530,19 +583,28 @@ class AxisItem(GraphicsWidget):
lineAlpha = 255 / (i+1) lineAlpha = 255 / (i+1)
if self.grid is not False: 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: for v in ticks:
## determine actual position to draw this tick
x = (v * xScale) - offset 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] p1 = [x, x]
p2 = [x, x] p2 = [x, x]
p1[axis] = tickStart p1[axis] = tickStart
p2[axis] = tickStop p2[axis] = tickStop
if self.grid is False: if self.grid is False:
p2[axis] += tickLength*tickDir 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)) p.drawLine(Point(p1), Point(p2))
tickPositions[i].append(x)
prof.mark('draw ticks') prof.mark('draw ticks')
## Draw text until there is no more room (or no more text) ## Draw text until there is no more room (or no more text)
@ -557,10 +619,15 @@ class AxisItem(GraphicsWidget):
if len(strings) == 0: if len(strings) == 0:
continue 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 if i > 0: ## always draw top level
## measure all text, make sure there's enough room ## 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: if axis == 0:
textSize = np.sum([r.height() for r in textRects]) textSize = np.sum([r.height() for r in textRects])
else: else:
@ -570,11 +637,12 @@ class AxisItem(GraphicsWidget):
textFillRatio = float(textSize) / lengthInPixels textFillRatio = float(textSize) / lengthInPixels
if textFillRatio > 0.7: if textFillRatio > 0.7:
break break
#spacing, values = tickLevels[best] #spacing, values = tickLevels[best]
#strings = self.tickStrings(values, self.scale, spacing) #strings = self.tickStrings(values, self.scale, spacing)
for j in range(len(strings)): for j in range(len(strings)):
vstr = strings[j] vstr = strings[j]
if vstr is None:## this tick was ignored because it is out of bounds
continue
x = tickPositions[i][j] x = tickPositions[i][j]
textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
height = textRect.height() height = textRect.height()
@ -592,7 +660,7 @@ class AxisItem(GraphicsWidget):
textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) 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) p.drawText(rect, textFlags, vstr)
prof.mark('draw text') prof.mark('draw text')
prof.finish() prof.finish()

View File

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

View File

@ -1,6 +1,7 @@
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.GraphicsScene import GraphicsScene
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
import weakref import weakref
class GraphicsItem(object): class GraphicsItem(object):
@ -149,16 +150,32 @@ class GraphicsItem(object):
"""Return vectors in local coordinates representing the width and height of a view pixel. """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. 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() dt = self.deviceTransform()
if dt is None: if dt is None:
return None, None return None, None
if direction is 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))) 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 orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
try: try:
@ -168,7 +185,7 @@ class GraphicsItem(object):
raise Exception("Invalid direction %s" %direction) 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))) return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
#vt = self.deviceTransform() #vt = self.deviceTransform()
@ -194,23 +211,26 @@ class GraphicsItem(object):
def pixelSize(self): def pixelSize(self):
## deprecated
v = self.pixelVectors() v = self.pixelVectors()
if v == (None, None): if v == (None, None):
return 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 return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5
def pixelWidth(self): def pixelWidth(self):
## deprecated
vt = self.deviceTransform() vt = self.deviceTransform()
if vt is None: if vt is None:
return 0 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() return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length()
def pixelHeight(self): def pixelHeight(self):
## deprecated
vt = self.deviceTransform() vt = self.deviceTransform()
if vt is None: if vt is None:
return 0 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() 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() vt = self.deviceTransform()
if vt is None: if vt is None:
return None return None
vt = vt.inverted()[0] vt = fn.invertQTransform(vt)
return vt.map(obj) return vt.map(obj)
def mapRectToDevice(self, rect): def mapRectToDevice(self, rect):
@ -253,7 +273,7 @@ class GraphicsItem(object):
vt = self.deviceTransform() vt = self.deviceTransform()
if vt is None: if vt is None:
return None return None
vt = vt.inverted()[0] vt = fn.invertQTransform(vt)
return vt.mapRect(rect) return vt.mapRect(rect)
def mapToView(self, obj): def mapToView(self, obj):
@ -272,14 +292,14 @@ class GraphicsItem(object):
vt = self.viewTransform() vt = self.viewTransform()
if vt is None: if vt is None:
return None return None
vt = vt.inverted()[0] vt = fn.invertQTransform(vt)
return vt.map(obj) return vt.map(obj)
def mapRectFromView(self, obj): def mapRectFromView(self, obj):
vt = self.viewTransform() vt = self.viewTransform()
if vt is None: if vt is None:
return None return None
vt = vt.inverted()[0] vt = fn.invertQTransform(vt)
return vt.mapRect(obj) return vt.mapRect(obj)
def pos(self): def pos(self):

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import pyqtgraph as pg
from .GraphicsWidget import GraphicsWidget from .GraphicsWidget import GraphicsWidget
@ -18,14 +19,13 @@ class LabelItem(GraphicsWidget):
GraphicsWidget.__init__(self, parent) GraphicsWidget.__init__(self, parent)
self.item = QtGui.QGraphicsTextItem(self) self.item = QtGui.QGraphicsTextItem(self)
self.opts = { self.opts = {
'color': 'CCC', 'color': None,
'justify': 'center' 'justify': 'center'
} }
self.opts.update(args) self.opts.update(args)
self.sizeHint = {} self._sizeHint = {}
self.setText(text) self.setText(text)
self.setAngle(angle) self.setAngle(angle)
def setAttr(self, attr, value): def setAttr(self, attr, value):
"""Set default text properties. See setText() for accepted parameters.""" """Set default text properties. See setText() for accepted parameters."""
@ -44,15 +44,17 @@ class LabelItem(GraphicsWidget):
==================== ============================== ==================== ==============================
""" """
self.text = text self.text = text
opts = self.opts.copy() opts = self.opts
for k in args: for k in args:
opts[k] = args[k] opts[k] = args[k]
optlist = [] optlist = []
if 'color' in opts:
if isinstance(opts['color'], QtGui.QColor): color = self.opts['color']
opts['color'] = fn.colorStr(opts['color'])[:6] if color is None:
optlist.append('color: #' + opts['color']) color = pg.getConfigOption('foreground')
color = fn.mkColor(color)
optlist.append('color: #' + fn.colorStr(color)[:6])
if 'size' in opts: if 'size' in opts:
optlist.append('font-size: ' + opts['size']) optlist.append('font-size: ' + opts['size'])
if 'bold' in opts and opts['bold'] in [True, False]: if 'bold' in opts and opts['bold'] in [True, False]:
@ -64,7 +66,7 @@ class LabelItem(GraphicsWidget):
self.item.setHtml(full) self.item.setHtml(full)
self.updateMin() self.updateMin()
self.resizeEvent(None) self.resizeEvent(None)
self.update() self.updateGeometry()
def resizeEvent(self, ev): def resizeEvent(self, ev):
#c1 = self.boundingRect().center() #c1 = self.boundingRect().center()
@ -72,16 +74,35 @@ class LabelItem(GraphicsWidget):
#dif = c1 - c2 #dif = c1 - c2
#self.item.moveBy(dif.x(), dif.y()) #self.item.moveBy(dif.x(), dif.y())
#print c1, c2, dif, self.item.pos() #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': 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': elif self.opts['justify'] == 'center':
bounds = self.item.mapRectToParent(self.item.boundingRect()) bounds.moveCenter(rect.center())
self.item.setPos(self.width()/2. - bounds.width()/2., 0) #bounds = self.itemRect()
#self.item.setPos(self.width()/2. - bounds.width()/2., 0)
elif self.opts['justify'] == 'right': elif self.opts['justify'] == 'right':
bounds = self.item.mapRectToParent(self.item.boundingRect()) if left.x() != 0:
self.item.setPos(self.width() - bounds.width(), 0) bounds.moveRight(rect.right())
#if self.width() > 0: if left.y() < 0:
#self.item.setTextWidth(self.width()) 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): def setAngle(self, angle):
self.angle = angle self.angle = angle
@ -89,27 +110,31 @@ class LabelItem(GraphicsWidget):
self.item.rotate(angle) self.item.rotate(angle)
self.updateMin() self.updateMin()
def updateMin(self): def updateMin(self):
bounds = self.item.mapRectToParent(self.item.boundingRect()) bounds = self.itemRect()
self.setMinimumWidth(bounds.width()) self.setMinimumWidth(bounds.width())
self.setMinimumHeight(bounds.height()) self.setMinimumHeight(bounds.height())
self.sizeHint = { self._sizeHint = {
QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()),
QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()),
QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2), QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2),
QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this?
} }
self.updateGeometry()
self.update()
def sizeHint(self, hint, constraint): 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(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): #def paint(self, p, *args):
#p.setPen(fn.mkPen('r')) #p.setPen(fn.mkPen('r'))
#p.drawRect(self.rect()) #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 .. LabelItem import LabelItem
from .. GraphicsWidget import GraphicsWidget from .. GraphicsWidget import GraphicsWidget
from .. ButtonItem import ButtonItem from .. ButtonItem import ButtonItem
#from .. GraphicsLayout import GraphicsLayout
from pyqtgraph.WidgetGroup import WidgetGroup from pyqtgraph.WidgetGroup import WidgetGroup
import collections import collections
__all__ = ['PlotItem'] __all__ = ['PlotItem']
#try:
#from WidgetGroup import *
#HAVE_WIDGETGROUP = True
#except:
#HAVE_WIDGETGROUP = False
try: try:
from metaarray import * from metaarray import *
HAVE_METAARRAY = True HAVE_METAARRAY = True
@ -78,6 +73,7 @@ class PlotItem(GraphicsWidget):
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`, :func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`, :func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`, :func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
:func:`invertY <pyqtgraph.ViewBox.invertY>`,
:func:`register <pyqtgraph.ViewBox.register>`, :func:`register <pyqtgraph.ViewBox.register>`,
:func:`unregister <pyqtgraph.ViewBox.unregister>` :func:`unregister <pyqtgraph.ViewBox.unregister>`
@ -99,26 +95,28 @@ class PlotItem(GraphicsWidget):
lastFileDir = None lastFileDir = None
managers = {} 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. Create a new PlotItem. All arguments are optional.
Any extra keyword arguments are passed to PlotItem.plot(). Any extra keyword arguments are passed to PlotItem.plot().
============= ========================================================================================== ============== ==========================================================================================
**Arguments** **Arguments**
*title* Title to display at the top of the item. Html is allowed. *title* Title to display at the top of the item. Html is allowed.
*labels* A dictionary specifying the axis labels to display:: *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 The name of each axis and the corresponding arguments are passed to
:func:`PlotItem.setLabel() <pyqtgraph.PlotItem.setLabel>` :func:`PlotItem.setLabel() <pyqtgraph.PlotItem.setLabel>`
Optionally, PlotItem my also be initialized with the keyword arguments left, Optionally, PlotItem my also be initialized with the keyword arguments left,
right, top, or bottom to achieve the same effect. right, top, or bottom to achieve the same effect.
*name* Registers a name for this view so that others may link to it *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) GraphicsWidget.__init__(self, parent)
@ -127,8 +125,6 @@ class PlotItem(GraphicsWidget):
## Set up control buttons ## Set up control buttons
path = os.path.dirname(__file__) 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.autoImageFile = os.path.join(path, 'auto.png')
self.lockImageFile = os.path.join(path, 'lock.png') self.lockImageFile = os.path.join(path, 'lock.png')
self.autoBtn = ButtonItem(self.autoImageFile, 14, self) self.autoBtn = ButtonItem(self.autoImageFile, 14, self)
@ -141,32 +137,33 @@ class PlotItem(GraphicsWidget):
self.layout.setHorizontalSpacing(0) self.layout.setHorizontalSpacing(0)
self.layout.setVerticalSpacing(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.sigRangeChanged.connect(self.sigRangeChanged)
self.vb.sigXRangeChanged.connect(self.sigXRangeChanged) self.vb.sigXRangeChanged.connect(self.sigXRangeChanged)
self.vb.sigYRangeChanged.connect(self.sigYRangeChanged) 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.layout.addItem(self.vb, 2, 1)
self.alpha = 1.0 self.alpha = 1.0
self.autoAlpha = True self.autoAlpha = True
self.spectrumMode = False self.spectrumMode = False
#self.autoScale = [True, True] ## Create and place axis items
if axisItems is None:
## Create and place scale items axisItems = {}
self.scales = { self.axes = {}
'top': {'item': AxisItem(orientation='top', linkView=self.vb), 'pos': (1, 1)}, for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
'bottom': {'item': AxisItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)}, axis = axisItems.get(k, AxisItem(orientation=k))
'left': {'item': AxisItem(orientation='left', linkView=self.vb), 'pos': (2, 0)}, axis.linkToView(self.vb)
'right': {'item': AxisItem(orientation='right', linkView=self.vb), 'pos': (2, 2)} self.axes[k] = {'item': axis, 'pos': pos}
} self.layout.addItem(axis, *pos)
for k in self.scales: axis.setZValue(-1000)
item = self.scales[k]['item'] axis.setFlag(axis.ItemNegativeZStacksBehindParent)
self.layout.addItem(item, *self.scales[k]['pos'])
item.setZValue(-1000)
item.setFlag(item.ItemNegativeZStacksBehindParent)
self.titleLabel = LabelItem('', size='11pt') self.titleLabel = LabelItem('', size='11pt')
self.layout.addItem(self.titleLabel, 0, 1) self.layout.addItem(self.titleLabel, 0, 1)
@ -192,8 +189,7 @@ class PlotItem(GraphicsWidget):
for m in [ for m in [
'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible',
'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled',
'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY',
'setMenuEnabled', 'menuEnabled',
'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well.
setattr(self, m, getattr(self.vb, m)) setattr(self, m, getattr(self.vb, m))
@ -233,45 +229,12 @@ class PlotItem(GraphicsWidget):
self.subMenus.append(sm) self.subMenus.append(sm)
self.ctrlMenu.addMenu(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() self.stateGroup = WidgetGroup()
for name, w in menuItems: for name, w in menuItems:
self.stateGroup.autoAdd(w) self.stateGroup.autoAdd(w)
self.fileDialog = None 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.alphaGroup.toggled.connect(self.updateAlpha)
c.alphaSlider.valueChanged.connect(self.updateAlpha) c.alphaSlider.valueChanged.connect(self.updateAlpha)
c.autoAlphaCheck.toggled.connect(self.updateAlpha) c.autoAlphaCheck.toggled.connect(self.updateAlpha)
@ -283,13 +246,6 @@ class PlotItem(GraphicsWidget):
c.fftCheck.toggled.connect(self.updateSpectrumMode) c.fftCheck.toggled.connect(self.updateSpectrumMode)
c.logXCheck.toggled.connect(self.updateLogMode) c.logXCheck.toggled.connect(self.updateLogMode)
c.logYCheck.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) c.downsampleSpin.valueChanged.connect(self.updateDownsampling)
@ -298,24 +254,15 @@ class PlotItem(GraphicsWidget):
self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation)
self.ctrl.maxTracesSpin.valueChanged.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('right')
self.hideAxis('top') self.hideAxis('top')
self.showAxis('left') self.showAxis('left')
self.showAxis('bottom') self.showAxis('bottom')
#if name is not None:
#self.registerPlot(name)
if labels is None: if labels is None:
labels = {} labels = {}
for label in list(self.scales.keys()): for label in list(self.axes.keys()):
if label in kargs: if label in kargs:
labels[label] = kargs[label] labels[label] = kargs[label]
del kargs[label] del kargs[label]
@ -330,15 +277,16 @@ class PlotItem(GraphicsWidget):
if len(kargs) > 0: if len(kargs) > 0:
self.plot(**kargs) self.plot(**kargs)
#self.enableAutoRange()
def implements(self, interface=None): def implements(self, interface=None):
return interface in ['ViewBoxWrapper'] return interface in ['ViewBoxWrapper']
def getViewBox(self): def getViewBox(self):
"""Return the ViewBox within.""" """Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within."""
return self.vb return self.vb
def setLogMode(self, x, y): def setLogMode(self, x, y):
""" """
Set log scaling for x and y axes. Set log scaling for x and y axes.
@ -399,11 +347,11 @@ class PlotItem(GraphicsWidget):
#self.autoBtn.setParent(None) #self.autoBtn.setParent(None)
#self.autoBtn = None #self.autoBtn = None
for k in self.scales: for k in self.axes:
i = self.scales[k]['item'] i = self.axes[k]['item']
i.close() i.close()
self.scales = None self.axes = None
self.scene().removeItem(self.vb) self.scene().removeItem(self.vb)
self.vb = None self.vb = None
@ -431,47 +379,6 @@ class PlotItem(GraphicsWidget):
def registerPlot(self, name): ## for backward compatibility def registerPlot(self, name): ## for backward compatibility
self.vb.register(name) 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): def updateGrid(self, *args):
alpha = self.ctrl.gridAlphaSlider.value() alpha = self.ctrl.gridAlphaSlider.value()
@ -492,91 +399,6 @@ class PlotItem(GraphicsWidget):
return wr 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): def avgToggled(self, b):
if b: if b:
self.recomputeAverages() self.recomputeAverages()
@ -650,50 +472,6 @@ class PlotItem(GraphicsWidget):
else: else:
plot.setData(x, y) 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): def autoBtnClicked(self):
if self.autoBtn.mode == 'auto': if self.autoBtn.mode == 'auto':
self.enableAutoRange() self.enableAutoRange()
@ -706,72 +484,6 @@ class PlotItem(GraphicsWidget):
""" """
print("Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead.") print("Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead.")
self.vb.enableAutoRange(self.vb.XYAxes) 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): 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) clear = kargs.get('clear', False)
params = kargs.get('params', None) params = kargs.get('params', None)
@ -888,23 +589,7 @@ class PlotItem(GraphicsWidget):
if params is None: if params is None:
params = {} 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) self.addItem(item, params=params)
#if pen is not None:
#curve.setPen(fn.mkPen(pen))
return item return item
@ -922,80 +607,34 @@ class PlotItem(GraphicsWidget):
del kargs['size'] del kargs['size']
return self.plot(*args, **kargs) 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): def replot(self):
#self.plotChanged()
self.update() self.update()
def updateParamList(self): def updateParamList(self):
self.ctrl.avgParamList.clear() self.ctrl.avgParamList.clear()
## Check to see that each parameter for each curve is present in the list ## 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: for c in self.curves:
#print " curve:", c
for p in list(self.itemMeta.get(c, {}).keys()): for p in list(self.itemMeta.get(c, {}).keys()):
#print " param:", p
if type(p) is tuple: if type(p) is tuple:
p = '.'.join(p) p = '.'.join(p)
## If the parameter is not in the list, add it. ## If the parameter is not in the list, add it.
matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly)
#print " matches:", matches
if len(matches) == 0: if len(matches) == 0:
i = QtGui.QListWidgetItem(p) i = QtGui.QListWidgetItem(p)
if p in self.paramList and self.paramList[p] is True: if p in self.paramList and self.paramList[p] is True:
#print " set checked"
i.setCheckState(QtCore.Qt.Checked) i.setCheckState(QtCore.Qt.Checked)
else: else:
#print " set unchecked"
i.setCheckState(QtCore.Qt.Unchecked) i.setCheckState(QtCore.Qt.Unchecked)
self.ctrl.avgParamList.addItem(i) self.ctrl.avgParamList.addItem(i)
else: else:
i = matches[0] i = matches[0]
self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) 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): def writeSvgCurves(self, fileName=None):
if fileName is None: if fileName is None:
self.fileDialog = FileDialog() self.fileDialog = FileDialog()
@ -1190,18 +829,12 @@ class PlotItem(GraphicsWidget):
def saveState(self): def saveState(self):
#if not HAVE_WIDGETGROUP:
#raise Exception("State save/restore requires WidgetGroup class.")
state = self.stateGroup.state() state = self.stateGroup.state()
state['paramList'] = self.paramList.copy() state['paramList'] = self.paramList.copy()
state['view'] = self.vb.getState() 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 return state
def restoreState(self, state): def restoreState(self, state):
#if not HAVE_WIDGETGROUP:
#raise Exception("State save/restore requires WidgetGroup class.")
if 'paramList' in state: if 'paramList' in state:
self.paramList = state['paramList'].copy() self.paramList = state['paramList'].copy()
@ -1218,8 +851,6 @@ class PlotItem(GraphicsWidget):
state['yGridCheck'] = state['gridGroup'] state['yGridCheck'] = state['gridGroup']
self.stateGroup.setState(state) self.stateGroup.setState(state)
#self.updateXScale()
#self.updateYScale()
self.updateParamList() self.updateParamList()
if 'view' not in state: if 'view' not in state:
@ -1232,13 +863,6 @@ class PlotItem(GraphicsWidget):
} }
self.vb.setState(state['view']) 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): def widgetGroupInterface(self):
return (None, PlotItem.saveState, PlotItem.restoreState) return (None, PlotItem.saveState, PlotItem.restoreState)
@ -1269,8 +893,6 @@ class PlotItem(GraphicsWidget):
for c in self.curves: for c in self.curves:
c.setDownsampling(ds) c.setDownsampling(ds)
self.recomputeAverages() self.recomputeAverages()
#for c in self.avgCurves.values():
#c[1].setDownsampling(ds)
def downsampleMode(self): def downsampleMode(self):
@ -1306,8 +928,6 @@ class PlotItem(GraphicsWidget):
(alpha, auto) = self.alphaState() (alpha, auto) = self.alphaState()
for c in self.curves: for c in self.curves:
c.setAlpha(alpha**2, auto) c.setAlpha(alpha**2, auto)
#self.replot(autoRange=False)
def alphaState(self): def alphaState(self):
enabled = self.ctrl.alphaGroup.isChecked() enabled = self.ctrl.alphaGroup.isChecked()
@ -1330,9 +950,6 @@ class PlotItem(GraphicsWidget):
mode = False mode = False
return mode return mode
#def wheelEvent(self, ev):
## disables default panning the whole scene by mousewheel
#ev.accept()
def resizeEvent(self, ev): def resizeEvent(self, ev):
if self.autoBtn is None: ## already closed down if self.autoBtn is None: ## already closed down
@ -1340,29 +957,42 @@ class PlotItem(GraphicsWidget):
btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect()) btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect())
y = self.size().height() - btnRect.height() y = self.size().height() - btnRect.height()
self.autoBtn.setPos(0, y) 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): def getMenu(self):
return self.ctrlMenu return self.ctrlMenu
def getContextMenus(self, event): def getContextMenus(self, event):
## called when another item is displaying its context menu; we get to add extras to the end of the menu. ## 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): def getLabel(self, key):
pass pass
def _checkScaleKey(self, key): def _checkScaleKey(self, key):
if key not in self.scales: if key not in self.axes:
raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.scales.keys())))) raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.axes.keys()))))
def getScale(self, key): def getScale(self, key):
return self.getAxis(key) return self.getAxis(key)
@ -1371,7 +1001,7 @@ class PlotItem(GraphicsWidget):
"""Return the specified AxisItem. """Return the specified AxisItem.
*name* should be 'left', 'bottom', 'top', or 'right'.""" *name* should be 'left', 'bottom', 'top', or 'right'."""
self._checkScaleKey(name) self._checkScaleKey(name)
return self.scales[name]['item'] return self.axes[name]['item']
def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args): 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' axis must be one of 'left', 'bottom', 'right', or 'top'
""" """
s = self.getScale(axis) s = self.getScale(axis)
p = self.scales[axis]['pos'] p = self.axes[axis]['pos']
if show: if show:
s.show() s.show()
else: else:
s.hide() s.hide()
def hideAxis(self, axis): def hideAxis(self, axis):
"""Hide one of the PlotItem's axes. ('left', 'bottom', 'right', or 'top')"""
self.showAxis(axis, False) self.showAxis(axis, False)
def showScale(self, *args, **kargs): def showScale(self, *args, **kargs):
@ -1431,6 +1062,7 @@ class PlotItem(GraphicsWidget):
return self.showAxis(*args, **kargs) return self.showAxis(*args, **kargs)
def hideButtons(self): def hideButtons(self):
"""Causes auto-scale button ('A' in lower-left corner) to be hidden for this PlotItem"""
#self.ctrlBtn.hide() #self.ctrlBtn.hide()
self.autoBtn.hide() self.autoBtn.hide()
@ -1454,7 +1086,6 @@ class PlotItem(GraphicsWidget):
## create curve ## create curve
try: try:
xv = arr.xvals(0) xv = arr.xvals(0)
#print 'xvals:', xv
except: except:
if x is None: if x is None:
xv = np.arange(arr.shape[0]) xv = np.arange(arr.shape[0])
@ -1474,17 +1105,6 @@ class PlotItem(GraphicsWidget):
return c 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): def setExportMode(self, export, opts):
if export: if export:
@ -1492,63 +1112,3 @@ class PlotItem(GraphicsWidget):
else: else:
self.autoBtn.show() 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.gridLayout_2.addWidget(self.yGridCheck, 1, 0, 1, 2)
self.gridAlphaSlider = QtGui.QSlider(self.gridGroup) self.gridAlphaSlider = QtGui.QSlider(self.gridGroup)
self.gridAlphaSlider.setMaximum(255) self.gridAlphaSlider.setMaximum(255)
self.gridAlphaSlider.setProperty("value", 70) self.gridAlphaSlider.setProperty("value", 128)
self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal)
self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider")) self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider"))
self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1) self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1)

View File

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

View File

@ -800,7 +800,7 @@ class ROI(GraphicsObject):
#print " dshape", dShape #print " dshape", dShape
## Determine transform that maps ROI bounding box to image coordinates ## 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 ## Modify transform to scale from image coords to data coords
#m = QtGui.QTransform() #m = QtGui.QTransform()
@ -832,35 +832,34 @@ class ROI(GraphicsObject):
else: else:
return bounds, tr return bounds, tr
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
def getArrayRegion(self, data, img, axes=(0,1)): """Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array.
"""Use the position 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)) All extra keyword arguments are passed to :func:`affineSlice <pyqtgraph.affineSlice>`.
"""
## 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)
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 ### transpose data so x and y are the first 2 axes
#trAx = range(0, data.ndim) #trAx = range(0, data.ndim)
#trAx.remove(axes[0]) #trAx.remove(axes[0])
@ -959,6 +958,37 @@ class ROI(GraphicsObject):
### Untranspose array before returning ### Untranspose array before returning
#return arr5.transpose(tr2) #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): def getGlobalTransform(self, relativeTo=None):
"""Return global transformation (rotation angle+translation) required to move """Return global transformation (rotation angle+translation) required to move
from relative state to current state. If relative state isn't specified, 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)) v = dt.map(QtCore.QPointF(1, 0)) - dt.map(QtCore.QPointF(0, 0))
va = np.arctan2(v.y(), v.x()) va = np.arctan2(v.y(), v.x())
dti = dt.inverted()[0] dti = fn.invertQTransform(dt)
devPos = dt.map(QtCore.QPointF(0,0)) devPos = dt.map(QtCore.QPointF(0,0))
tr = QtGui.QTransform() tr = QtGui.QTransform()
tr.translate(devPos.x(), devPos.y()) tr.translate(devPos.x(), devPos.y())

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ from pyqtgraph.SignalProxy import SignalProxy
class PlotROI(ROI): class PlotROI(ROI):
def __init__(self, size): 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.addScaleHandle([1, 1], [0, 0])
self.addRotateHandle([0, 0], [0.5, 0.5]) self.addRotateHandle([0, 0], [0.5, 0.5])
@ -67,7 +67,12 @@ class ImageView(QtGui.QWidget):
sigTimeChanged = QtCore.Signal(object, object) sigTimeChanged = QtCore.Signal(object, object)
sigProcessingChanged = QtCore.Signal(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) QtGui.QWidget.__init__(self, parent, *args)
self.levelMax = 4096 self.levelMax = 4096
self.levelMin = 0 self.levelMin = 0
@ -89,7 +94,10 @@ class ImageView(QtGui.QWidget):
#self.ui.graphicsView.setAspectLocked(True) #self.ui.graphicsView.setAspectLocked(True)
#self.ui.graphicsView.invertY() #self.ui.graphicsView.invertY()
#self.ui.graphicsView.enableMouse() #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.ui.graphicsView.setCentralItem(self.view)
self.view.setAspectLocked(True) self.view.setAspectLocked(True)
self.view.invertY() 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.setTickColor(self.ticks[1], QtGui.QColor(255,255,255))
#self.ui.gradientWidget.setOrientation('right') #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.view.addItem(self.imageItem)
self.currentIndex = 0 self.currentIndex = 0
@ -531,14 +542,18 @@ class ImageView(QtGui.QWidget):
axes = (1, 2) axes = (1, 2)
else: else:
return 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: if data is not None:
while data.ndim > 1: while data.ndim > 1:
data = data.mean(axis=1) data = data.mean(axis=1)
if image.ndim == 3: if image.ndim == 3:
self.roiCurve.setData(y=data, x=self.tVals) self.roiCurve.setData(y=data, x=self.tVals)
else: 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() #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.gradientWidget.tickValue(self.ticks[0])
##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() ##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) (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 processes import ForkedProcess
from remoteproxy import ExitError from remoteproxy import ExitError
class CanceledError(Exception):
"""Raised when the progress dialog is canceled during a processing operation."""
pass
class Parallelize: class Parallelize:
""" """
Class for ultra-simple inline parallelization on multi-core CPUs Class for ultra-simple inline parallelization on multi-core CPUs
@ -29,35 +33,82 @@ class Parallelize:
print results 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) Arguments:
workers - number of worker processes or None to use number of CPUs in the system tasks list of objects to be processed (Parallelize will determine how to
kwds - objects to be shared by proxy with child processes 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: if workers is None:
workers = multiprocessing.cpu_count() workers = self.suggestedWorkerCount()
if not hasattr(os, 'fork'): if not hasattr(os, 'fork'):
workers = 1 workers = 1
self.workers = workers self.workers = workers
self.tasks = list(tasks) self.tasks = list(tasks)
self.kwds = kwds self.reseed = randomReseed
self.kwds = kwds.copy()
self.kwds['_taskStarted'] = self._taskStarted
def __enter__(self): def __enter__(self):
self.proc = None self.proc = None
workers = self.workers if self.workers == 1:
if workers == 1: return self.runSerial()
return Tasker(None, self.tasks, self.kwds) 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 = [] self.childs = []
## break up tasks into one set per worker ## break up tasks into one set per worker
workers = self.workers
chunks = [[] for i in xrange(workers)] chunks = [[] for i in xrange(workers)]
i = 0 i = 0
for i in range(len(self.tasks)): for i in range(len(self.tasks)):
@ -65,37 +116,91 @@ class Parallelize:
## fork and assign tasks to each worker ## fork and assign tasks to each worker
for i in range(workers): 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: if not proc.isParent:
self.proc = proc self.proc = proc
return Tasker(proc, chunks[i], proc.forkedProxies) return Tasker(proc, chunks[i], proc.forkedProxies)
else: else:
self.childs.append(proc) self.childs.append(proc)
## process events from workers until all have exited. ## Keep track of the progress of each worker independently.
activeChilds = self.childs[:] self.progress = {ch.childPid: [] for ch in self.childs}
while len(activeChilds) > 0: ## for each child process, self.progress[pid] is a list
for ch in activeChilds: ## 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 = [] rem = []
try: for ch in activeChilds:
ch.processRequests() try:
except ExitError: n = ch.processRequests()
rem.append(ch) if n > 0:
for ch in rem: waitingChildren += 1
activeChilds.remove(ch) except ExitError:
time.sleep(0.1) #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. 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 @staticmethod
pass 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: class Tasker:
def __init__(self, proc, tasks, kwds): def __init__(self, proc, tasks, kwds):
@ -106,9 +211,13 @@ class Tasker:
def __iter__(self): def __iter__(self):
## we could fix this up such that tasks are retrieved from the parent process one at a time.. ## 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 yield task
if self.proc is not None: if self.proc is not None:
#print os.getpid(), 'no more tasks'
self.proc.close() self.proc.close()

View File

@ -1,10 +1,51 @@
from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy 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 cPickle as pickle
import multiprocessing.connection import multiprocessing.connection
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError']
class Process(RemoteEventHandler): 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: if target is None:
target = startEventLoop target = startEventLoop
if name is None: if name is None:
@ -25,8 +66,12 @@ class Process(RemoteEventHandler):
port += 1 port += 1
## start remote process, instruct it to run target function ## start remote process, instruct it to run target function
self.proc = subprocess.Popen((sys.executable, __file__, 'remote'), stdin=subprocess.PIPE) sysPath = sys.path if copySysPath else None
pickle.dump((name+'_child', port, authkey, target), self.proc.stdin) 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() self.proc.stdin.close()
## open connection for remote process ## open connection for remote process
@ -60,19 +105,32 @@ def startEventLoop(name, port, authkey):
class ForkedProcess(RemoteEventHandler): class ForkedProcess(RemoteEventHandler):
""" """
ForkedProcess is a substitute for Process that uses os.fork() to generate a new process. 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 This is much faster than starting a completely new interpreter and child processes
and limitations: automatically have a copy of the entire program state from before the fork. This
- open file handles are shared with the parent process, which is potentially dangerous makes it an appealing approach when parallelizing expensive computations. (see
- it is not possible to have a QApplication in both parent and child process also Parallelizer)
(unless both QApplications are created _after_ the call to fork())
- generally not thread-safe. Also, threads are not copied by fork(); the new process However, fork() comes with some caveats and limitations:
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 - fork() is not available on Windows.
given any opportunity to clean up. (This prevents them calling any cleanup code that - It is not possible to have a QApplication in both parent and child process
was only intended to be used by the parent 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. When initializing, an optional target may be given.
If no target is specified, self.eventLoop will be used. 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 in the remote process (but do not need to be sent explicitly since
they are available immediately before the call to fork(). they are available immediately before the call to fork().
Proxies will be availabe as self.proxies[name]. 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 self.hasJoined = False
if target == 0: if target == 0:
@ -101,16 +162,51 @@ class ForkedProcess(RemoteEventHandler):
pid = os.fork() pid = os.fork()
if pid == 0: if pid == 0:
self.isParent = False 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() conn.close()
sys.stdin.close() ## otherwise we screw with interactive prompts. 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()) RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid())
if target is not None:
target()
ppid = os.getppid() ppid = os.getppid()
self.forkedProxies = {} self.forkedProxies = {}
for name, proxyId in proxyIDs.iteritems(): for name, proxyId in proxyIDs.iteritems():
self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name])) self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name]))
if target is not None:
target()
else: else:
self.isParent = True self.isParent = True
self.childPid = pid self.childPid = pid
@ -127,10 +223,11 @@ class ForkedProcess(RemoteEventHandler):
self.processRequests() # exception raised when the loop should exit self.processRequests() # exception raised when the loop should exit
time.sleep(0.01) time.sleep(0.01)
except ExitError: except ExitError:
sys.exit(0) break
except: except:
print "Error occurred in forked event loop:" print "Error occurred in forked event loop:"
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
sys.exit(0)
def join(self, timeout=10): def join(self, timeout=10):
if self.hasJoined: if self.hasJoined:
@ -138,10 +235,19 @@ class ForkedProcess(RemoteEventHandler):
#os.kill(pid, 9) #os.kill(pid, 9)
try: try:
self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation. 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 except IOError: ## probably remote process has already quit
pass pass
self.hasJoined = True 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. ##Special set of subclasses that implement a Qt event loop instead.
@ -165,8 +271,33 @@ class RemoteQtEventHandler(RemoteEventHandler):
#raise #raise
class QtProcess(Process): 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() self.startEventTimer()
def startEventTimer(self): def startEventTimer(self):
@ -201,8 +332,3 @@ def startQtEventLoop(name, port, authkey):
app.exec_() 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): 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 handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process
## an object proxy belongs to ## an object proxy belongs to
@ -55,19 +74,25 @@ class RemoteEventHandler(object):
def processRequests(self): def processRequests(self):
"""Process all pending requests from the pipe, return """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: if self.exited:
raise ExitError() raise ExitError()
numProcessed = 0
while self.conn.poll(): while self.conn.poll():
try: try:
self.handleRequest() self.handleRequest()
numProcessed += 1
except ExitError: except ExitError:
self.exited = True self.exited = True
raise raise
except: except:
print "Error in process %s" % self.name print "Error in process %s" % self.name
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
return numProcessed
def handleRequest(self): def handleRequest(self):
"""Handle a single request from the remote process. """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)) self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result))
def replyError(self, reqId, *exc): def replyError(self, reqId, *exc):
print "error:", self.name, reqId, exc[1]
excStr = traceback.format_exception(*exc) excStr = traceback.format_exception(*exc)
try: try:
self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr)) self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr))
@ -282,7 +308,9 @@ class RemoteEventHandler(object):
try: try:
optStr = pickle.dumps(opts) optStr = pickle.dumps(opts)
except: except:
print "Error pickling:", opts print "==== Error pickling this object: ===="
print opts
print "======================================="
raise raise
request = (request, reqId, optStr) request = (request, reqId, optStr)
@ -381,8 +409,8 @@ class RemoteEventHandler(object):
def transfer(self, obj, **kwds): def transfer(self, obj, **kwds):
""" """
Transfer an object to the remote host (the object must be picklable) and return Transfer an object by value to the remote host (the object must be picklable)
a proxy for the new remote object. and return a proxy for the new remote object.
""" """
return self.send(request='transfer', opts=dict(obj=obj), **kwds) return self.send(request='transfer', opts=dict(obj=obj), **kwds)
@ -395,7 +423,12 @@ class RemoteEventHandler(object):
class Request: 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): def __init__(self, process, reqId, description=None, timeout=10):
self.proc = process self.proc = process
self.description = description self.description = description
@ -405,10 +438,13 @@ class Request:
self.timeout = timeout self.timeout = timeout
def result(self, block=True, timeout=None): 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 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 the timeout is reached, raise NoResultError. (use timeout=None to disable)
If block is False, raises an exception if the result has not arrived yet.""" If block is False, raise NoResultError immediately if the result has not arrived yet.
"""
if self.gotResult: if self.gotResult:
return self._result return self._result
@ -434,16 +470,24 @@ class Request:
def hasResult(self): def hasResult(self):
"""Returns True if the result for this request has arrived.""" """Returns True if the result for this request has arrived."""
try: try:
#print "check result", self.description
self.result(block=False) self.result(block=False)
except NoResultError: except NoResultError:
#print " -> not yet"
pass pass
return self.gotResult return self.gotResult
class LocalObjectProxy(object): 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 nextProxyId = 0
proxiedObjects = {} ## maps {proxyId: object} proxiedObjects = {} ## maps {proxyId: object}
@ -467,24 +511,31 @@ class LocalObjectProxy(object):
del cls.proxiedObjects[pid] del cls.proxiedObjects[pid]
#print "release:", cls.proxiedObjects #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.processId = os.getpid()
#self.objectId = id(obj) #self.objectId = id(obj)
self.typeStr = repr(obj) self.typeStr = repr(obj)
#self.handler = handler #self.handler = handler
self.obj = obj self.obj = obj
self.opts = opts
def __reduce__(self): def __reduce__(self):
## a proxy is being pickled and sent to a remote process. ## 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, ## 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. ## so we keep a new ID so we can track when each is released.
pid = LocalObjectProxy.registerObject(self.obj) pid = LocalObjectProxy.registerObject(self.obj)
return (unpickleObjectProxy, (self.processId, pid, self.typeStr)) return (unpickleObjectProxy, (self.processId, pid, self.typeStr, None, self.opts))
## alias ## alias
proxy = LocalObjectProxy proxy = LocalObjectProxy
def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None): def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None, opts=None):
if processId == os.getpid(): if processId == os.getpid():
obj = LocalObjectProxy.lookupProxyId(proxyId) obj = LocalObjectProxy.lookupProxyId(proxyId)
if attributes is not None: if attributes is not None:
@ -492,7 +543,10 @@ def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None):
obj = getattr(obj, attr) obj = getattr(obj, attr)
return obj return obj
else: 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): class ObjectProxy(object):
""" """
@ -501,7 +555,44 @@ class ObjectProxy(object):
attributes on existing proxy objects. attributes on existing proxy objects.
For the most part, this object can be used exactly as if it 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): def __init__(self, processId, proxyId, typeStr='', parent=None):
object.__init__(self) object.__init__(self)
@ -574,6 +665,13 @@ class ObjectProxy(object):
""" """
self._proxyOptions.update(kwds) 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): def _getProxyOption(self, opt):
val = self._proxyOptions[opt] val = self._proxyOptions[opt]
if val is None: 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) return "<ObjectProxy for process %d, object 0x%x: %s >" % (self._processId, self._proxyId, self._typeStr)
def __getattr__(self, attr): def __getattr__(self, attr, **kwds):
#if '_processId' not in self.__dict__: """
#raise Exception("ObjectProxy has no processId") Calls __getattr__ on the remote object and returns the attribute
#proc = Process._processes[self._processId] by value or by proxy depending on the options set (see
deferred = self._getProxyOption('deferGetattr') ObjectProxy._setProxyOptions and RemoteEventHandler.setProxyOptions)
if deferred is True:
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) return self._deferredAttr(attr)
else: else:
opts = self._getProxyOptions() #opts = self._getProxyOptions()
return self._handler.getObjAttr(self, attr, **opts) return self._handler.getObjAttr(self, attr, **opts)
def _deferredAttr(self, attr): def _deferredAttr(self, attr):
return DeferredObjectProxy(self, attr) return DeferredObjectProxy(self, attr)
def __call__(self, *args, **kwds): def __call__(self, *args, **kwds):
""" """
Attempts to call the proxied object from the remote process. Attempts to call the proxied object from the remote process.
@ -613,44 +722,34 @@ class ObjectProxy(object):
_callSync 'off', 'sync', or 'async' _callSync 'off', 'sync', or 'async'
_returnType 'value', 'proxy', or 'auto' _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() opts = self._getProxyOptions()
for k in opts: for k in opts:
if '_'+k in kwds: if '_'+k in kwds:
opts[k] = kwds.pop('_'+k) opts[k] = kwds.pop('_'+k)
#print "call", opts
return self._handler.callObj(obj=self, args=args, kwds=kwds, **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?? ## Explicitly proxy special methods. Is there a better way to do this??
def _getSpecialAttr(self, attr): 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) return self._deferredAttr(attr)
def __getitem__(self, *args): def __getitem__(self, *args):
return self._getSpecialAttr('__getitem__')(*args) return self._getSpecialAttr('__getitem__')(*args)
def __setitem__(self, *args): def __setitem__(self, *args):
return self._getSpecialAttr('__setitem__')(*args) return self._getSpecialAttr('__setitem__')(*args, _callSync='off')
def __setattr__(self, *args): def __setattr__(self, *args):
return self._getSpecialAttr('__setattr__')(*args) return self._getSpecialAttr('__setattr__')(*args, _callSync='off')
def __str__(self, *args): def __str__(self, *args):
return self._getSpecialAttr('__str__')(*args, _returnType=True) return self._getSpecialAttr('__str__')(*args, _returnType='value')
def __len__(self, *args): def __len__(self, *args):
return self._getSpecialAttr('__len__')(*args) return self._getSpecialAttr('__len__')(*args)
@ -670,6 +769,21 @@ class ObjectProxy(object):
def __pow__(self, *args): def __pow__(self, *args):
return self._getSpecialAttr('__pow__')(*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): def __rshift__(self, *args):
return self._getSpecialAttr('__rshift__')(*args) return self._getSpecialAttr('__rshift__')(*args)
@ -679,6 +793,15 @@ class ObjectProxy(object):
def __floordiv__(self, *args): def __floordiv__(self, *args):
return self._getSpecialAttr('__pow__')(*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): def __eq__(self, *args):
return self._getSpecialAttr('__eq__')(*args) return self._getSpecialAttr('__eq__')(*args)
@ -704,7 +827,16 @@ class ObjectProxy(object):
return self._getSpecialAttr('__or__')(*args) return self._getSpecialAttr('__or__')(*args)
def __xor__(self, *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): def __mod__(self, *args):
return self._getSpecialAttr('__mod__')(*args) return self._getSpecialAttr('__mod__')(*args)
@ -746,6 +878,37 @@ class ObjectProxy(object):
return self._getSpecialAttr('__rmod__')(*args) return self._getSpecialAttr('__rmod__')(*args)
class DeferredObjectProxy(ObjectProxy): 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): def __init__(self, parentProxy, attribute):
## can't set attributes directly because setattr is overridden. ## can't set attributes directly because setattr is overridden.
for k in ['_processId', '_typeStr', '_proxyId', '_handler']: for k in ['_processId', '_typeStr', '_proxyId', '_handler']:
@ -756,4 +919,10 @@ class DeferredObjectProxy(ObjectProxy):
def __repr__(self): def __repr__(self):
return ObjectProxy.__repr__(self) + '.' + '.'.join(self._attributes) 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.keysPressed = {}
self.keyTimer = QtCore.QTimer() self.keyTimer = QtCore.QTimer()
self.keyTimer.timeout.connect(self.evalKeyState) self.keyTimer.timeout.connect(self.evalKeyState)
self.makeCurrent()
def addItem(self, item): def addItem(self, item):
self.items.append(item) self.items.append(item)
if hasattr(item, 'initializeGL'): if hasattr(item, 'initializeGL'):
self.makeCurrent() self.makeCurrent()
item.initializeGL() try:
item.initializeGL()
except:
self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item))
item._setView(self) item._setView(self)
#print "set view", item, self, item.view() #print "set view", item, self, item.view()
self.update() self.update()
@ -100,20 +106,28 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glPushAttrib(GL_ALL_ATTRIB_BITS) glPushAttrib(GL_ALL_ATTRIB_BITS)
i.paint() i.paint()
except: except:
import sys import pyqtgraph.debug
sys.excepthook(*sys.exc_info()) pyqtgraph.debug.printExc()
print("Error while drawing item", i) 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: finally:
glPopAttrib(GL_ALL_ATTRIB_BITS) glPopAttrib()
else: else:
glMatrixMode(GL_MODELVIEW) glMatrixMode(GL_MODELVIEW)
glPushMatrix() glPushMatrix()
tr = i.transform() try:
a = np.array(tr.copyDataTo()).reshape((4,4)) tr = i.transform()
glMultMatrixf(a.transpose()) a = np.array(tr.copyDataTo()).reshape((4,4))
self.drawItemTree(i) glMultMatrixf(a.transpose())
glMatrixMode(GL_MODELVIEW) self.drawItemTree(i)
glPopMatrix() finally:
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
def cameraPosition(self): def cameraPosition(self):
@ -237,4 +251,15 @@ class GLViewWidget(QtOpenGL.QGLWidget):
else: else:
self.keyTimer.stop() 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) glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER)
shape = self.data.shape 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))) 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) glDisable(GL_TEXTURE_3D)

View File

@ -131,11 +131,16 @@ class Parameter(QtCore.QObject):
return name return name
def childPath(self, child): 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 = [] path = []
while child is not self: while child is not self:
path.insert(0, child.name()) path.insert(0, child.name())
child = child.parent() child = child.parent()
if child is None:
return None
return path return path
def setValue(self, value, blockSignal=None): def setValue(self, value, blockSignal=None):

View File

@ -113,4 +113,6 @@ class ParameterTree(TreeWidget):
sel[0].selected(True) sel[0].selected(True)
return TreeWidget.selectionChanged(self, *args) 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.Qt import QtCore, QtGui
from pyqtgraph.python2_3 import asUnicode
from .Parameter import Parameter, registerParameterType from .Parameter import Parameter, registerParameterType
from .ParameterItem import ParameterItem from .ParameterItem import ParameterItem
from pyqtgraph.widgets.SpinBox import SpinBox from pyqtgraph.widgets.SpinBox import SpinBox
@ -348,6 +349,7 @@ class GroupParameterItem(ParameterItem):
def treeWidgetChanged(self): def treeWidgetChanged(self):
ParameterItem.treeWidgetChanged(self) ParameterItem.treeWidgetChanged(self)
self.treeWidget().setFirstItemColumnSpanned(self, True)
if self.addItem is not None: if self.addItem is not None:
self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox) self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox)
self.treeWidget().setFirstItemColumnSpanned(self.addItem, True) self.treeWidget().setFirstItemColumnSpanned(self.addItem, True)

View File

@ -42,8 +42,9 @@ def sortList(l, cmpFunc):
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
import builtins import builtins
builtins.basestring = str builtins.basestring = str
builtins.asUnicode = asUnicode #builtins.asUnicode = asUnicode
builtins.sortList = sortList #builtins.sortList = sortList
basestring = str
def cmp(a,b): def cmp(a,b):
if a>b: if a>b:
return 1 return 1
@ -52,7 +53,7 @@ if sys.version_info[0] == 3:
else: else:
return 0 return 0
builtins.cmp = cmp builtins.cmp = cmp
else: #else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe
import __builtin__ #import __builtin__
__builtin__.asUnicode = asUnicode #__builtin__.asUnicode = asUnicode
__builtin__.sortList = sortList #__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) - Skips reload if the file has not been updated (if .pyc is newer than .py)
- if prefix is None, checks all loaded modules - 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 for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload
if not inspect.ismodule(mod): if not inspect.ismodule(mod):
continue continue
@ -58,7 +59,10 @@ def reloadAll(prefix=None, debug=False):
reload(mod, debug=debug) reload(mod, debug=debug)
except: except:
printExc("Error while reloading module %s, skipping\n" % mod) 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): def reload(module, debug=False, lists=False, dicts=False):
"""Replacement for the builtin reload function: """Replacement for the builtin reload function:

View File

@ -15,7 +15,7 @@ class GradientWidget(GraphicsView):
def __init__(self, parent=None, orientation='bottom', *args, **kargs): def __init__(self, parent=None, orientation='bottom', *args, **kargs):
GraphicsView.__init__(self, parent, useOpenGL=False, background=None) GraphicsView.__init__(self, parent, useOpenGL=False, background=None)
self.maxDim = 27 self.maxDim = 31
kargs['tickPen'] = 'k' kargs['tickPen'] = 'k'
self.item = GradientEditorItem(*args, **kargs) self.item = GradientEditorItem(*args, **kargs)
self.item.sigGradientChanged.connect(self.sigGradientChanged) self.item.sigGradientChanged.connect(self.sigGradientChanged)
@ -24,7 +24,7 @@ class GradientWidget(GraphicsView):
self.setCacheMode(self.CacheNone) self.setCacheMode(self.CacheNone)
self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing)
self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) 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.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
#self.setAutoFillBackground(False) #self.setAutoFillBackground(False)
#self.setAttribute(QtCore.Qt.WA_PaintOnScreen, 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 from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
try: try:
from pyqtgraph.Qt import QtOpenGL from pyqtgraph.Qt import QtOpenGL
@ -13,12 +14,8 @@ try:
except ImportError: except ImportError:
HAVE_OPENGL = False HAVE_OPENGL = False
#from numpy import vstack
#import time
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
#from vector import *
import sys, os import sys, os
#import debug
from .FileDialog import FileDialog from .FileDialog import FileDialog
from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.GraphicsScene import GraphicsScene
import numpy as np import numpy as np
@ -29,6 +26,20 @@ import pyqtgraph
__all__ = ['GraphicsView'] __all__ = ['GraphicsView']
class GraphicsView(QtGui.QGraphicsView): 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) sigRangeChanged = QtCore.Signal(object, object)
sigMouseReleased = QtCore.Signal(object) sigMouseReleased = QtCore.Signal(object)
@ -37,17 +48,25 @@ class GraphicsView(QtGui.QGraphicsView):
sigScaleChanged = QtCore.Signal(object) sigScaleChanged = QtCore.Signal(object)
lastFileDir = None lastFileDir = None
def __init__(self, parent=None, useOpenGL=None, background='k'): def __init__(self, parent=None, useOpenGL=None, background='default'):
"""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. 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 self.closed = False
QtGui.QGraphicsView.__init__(self, parent) 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.. ## This might help, but it's probably dangerous in the general case..
#self.setOptimizationFlag(self.DontSavePainterState, True) #self.setOptimizationFlag(self.DontSavePainterState, True)
if background is not None: self.setBackground(background)
brush = fn.mkBrush(background)
self.setBackgroundBrush(brush)
self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setFrameShape(QtGui.QFrame.NoFrame) self.setFrameShape(QtGui.QFrame.NoFrame)
@ -75,13 +92,10 @@ class GraphicsView(QtGui.QGraphicsView):
self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate) self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate)
#self.setSceneRect(QtCore.QRectF(-1e10, -1e10, 2e10, 2e10))
self.lockedViewports = [] self.lockedViewports = []
self.lastMousePos = None self.lastMousePos = None
self.setMouseTracking(True) self.setMouseTracking(True)
self.aspectLocked = False self.aspectLocked = False
#self.yInverted = True
self.range = QtCore.QRectF(0, 0, 1, 1) self.range = QtCore.QRectF(0, 0, 1, 1)
self.autoPixelRange = True self.autoPixelRange = True
self.currentItem = None self.currentItem = None
@ -90,6 +104,11 @@ class GraphicsView(QtGui.QGraphicsView):
self.sceneObj = GraphicsScene() self.sceneObj = GraphicsScene()
self.setScene(self.sceneObj) 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. ## by default we set up a central widget with a grid layout.
## this can be replaced if needed. ## this can be replaced if needed.
self.centralWidget = None 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.scaleCenter = False ## should scaling center around view center (True) or mouse click (False)
self.clickAccepted = False self.clickAccepted = False
#def paintEvent(self, *args): def setBackground(self, background):
#prof = debug.Profiler('GraphicsView.paintEvent '+str(id(self)), disabled=False) """
#QtGui.QGraphicsView.paintEvent(self, *args) Set the background color of the GraphicsView.
#prof.finish() 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): def close(self):
self.centralWidget = None self.centralWidget = None
self.scene().clear() self.scene().clear()
#print " ", self.scene().itemCount()
self.currentItem = None self.currentItem = None
self.sceneObj = None self.sceneObj = None
self.closed = True self.closed = True
@ -123,11 +152,9 @@ class GraphicsView(QtGui.QGraphicsView):
else: else:
v = QtGui.QWidget() v = QtGui.QWidget()
#v.setStyleSheet("background-color: #000000;")
self.setViewport(v) self.setViewport(v)
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
#QtGui.QGraphicsView.keyPressEvent(self, ev)
self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene
## (view likes to eat arrow key events) ## (view likes to eat arrow key events)
@ -136,7 +163,8 @@ class GraphicsView(QtGui.QGraphicsView):
return self.setCentralWidget(item) return self.setCentralWidget(item)
def setCentralWidget(self, 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: if self.centralWidget is not None:
self.scene().removeItem(self.centralWidget) self.scene().removeItem(self.centralWidget)
self.centralWidget = item self.centralWidget = item
@ -162,15 +190,18 @@ class GraphicsView(QtGui.QGraphicsView):
return return
if self.autoPixelRange: if self.autoPixelRange:
self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) 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() self.updateMatrix()
def updateMatrix(self, propagate=True): def updateMatrix(self, propagate=True):
self.setSceneRect(self.range) self.setSceneRect(self.range)
if self.aspectLocked: if self.autoPixelRange:
self.fitInView(self.range, QtCore.Qt.KeepAspectRatio) self.resetTransform()
else: 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) self.sigRangeChanged.emit(self, self.range)
@ -196,11 +227,6 @@ class GraphicsView(QtGui.QGraphicsView):
scale = [sx, sy] scale = [sx, sy]
if self.aspectLocked: if self.aspectLocked:
scale[0] = scale[1] 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: if self.scaleCenter:
center = None center = None
@ -270,13 +296,6 @@ class GraphicsView(QtGui.QGraphicsView):
r1.setBottom(r.bottom()) r1.setBottom(r.bottom())
GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False) 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): def wheelEvent(self, ev):
QtGui.QGraphicsView.wheelEvent(self, ev) QtGui.QGraphicsView.wheelEvent(self, ev)
if not self.mouseEnabled: if not self.mouseEnabled:
@ -289,39 +308,11 @@ class GraphicsView(QtGui.QGraphicsView):
def setAspectLocked(self, s): def setAspectLocked(self, s):
self.aspectLocked = 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): def leaveEvent(self, ev):
self.scene().leaveEvent(ev) ## inform scene when mouse leaves self.scene().leaveEvent(ev) ## inform scene when mouse leaves
def mousePressEvent(self, ev): def mousePressEvent(self, ev):
QtGui.QGraphicsView.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: if not self.mouseEnabled:
@ -333,39 +324,14 @@ class GraphicsView(QtGui.QGraphicsView):
self.scene().clearSelection() self.scene().clearSelection()
return ## Everything below disabled for now.. 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): def mouseReleaseEvent(self, ev):
QtGui.QGraphicsView.mouseReleaseEvent(self, ev) QtGui.QGraphicsView.mouseReleaseEvent(self, ev)
if not self.mouseEnabled: if not self.mouseEnabled:
return return
#self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
#self.emit(QtCore.SIGNAL("mouseReleased"), ev)
self.sigMouseReleased.emit(ev) self.sigMouseReleased.emit(ev)
self.lastButtonReleased = ev.button() self.lastButtonReleased = ev.button()
return ## Everything below disabled for now.. 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): def mouseMoveEvent(self, ev):
if self.lastMousePos is None: if self.lastMousePos is None:
self.lastMousePos = Point(ev.pos()) self.lastMousePos = Point(ev.pos())
@ -375,10 +341,7 @@ class GraphicsView(QtGui.QGraphicsView):
QtGui.QGraphicsView.mouseMoveEvent(self, ev) QtGui.QGraphicsView.mouseMoveEvent(self, ev)
if not self.mouseEnabled: if not self.mouseEnabled:
return return
#self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos()))
self.sigSceneMouseMoved.emit(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. if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it.
return return
@ -386,10 +349,7 @@ class GraphicsView(QtGui.QGraphicsView):
if ev.buttons() == QtCore.Qt.RightButton: if ev.buttons() == QtCore.Qt.RightButton:
delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50)) delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50))
scale = 1.01 ** delta scale = 1.01 ** delta
#if self.yInverted:
#scale[0] = 1. / scale[0]
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) 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) self.sigRangeChanged.emit(self, self.range)
elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. 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 tr = -delta * px
self.translate(tr[0], tr[1]) self.translate(tr[0], tr[1])
#self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range)
self.sigRangeChanged.emit(self, 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): def pixelSize(self):
"""Return vector with the length and width of one view pixel in scene coordinates""" """Return vector with the length and width of one view pixel in scene coordinates"""
p0 = Point(0,0) p0 = Point(0,0)
@ -423,80 +368,7 @@ class GraphicsView(QtGui.QGraphicsView):
p11 = tr.map(p1) p11 = tr.map(p1)
return Point(p11 - p01) 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): def dragEnterEvent(self, ev):
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events 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.item = HistogramLUTItem(*args, **kargs)
self.setCentralItem(self.item) self.setCentralItem(self.item)
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
self.setMinimumWidth(92) self.setMinimumWidth(95)
def sizeHint(self): 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>`. other methods, use :func:`getPlotItem <pyqtgraph.PlotWidget.getPlotItem>`.
""" """
def __init__(self, parent=None, **kargs): 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) GraphicsView.__init__(self, parent)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.enableMouse(False) self.enableMouse(False)

View File

@ -14,7 +14,7 @@ class ProgressDialog(QtGui.QProgressDialog):
if dlg.wasCanceled(): if dlg.wasCanceled():
raise Exception("Processing canceled by user") 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:** **Arguments:**
@ -25,15 +25,16 @@ class ProgressDialog(QtGui.QProgressDialog):
parent parent
wait Length of time (im ms) to wait before displaying dialog wait Length of time (im ms) to wait before displaying dialog
busyCursor If True, show busy cursor until dialog finishes 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() isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
if not isGuiThread: self.disabled = disable or (not isGuiThread)
self.disabled = True if self.disabled:
return return
self.disabled = False
noCancel = False noCancel = False
if cancelText is None: 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 -*- # -*- coding: utf-8 -*-
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.SignalProxy import SignalProxy from pyqtgraph.SignalProxy import SignalProxy
import pyqtgraph.functions as fn import pyqtgraph.functions as fn

View File

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