From c97c5f51e244ad83691ebf84daf23b842b337462 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 30 Nov 2016 09:16:56 -0800 Subject: [PATCH] Add spinbox option for custom formatting --- pyqtgraph/functions.py | 3 +- pyqtgraph/widgets/SpinBox.py | 138 +++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 64 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d79c350f..6ec3932f 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -36,8 +36,6 @@ SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' - - def siScale(x, minVal=1e-25, allowUnicode=True): """ Return the recommended scale factor and SI prefix string for x. @@ -76,6 +74,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True): return (p, pref) + def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): """ Return the number x formatted in engineering notation with SI prefix. diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index a863cd60..50429dee 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -49,28 +49,9 @@ class SpinBox(QtGui.QAbstractSpinBox): **Arguments:** parent Sets the parent widget for this SpinBox (optional). Default is None. value (float/int) initial value. Default is 0.0. - bounds (min,max) Minimum and maximum values allowed in the SpinBox. - Either may be None to leave the value unbounded. By default, values are unbounded. - suffix (str) suffix (units) to display after the numerical value. By default, suffix is an empty str. - siPrefix (bool) If True, then an SI prefix is automatically prepended - to the units and the value is scaled accordingly. For example, - if value=0.003 and suffix='V', then the SpinBox will display - "300 mV" (but a call to SpinBox.value will still return 0.003). Default is False. - step (float) The size of a single step. This is used when clicking the up/ - down arrows, when rolling the mouse wheel, or when pressing - keyboard arrows while the widget has keyboard focus. Note that - the interpretation of this value is different when specifying - the 'dec' argument. Default is 0.01. - dec (bool) If True, then the step value will be adjusted to match - the current size of the variable (for example, a value of 15 - might step in increments of 1 whereas a value of 1500 would - step in increments of 100). In this case, the 'step' argument - is interpreted *relative* to the current value. The most common - 'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is False. - minStep (float) When dec=True, this specifies the minimum allowable step size. - int (bool) if True, the value is forced to integer type. Default is False - decimals (int) Number of decimal values to display. Default is 2. ============== ======================================================================== + + All keyword arguments are passed to :func:`setOpts`. """ QtGui.QAbstractSpinBox.__init__(self, parent) self.lastValEmitted = None @@ -81,28 +62,15 @@ class SpinBox(QtGui.QAbstractSpinBox): self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.opts = { 'bounds': [None, None], - - ## Log scaling options #### Log mode is no longer supported. - #'step': 0.1, - #'minStep': 0.001, - #'log': True, - #'dec': False, - - ## decimal scaling option - example - #'step': 0.1, - #'minStep': .001, - #'log': False, - #'dec': True, ## normal arithmetic step 'step': D('0.01'), ## if 'dec' is false, the spinBox steps by 'step' every time ## if 'dec' is True, the step size is relative to the value ## 'step' needs to be an integral divisor of ten, ie 'step'*n=10 for some integer value of n (but only if dec is True) - 'log': False, + 'log': False, # deprecated 'dec': False, ## if true, does decimal stepping. ie from 1-10 it steps by 'step', from 10 to 100 it steps by 10*'step', etc. ## if true, minStep must be set in order to cross zero. - 'int': False, ## Set True to force value to be integer 'suffix': '', @@ -114,6 +82,8 @@ class SpinBox(QtGui.QAbstractSpinBox): 'decimals': 3, + 'format': asUnicode("{scaledValue:.{decimals}g}{suffixGap}{siPrefix}{suffix}"), + } self.decOpts = ['step', 'minStep'] @@ -134,12 +104,47 @@ class SpinBox(QtGui.QAbstractSpinBox): ret = True ## For some reason, spinbox pretends to ignore return key press return ret - ##lots of config options, just gonna stuff 'em all in here rather than do the get/set crap. def setOpts(self, **opts): - """ - Changes the behavior of the SpinBox. Accepts most of the arguments - allowed in :func:`__init__ `. + """Set options affecting the behavior of the SpinBox. + ============== ======================================================================== + **Arguments:** + bounds (min,max) Minimum and maximum values allowed in the SpinBox. + Either may be None to leave the value unbounded. By default, values are + unbounded. + suffix (str) suffix (units) to display after the numerical value. By default, + suffix is an empty str. + siPrefix (bool) If True, then an SI prefix is automatically prepended + to the units and the value is scaled accordingly. For example, + if value=0.003 and suffix='V', then the SpinBox will display + "300 mV" (but a call to SpinBox.value will still return 0.003). Default + is False. + step (float) The size of a single step. This is used when clicking the up/ + down arrows, when rolling the mouse wheel, or when pressing + keyboard arrows while the widget has keyboard focus. Note that + the interpretation of this value is different when specifying + the 'dec' argument. Default is 0.01. + dec (bool) If True, then the step value will be adjusted to match + the current size of the variable (for example, a value of 15 + might step in increments of 1 whereas a value of 1500 would + step in increments of 100). In this case, the 'step' argument + is interpreted *relative* to the current value. The most common + 'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is + False. + minStep (float) When dec=True, this specifies the minimum allowable step size. + int (bool) if True, the value is forced to integer type. Default is False + decimals (int) Number of decimal values to display. Default is 3. + format (str) Formatting string used to generate the text shown. Formatting is + done with ``str.format()`` and makes use of several arguments: + + * *value* - the unscaled value of the spin box + * *suffix* - the suffix string + * *scaledValue* - the scaled value to use when an SI prefix is present + * *siPrefix* - the SI prefix string (if any), or an empty string if + this feature has been disabled + * *suffixGap* - a single space if a suffix is present, or an empty + string otherwise. + ============== ======================================================================== """ #print opts for k in opts: @@ -154,6 +159,8 @@ class SpinBox(QtGui.QAbstractSpinBox): self.opts[k] = D(asUnicode(opts[k])) elif k == 'value': pass ## don't set value until bounds have been set + elif k == 'format': + self.opts[k] = asUnicode(opts[k]) elif k in self.opts: self.opts[k] = opts[k] else: @@ -378,37 +385,44 @@ class SpinBox(QtGui.QAbstractSpinBox): return True def updateText(self, prev=None): - # get the number of decimal places to print - decimals = self.opts.get('decimals') - # temporarily disable validation self.skipValidate = True - - # add a prefix to the units if requested - if self.opts['siPrefix']: - - # special case: if it's zero use the previous prefix - if self.val == 0 and prev is not None: - (s, p) = fn.siScale(prev) - - # NOTE: insert optional format string here? - txt = ("%."+str(decimals)+"g %s%s") % (0, p, self.opts['suffix']) - else: - # NOTE: insert optional format string here as an argument? - txt = fn.siFormat(float(self.val), precision=decimals, suffix=self.opts['suffix']) - - # otherwise, format the string manually - else: - # NOTE: insert optional format string here? - txt = ('%.'+str(decimals)+'g%s') % (self.val , self.opts['suffix']) - + + txt = self.formatText(prev=prev) + # actually set the text self.lineEdit().setText(txt) self.lastText = txt # re-enable the validation self.skipValidate = False - + + def formatText(self, prev=None): + # get the number of decimal places to print + decimals = self.opts['decimals'] if self.opts['int'] is False else 9 + suffix = self.opts['suffix'] + + # format the string + val = float(self.val) + if self.opts['siPrefix']: + # SI prefix was requested, so scale the value accordingly + + if self.val == 0 and prev is not None: + # special case: if it's zero use the previous prefix + (s, p) = fn.siScale(prev) + else: + (s, p) = fn.siScale(val) + parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val} + + else: + # no SI prefix requested; scale is 1 + parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': '', 'scaledValue': val} + + parts['suffixGap'] = '' if (parts['suffix'] == '' and parts['siPrefix'] == '') else ' ' + + format = self.opts['format'] + return format.format(**parts) + def validate(self, strn, pos): if self.skipValidate: ret = QtGui.QValidator.Acceptable