Several minor bugfixes and features

- Added rate-limited mode to SignalProxy
  - Added basic text justification to LabelItem
  - ViewBox.addItem now has ignoreBounds option, which causes the item to be ignored when autoscaling
  - Added ValueLabel widget
  - Fixed some autoscaling bugs
  - InfiniteLine fix - no hilight if movable=False
This commit is contained in:
Luke Campagnola 2012-04-03 01:01:33 -04:00
parent bdef8dc4c7
commit 5a357ddb2a
9 changed files with 155 additions and 36 deletions

View File

@ -7,31 +7,35 @@ __all__ = ['SignalProxy']
class SignalProxy(QtCore.QObject):
"""Object which collects rapid-fire signals and condenses them
into a single signal. Used, for example, to prevent a SpinBox
from generating multiple signals when the mouse wheel is rolled
over it.
into a single signal or a rate-limited stream of signals.
Used, for example, to prevent a SpinBox from generating multiple
signals when the mouse wheel is rolled over it.
Emits sigDelayed after input signals have stopped for a certain period of time.
"""
sigDelayed = QtCore.Signal(object)
def __init__(self, signal, delay=0.3, slot=None):
def __init__(self, signal, delay=0.3, rateLimit=0, slot=None):
"""Initialization arguments:
signal - a bound Signal or pyqtSignal instance
delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s)
slot - Optional function to connect sigDelayed to.
rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a
steady rate while they are being received.
"""
QtCore.QObject.__init__(self)
signal.connect(self.signalReceived)
self.signal = signal
self.delay = delay
self.rateLimit = rateLimit
self.args = None
self.timer = ThreadsafeTimer.ThreadsafeTimer()
self.timer.timeout.connect(self.flush)
self.block = False
self.slot = slot
self.lastFlushTime = None
if slot is not None:
self.sigDelayed.connect(slot)
@ -43,8 +47,20 @@ class SignalProxy(QtCore.QObject):
if self.block:
return
self.args = args
if self.rateLimit == 0:
self.timer.stop()
self.timer.start((self.delay*1000)+1)
else:
now = time()
if self.lastFlushTime is None:
leakTime = 0
else:
lastFlush = self.lastFlushTime
leakTime = max(0, (lastFlush + (1.0 / self.rateLimit)) - now)
self.timer.stop()
self.timer.start((min(leakTime, self.delay)*1000)+1)
def flush(self):
"""If there is a signal queued up, send it now."""
@ -54,6 +70,7 @@ class SignalProxy(QtCore.QObject):
self.sigDelayed.emit(self.args)
self.args = None
self.timer.stop()
self.lastFlushTime = time()
return True
def disconnect(self):

View File

@ -5,8 +5,7 @@ import numpy as np
class Transform(QtGui.QTransform):
"""Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate
This transform always has 0 shear.
This transform has no shear; angles are always preserved.
"""
def __init__(self, init=None):
QtGui.QTransform.__init__(self)
@ -24,6 +23,16 @@ class Transform(QtGui.QTransform):
elif isinstance(init, QtGui.QTransform):
self.setFromQTransform(init)
def getScale(self):
return self._state['scale']
def getAngle(self):
return self._state['angle']
def getTranslation(self):
return self._state['pos']
def reset(self):
self._state = {
'pos': Point(0,0),

View File

@ -72,7 +72,6 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al
Return the number x formatted in engineering notation with SI prefix.
Example::
siFormat(0.0001, suffix='V') # returns "100 μV"
"""
@ -90,8 +89,11 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al
fmt = "%." + str(precision) + "g%s%s"
return fmt % (x*p, pref, suffix)
else:
if allowUnicode:
plusminus = space + u"±" + space
fmt = "%." + str(precision) + u"g%s%s%s%s"
else:
plusminus = " +/- "
fmt = "%." + str(precision) + "g%s%s%s%s"
return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal))
def siEval(s):

View File

@ -45,7 +45,7 @@ class GraphicsLayout(GraphicsWidget):
self.addItem(vb, row, col, rowspan, colspan)
return vb
def addLabel(self, text, row=None, col=None, rowspan=1, colspan=1, **kargs):
def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs):
text = LabelItem(text, **kargs)
self.addItem(text, row, col, rowspan, colspan)
return text

View File

@ -221,7 +221,7 @@ class InfiniteLine(UIGraphicsItem):
self.sigPositionChangeFinished.emit(self)
def hoverEvent(self, ev):
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
if (not ev.isExit()) and self.movable and ev.acceptDrags(QtCore.Qt.LeftButton):
self.currentPen = fn.mkPen(255, 0,0)
else:
self.currentPen = self.pen

View File

@ -14,15 +14,14 @@ class LabelItem(GraphicsWidget):
"""
def __init__(self, text, parent=None, angle=0, **args):
def __init__(self, text=' ', parent=None, angle=0, **args):
GraphicsWidget.__init__(self, parent)
self.item = QtGui.QGraphicsTextItem(self)
self.opts = args
if 'color' not in args:
self.opts['color'] = 'CCC'
else:
if isinstance(args['color'], QtGui.QColor):
self.opts['color'] = fn.colorStr(args['color'])[:6]
self.opts = {
'color': 'CCC',
'justify': 'center'
}
self.opts.update(args)
self.sizeHint = {}
self.setText(text)
self.setAngle(angle)
@ -47,6 +46,8 @@ class LabelItem(GraphicsWidget):
optlist = []
if 'color' in opts:
if isinstance(opts['color'], QtGui.QColor):
opts['color'] = fn.colorStr(opts['color'])[:6]
optlist.append('color: #' + opts['color'])
if 'size' in opts:
optlist.append('font-size: ' + opts['size'])
@ -58,13 +59,25 @@ class LabelItem(GraphicsWidget):
#print full
self.item.setHtml(full)
self.updateMin()
self.resizeEvent(None)
self.update()
def resizeEvent(self, ev):
c1 = self.boundingRect().center()
c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos()
dif = c1 - c2
self.item.moveBy(dif.x(), dif.y())
#c1 = self.boundingRect().center()
#c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos()
#dif = c1 - c2
#self.item.moveBy(dif.x(), dif.y())
#print c1, c2, dif, self.item.pos()
if self.opts['justify'] == 'left':
self.item.setPos(0,0)
elif self.opts['justify'] == 'center':
bounds = self.item.mapRectToParent(self.item.boundingRect())
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())
def setAngle(self, angle):
self.angle = angle
@ -76,16 +89,23 @@ class LabelItem(GraphicsWidget):
bounds = self.item.mapRectToParent(self.item.boundingRect())
self.setMinimumWidth(bounds.width())
self.setMinimumHeight(bounds.height())
#print self.text, bounds.width(), bounds.height()
#self.sizeHint = {
#QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()),
#QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()),
#QtCore.Qt.MaximumSize: (bounds.width()*2, bounds.height()*2),
#QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this?
#}
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()
#def sizeHint(self, hint, constraint):
#return self.sizeHint[hint]
def sizeHint(self, hint, constraint):
if hint not in self.sizeHint:
return QtCore.QSizeF(0, 0)
return QtCore.QSizeF(*self.sizeHint[hint])
#def paint(self, p, *args):
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.rect())
#p.drawRect(self.item.boundingRect())

