Last active
February 13, 2016 04:06
-
-
Save blitzmann/cd698a62b741dd9a163a to your computer and use it in GitHub Desktop.
pygauge.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #=============================================================================== | |
| # PyfaGauge is a generic Gauge implementation tailored for pyfa (the Python | |
| # Fitting Assistant). It uses the easeOutQuad equation from | |
| # caurina.transitions.Tweener to do animations | |
| # | |
| # ToDo: make SetGradient(<value, colour start, colour end) | |
| # ToDo: make a solid gradient (not to->from and not dependant on value) | |
| # ToDo: fix 0 range (currently resets range to 0.01, but this causes problems if | |
| # we really set range at 0.01). Perhaps make it -1 and test percentage as | |
| # a negative or something. | |
| # ToDo: possibly devise a way to determine transition percents on init | |
| # (currently hardcoded) | |
| # ToDo: All value and range setting must be done in floats. Assert this. | |
| #=============================================================================== | |
| import copy | |
| import wx | |
| import random | |
| # Animation effect | |
| def OUT_QUAD (t, b, c, d): | |
| t=float(t) | |
| b=float(b) | |
| c=float(c) | |
| d=float(d) | |
| t/=d | |
| return -c *(t)*(t-2) + b | |
| def DrawGradientBar(width, height, gStart, gEnd, gMid = None, fillRatio = 4): | |
| # we need to have dimensions to draw | |
| #assert width > 0 and height > 0 | |
| canvas = wx.EmptyBitmap(width,height) | |
| mdc = wx.MemoryDC() | |
| mdc.SelectObject(canvas) | |
| r = wx.Rect(0, 0, width, height) | |
| r.height = height / fillRatio | |
| if gMid is None: | |
| gMid = gStart | |
| mdc.GradientFillLinear(r, gStart, gMid, wx.SOUTH) | |
| r.top = r.height | |
| r.height = height * (fillRatio - 1)/fillRatio + (1 if height % fillRatio != 0 else 0) | |
| mdc.GradientFillLinear(r, gMid, gEnd, wx.SOUTH) | |
| mdc.SelectObject(wx.NullBitmap) | |
| return canvas | |
| #Brightens a color (wx.Colour), factor = [0,1] | |
| def BrightenColor(color, factor): | |
| r,g,b = color | |
| a = color.Alpha() | |
| factor = min(max(factor, 0), 1) | |
| r+=(255-r)*factor | |
| b+=(255-b)*factor | |
| g+=(255-g)*factor | |
| return wx.Colour(r,g,b,a) | |
| #Darkens a color (wx.Colour), factor = [0, 1] | |
| def DarkenColor(color, factor): | |
| bkR ,bkG , bkB = color | |
| alpha = color.Alpha() | |
| factor = min(max(factor, 0), 1) | |
| factor = 1 - factor | |
| r = float(bkR * factor) | |
| g = float(bkG * factor) | |
| b = float(bkB * factor) | |
| r = min(max(r,0),255) | |
| b = min(max(b,0),255) | |
| g = min(max(g,0),255) | |
| return wx.Colour(r, g, b, alpha) | |
| def CalculateTransitionColor(startColor, endColor, delta): | |
| sR,sG,sB = startColor | |
| eR,eG,eB = endColor | |
| alphaS = startColor.Alpha() | |
| alphaE = endColor.Alpha() | |
| tR = sR + (eR - sR) * delta | |
| tG = sG + (eG - sG) * delta | |
| tB = sB + (eB - sB) * delta | |
| return wx.Colour(tR, tG, tB, (alphaS + alphaE)/2) | |
| class PyGauge(wx.Window): | |
| def __init__(self, parent, font, max_range=100, size=(-1, 30), *args, | |
| **kargs): | |
| wx.Window.__init__(self, parent=parent, size=size, *args, **kargs) | |
| self._size = size | |
| self._border_colour = wx.BLACK | |
| self._bar_colour = None | |
| self._bar_gradient = None | |
| self._border_padding = 0 | |
| self._max_range = max_range | |
| self._value = 0 | |
| self._fraction_digits = 0 | |
| self._timer_id = wx.NewId() | |
| self._timer = None | |
| self._oldValue = 0 | |
| self._anim_duration = 500 | |
| self._anim_step = 0 | |
| self._period = 20 | |
| self._anim_value = 0 | |
| self._anim_direction = 0 | |
| self.anim_effect = OUT_QUAD | |
| # transition colors used based on how full (or overfilled) the gauge is. | |
| self.transition_colors = [ | |
| (wx.Colour(191, 191, 191), wx.Colour(96, 191, 0)), # < 0-100% | |
| (wx.Colour(191, 167, 96), wx.Colour(255, 191, 0)), # < 100-101% | |
| (wx.Colour(255, 191, 0), wx.Colour(255, 128, 0)), # < 101-103% | |
| (wx.Colour(255, 128, 0), wx.Colour(255, 0, 0)) # < 103-105% | |
| ] | |
| self.gradient_effect = -35 | |
| self._percentage = 0 | |
| self._old_percentage = 0 | |
| self._show_remaining = False | |
| self.font = font | |
| self.SetBackgroundColour(wx.Colour(51, 51, 51)) | |
| self._tooltip = wx.ToolTip("0.00/100.00") | |
| self.SetToolTip(self._tooltip) | |
| self.Bind(wx.EVT_PAINT, self.OnPaint) | |
| self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) | |
| self.Bind(wx.EVT_TIMER, self.OnTimer) | |
| self.Bind(wx.EVT_ENTER_WINDOW, self.OnWindowEnter) | |
| self.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) | |
| def OnWindowEnter(self, event): | |
| self._show_remaining = True | |
| self.Refresh() | |
| def OnWindowLeave(self, event): | |
| self._show_remaining = False | |
| self.Refresh() | |
| def GetBorderColour(self): | |
| return self._border_colour | |
| def SetBorderColour(self, colour): | |
| self._border_colour = colour | |
| def GetBarColour(self): | |
| return self._bar_colour | |
| def SetBarColour(self, colour=None): | |
| self._bar_colour = colour | |
| def SetFractionDigits(self, digits): | |
| self._fraction_digits = digits | |
| def GetBarGradient(self): | |
| raise NotImplemented() | |
| def SetBarGradient(self, gradient=None): | |
| raise NotImplemented() | |
| def GetBorderPadding(self): | |
| return self._border_padding | |
| def SetBorderPadding(self, padding): | |
| self._border_padding = padding | |
| def GetRange(self): | |
| """ Returns the maximum value of the gauge. """ | |
| return self._max_range | |
| def Animate(self, enabled=True): | |
| if enabled: | |
| if not self._timer: | |
| self._timer = wx.Timer(self, self._timer_id) | |
| self._anim_step = 0 | |
| self._timer.Start(self._period) | |
| else: | |
| self._animValue = self._percentage | |
| self.Refresh() | |
| def SetRange(self, range, reinit=False, animate=True): | |
| """ | |
| Sets the range of the gauge. The gauge length is its | |
| value as a proportion of the range. | |
| """ | |
| if self._max_range == range: | |
| return | |
| # we cannot have a range of zero (laws of physics, etc), so we set it | |
| if range <= 0: | |
| self._max_range = 0.01 | |
| else: | |
| self._max_range = range | |
| if reinit is False: | |
| self._old_percentage = self._percentage | |
| self._percentage = (self._value/self._max_range) * 100 | |
| else: | |
| self._old_percentage = self._percentage | |
| self._percentage = 0 | |
| self._value = 0 | |
| self.Animate(animate) | |
| self._tooltip.SetTip("%.2f/%.2f" % (self._value, self._max_range if self._max_range >0.01 else 0)) | |
| def GetValue(self): | |
| return self._value | |
| def SetValue(self, value, animate=True): | |
| """ Sets the current position of the gauge. """ | |
| if self._value == value: | |
| return | |
| self._old_percentage = self._percentage | |
| self._value = value | |
| if value < 0: | |
| self._value = 0 | |
| self._percentage = (self._value/self._max_range) * 100 | |
| self.Animate(animate) | |
| self._tooltip.SetTip("%.2f/%.2f" % (self._value, self._max_range)) | |
| def SetValueRange(self, value, range, reinit=False): | |
| """ Set both value and range of the gauge. """ | |
| # todo: option to turn off animation here too. | |
| self.SetRange(range, reinit, animate=False) | |
| self.SetValue(value, animate=False) | |
| self.Animate() | |
| self._tooltip.SetTip("%.2f/%.2f" % | |
| (self._value, | |
| self._max_range if self._max_range > 0.01 else 0)) | |
| def OnEraseBackground(self, event): | |
| pass | |
| def OnPaint(self, event): | |
| dc = wx.BufferedPaintDC(self) | |
| rect = self.GetClientRect() | |
| dc.SetBackground(wx.Brush(self.GetBackgroundColour())) | |
| dc.Clear() | |
| colour = self.GetBackgroundColour() | |
| dc.SetBrush(wx.Brush(colour)) | |
| dc.SetPen(wx.Pen(colour)) | |
| dc.DrawRectangleRect(rect) | |
| value = self._percentage | |
| if self._timer: | |
| if self._timer.IsRunning(): | |
| value = self._anim_value | |
| if self._border_colour: | |
| dc.SetPen(wx.Pen(self.GetBorderColour())) | |
| dc.DrawRectangleRect(rect) | |
| pad = 1 + self.GetBorderPadding() | |
| rect.Deflate(pad, pad) | |
| if self.GetBarColour(): | |
| # if we have a bar color set, then we will use this | |
| colour = self.GetBarColour() | |
| dc.SetBrush(wx.Brush(colour)) | |
| dc.SetPen(wx.Pen(colour)) | |
| # calculate width of bar and draw it | |
| if value > 100: | |
| w = rect.width | |
| else: | |
| w = rect.width * (float(value) / 100) | |
| r = copy.copy(rect) | |
| r.width = w | |
| dc.tangleRect(r) | |
| else: | |
| # if bar color is not set, then we use pre-defined transitions | |
| # for the colors based on the percentage value | |
| # calculate width of bar | |
| if value > 100: | |
| w = rect.width | |
| else: | |
| w = rect.width * (float(value) / 100) | |
| r = copy.copy(rect) | |
| r.width = w | |
| # determine transition range number and calculate xv (which is the | |
| # progress between the two transition ranges) | |
| pv = value | |
| if pv <= 100: | |
| xv = pv/100 | |
| transition = 0 | |
| elif pv <= 101: | |
| xv = pv - 100 | |
| transition = 1 | |
| elif pv <= 103: | |
| xv = (pv - 101)/2 | |
| transition = 2 | |
| elif pv <= 105: | |
| xv = (pv - 103)/2 | |
| transition = 3 | |
| else: | |
| pv = 106 | |
| xv = pv - 100 | |
| transition = -1 | |
| if transition != -1: | |
| start_color, end_color = self.transition_colors[transition] | |
| color = CalculateTransitionColor(start_color, end_color, | |
| xv) | |
| else: | |
| color = wx.Colour(191, 48, 48) # dark red | |
| color_factor = self.gradient_effect / 100 | |
| mid_factor = (self.gradient_effect / 2) / 100 | |
| if self.gradient_effect > 0: | |
| gradient_color = BrightenColor(color, color_factor) | |
| gradient_mid = BrightenColor(color, mid_factor) | |
| else: | |
| gradient_color = DarkenColor(color, color_factor * -1) | |
| gradient_mid = DarkenColor(color, mid_factor * -1) | |
| # draw bar | |
| gradient_bitmap = DrawGradientBar( | |
| r.width, | |
| r.height, | |
| gradient_mid, | |
| color, | |
| gradient_color | |
| ) | |
| dc.DrawBitmap(gradient_bitmap, r.left, r.top) | |
| # font stuff begins here | |
| dc.SetFont(self.font) | |
| # determine shadow position | |
| r = copy.copy(rect) | |
| r.left += 1 | |
| r.top += 1 | |
| if self._max_range == 0.01 and self._value > 0: | |
| format = u'\u221e' # infinity symbol | |
| # drop shadow | |
| dc.SetTextForeground(wx.Colour(80, 80, 80)) # dark grey | |
| dc.DrawLabel(format, r, wx.ALIGN_CENTER) | |
| # text | |
| dc.SetTextForeground(wx.WHITE) | |
| dc.DrawLabel(format, rect, wx.ALIGN_CENTER) | |
| else: | |
| if not self.GetBarColour() and self._show_remaining: | |
| # we only do these for gradients with mouse over | |
| range = self._max_range if self._max_range > 0.01 else 0 | |
| value = range - self._value | |
| if value < 0: | |
| label = "over" | |
| value = -value | |
| else: | |
| label = "left" | |
| format = "{0:." + str(self._fraction_digits) + "f} " + label | |
| else: | |
| format = "{0:." + str(self._fraction_digits) + "f}%" | |
| # drop shadow | |
| dc.SetTextForeground(wx.Colour(80, 80, 80)) | |
| dc.DrawLabel(format.format(value), r, wx.ALIGN_CENTER) | |
| # text | |
| dc.SetTextForeground(wx.WHITE) | |
| dc.DrawLabel(format.format(value), rect, wx.ALIGN_CENTER) | |
| def OnTimer(self, event): | |
| old_value = self._old_percentage | |
| value = self._percentage | |
| start = 0 | |
| # -1 = left direction, 1 = right direction | |
| direction = 1 if old_value < value else -1 | |
| end = direction * (value - old_value) | |
| self._anim_direction = direction | |
| step = self.anim_effect(self._anim_step, start, end, self._anim_duration) | |
| self._anim_step += self._period | |
| if self._timer_id == event.GetId(): | |
| stop_timer = False | |
| if self._anim_step > self._anim_duration: | |
| stop_timer = True | |
| # add new value to the animation if we haven't reached our goal | |
| # otherwise, stop animation | |
| if direction == 1: | |
| if old_value + step < value: | |
| self._anim_value = old_value + step | |
| else: | |
| stop_timer = True | |
| else: | |
| if old_value - step > value: | |
| self._anim_value = old_value - step | |
| else: | |
| stop_timer = True | |
| if stop_timer: | |
| self._timer.Stop() | |
| self.Refresh() | |
| if __name__ == "__main__": | |
| class TestPanel(wx.Panel): | |
| def __init__(self, parent, size=(500, 500)): | |
| wx.Panel.__init__(self, parent, size=size) | |
| self.number_of_tests = 16 | |
| self.box = wx.BoxSizer(wx.VERTICAL) | |
| self.testAllBtn = wx.Button(self, wx.ID_ANY, "Run all") | |
| self.box.Add(self.testAllBtn, 0, wx.ALL, 2) | |
| self.spinCtrl = wx.SpinCtrl(self, min=1, max=self.number_of_tests, value="1", initial=1) | |
| self.box.Add(self.spinCtrl, 0, wx.ALL, 2) | |
| self.testRandomBtn = wx.Button(self, wx.ID_ANY, "Test x gauges") | |
| self.box.Add(self.testRandomBtn, 0, wx.ALL, 2) | |
| self.resetBtn = wx.Button(self, wx.ID_ANY, "Reset") | |
| self.box.Add(self.resetBtn, 0, wx.ALL, 2) | |
| self.gauges = [] | |
| font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False) | |
| for t in xrange(self.number_of_tests): | |
| g = PyGauge(self, font, size=(100, 25)) | |
| g.SetValueRange(0.0, 100.0) | |
| g.SetFractionDigits(2) | |
| self.gauges.append(g) | |
| self.box.Add(g, 0, wx.ALL, 2) | |
| self.testAllBtn.Bind(wx.EVT_BUTTON, self.doAllTest) | |
| self.testRandomBtn.Bind(wx.EVT_BUTTON, self.doXTests) | |
| self.resetBtn.Bind(wx.EVT_BUTTON, self.doReset) | |
| self.SetSizer(self.box) | |
| self.Layout() | |
| def doAllTest(self, event): | |
| self.Parent.statusbar.SetStatusText("Running all tests in 0.5 increments starting with 99") | |
| i = 99.0 | |
| for g in self.gauges: | |
| g.SetValue(i) | |
| i += 0.5 | |
| def doXTests(self, event): | |
| list = random.sample(range(self.number_of_tests), self.spinCtrl.GetValue()) | |
| self.Parent.statusbar.SetStatusText("Setting %d random gauge(s) to between 0 and 106"%len(list)) | |
| for x in list: | |
| i = random.randint(0, 106) | |
| g = self.gauges[x] | |
| g.SetValue(float(i)) | |
| def doReset(self, event): | |
| self.Parent.statusbar.SetStatusText("Resetting gauges, animation disabled") | |
| for g in self.gauges: | |
| g.SetValue(0.0, animate=False) | |
| class Frame(wx.Frame): | |
| def __init__(self, title, size=(250, 650)): | |
| wx.Frame.__init__(self, None, title=title, size=size) | |
| self.statusbar = self.CreateStatusBar() | |
| main_sizer = wx.BoxSizer(wx.VERTICAL) | |
| panel = TestPanel(self, size=size) | |
| main_sizer.Add(panel) | |
| self.SetSizer(main_sizer) | |
| app = wx.App(redirect=False) # Error messages go to popup window | |
| top = Frame("Test Gauges") | |
| top.Show() | |
| app.MainLoop() |
Author
Author
https://gist.github.com/blitzmann/0089327bf9c3a83b743f this one has 64 gauge bars for more visual aid
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Windows: https://gyazo.com/5dd3982ee2ec9c4f92f27a196fab5b41
OS X 10.10: http://www.gfycat.com/GreedyHelplessAltiplanochinchillamouse
OS X 10.11: http://www.gfycat.com/PassionateDefiniteGoat