-
-
Save gabrieldp/e19611abead7f6617872d33866c568a3 to your computer and use it in GitHub Desktop.
| import wx | |
| def fraction_to_value(fraction, min_value, max_value): | |
| return (max_value - min_value) * fraction + min_value | |
| def value_to_fraction(value, min_value, max_value): | |
| return float(value - min_value) / (max_value - min_value) | |
| class SliderThumb: | |
| def __init__(self, parent, value): | |
| self.parent = parent | |
| self.dragged = False | |
| self.mouse_over = False | |
| self.thumb_poly = ((0, 0), (0, 13), (5, 18), (10, 13), (10, 0)) | |
| self.thumb_shadow_poly = ((0, 14), (4, 18), (6, 18), (10, 14)) | |
| min_coords = [float('Inf'), float('Inf')] | |
| max_coords = [-float('Inf'), -float('Inf')] | |
| for pt in list(self.thumb_poly) + list(self.thumb_shadow_poly): | |
| for i_coord, coord in enumerate(pt): | |
| if coord > max_coords[i_coord]: | |
| max_coords[i_coord] = coord | |
| if coord < min_coords[i_coord]: | |
| min_coords[i_coord] = coord | |
| self.size = (max_coords[0] - min_coords[0], | |
| max_coords[1] - min_coords[1]) | |
| self.value = value | |
| self.normal_color = wx.Colour((0, 120, 215)) | |
| self.normal_shadow_color = wx.Colour((120, 180, 228)) | |
| self.dragged_color = wx.Colour((204, 204, 204)) | |
| self.dragged_shadow_color = wx.Colour((222, 222, 222)) | |
| self.mouse_over_color = wx.Colour((23, 23, 23)) | |
| self.mouse_over_shadow_color = wx.Colour((132, 132, 132)) | |
| def GetPosition(self): | |
| min_x = self.GetMin() | |
| max_x = self.GetMax() | |
| parent_size = self.parent.GetSize() | |
| min_value = self.parent.GetMin() | |
| max_value = self.parent.GetMax() | |
| fraction = value_to_fraction(self.value, min_value, max_value) | |
| pos = (fraction_to_value(fraction, min_x, max_x), parent_size[1] / 2 + 1) | |
| return pos | |
| def SetPosition(self, pos): | |
| pos_x = pos[0] | |
| # Limit movement by the position of the other thumb | |
| who_other, other_thumb = self.GetOtherThumb() | |
| other_pos = other_thumb.GetPosition() | |
| if who_other == 'low': | |
| pos_x = max(other_pos[0] + other_thumb.size[0]/2 + self.size[0]/2, pos_x) | |
| else: | |
| pos_x = min(other_pos[0] - other_thumb.size[0]/2 - self.size[0]/2, pos_x) | |
| # Limit movement by slider boundaries | |
| min_x = self.GetMin() | |
| max_x = self.GetMax() | |
| pos_x = min(max(pos_x, min_x), max_x) | |
| fraction = value_to_fraction(pos_x, min_x, max_x) | |
| self.value = fraction_to_value(fraction, self.parent.GetMin(), self.parent.GetMax()) | |
| # Post event notifying that position changed | |
| self.PostEvent() | |
| def GetValue(self): | |
| return self.value | |
| def SetValue(self, value): | |
| self.value = value | |
| # Post event notifying that value changed | |
| self.PostEvent() | |
| def PostEvent(self): | |
| event = wx.PyCommandEvent(wx.EVT_SLIDER.typeId, self.parent.GetId()) | |
| event.SetEventObject(self.parent) | |
| wx.PostEvent(self.parent.GetEventHandler(), event) | |
| def GetMin(self): | |
| min_x = self.parent.border_width + self.size[0] / 2 | |
| return min_x | |
| def GetMax(self): | |
| parent_size = self.parent.GetSize() | |
| max_x = parent_size[0] - self.parent.border_width - self.size[0] / 2 | |
| return max_x | |
| def IsMouseOver(self, mouse_pos): | |
| in_hitbox = True | |
| my_pos = self.GetPosition() | |
| for i_coord, mouse_coord in enumerate(mouse_pos): | |
| boundary_low = my_pos[i_coord] - self.size[i_coord] / 2 | |
| boundary_high = my_pos[i_coord] + self.size[i_coord] / 2 | |
| in_hitbox = in_hitbox and (boundary_low <= mouse_coord <= boundary_high) | |
| return in_hitbox | |
| def GetOtherThumb(self): | |
| if self.parent.thumbs['low'] != self: | |
| return 'low', self.parent.thumbs['low'] | |
| else: | |
| return 'high', self.parent.thumbs['high'] | |
| def OnPaint(self, dc): | |
| if self.dragged or not self.parent.IsEnabled(): | |
| thumb_color = self.dragged_color | |
| thumb_shadow_color = self.dragged_shadow_color | |
| elif self.mouse_over: | |
| thumb_color = self.mouse_over_color | |
| thumb_shadow_color = self.mouse_over_shadow_color | |
| else: | |
| thumb_color = self.normal_color | |
| thumb_shadow_color = self.normal_shadow_color | |
| my_pos = self.GetPosition() | |
| # Draw thumb shadow (or anti-aliasing effect) | |
| dc.SetBrush(wx.Brush(thumb_shadow_color, style=wx.BRUSHSTYLE_SOLID)) | |
| dc.SetPen(wx.Pen(thumb_shadow_color, width=1, style=wx.PENSTYLE_SOLID)) | |
| dc.DrawPolygon(points=self.thumb_shadow_poly, | |
| xoffset=my_pos[0] - self.size[0]/2, | |
| yoffset=my_pos[1] - self.size[1]/2) | |
| # Draw thumb itself | |
| dc.SetBrush(wx.Brush(thumb_color, style=wx.BRUSHSTYLE_SOLID)) | |
| dc.SetPen(wx.Pen(thumb_color, width=1, style=wx.PENSTYLE_SOLID)) | |
| dc.DrawPolygon(points=self.thumb_poly, | |
| xoffset=my_pos[0] - self.size[0] / 2, | |
| yoffset=my_pos[1] - self.size[1] / 2) | |
| class RangeSlider(wx.Panel): | |
| def __init__(self, parent, id=wx.ID_ANY, lowValue=None, highValue=None, minValue=0, maxValue=100, | |
| pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.SL_HORIZONTAL, validator=wx.DefaultValidator, | |
| name='rangeSlider'): | |
| if style != wx.SL_HORIZONTAL: | |
| raise NotImplementedError('Styles not implemented') | |
| if validator != wx.DefaultValidator: | |
| raise NotImplementedError('Validator not implemented') | |
| super().__init__(parent=parent, id=id, pos=pos, size=size, name=name) | |
| self.SetMinSize(size=(max(50, size[0]), max(26, size[1]))) | |
| if minValue > maxValue: | |
| minValue, maxValue = maxValue, minValue | |
| self.min_value = minValue | |
| self.max_value = maxValue | |
| if lowValue is None: | |
| lowValue = self.min_value | |
| if highValue is None: | |
| highValue = self.max_value | |
| if lowValue > highValue: | |
| lowValue, highValue = highValue, lowValue | |
| lowValue = max(lowValue, self.min_value) | |
| highValue = min(highValue, self.max_value) | |
| self.border_width = 8 | |
| self.thumbs = { | |
| 'low': SliderThumb(parent=self, value=lowValue), | |
| 'high': SliderThumb(parent=self, value=highValue) | |
| } | |
| self.thumb_width = self.thumbs['low'].size[0] | |
| # Aesthetic definitions | |
| self.slider_background_color = wx.Colour((231, 234, 234)) | |
| self.slider_outline_color = wx.Colour((214, 214, 214)) | |
| self.selected_range_color = wx.Colour((0, 120, 215)) | |
| self.selected_range_outline_color = wx.Colour((0, 120, 215)) | |
| # Bind events | |
| self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) | |
| self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp) | |
| self.Bind(wx.EVT_MOTION, self.OnMouseMotion) | |
| self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnMouseLost) | |
| self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter) | |
| self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) | |
| self.Bind(wx.EVT_PAINT, self.OnPaint) | |
| self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) | |
| self.Bind(wx.EVT_SIZE, self.OnResize) | |
| def Enable(self, enable=True): | |
| super().Enable(enable) | |
| self.Refresh() | |
| def Disable(self): | |
| super().Disable() | |
| self.Refresh() | |
| def SetValueFromMousePosition(self, click_pos): | |
| for thumb in self.thumbs.values(): | |
| if thumb.dragged: | |
| thumb.SetPosition(click_pos) | |
| def OnMouseDown(self, evt): | |
| if not self.IsEnabled(): | |
| return | |
| click_pos = evt.GetPosition() | |
| for thumb in self.thumbs.values(): | |
| if thumb.IsMouseOver(click_pos): | |
| thumb.dragged = True | |
| thumb.mouse_over = False | |
| break | |
| self.SetValueFromMousePosition(click_pos) | |
| self.CaptureMouse() | |
| self.Refresh() | |
| def OnMouseUp(self, evt): | |
| if not self.IsEnabled(): | |
| return | |
| self.SetValueFromMousePosition(evt.GetPosition()) | |
| for thumb in self.thumbs.values(): | |
| thumb.dragged = False | |
| if self.HasCapture(): | |
| self.ReleaseMouse() | |
| self.Refresh() | |
| def OnMouseLost(self, evt): | |
| for thumb in self.thumbs.values(): | |
| thumb.dragged = False | |
| thumb.mouse_over = False | |
| self.Refresh() | |
| def OnMouseMotion(self, evt): | |
| if not self.IsEnabled(): | |
| return | |
| refresh_needed = False | |
| mouse_pos = evt.GetPosition() | |
| if evt.Dragging() and evt.LeftIsDown(): | |
| self.SetValueFromMousePosition(mouse_pos) | |
| refresh_needed = True | |
| else: | |
| for thumb in self.thumbs.values(): | |
| old_mouse_over = thumb.mouse_over | |
| thumb.mouse_over = thumb.IsMouseOver(mouse_pos) | |
| if old_mouse_over != thumb.mouse_over: | |
| refresh_needed = True | |
| if refresh_needed: | |
| self.Refresh() | |
| def OnMouseEnter(self, evt): | |
| if not self.IsEnabled(): | |
| return | |
| mouse_pos = evt.GetPosition() | |
| for thumb in self.thumbs.values(): | |
| if thumb.IsMouseOver(mouse_pos): | |
| thumb.mouse_over = True | |
| self.Refresh() | |
| break | |
| def OnMouseLeave(self, evt): | |
| if not self.IsEnabled(): | |
| return | |
| for thumb in self.thumbs.values(): | |
| thumb.mouse_over = False | |
| self.Refresh() | |
| def OnResize(self, evt): | |
| self.Refresh() | |
| def OnPaint(self, evt): | |
| w, h = self.GetSize() | |
| # BufferedPaintDC should reduce flickering | |
| dc = wx.BufferedPaintDC(self) | |
| background_brush = wx.Brush(self.GetBackgroundColour(), wx.SOLID) | |
| dc.SetBackground(background_brush) | |
| dc.Clear() | |
| # Draw slider | |
| track_height = 12 | |
| dc.SetPen(wx.Pen(self.slider_outline_color, width=1, style=wx.PENSTYLE_SOLID)) | |
| dc.SetBrush(wx.Brush(self.slider_background_color, style=wx.BRUSHSTYLE_SOLID)) | |
| dc.DrawRectangle(self.border_width, h/2 - track_height/2, w - 2 * self.border_width, track_height) | |
| # Draw selected range | |
| if self.IsEnabled(): | |
| dc.SetPen(wx.Pen(self.selected_range_outline_color, width=1, style=wx.PENSTYLE_SOLID)) | |
| dc.SetBrush(wx.Brush(self.selected_range_color, style=wx.BRUSHSTYLE_SOLID)) | |
| else: | |
| dc.SetPen(wx.Pen(self.slider_outline_color, width=1, style=wx.PENSTYLE_SOLID)) | |
| dc.SetBrush(wx.Brush(self.slider_outline_color, style=wx.BRUSHSTYLE_SOLID)) | |
| low_pos = self.thumbs['low'].GetPosition()[0] | |
| high_pos = self.thumbs['high'].GetPosition()[0] | |
| dc.DrawRectangle(low_pos, h / 2 - track_height / 4, high_pos - low_pos, track_height / 2) | |
| # Draw thumbs | |
| for thumb in self.thumbs.values(): | |
| thumb.OnPaint(dc) | |
| evt.Skip() | |
| def OnEraseBackground(self, evt): | |
| # This should reduce flickering | |
| pass | |
| def GetValues(self): | |
| return self.thumbs['low'].value, self.thumbs['high'].value | |
| def SetValues(self, lowValue, highValue): | |
| if lowValue > highValue: | |
| lowValue, highValue = highValue, lowValue | |
| lowValue = max(lowValue, self.min_value) | |
| highValue = min(highValue, self.max_value) | |
| self.thumbs['low'].SetValue(lowValue) | |
| self.thumbs['high'].SetValue(highValue) | |
| self.Refresh() | |
| def GetMax(self): | |
| return self.max_value | |
| def GetMin(self): | |
| return self.min_value | |
| def SetMax(self, maxValue): | |
| if maxValue < self.min_value: | |
| maxValue = self.min_value | |
| _, old_high = self.GetValues() | |
| if old_high > maxValue: | |
| self.thumbs['high'].SetValue(maxValue) | |
| self.max_value = maxValue | |
| self.Refresh() | |
| def SetMin(self, minValue): | |
| if minValue > self.max_value: | |
| minValue = self.max_value | |
| old_low, _ = self.GetValues() | |
| if old_low < minValue: | |
| self.thumbs['low'].SetValue(minValue) | |
| self.min_value = minValue | |
| self.Refresh() | |
| class TestFrame(wx.Frame): | |
| def __init__(self): | |
| wx.Frame.__init__(self, None, -1, 'Range Slider Demo', size=(300, 100)) | |
| panel = wx.Panel(self) | |
| b = 6 | |
| vbox = wx.BoxSizer(orient=wx.VERTICAL) | |
| vbox.Add(wx.StaticText(parent=panel, label='Custom Range Slider:'), flag=wx.ALIGN_LEFT | wx.ALL, border=b) | |
| self.rangeslider = RangeSlider(parent=panel, lowValue=20, highValue=80, minValue=0, maxValue=100, | |
| size=(300, 26)) | |
| self.rangeslider.Bind(wx.EVT_SLIDER, self.rangeslider_changed) | |
| vbox.Add(self.rangeslider, proportion=1, flag=wx.EXPAND | wx.ALL, border=b) | |
| self.rangeslider_static = wx.StaticText(panel) | |
| vbox.Add(self.rangeslider_static, flag=wx.ALIGN_LEFT | wx.ALL, border=b) | |
| vbox.Add(wx.StaticText(parent=panel, label='Regular Slider with wx.SL_SELRANGE style:'), | |
| flag=wx.ALIGN_LEFT | wx.ALL, border=b) | |
| self.slider = wx.Slider(parent=panel, style=wx.SL_SELRANGE) | |
| self.slider.SetSelection(20, 40) | |
| self.slider.Bind(wx.EVT_SLIDER, self.slider_changed) | |
| vbox.Add(self.slider, proportion=1, flag=wx.EXPAND | wx.ALL, border=b) | |
| self.slider_static = wx.StaticText(panel) | |
| vbox.Add(self.slider_static, flag=wx.ALIGN_LEFT | wx.ALL, border=b) | |
| self.button_toggle = wx.Button(parent=panel, label='Disable') | |
| self.button_toggle.Bind(wx.EVT_BUTTON, self.toggle_slider_enable) | |
| vbox.Add(self.button_toggle, flag=wx.ALIGN_CENTER | wx.ALL, border=b) | |
| panel.SetSizerAndFit(vbox) | |
| box = wx.BoxSizer() | |
| box.Add(panel, proportion=1, flag=wx.EXPAND) | |
| self.SetSizerAndFit(box) | |
| def slider_changed(self, evt): | |
| obj = evt.GetEventObject() | |
| val = obj.GetValue() | |
| self.slider_static.SetLabel('Value: {}'.format(val)) | |
| def rangeslider_changed(self, evt): | |
| obj = evt.GetEventObject() | |
| lv, hv = obj.GetValues() | |
| self.rangeslider_static.SetLabel('Low value: {:.0f}, High value: {:.0f}'.format(lv, hv)) | |
| def toggle_slider_enable(self, evt): | |
| if self.button_toggle.GetLabel() == 'Disable': | |
| self.slider.Enable(False) | |
| self.rangeslider.Enable(False) | |
| self.button_toggle.SetLabel('Enable') | |
| else: | |
| self.slider.Enable(True) | |
| self.rangeslider.Enable(True) | |
| self.button_toggle.SetLabel('Disable') | |
| def main(): | |
| app = wx.App() | |
| TestFrame().Show() | |
| app.MainLoop() | |
| if __name__ == "__main__": | |
| main() |
gabrieldp
commented
Dec 27, 2019

