Re: Re: Looking for an "on/off" toggle control

Rolf <[email protected]> Thu, 21 Jan 2021 17:13:31 +0100
Newsgroups gmane.comp.python.wxpython
Message-ID <[email protected]>
Whilst I agreed with Johnf, I decided that I liked the idea of an on/off
button, so while this is not of industrial strength, it serves the
purpose for my requirements.

You may want to alter the code to fit yours.

I enclose the code, the demo image and an image of the control in one of
my projects.

Use at your own discretion.

Usage:

'python onoffbutton.py'

and

'import onoffbutton as oob' then 'self.fc_logging =
oob.OnOffButton(self.tab7, wx.ID_ANY)' as a drop-in replacement for CheckBox

Regards

Rolf

demo project
On 14/01/2021 07:39, Matthias Brennwald wrote:
> Wouldn't bitmapped buttons ask for trouble with todays variety of
> screen resolutions?
>
> On Do, Jan 14, 2021 at 04:43, Steve Barnes <[email protected]> wrote:
>>
>> Of course the agw SBitmapToggleButton could be used to provide a
>> non-native switch effect. It would still be a button rather than a
>> switch but with the appropriate bitmaps could give the appearance of
>> just about any switch.
>>
>>  
>>
>> *From:*[email protected]
>> <[email protected]> *On Behalf Of *john fabiani
>> *Sent:* 13 January 2021 22:18
>> *To:* [email protected]
>> *Subject:* Re: [wxPython-users] Re: Looking for an "on/off" toggle
>> control
>>
>>  
>>
>> I agree with your links.  That said - all we had was a checkbox or
>> the toggle switch and some how we made due. 
>>
>> Johnf
>>
>> On 1/13/21 12:45 PM, Matt Newville wrote:
>>
>>      
>>
>>      
>>
>>     On Wed, Jan 13, 2021 at 2:01 PM john fabiani
>>     <[email protected] <mailto:[email protected]>> wrote:
>>
>>         I normally don't use anything from windows 10 uwp - but I am
>>         fairly sure
>>         the Toggle switch is available.
>>
>>         Also "re-inventing"  - if you are not willing to write a
>>         custom widget
>>         then you will either have to wait until some future version
>>         that has the
>>         control built-in or make do with the checkbox.  You must be
>>         aware that
>>         for many many years the checkbox was all that was available
>>         on all
>>         platforms.  It seem to work just fine.  The switch is a new UI
>>         development as are many others.  I happen to like it - but
>>         many others I
>>         don't believe add anything other than change for changes
>>         sake.  That
>>         said, I believe the current toggle switch conveys the action
>>         of being
>>         "on or off" very well.
>>
>>      
>>
>>     References: 
>>
>>      
>>      https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/toggles
>>     <https://emea01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fwindows%2Fuwp%2Fdesign%2Fcontrols-and-patterns%2Ftoggles&data=04%7C01%7C%7Ca2a95f59c0cb4a03ee8608d8b81119cb%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637461730867743993%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C1000&sdata=tpJyuSTDHH9cXv61MujvETweguvJba740XGFmw%2BhWpE%3D&reserved=0>
>>
>>      
>>      https://developer.apple.com/design/human-interface-guidelines/macos/buttons/switches/
>>     <https://emea01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fdeveloper.apple.com%2Fdesign%2Fhuman-interface-guidelines%2Fmacos%2Fbuttons%2Fswitches%2F&data=04%7C01%7C%7Ca2a95f59c0cb4a03ee8608d8b81119cb%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637461730867773977%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C1000&sdata=tu8c3inyJ9zsPEfg%2BQdZII0Wjjj7Fp1YYTBNEeAr2f0%3D&reserved=0>
>>
>>      
>>
>>     Both companies suggest that toggle switches have a different
>>     purpose from a checkbox, implying both immediate action (as
>>     opposed to the selection of a preference) and propagation to
>>     other GUI elements, turning on/off a whole category of options
>>     and displays for example.
>>
>>      
>>
>>     --Matt
>>
>>      
>>
>>     -- 
>>     You received this message because you are subscribed to the
>>     Google Groups "wxPython-users" group.
>>     To unsubscribe from this group and stop receiving emails from it,
>>     send an email to [email protected]
>>     <mailto:[email protected]>.
>>     To view this discussion on the web visit
>>     https://groups.google.com/d/msgid/wxpython-users/CA%2B7ESbqwVdq%3DyXbNypgGNHCWnuM4Qe_0T1i7Tuk3yNjnQspzbg%40mail.gmail.com
>>     <https://emea01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgroups.google.com%2Fd%2Fmsgid%2Fwxpython-users%2FCA%252B7ESbqwVdq%253DyXbNypgGNHCWnuM4Qe_0T1i7Tuk3yNjnQspzbg%2540mail.gmail.com%3Futm_medium%3Demail%26utm_source%3Dfooter&data=04%7C01%7C%7Ca2a95f59c0cb4a03ee8608d8b81119cb%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637461730867783972%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C1000&sdata=QkXnY58ZGooOPnVaWxbKZgLbpJLAOsxBVEMxEyaT6J0%3D&reserved=0>.
>>
>> -- 
>> You received this message because you are subscribed to the Google
>> Groups "wxPython-users" group.
>> To unsubscribe from this group and stop receiving emails from it,
>> send an email to [email protected]
>> <mailto:[email protected]>.
>> To view this discussion on the web visit
>> https://groups.google.com/d/msgid/wxpython-users/48ec4c47-d518-9975-3391-5f6e5f2d29cc%40gmail.com
>> <https://emea01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgroups.google.com%2Fd%2Fmsgid%2Fwxpython-users%2F48ec4c47-d518-9975-3391-5f6e5f2d29cc%2540gmail.com%3Futm_medium%3Demail%26utm_source%3Dfooter&data=04%7C01%7C%7Ca2a95f59c0cb4a03ee8608d8b81119cb%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637461730867793966%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C1000&sdata=vCIvhd%2B24K2P4h4R3Dd%2Fp9NhPp7Mf1NXrFkzEZISRoo%3D&reserved=0>.
>>
>> -- 
>> You received this message because you are subscribed to the Google
>> Groups "wxPython-users" group.
>> To unsubscribe from this group and stop receiving emails from it,
>> send an email to [email protected]
>> <mailto:[email protected]>.
>> To view this discussion on the web visit
>> https://groups.google.com/d/msgid/wxpython-users/VI1PR03MB44790BABF461FD2FEF12BE8D9BA80%40VI1PR03MB4479.eurprd03.prod.outlook.com
>> <https://groups.google.com/d/msgid/wxpython-users/VI1PR03MB44790BABF461FD2FEF12BE8D9BA80%40VI1PR03MB4479.eurprd03.prod.outlook.com?utm_medium=email&utm_source=footer>.
> -- 
> You received this message because you are subscribed to the Google
> Groups "wxPython-users" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to [email protected]
> <mailto:[email protected]>.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/wxpython-users/0UVWMQ.7I5FXIYNQBZJ1%40gmail.com
> <https://groups.google.com/d/msgid/wxpython-users/0UVWMQ.7I5FXIYNQBZJ1%40gmail.com?utm_medium=email&utm_source=footer>.