View File

@ -690,7 +690,10 @@ class PlotItem(GraphicsWidget):
def addItem(self, item, *args, **kargs):
self.items.append(item)
self.vb.addItem(item, *args)
vbargs = {}
if 'ignoreBounds' in kargs:
vbargs['ignoreBounds'] = kargs['ignoreBounds']
self.vb.addItem(item, *args, **vbargs)
if hasattr(item, 'implements') and item.implements('plotData'):
self.dataItems.append(item)
#self.plotChanged()

View File

@ -197,10 +197,11 @@ class ViewBox(GraphicsWidget):
def mouseEnabled(self):
return self.state['mouseEnabled'][:]
def addItem(self, item):
def addItem(self, item, ignoreBounds=False):
if item.zValue() < self.zValue():
item.setZValue(self.zValue()+1)
item.setParentItem(self.childGroup)
if not ignoreBounds:
self.addedItems.append(item)
self.updateAutoRange()
#print "addItem:", item, item.boundingRect()

67
widgets/ValueLabel.py Normal file
View File

@ -0,0 +1,67 @@
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.ptime import time
import pyqtgraph as pg
__all__ = ['ValueLabel']
class ValueLabel(QtGui.QLabel):
"""
QLabel specifically for displaying numerical values.
Extends QLabel adding some extra functionality:
- displaying units with si prefix
- built-in exponential averaging
"""
def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None):
"""
Arguments:
*suffix* (str or None) The suffix to place after the value
*siPrefix* (bool) Whether to add an SI prefix to the units and display a scaled value
*averageTime* (float) The length of time in seconds to average values. If this value
is 0, then no averaging is performed. As this value increases
the display value will appear to change more slowly and smoothly.
*formatStr* (str) Optionally, provide a format string to use when displaying text. The text will
be generated by calling formatStr.format(value=, avgValue=, suffix=)
(see Python documentation on str.format)
This option is not compatible with siPrefix
"""
QtGui.QLabel.__init__(self, parent)
self.values = []
self.averageTime = averageTime ## no averaging by default
self.suffix = suffix
self.siPrefix = siPrefix
if formatStr is None:
formatStr = '{avgValue:0.2g} {suffix}'
self.formatStr = formatStr
def setValue(self, value):
now = time()
self.values.append((now, value))
cutoff = now - self.averageTime
while len(self.values) > 0 and self.values[0][0] < cutoff:
self.values.pop(0)
self.update()
def setFormatStr(self, text):
self.formatStr = text
self.update()
def averageValue(self):
return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values))
def paintEvent(self, ev):
self.setText(self.generateText())
return QtGui.QLabel.paintEvent(self, ev)
def generateText(self):
if len(self.values) == 0:
return ''
avg = self.averageValue()
val = self.values[-1][1]
if self.siPrefix:
return pg.siFormat(avg, suffix=self.suffix)
else:
return self.formatStr.format(value=val, avgValue=avg, suffix=self.suffix)