merge from dev
This commit is contained in:
commit
0f97ac77e2
@ -1,4 +1,5 @@
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
from pyqtgraph.python2_3 import sortList
|
||||
#try:
|
||||
#from PyQt4 import QtOpenGL
|
||||
#HAVE_OPENGL = True
|
||||
@ -505,18 +506,19 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
menusToAdd = []
|
||||
while item is not self:
|
||||
item = item.parentItem()
|
||||
|
||||
|
||||
if item is None:
|
||||
item = self
|
||||
|
||||
if not hasattr(item, "getContextMenus"):
|
||||
continue
|
||||
|
||||
|
||||
subMenus = item.getContextMenus(event)
|
||||
if subMenus is None:
|
||||
continue
|
||||
if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus
|
||||
subMenus = [subMenus]
|
||||
|
||||
|
||||
for sm in subMenus:
|
||||
menusToAdd.append(sm)
|
||||
|
||||
|
30
Qt.py
30
Qt.py
@ -1,15 +1,23 @@
|
||||
## Do all Qt imports from here to allow easier PyQt / PySide compatibility
|
||||
|
||||
#from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
||||
from PyQt4 import QtGui, QtCore
|
||||
try:
|
||||
from PyQt4 import QtSvg
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from PyQt4 import QtOpenGL
|
||||
except ImportError:
|
||||
pass
|
||||
USE_PYSIDE = False ## If False, import PyQt4. If True, import PySide
|
||||
## Note that when switching between PyQt and PySide, all template
|
||||
## files (*.ui) must be rebuilt for the target library.
|
||||
|
||||
if USE_PYSIDE:
|
||||
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
||||
import PySide
|
||||
VERSION_INFO = 'PySide ' + PySide.__version__
|
||||
else:
|
||||
from PyQt4 import QtGui, QtCore
|
||||
try:
|
||||
from PyQt4 import QtSvg
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from PyQt4 import QtOpenGL
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not hasattr(QtCore, 'Signal'):
|
||||
QtCore.Signal = QtCore.pyqtSignal
|
||||
VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
|
||||
|
@ -76,7 +76,7 @@ class SRTTransform(QtGui.QTransform):
|
||||
m = pg.SRTTransform3D(m)
|
||||
angle, axis = m.getRotation()
|
||||
if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1):
|
||||
print angle, axis
|
||||
print("angle: %s axis: %s" % (str(angle), str(axis)))
|
||||
raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.")
|
||||
self._state = {
|
||||
'pos': Point(m.getTranslation()),
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from Qt import QtCore, QtGui
|
||||
from Vector import Vector
|
||||
from SRTTransform import SRTTransform
|
||||
from .Qt import QtCore, QtGui
|
||||
from .Vector import Vector
|
||||
from .SRTTransform import SRTTransform
|
||||
import pyqtgraph as pg
|
||||
import numpy as np
|
||||
import scipy.linalg
|
||||
@ -136,15 +136,15 @@ class SRTTransform3D(QtGui.QMatrix4x4):
|
||||
try:
|
||||
evals, evecs = scipy.linalg.eig(r)
|
||||
except:
|
||||
print "Rotation matrix:", r
|
||||
print "Scale:", scale
|
||||
print "Original matrix:", m
|
||||
print("Rotation matrix: %s" % str(r))
|
||||
print("Scale: %s" % str(scale))
|
||||
print("Original matrix: %s" % str(m))
|
||||
raise
|
||||
eigIndex = np.argwhere(np.abs(evals-1) < 1e-7)
|
||||
if len(eigIndex) < 1:
|
||||
print "eigenvalues:", evals
|
||||
print "eigenvectors:", evecs
|
||||
print "index:", eigIndex, evals-1
|
||||
print("eigenvalues: %s" % str(evals))
|
||||
print("eigenvectors: %s" % str(evecs))
|
||||
print("index: %s, %s" % (str(eigIndex), str(evals-1)))
|
||||
raise Exception("Could not determine rotation axis.")
|
||||
axis = evecs[eigIndex[0,0]].real
|
||||
axis /= ((axis**2).sum())**0.5
|
||||
@ -259,23 +259,23 @@ if __name__ == '__main__':
|
||||
tr3 = QtGui.QTransform()
|
||||
tr3.translate(20, 0)
|
||||
tr3.rotate(45)
|
||||
print "QTransform -> Transform:", SRTTransform(tr3)
|
||||
print("QTransform -> Transform: %s" % str(SRTTransform(tr3)))
|
||||
|
||||
print "tr1:", tr1
|
||||
print("tr1: %s" % str(tr1))
|
||||
|
||||
tr2.translate(20, 0)
|
||||
tr2.rotate(45)
|
||||
print "tr2:", tr2
|
||||
print("tr2: %s" % str(tr2))
|
||||
|
||||
dt = tr2/tr1
|
||||
print "tr2 / tr1 = ", dt
|
||||
print("tr2 / tr1 = %s" % str(dt))
|
||||
|
||||
print "tr2 * tr1 = ", tr2*tr1
|
||||
print("tr2 * tr1 = %s" % str(tr2*tr1))
|
||||
|
||||
tr4 = SRTTransform()
|
||||
tr4.scale(-1, 1)
|
||||
tr4.rotate(30)
|
||||
print "tr1 * tr4 = ", tr1*tr4
|
||||
print("tr1 * tr4 = %s" % str(tr1*tr4))
|
||||
|
||||
w1 = widgets.TestROI((19,19), (22, 22), invertible=True)
|
||||
#w2 = widgets.TestROI((0,0), (150, 150))
|
||||
|
@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
"""
|
||||
|
||||
from Qt import QtGui, QtCore
|
||||
from .Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
|
||||
class Vector(QtGui.QVector3D):
|
||||
|
@ -10,6 +10,7 @@ of a large group of widgets.
|
||||
|
||||
from .Qt import QtCore, QtGui
|
||||
import weakref, inspect
|
||||
from .python2_3 import asUnicode
|
||||
|
||||
|
||||
__all__ = ['WidgetGroup']
|
||||
|
36
__init__.py
36
__init__.py
@ -1,18 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
REVISION = None
|
||||
|
||||
### import all the goodies and add some helper functions for easy CLI use
|
||||
|
||||
## 'Qt' is a local module; it is intended mainly to cover up the differences
|
||||
## between PyQt4 and PySide.
|
||||
from .Qt import QtGui
|
||||
from .Qt import QtGui
|
||||
|
||||
## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause)
|
||||
#if QtGui.QApplication.instance() is None:
|
||||
#app = QtGui.QApplication([])
|
||||
|
||||
import sys
|
||||
import os, sys
|
||||
|
||||
## check python version
|
||||
if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] != 7):
|
||||
## Allow anything >= 2.7
|
||||
if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] < 7):
|
||||
raise Exception("Pyqtgraph requires Python version 2.7 (this is %d.%d)" % (sys.version_info[0], sys.version_info[1]))
|
||||
|
||||
## helpers for 2/3 compatibility
|
||||
@ -30,13 +33,15 @@ else:
|
||||
useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff.
|
||||
|
||||
CONFIG_OPTIONS = {
|
||||
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
|
||||
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
|
||||
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
|
||||
'foregroundColor': (200,200,200),
|
||||
'backgroundColor': (0,0,0),
|
||||
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
|
||||
'background': (0, 0, 0), ## default background for GraphicsWidget
|
||||
'antialias': False,
|
||||
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
||||
}
|
||||
|
||||
|
||||
def setConfigOption(opt, value):
|
||||
CONFIG_OPTIONS[opt] = value
|
||||
|
||||
@ -44,6 +49,23 @@ def getConfigOption(opt):
|
||||
return CONFIG_OPTIONS[opt]
|
||||
|
||||
|
||||
def systemInfo():
|
||||
print("sys.platform: %s" % sys.platform)
|
||||
print("sys.version: %s" % sys.version)
|
||||
from .Qt import VERSION_INFO
|
||||
print("qt bindings: %s" % VERSION_INFO)
|
||||
|
||||
global REVISION
|
||||
if REVISION is None: ## this code was probably checked out from bzr; look up the last-revision file
|
||||
lastRevFile = os.path.join(os.path.dirname(__file__), '.bzr', 'branch', 'last-revision')
|
||||
if os.path.exists(lastRevFile):
|
||||
REVISION = open(lastRevFile, 'r').read().strip()
|
||||
|
||||
print("pyqtgraph: %s" % REVISION)
|
||||
print("config:")
|
||||
import pprint
|
||||
pprint.pprint(CONFIG_OPTIONS)
|
||||
|
||||
## Rename orphaned .pyc files. This is *probably* safe :)
|
||||
|
||||
def renamePyc(startDir):
|
||||
@ -105,7 +127,7 @@ def importAll(path, excludes=()):
|
||||
globals()[k] = getattr(mod, k)
|
||||
|
||||
importAll('graphicsItems')
|
||||
importAll('widgets', excludes=['MatplotlibWidget'])
|
||||
importAll('widgets', excludes=['MatplotlibWidget', 'RemoteGraphicsView'])
|
||||
|
||||
from .imageview import *
|
||||
from .WidgetGroup import *
|
||||
|
@ -13,6 +13,7 @@ import re, os, sys
|
||||
from collections import OrderedDict
|
||||
GLOBAL_PATH = None # so not thread safe.
|
||||
from . import units
|
||||
from .python2_3 import asUnicode
|
||||
|
||||
class ParseError(Exception):
|
||||
def __init__(self, message, lineNum, line, fileName=None):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
from pyqtgraph.python2_3 import asUnicode
|
||||
|
||||
class CmdInput(QtGui.QLineEdit):
|
||||
|
||||
@ -25,10 +26,10 @@ class CmdInput(QtGui.QLineEdit):
|
||||
self.execCmd()
|
||||
else:
|
||||
QtGui.QLineEdit.keyPressEvent(self, ev)
|
||||
self.history[0] = unicode(self.text())
|
||||
self.history[0] = asUnicode(self.text())
|
||||
|
||||
def execCmd(self):
|
||||
cmd = unicode(self.text())
|
||||
cmd = asUnicode(self.text())
|
||||
if len(self.history) == 1 or cmd != self.history[1]:
|
||||
self.history.insert(1, cmd)
|
||||
#self.lastCmd = cmd
|
||||
|
@ -1,13 +1,11 @@
|
||||
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import sys, re, os, time, traceback
|
||||
import sys, re, os, time, traceback, subprocess
|
||||
import pyqtgraph as pg
|
||||
import template
|
||||
from . import template
|
||||
import pyqtgraph.exceptionHandling as exceptionHandling
|
||||
import pickle
|
||||
|
||||
EDITOR = "pykate {fileName}:{lineNum}"
|
||||
|
||||
class ConsoleWidget(QtGui.QWidget):
|
||||
"""
|
||||
Widget displaying console output and accepting command input.
|
||||
@ -24,7 +22,7 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
be baffling and frustrating to users since it would appear the program has frozen.
|
||||
- some terminals (eg windows cmd.exe) have notoriously unfriendly interfaces
|
||||
- ability to add extra features like exception stack introspection
|
||||
- ability to have multiple interactive prompts for remotely generated processes
|
||||
- ability to have multiple interactive prompts, including for spawned sub-processes
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None):
|
||||
@ -72,8 +70,8 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
self.ui.historyList.itemDoubleClicked.connect(self.cmdDblClicked)
|
||||
self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible)
|
||||
|
||||
self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllToggled)
|
||||
self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextToggled)
|
||||
self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions)
|
||||
self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextException)
|
||||
self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked)
|
||||
self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked)
|
||||
self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked)
|
||||
@ -229,15 +227,25 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def catchAllToggled(self, b):
|
||||
if b:
|
||||
def catchAllExceptions(self, catch=True):
|
||||
"""
|
||||
If True, the console will catch all unhandled exceptions and display the stack
|
||||
trace. Each exception caught clears the last.
|
||||
"""
|
||||
self.ui.catchAllExceptionsBtn.setChecked(catch)
|
||||
if catch:
|
||||
self.ui.catchNextExceptionBtn.setChecked(False)
|
||||
exceptionHandling.register(self.allExceptionsHandler)
|
||||
else:
|
||||
exceptionHandling.unregister(self.allExceptionsHandler)
|
||||
|
||||
def catchNextToggled(self, b):
|
||||
if b:
|
||||
def catchNextException(self, catch=True):
|
||||
"""
|
||||
If True, the console will catch the next unhandled exception and display the stack
|
||||
trace.
|
||||
"""
|
||||
self.ui.catchNextExceptionBtn.setChecked(catch)
|
||||
if catch:
|
||||
self.ui.catchAllExceptionsBtn.setChecked(False)
|
||||
exceptionHandling.register(self.nextExceptionHandler)
|
||||
else:
|
||||
@ -254,11 +262,15 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
pass
|
||||
|
||||
def stackItemDblClicked(self, item):
|
||||
global EDITOR
|
||||
editor = self.editor
|
||||
if editor is None:
|
||||
editor = pg.getConfigOption('editorCommand')
|
||||
if editor is None:
|
||||
return
|
||||
tb = self.currentFrame()
|
||||
lineNum = tb.tb_lineno
|
||||
fileName = tb.tb_frame.f_code.co_filename
|
||||
os.system(EDITOR.format(fileName=fileName, lineNum=lineNum))
|
||||
subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True)
|
||||
|
||||
|
||||
def allExceptionsHandler(self, *args):
|
||||
|
@ -1 +1 @@
|
||||
from Console import ConsoleWidget
|
||||
from .Console import ConsoleWidget
|
@ -91,7 +91,7 @@ class Ui_Form(object):
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Console", "Console", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8))
|
||||
@ -101,4 +101,4 @@ class Ui_Form(object):
|
||||
self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
||||
from CmdInput import CmdInput
|
||||
from .CmdInput import CmdInput
|
||||
|
@ -11,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
<string>Console</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
@ -153,7 +153,7 @@
|
||||
<customwidget>
|
||||
<class>CmdInput</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>CmdInput</header>
|
||||
<header>.CmdInput</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
|
@ -2,6 +2,7 @@
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
from .Container import *
|
||||
from .DockDrop import *
|
||||
from .Dock import Dock
|
||||
import pyqtgraph.debug as debug
|
||||
import weakref
|
||||
|
||||
|
@ -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()
|
||||
|
@ -18,6 +18,7 @@ import sys, os
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(path, '..', '..', '..'))
|
||||
print sys.path
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
|
@ -43,5 +43,12 @@ While I consider this approach somewhat lazy, it is often the case that 'lazy' i
|
||||
Embedding widgets inside PyQt applications
|
||||
------------------------------------------
|
||||
|
||||
For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget <pyqtgraph.PlotWidget>`, :class:`ImageView <pyqtgraph.ImageView>`, :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and :class:`GraphicsView <pyqtgraph.GraphicsView>`. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality.
|
||||
For the serious application developer, all of the functionality in pyqtgraph is available via :ref:`widgets <api_widgets>` that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget <pyqtgraph.PlotWidget>`, :class:`ImageView <pyqtgraph.ImageView>`, :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and :class:`GraphicsView <pyqtgraph.GraphicsView>`. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality:
|
||||
|
||||
#. In Designer, create a QGraphicsView widget ("Graphics View" under the "Display Widgets" category).
|
||||
#. Right-click on the QGraphicsView and select "Promote To...".
|
||||
#. Under "Promoted class name", enter the class name you wish to use ("PlotWidget", "GraphicsLayoutWidget", etc).
|
||||
#. Under "Header file", enter "pyqtgraph".
|
||||
#. Click "Add", then click "Promote".
|
||||
|
||||
See the designer documentation for more information on promoting widgets.
|
||||
|
@ -1,22 +1,47 @@
|
||||
Line, Fill, and Color
|
||||
=====================
|
||||
|
||||
Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color.
|
||||
Qt relies on its QColor, QPen and QBrush classes for specifying line and fill styles for all of its drawing.
|
||||
Internally, pyqtgraph uses the same system but also allows many shorthand methods of specifying
|
||||
the same style options.
|
||||
|
||||
For these function arguments, the following values may be used:
|
||||
Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color.
|
||||
For most of these function arguments, the following values may be used:
|
||||
|
||||
* single-character string representing color (b, g, r, c, m, y, k, w)
|
||||
* (r, g, b) or (r, g, b, a) tuple
|
||||
* single greyscale value (0.0 - 1.0)
|
||||
* (index, maximum) tuple for automatically iterating through colors (see functions.intColor)
|
||||
* (index, maximum) tuple for automatically iterating through colors (see :func:`intColor <pyqtgraph.intColor>`)
|
||||
* QColor
|
||||
* QPen / QBrush where appropriate
|
||||
|
||||
Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes::
|
||||
Notably, more complex pens and brushes can be easily built using the
|
||||
:func:`mkPen() <pyqtgraph.mkPen>` / :func:`mkBrush() <pyqtgraph.mkBrush>` functions or with Qt's QPen and QBrush classes::
|
||||
|
||||
mkPen('y', width=3, style=QtCore.Qt.DashLine) ## Make a dashed yellow line 2px wide
|
||||
mkPen(0.5) ## solid grey line 1px wide
|
||||
mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line
|
||||
|
||||
See the Qt documentation for 'QPen' and 'PenStyle' for more options.
|
||||
Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class
|
||||
See the Qt documentation for 'QPen' and 'PenStyle' for more line-style options and 'QBrush' for more fill options.
|
||||
Colors can also be built using :func:`mkColor() <pyqtgraph.mkColor>`,
|
||||
:func:`intColor() <pyqtgraph.intColor>`, :func:`hsvColor() <pyqtgraph.hsvColor>`, or Qt's QColor class.
|
||||
|
||||
|
||||
Default Background and Foreground Colors
|
||||
----------------------------------------
|
||||
|
||||
By default, pyqtgraph uses a black background for its plots and grey for axes, text, and plot lines.
|
||||
These defaults can be changed using pyqtgraph.setConfigOption()::
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
||||
## Switch to using white background and black foreground
|
||||
pg.setConfigOption('background', 'w')
|
||||
pg.setConfigOption('foreground', 'k')
|
||||
|
||||
## The following plot has inverted colors
|
||||
pg.plot([1,4,2,3,5])
|
||||
|
||||
(Note that this must be set *before* creating any widgets)
|
||||
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
.. _api_widgets:
|
||||
|
||||
Pyqtgraph's Widgets
|
||||
===================
|
||||
|
||||
|
@ -4,41 +4,73 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
import pyqtgraph as pg
|
||||
import user
|
||||
import numpy as np
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
view = pg.GraphicsView()
|
||||
l = pg.GraphicsLayout(border=pg.mkPen(0, 0, 255))
|
||||
l = pg.GraphicsLayout(border=(100,100,100))
|
||||
view.setCentralItem(l)
|
||||
view.show()
|
||||
view.resize(800,600)
|
||||
|
||||
## Title at top
|
||||
text = """
|
||||
This example demonstrates the use of GraphicsLayout to arrange items in a grid.<br>
|
||||
The items added to the layout must be subclasses of QGraphicsWidget (this includes <br>
|
||||
PlotItem, ViewBox, LabelItem, and GrphicsLayout itself).
|
||||
"""
|
||||
l.addLabel(text, col=1, colspan=4)
|
||||
l.nextRow()
|
||||
|
||||
## Put vertical label on left side
|
||||
l.addLabel('Long Vertical Label', angle=-90, rowspan=3)
|
||||
|
||||
## Add 3 plots into the first row (automatic position)
|
||||
p1 = l.addPlot()
|
||||
p2 = l.addPlot()
|
||||
p3 = l.addPlot()
|
||||
p1 = l.addPlot(title="Plot 1")
|
||||
p2 = l.addPlot(title="Plot 2")
|
||||
vb = l.addViewBox(lockAspect=True)
|
||||
img = pg.ImageItem(np.random.normal(size=(100,100)))
|
||||
vb.addItem(img)
|
||||
vb.autoRange()
|
||||
|
||||
## Add a viewbox into the second row (automatic position)
|
||||
|
||||
## Add a sub-layout into the second row (automatic position)
|
||||
## The added item should avoid the first column, which is already filled
|
||||
l.nextRow()
|
||||
vb = l.addViewBox(colspan=3)
|
||||
l2 = l.addLayout(colspan=3, border=(50,0,0))
|
||||
l2.setContentsMargins(10, 10, 10, 10)
|
||||
l2.addLabel("Sub-layout: this layout demonstrates the use of shared axes and axis labels", colspan=3)
|
||||
l2.nextRow()
|
||||
l2.addLabel('Vertical Axis Label', angle=-90, rowspan=2)
|
||||
p21 = l2.addPlot()
|
||||
p22 = l2.addPlot()
|
||||
l2.nextRow()
|
||||
p23 = l2.addPlot()
|
||||
p24 = l2.addPlot()
|
||||
l2.nextRow()
|
||||
l2.addLabel("HorizontalAxisLabel", col=1, colspan=2)
|
||||
|
||||
## hide axes on some plots
|
||||
p21.hideAxis('bottom')
|
||||
p22.hideAxis('bottom')
|
||||
p22.hideAxis('left')
|
||||
p24.hideAxis('left')
|
||||
p21.hideButtons()
|
||||
p22.hideButtons()
|
||||
p23.hideButtons()
|
||||
p24.hideButtons()
|
||||
|
||||
|
||||
## Add 2 more plots into the third row (manual position)
|
||||
p4 = l.addPlot(row=2, col=0)
|
||||
p5 = l.addPlot(row=2, col=1, colspan=2)
|
||||
p4 = l.addPlot(row=3, col=1)
|
||||
p5 = l.addPlot(row=3, col=2, colspan=2)
|
||||
|
||||
|
||||
|
||||
## show some content
|
||||
## show some content in the plots
|
||||
p1.plot([1,3,2,4,3,5])
|
||||
p2.plot([1,3,2,4,3,5])
|
||||
p3.plot([1,3,2,4,3,5])
|
||||
p4.plot([1,3,2,4,3,5])
|
||||
p5.plot([1,3,2,4,3,5])
|
||||
|
||||
b = QtGui.QGraphicsRectItem(0, 0, 1, 1)
|
||||
b.setPen(pg.mkPen(255,255,0))
|
||||
vb.addItem(b)
|
||||
vb.setRange(QtCore.QRectF(-1, -1, 3, 3))
|
||||
|
||||
|
||||
|
||||
## Start Qt event loop unless running in interactive mode.
|
||||
|
19
examples/RemoteGraphicsView.py
Normal file
19
examples/RemoteGraphicsView.py
Normal 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_()
|
@ -11,11 +11,14 @@ examples = OrderedDict([
|
||||
('Command-line usage', 'CLIexample.py'),
|
||||
('Basic Plotting', 'Plotting.py'),
|
||||
('ImageView', 'ImageView.py'),
|
||||
('ParameterTree', '../parametertree'),
|
||||
('ParameterTree', 'parametertree.py'),
|
||||
('Crosshair / Mouse interaction', 'crosshair.py'),
|
||||
('Video speed test', 'VideoSpeedTest.py'),
|
||||
('Plot speed test', 'PlotSpeedTest.py'),
|
||||
('Data Slicing', 'DataSlicing.py'),
|
||||
('Plot Customization', 'customPlot.py'),
|
||||
('Dock widgets', 'dockarea.py'),
|
||||
('Console', 'ConsoleWidget.py'),
|
||||
('GraphicsItems', OrderedDict([
|
||||
('Scatter Plot', 'ScatterPlot.py'),
|
||||
#('PlotItem', 'PlotItem.py'),
|
||||
@ -46,7 +49,7 @@ examples = OrderedDict([
|
||||
#('VerticalLabel', '../widgets/VerticalLabel.py'),
|
||||
('JoystickButton', 'JoystickButton.py'),
|
||||
])),
|
||||
|
||||
|
||||
('GraphicsScene', 'GraphicsScene.py'),
|
||||
('Flowcharts', 'Flowchart.py'),
|
||||
#('Canvas', '../canvas'),
|
||||
@ -67,9 +70,9 @@ class ExampleLoader(QtGui.QMainWindow):
|
||||
self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples)
|
||||
self.ui.exampleTree.expandAll()
|
||||
|
||||
self.resize(900,500)
|
||||
self.resize(1000,500)
|
||||
self.show()
|
||||
self.ui.splitter.setSizes([150,750])
|
||||
self.ui.splitter.setSizes([250,750])
|
||||
self.ui.loadBtn.clicked.connect(self.loadFile)
|
||||
self.ui.exampleTree.currentItemChanged.connect(self.showFile)
|
||||
self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile)
|
||||
|
52
examples/customPlot.py
Normal file
52
examples/customPlot.py
Normal 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
116
examples/dockarea.py
Normal 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_()
|
@ -1,3 +1,3 @@
|
||||
## make this version of pyqtgraph importable before any others
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
|
||||
|
@ -1,38 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import initExample ## Add path to library (just for examples; you do not need this)
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph.multiprocess as mp
|
||||
from pyqtgraph.multiprocess.parallelizer import Parallelize #, Parallelizer
|
||||
import pyqtgraph as pg
|
||||
import time
|
||||
|
||||
print "\n=================\nParallelize"
|
||||
tasks = [1,2,4,8]
|
||||
results = [None] * len(tasks)
|
||||
size = 2000000
|
||||
|
||||
start = time.time()
|
||||
with Parallelize(enumerate(tasks), results=results, workers=1) as tasker:
|
||||
for i, x in tasker:
|
||||
print i, x
|
||||
tot = 0
|
||||
for j in xrange(size):
|
||||
tot += j * x
|
||||
results[i] = tot
|
||||
print results
|
||||
print "serial:", time.time() - start
|
||||
|
||||
start = time.time()
|
||||
with Parallelize(enumerate(tasks), results=results) as tasker:
|
||||
for i, x in tasker:
|
||||
print i, x
|
||||
tot = 0
|
||||
for j in xrange(size):
|
||||
tot += j * x
|
||||
results[i] = tot
|
||||
print results
|
||||
print "parallel:", time.time() - start
|
||||
|
||||
|
||||
|
||||
|
||||
|
63
examples/parallelize.py
Normal file
63
examples/parallelize.py
Normal 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
160
examples/parametertree.py
Normal 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_()
|
@ -51,16 +51,16 @@ class ExceptionHandler:
|
||||
def __call__(self, *args):
|
||||
## call original exception handler first (prints exception)
|
||||
global original_excepthook, callbacks, clear_tracebacks
|
||||
print "=====", time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())), "====="
|
||||
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
|
||||
ret = original_excepthook(*args)
|
||||
|
||||
for cb in callbacks:
|
||||
try:
|
||||
cb(*args)
|
||||
except:
|
||||
print " --------------------------------------------------------------"
|
||||
print " Error occurred during exception callback", cb
|
||||
print " --------------------------------------------------------------"
|
||||
print(" --------------------------------------------------------------")
|
||||
print(" Error occurred during exception callback %s" % str(cb))
|
||||
print(" --------------------------------------------------------------")
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
|
||||
|
||||
|
@ -71,7 +71,8 @@ class Flowchart(Node):
|
||||
if terminals is None:
|
||||
terminals = {}
|
||||
self.filePath = filePath
|
||||
Node.__init__(self, name) ## create node without terminals; we'll add these later
|
||||
Node.__init__(self, name, allowAddInput=True, allowAddOutput=True) ## create node without terminals; we'll add these later
|
||||
|
||||
|
||||
self.inputWasSet = False ## flag allows detection of changes in the absence of input change.
|
||||
self._nodes = {}
|
||||
@ -457,7 +458,7 @@ class Flowchart(Node):
|
||||
state = Node.saveState(self)
|
||||
state['nodes'] = []
|
||||
state['connects'] = []
|
||||
state['terminals'] = self.saveTerminals()
|
||||
#state['terminals'] = self.saveTerminals()
|
||||
|
||||
for name, node in self._nodes.items():
|
||||
cls = type(node)
|
||||
@ -470,7 +471,7 @@ class Flowchart(Node):
|
||||
conn = self.listConnections()
|
||||
for a, b in conn:
|
||||
state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name()))
|
||||
|
||||
|
||||
state['inputNode'] = self.inputNode.saveState()
|
||||
state['outputNode'] = self.outputNode.saveState()
|
||||
|
||||
@ -486,7 +487,8 @@ class Flowchart(Node):
|
||||
nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
|
||||
for n in nodes:
|
||||
if n['name'] in self._nodes:
|
||||
self._nodes[n['name']].moveBy(*n['pos'])
|
||||
#self._nodes[n['name']].graphicsItem().moveBy(*n['pos'])
|
||||
self._nodes[n['name']].restoreState(n['state'])
|
||||
continue
|
||||
try:
|
||||
node = self.createNode(n['class'], name=n['name'])
|
||||
@ -498,7 +500,7 @@ class Flowchart(Node):
|
||||
self.inputNode.restoreState(state.get('inputNode', {}))
|
||||
self.outputNode.restoreState(state.get('outputNode', {}))
|
||||
|
||||
self.restoreTerminals(state['terminals'])
|
||||
#self.restoreTerminals(state['terminals'])
|
||||
for n1, t1, n2, t2 in state['connects']:
|
||||
try:
|
||||
self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2])
|
||||
|
@ -1,16 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
#from PySide import QtCore, QtGui
|
||||
from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject
|
||||
import pyqtgraph.functions as fn
|
||||
from .Terminal import *
|
||||
from collections import OrderedDict
|
||||
from pyqtgraph.debug import *
|
||||
import numpy as np
|
||||
#from pyqtgraph.ObjectWorkaround import QObjectWorkaround
|
||||
from .eq import *
|
||||
|
||||
#TETRACYCLINE = True
|
||||
|
||||
def strDict(d):
|
||||
return dict([(str(k), v) for k, v in d.items()])
|
||||
@ -32,8 +29,8 @@ class Node(QtCore.QObject):
|
||||
self.bypassButton = None ## this will be set by the flowchart ctrl widget..
|
||||
self._graphicsItem = None
|
||||
self.terminals = OrderedDict()
|
||||
self._inputs = {}
|
||||
self._outputs = {}
|
||||
self._inputs = OrderedDict()
|
||||
self._outputs = OrderedDict()
|
||||
self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals
|
||||
self._allowAddOutput = allowAddOutput
|
||||
self._allowRemove = allowRemove
|
||||
@ -85,24 +82,16 @@ class Node(QtCore.QObject):
|
||||
def terminalRenamed(self, term, oldName):
|
||||
"""Called after a terminal has been renamed"""
|
||||
newName = term.name()
|
||||
#print "node", self, "handling rename..", newName, oldName
|
||||
for d in [self.terminals, self._inputs, self._outputs]:
|
||||
if oldName not in d:
|
||||
continue
|
||||
#print " got one"
|
||||
d[newName] = d[oldName]
|
||||
del d[oldName]
|
||||
|
||||
self.graphicsItem().updateTerminals()
|
||||
#self.emit(QtCore.SIGNAL('terminalRenamed'), term, oldName)
|
||||
self.sigTerminalRenamed.emit(term, oldName)
|
||||
|
||||
def addTerminal(self, name, **opts):
|
||||
#print "Node.addTerminal called. name:", name, "opts:", opts
|
||||
#global TETRACYCLINE
|
||||
#print "TETRACYCLINE: ", TETRACYCLINE
|
||||
#if TETRACYCLINE:
|
||||
#print "Creating Terminal..."
|
||||
name = self.nextTerminalName(name)
|
||||
term = Terminal(self, name, **opts)
|
||||
self.terminals[name] = term
|
||||
@ -278,12 +267,20 @@ class Node(QtCore.QObject):
|
||||
|
||||
def saveState(self):
|
||||
pos = self.graphicsItem().pos()
|
||||
return {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
||||
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
||||
termsEditable = self._allowAddInput | self._allowAddOutput
|
||||
for term in self._inputs.values() + self._outputs.values():
|
||||
termsEditable |= term._renamable | term._removable | term._multiable
|
||||
if termsEditable:
|
||||
state['terminals'] = self.saveTerminals()
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
pos = state.get('pos', (0,0))
|
||||
self.graphicsItem().setPos(*pos)
|
||||
self.bypass(state.get('bypass', False))
|
||||
if 'terminals' in state:
|
||||
self.restoreTerminals(state['terminals'])
|
||||
|
||||
def saveTerminals(self):
|
||||
terms = OrderedDict()
|
||||
@ -309,8 +306,8 @@ class Node(QtCore.QObject):
|
||||
for t in self.terminals.values():
|
||||
t.close()
|
||||
self.terminals = OrderedDict()
|
||||
self._inputs = {}
|
||||
self._outputs = {}
|
||||
self._inputs = OrderedDict()
|
||||
self._outputs = OrderedDict()
|
||||
|
||||
def close(self):
|
||||
"""Cleans up after the node--removes terminals, graphicsItem, widget"""
|
||||
@ -493,10 +490,6 @@ class NodeGraphicsItem(GraphicsObject):
|
||||
self.hovered = False
|
||||
self.update()
|
||||
|
||||
#def mouseReleaseEvent(self, ev):
|
||||
#ret = QtGui.QGraphicsItem.mouseReleaseEvent(self, ev)
|
||||
#return ret
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
|
||||
ev.accept()
|
||||
@ -513,13 +506,8 @@ class NodeGraphicsItem(GraphicsObject):
|
||||
return GraphicsObject.itemChange(self, change, val)
|
||||
|
||||
|
||||
#def contextMenuEvent(self, ev):
|
||||
#ev.accept()
|
||||
#self.menu.popup(ev.screenPos())
|
||||
|
||||
def getMenu(self):
|
||||
return self.menu
|
||||
|
||||
|
||||
def getContextMenus(self, event):
|
||||
return [self.menu]
|
||||
@ -548,25 +536,3 @@ class NodeGraphicsItem(GraphicsObject):
|
||||
def addOutputFromMenu(self): ## called when add output is clicked in context menu
|
||||
self.node.addOutput(renamable=True, removable=True, multiable=False)
|
||||
|
||||
#def menuTriggered(self, action):
|
||||
##print "node.menuTriggered called. action:", action
|
||||
#act = str(action.text())
|
||||
#if act == "Add input":
|
||||
#self.node.addInput()
|
||||
#self.updateActionMenu()
|
||||
#elif act == "Add output":
|
||||
#self.node.addOutput()
|
||||
#self.updateActionMenu()
|
||||
#elif act == "Remove node":
|
||||
#self.node.close()
|
||||
#else: ## only other option is to remove a terminal
|
||||
#self.node.removeTerminal(act)
|
||||
#self.terminalMenu.removeAction(action)
|
||||
|
||||
#def updateActionMenu(self):
|
||||
#for t in self.node.terminals:
|
||||
#if t not in [str(a.text()) for a in self.terminalMenu.actions()]:
|
||||
#self.terminalMenu.addAction(t)
|
||||
#for a in self.terminalMenu.actions():
|
||||
#if str(a.text()) not in self.node.terminals:
|
||||
#self.terminalMenu.removeAction(a)
|
||||
|
@ -45,7 +45,7 @@ class Terminal:
|
||||
self._value = {} ## dictionary of terminal:value pairs.
|
||||
else:
|
||||
self._value = None
|
||||
|
||||
|
||||
self.valueOk = None
|
||||
self.recolor()
|
||||
|
||||
@ -70,6 +70,8 @@ class Terminal:
|
||||
return
|
||||
self._value = val
|
||||
else:
|
||||
if not isinstance(self._value, dict):
|
||||
self._value = {}
|
||||
if val is not None:
|
||||
self._value.update(val)
|
||||
|
||||
@ -132,9 +134,14 @@ class Terminal:
|
||||
def isMultiValue(self):
|
||||
return self._multi
|
||||
|
||||
def setMultiValue(self, b):
|
||||
def setMultiValue(self, multi):
|
||||
"""Set whether this is a multi-value terminal."""
|
||||
self._multi = b
|
||||
self._multi = multi
|
||||
if not multi and len(self.inputTerminals()) > 1:
|
||||
self.disconnectAll()
|
||||
|
||||
for term in self.inputTerminals():
|
||||
self.inputChanged(term)
|
||||
|
||||
def isOutput(self):
|
||||
return self._io == 'out'
|
||||
@ -407,6 +414,8 @@ class TerminalGraphicsItem(GraphicsObject):
|
||||
multiAct = QtGui.QAction("Multi-value", self.menu)
|
||||
multiAct.setCheckable(True)
|
||||
multiAct.setChecked(self.term.isMultiValue())
|
||||
multiAct.setEnabled(self.term.isMultiable())
|
||||
|
||||
multiAct.triggered.connect(self.toggleMulti)
|
||||
self.menu.addAction(multiAct)
|
||||
self.menu.multiAct = multiAct
|
||||
|
@ -240,7 +240,7 @@ class EvalNode(Node):
|
||||
def saveState(self):
|
||||
state = Node.saveState(self)
|
||||
state['text'] = str(self.text.toPlainText())
|
||||
state['terminals'] = self.saveTerminals()
|
||||
#state['terminals'] = self.saveTerminals()
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
@ -282,7 +282,7 @@ class ColumnJoinNode(Node):
|
||||
|
||||
def addInput(self):
|
||||
#print "ColumnJoinNode.addInput called."
|
||||
term = Node.addInput(self, 'input', renamable=True)
|
||||
term = Node.addInput(self, 'input', renamable=True, removable=True)
|
||||
#print "Node.addInput returned. term:", term
|
||||
item = QtGui.QTreeWidgetItem([term.name()])
|
||||
item.term = term
|
||||
@ -322,16 +322,14 @@ class ColumnJoinNode(Node):
|
||||
|
||||
def restoreState(self, state):
|
||||
Node.restoreState(self, state)
|
||||
inputs = [inp.name() for inp in self.inputs()]
|
||||
inputs = self.inputs()
|
||||
order = [name for name in state['order'] if name in inputs]
|
||||
for name in inputs:
|
||||
if name not in state['order']:
|
||||
self.removeTerminal(name)
|
||||
for name in state['order']:
|
||||
if name not in inputs:
|
||||
Node.addInput(self, name, renamable=True)
|
||||
if name not in order:
|
||||
order.append(name)
|
||||
|
||||
self.tree.clear()
|
||||
for name in state['order']:
|
||||
for name in order:
|
||||
term = self[name]
|
||||
item = QtGui.QTreeWidgetItem([name])
|
||||
item.term = term
|
||||
|
@ -153,7 +153,7 @@ def denoise(data, radius=2, threshold=4):
|
||||
|
||||
r2 = radius * 2
|
||||
d1 = data.view(np.ndarray)
|
||||
d2 = data[radius:] - data[:-radius] #a derivative
|
||||
d2 = d1[radius:] - d1[:-radius] #a derivative
|
||||
#d3 = data[r2:] - data[:-r2]
|
||||
#d4 = d2 - d3
|
||||
stdev = d2.std()
|
||||
|
72
functions.py
72
functions.py
@ -5,6 +5,7 @@ Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
"""
|
||||
|
||||
from .python2_3 import asUnicode
|
||||
Colors = {
|
||||
'b': (0,0,255,255),
|
||||
'g': (0,255,0,255),
|
||||
@ -357,28 +358,36 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0)
|
||||
|
||||
|
||||
|
||||
def affineSlice(data, shape, origin, vectors, axes, **kargs):
|
||||
def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs):
|
||||
"""
|
||||
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.
|
||||
|
||||
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets.
|
||||
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this).
|
||||
|
||||
For a graphical interface to this function, see :func:`ROI.getArrayRegion`
|
||||
For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>`
|
||||
|
||||
============== ====================================================================================================
|
||||
Arguments:
|
||||
*data* (ndarray) the original dataset
|
||||
*shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape))
|
||||
*origin* the location in the original dataset that will become the origin of the sliced data.
|
||||
*vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same
|
||||
length as *axes*. If the vectors are not unit length, the result will be scaled relative to the
|
||||
original data. If the vectors are not orthogonal, the result will be sheared relative to the
|
||||
original data.
|
||||
*axes* The axes in the original dataset which correspond to the slice *vectors*
|
||||
*order* The order of spline interpolation. Default is 1 (linear). See scipy.ndimage.map_coordinates
|
||||
for more information.
|
||||
*returnCoords* If True, return a tuple (result, coords) where coords is the array of coordinates used to select
|
||||
values from the original dataset.
|
||||
*All extra keyword arguments are passed to scipy.ndimage.map_coordinates.*
|
||||
--------------------------------------------------------------------------------------------------------------------
|
||||
============== ====================================================================================================
|
||||
|
||||
| *data* (ndarray): the original dataset
|
||||
| *shape*: the shape of the slice to take (Note the return value may have more dimensions than len(shape))
|
||||
| *origin*: the location in the original dataset that will become the origin in the sliced data.
|
||||
| *vectors*: list of unit vectors which point in the direction of the slice axes
|
||||
Note the following must be true:
|
||||
|
||||
* each vector must have the same length as *axes*
|
||||
* If the vectors are not unit length, the result will be scaled.
|
||||
* If the vectors are not orthogonal, the result will be sheared.
|
||||
|
||||
*axes*: the axes in the original dataset which correspond to the slice *vectors*
|
||||
|
||||
All extra keyword arguments are passed to scipy.ndimage.map_coordinates
|
||||
| len(shape) == len(vectors)
|
||||
| len(origin) == len(axes) == len(vectors[i])
|
||||
|
||||
Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes
|
||||
|
||||
@ -391,10 +400,6 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
|
||||
|
||||
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
|
||||
|
||||
Note the following must be true:
|
||||
|
||||
| len(shape) == len(vectors)
|
||||
| len(origin) == len(axes) == len(vectors[0])
|
||||
"""
|
||||
|
||||
# sanity check
|
||||
@ -436,7 +441,7 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
|
||||
for inds in np.ndindex(*extraShape):
|
||||
ind = (Ellipsis,) + inds
|
||||
#print data[ind].shape, x.shape, output[ind].shape, output.shape
|
||||
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, **kargs)
|
||||
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs)
|
||||
|
||||
tr = list(range(output.ndim))
|
||||
trb = []
|
||||
@ -447,9 +452,18 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs):
|
||||
tr2 = tuple(trb+tr)
|
||||
|
||||
## Untranspose array before returning
|
||||
return output.transpose(tr2)
|
||||
|
||||
output = output.transpose(tr2)
|
||||
if returnCoords:
|
||||
return (output, x)
|
||||
else:
|
||||
return output
|
||||
|
||||
def transformToArray(tr):
|
||||
"""
|
||||
Given a QTransform, return a 3x3 numpy array.
|
||||
"""
|
||||
return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]])
|
||||
|
||||
def solve3DTransform(points1, points2):
|
||||
"""
|
||||
Find a 3D transformation matrix that maps points1 onto points2
|
||||
@ -1275,3 +1289,19 @@ def isosurface(data, level):
|
||||
|
||||
return facets
|
||||
|
||||
|
||||
|
||||
def invertQTransform(tr):
|
||||
"""Return a QTransform that is the inverse of *tr*.
|
||||
Rasises an exception if tr is not invertible.
|
||||
|
||||
Note that this function is preferred over QTransform.inverted() due to
|
||||
bugs in that method. (specifically, Qt has floating-point precision issues
|
||||
when determining whether a matrix is invertible)
|
||||
"""
|
||||
#return tr.inverted()[0]
|
||||
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
|
||||
inv = scipy.linalg.inv(arr)
|
||||
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph.python2_3 import asUnicode
|
||||
import numpy as np
|
||||
from pyqtgraph.Point import Point
|
||||
import pyqtgraph.debug as debug
|
||||
import weakref
|
||||
import pyqtgraph.functions as fn
|
||||
import pyqtgraph as pg
|
||||
from .GraphicsWidget import GraphicsWidget
|
||||
|
||||
__all__ = ['AxisItem']
|
||||
@ -65,8 +67,6 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
self.setRange(0, 1)
|
||||
|
||||
if pen is None:
|
||||
pen = QtGui.QPen(QtGui.QColor(100, 100, 100))
|
||||
self.setPen(pen)
|
||||
|
||||
self._linkedView = None
|
||||
@ -189,23 +189,31 @@ class AxisItem(GraphicsWidget):
|
||||
self.setMaximumWidth(w)
|
||||
self.setMinimumWidth(w)
|
||||
|
||||
def pen(self):
|
||||
if self._pen is None:
|
||||
return fn.mkPen(pg.getConfigOption('foreground'))
|
||||
return self._pen
|
||||
|
||||
def setPen(self, pen):
|
||||
self.pen = pen
|
||||
"""
|
||||
Set the pen used for drawing text, axes, ticks, and grid lines.
|
||||
if pen == None, the default will be used (see :func:`setConfigOption
|
||||
<pyqtgraph.setConfigOption>`)
|
||||
"""
|
||||
self._pen = pen
|
||||
self.picture = None
|
||||
self.update()
|
||||
|
||||
def setScale(self, scale=None):
|
||||
"""
|
||||
Set the value scaling for this axis.
|
||||
The scaling value 1) multiplies the values displayed along the axis
|
||||
and 2) changes the way units are displayed in the label.
|
||||
Set the value scaling for this axis. Values on the axis are multiplied
|
||||
by this scale factor before being displayed as text. By default,
|
||||
this scaling value is automatically determined based on the visible range
|
||||
and the axis units are updated to reflect the chosen scale factor.
|
||||
|
||||
For example: If the axis spans values from -0.1 to 0.1 and has units set
|
||||
to 'V' then a scale of 1000 would cause the axis to display values -100 to 100
|
||||
and the units would appear as 'mV'
|
||||
|
||||
If scale is None, then it will be determined automatically based on the current
|
||||
range displayed by the axis.
|
||||
"""
|
||||
if scale is None:
|
||||
#if self.drawLabel: ## If there is a label, then we are free to rescale the values
|
||||
@ -219,8 +227,10 @@ class AxisItem(GraphicsWidget):
|
||||
self.setLabel(unitPrefix=prefix)
|
||||
else:
|
||||
scale = 1.0
|
||||
|
||||
|
||||
else:
|
||||
self.setLabel(unitPrefix='')
|
||||
self.autoScale = False
|
||||
|
||||
if scale != self.scale:
|
||||
self.scale = scale
|
||||
self.setLabel()
|
||||
@ -354,6 +364,29 @@ class AxisItem(GraphicsWidget):
|
||||
(intervals[minorIndex], 0)
|
||||
]
|
||||
|
||||
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
|
||||
### Determine major/minor tick spacings which flank the optimal spacing.
|
||||
#intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit
|
||||
#minorIndex = 0
|
||||
#while intervals[minorIndex+1] <= optimalSpacing:
|
||||
#minorIndex += 1
|
||||
|
||||
### make sure we never see 5 and 2 at the same time
|
||||
#intIndexes = [
|
||||
#[0,1,3],
|
||||
#[0,2,3],
|
||||
#[2,3,4],
|
||||
#[3,4,6],
|
||||
#[3,5,6],
|
||||
#][minorIndex]
|
||||
|
||||
#return [
|
||||
#(intervals[intIndexes[2]], 0),
|
||||
#(intervals[intIndexes[1]], 0),
|
||||
#(intervals[intIndexes[0]], 0)
|
||||
#]
|
||||
|
||||
|
||||
|
||||
def tickValues(self, minVal, maxVal, size):
|
||||
"""
|
||||
@ -370,8 +403,6 @@ class AxisItem(GraphicsWidget):
|
||||
"""
|
||||
minVal, maxVal = sorted((minVal, maxVal))
|
||||
|
||||
if self.logMode:
|
||||
return self.logTickValues(minVal, maxVal, size)
|
||||
|
||||
ticks = []
|
||||
tickLevels = self.tickSpacing(minVal, maxVal, size)
|
||||
@ -388,21 +419,36 @@ class AxisItem(GraphicsWidget):
|
||||
## remove any ticks that were present in higher levels
|
||||
## we assume here that if the difference between a tick value and a previously seen tick value
|
||||
## is less than spacing/100, then they are 'equal' and we can ignore the new tick.
|
||||
values = filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values)
|
||||
values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
|
||||
allValues = np.concatenate([allValues, values])
|
||||
ticks.append((spacing, values))
|
||||
|
||||
if self.logMode:
|
||||
return self.logTickValues(minVal, maxVal, size, ticks)
|
||||
|
||||
return ticks
|
||||
|
||||
def logTickValues(self, minVal, maxVal, size):
|
||||
v1 = int(np.floor(minVal))
|
||||
v2 = int(np.ceil(maxVal))
|
||||
major = list(range(v1+1, v2))
|
||||
def logTickValues(self, minVal, maxVal, size, stdTicks):
|
||||
|
||||
minor = []
|
||||
for v in range(v1, v2):
|
||||
minor.extend(v + np.log10(np.arange(1, 10)))
|
||||
minor = [x for x in minor if x>minVal and x<maxVal]
|
||||
return [(1.0, major), (None, minor)]
|
||||
## start with the tick spacing given by tickValues().
|
||||
## Any level whose spacing is < 1 needs to be converted to log scale
|
||||
|
||||
ticks = []
|
||||
for (spacing, t) in stdTicks:
|
||||
if spacing >= 1.0:
|
||||
ticks.append((spacing, t))
|
||||
|
||||
if len(ticks) < 3:
|
||||
v1 = int(np.floor(minVal))
|
||||
v2 = int(np.ceil(maxVal))
|
||||
#major = list(range(v1+1, v2))
|
||||
|
||||
minor = []
|
||||
for v in range(v1, v2):
|
||||
minor.extend(v + np.log10(np.arange(1, 10)))
|
||||
minor = [x for x in minor if x>minVal and x<maxVal]
|
||||
ticks.append((None, minor))
|
||||
return ticks
|
||||
|
||||
def tickStrings(self, values, scale, spacing):
|
||||
"""Return the strings that should be placed next to ticks. This method is called
|
||||
@ -477,12 +523,14 @@ class AxisItem(GraphicsWidget):
|
||||
#print tickStart, tickStop, span
|
||||
|
||||
## draw long line along axis
|
||||
p.setPen(self.pen)
|
||||
p.setPen(self.pen())
|
||||
p.drawLine(*span)
|
||||
p.translate(0.5,0) ## resolves some damn pixel ambiguity
|
||||
|
||||
## determine size of this item in pixels
|
||||
points = list(map(self.mapToDevice, span))
|
||||
if None in points:
|
||||
return
|
||||
lengthInPixels = Point(points[1] - points[0]).length()
|
||||
if lengthInPixels == 0:
|
||||
return
|
||||
@ -513,6 +561,10 @@ class AxisItem(GraphicsWidget):
|
||||
else:
|
||||
xScale = bounds.width() / dif
|
||||
offset = self.range[0] * xScale
|
||||
|
||||
xRange = [x * xScale - offset for x in self.range]
|
||||
xMin = min(xRange)
|
||||
xMax = max(xRange)
|
||||
|
||||
prof.mark('init')
|
||||
|
||||
@ -521,6 +573,7 @@ class AxisItem(GraphicsWidget):
|
||||
## draw ticks
|
||||
## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
|
||||
## draw three different intervals, long ticks first
|
||||
|
||||
for i in range(len(tickLevels)):
|
||||
tickPositions.append([])
|
||||
ticks = tickLevels[i][1]
|
||||
@ -530,19 +583,28 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
lineAlpha = 255 / (i+1)
|
||||
if self.grid is not False:
|
||||
lineAlpha = self.grid
|
||||
lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.)
|
||||
|
||||
for v in ticks:
|
||||
## determine actual position to draw this tick
|
||||
x = (v * xScale) - offset
|
||||
if x < xMin or x > xMax: ## last check to make sure no out-of-bounds ticks are drawn
|
||||
tickPositions[i].append(None)
|
||||
continue
|
||||
tickPositions[i].append(x)
|
||||
|
||||
p1 = [x, x]
|
||||
p2 = [x, x]
|
||||
p1[axis] = tickStart
|
||||
p2[axis] = tickStop
|
||||
if self.grid is False:
|
||||
p2[axis] += tickLength*tickDir
|
||||
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha)))
|
||||
tickPen = self.pen()
|
||||
color = tickPen.color()
|
||||
color.setAlpha(lineAlpha)
|
||||
tickPen.setColor(color)
|
||||
p.setPen(tickPen)
|
||||
p.drawLine(Point(p1), Point(p2))
|
||||
tickPositions[i].append(x)
|
||||
prof.mark('draw ticks')
|
||||
|
||||
## Draw text until there is no more room (or no more text)
|
||||
@ -557,10 +619,15 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
if len(strings) == 0:
|
||||
continue
|
||||
|
||||
## ignore strings belonging to ticks that were previously ignored
|
||||
for j in range(len(strings)):
|
||||
if tickPositions[i][j] is None:
|
||||
strings[j] = None
|
||||
|
||||
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None])
|
||||
if i > 0: ## always draw top level
|
||||
## measure all text, make sure there's enough room
|
||||
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings])
|
||||
if axis == 0:
|
||||
textSize = np.sum([r.height() for r in textRects])
|
||||
else:
|
||||
@ -570,11 +637,12 @@ class AxisItem(GraphicsWidget):
|
||||
textFillRatio = float(textSize) / lengthInPixels
|
||||
if textFillRatio > 0.7:
|
||||
break
|
||||
|
||||
#spacing, values = tickLevels[best]
|
||||
#strings = self.tickStrings(values, self.scale, spacing)
|
||||
for j in range(len(strings)):
|
||||
vstr = strings[j]
|
||||
if vstr is None:## this tick was ignored because it is out of bounds
|
||||
continue
|
||||
x = tickPositions[i][j]
|
||||
textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
|
||||
height = textRect.height()
|
||||
@ -592,7 +660,7 @@ class AxisItem(GraphicsWidget):
|
||||
textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
|
||||
rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height)
|
||||
|
||||
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150)))
|
||||
p.setPen(self.pen())
|
||||
p.drawText(rect, textFlags, vstr)
|
||||
prof.mark('draw text')
|
||||
prof.finish()
|
||||
|
@ -1,4 +1,5 @@
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph.python2_3 import sortList
|
||||
import pyqtgraph.functions as fn
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from .GraphicsWidget import GraphicsWidget
|
||||
@ -175,7 +176,7 @@ class TickSliderItem(GraphicsWidget):
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
wlen = max(40, self.widgetLength())
|
||||
self.setLength(wlen-self.tickSize)
|
||||
self.setLength(wlen-self.tickSize-2)
|
||||
self.setOrientation(self.orientation)
|
||||
#bounds = self.scene().itemsBoundingRect()
|
||||
#bounds.setLeft(min(-self.tickSize*0.5, bounds.left()))
|
||||
@ -186,7 +187,7 @@ class TickSliderItem(GraphicsWidget):
|
||||
def setLength(self, newLen):
|
||||
#private
|
||||
for t, x in list(self.ticks.items()):
|
||||
t.setPos(x * newLen, t.pos().y())
|
||||
t.setPos(x * newLen + 1, t.pos().y())
|
||||
self.length = float(newLen)
|
||||
|
||||
#def mousePressEvent(self, ev):
|
||||
@ -491,8 +492,8 @@ class GradientEditorItem(TickSliderItem):
|
||||
def setLength(self, newLen):
|
||||
#private (but maybe public)
|
||||
TickSliderItem.setLength(self, newLen)
|
||||
self.backgroundRect.setRect(0, -self.rectSize, newLen, self.rectSize)
|
||||
self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize)
|
||||
self.backgroundRect.setRect(1, -self.rectSize, newLen, self.rectSize)
|
||||
self.gradRect.setRect(1, -self.rectSize, newLen, self.rectSize)
|
||||
self.updateGradient()
|
||||
|
||||
def currentColorChanged(self, color):
|
||||
|
@ -1,6 +1,7 @@
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph.GraphicsScene import GraphicsScene
|
||||
from pyqtgraph.Point import Point
|
||||
import pyqtgraph.functions as fn
|
||||
import weakref
|
||||
|
||||
class GraphicsItem(object):
|
||||
@ -149,16 +150,32 @@ class GraphicsItem(object):
|
||||
"""Return vectors in local coordinates representing the width and height of a view pixel.
|
||||
If direction is specified, then return vectors parallel and orthogonal to it.
|
||||
|
||||
Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)."""
|
||||
Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)
|
||||
or if pixel size is below floating-point precision limit.
|
||||
"""
|
||||
|
||||
dt = self.deviceTransform()
|
||||
if dt is None:
|
||||
return None, None
|
||||
|
||||
if direction is None:
|
||||
direction = Point(1, 0)
|
||||
direction = Point(1, 0)
|
||||
if direction.manhattanLength() == 0:
|
||||
raise Exception("Cannot compute pixel length for 0-length vector.")
|
||||
|
||||
## attempt to re-scale direction vector to fit within the precision of the coordinate system
|
||||
if direction.x() == 0:
|
||||
r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))
|
||||
elif direction.y() == 0:
|
||||
r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))
|
||||
else:
|
||||
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
|
||||
direction = direction * r
|
||||
|
||||
viewDir = Point(dt.map(direction) - dt.map(Point(0,0)))
|
||||
if viewDir.manhattanLength() == 0:
|
||||
return None, None ## pixel size cannot be represented on this scale
|
||||
|
||||
orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
|
||||
|
||||
try:
|
||||
@ -168,7 +185,7 @@ class GraphicsItem(object):
|
||||
raise Exception("Invalid direction %s" %direction)
|
||||
|
||||
|
||||
dti = dt.inverted()[0]
|
||||
dti = fn.invertQTransform(dt)
|
||||
return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
|
||||
|
||||
#vt = self.deviceTransform()
|
||||
@ -194,23 +211,26 @@ class GraphicsItem(object):
|
||||
|
||||
|
||||
def pixelSize(self):
|
||||
## deprecated
|
||||
v = self.pixelVectors()
|
||||
if v == (None, None):
|
||||
return None, None
|
||||
return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5
|
||||
|
||||
def pixelWidth(self):
|
||||
## deprecated
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return 0
|
||||
vt = vt.inverted()[0]
|
||||
vt = fn.invertQTransform(vt)
|
||||
return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length()
|
||||
|
||||
def pixelHeight(self):
|
||||
## deprecated
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return 0
|
||||
vt = vt.inverted()[0]
|
||||
vt = fn.invertQTransform(vt)
|
||||
return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
|
||||
|
||||
|
||||
@ -232,7 +252,7 @@ class GraphicsItem(object):
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
vt = vt.inverted()[0]
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.map(obj)
|
||||
|
||||
def mapRectToDevice(self, rect):
|
||||
@ -253,7 +273,7 @@ class GraphicsItem(object):
|
||||
vt = self.deviceTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
vt = vt.inverted()[0]
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.mapRect(rect)
|
||||
|
||||
def mapToView(self, obj):
|
||||
@ -272,14 +292,14 @@ class GraphicsItem(object):
|
||||
vt = self.viewTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
vt = vt.inverted()[0]
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.map(obj)
|
||||
|
||||
def mapRectFromView(self, obj):
|
||||
vt = self.viewTransform()
|
||||
if vt is None:
|
||||
return None
|
||||
vt = vt.inverted()[0]
|
||||
vt = fn.invertQTransform(vt)
|
||||
return vt.mapRect(obj)
|
||||
|
||||
def pos(self):
|
||||
|
@ -21,21 +21,29 @@ class GraphicsLayout(GraphicsWidget):
|
||||
self.border = border
|
||||
self.layout = QtGui.QGraphicsGridLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.items = {}
|
||||
self.rows = {}
|
||||
self.items = {} ## item: [(row, col), (row, col), ...] lists all cells occupied by the item
|
||||
self.rows = {} ## row: {col1: item1, col2: item2, ...} maps cell location to item
|
||||
self.currentRow = 0
|
||||
self.currentCol = 0
|
||||
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
|
||||
|
||||
#def resizeEvent(self, ev):
|
||||
#ret = GraphicsWidget.resizeEvent(self, ev)
|
||||
#print self.pos(), self.mapToDevice(self.rect().topLeft())
|
||||
#return ret
|
||||
|
||||
def nextRow(self):
|
||||
"""Advance to next row for automatic item placement"""
|
||||
self.currentRow += 1
|
||||
self.currentCol = 0
|
||||
self.currentCol = -1
|
||||
self.nextColumn()
|
||||
|
||||
def nextColumn(self, colspan=1):
|
||||
"""Advance to next column, while returning the current column number
|
||||
def nextColumn(self):
|
||||
"""Advance to next available column
|
||||
(generally only for internal use--called by addItem)"""
|
||||
self.currentCol += colspan
|
||||
return self.currentCol-colspan
|
||||
self.currentCol += 1
|
||||
while self.getItem(self.currentRow, self.currentCol) is not None:
|
||||
self.currentCol += 1
|
||||
|
||||
def nextCol(self, *args, **kargs):
|
||||
"""Alias of nextColumn"""
|
||||
@ -66,6 +74,8 @@ class GraphicsLayout(GraphicsWidget):
|
||||
Create a LabelItem with *text* and place it in the next available cell (or in the cell specified)
|
||||
All extra keyword arguments are passed to :func:`LabelItem.__init__ <pyqtgraph.LabelItem.__init__>`
|
||||
Returns the created item.
|
||||
|
||||
To create a vertical label, use *angle*=-90
|
||||
"""
|
||||
text = LabelItem(text, **kargs)
|
||||
self.addItem(text, row, col, rowspan, colspan)
|
||||
@ -89,18 +99,24 @@ class GraphicsLayout(GraphicsWidget):
|
||||
if row is None:
|
||||
row = self.currentRow
|
||||
if col is None:
|
||||
col = self.nextCol(colspan)
|
||||
col = self.currentCol
|
||||
|
||||
if row not in self.rows:
|
||||
self.rows[row] = {}
|
||||
self.rows[row][col] = item
|
||||
self.items[item] = (row, col)
|
||||
self.items[item] = []
|
||||
for i in range(rowspan):
|
||||
for j in range(colspan):
|
||||
row2 = row + i
|
||||
col2 = col + j
|
||||
if row2 not in self.rows:
|
||||
self.rows[row2] = {}
|
||||
self.rows[row2][col2] = item
|
||||
self.items[item].append((row2, col2))
|
||||
|
||||
self.layout.addItem(item, row, col, rowspan, colspan)
|
||||
self.nextColumn()
|
||||
|
||||
def getItem(self, row, col):
|
||||
"""Return the item in (*row*, *col*)"""
|
||||
return self.row[row][col]
|
||||
"""Return the item in (*row*, *col*). If the cell is empty, return None."""
|
||||
return self.rows.get(row, {}).get(col, None)
|
||||
|
||||
def boundingRect(self):
|
||||
return self.rect()
|
||||
@ -124,9 +140,10 @@ class GraphicsLayout(GraphicsWidget):
|
||||
ind = self.itemIndex(item)
|
||||
self.layout.removeAt(ind)
|
||||
self.scene().removeItem(item)
|
||||
r,c = self.items[item]
|
||||
|
||||
for r,c in self.items[item]:
|
||||
del self.rows[r][c]
|
||||
del self.items[item]
|
||||
del self.rows[r][c]
|
||||
self.update()
|
||||
|
||||
def clear(self):
|
||||
|
@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore
|
||||
from .UIGraphicsItem import *
|
||||
import numpy as np
|
||||
from pyqtgraph.Point import Point
|
||||
import pyqtgraph.functions as fn
|
||||
|
||||
__all__ = ['GridItem']
|
||||
class GridItem(UIGraphicsItem):
|
||||
@ -47,7 +48,7 @@ class GridItem(UIGraphicsItem):
|
||||
p = QtGui.QPainter()
|
||||
p.begin(self.picture)
|
||||
|
||||
dt = self.viewTransform().inverted()[0]
|
||||
dt = fn.invertQTransform(self.viewTransform())
|
||||
vr = self.getViewWidget().rect()
|
||||
unit = self.pixelWidth(), self.pixelHeight()
|
||||
dim = [vr.width(), vr.height()]
|
||||
@ -112,7 +113,7 @@ class GridItem(UIGraphicsItem):
|
||||
texts.append((QtCore.QPointF(x, y), "%g"%p1[ax]))
|
||||
tr = self.deviceTransform()
|
||||
#tr.scale(1.5, 1.5)
|
||||
p.setWorldTransform(tr.inverted()[0])
|
||||
p.setWorldTransform(fn.invertQTransform(tr))
|
||||
for t in texts:
|
||||
x = tr.map(t[0]) + Point(0.5, 0.5)
|
||||
p.drawText(x, t[1])
|
||||
|
@ -50,7 +50,7 @@ class HistogramLUTItem(GraphicsWidget):
|
||||
self.layout.setSpacing(0)
|
||||
self.vb = ViewBox()
|
||||
self.vb.setMaximumWidth(152)
|
||||
self.vb.setMinimumWidth(52)
|
||||
self.vb.setMinimumWidth(45)
|
||||
self.vb.setMouseEnabled(x=False, y=True)
|
||||
self.gradient = GradientEditorItem()
|
||||
self.gradient.setOrientation('right')
|
||||
|
@ -1,5 +1,6 @@
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
import pyqtgraph.functions as fn
|
||||
import pyqtgraph as pg
|
||||
from .GraphicsWidget import GraphicsWidget
|
||||
|
||||
|
||||
@ -18,14 +19,13 @@ class LabelItem(GraphicsWidget):
|
||||
GraphicsWidget.__init__(self, parent)
|
||||
self.item = QtGui.QGraphicsTextItem(self)
|
||||
self.opts = {
|
||||
'color': 'CCC',
|
||||
'color': None,
|
||||
'justify': 'center'
|
||||
}
|
||||
self.opts.update(args)
|
||||
self.sizeHint = {}
|
||||
self._sizeHint = {}
|
||||
self.setText(text)
|
||||
self.setAngle(angle)
|
||||
|
||||
|
||||
def setAttr(self, attr, value):
|
||||
"""Set default text properties. See setText() for accepted parameters."""
|
||||
@ -44,15 +44,17 @@ class LabelItem(GraphicsWidget):
|
||||
==================== ==============================
|
||||
"""
|
||||
self.text = text
|
||||
opts = self.opts.copy()
|
||||
opts = self.opts
|
||||
for k in args:
|
||||
opts[k] = args[k]
|
||||
|
||||
optlist = []
|
||||
if 'color' in opts:
|
||||
if isinstance(opts['color'], QtGui.QColor):
|
||||
opts['color'] = fn.colorStr(opts['color'])[:6]
|
||||
optlist.append('color: #' + opts['color'])
|
||||
|
||||
color = self.opts['color']
|
||||
if color is None:
|
||||
color = pg.getConfigOption('foreground')
|
||||
color = fn.mkColor(color)
|
||||
optlist.append('color: #' + fn.colorStr(color)[:6])
|
||||
if 'size' in opts:
|
||||
optlist.append('font-size: ' + opts['size'])
|
||||
if 'bold' in opts and opts['bold'] in [True, False]:
|
||||
@ -64,7 +66,7 @@ class LabelItem(GraphicsWidget):
|
||||
self.item.setHtml(full)
|
||||
self.updateMin()
|
||||
self.resizeEvent(None)
|
||||
self.update()
|
||||
self.updateGeometry()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
#c1 = self.boundingRect().center()
|
||||
@ -72,16 +74,35 @@ class LabelItem(GraphicsWidget):
|
||||
#dif = c1 - c2
|
||||
#self.item.moveBy(dif.x(), dif.y())
|
||||
#print c1, c2, dif, self.item.pos()
|
||||
self.item.setPos(0,0)
|
||||
bounds = self.itemRect()
|
||||
left = self.mapFromItem(self.item, QtCore.QPointF(0,0)) - self.mapFromItem(self.item, QtCore.QPointF(1,0))
|
||||
rect = self.rect()
|
||||
|
||||
if self.opts['justify'] == 'left':
|
||||
self.item.setPos(0,0)
|
||||
if left.x() != 0:
|
||||
bounds.moveLeft(rect.left())
|
||||
if left.y() < 0:
|
||||
bounds.moveTop(rect.top())
|
||||
elif left.y() > 0:
|
||||
bounds.moveBottom(rect.bottom())
|
||||
|
||||
elif self.opts['justify'] == 'center':
|
||||
bounds = self.item.mapRectToParent(self.item.boundingRect())
|
||||
self.item.setPos(self.width()/2. - bounds.width()/2., 0)
|
||||
bounds.moveCenter(rect.center())
|
||||
#bounds = self.itemRect()
|
||||
#self.item.setPos(self.width()/2. - bounds.width()/2., 0)
|
||||
elif self.opts['justify'] == 'right':
|
||||
bounds = self.item.mapRectToParent(self.item.boundingRect())
|
||||
self.item.setPos(self.width() - bounds.width(), 0)
|
||||
#if self.width() > 0:
|
||||
#self.item.setTextWidth(self.width())
|
||||
if left.x() != 0:
|
||||
bounds.moveRight(rect.right())
|
||||
if left.y() < 0:
|
||||
bounds.moveBottom(rect.bottom())
|
||||
elif left.y() > 0:
|
||||
bounds.moveTop(rect.top())
|
||||
#bounds = self.itemRect()
|
||||
#self.item.setPos(self.width() - bounds.width(), 0)
|
||||
|
||||
self.item.setPos(bounds.topLeft() - self.itemRect().topLeft())
|
||||
self.updateMin()
|
||||
|
||||
def setAngle(self, angle):
|
||||
self.angle = angle
|
||||
@ -89,27 +110,31 @@ class LabelItem(GraphicsWidget):
|
||||
self.item.rotate(angle)
|
||||
self.updateMin()
|
||||
|
||||
|
||||
def updateMin(self):
|
||||
bounds = self.item.mapRectToParent(self.item.boundingRect())
|
||||
bounds = self.itemRect()
|
||||
self.setMinimumWidth(bounds.width())
|
||||
self.setMinimumHeight(bounds.height())
|
||||
|
||||
self.sizeHint = {
|
||||
self._sizeHint = {
|
||||
QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()),
|
||||
QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()),
|
||||
QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2),
|
||||
QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this?
|
||||
}
|
||||
|
||||
self.update()
|
||||
self.updateGeometry()
|
||||
|
||||
def sizeHint(self, hint, constraint):
|
||||
if hint not in self.sizeHint:
|
||||
if hint not in self._sizeHint:
|
||||
return QtCore.QSizeF(0, 0)
|
||||
return QtCore.QSizeF(*self.sizeHint[hint])
|
||||
return QtCore.QSizeF(*self._sizeHint[hint])
|
||||
|
||||
def itemRect(self):
|
||||
return self.item.mapRectToParent(self.item.boundingRect())
|
||||
|
||||
#def paint(self, p, *args):
|
||||
#p.setPen(fn.mkPen('r'))
|
||||
#p.drawRect(self.rect())
|
||||
#p.drawRect(self.item.boundingRect())
|
||||
#p.setPen(fn.mkPen('g'))
|
||||
#p.drawRect(self.itemRect())
|
||||
|
||||
|
@ -33,17 +33,12 @@ from .. AxisItem import AxisItem
|
||||
from .. LabelItem import LabelItem
|
||||
from .. GraphicsWidget import GraphicsWidget
|
||||
from .. ButtonItem import ButtonItem
|
||||
#from .. GraphicsLayout import GraphicsLayout
|
||||
from pyqtgraph.WidgetGroup import WidgetGroup
|
||||
import collections
|
||||
|
||||
__all__ = ['PlotItem']
|
||||
|
||||
#try:
|
||||
#from WidgetGroup import *
|
||||
#HAVE_WIDGETGROUP = True
|
||||
#except:
|
||||
#HAVE_WIDGETGROUP = False
|
||||
|
||||
try:
|
||||
from metaarray import *
|
||||
HAVE_METAARRAY = True
|
||||
@ -78,6 +73,7 @@ class PlotItem(GraphicsWidget):
|
||||
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
|
||||
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
|
||||
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
|
||||
:func:`invertY <pyqtgraph.ViewBox.invertY>`,
|
||||
:func:`register <pyqtgraph.ViewBox.register>`,
|
||||
:func:`unregister <pyqtgraph.ViewBox.unregister>`
|
||||
|
||||
@ -99,26 +95,28 @@ class PlotItem(GraphicsWidget):
|
||||
lastFileDir = None
|
||||
managers = {}
|
||||
|
||||
def __init__(self, parent=None, name=None, labels=None, title=None, **kargs):
|
||||
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs):
|
||||
"""
|
||||
Create a new PlotItem. All arguments are optional.
|
||||
Any extra keyword arguments are passed to PlotItem.plot().
|
||||
|
||||
============= ==========================================================================================
|
||||
============== ==========================================================================================
|
||||
**Arguments**
|
||||
*title* Title to display at the top of the item. Html is allowed.
|
||||
*labels* A dictionary specifying the axis labels to display::
|
||||
*title* Title to display at the top of the item. Html is allowed.
|
||||
*labels* A dictionary specifying the axis labels to display::
|
||||
|
||||
{'left': (args), 'bottom': (args), ...}
|
||||
{'left': (args), 'bottom': (args), ...}
|
||||
|
||||
The name of each axis and the corresponding arguments are passed to
|
||||
:func:`PlotItem.setLabel() <pyqtgraph.PlotItem.setLabel>`
|
||||
Optionally, PlotItem my also be initialized with the keyword arguments left,
|
||||
right, top, or bottom to achieve the same effect.
|
||||
*name* Registers a name for this view so that others may link to it
|
||||
============= ==========================================================================================
|
||||
|
||||
|
||||
The name of each axis and the corresponding arguments are passed to
|
||||
:func:`PlotItem.setLabel() <pyqtgraph.PlotItem.setLabel>`
|
||||
Optionally, PlotItem my also be initialized with the keyword arguments left,
|
||||
right, top, or bottom to achieve the same effect.
|
||||
*name* Registers a name for this view so that others may link to it
|
||||
*viewBox* If specified, the PlotItem will be constructed with this as its ViewBox.
|
||||
*axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items
|
||||
for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top')
|
||||
and the values must be instances of AxisItem (or at least compatible with AxisItem).
|
||||
============== ==========================================================================================
|
||||
"""
|
||||
|
||||
GraphicsWidget.__init__(self, parent)
|
||||
@ -127,8 +125,6 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
## Set up control buttons
|
||||
path = os.path.dirname(__file__)
|
||||
#self.ctrlBtn = ButtonItem(os.path.join(path, 'ctrl.png'), 14, self)
|
||||
#self.ctrlBtn.clicked.connect(self.ctrlBtnClicked)
|
||||
self.autoImageFile = os.path.join(path, 'auto.png')
|
||||
self.lockImageFile = os.path.join(path, 'lock.png')
|
||||
self.autoBtn = ButtonItem(self.autoImageFile, 14, self)
|
||||
@ -141,32 +137,33 @@ class PlotItem(GraphicsWidget):
|
||||
self.layout.setHorizontalSpacing(0)
|
||||
self.layout.setVerticalSpacing(0)
|
||||
|
||||
self.vb = ViewBox(name=name)
|
||||
if viewBox is None:
|
||||
viewBox = ViewBox()
|
||||
self.vb = viewBox
|
||||
self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus
|
||||
|
||||
if name is not None:
|
||||
self.vb.register(name)
|
||||
self.vb.sigRangeChanged.connect(self.sigRangeChanged)
|
||||
self.vb.sigXRangeChanged.connect(self.sigXRangeChanged)
|
||||
self.vb.sigYRangeChanged.connect(self.sigYRangeChanged)
|
||||
#self.vb.sigRangeChangedManually.connect(self.enableManualScale)
|
||||
#self.vb.sigRangeChanged.connect(self.viewRangeChanged)
|
||||
|
||||
self.layout.addItem(self.vb, 2, 1)
|
||||
self.alpha = 1.0
|
||||
self.autoAlpha = True
|
||||
self.spectrumMode = False
|
||||
|
||||
#self.autoScale = [True, True]
|
||||
|
||||
## Create and place scale items
|
||||
self.scales = {
|
||||
'top': {'item': AxisItem(orientation='top', linkView=self.vb), 'pos': (1, 1)},
|
||||
'bottom': {'item': AxisItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)},
|
||||
'left': {'item': AxisItem(orientation='left', linkView=self.vb), 'pos': (2, 0)},
|
||||
'right': {'item': AxisItem(orientation='right', linkView=self.vb), 'pos': (2, 2)}
|
||||
}
|
||||
for k in self.scales:
|
||||
item = self.scales[k]['item']
|
||||
self.layout.addItem(item, *self.scales[k]['pos'])
|
||||
item.setZValue(-1000)
|
||||
item.setFlag(item.ItemNegativeZStacksBehindParent)
|
||||
## Create and place axis items
|
||||
if axisItems is None:
|
||||
axisItems = {}
|
||||
self.axes = {}
|
||||
for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
|
||||
axis = axisItems.get(k, AxisItem(orientation=k))
|
||||
axis.linkToView(self.vb)
|
||||
self.axes[k] = {'item': axis, 'pos': pos}
|
||||
self.layout.addItem(axis, *pos)
|
||||
axis.setZValue(-1000)
|
||||
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
|
||||
|
||||
self.titleLabel = LabelItem('', size='11pt')
|
||||
self.layout.addItem(self.titleLabel, 0, 1)
|
||||
@ -192,8 +189,7 @@ class PlotItem(GraphicsWidget):
|
||||
for m in [
|
||||
'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible',
|
||||
'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled',
|
||||
'enableAutoRange', 'disableAutoRange', 'setAspectLocked',
|
||||
'setMenuEnabled', 'menuEnabled',
|
||||
'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY',
|
||||
'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well.
|
||||
setattr(self, m, getattr(self.vb, m))
|
||||
|
||||
@ -233,45 +229,12 @@ class PlotItem(GraphicsWidget):
|
||||
self.subMenus.append(sm)
|
||||
self.ctrlMenu.addMenu(sm)
|
||||
|
||||
## exporting is handled by GraphicsScene now
|
||||
#exportOpts = collections.OrderedDict([
|
||||
#('SVG - Full Plot', self.saveSvgClicked),
|
||||
#('SVG - Curves Only', self.saveSvgCurvesClicked),
|
||||
#('Image', self.saveImgClicked),
|
||||
#('CSV', self.saveCsvClicked),
|
||||
#])
|
||||
|
||||
#self.vb.menu.setExportMethods(exportOpts)
|
||||
|
||||
|
||||
#if HAVE_WIDGETGROUP:
|
||||
self.stateGroup = WidgetGroup()
|
||||
for name, w in menuItems:
|
||||
self.stateGroup.autoAdd(w)
|
||||
|
||||
self.fileDialog = None
|
||||
|
||||
#self.xLinkPlot = None
|
||||
#self.yLinkPlot = None
|
||||
#self.linksBlocked = False
|
||||
|
||||
#self.setAcceptHoverEvents(True)
|
||||
|
||||
## Connect control widgets
|
||||
#c.xMinText.editingFinished.connect(self.setManualXScale)
|
||||
#c.xMaxText.editingFinished.connect(self.setManualXScale)
|
||||
#c.yMinText.editingFinished.connect(self.setManualYScale)
|
||||
#c.yMaxText.editingFinished.connect(self.setManualYScale)
|
||||
|
||||
#c.xManualRadio.clicked.connect(lambda: self.updateXScale())
|
||||
#c.yManualRadio.clicked.connect(lambda: self.updateYScale())
|
||||
|
||||
#c.xAutoRadio.clicked.connect(self.updateXScale)
|
||||
#c.yAutoRadio.clicked.connect(self.updateYScale)
|
||||
|
||||
#c.xAutoPercentSpin.valueChanged.connect(self.replot)
|
||||
#c.yAutoPercentSpin.valueChanged.connect(self.replot)
|
||||
|
||||
c.alphaGroup.toggled.connect(self.updateAlpha)
|
||||
c.alphaSlider.valueChanged.connect(self.updateAlpha)
|
||||
c.autoAlphaCheck.toggled.connect(self.updateAlpha)
|
||||
@ -283,13 +246,6 @@ class PlotItem(GraphicsWidget):
|
||||
c.fftCheck.toggled.connect(self.updateSpectrumMode)
|
||||
c.logXCheck.toggled.connect(self.updateLogMode)
|
||||
c.logYCheck.toggled.connect(self.updateLogMode)
|
||||
#c.saveSvgBtn.clicked.connect(self.saveSvgClicked)
|
||||
#c.saveSvgCurvesBtn.clicked.connect(self.saveSvgCurvesClicked)
|
||||
#c.saveImgBtn.clicked.connect(self.saveImgClicked)
|
||||
#c.saveCsvBtn.clicked.connect(self.saveCsvClicked)
|
||||
|
||||
#self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged)
|
||||
#self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged)
|
||||
|
||||
c.downsampleSpin.valueChanged.connect(self.updateDownsampling)
|
||||
|
||||
@ -298,24 +254,15 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation)
|
||||
self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation)
|
||||
#c.xMouseCheck.toggled.connect(self.mouseCheckChanged)
|
||||
#c.yMouseCheck.toggled.connect(self.mouseCheckChanged)
|
||||
|
||||
#self.xLinkPlot = None
|
||||
#self.yLinkPlot = None
|
||||
#self.linksBlocked = False
|
||||
self.manager = None
|
||||
|
||||
self.hideAxis('right')
|
||||
self.hideAxis('top')
|
||||
self.showAxis('left')
|
||||
self.showAxis('bottom')
|
||||
|
||||
#if name is not None:
|
||||
#self.registerPlot(name)
|
||||
if labels is None:
|
||||
labels = {}
|
||||
for label in list(self.scales.keys()):
|
||||
for label in list(self.axes.keys()):
|
||||
if label in kargs:
|
||||
labels[label] = kargs[label]
|
||||
del kargs[label]
|
||||
@ -330,15 +277,16 @@ class PlotItem(GraphicsWidget):
|
||||
if len(kargs) > 0:
|
||||
self.plot(**kargs)
|
||||
|
||||
#self.enableAutoRange()
|
||||
|
||||
def implements(self, interface=None):
|
||||
return interface in ['ViewBoxWrapper']
|
||||
|
||||
def getViewBox(self):
|
||||
"""Return the ViewBox within."""
|
||||
"""Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within."""
|
||||
return self.vb
|
||||
|
||||
|
||||
|
||||
def setLogMode(self, x, y):
|
||||
"""
|
||||
Set log scaling for x and y axes.
|
||||
@ -399,11 +347,11 @@ class PlotItem(GraphicsWidget):
|
||||
#self.autoBtn.setParent(None)
|
||||
#self.autoBtn = None
|
||||
|
||||
for k in self.scales:
|
||||
i = self.scales[k]['item']
|
||||
for k in self.axes:
|
||||
i = self.axes[k]['item']
|
||||
i.close()
|
||||
|
||||
self.scales = None
|
||||
self.axes = None
|
||||
self.scene().removeItem(self.vb)
|
||||
self.vb = None
|
||||
|
||||
@ -431,47 +379,6 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
def registerPlot(self, name): ## for backward compatibility
|
||||
self.vb.register(name)
|
||||
#self.name = name
|
||||
#win = str(self.window())
|
||||
##print "register", name, win
|
||||
#if win not in PlotItem.managers:
|
||||
#PlotItem.managers[win] = PlotWidgetManager()
|
||||
#self.manager = PlotItem.managers[win]
|
||||
#self.manager.addWidget(self, name)
|
||||
##QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList)
|
||||
#self.manager.sigWidgetListChanged.connect(self.updatePlotList)
|
||||
#self.updatePlotList()
|
||||
|
||||
#def updatePlotList(self):
|
||||
#"""Update the list of all plotWidgets in the "link" combos"""
|
||||
##print "update plot list", self
|
||||
#try:
|
||||
#for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]:
|
||||
#current = unicode(sc.currentText())
|
||||
#sc.blockSignals(True)
|
||||
#try:
|
||||
#sc.clear()
|
||||
#sc.addItem("")
|
||||
#if self.manager is not None:
|
||||
#for w in self.manager.listWidgets():
|
||||
##print w
|
||||
#if w == self.name:
|
||||
#continue
|
||||
#sc.addItem(w)
|
||||
#if w == current:
|
||||
#sc.setCurrentIndex(sc.count()-1)
|
||||
#finally:
|
||||
#sc.blockSignals(False)
|
||||
#if unicode(sc.currentText()) != current:
|
||||
#sc.currentItemChanged.emit()
|
||||
#except:
|
||||
#import gc
|
||||
#refs= gc.get_referrers(self)
|
||||
#print " error during update of", self
|
||||
#print " Referrers are:", refs
|
||||
#raise
|
||||
|
||||
|
||||
|
||||
def updateGrid(self, *args):
|
||||
alpha = self.ctrl.gridAlphaSlider.value()
|
||||
@ -492,91 +399,6 @@ class PlotItem(GraphicsWidget):
|
||||
return wr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#def viewRangeChanged(self, vb, range):
|
||||
##self.emit(QtCore.SIGNAL('viewChanged'), *args)
|
||||
#self.sigRangeChanged.emit(self, range)
|
||||
|
||||
#def blockLink(self, b):
|
||||
#self.linksBlocked = b
|
||||
|
||||
#def xLinkComboChanged(self):
|
||||
#self.setXLink(str(self.ctrl.xLinkCombo.currentText()))
|
||||
|
||||
#def yLinkComboChanged(self):
|
||||
#self.setYLink(str(self.ctrl.yLinkCombo.currentText()))
|
||||
|
||||
#def setXLink(self, plot=None):
|
||||
#"""Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)"""
|
||||
#if isinstance(plot, basestring):
|
||||
#if self.manager is None:
|
||||
#return
|
||||
#if self.xLinkPlot is not None:
|
||||
#self.manager.unlinkX(self, self.xLinkPlot)
|
||||
#plot = self.manager.getWidget(plot)
|
||||
#if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'):
|
||||
#plot = plot.getPlotItem()
|
||||
#self.xLinkPlot = plot
|
||||
#if plot is not None:
|
||||
#self.setManualXScale()
|
||||
#self.manager.linkX(self, plot)
|
||||
|
||||
|
||||
|
||||
#def setYLink(self, plot=None):
|
||||
#"""Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)"""
|
||||
#if isinstance(plot, basestring):
|
||||
#if self.manager is None:
|
||||
#return
|
||||
#if self.yLinkPlot is not None:
|
||||
#self.manager.unlinkY(self, self.yLinkPlot)
|
||||
#plot = self.manager.getWidget(plot)
|
||||
#if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'):
|
||||
#plot = plot.getPlotItem()
|
||||
#self.yLinkPlot = plot
|
||||
#if plot is not None:
|
||||
#self.setManualYScale()
|
||||
#self.manager.linkY(self, plot)
|
||||
|
||||
#def linkXChanged(self, plot):
|
||||
#"""Called when a linked plot has changed its X scale"""
|
||||
##print "update from", plot
|
||||
#if self.linksBlocked:
|
||||
#return
|
||||
#pr = plot.vb.viewRect()
|
||||
#pg = plot.viewGeometry()
|
||||
#if pg is None:
|
||||
##print " return early"
|
||||
#return
|
||||
#sg = self.viewGeometry()
|
||||
#upp = float(pr.width()) / pg.width()
|
||||
#x1 = pr.left() + (sg.x()-pg.x()) * upp
|
||||
#x2 = x1 + sg.width() * upp
|
||||
#plot.blockLink(True)
|
||||
#self.setManualXScale()
|
||||
#self.setXRange(x1, x2, padding=0)
|
||||
#plot.blockLink(False)
|
||||
#self.replot()
|
||||
|
||||
#def linkYChanged(self, plot):
|
||||
#"""Called when a linked plot has changed its Y scale"""
|
||||
#if self.linksBlocked:
|
||||
#return
|
||||
#pr = plot.vb.viewRect()
|
||||
#pg = plot.vb.boundingRect()
|
||||
#sg = self.vb.boundingRect()
|
||||
#upp = float(pr.height()) / pg.height()
|
||||
#y1 = pr.bottom() + (sg.y()-pg.y()) * upp
|
||||
#y2 = y1 + sg.height() * upp
|
||||
#plot.blockLink(True)
|
||||
#self.setManualYScale()
|
||||
#self.setYRange(y1, y2, padding=0)
|
||||
#plot.blockLink(False)
|
||||
#self.replot()
|
||||
|
||||
|
||||
def avgToggled(self, b):
|
||||
if b:
|
||||
self.recomputeAverages()
|
||||
@ -650,50 +472,6 @@ class PlotItem(GraphicsWidget):
|
||||
else:
|
||||
plot.setData(x, y)
|
||||
|
||||
|
||||
#def mouseCheckChanged(self):
|
||||
#state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()]
|
||||
#self.vb.setMouseEnabled(*state)
|
||||
|
||||
#def xRangeChanged(self, _, range):
|
||||
#if any(np.isnan(range)) or any(np.isinf(range)):
|
||||
#raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender())))
|
||||
#self.ctrl.xMinText.setText('%0.5g' % range[0])
|
||||
#self.ctrl.xMaxText.setText('%0.5g' % range[1])
|
||||
|
||||
### automatically change unit scale
|
||||
#maxVal = max(abs(range[0]), abs(range[1]))
|
||||
#(scale, prefix) = fn.siScale(maxVal)
|
||||
##for l in ['top', 'bottom']:
|
||||
##if self.getLabel(l).isVisible():
|
||||
##self.setLabel(l, unitPrefix=prefix)
|
||||
##self.getScale(l).setScale(scale)
|
||||
##else:
|
||||
##self.setLabel(l, unitPrefix='')
|
||||
##self.getScale(l).setScale(1.0)
|
||||
|
||||
##self.emit(QtCore.SIGNAL('xRangeChanged'), self, range)
|
||||
#self.sigXRangeChanged.emit(self, range)
|
||||
|
||||
#def yRangeChanged(self, _, range):
|
||||
#if any(np.isnan(range)) or any(np.isinf(range)):
|
||||
#raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender())))
|
||||
#self.ctrl.yMinText.setText('%0.5g' % range[0])
|
||||
#self.ctrl.yMaxText.setText('%0.5g' % range[1])
|
||||
|
||||
### automatically change unit scale
|
||||
#maxVal = max(abs(range[0]), abs(range[1]))
|
||||
#(scale, prefix) = fn.siScale(maxVal)
|
||||
##for l in ['left', 'right']:
|
||||
##if self.getLabel(l).isVisible():
|
||||
##self.setLabel(l, unitPrefix=prefix)
|
||||
##self.getScale(l).setScale(scale)
|
||||
##else:
|
||||
##self.setLabel(l, unitPrefix='')
|
||||
##self.getScale(l).setScale(1.0)
|
||||
##self.emit(QtCore.SIGNAL('yRangeChanged'), self, range)
|
||||
#self.sigYRangeChanged.emit(self, range)
|
||||
|
||||
def autoBtnClicked(self):
|
||||
if self.autoBtn.mode == 'auto':
|
||||
self.enableAutoRange()
|
||||
@ -706,72 +484,6 @@ class PlotItem(GraphicsWidget):
|
||||
"""
|
||||
print("Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead.")
|
||||
self.vb.enableAutoRange(self.vb.XYAxes)
|
||||
#self.ctrl.xAutoRadio.setChecked(True)
|
||||
#self.ctrl.yAutoRadio.setChecked(True)
|
||||
|
||||
#self.autoBtn.setImageFile(self.lockImageFile)
|
||||
#self.autoBtn.mode = 'lock'
|
||||
#self.updateXScale()
|
||||
#self.updateYScale()
|
||||
#self.replot()
|
||||
|
||||
#def updateXScale(self):
|
||||
#"""Set plot to autoscale or not depending on state of radio buttons"""
|
||||
#if self.ctrl.xManualRadio.isChecked():
|
||||
#self.setManualXScale()
|
||||
#else:
|
||||
#self.setAutoXScale()
|
||||
#self.replot()
|
||||
|
||||
#def updateYScale(self, b=False):
|
||||
#"""Set plot to autoscale or not depending on state of radio buttons"""
|
||||
#if self.ctrl.yManualRadio.isChecked():
|
||||
#self.setManualYScale()
|
||||
#else:
|
||||
#self.setAutoYScale()
|
||||
#self.replot()
|
||||
|
||||
#def enableManualScale(self, v=[True, True]):
|
||||
#if v[0]:
|
||||
#self.autoScale[0] = False
|
||||
#self.ctrl.xManualRadio.setChecked(True)
|
||||
##self.setManualXScale()
|
||||
#if v[1]:
|
||||
#self.autoScale[1] = False
|
||||
#self.ctrl.yManualRadio.setChecked(True)
|
||||
##self.setManualYScale()
|
||||
##self.autoBtn.enable()
|
||||
#self.autoBtn.setImageFile(self.autoImageFile)
|
||||
#self.autoBtn.mode = 'auto'
|
||||
##self.replot()
|
||||
|
||||
#def setManualXScale(self):
|
||||
#self.autoScale[0] = False
|
||||
#x1 = float(self.ctrl.xMinText.text())
|
||||
#x2 = float(self.ctrl.xMaxText.text())
|
||||
#self.ctrl.xManualRadio.setChecked(True)
|
||||
#self.setXRange(x1, x2, padding=0)
|
||||
#self.autoBtn.show()
|
||||
##self.replot()
|
||||
|
||||
#def setManualYScale(self):
|
||||
#self.autoScale[1] = False
|
||||
#y1 = float(self.ctrl.yMinText.text())
|
||||
#y2 = float(self.ctrl.yMaxText.text())
|
||||
#self.ctrl.yManualRadio.setChecked(True)
|
||||
#self.setYRange(y1, y2, padding=0)
|
||||
#self.autoBtn.show()
|
||||
##self.replot()
|
||||
|
||||
#def setAutoXScale(self):
|
||||
#self.autoScale[0] = True
|
||||
#self.ctrl.xAutoRadio.setChecked(True)
|
||||
##self.replot()
|
||||
|
||||
#def setAutoYScale(self):
|
||||
#self.autoScale[1] = True
|
||||
#self.ctrl.yAutoRadio.setChecked(True)
|
||||
##self.replot()
|
||||
|
||||
def addItem(self, item, *args, **kargs):
|
||||
"""
|
||||
@ -867,17 +579,6 @@ class PlotItem(GraphicsWidget):
|
||||
"""
|
||||
|
||||
|
||||
|
||||
#if y is not None:
|
||||
#data = y
|
||||
#if data2 is not None:
|
||||
#x = data
|
||||
#data = data2
|
||||
#if decimate is not None and decimate > 1:
|
||||
#data = data[::decimate]
|
||||
#if x is not None:
|
||||
#x = x[::decimate]
|
||||
## print 'plot with decimate = %d' % (decimate)
|
||||
clear = kargs.get('clear', False)
|
||||
params = kargs.get('params', None)
|
||||
|
||||
@ -888,23 +589,7 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
#if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
#curve = self._plotMetaArray(data, x=x, **kargs)
|
||||
#elif isinstance(data, np.ndarray):
|
||||
#curve = self._plotArray(data, x=x, **kargs)
|
||||
#elif isinstance(data, list):
|
||||
#if x is not None:
|
||||
#x = np.array(x)
|
||||
#curve = self._plotArray(np.array(data), x=x, **kargs)
|
||||
#elif data is None:
|
||||
#curve = PlotCurveItem(**kargs)
|
||||
#else:
|
||||
#raise Exception('Not sure how to plot object of type %s' % type(data))
|
||||
|
||||
#print data, curve
|
||||
self.addItem(item, params=params)
|
||||
#if pen is not None:
|
||||
#curve.setPen(fn.mkPen(pen))
|
||||
|
||||
return item
|
||||
|
||||
@ -922,80 +607,34 @@ class PlotItem(GraphicsWidget):
|
||||
del kargs['size']
|
||||
|
||||
return self.plot(*args, **kargs)
|
||||
#sp = ScatterPlotItem(*args, **kargs)
|
||||
#self.addItem(sp)
|
||||
#return sp
|
||||
|
||||
|
||||
|
||||
#def plotChanged(self, curve=None):
|
||||
## Recompute auto range if needed
|
||||
#args = {}
|
||||
#for ax in [0, 1]:
|
||||
#print "range", ax
|
||||
#if self.autoScale[ax]:
|
||||
#percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01
|
||||
#mn = None
|
||||
#mx = None
|
||||
#for c in self.curves + [c[1] for c in self.avgCurves.values()] + self.dataItems:
|
||||
#if not c.isVisible():
|
||||
#continue
|
||||
#cmn, cmx = c.getRange(ax, percentScale)
|
||||
##print " ", c, cmn, cmx
|
||||
#if mn is None or cmn < mn:
|
||||
#mn = cmn
|
||||
#if mx is None or cmx > mx:
|
||||
#mx = cmx
|
||||
#if mn is None or mx is None or any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])):
|
||||
#continue
|
||||
#if mn == mx:
|
||||
#mn -= 1
|
||||
#mx += 1
|
||||
#if ax == 0:
|
||||
#args['xRange'] = [mn, mx]
|
||||
#else:
|
||||
#args['yRange'] = [mn, mx]
|
||||
|
||||
#if len(args) > 0:
|
||||
##print args
|
||||
#self.setRange(**args)
|
||||
|
||||
def replot(self):
|
||||
#self.plotChanged()
|
||||
self.update()
|
||||
|
||||
def updateParamList(self):
|
||||
self.ctrl.avgParamList.clear()
|
||||
## Check to see that each parameter for each curve is present in the list
|
||||
#print "\nUpdate param list", self
|
||||
#print "paramList:", self.paramList
|
||||
for c in self.curves:
|
||||
#print " curve:", c
|
||||
for p in list(self.itemMeta.get(c, {}).keys()):
|
||||
#print " param:", p
|
||||
if type(p) is tuple:
|
||||
p = '.'.join(p)
|
||||
|
||||
## If the parameter is not in the list, add it.
|
||||
matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly)
|
||||
#print " matches:", matches
|
||||
if len(matches) == 0:
|
||||
i = QtGui.QListWidgetItem(p)
|
||||
if p in self.paramList and self.paramList[p] is True:
|
||||
#print " set checked"
|
||||
i.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
#print " set unchecked"
|
||||
i.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.ctrl.avgParamList.addItem(i)
|
||||
else:
|
||||
i = matches[0]
|
||||
|
||||
self.paramList[p] = (i.checkState() == QtCore.Qt.Checked)
|
||||
#print "paramList:", self.paramList
|
||||
|
||||
|
||||
## This is bullshit.
|
||||
## Qt's SVG-writing capabilities are pretty terrible.
|
||||
def writeSvgCurves(self, fileName=None):
|
||||
if fileName is None:
|
||||
self.fileDialog = FileDialog()
|
||||
@ -1190,18 +829,12 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
|
||||
def saveState(self):
|
||||
#if not HAVE_WIDGETGROUP:
|
||||
#raise Exception("State save/restore requires WidgetGroup class.")
|
||||
state = self.stateGroup.state()
|
||||
state['paramList'] = self.paramList.copy()
|
||||
state['view'] = self.vb.getState()
|
||||
#print "\nSAVE %s:\n" % str(self.name), state
|
||||
#print "Saving state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup']))
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
#if not HAVE_WIDGETGROUP:
|
||||
#raise Exception("State save/restore requires WidgetGroup class.")
|
||||
if 'paramList' in state:
|
||||
self.paramList = state['paramList'].copy()
|
||||
|
||||
@ -1218,8 +851,6 @@ class PlotItem(GraphicsWidget):
|
||||
state['yGridCheck'] = state['gridGroup']
|
||||
|
||||
self.stateGroup.setState(state)
|
||||
#self.updateXScale()
|
||||
#self.updateYScale()
|
||||
self.updateParamList()
|
||||
|
||||
if 'view' not in state:
|
||||
@ -1232,13 +863,6 @@ class PlotItem(GraphicsWidget):
|
||||
}
|
||||
self.vb.setState(state['view'])
|
||||
|
||||
|
||||
#print "\nRESTORE %s:\n" % str(self.name), state
|
||||
#print "Restoring state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup']))
|
||||
#avg = self.ctrl.averageGroup.isChecked()
|
||||
#if avg != state['averageGroup']:
|
||||
#print " WARNING: avgGroup is %s, should be %s" % (str(avg), str(state['averageGroup']))
|
||||
|
||||
|
||||
def widgetGroupInterface(self):
|
||||
return (None, PlotItem.saveState, PlotItem.restoreState)
|
||||
@ -1269,8 +893,6 @@ class PlotItem(GraphicsWidget):
|
||||
for c in self.curves:
|
||||
c.setDownsampling(ds)
|
||||
self.recomputeAverages()
|
||||
#for c in self.avgCurves.values():
|
||||
#c[1].setDownsampling(ds)
|
||||
|
||||
|
||||
def downsampleMode(self):
|
||||
@ -1306,8 +928,6 @@ class PlotItem(GraphicsWidget):
|
||||
(alpha, auto) = self.alphaState()
|
||||
for c in self.curves:
|
||||
c.setAlpha(alpha**2, auto)
|
||||
|
||||
#self.replot(autoRange=False)
|
||||
|
||||
def alphaState(self):
|
||||
enabled = self.ctrl.alphaGroup.isChecked()
|
||||
@ -1330,9 +950,6 @@ class PlotItem(GraphicsWidget):
|
||||
mode = False
|
||||
return mode
|
||||
|
||||
#def wheelEvent(self, ev):
|
||||
## disables default panning the whole scene by mousewheel
|
||||
#ev.accept()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
if self.autoBtn is None: ## already closed down
|
||||
@ -1340,29 +957,42 @@ class PlotItem(GraphicsWidget):
|
||||
btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect())
|
||||
y = self.size().height() - btnRect.height()
|
||||
self.autoBtn.setPos(0, y)
|
||||
|
||||
#def hoverMoveEvent(self, ev):
|
||||
#self.mousePos = ev.pos()
|
||||
#self.mouseScreenPos = ev.screenPos()
|
||||
|
||||
|
||||
#def ctrlBtnClicked(self):
|
||||
#self.ctrlMenu.popup(self.mouseScreenPos)
|
||||
|
||||
|
||||
|
||||
def getMenu(self):
|
||||
return self.ctrlMenu
|
||||
|
||||
def getContextMenus(self, event):
|
||||
## called when another item is displaying its context menu; we get to add extras to the end of the menu.
|
||||
return self.ctrlMenu
|
||||
|
||||
if self.menuEnabled():
|
||||
return self.ctrlMenu
|
||||
else:
|
||||
return None
|
||||
|
||||
def setMenuEnabled(self, enableMenu=True, enableViewBoxMenu='same'):
|
||||
"""
|
||||
Enable or disable the context menu for this PlotItem.
|
||||
By default, the ViewBox's context menu will also be affected.
|
||||
(use enableViewBoxMenu=None to leave the ViewBox unchanged)
|
||||
"""
|
||||
self._menuEnabled = enableMenu
|
||||
if enableViewBoxMenu is None:
|
||||
return
|
||||
if enableViewBoxMenu is 'same':
|
||||
enableViewBoxMenu = enableMenu
|
||||
self.vb.setMenuEnabled(enableViewBoxMenu)
|
||||
|
||||
def menuEnabled(self):
|
||||
return self._menuEnabled
|
||||
|
||||
|
||||
|
||||
def getLabel(self, key):
|
||||
pass
|
||||
|
||||
def _checkScaleKey(self, key):
|
||||
if key not in self.scales:
|
||||
raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.scales.keys()))))
|
||||
if key not in self.axes:
|
||||
raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.axes.keys()))))
|
||||
|
||||
def getScale(self, key):
|
||||
return self.getAxis(key)
|
||||
@ -1371,7 +1001,7 @@ class PlotItem(GraphicsWidget):
|
||||
"""Return the specified AxisItem.
|
||||
*name* should be 'left', 'bottom', 'top', or 'right'."""
|
||||
self._checkScaleKey(name)
|
||||
return self.scales[name]['item']
|
||||
return self.axes[name]['item']
|
||||
|
||||
def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args):
|
||||
"""
|
||||
@ -1417,13 +1047,14 @@ class PlotItem(GraphicsWidget):
|
||||
axis must be one of 'left', 'bottom', 'right', or 'top'
|
||||
"""
|
||||
s = self.getScale(axis)
|
||||
p = self.scales[axis]['pos']
|
||||
p = self.axes[axis]['pos']
|
||||
if show:
|
||||
s.show()
|
||||
else:
|
||||
s.hide()
|
||||
|
||||
def hideAxis(self, axis):
|
||||
"""Hide one of the PlotItem's axes. ('left', 'bottom', 'right', or 'top')"""
|
||||
self.showAxis(axis, False)
|
||||
|
||||
def showScale(self, *args, **kargs):
|
||||
@ -1431,6 +1062,7 @@ class PlotItem(GraphicsWidget):
|
||||
return self.showAxis(*args, **kargs)
|
||||
|
||||
def hideButtons(self):
|
||||
"""Causes auto-scale button ('A' in lower-left corner) to be hidden for this PlotItem"""
|
||||
#self.ctrlBtn.hide()
|
||||
self.autoBtn.hide()
|
||||
|
||||
@ -1454,7 +1086,6 @@ class PlotItem(GraphicsWidget):
|
||||
## create curve
|
||||
try:
|
||||
xv = arr.xvals(0)
|
||||
#print 'xvals:', xv
|
||||
except:
|
||||
if x is None:
|
||||
xv = np.arange(arr.shape[0])
|
||||
@ -1474,17 +1105,6 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
return c
|
||||
|
||||
#def saveSvgClicked(self):
|
||||
#self.writeSvg()
|
||||
|
||||
#def saveSvgCurvesClicked(self):
|
||||
#self.writeSvgCurves()
|
||||
|
||||
#def saveImgClicked(self):
|
||||
#self.writeImage()
|
||||
|
||||
#def saveCsvClicked(self):
|
||||
#self.writeCsv()
|
||||
|
||||
def setExportMode(self, export, opts):
|
||||
if export:
|
||||
@ -1492,63 +1112,3 @@ class PlotItem(GraphicsWidget):
|
||||
else:
|
||||
self.autoBtn.show()
|
||||
|
||||
|
||||
#class PlotWidgetManager(QtCore.QObject):
|
||||
|
||||
#sigWidgetListChanged = QtCore.Signal(object)
|
||||
|
||||
#"""Used for managing communication between PlotWidgets"""
|
||||
#def __init__(self):
|
||||
#QtCore.QObject.__init__(self)
|
||||
#self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here
|
||||
|
||||
#def addWidget(self, w, name):
|
||||
#self.widgets[name] = w
|
||||
##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys())
|
||||
#self.sigWidgetListChanged.emit(self.widgets.keys())
|
||||
|
||||
#def removeWidget(self, name):
|
||||
#if name in self.widgets:
|
||||
#del self.widgets[name]
|
||||
##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys())
|
||||
#self.sigWidgetListChanged.emit(self.widgets.keys())
|
||||
#else:
|
||||
#print "plot %s not managed" % name
|
||||
|
||||
|
||||
#def listWidgets(self):
|
||||
#return self.widgets.keys()
|
||||
|
||||
#def getWidget(self, name):
|
||||
#if name not in self.widgets:
|
||||
#return None
|
||||
#else:
|
||||
#return self.widgets[name]
|
||||
|
||||
#def linkX(self, p1, p2):
|
||||
##QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged)
|
||||
#p1.sigXRangeChanged.connect(p2.linkXChanged)
|
||||
##QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged)
|
||||
#p2.sigXRangeChanged.connect(p1.linkXChanged)
|
||||
#p1.linkXChanged(p2)
|
||||
##p2.setManualXScale()
|
||||
|
||||
#def unlinkX(self, p1, p2):
|
||||
##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged)
|
||||
#p1.sigXRangeChanged.disconnect(p2.linkXChanged)
|
||||
##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged)
|
||||
#p2.sigXRangeChanged.disconnect(p1.linkXChanged)
|
||||
|
||||
#def linkY(self, p1, p2):
|
||||
##QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged)
|
||||
#p1.sigYRangeChanged.connect(p2.linkYChanged)
|
||||
##QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged)
|
||||
#p2.sigYRangeChanged.connect(p1.linkYChanged)
|
||||
#p1.linkYChanged(p2)
|
||||
##p2.setManualYScale()
|
||||
|
||||
#def unlinkY(self, p1, p2):
|
||||
##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged)
|
||||
#p1.sigYRangeChanged.disconnect(p2.linkYChanged)
|
||||
##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged)
|
||||
#p2.sigYRangeChanged.disconnect(p1.linkYChanged)
|
||||
|
@ -116,7 +116,7 @@ class Ui_Form(object):
|
||||
self.gridLayout_2.addWidget(self.yGridCheck, 1, 0, 1, 2)
|
||||
self.gridAlphaSlider = QtGui.QSlider(self.gridGroup)
|
||||
self.gridAlphaSlider.setMaximum(255)
|
||||
self.gridAlphaSlider.setProperty("value", 70)
|
||||
self.gridAlphaSlider.setProperty("value", 128)
|
||||
self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider"))
|
||||
self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1)
|
||||
|
@ -221,7 +221,7 @@
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>70</number>
|
||||
<number>128</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
|
@ -800,7 +800,7 @@ class ROI(GraphicsObject):
|
||||
#print " dshape", dShape
|
||||
|
||||
## Determine transform that maps ROI bounding box to image coordinates
|
||||
tr = self.sceneTransform() * img.sceneTransform().inverted()[0]
|
||||
tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform())
|
||||
|
||||
## Modify transform to scale from image coords to data coords
|
||||
#m = QtGui.QTransform()
|
||||
@ -832,35 +832,34 @@ class ROI(GraphicsObject):
|
||||
else:
|
||||
return bounds, tr
|
||||
|
||||
|
||||
def getArrayRegion(self, data, img, axes=(0,1)):
|
||||
"""Use the position of this ROI relative to an imageItem to pull a slice from an array."""
|
||||
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
||||
"""Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array.
|
||||
|
||||
This method uses :func:`affineSlice <pyqtgraph.affineSlice>` to generate
|
||||
the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to
|
||||
pass to :func:`affineSlice <pyqtgraph.affineSlice>`.
|
||||
|
||||
shape = self.state['size']
|
||||
If *returnMappedCoords* is True, then the method returns a tuple (result, coords)
|
||||
such that coords is the set of coordinates used to interpolate values from the original
|
||||
data, mapped into the parent coordinate system of the image. This is useful, when slicing
|
||||
data from images that have been transformed, for determining the location of each value
|
||||
in the sliced data.
|
||||
|
||||
origin = self.mapToItem(img, QtCore.QPointF(0, 0))
|
||||
|
||||
## vx and vy point in the directions of the slice axes, but must be scaled properly
|
||||
vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin
|
||||
vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin
|
||||
|
||||
lvx = np.sqrt(vx.x()**2 + vx.y()**2)
|
||||
lvy = np.sqrt(vy.x()**2 + vy.y()**2)
|
||||
pxLen = img.width() / float(data.shape[axes[0]])
|
||||
sx = pxLen / lvx
|
||||
sy = pxLen / lvy
|
||||
|
||||
vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy))
|
||||
shape = self.state['size']
|
||||
shape = [abs(shape[0]/sx), abs(shape[1]/sy)]
|
||||
|
||||
origin = (origin.x(), origin.y())
|
||||
|
||||
#print "shape", shape, "vectors", vectors, "origin", origin
|
||||
|
||||
return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, order=1)
|
||||
All extra keyword arguments are passed to :func:`affineSlice <pyqtgraph.affineSlice>`.
|
||||
"""
|
||||
|
||||
shape, vectors, origin = self.getAffineSliceParams(data, img, axes)
|
||||
if not returnMappedCoords:
|
||||
return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds)
|
||||
else:
|
||||
kwds['returnCoords'] = True
|
||||
result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds)
|
||||
tr = fn.transformToArray(img.transform())[:,:2].reshape((3, 2) + (1,)*(coords.ndim-1))
|
||||
coords = coords[np.newaxis, ...]
|
||||
mapped = (tr*coords).sum(axis=0)
|
||||
return result, mapped
|
||||
|
||||
|
||||
### transpose data so x and y are the first 2 axes
|
||||
#trAx = range(0, data.ndim)
|
||||
#trAx.remove(axes[0])
|
||||
@ -959,6 +958,37 @@ class ROI(GraphicsObject):
|
||||
### Untranspose array before returning
|
||||
#return arr5.transpose(tr2)
|
||||
|
||||
def getAffineSliceParams(self, data, img, axes=(0.1)):
|
||||
"""
|
||||
Returns the parameters needed to use :func:`affineSlice <pyqtgraph.affineSlice>` to
|
||||
extract a subset of *data* using this ROI and *img* to specify the subset.
|
||||
|
||||
See :func:`getArrayRegion <pyqtgraph.ROI.getArrayRegion>` for more information.
|
||||
"""
|
||||
|
||||
shape = self.state['size']
|
||||
|
||||
origin = self.mapToItem(img, QtCore.QPointF(0, 0))
|
||||
|
||||
## vx and vy point in the directions of the slice axes, but must be scaled properly
|
||||
vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin
|
||||
vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin
|
||||
|
||||
lvx = np.sqrt(vx.x()**2 + vx.y()**2)
|
||||
lvy = np.sqrt(vy.x()**2 + vy.y()**2)
|
||||
pxLen = img.width() / float(data.shape[axes[0]])
|
||||
#img.width is number of pixels or width of item?
|
||||
#need pxWidth and pxHeight instead of pxLen ?
|
||||
sx = pxLen / lvx
|
||||
sy = pxLen / lvy
|
||||
|
||||
vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy))
|
||||
shape = self.state['size']
|
||||
shape = [abs(shape[0]/sx), abs(shape[1]/sy)]
|
||||
|
||||
origin = (origin.x(), origin.y())
|
||||
return shape, vectors, origin
|
||||
|
||||
def getGlobalTransform(self, relativeTo=None):
|
||||
"""Return global transformation (rotation angle+translation) required to move
|
||||
from relative state to current state. If relative state isn't specified,
|
||||
@ -1251,7 +1281,7 @@ class Handle(UIGraphicsItem):
|
||||
v = dt.map(QtCore.QPointF(1, 0)) - dt.map(QtCore.QPointF(0, 0))
|
||||
va = np.arctan2(v.y(), v.x())
|
||||
|
||||
dti = dt.inverted()[0]
|
||||
dti = fn.invertQTransform(dt)
|
||||
devPos = dt.map(QtCore.QPointF(0,0))
|
||||
tr = QtGui.QTransform()
|
||||
tr.translate(devPos.x(), devPos.y())
|
||||
|
@ -79,7 +79,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
|
||||
GraphicsObject.__init__(self)
|
||||
self.setFlag(self.ItemHasNoContents, True)
|
||||
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)])
|
||||
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('item', object), ('data', object)])
|
||||
self.bounds = [None, None] ## caches data bounds
|
||||
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
|
||||
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
|
||||
@ -226,6 +226,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.setPointData(kargs['data'], dataSet=newData)
|
||||
|
||||
#self.updateSpots()
|
||||
self.prepareGeometryChange()
|
||||
self.bounds = [None, None]
|
||||
self.generateSpotItems()
|
||||
self.sigPlotChanged.emit(self)
|
||||
@ -396,7 +397,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
if frac >= 1.0 and self.bounds[ax] is not None:
|
||||
return self.bounds[ax]
|
||||
|
||||
self.prepareGeometryChange()
|
||||
#self.prepareGeometryChange()
|
||||
if self.data is None or len(self.data) == 0:
|
||||
return (None, None)
|
||||
|
||||
@ -464,6 +465,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
|
||||
|
||||
def viewRangeChanged(self):
|
||||
self.prepareGeometryChange()
|
||||
GraphicsObject.viewRangeChanged(self)
|
||||
self.bounds = [None, None]
|
||||
|
||||
@ -557,7 +559,7 @@ class SpotItem(GraphicsItem):
|
||||
If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead.
|
||||
"""
|
||||
symbol = self._data['symbol']
|
||||
if symbol == '':
|
||||
if symbol is None:
|
||||
symbol = self._plot.opts['symbol']
|
||||
try:
|
||||
n = int(symbol)
|
||||
|
@ -1,6 +1,7 @@
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import pyqtgraph as pg
|
||||
from .UIGraphicsItem import *
|
||||
import pyqtgraph.functions as fn
|
||||
|
||||
class TextItem(UIGraphicsItem):
|
||||
"""
|
||||
@ -87,7 +88,7 @@ class TextItem(UIGraphicsItem):
|
||||
if br is None:
|
||||
return
|
||||
self.prepareGeometryChange()
|
||||
self._bounds = self.deviceTransform().inverted()[0].mapRect(br)
|
||||
self._bounds = fn.invertQTransform(self.deviceTransform()).mapRect(br)
|
||||
#print self._bounds
|
||||
|
||||
def boundingRect(self):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph.python2_3 import sortList
|
||||
import numpy as np
|
||||
from pyqtgraph.Point import Point
|
||||
import pyqtgraph.functions as fn
|
||||
@ -62,7 +63,7 @@ class ViewBox(GraphicsWidget):
|
||||
NamedViews = weakref.WeakValueDictionary() # name: ViewBox
|
||||
AllViews = weakref.WeakKeyDictionary() # ViewBox: None
|
||||
|
||||
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu = True, name=None):
|
||||
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
|
||||
"""
|
||||
============= =============================================================
|
||||
**Arguments**
|
||||
@ -105,6 +106,8 @@ class ViewBox(GraphicsWidget):
|
||||
'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode,
|
||||
'enableMenu': enableMenu,
|
||||
'wheelScaleFactor': -1.0 / 8.0,
|
||||
|
||||
'background': None,
|
||||
}
|
||||
|
||||
|
||||
@ -118,17 +121,23 @@ class ViewBox(GraphicsWidget):
|
||||
self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses
|
||||
|
||||
## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
|
||||
## this is a workaround for a Qt + OpenGL but that causes improper clipping
|
||||
## this is a workaround for a Qt + OpenGL bug that causes improper clipping
|
||||
## https://bugreports.qt.nokia.com/browse/QTBUG-23723
|
||||
self.childGroup = ChildGroup(self)
|
||||
self.childGroup.sigItemsChanged.connect(self.itemsChanged)
|
||||
|
||||
self.background = QtGui.QGraphicsRectItem(self.rect())
|
||||
self.background.setParentItem(self)
|
||||
self.background.setZValue(-1e6)
|
||||
self.background.setPen(fn.mkPen(None))
|
||||
self.updateBackground()
|
||||
|
||||
#self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
|
||||
# this also enables capture of keyPressEvents.
|
||||
|
||||
## Make scale box that is shown when dragging on the view
|
||||
self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
|
||||
self.rbScaleBox.setPen(fn.mkPen((255,0,0), width=1))
|
||||
self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1))
|
||||
self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
|
||||
self.rbScaleBox.hide()
|
||||
self.addItem(self.rbScaleBox)
|
||||
@ -286,6 +295,7 @@ class ViewBox(GraphicsWidget):
|
||||
#self.updateAutoRange()
|
||||
self.updateMatrix()
|
||||
self.sigStateChanged.emit(self)
|
||||
self.background.setRect(self.rect())
|
||||
#self.linkedXChanged()
|
||||
#self.linkedYChanged()
|
||||
|
||||
@ -349,7 +359,7 @@ class ViewBox(GraphicsWidget):
|
||||
changes[1] = yRange
|
||||
|
||||
if len(changes) == 0:
|
||||
print rect
|
||||
print(rect)
|
||||
raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect)))
|
||||
|
||||
changed = [False, False]
|
||||
@ -442,10 +452,8 @@ class ViewBox(GraphicsWidget):
|
||||
center = Point(vr.center())
|
||||
else:
|
||||
center = Point(center)
|
||||
|
||||
tl = center + (vr.topLeft()-center) * scale
|
||||
br = center + (vr.bottomRight()-center) * scale
|
||||
|
||||
self.setRange(QtCore.QRectF(tl, br), padding=0)
|
||||
|
||||
def translateBy(self, t):
|
||||
@ -755,7 +763,7 @@ class ViewBox(GraphicsWidget):
|
||||
|
||||
def mapToView(self, obj):
|
||||
"""Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox"""
|
||||
m = self.childTransform().inverted()[0]
|
||||
m = fn.invertQTransform(self.childTransform())
|
||||
return m.map(obj)
|
||||
|
||||
def mapFromView(self, obj):
|
||||
@ -821,7 +829,7 @@ class ViewBox(GraphicsWidget):
|
||||
mask[axis] = mv
|
||||
s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor
|
||||
|
||||
center = Point(self.childGroup.transform().inverted()[0].map(ev.pos()))
|
||||
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
|
||||
#center = ev.pos()
|
||||
|
||||
self.scaleBy(s, center)
|
||||
@ -854,7 +862,10 @@ class ViewBox(GraphicsWidget):
|
||||
return self._menuCopy
|
||||
|
||||
def getContextMenus(self, event):
|
||||
return self.menu.subMenus()
|
||||
if self.menuEnabled():
|
||||
return self.menu.subMenus()
|
||||
else:
|
||||
return None
|
||||
#return [self.getMenu(event)]
|
||||
|
||||
|
||||
@ -901,8 +912,11 @@ class ViewBox(GraphicsWidget):
|
||||
dif = np.array([dif.x(), dif.y()])
|
||||
dif[0] *= -1
|
||||
s = ((mask * 0.02) + 1) ** dif
|
||||
center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton)))
|
||||
#center = Point(ev.buttonDownPos(QtCore.Qt.RightButton))
|
||||
|
||||
tr = self.childGroup.transform()
|
||||
tr = fn.invertQTransform(tr)
|
||||
|
||||
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
|
||||
self.scaleBy(s, center)
|
||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||
|
||||
@ -1155,6 +1169,15 @@ class ViewBox(GraphicsWidget):
|
||||
#self.scene().render(p)
|
||||
#p.end()
|
||||
|
||||
def updateBackground(self):
|
||||
bg = self.state['background']
|
||||
if bg is None:
|
||||
self.background.hide()
|
||||
else:
|
||||
self.background.show()
|
||||
self.background.setBrush(fn.mkBrush(bg))
|
||||
|
||||
|
||||
def updateViewLists(self):
|
||||
def cmpViews(a, b):
|
||||
wins = 100 * cmp(a.window() is self.window(), b.window() is self.window())
|
||||
|
@ -1,4 +1,5 @@
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
from pyqtgraph.python2_3 import asUnicode
|
||||
from pyqtgraph.WidgetGroup import WidgetGroup
|
||||
from .axisCtrlTemplate import Ui_Form as AxisCtrlTemplate
|
||||
import weakref
|
||||
|
@ -38,7 +38,7 @@ from pyqtgraph.SignalProxy import SignalProxy
|
||||
|
||||
class PlotROI(ROI):
|
||||
def __init__(self, size):
|
||||
ROI.__init__(self, pos=[0,0], size=size, scaleSnap=True, translateSnap=True)
|
||||
ROI.__init__(self, pos=[0,0], size=size) #, scaleSnap=True, translateSnap=True)
|
||||
self.addScaleHandle([1, 1], [0, 0])
|
||||
self.addRotateHandle([0, 0], [0.5, 0.5])
|
||||
|
||||
@ -67,7 +67,12 @@ class ImageView(QtGui.QWidget):
|
||||
sigTimeChanged = QtCore.Signal(object, object)
|
||||
sigProcessingChanged = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, parent=None, name="ImageView", *args):
|
||||
def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args):
|
||||
"""
|
||||
By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data
|
||||
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem. Custom items may be given instead
|
||||
by specifying the *view* and/or *imageItem* arguments.
|
||||
"""
|
||||
QtGui.QWidget.__init__(self, parent, *args)
|
||||
self.levelMax = 4096
|
||||
self.levelMin = 0
|
||||
@ -89,7 +94,10 @@ class ImageView(QtGui.QWidget):
|
||||
#self.ui.graphicsView.setAspectLocked(True)
|
||||
#self.ui.graphicsView.invertY()
|
||||
#self.ui.graphicsView.enableMouse()
|
||||
self.view = ViewBox()
|
||||
if view is None:
|
||||
self.view = ViewBox()
|
||||
else:
|
||||
self.view = view
|
||||
self.ui.graphicsView.setCentralItem(self.view)
|
||||
self.view.setAspectLocked(True)
|
||||
self.view.invertY()
|
||||
@ -101,7 +109,10 @@ class ImageView(QtGui.QWidget):
|
||||
#self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255))
|
||||
#self.ui.gradientWidget.setOrientation('right')
|
||||
|
||||
self.imageItem = ImageItem()
|
||||
if imageItem is None:
|
||||
self.imageItem = ImageItem()
|
||||
else:
|
||||
self.imageItem = imageItem
|
||||
self.view.addItem(self.imageItem)
|
||||
self.currentIndex = 0
|
||||
|
||||
@ -531,14 +542,18 @@ class ImageView(QtGui.QWidget):
|
||||
axes = (1, 2)
|
||||
else:
|
||||
return
|
||||
data = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes)
|
||||
data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True)
|
||||
if data is not None:
|
||||
while data.ndim > 1:
|
||||
data = data.mean(axis=1)
|
||||
if image.ndim == 3:
|
||||
self.roiCurve.setData(y=data, x=self.tVals)
|
||||
else:
|
||||
self.roiCurve.setData(y=data, x=list(range(len(data))))
|
||||
while coords.ndim > 2:
|
||||
coords = coords[:,:,0]
|
||||
coords = coords - coords[:,0,np.newaxis]
|
||||
xvals = (coords**2).sum(axis=0) ** 0.5
|
||||
self.roiCurve.setData(y=data, x=xvals)
|
||||
|
||||
#self.ui.roiPlot.replot()
|
||||
|
||||
@ -664,4 +679,18 @@ class ImageView(QtGui.QWidget):
|
||||
#return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0])
|
||||
##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value()
|
||||
|
||||
|
||||
def getView(self):
|
||||
"""Return the ViewBox (or other compatible object) which displays the ImageItem"""
|
||||
return self.view
|
||||
|
||||
def getImageItem(self):
|
||||
"""Return the ImageItem for this ImageView."""
|
||||
return self.imageItem
|
||||
|
||||
def getRoiPlot(self):
|
||||
"""Return the ROI PlotWidget for this ImageView"""
|
||||
return self.ui.roiPlot
|
||||
|
||||
def getHistogramWidget(self):
|
||||
"""Return the HistogramLUTWidget for this ImageView"""
|
||||
return self.ui.histogram
|
||||
|
@ -19,4 +19,6 @@ TODO:
|
||||
(RemoteGraphicsView class)
|
||||
"""
|
||||
|
||||
from processes import *
|
||||
from .processes import *
|
||||
from .parallelizer import Parallelize, CanceledError
|
||||
from .remoteproxy import proxy
|
15
multiprocess/bootstrap.py
Normal file
15
multiprocess/bootstrap.py
Normal 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)
|
@ -2,6 +2,10 @@ import os, sys, time, multiprocessing
|
||||
from processes import ForkedProcess
|
||||
from remoteproxy import ExitError
|
||||
|
||||
class CanceledError(Exception):
|
||||
"""Raised when the progress dialog is canceled during a processing operation."""
|
||||
pass
|
||||
|
||||
class Parallelize:
|
||||
"""
|
||||
Class for ultra-simple inline parallelization on multi-core CPUs
|
||||
@ -29,35 +33,82 @@ class Parallelize:
|
||||
print results
|
||||
|
||||
|
||||
The only major caveat is that *result* in the example above must be picklable.
|
||||
The only major caveat is that *result* in the example above must be picklable,
|
||||
since it is automatically sent via pipe back to the parent process.
|
||||
"""
|
||||
|
||||
def __init__(self, tasks, workers=None, block=True, **kwds):
|
||||
def __init__(self, tasks, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds):
|
||||
"""
|
||||
Args:
|
||||
tasks - list of objects to be processed (Parallelize will determine how to distribute the tasks)
|
||||
workers - number of worker processes or None to use number of CPUs in the system
|
||||
kwds - objects to be shared by proxy with child processes
|
||||
=============== ===================================================================
|
||||
Arguments:
|
||||
tasks list of objects to be processed (Parallelize will determine how to
|
||||
distribute the tasks)
|
||||
workers number of worker processes or None to use number of CPUs in the
|
||||
system
|
||||
progressDialog optional dict of arguments for ProgressDialog
|
||||
to update while tasks are processed
|
||||
randomReseed If True, each forked process will reseed its random number generator
|
||||
to ensure independent results. Works with the built-in random
|
||||
and numpy.random.
|
||||
kwds objects to be shared by proxy with child processes (they will
|
||||
appear as attributes of the tasker)
|
||||
=============== ===================================================================
|
||||
"""
|
||||
|
||||
self.block = block
|
||||
## Generate progress dialog.
|
||||
## Note that we want to avoid letting forked child processes play with progress dialogs..
|
||||
self.showProgress = False
|
||||
if progressDialog is not None:
|
||||
self.showProgress = True
|
||||
if isinstance(progressDialog, basestring):
|
||||
progressDialog = {'labelText': progressDialog}
|
||||
import pyqtgraph as pg
|
||||
self.progressDlg = pg.ProgressDialog(**progressDialog)
|
||||
|
||||
if workers is None:
|
||||
workers = multiprocessing.cpu_count()
|
||||
workers = self.suggestedWorkerCount()
|
||||
if not hasattr(os, 'fork'):
|
||||
workers = 1
|
||||
self.workers = workers
|
||||
self.tasks = list(tasks)
|
||||
self.kwds = kwds
|
||||
self.reseed = randomReseed
|
||||
self.kwds = kwds.copy()
|
||||
self.kwds['_taskStarted'] = self._taskStarted
|
||||
|
||||
def __enter__(self):
|
||||
self.proc = None
|
||||
workers = self.workers
|
||||
if workers == 1:
|
||||
return Tasker(None, self.tasks, self.kwds)
|
||||
|
||||
if self.workers == 1:
|
||||
return self.runSerial()
|
||||
else:
|
||||
return self.runParallel()
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
|
||||
if self.proc is not None: ## worker
|
||||
try:
|
||||
if exc_info[0] is not None:
|
||||
sys.excepthook(*exc_info)
|
||||
finally:
|
||||
#print os.getpid(), 'exit'
|
||||
os._exit(0)
|
||||
|
||||
else: ## parent
|
||||
if self.showProgress:
|
||||
self.progressDlg.__exit__(None, None, None)
|
||||
|
||||
def runSerial(self):
|
||||
if self.showProgress:
|
||||
self.progressDlg.__enter__()
|
||||
self.progressDlg.setMaximum(len(self.tasks))
|
||||
self.progress = {os.getpid(): []}
|
||||
return Tasker(None, self.tasks, self.kwds)
|
||||
|
||||
|
||||
def runParallel(self):
|
||||
self.childs = []
|
||||
|
||||
## break up tasks into one set per worker
|
||||
workers = self.workers
|
||||
chunks = [[] for i in xrange(workers)]
|
||||
i = 0
|
||||
for i in range(len(self.tasks)):
|
||||
@ -65,37 +116,91 @@ class Parallelize:
|
||||
|
||||
## fork and assign tasks to each worker
|
||||
for i in range(workers):
|
||||
proc = ForkedProcess(target=None, preProxy=self.kwds)
|
||||
proc = ForkedProcess(target=None, preProxy=self.kwds, randomReseed=self.reseed)
|
||||
if not proc.isParent:
|
||||
self.proc = proc
|
||||
return Tasker(proc, chunks[i], proc.forkedProxies)
|
||||
else:
|
||||
self.childs.append(proc)
|
||||
|
||||
## process events from workers until all have exited.
|
||||
activeChilds = self.childs[:]
|
||||
while len(activeChilds) > 0:
|
||||
for ch in activeChilds:
|
||||
## Keep track of the progress of each worker independently.
|
||||
self.progress = {ch.childPid: [] for ch in self.childs}
|
||||
## for each child process, self.progress[pid] is a list
|
||||
## of task indexes. The last index is the task currently being
|
||||
## processed; all others are finished.
|
||||
|
||||
|
||||
try:
|
||||
if self.showProgress:
|
||||
self.progressDlg.__enter__()
|
||||
self.progressDlg.setMaximum(len(self.tasks))
|
||||
## process events from workers until all have exited.
|
||||
|
||||
activeChilds = self.childs[:]
|
||||
pollInterval = 0.01
|
||||
while len(activeChilds) > 0:
|
||||
waitingChildren = 0
|
||||
rem = []
|
||||
try:
|
||||
ch.processRequests()
|
||||
except ExitError:
|
||||
rem.append(ch)
|
||||
for ch in rem:
|
||||
activeChilds.remove(ch)
|
||||
time.sleep(0.1)
|
||||
|
||||
for ch in activeChilds:
|
||||
try:
|
||||
n = ch.processRequests()
|
||||
if n > 0:
|
||||
waitingChildren += 1
|
||||
except ExitError:
|
||||
#print ch.childPid, 'process finished'
|
||||
rem.append(ch)
|
||||
if self.showProgress:
|
||||
self.progressDlg += 1
|
||||
#print "remove:", [ch.childPid for ch in rem]
|
||||
for ch in rem:
|
||||
activeChilds.remove(ch)
|
||||
while True:
|
||||
try:
|
||||
os.waitpid(ch.childPid, 0)
|
||||
break
|
||||
except OSError as ex:
|
||||
if ex.errno == 4: ## If we get this error, just try again
|
||||
continue
|
||||
#print "Ignored system call interruption"
|
||||
else:
|
||||
raise
|
||||
|
||||
#print [ch.childPid for ch in activeChilds]
|
||||
|
||||
if self.showProgress and self.progressDlg.wasCanceled():
|
||||
for ch in activeChilds:
|
||||
ch.kill()
|
||||
raise CanceledError()
|
||||
|
||||
## adjust polling interval--prefer to get exactly 1 event per poll cycle.
|
||||
if waitingChildren > 1:
|
||||
pollInterval *= 0.7
|
||||
elif waitingChildren == 0:
|
||||
pollInterval /= 0.7
|
||||
pollInterval = max(min(pollInterval, 0.5), 0.0005) ## but keep it within reasonable limits
|
||||
|
||||
time.sleep(pollInterval)
|
||||
finally:
|
||||
if self.showProgress:
|
||||
self.progressDlg.__exit__(None, None, None)
|
||||
return [] ## no tasks for parent process.
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
if exc_info[0] is not None:
|
||||
sys.excepthook(*exc_info)
|
||||
if self.proc is not None:
|
||||
os._exit(0)
|
||||
|
||||
def wait(self):
|
||||
## wait for all child processes to finish
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def suggestedWorkerCount():
|
||||
return multiprocessing.cpu_count() ## is this really the best option?
|
||||
|
||||
def _taskStarted(self, pid, i, **kwds):
|
||||
## called remotely by tasker to indicate it has started working on task i
|
||||
#print pid, 'reported starting task', i
|
||||
if self.showProgress:
|
||||
if len(self.progress[pid]) > 0:
|
||||
self.progressDlg += 1
|
||||
if pid == os.getpid(): ## single-worker process
|
||||
if self.progressDlg.wasCanceled():
|
||||
raise CanceledError()
|
||||
self.progress[pid].append(i)
|
||||
|
||||
|
||||
class Tasker:
|
||||
def __init__(self, proc, tasks, kwds):
|
||||
@ -106,9 +211,13 @@ class Tasker:
|
||||
|
||||
def __iter__(self):
|
||||
## we could fix this up such that tasks are retrieved from the parent process one at a time..
|
||||
for task in self.tasks:
|
||||
for i, task in enumerate(self.tasks):
|
||||
self.index = i
|
||||
#print os.getpid(), 'starting task', i
|
||||
self._taskStarted(os.getpid(), i, _callSync='off')
|
||||
yield task
|
||||
if self.proc is not None:
|
||||
#print os.getpid(), 'no more tasks'
|
||||
self.proc.close()
|
||||
|
||||
|
||||
|
@ -1,10 +1,51 @@
|
||||
from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy
|
||||
import subprocess, atexit, os, sys, time, random, socket
|
||||
import subprocess, atexit, os, sys, time, random, socket, signal
|
||||
import cPickle as pickle
|
||||
import multiprocessing.connection
|
||||
|
||||
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError']
|
||||
|
||||
class Process(RemoteEventHandler):
|
||||
def __init__(self, name=None, target=None):
|
||||
"""
|
||||
Bases: RemoteEventHandler
|
||||
|
||||
This class is used to spawn and control a new python interpreter.
|
||||
It uses subprocess.Popen to start the new process and communicates with it
|
||||
using multiprocessing.Connection objects over a network socket.
|
||||
|
||||
By default, the remote process will immediately enter an event-processing
|
||||
loop that carries out requests send from the parent process.
|
||||
|
||||
Remote control works mainly through proxy objects::
|
||||
|
||||
proc = Process() ## starts process, returns handle
|
||||
rsys = proc._import('sys') ## asks remote process to import 'sys', returns
|
||||
## a proxy which references the imported module
|
||||
rsys.stdout.write('hello\n') ## This message will be printed from the remote
|
||||
## process. Proxy objects can usually be used
|
||||
## exactly as regular objects are.
|
||||
proc.close() ## Request the remote process shut down
|
||||
|
||||
Requests made via proxy objects may be synchronous or asynchronous and may
|
||||
return objects either by proxy or by value (if they are picklable). See
|
||||
ProxyObject for more information.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, target=None, copySysPath=True):
|
||||
"""
|
||||
============ =============================================================
|
||||
Arguments:
|
||||
name Optional name for this process used when printing messages
|
||||
from the remote process.
|
||||
target Optional function to call after starting remote process.
|
||||
By default, this is startEventLoop(), which causes the remote
|
||||
process to process requests from the parent process until it
|
||||
is asked to quit. If you wish to specify a different target,
|
||||
it must be picklable (bound methods are not).
|
||||
copySysPath If true, copy the contents of sys.path to the remote process
|
||||
============ =============================================================
|
||||
|
||||
"""
|
||||
if target is None:
|
||||
target = startEventLoop
|
||||
if name is None:
|
||||
@ -25,8 +66,12 @@ class Process(RemoteEventHandler):
|
||||
port += 1
|
||||
|
||||
## start remote process, instruct it to run target function
|
||||
self.proc = subprocess.Popen((sys.executable, __file__, 'remote'), stdin=subprocess.PIPE)
|
||||
pickle.dump((name+'_child', port, authkey, target), self.proc.stdin)
|
||||
sysPath = sys.path if copySysPath else None
|
||||
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
|
||||
self.proc = subprocess.Popen((sys.executable, bootstrap), stdin=subprocess.PIPE)
|
||||
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
|
||||
## set its sys.path properly before unpickling the target
|
||||
pickle.dump((name+'_child', port, authkey, targetStr, sysPath), self.proc.stdin)
|
||||
self.proc.stdin.close()
|
||||
|
||||
## open connection for remote process
|
||||
@ -60,19 +105,32 @@ def startEventLoop(name, port, authkey):
|
||||
class ForkedProcess(RemoteEventHandler):
|
||||
"""
|
||||
ForkedProcess is a substitute for Process that uses os.fork() to generate a new process.
|
||||
This is much faster than starting a completely new interpreter, but carries some caveats
|
||||
and limitations:
|
||||
- open file handles are shared with the parent process, which is potentially dangerous
|
||||
- it is not possible to have a QApplication in both parent and child process
|
||||
(unless both QApplications are created _after_ the call to fork())
|
||||
- generally not thread-safe. Also, threads are not copied by fork(); the new process
|
||||
will have only one thread that starts wherever fork() was called in the parent process.
|
||||
- forked processes are unceremoniously terminated when join() is called; they are not
|
||||
given any opportunity to clean up. (This prevents them calling any cleanup code that
|
||||
was only intended to be used by the parent process)
|
||||
This is much faster than starting a completely new interpreter and child processes
|
||||
automatically have a copy of the entire program state from before the fork. This
|
||||
makes it an appealing approach when parallelizing expensive computations. (see
|
||||
also Parallelizer)
|
||||
|
||||
However, fork() comes with some caveats and limitations:
|
||||
|
||||
- fork() is not available on Windows.
|
||||
- It is not possible to have a QApplication in both parent and child process
|
||||
(unless both QApplications are created _after_ the call to fork())
|
||||
Attempts by the forked process to access Qt GUI elements created by the parent
|
||||
will most likely cause the child to crash.
|
||||
- Likewise, database connections are unlikely to function correctly in a forked child.
|
||||
- Threads are not copied by fork(); the new process
|
||||
will have only one thread that starts wherever fork() was called in the parent process.
|
||||
- Forked processes are unceremoniously terminated when join() is called; they are not
|
||||
given any opportunity to clean up. (This prevents them calling any cleanup code that
|
||||
was only intended to be used by the parent process)
|
||||
- Normally when fork()ing, open file handles are shared with the parent process,
|
||||
which is potentially dangerous. ForkedProcess is careful to close all file handles
|
||||
that are not explicitly needed--stdout, stderr, and a single pipe to the parent
|
||||
process.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, target=0, preProxy=None):
|
||||
def __init__(self, name=None, target=0, preProxy=None, randomReseed=True):
|
||||
"""
|
||||
When initializing, an optional target may be given.
|
||||
If no target is specified, self.eventLoop will be used.
|
||||
@ -83,6 +141,9 @@ class ForkedProcess(RemoteEventHandler):
|
||||
in the remote process (but do not need to be sent explicitly since
|
||||
they are available immediately before the call to fork().
|
||||
Proxies will be availabe as self.proxies[name].
|
||||
|
||||
If randomReseed is True, the built-in random and numpy.random generators
|
||||
will be reseeded in the child process.
|
||||
"""
|
||||
self.hasJoined = False
|
||||
if target == 0:
|
||||
@ -101,16 +162,51 @@ class ForkedProcess(RemoteEventHandler):
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
self.isParent = False
|
||||
## We are now in the forked process; need to be extra careful what we touch while here.
|
||||
## - no reading/writing file handles/sockets owned by parent process (stdout is ok)
|
||||
## - don't touch QtGui or QApplication at all; these are landmines.
|
||||
## - don't let the process call exit handlers
|
||||
## -
|
||||
|
||||
## close all file handles we do not want shared with parent
|
||||
conn.close()
|
||||
sys.stdin.close() ## otherwise we screw with interactive prompts.
|
||||
fid = remoteConn.fileno()
|
||||
os.closerange(3, fid)
|
||||
os.closerange(fid+1, 4096) ## just guessing on the maximum descriptor count..
|
||||
|
||||
## Override any custom exception hooks
|
||||
def excepthook(*args):
|
||||
import traceback
|
||||
traceback.print_exception(*args)
|
||||
sys.excepthook = excepthook
|
||||
|
||||
## Make it harder to access QApplication instance
|
||||
if 'PyQt4.QtGui' in sys.modules:
|
||||
sys.modules['PyQt4.QtGui'].QApplication = None
|
||||
sys.modules.pop('PyQt4.QtGui', None)
|
||||
sys.modules.pop('PyQt4.QtCore', None)
|
||||
|
||||
## sabotage atexit callbacks
|
||||
atexit._exithandlers = []
|
||||
atexit.register(lambda: os._exit(0))
|
||||
|
||||
if randomReseed:
|
||||
if 'numpy.random' in sys.modules:
|
||||
sys.modules['numpy.random'].seed(os.getpid() ^ int(time.time()*10000%10000))
|
||||
if 'random' in sys.modules:
|
||||
sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000))
|
||||
|
||||
RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid())
|
||||
if target is not None:
|
||||
target()
|
||||
|
||||
|
||||
ppid = os.getppid()
|
||||
self.forkedProxies = {}
|
||||
for name, proxyId in proxyIDs.iteritems():
|
||||
self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name]))
|
||||
|
||||
if target is not None:
|
||||
target()
|
||||
|
||||
else:
|
||||
self.isParent = True
|
||||
self.childPid = pid
|
||||
@ -127,10 +223,11 @@ class ForkedProcess(RemoteEventHandler):
|
||||
self.processRequests() # exception raised when the loop should exit
|
||||
time.sleep(0.01)
|
||||
except ExitError:
|
||||
sys.exit(0)
|
||||
break
|
||||
except:
|
||||
print "Error occurred in forked event loop:"
|
||||
sys.excepthook(*sys.exc_info())
|
||||
sys.exit(0)
|
||||
|
||||
def join(self, timeout=10):
|
||||
if self.hasJoined:
|
||||
@ -138,10 +235,19 @@ class ForkedProcess(RemoteEventHandler):
|
||||
#os.kill(pid, 9)
|
||||
try:
|
||||
self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation.
|
||||
os.waitpid(self.childPid, 0)
|
||||
except IOError: ## probably remote process has already quit
|
||||
pass
|
||||
self.hasJoined = True
|
||||
|
||||
def kill(self):
|
||||
"""Immediately kill the forked remote process.
|
||||
This is generally safe because forked processes are already
|
||||
expected to _avoid_ any cleanup at exit."""
|
||||
os.kill(self.childPid, signal.SIGKILL)
|
||||
self.hasJoined = True
|
||||
|
||||
|
||||
|
||||
##Special set of subclasses that implement a Qt event loop instead.
|
||||
|
||||
@ -165,8 +271,33 @@ class RemoteQtEventHandler(RemoteEventHandler):
|
||||
#raise
|
||||
|
||||
class QtProcess(Process):
|
||||
def __init__(self, name=None):
|
||||
Process.__init__(self, name, target=startQtEventLoop)
|
||||
"""
|
||||
QtProcess is essentially the same as Process, with two major differences:
|
||||
|
||||
- The remote process starts by running startQtEventLoop() which creates a
|
||||
QApplication in the remote process and uses a QTimer to trigger
|
||||
remote event processing. This allows the remote process to have its own
|
||||
GUI.
|
||||
- A QTimer is also started on the parent process which polls for requests
|
||||
from the child process. This allows Qt signals emitted within the child
|
||||
process to invoke slots on the parent process and vice-versa.
|
||||
|
||||
Example::
|
||||
|
||||
proc = QtProcess()
|
||||
rQtGui = proc._import('PyQt4.QtGui')
|
||||
btn = rQtGui.QPushButton('button on child process')
|
||||
btn.show()
|
||||
|
||||
def slot():
|
||||
print 'slot invoked on parent process'
|
||||
btn.clicked.connect(proxy(slot)) # be sure to send a proxy of the slot
|
||||
"""
|
||||
|
||||
def __init__(self, **kwds):
|
||||
if 'target' not in kwds:
|
||||
kwds['target'] = startQtEventLoop
|
||||
Process.__init__(self, **kwds)
|
||||
self.startEventTimer()
|
||||
|
||||
def startEventTimer(self):
|
||||
@ -201,8 +332,3 @@ def startQtEventLoop(name, port, authkey):
|
||||
app.exec_()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2 and sys.argv[1] == 'remote': ## module has been invoked as script in new python interpreter.
|
||||
name, port, authkey, target = pickle.load(sys.stdin)
|
||||
target(name, port, authkey)
|
||||
sys.exit(0)
|
||||
|
@ -9,7 +9,26 @@ class NoResultError(Exception):
|
||||
|
||||
|
||||
class RemoteEventHandler(object):
|
||||
"""
|
||||
This class handles communication between two processes. One instance is present on
|
||||
each process and listens for communication from the other process. This enables
|
||||
(amongst other things) ObjectProxy instances to look up their attributes and call
|
||||
their methods.
|
||||
|
||||
This class is responsible for carrying out actions on behalf of the remote process.
|
||||
Each instance holds one end of a Connection which allows python
|
||||
objects to be passed between processes.
|
||||
|
||||
For the most common operations, see _import(), close(), and transfer()
|
||||
|
||||
To handle and respond to incoming requests, RemoteEventHandler requires that its
|
||||
processRequests method is called repeatedly (this is usually handled by the Process
|
||||
classes defined in multiprocess.processes).
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process
|
||||
## an object proxy belongs to
|
||||
|
||||
@ -55,19 +74,25 @@ class RemoteEventHandler(object):
|
||||
|
||||
def processRequests(self):
|
||||
"""Process all pending requests from the pipe, return
|
||||
after no more events are immediately available. (non-blocking)"""
|
||||
after no more events are immediately available. (non-blocking)
|
||||
Returns the number of events processed.
|
||||
"""
|
||||
if self.exited:
|
||||
raise ExitError()
|
||||
|
||||
numProcessed = 0
|
||||
while self.conn.poll():
|
||||
try:
|
||||
self.handleRequest()
|
||||
numProcessed += 1
|
||||
except ExitError:
|
||||
self.exited = True
|
||||
raise
|
||||
except:
|
||||
print "Error in process %s" % self.name
|
||||
sys.excepthook(*sys.exc_info())
|
||||
|
||||
return numProcessed
|
||||
|
||||
def handleRequest(self):
|
||||
"""Handle a single request from the remote process.
|
||||
@ -175,6 +200,7 @@ class RemoteEventHandler(object):
|
||||
self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result))
|
||||
|
||||
def replyError(self, reqId, *exc):
|
||||
print "error:", self.name, reqId, exc[1]
|
||||
excStr = traceback.format_exception(*exc)
|
||||
try:
|
||||
self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr))
|
||||
@ -282,7 +308,9 @@ class RemoteEventHandler(object):
|
||||
try:
|
||||
optStr = pickle.dumps(opts)
|
||||
except:
|
||||
print "Error pickling:", opts
|
||||
print "==== Error pickling this object: ===="
|
||||
print opts
|
||||
print "======================================="
|
||||
raise
|
||||
|
||||
request = (request, reqId, optStr)
|
||||
@ -381,8 +409,8 @@ class RemoteEventHandler(object):
|
||||
|
||||
def transfer(self, obj, **kwds):
|
||||
"""
|
||||
Transfer an object to the remote host (the object must be picklable) and return
|
||||
a proxy for the new remote object.
|
||||
Transfer an object by value to the remote host (the object must be picklable)
|
||||
and return a proxy for the new remote object.
|
||||
"""
|
||||
return self.send(request='transfer', opts=dict(obj=obj), **kwds)
|
||||
|
||||
@ -395,7 +423,12 @@ class RemoteEventHandler(object):
|
||||
|
||||
|
||||
class Request:
|
||||
## used internally for tracking asynchronous requests and returning results
|
||||
"""
|
||||
Request objects are returned when calling an ObjectProxy in asynchronous mode
|
||||
or if a synchronous call has timed out. Use hasResult() to ask whether
|
||||
the result of the call has been returned yet. Use result() to get
|
||||
the returned value.
|
||||
"""
|
||||
def __init__(self, process, reqId, description=None, timeout=10):
|
||||
self.proc = process
|
||||
self.description = description
|
||||
@ -405,10 +438,13 @@ class Request:
|
||||
self.timeout = timeout
|
||||
|
||||
def result(self, block=True, timeout=None):
|
||||
"""Return the result for this request.
|
||||
"""
|
||||
Return the result for this request.
|
||||
|
||||
If block is True, wait until the result has arrived or *timeout* seconds passes.
|
||||
If the timeout is reached, raise an exception. (use timeout=None to disable)
|
||||
If block is False, raises an exception if the result has not arrived yet."""
|
||||
If the timeout is reached, raise NoResultError. (use timeout=None to disable)
|
||||
If block is False, raise NoResultError immediately if the result has not arrived yet.
|
||||
"""
|
||||
|
||||
if self.gotResult:
|
||||
return self._result
|
||||
@ -434,16 +470,24 @@ class Request:
|
||||
def hasResult(self):
|
||||
"""Returns True if the result for this request has arrived."""
|
||||
try:
|
||||
#print "check result", self.description
|
||||
self.result(block=False)
|
||||
except NoResultError:
|
||||
#print " -> not yet"
|
||||
pass
|
||||
|
||||
return self.gotResult
|
||||
|
||||
class LocalObjectProxy(object):
|
||||
"""Used for wrapping local objects to ensure that they are send by proxy to a remote host."""
|
||||
"""
|
||||
Used for wrapping local objects to ensure that they are send by proxy to a remote host.
|
||||
Note that 'proxy' is just a shorter alias for LocalObjectProxy.
|
||||
|
||||
For example::
|
||||
|
||||
data = [1,2,3,4,5]
|
||||
remotePlot.plot(data) ## by default, lists are pickled and sent by value
|
||||
remotePlot.plot(proxy(data)) ## force the object to be sent by proxy
|
||||
|
||||
"""
|
||||
nextProxyId = 0
|
||||
proxiedObjects = {} ## maps {proxyId: object}
|
||||
|
||||
@ -467,24 +511,31 @@ class LocalObjectProxy(object):
|
||||
del cls.proxiedObjects[pid]
|
||||
#print "release:", cls.proxiedObjects
|
||||
|
||||
def __init__(self, obj):
|
||||
def __init__(self, obj, **opts):
|
||||
"""
|
||||
Create a 'local' proxy object that, when sent to a remote host,
|
||||
will appear as a normal ObjectProxy to *obj*.
|
||||
Any extra keyword arguments are passed to proxy._setProxyOptions()
|
||||
on the remote side.
|
||||
"""
|
||||
self.processId = os.getpid()
|
||||
#self.objectId = id(obj)
|
||||
self.typeStr = repr(obj)
|
||||
#self.handler = handler
|
||||
self.obj = obj
|
||||
self.opts = opts
|
||||
|
||||
def __reduce__(self):
|
||||
## a proxy is being pickled and sent to a remote process.
|
||||
## every time this happens, a new proxy will be generated in the remote process,
|
||||
## so we keep a new ID so we can track when each is released.
|
||||
pid = LocalObjectProxy.registerObject(self.obj)
|
||||
return (unpickleObjectProxy, (self.processId, pid, self.typeStr))
|
||||
return (unpickleObjectProxy, (self.processId, pid, self.typeStr, None, self.opts))
|
||||
|
||||
## alias
|
||||
proxy = LocalObjectProxy
|
||||
|
||||
def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None):
|
||||
def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None, opts=None):
|
||||
if processId == os.getpid():
|
||||
obj = LocalObjectProxy.lookupProxyId(proxyId)
|
||||
if attributes is not None:
|
||||
@ -492,7 +543,10 @@ def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None):
|
||||
obj = getattr(obj, attr)
|
||||
return obj
|
||||
else:
|
||||
return ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr)
|
||||
proxy = ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr)
|
||||
if opts is not None:
|
||||
proxy._setProxyOptions(**opts)
|
||||
return proxy
|
||||
|
||||
class ObjectProxy(object):
|
||||
"""
|
||||
@ -501,7 +555,44 @@ class ObjectProxy(object):
|
||||
attributes on existing proxy objects.
|
||||
|
||||
For the most part, this object can be used exactly as if it
|
||||
were a local object.
|
||||
were a local object::
|
||||
|
||||
rsys = proc._import('sys') # returns proxy to sys module on remote process
|
||||
rsys.stdout # proxy to remote sys.stdout
|
||||
rsys.stdout.write # proxy to remote sys.stdout.write
|
||||
rsys.stdout.write('hello') # calls sys.stdout.write('hello') on remote machine
|
||||
# and returns the result (None)
|
||||
|
||||
When calling a proxy to a remote function, the call can be made synchronous
|
||||
(result of call is returned immediately), asynchronous (result is returned later),
|
||||
or return can be disabled entirely::
|
||||
|
||||
ros = proc._import('os')
|
||||
|
||||
## synchronous call; result is returned immediately
|
||||
pid = ros.getpid()
|
||||
|
||||
## asynchronous call
|
||||
request = ros.getpid(_callSync='async')
|
||||
while not request.hasResult():
|
||||
time.sleep(0.01)
|
||||
pid = request.result()
|
||||
|
||||
## disable return when we know it isn't needed
|
||||
rsys.stdout.write('hello', _callSync='off')
|
||||
|
||||
Additionally, values returned from a remote function call are automatically
|
||||
returned either by value (must be picklable) or by proxy.
|
||||
This behavior can be forced::
|
||||
|
||||
rnp = proc._import('numpy')
|
||||
arrProxy = rnp.array([1,2,3,4], _returnType='proxy')
|
||||
arrValue = rnp.array([1,2,3,4], _returnType='value')
|
||||
|
||||
The default callSync and returnType behaviors (as well as others) can be set
|
||||
for each proxy individually using ObjectProxy._setProxyOptions() or globally using
|
||||
proc.setProxyOptions().
|
||||
|
||||
"""
|
||||
def __init__(self, processId, proxyId, typeStr='', parent=None):
|
||||
object.__init__(self)
|
||||
@ -574,6 +665,13 @@ class ObjectProxy(object):
|
||||
"""
|
||||
self._proxyOptions.update(kwds)
|
||||
|
||||
def _getValue(self):
|
||||
"""
|
||||
Return the value of the proxied object
|
||||
(the remote object must be picklable)
|
||||
"""
|
||||
return self._handler.getObjValue(self)
|
||||
|
||||
def _getProxyOption(self, opt):
|
||||
val = self._proxyOptions[opt]
|
||||
if val is None:
|
||||
@ -591,20 +689,31 @@ class ObjectProxy(object):
|
||||
return "<ObjectProxy for process %d, object 0x%x: %s >" % (self._processId, self._proxyId, self._typeStr)
|
||||
|
||||
|
||||
def __getattr__(self, attr):
|
||||
#if '_processId' not in self.__dict__:
|
||||
#raise Exception("ObjectProxy has no processId")
|
||||
#proc = Process._processes[self._processId]
|
||||
deferred = self._getProxyOption('deferGetattr')
|
||||
if deferred is True:
|
||||
def __getattr__(self, attr, **kwds):
|
||||
"""
|
||||
Calls __getattr__ on the remote object and returns the attribute
|
||||
by value or by proxy depending on the options set (see
|
||||
ObjectProxy._setProxyOptions and RemoteEventHandler.setProxyOptions)
|
||||
|
||||
If the option 'deferGetattr' is True for this proxy, then a new proxy object
|
||||
is returned _without_ asking the remote object whether the named attribute exists.
|
||||
This can save time when making multiple chained attribute requests,
|
||||
but may also defer a possible AttributeError until later, making
|
||||
them more difficult to debug.
|
||||
"""
|
||||
opts = self._getProxyOptions()
|
||||
for k in opts:
|
||||
if '_'+k in kwds:
|
||||
opts[k] = kwds.pop('_'+k)
|
||||
if opts['deferGetattr'] is True:
|
||||
return self._deferredAttr(attr)
|
||||
else:
|
||||
opts = self._getProxyOptions()
|
||||
#opts = self._getProxyOptions()
|
||||
return self._handler.getObjAttr(self, attr, **opts)
|
||||
|
||||
|
||||
def _deferredAttr(self, attr):
|
||||
return DeferredObjectProxy(self, attr)
|
||||
|
||||
|
||||
def __call__(self, *args, **kwds):
|
||||
"""
|
||||
Attempts to call the proxied object from the remote process.
|
||||
@ -613,44 +722,34 @@ class ObjectProxy(object):
|
||||
_callSync 'off', 'sync', or 'async'
|
||||
_returnType 'value', 'proxy', or 'auto'
|
||||
|
||||
If the remote call raises an exception on the remote process,
|
||||
it will be re-raised on the local process.
|
||||
|
||||
"""
|
||||
#opts = {}
|
||||
#callSync = kwds.pop('_callSync', self.)
|
||||
#if callSync is not None:
|
||||
#opts['callSync'] = callSync
|
||||
#returnType = kwds.pop('_returnType', self._defaultReturnValue)
|
||||
#if returnType is not None:
|
||||
#opts['returnType'] = returnType
|
||||
opts = self._getProxyOptions()
|
||||
for k in opts:
|
||||
if '_'+k in kwds:
|
||||
opts[k] = kwds.pop('_'+k)
|
||||
#print "call", opts
|
||||
return self._handler.callObj(obj=self, args=args, kwds=kwds, **opts)
|
||||
|
||||
def _getValue(self):
|
||||
## this just gives us an easy way to change the behavior of the special methods
|
||||
#proc = Process._processes[self._processId]
|
||||
return self._handler.getObjValue(self)
|
||||
|
||||
|
||||
## Explicitly proxy special methods. Is there a better way to do this??
|
||||
|
||||
def _getSpecialAttr(self, attr):
|
||||
#return self.__getattr__(attr)
|
||||
## this just gives us an easy way to change the behavior of the special methods
|
||||
return self._deferredAttr(attr)
|
||||
|
||||
def __getitem__(self, *args):
|
||||
return self._getSpecialAttr('__getitem__')(*args)
|
||||
|
||||
def __setitem__(self, *args):
|
||||
return self._getSpecialAttr('__setitem__')(*args)
|
||||
return self._getSpecialAttr('__setitem__')(*args, _callSync='off')
|
||||
|
||||
def __setattr__(self, *args):
|
||||
return self._getSpecialAttr('__setattr__')(*args)
|
||||
return self._getSpecialAttr('__setattr__')(*args, _callSync='off')
|
||||
|
||||
def __str__(self, *args):
|
||||
return self._getSpecialAttr('__str__')(*args, _returnType=True)
|
||||
return self._getSpecialAttr('__str__')(*args, _returnType='value')
|
||||
|
||||
def __len__(self, *args):
|
||||
return self._getSpecialAttr('__len__')(*args)
|
||||
@ -670,6 +769,21 @@ class ObjectProxy(object):
|
||||
def __pow__(self, *args):
|
||||
return self._getSpecialAttr('__pow__')(*args)
|
||||
|
||||
def __iadd__(self, *args):
|
||||
return self._getSpecialAttr('__iadd__')(*args, _callSync='off')
|
||||
|
||||
def __isub__(self, *args):
|
||||
return self._getSpecialAttr('__isub__')(*args, _callSync='off')
|
||||
|
||||
def __idiv__(self, *args):
|
||||
return self._getSpecialAttr('__idiv__')(*args, _callSync='off')
|
||||
|
||||
def __imul__(self, *args):
|
||||
return self._getSpecialAttr('__imul__')(*args, _callSync='off')
|
||||
|
||||
def __ipow__(self, *args):
|
||||
return self._getSpecialAttr('__ipow__')(*args, _callSync='off')
|
||||
|
||||
def __rshift__(self, *args):
|
||||
return self._getSpecialAttr('__rshift__')(*args)
|
||||
|
||||
@ -679,6 +793,15 @@ class ObjectProxy(object):
|
||||
def __floordiv__(self, *args):
|
||||
return self._getSpecialAttr('__pow__')(*args)
|
||||
|
||||
def __irshift__(self, *args):
|
||||
return self._getSpecialAttr('__rshift__')(*args, _callSync='off')
|
||||
|
||||
def __ilshift__(self, *args):
|
||||
return self._getSpecialAttr('__lshift__')(*args, _callSync='off')
|
||||
|
||||
def __ifloordiv__(self, *args):
|
||||
return self._getSpecialAttr('__pow__')(*args, _callSync='off')
|
||||
|
||||
def __eq__(self, *args):
|
||||
return self._getSpecialAttr('__eq__')(*args)
|
||||
|
||||
@ -704,7 +827,16 @@ class ObjectProxy(object):
|
||||
return self._getSpecialAttr('__or__')(*args)
|
||||
|
||||
def __xor__(self, *args):
|
||||
return self._getSpecialAttr('__or__')(*args)
|
||||
return self._getSpecialAttr('__xor__')(*args)
|
||||
|
||||
def __iand__(self, *args):
|
||||
return self._getSpecialAttr('__iand__')(*args, _callSync='off')
|
||||
|
||||
def __ior__(self, *args):
|
||||
return self._getSpecialAttr('__ior__')(*args, _callSync='off')
|
||||
|
||||
def __ixor__(self, *args):
|
||||
return self._getSpecialAttr('__ixor__')(*args, _callSync='off')
|
||||
|
||||
def __mod__(self, *args):
|
||||
return self._getSpecialAttr('__mod__')(*args)
|
||||
@ -746,6 +878,37 @@ class ObjectProxy(object):
|
||||
return self._getSpecialAttr('__rmod__')(*args)
|
||||
|
||||
class DeferredObjectProxy(ObjectProxy):
|
||||
"""
|
||||
This class represents an attribute (or sub-attribute) of a proxied object.
|
||||
It is used to speed up attribute requests. Take the following scenario::
|
||||
|
||||
rsys = proc._import('sys')
|
||||
rsys.stdout.write('hello')
|
||||
|
||||
For this simple example, a total of 4 synchronous requests are made to
|
||||
the remote process:
|
||||
|
||||
1) import sys
|
||||
2) getattr(sys, 'stdout')
|
||||
3) getattr(stdout, 'write')
|
||||
4) write('hello')
|
||||
|
||||
This takes a lot longer than running the equivalent code locally. To
|
||||
speed things up, we can 'defer' the two attribute lookups so they are
|
||||
only carried out when neccessary::
|
||||
|
||||
rsys = proc._import('sys')
|
||||
rsys._setProxyOptions(deferGetattr=True)
|
||||
rsys.stdout.write('hello')
|
||||
|
||||
This example only makes two requests to the remote process; the two
|
||||
attribute lookups immediately return DeferredObjectProxy instances
|
||||
immediately without contacting the remote process. When the call
|
||||
to write() is made, all attribute requests are processed at the same time.
|
||||
|
||||
Note that if the attributes requested do not exist on the remote object,
|
||||
making the call to write() will raise an AttributeError.
|
||||
"""
|
||||
def __init__(self, parentProxy, attribute):
|
||||
## can't set attributes directly because setattr is overridden.
|
||||
for k in ['_processId', '_typeStr', '_proxyId', '_handler']:
|
||||
@ -756,4 +919,10 @@ class DeferredObjectProxy(ObjectProxy):
|
||||
|
||||
def __repr__(self):
|
||||
return ObjectProxy.__repr__(self) + '.' + '.'.join(self._attributes)
|
||||
|
||||
|
||||
def _undefer(self):
|
||||
"""
|
||||
Return a non-deferred ObjectProxy referencing the same object
|
||||
"""
|
||||
return self._parent.__getattr__(self._attributes[-1], _deferGetattr=False)
|
||||
|
||||
|
@ -29,12 +29,18 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
self.keysPressed = {}
|
||||
self.keyTimer = QtCore.QTimer()
|
||||
self.keyTimer.timeout.connect(self.evalKeyState)
|
||||
|
||||
self.makeCurrent()
|
||||
|
||||
def addItem(self, item):
|
||||
self.items.append(item)
|
||||
if hasattr(item, 'initializeGL'):
|
||||
self.makeCurrent()
|
||||
item.initializeGL()
|
||||
try:
|
||||
item.initializeGL()
|
||||
except:
|
||||
self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item))
|
||||
|
||||
item._setView(self)
|
||||
#print "set view", item, self, item.view()
|
||||
self.update()
|
||||
@ -100,20 +106,28 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
glPushAttrib(GL_ALL_ATTRIB_BITS)
|
||||
i.paint()
|
||||
except:
|
||||
import sys
|
||||
sys.excepthook(*sys.exc_info())
|
||||
print("Error while drawing item", i)
|
||||
import pyqtgraph.debug
|
||||
pyqtgraph.debug.printExc()
|
||||
msg = "Error while drawing item %s." % str(item)
|
||||
ver = glGetString(GL_VERSION).split()[0]
|
||||
if int(ver.split('.')[0]) < 2:
|
||||
print(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
|
||||
else:
|
||||
print(msg)
|
||||
|
||||
finally:
|
||||
glPopAttrib(GL_ALL_ATTRIB_BITS)
|
||||
glPopAttrib()
|
||||
else:
|
||||
glMatrixMode(GL_MODELVIEW)
|
||||
glPushMatrix()
|
||||
tr = i.transform()
|
||||
a = np.array(tr.copyDataTo()).reshape((4,4))
|
||||
glMultMatrixf(a.transpose())
|
||||
self.drawItemTree(i)
|
||||
glMatrixMode(GL_MODELVIEW)
|
||||
glPopMatrix()
|
||||
try:
|
||||
tr = i.transform()
|
||||
a = np.array(tr.copyDataTo()).reshape((4,4))
|
||||
glMultMatrixf(a.transpose())
|
||||
self.drawItemTree(i)
|
||||
finally:
|
||||
glMatrixMode(GL_MODELVIEW)
|
||||
glPopMatrix()
|
||||
|
||||
|
||||
def cameraPosition(self):
|
||||
@ -237,4 +251,15 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
else:
|
||||
self.keyTimer.stop()
|
||||
|
||||
def checkOpenGLVersion(self, msg):
|
||||
## Only to be called from within exception handler.
|
||||
ver = glGetString(GL_VERSION).split()[0]
|
||||
if int(ver.split('.')[0]) < 2:
|
||||
import pyqtgraph.debug
|
||||
pyqtgraph.debug.printExc()
|
||||
raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
|
16
opengl/glInfo.py
Normal file
16
opengl/glInfo.py
Normal 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()
|
||||
|
||||
|
@ -43,6 +43,11 @@ class GLVolumeItem(GLGraphicsItem):
|
||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER)
|
||||
shape = self.data.shape
|
||||
|
||||
## Test texture dimensions first
|
||||
glTexImage3D(GL_PROXY_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
|
||||
if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, 0, GL_TEXTURE_WIDTH) == 0:
|
||||
raise Exception("OpenGL failed to create 3D texture (%dx%dx%d); too large for this hardware." % shape[:3])
|
||||
|
||||
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data.transpose((2,1,0,3)))
|
||||
glDisable(GL_TEXTURE_3D)
|
||||
|
||||
|
@ -131,11 +131,16 @@ class Parameter(QtCore.QObject):
|
||||
return name
|
||||
|
||||
def childPath(self, child):
|
||||
"""Return the path of parameter names from self to child."""
|
||||
"""
|
||||
Return the path of parameter names from self to child.
|
||||
If child is not a (grand)child of self, return None.
|
||||
"""
|
||||
path = []
|
||||
while child is not self:
|
||||
path.insert(0, child.name())
|
||||
child = child.parent()
|
||||
if child is None:
|
||||
return None
|
||||
return path
|
||||
|
||||
def setValue(self, value, blockSignal=None):
|
||||
|
@ -113,4 +113,6 @@ class ParameterTree(TreeWidget):
|
||||
sel[0].selected(True)
|
||||
return TreeWidget.selectionChanged(self, *args)
|
||||
|
||||
|
||||
def wheelEvent(self, ev):
|
||||
self.clearSelection()
|
||||
return TreeWidget.wheelEvent(self, ev)
|
||||
|
@ -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_()
|
@ -1,4 +1,5 @@
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
from pyqtgraph.python2_3 import asUnicode
|
||||
from .Parameter import Parameter, registerParameterType
|
||||
from .ParameterItem import ParameterItem
|
||||
from pyqtgraph.widgets.SpinBox import SpinBox
|
||||
@ -348,6 +349,7 @@ class GroupParameterItem(ParameterItem):
|
||||
|
||||
def treeWidgetChanged(self):
|
||||
ParameterItem.treeWidgetChanged(self)
|
||||
self.treeWidget().setFirstItemColumnSpanned(self, True)
|
||||
if self.addItem is not None:
|
||||
self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox)
|
||||
self.treeWidget().setFirstItemColumnSpanned(self.addItem, True)
|
||||
|
13
python2_3.py
13
python2_3.py
@ -42,8 +42,9 @@ def sortList(l, cmpFunc):
|
||||
if sys.version_info[0] == 3:
|
||||
import builtins
|
||||
builtins.basestring = str
|
||||
builtins.asUnicode = asUnicode
|
||||
builtins.sortList = sortList
|
||||
#builtins.asUnicode = asUnicode
|
||||
#builtins.sortList = sortList
|
||||
basestring = str
|
||||
def cmp(a,b):
|
||||
if a>b:
|
||||
return 1
|
||||
@ -52,7 +53,7 @@ if sys.version_info[0] == 3:
|
||||
else:
|
||||
return 0
|
||||
builtins.cmp = cmp
|
||||
else:
|
||||
import __builtin__
|
||||
__builtin__.asUnicode = asUnicode
|
||||
__builtin__.sortList = sortList
|
||||
#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe
|
||||
#import __builtin__
|
||||
#__builtin__.asUnicode = asUnicode
|
||||
#__builtin__.sortList = sortList
|
||||
|
@ -34,6 +34,7 @@ def reloadAll(prefix=None, debug=False):
|
||||
- Skips reload if the file has not been updated (if .pyc is newer than .py)
|
||||
- if prefix is None, checks all loaded modules
|
||||
"""
|
||||
failed = []
|
||||
for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload
|
||||
if not inspect.ismodule(mod):
|
||||
continue
|
||||
@ -58,7 +59,10 @@ def reloadAll(prefix=None, debug=False):
|
||||
reload(mod, debug=debug)
|
||||
except:
|
||||
printExc("Error while reloading module %s, skipping\n" % mod)
|
||||
|
||||
failed.append(mod.__name__)
|
||||
|
||||
if len(failed) > 0:
|
||||
raise Exception("Some modules failed to reload: %s" % ', '.join(failed))
|
||||
|
||||
def reload(module, debug=False, lists=False, dicts=False):
|
||||
"""Replacement for the builtin reload function:
|
||||
|
@ -15,7 +15,7 @@ class GradientWidget(GraphicsView):
|
||||
|
||||
def __init__(self, parent=None, orientation='bottom', *args, **kargs):
|
||||
GraphicsView.__init__(self, parent, useOpenGL=False, background=None)
|
||||
self.maxDim = 27
|
||||
self.maxDim = 31
|
||||
kargs['tickPen'] = 'k'
|
||||
self.item = GradientEditorItem(*args, **kargs)
|
||||
self.item.sigGradientChanged.connect(self.sigGradientChanged)
|
||||
@ -24,7 +24,7 @@ class GradientWidget(GraphicsView):
|
||||
self.setCacheMode(self.CacheNone)
|
||||
self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing)
|
||||
self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain)
|
||||
self.setBackgroundRole(QtGui.QPalette.NoRole)
|
||||
#self.setBackgroundRole(QtGui.QPalette.NoRole)
|
||||
#self.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
|
||||
#self.setAutoFillBackground(False)
|
||||
#self.setAttribute(QtCore.Qt.WA_PaintOnScreen, False)
|
||||
|
@ -6,6 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
"""
|
||||
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import pyqtgraph as pg
|
||||
|
||||
try:
|
||||
from pyqtgraph.Qt import QtOpenGL
|
||||
@ -13,12 +14,8 @@ try:
|
||||
except ImportError:
|
||||
HAVE_OPENGL = False
|
||||
|
||||
#from numpy import vstack
|
||||
#import time
|
||||
from pyqtgraph.Point import Point
|
||||
#from vector import *
|
||||
import sys, os
|
||||
#import debug
|
||||
from .FileDialog import FileDialog
|
||||
from pyqtgraph.GraphicsScene import GraphicsScene
|
||||
import numpy as np
|
||||
@ -29,6 +26,20 @@ import pyqtgraph
|
||||
__all__ = ['GraphicsView']
|
||||
|
||||
class GraphicsView(QtGui.QGraphicsView):
|
||||
"""Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the
|
||||
viewed coordinate range. Also automatically creates a GraphicsScene and a central QGraphicsWidget
|
||||
that is automatically scaled to the full view geometry.
|
||||
|
||||
This widget is the basis for :class:`PlotWidget <pyqtgraph.PlotWidget>`,
|
||||
:class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and the view widget in
|
||||
:class:`ImageView <pyqtgraph.ImageView>`.
|
||||
|
||||
By default, the view coordinate system matches the widget's pixel coordinates and
|
||||
automatically updates when the view is resized. This can be overridden by setting
|
||||
autoPixelRange=False. The exact visible range can be set with setRange().
|
||||
|
||||
The view can be panned using the middle mouse button and scaled using the right mouse button if
|
||||
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
|
||||
|
||||
sigRangeChanged = QtCore.Signal(object, object)
|
||||
sigMouseReleased = QtCore.Signal(object)
|
||||
@ -37,17 +48,25 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
sigScaleChanged = QtCore.Signal(object)
|
||||
lastFileDir = None
|
||||
|
||||
def __init__(self, parent=None, useOpenGL=None, background='k'):
|
||||
"""Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the
|
||||
viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget
|
||||
that is automatically scaled to the full view geometry.
|
||||
def __init__(self, parent=None, useOpenGL=None, background='default'):
|
||||
"""
|
||||
============ ============================================================
|
||||
Arguments:
|
||||
parent Optional parent widget
|
||||
useOpenGL If True, the GraphicsView will use OpenGL to do all of its
|
||||
rendering. This can improve performance on some systems,
|
||||
but may also introduce bugs (the combination of
|
||||
QGraphicsView and QGLWidget is still an 'experimental'
|
||||
feature of Qt)
|
||||
background Set the background color of the GraphicsView. Accepts any
|
||||
single argument accepted by
|
||||
:func:`mkColor <pyqtgraph.mkColor>`. By
|
||||
default, the background color is determined using the
|
||||
'backgroundColor' configuration option (see
|
||||
:func:`setConfigOption <pyqtgraph.setConfigOption>`.
|
||||
============ ============================================================
|
||||
"""
|
||||
|
||||
By default, the view coordinate system matches the widget's pixel coordinates and
|
||||
automatically updates when the view is resized. This can be overridden by setting
|
||||
autoPixelRange=False. The exact visible range can be set with setRange().
|
||||
|
||||
The view can be panned using the middle mouse button and scaled using the right mouse button if
|
||||
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
|
||||
self.closed = False
|
||||
|
||||
QtGui.QGraphicsView.__init__(self, parent)
|
||||
@ -62,9 +81,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
## This might help, but it's probably dangerous in the general case..
|
||||
#self.setOptimizationFlag(self.DontSavePainterState, True)
|
||||
|
||||
if background is not None:
|
||||
brush = fn.mkBrush(background)
|
||||
self.setBackgroundBrush(brush)
|
||||
self.setBackground(background)
|
||||
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.setFrameShape(QtGui.QFrame.NoFrame)
|
||||
@ -75,13 +92,10 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate)
|
||||
|
||||
|
||||
#self.setSceneRect(QtCore.QRectF(-1e10, -1e10, 2e10, 2e10))
|
||||
|
||||
self.lockedViewports = []
|
||||
self.lastMousePos = None
|
||||
self.setMouseTracking(True)
|
||||
self.aspectLocked = False
|
||||
#self.yInverted = True
|
||||
self.range = QtCore.QRectF(0, 0, 1, 1)
|
||||
self.autoPixelRange = True
|
||||
self.currentItem = None
|
||||
@ -90,6 +104,11 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
self.sceneObj = GraphicsScene()
|
||||
self.setScene(self.sceneObj)
|
||||
|
||||
## Workaround for PySide crash
|
||||
## This ensures that the scene will outlive the view.
|
||||
if pyqtgraph.Qt.USE_PYSIDE:
|
||||
self.sceneObj._view_ref_workaround = self
|
||||
|
||||
## by default we set up a central widget with a grid layout.
|
||||
## this can be replaced if needed.
|
||||
self.centralWidget = None
|
||||
@ -101,15 +120,25 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False)
|
||||
self.clickAccepted = False
|
||||
|
||||
#def paintEvent(self, *args):
|
||||
#prof = debug.Profiler('GraphicsView.paintEvent '+str(id(self)), disabled=False)
|
||||
#QtGui.QGraphicsView.paintEvent(self, *args)
|
||||
#prof.finish()
|
||||
|
||||
def setBackground(self, background):
|
||||
"""
|
||||
Set the background color of the GraphicsView.
|
||||
To use the defaults specified py pyqtgraph.setConfigOption, use background='default'.
|
||||
To make the background transparent, use background=None.
|
||||
"""
|
||||
self._background = background
|
||||
if background == 'default':
|
||||
background = pyqtgraph.getConfigOption('background')
|
||||
if background is None:
|
||||
self.setBackgroundRole(QtGui.QPalette.NoRole)
|
||||
else:
|
||||
brush = fn.mkBrush(background)
|
||||
self.setBackgroundBrush(brush)
|
||||
|
||||
|
||||
def close(self):
|
||||
self.centralWidget = None
|
||||
self.scene().clear()
|
||||
#print " ", self.scene().itemCount()
|
||||
self.currentItem = None
|
||||
self.sceneObj = None
|
||||
self.closed = True
|
||||
@ -123,11 +152,9 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
else:
|
||||
v = QtGui.QWidget()
|
||||
|
||||
#v.setStyleSheet("background-color: #000000;")
|
||||
self.setViewport(v)
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
#QtGui.QGraphicsView.keyPressEvent(self, ev)
|
||||
self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene
|
||||
## (view likes to eat arrow key events)
|
||||
|
||||
@ -136,7 +163,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
return self.setCentralWidget(item)
|
||||
|
||||
def setCentralWidget(self, item):
|
||||
"""Sets a QGraphicsWidget to automatically fill the entire view."""
|
||||
"""Sets a QGraphicsWidget to automatically fill the entire view (the item will be automatically
|
||||
resize whenever the GraphicsView is resized)."""
|
||||
if self.centralWidget is not None:
|
||||
self.scene().removeItem(self.centralWidget)
|
||||
self.centralWidget = item
|
||||
@ -162,15 +190,18 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
return
|
||||
if self.autoPixelRange:
|
||||
self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height())
|
||||
GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False)
|
||||
GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way.
|
||||
self.updateMatrix()
|
||||
|
||||
def updateMatrix(self, propagate=True):
|
||||
self.setSceneRect(self.range)
|
||||
if self.aspectLocked:
|
||||
self.fitInView(self.range, QtCore.Qt.KeepAspectRatio)
|
||||
if self.autoPixelRange:
|
||||
self.resetTransform()
|
||||
else:
|
||||
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
||||
if self.aspectLocked:
|
||||
self.fitInView(self.range, QtCore.Qt.KeepAspectRatio)
|
||||
else:
|
||||
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
||||
|
||||
self.sigRangeChanged.emit(self, self.range)
|
||||
|
||||
@ -196,11 +227,6 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
scale = [sx, sy]
|
||||
if self.aspectLocked:
|
||||
scale[0] = scale[1]
|
||||
#adj = (self.range.width()*0.5*(1.0-(1.0/scale[0])), self.range.height()*0.5*(1.0-(1.0/scale[1])))
|
||||
#print "======\n", scale, adj
|
||||
#print self.range
|
||||
#self.range.adjust(adj[0], adj[1], -adj[0], -adj[1])
|
||||
#print self.range
|
||||
|
||||
if self.scaleCenter:
|
||||
center = None
|
||||
@ -270,13 +296,6 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
r1.setBottom(r.bottom())
|
||||
GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False)
|
||||
|
||||
#def invertY(self, invert=True):
|
||||
##if self.yInverted != invert:
|
||||
##self.scale[1] *= -1.
|
||||
#self.yInverted = invert
|
||||
#self.updateMatrix()
|
||||
|
||||
|
||||
def wheelEvent(self, ev):
|
||||
QtGui.QGraphicsView.wheelEvent(self, ev)
|
||||
if not self.mouseEnabled:
|
||||
@ -289,39 +308,11 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
def setAspectLocked(self, s):
|
||||
self.aspectLocked = s
|
||||
|
||||
#def mouseDoubleClickEvent(self, ev):
|
||||
#QtGui.QGraphicsView.mouseDoubleClickEvent(self, ev)
|
||||
#pass
|
||||
|
||||
### This function is here because interactive mode is disabled due to bugs.
|
||||
#def graphicsSceneEvent(self, ev, pev=None, fev=None):
|
||||
#ev1 = GraphicsSceneMouseEvent()
|
||||
#ev1.setPos(QtCore.QPointF(ev.pos().x(), ev.pos().y()))
|
||||
#ev1.setButtons(ev.buttons())
|
||||
#ev1.setButton(ev.button())
|
||||
#ev1.setModifiers(ev.modifiers())
|
||||
#ev1.setScenePos(self.mapToScene(QtCore.QPoint(ev.pos())))
|
||||
#if pev is not None:
|
||||
#ev1.setLastPos(pev.pos())
|
||||
#ev1.setLastScenePos(pev.scenePos())
|
||||
#ev1.setLastScreenPos(pev.screenPos())
|
||||
#if fev is not None:
|
||||
#ev1.setButtonDownPos(fev.pos())
|
||||
#ev1.setButtonDownScenePos(fev.scenePos())
|
||||
#ev1.setButtonDownScreenPos(fev.screenPos())
|
||||
#return ev1
|
||||
|
||||
def leaveEvent(self, ev):
|
||||
self.scene().leaveEvent(ev) ## inform scene when mouse leaves
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
QtGui.QGraphicsView.mousePressEvent(self, ev)
|
||||
|
||||
#print "Press over:"
|
||||
#for i in self.items(ev.pos()):
|
||||
# print i.zValue(), int(i.acceptedMouseButtons()), i, i.scenePos()
|
||||
#print "Event accepted:", ev.isAccepted()
|
||||
#print "Grabber:", self.scene().mouseGrabberItem()
|
||||
|
||||
|
||||
if not self.mouseEnabled:
|
||||
@ -333,39 +324,14 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
self.scene().clearSelection()
|
||||
return ## Everything below disabled for now..
|
||||
|
||||
#self.currentItem = None
|
||||
#maxZ = None
|
||||
#for i in self.items(ev.pos()):
|
||||
#if maxZ is None or maxZ < i.zValue():
|
||||
#self.currentItem = i
|
||||
#maxZ = i.zValue()
|
||||
#print "make event"
|
||||
#self.pev = self.graphicsSceneEvent(ev)
|
||||
#self.fev = self.pev
|
||||
#if self.currentItem is not None:
|
||||
#self.currentItem.mousePressEvent(self.pev)
|
||||
##self.clearMouse()
|
||||
##self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
|
||||
#self.emit(QtCore.SIGNAL("mousePressed(PyQt_PyObject)"), self.mouseTrail)
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
QtGui.QGraphicsView.mouseReleaseEvent(self, ev)
|
||||
if not self.mouseEnabled:
|
||||
return
|
||||
#self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
|
||||
#self.emit(QtCore.SIGNAL("mouseReleased"), ev)
|
||||
self.sigMouseReleased.emit(ev)
|
||||
self.lastButtonReleased = ev.button()
|
||||
return ## Everything below disabled for now..
|
||||
|
||||
##self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
|
||||
#self.emit(QtCore.SIGNAL("mouseReleased(PyQt_PyObject)"), self.mouseTrail)
|
||||
#if self.currentItem is not None:
|
||||
#pev = self.graphicsSceneEvent(ev, self.pev, self.fev)
|
||||
#self.pev = pev
|
||||
#self.currentItem.mouseReleaseEvent(pev)
|
||||
#self.currentItem = None
|
||||
|
||||
def mouseMoveEvent(self, ev):
|
||||
if self.lastMousePos is None:
|
||||
self.lastMousePos = Point(ev.pos())
|
||||
@ -375,10 +341,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
QtGui.QGraphicsView.mouseMoveEvent(self, ev)
|
||||
if not self.mouseEnabled:
|
||||
return
|
||||
#self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos()))
|
||||
self.sigSceneMouseMoved.emit(self.mapToScene(ev.pos()))
|
||||
#print "moved. Grabber:", self.scene().mouseGrabberItem()
|
||||
|
||||
|
||||
if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it.
|
||||
return
|
||||
@ -386,10 +349,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if ev.buttons() == QtCore.Qt.RightButton:
|
||||
delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50))
|
||||
scale = 1.01 ** delta
|
||||
#if self.yInverted:
|
||||
#scale[0] = 1. / scale[0]
|
||||
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
|
||||
#self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range)
|
||||
self.sigRangeChanged.emit(self, self.range)
|
||||
|
||||
elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button.
|
||||
@ -397,23 +357,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
tr = -delta * px
|
||||
|
||||
self.translate(tr[0], tr[1])
|
||||
#self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range)
|
||||
self.sigRangeChanged.emit(self, self.range)
|
||||
|
||||
#return ## Everything below disabled for now..
|
||||
|
||||
##self.mouseTrail.append(Point(self.mapToScene(ev.pos())))
|
||||
#if self.currentItem is not None:
|
||||
#pev = self.graphicsSceneEvent(ev, self.pev, self.fev)
|
||||
#self.pev = pev
|
||||
#self.currentItem.mouseMoveEvent(pev)
|
||||
|
||||
#def paintEvent(self, ev):
|
||||
#prof = debug.Profiler('GraphicsView.paintEvent (0x%x)' % id(self))
|
||||
#QtGui.QGraphicsView.paintEvent(self, ev)
|
||||
#prof.finish()
|
||||
|
||||
|
||||
def pixelSize(self):
|
||||
"""Return vector with the length and width of one view pixel in scene coordinates"""
|
||||
p0 = Point(0,0)
|
||||
@ -423,80 +368,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
p11 = tr.map(p1)
|
||||
return Point(p11 - p01)
|
||||
|
||||
|
||||
#def writeSvg(self, fileName=None):
|
||||
#if fileName is None:
|
||||
#self.fileDialog = FileDialog()
|
||||
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
|
||||
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
#if GraphicsView.lastFileDir is not None:
|
||||
#self.fileDialog.setDirectory(GraphicsView.lastFileDir)
|
||||
#self.fileDialog.show()
|
||||
#self.fileDialog.fileSelected.connect(self.writeSvg)
|
||||
#return
|
||||
#fileName = str(fileName)
|
||||
#GraphicsView.lastFileDir = os.path.split(fileName)[0]
|
||||
#self.svg = QtSvg.QSvgGenerator()
|
||||
#self.svg.setFileName(fileName)
|
||||
#self.svg.setSize(self.size())
|
||||
#self.svg.setResolution(600)
|
||||
#painter = QtGui.QPainter(self.svg)
|
||||
#self.render(painter)
|
||||
|
||||
#def writeImage(self, fileName=None):
|
||||
#if fileName is None:
|
||||
#self.fileDialog = FileDialog()
|
||||
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
|
||||
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) ## this is the line that makes the fileDialog not show on mac
|
||||
#if GraphicsView.lastFileDir is not None:
|
||||
#self.fileDialog.setDirectory(GraphicsView.lastFileDir)
|
||||
#self.fileDialog.show()
|
||||
#self.fileDialog.fileSelected.connect(self.writeImage)
|
||||
#return
|
||||
#fileName = str(fileName)
|
||||
#GraphicsView.lastFileDir = os.path.split(fileName)[0]
|
||||
#self.png = QtGui.QImage(self.size(), QtGui.QImage.Format_ARGB32)
|
||||
#painter = QtGui.QPainter(self.png)
|
||||
#rh = self.renderHints()
|
||||
#self.setRenderHints(QtGui.QPainter.Antialiasing)
|
||||
#self.render(painter)
|
||||
#self.setRenderHints(rh)
|
||||
#self.png.save(fileName)
|
||||
|
||||
#def writePs(self, fileName=None):
|
||||
#if fileName is None:
|
||||
#self.fileDialog = FileDialog()
|
||||
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
|
||||
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
#self.fileDialog.show()
|
||||
#self.fileDialog.fileSelected.connect(self.writePs)
|
||||
#return
|
||||
##if fileName is None:
|
||||
## fileName = str(QtGui.QFileDialog.getSaveFileName())
|
||||
#printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution)
|
||||
#printer.setOutputFileName(fileName)
|
||||
#painter = QtGui.QPainter(printer)
|
||||
#self.render(painter)
|
||||
#painter.end()
|
||||
|
||||
def dragEnterEvent(self, ev):
|
||||
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events
|
||||
|
||||
|
||||
|
||||
#def getFreehandLine(self):
|
||||
|
||||
## Wait for click
|
||||
#self.clearMouse()
|
||||
#while self.lastButtonReleased != QtCore.Qt.LeftButton:
|
||||
#QtGui.qApp.sendPostedEvents()
|
||||
#QtGui.qApp.processEvents()
|
||||
#time.sleep(0.01)
|
||||
#fl = vstack(self.mouseTrail)
|
||||
#return fl
|
||||
|
||||
#def getClick(self):
|
||||
#fl = self.getFreehandLine()
|
||||
#return fl[-1]
|
||||
|
||||
|
||||
|
@ -18,7 +18,7 @@ class HistogramLUTWidget(GraphicsView):
|
||||
self.item = HistogramLUTItem(*args, **kargs)
|
||||
self.setCentralItem(self.item)
|
||||
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
|
||||
self.setMinimumWidth(92)
|
||||
self.setMinimumWidth(95)
|
||||
|
||||
|
||||
def sizeHint(self):
|
||||
|
96
widgets/LayoutWidget.py
Normal file
96
widgets/LayoutWidget.py
Normal 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)
|
||||
|
||||
|
@ -41,6 +41,8 @@ class PlotWidget(GraphicsView):
|
||||
other methods, use :func:`getPlotItem <pyqtgraph.PlotWidget.getPlotItem>`.
|
||||
"""
|
||||
def __init__(self, parent=None, **kargs):
|
||||
"""When initializing PlotWidget, all keyword arguments except *parent* are passed
|
||||
to :func:`PlotItem.__init__() <pyqtgraph.PlotItem.__init__>`."""
|
||||
GraphicsView.__init__(self, parent)
|
||||
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||
self.enableMouse(False)
|
||||
|
@ -14,7 +14,7 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||
if dlg.wasCanceled():
|
||||
raise Exception("Processing canceled by user")
|
||||
"""
|
||||
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False):
|
||||
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False):
|
||||
"""
|
||||
============== ================================================================
|
||||
**Arguments:**
|
||||
@ -25,15 +25,16 @@ class ProgressDialog(QtGui.QProgressDialog):
|
||||
parent
|
||||
wait Length of time (im ms) to wait before displaying dialog
|
||||
busyCursor If True, show busy cursor until dialog finishes
|
||||
disable If True, the progress dialog will not be displayed
|
||||
and calls to wasCanceled() will always return False.
|
||||
If ProgressDialog is entered from a non-gui thread, it will
|
||||
always be disabled.
|
||||
============== ================================================================
|
||||
"""
|
||||
|
||||
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
|
||||
if not isGuiThread:
|
||||
self.disabled = True
|
||||
self.disabled = disable or (not isGuiThread)
|
||||
if self.disabled:
|
||||
return
|
||||
|
||||
self.disabled = False
|
||||
|
||||
noCancel = False
|
||||
if cancelText is None:
|
||||
|
172
widgets/RemoteGraphicsView.py
Normal file
172
widgets/RemoteGraphicsView.py
Normal 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()
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph.python2_3 import asUnicode
|
||||
from pyqtgraph.SignalProxy import SignalProxy
|
||||
|
||||
import pyqtgraph.functions as fn
|
||||
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph.python2_3 import asUnicode
|
||||
|
||||
import numpy as np
|
||||
try:
|
||||
|
Loading…
Reference in New Issue
Block a user