Hello.
This custom-widget enables me flexible value setting and is very beneficial I think.
It would be helpful if I could modify the code, import them into my application and distribute it.
Are there any license terms of this ?
Hello. This custom-widget enables me flexible value setting and is very beneficial I think. It would be helpful if I could modify the code, import them into my application and distribute it. Are there any license terms of this ?
No license, just use/modify as needed. Go nuts.
No license, just use/modify as needed. Go nuts.
Got it. Thanks for reply.
Thank you for sharing your RangeSlider code! It's been really useful for my project.
I built on top of your work to add some extra functionality that others might find helpful:
- Range dragging: You can now click and drag the entire selected range between the thumbs
- Empty area clicks: Clicking on empty parts of the slider moves the nearest thumb to that position
The best part is you can upgrade by just changing RangeSlider to RangeSliderExt - no other changes needed.
Here's the extended code if anyone wants to use these features:
class RangeSliderExt(RangeSlider):
"""
Extended RangeSlider with range dragging capability.
* Click and drag between thumbs to move the entire range.
* When dragged to boundaries, the range maintains its size.
* Click on empty areas to move the nearest thumb.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Range dragging state tracking
self.range_dragged = False
self.range_mouse_over = False
self.drag_start_pos = None
self.drag_start_values = None
def SetValueFromMousePosition(self, click_pos):
if self.range_dragged:
# Calculate movement and convert to value delta
dx = click_pos[0] - self.drag_start_pos[0]
min_x = self.thumbs["low"].GetMin()
max_x = self.thumbs["low"].GetMax()
value_range = self.max_value - self.min_value
pixel_range = max_x - min_x
value_delta = dx * value_range / pixel_range
# Calculate new values
new_low = self.drag_start_values[0] + value_delta
new_high = self.drag_start_values[1] + value_delta
# Maintain range size when hitting boundaries
range_size = self.drag_start_values[1] - self.drag_start_values[0]
if new_low < self.min_value:
new_low = self.min_value
new_high = self.min_value + range_size
elif new_high > self.max_value:
new_high = self.max_value
new_low = self.max_value - range_size
self.thumbs["low"].SetValue(new_low)
self.thumbs["high"].SetValue(new_high)
else:
super().SetValueFromMousePosition(click_pos)
def IsMouseOverRange(self, mouse_pos):
"""Check if mouse is between thumbs (excluding thumb hitboxes)"""
low_pos = self.thumbs["low"].GetPosition()[0]
high_pos = self.thumbs["high"].GetPosition()[0]
thumb_width = self.thumb_width
return low_pos + thumb_width / 2 <= mouse_pos[0] <= high_pos - thumb_width / 2
def IsMouseOverEmptyArea(self, mouse_pos):
"""Check if mouse is on empty slider area (not on range or thumbs)"""
# Check if mouse is over any thumb
for thumb in self.thumbs.values():
if thumb.IsMouseOver(mouse_pos):
return False
# Check if mouse is over range
if self.IsMouseOverRange(mouse_pos):
return False
# Check if mouse is within slider track
track_left = self.border_width
track_right = self.GetSize()[0] - self.border_width
return track_left <= mouse_pos[0] <= track_right
def GetNearestThumb(self, mouse_pos):
"""Get the thumb nearest to the mouse position"""
low_pos = self.thumbs["low"].GetPosition()[0]
high_pos = self.thumbs["high"].GetPosition()[0]
distance_to_low = abs(mouse_pos[0] - low_pos)
distance_to_high = abs(mouse_pos[0] - high_pos)
return (
self.thumbs["low"]
if distance_to_low < distance_to_high
else self.thumbs["high"]
)
def MoveThumbToPosition(self, thumb, mouse_pos):
"""Move thumb to specified mouse position"""
min_x = thumb.GetMin()
max_x = thumb.GetMax()
fraction = value_to_fraction(mouse_pos[0], min_x, max_x)
new_value = fraction_to_value(fraction, self.min_value, self.max_value)
thumb.SetValue(new_value)
def OnMouseDown(self, evt):
if not self.IsEnabled():
return
click_pos = evt.GetPosition()
if self.IsMouseOverRange(click_pos):
# Start range dragging
self.range_dragged = True
self.drag_start_pos = click_pos
self.drag_start_values = self.GetValues()
elif self.IsMouseOverEmptyArea(click_pos):
# Click on empty area - move nearest thumb
nearest_thumb = self.GetNearestThumb(click_pos)
self.MoveThumbToPosition(nearest_thumb, click_pos)
nearest_thumb.dragged = True # Start dragging the thumb
self.CaptureMouse()
self.Refresh()
else:
super().OnMouseDown(evt)
def OnMouseUp(self, evt):
# Reset range dragging state
self.range_dragged = False
self.drag_start_pos = None
self.drag_start_values = None
super().OnMouseUp(evt)
def OnMouseMotion(self, evt):
if not self.IsEnabled():
return
mouse_pos = evt.GetPosition()
# Check range mouse over state
old_range_mouse_over = self.range_mouse_over
self.range_mouse_over = self.IsMouseOverRange(mouse_pos)
if old_range_mouse_over != self.range_mouse_over:
self.Refresh()
else:
super().OnMouseMotion(evt)
def OnMouseLeave(self, evt):
if not self.IsEnabled():
return
self.range_mouse_over = False
super().OnMouseLeave(evt)
def OnMouseEnter(self, evt):
if not self.IsEnabled():
return
mouse_pos = evt.GetPosition()
self.range_mouse_over = self.IsMouseOverRange(mouse_pos)
super().OnMouseEnter(evt)
def OnMouseLost(self, evt):
# Clean up dragging state on capture loss
self.range_dragged = False
self.range_mouse_over = False
self.drag_start_pos = None
self.drag_start_values = None
super().OnMouseLost(evt)Thanks again for the great starting point!
@qaz10102030 Nice, thanks for sharing your contributions!