-- 
You received this message because you are subscribed to the Google Groups "wxPython-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To view this discussion on the web visit https://groups.google.com/d/msgid/wxpython-users/b094c960-1dc9-18cd-abeb-367a598a48e8%40gmail.com.
onoffbutton.py (text/x-python, 11.6 KB)
'''
    onoffbutton.py

    A custom class On/Off clickable.

    Events: EVT_ON_OFF          Generic event on binary change

            EVT_ON              The control changed to On

            EVT_OFF             The control changed to Off

    Event Notes:
        All events return GetValue() i.e.

            def OnOff(self,event):
                current_value = event.GetValue()

    Functions:
        GetValue()              Returns numeric value in the control On (True) = 1 Off (False) = 0

        SetValue(int)           Set numeric value in the control
        SetLabel(str)           Set label for the control
        SetToolTip(str)         Set tooltip for the control
        SetForegroundColour     Set text colour
        SetBackgroundColour     Set background colour

        Enable(Bool)            Enable/Disable the control
                                On Disable the control value is frozen

        IsEnabled()             Is the control Enabled - Returns True/False

    Default Values:
        initial -       0 (False) Off
        
    Issues:
        I traded the ability to scale the image for any sensible ability to
        change the label font size, as I rarely use the label of a checkbox,
        instead opting to use a statictext to the left of it.
        
Author:     J Healey
Created:    14/01/2021
Copyright:  J Healey - 2021
License:    GPL 3 or any later version
Email:      [email protected]
'''

import wx

#Events OnOff, On and Off
oobEVT_ON_OFF = wx.NewEventType()
EVT_ON_OFF = wx.PyEventBinder(oobEVT_ON_OFF, 1)
oobEVT_ON = wx.NewEventType()
EVT_ON = wx.PyEventBinder(oobEVT_ON, 1)
oobEVT_OFF = wx.NewEventType()
EVT_OFF = wx.PyEventBinder(oobEVT_OFF, 1)

class OnOffEvent(wx.PyCommandEvent):
    """ Event sent from the :class:`OnOff` when the control changes. """

    def __init__(self, eventType, eventId=1, value=0):
        """
        Default class constructor.

        :param `eventType`: the event type;
        :param `eventId`: the event identifier.
        """

        wx.PyCommandEvent.__init__(self, eventType, eventId)
        self._eventType = eventType
        self.value = value

    def GetValue(self):
        """
        Retrieve the value of the control at the time
        this event was generated."""
        return self.value

class OnOffButton(wx.Control):

    def __init__(self, parent, id=wx.ID_ANY, label="", pos=wx.DefaultPosition, size=wx.DefaultSize, initial=0, style=wx.BORDER_NONE, mono=False, border=True, name="OnOff_Button"):
        """
        Default class constructor.

        @param parent:  Parent window. Must not be None.
        @param id:      identifier. A value of -1 indicates a default value.
        @param label:   control label.
        @param pos:     Position. If the position (-1, -1) is specified
                        then a default position is chosen.
        @param size:    If the default size (-1, -1) is specified then the minimum size is chosen.
        @param initial: Initial value 0 False or 1 True - default False.
        @param style:   control border style.
        @param mono:    True or False makes the image monochrome or colour - default False
        @param border:  True or False adds a border to the image - default True
        @param name:    Widget name.
        """

        wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
        self._initial = initial
        self._frozen_value = initial
        self._label = label
        self._pos = pos
        self._size = size
        self._name = name
        self._id = id
        self._mono = mono
        self._border = border
        self._style = style
        self.OnClr = 'green' 
        self.OffClr = 'red' 
        if self._mono:
            self.OnClr = self.OffClr = 'white' 
        if self._initial > 1:
            self._initial = 1
        if self._initial < 0:
            self._initial = 0

        self._Value = initial

        # Initialize images
        self.InitialiseBitmaps()
        self.onoff = wx.StaticBitmap(self, -1, bitmap=wx.Bitmap(self._img))
        self.label = wx.StaticText(self, -1, self._label)

        # Bind the event
        self.onoff.Bind(wx.EVT_LEFT_DOWN, self.OnOff)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.onoff, 0, 0, 0)
        sizer.Add(self.label, 0, wx.ALIGN_CENTRE, 0)
        self.SetImage(self._initial)
        self.SetSizerAndFit(sizer)
        self.Show()

    def InitialiseBitmaps(self):
        self._bitmaps = {
            "On": self.SetImageSize(self._CreateBitmap("On")),
            "Off": self.SetImageSize(self._CreateBitmap("Off")),
            "Disable": self.SetImageSize(self._CreateBitmap("Disable")),
            }
        if self._initial <= 0:
            self._img = self._bitmaps['Off']
        elif self._initial >= 1:
            self._img = self._bitmaps['On']

    def _CreateBitmap(self, type):
        bmp = wx.Bitmap(80, 36)
        dc = wx.MemoryDC(bmp)
        dc.SetBackgroundMode(wx.TRANSPARENT)
        bg = self.GetBackgroundColour()
        fg = self.GetForegroundColour()
        brush = wx.Brush(bg)  
        dc.SetBackground(brush)
        dc.Clear() 
        
        dc.SetBrush(brush) 
        if self._border:
            dc.DrawRoundedRectangle(2,3,70,33,16.0)
        if type == "On":
            dc.SetBrush(wx.Brush(self.OnClr)) 
        elif type == "Off":
            dc.SetBrush(wx.Brush(self.OffClr)) 
        else:
            dc.SetBrush(wx.Brush('grey')) 
        dc.DrawRoundedRectangle(12,9,48,21,10.0)
        dc.SetBrush(wx.Brush(fg))
        if type == "On":
            dc.DrawCircle(52,19,12)
        else:
            dc.DrawCircle(22,19,12)
        bmp = dc.GetAsBitmap((0, 0, 80, 36))
        return bmp

    def SetImageSize(self,img):
        imgsize = img.GetSize()
        imgh = imgsize[1]
        imgw = imgsize[0]
        w = self._size[0]
        h = self._size[1]
        if w <= 0:
            w = 20
        if h <= 0:
            h = 12
        img = img.ConvertToImage()
        img = img.Scale(w,h,quality=wx.IMAGE_QUALITY_HIGH)
        return img

    def SetValue(self,value):
        if value > 1:
            value = 1
        if value < 0:
            value = 0
        self._Value = value
        self.SetImage(value)
        self.Update()

    def GetValue(self):
        return self._Value

    def SetLabel(self,label):
        self.label.SetLabel(label)
        self.Update()

    def GetLabel(self,label):
        return self.label.GetLabel()

    def IsEnabled(self):
        return wx.Control.IsEnabled(self)

    def Enable(self, value):
        if value and self.IsEnabled(): # If value = current state do nothing
            return
        if not value and not self.IsEnabled():
            return
        wx.Control.Enable(self, value)
        self.Update()
        if value:
            #Enable via callafter in case someone has been stabbing away on the disabled control
            wx.CallAfter(self.OnReset)
        else:
            #Disable - Freeze the controls Value and change bitmap
            self._frozen_value = self.GetValue()
            self._img = self._bitmaps['Disable']
            self.onoff.SetBitmap(wx.Bitmap(self._img))
            self.onoff.SetToolTip(self._name+" - [Disabled]")

    def SetToolTip(self, tip):
        wx.Control.SetToolTip(self,tip)
        self.Refresh()

    def SetForegroundColour(self, colour):
        wx.Control.SetForegroundColour(self, colour)
        self.InitialiseBitmaps()
        self.SetImage(self._Value)        
        self.Refresh()

    def SetBackgroundColour(self, colour):
        wx.Control.SetBackgroundColour(self, colour)
        self.InitialiseBitmaps()
        self.SetImage(self._Value)        
        self.Refresh()

    def OnReset(self):
        #Reset the control to the state it was in when it was Disabled
        self.SetValue(self._frozen_value)
        self.SetImage(self._frozen_value)

    def OnOff(self, event):
        state = self._Value
        if state == 0:
            state = 1
        else:
            state = 0

        if state:
            self.SetValue(1)
            value = 1
            #event change
            event = OnOffEvent(oobEVT_ON_OFF, self.GetId(), value)
            event.SetEventObject(self)
            self.GetEventHandler().ProcessEvent(event)
            #event On
            event = OnOffEvent(oobEVT_ON, self.GetId(), value)
            event.SetEventObject(self)
            self.GetEventHandler().ProcessEvent(event)
        else:
            self.SetValue(0)
            value = 0
            #event change
            event = OnOffEvent(oobEVT_ON_OFF, self.GetId(), value)
            event.SetEventObject(self)
            self.GetEventHandler().ProcessEvent(event)
            #event Off
            event = OnOffEvent(oobEVT_OFF, self.GetId(), value)
            event.SetEventObject(self)
            self.GetEventHandler().ProcessEvent(event)
        self.SetImage(value)

    def SetImage(self, value):
        #Set appropriate image and tooltip
        tp = self.GetToolTip()
        if not self.IsEnabled():
            self._img = self._bitmaps['Disable']
            if tp != None:
                tt = tp.GetTip()+" - [Currently Disabled]"
            else:
                tt = "[Disabled]"
        else:
            if value <= 0:
                self._img = self._bitmaps['Off']
                if tp != None:
                    tt = tp.GetTip()+" - [Currently Off]"
                else:
                    tt = "[ Off ]"
            else:
                self._img = self._bitmaps['On']
                if tp != None:
                    tt = tp.GetTip()+" - [Currently On]"
                else:
                    tt = "[ On ]"

        self.onoff.SetBitmap(wx.Bitmap(self._img))
        self.onoff.SetToolTip(tt)
        self.Layout()

if __name__ == '__main__':

    class MyFrame(wx.Frame):

        def __init__(self, parent):

            wx.Frame.__init__(self, parent, -1, "On/Off Demo")

            panel = wx.Panel(self)
            panel.SetBackgroundColour("lightgreen")
            
            self.onoff = OnOffButton(panel, -1, label="80 X 48", pos=(50, 50), size=(80,48), initial=1)
            self.onoff1 = OnOffButton(panel, -1, label="60 X 36 No border", pos=(50, 100), size=(60,36), initial=1, border=False)
            self.onoff2 = OnOffButton(panel, -1, "40 x 24", pos=(50, 150), size =(40,24), initial=0)
            self.onoff3 = OnOffButton(panel, -1, "30 x 18 Mono", pos=(50, 200), size =(30,18), initial=0, mono=True)
            self.onoff4 = OnOffButton(panel, -1, " Minimum Mono No border", pos=(50, 250), initial=0, mono=True, border=False)
            self.onoff5 = OnOffButton(panel, -1, " Minimum 20 X 16", pos=(50, 300), initial=0)
            self.txt = wx.TextCtrl(panel, -1, "On", size=(50,30), pos=(220,50))
            self.txt1 = wx.TextCtrl(panel, -1, "Off", size=(50,30), pos=(220,100))
            self.onoff.Bind(EVT_ON_OFF, self.OnOff)
            self.Bind(EVT_ON, self.SwOn)
            self.Bind(EVT_OFF, self.SwOff)
            self.onoff1.Bind(EVT_ON_OFF, self.OnOff1)

        def OnOff(self, event):
            if event.GetValue():
                self.txt.SetValue("On")
            else:
                self.txt.SetValue("Off")
            event.Skip()

        def OnOff1(self, event):
            if event.GetValue():
                self.txt1.SetValue("On")
            else:
                self.txt1.SetValue("Off")
            event.Skip()

        def SwOn(self, event):
            obj = event.GetEventObject()
            print(obj.Name + " On")

        def SwOff(self, event):
            obj = event.GetEventObject()
            print(obj.Name + " Off")

    app = wx.App()
    frame = MyFrame(None)
    app.SetTopWindow(frame)
    frame.Show()
    app.MainLoop()