Subclassing from wx.Control vs subclassing from wx.Panel

I’m trying to make a custom slider and I’m unsure which class to subclass.
Should I subclass wx.Control, wx.Panel or wx.Slider?

In the wxpython documentation under wx.Control it says:

This is the base class for a control or “widget”.

A control is generally a small window which processes user input and/or displays one or more item of data.

So from this it seems that my custom slider class should subclass from wx.Control. I think that wx.Control is subclassing wx.Window because in the file core.pyi is a line

class Control(Window):

(Is the Control class implementation written in C because I’m not able to find the source code for it? so are all classes listed in .pyi files implemented in C?)

In the wxpython documentation under wx.Panel it says

A panel is a window on which controls are placed.

It is usually placed within a frame. Its main feature over its parent class wx.Window is code for handling child windows and TAB traversal, which is implemented natively if possible (e.g. in wxGTK) or by wxWidgets itself otherwise.

In the code for my slider I subclass wx.Panel. From the documentation mentioned above I get the impression that the intention is that I should subclass wx.Control instead. I don’t understand the implications of this. What behavior or functionality is changed when subclassing wx.Panel instead of wx.Control. Since TAB traversal is explicitly mentioned under wx.Panel I assume that there is going to be some difference there.

There is also the option of subclassing wx.Slider…

So my question is really what is the difference between subclassing wx.Panel, wx.Control or wx.Slider?
In what cases should I subclass from wx.Panel instead of wx.Control and vice verse?

The code for my custom slider is

import wx


RW = 300 # slider width
RM = 20  # slider margin
RH = 25  # slider height
CR = 5   # circle radius
BG_COLOR = '#212121'
FG_COLOR = '#838383'

class TickSlider(wx.Panel):
    def __init__(self, parent, minvalue, maxvalue, minortickstep = 0, majortickstep = 0):
        wx.Panel.__init__(self, parent, size=(RW + 2*RM, RH))

        self.font = wx.Font(7, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, 'Courier 10 Pitch')

        self.minvalue = minvalue
        self.maxvalue = maxvalue
        self.minortickstep = minortickstep
        self.majortickstep = majortickstep

        self.value = self.minvalue

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_MOTION, self.OnMotion)

        # helper
        self.baseheight = RH-CR-2

        self.selected   = False
        self.mouseOver  = False
        self.dragging   = False


    def OnPaint(self, e):
        # double buffering
        buffer = wx.Bitmap(RW + 2*RM, RH)
        dc = wx.BufferedPaintDC(self, buffer)
        dc.Clear()

        brush = wx.Brush(BG_COLOR)
        dc.SetBrush(brush)
        dc.SetPen(wx.Pen(BG_COLOR, 1, style=wx.TRANSPARENT))

        dc.DrawRectangle(0, 0, RW+2*RM , RH)
        dc.SetFont(self.font)

        dc.SetPen(wx.Pen('#535353', 2))
        dc.DrawLine(RM, self.baseheight, RW+RM, self.baseheight)

        dc.SetPen(wx.Pen(FG_COLOR, 1))
        dc.SetTextForeground(FG_COLOR)

        # draw major ticks
        for tick in range(self.minvalue, self.maxvalue+1, self.majortickstep):
            hpos = RW*(tick - self.minvalue)/(self.maxvalue - self.minvalue) + RM
            dc.DrawLine(hpos, self.baseheight, hpos, self.baseheight-6)
            w, h = dc.GetTextExtent(str(tick))
            dc.DrawText(str(tick), hpos-w/2, self.baseheight-17)

        for tick in range(self.minvalue, self.maxvalue+1, self.minortickstep):
            hpos = RW*(tick - self.minvalue)/(self.maxvalue - self.minvalue) + RM
            dc.DrawLine(hpos, self.baseheight, hpos, self.baseheight-4)

        if self.selected:
            brush = wx.Brush('#21c151')
            dc.SetBrush(brush)
            dc.SetPen(wx.Pen('#21c151', 2))
        else:
            if self.mouseOver:
                dc.SetPen(wx.Pen('#B3B3B3', 2))
            else:
                dc.SetPen(wx.Pen('#838383', 2))

        hpos = RW*(self.value - self.minvalue)/(self.maxvalue - self.minvalue) + RM
        dc.DrawCircle(hpos, self.baseheight, CR)


    def Clip(self, val):
        if(val < self.minvalue):
            val = self.minvalue
        if(val > self.maxvalue):
            val = self.maxvalue
        return val

    def PosToValue(self, pos):
        val = ((self.maxvalue - self.minvalue)*(pos - RM)/RW) + self.minvalue
        return val

    def ValueToPos(self, val):
        pos = RW*(val - self.minvalue)/(self.maxvalue - self.minvalue) + RM
        return pos


    def OnLeftDown(self, e):
        xpos, ypos = e.GetPosition()
        xslider = self.ValueToPos(self.value)
        yslider = self.baseheight
        d = ((xpos - xslider)**2 + (ypos - yslider)**2)**0.5
        if d <= CR:
            self.dragging     = True
            self.selected     = True
        else:
            # check click on slider line
            if (ypos <= self.baseheight + CR + 2) and (ypos >= self.baseheight - CR - 2):
                self.value = self.PosToValue(xpos)
                self.value = self.Clip(self.value)
                self.dragging     = True
                self.selected     = True
            else:
                self.selected     = False
                self.dragging     = False

        self.Refresh()


    def OnMotion(self, e):
        if self.dragging and e.LeftIsDown():
            xpos, ypos = e.GetPosition()
            val = ((self.maxvalue - self.minvalue)*(xpos - RM)/RW) + self.minvalue
            self.value = self.Clip(val)
            self.Refresh(eraseBackground=False)
        else:
            xpos, ypos = e.GetPosition()
            xslider = RW*(self.value - self.minvalue)/(self.maxvalue - self.minvalue) + RM
            yslider = self.baseheight
            d = ((xpos - xslider)**2 + (ypos - yslider)**2)**0.5
            if d <= CR:
                mouseState = True
            else:
                mouseState = False

            if self.mouseOver != mouseState:
                self.mouseOver = mouseState
                self.Refresh()


    def OnLeftUp(self, e):
        if self.HasCapture():
            self.ReleaseMouse()

        if self.dragging:
            self.dragging = False
            self.Refresh


class MyFrame(wx.Frame):
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 'MyFrame', size=(1000, 600))

        self.p  = wx.Panel(self, wx.ID_ANY)

        # create a sizer
        boxSizer = wx.BoxSizer(wx.VERTICAL)

        minvalue = 0
        maxvalue = 500
        minortickstep = 20
        majortickstep = 100

        # create custom slider
        self.slider = TickSlider(self.p, minvalue, maxvalue, minortickstep, majortickstep)

        boxSizer.Add(self.slider)
        self.p.SetSizer(boxSizer)
        self.Layout()



def main():

    app = wx.App()
    ex = MyFrame()
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

Either wx.Control or wx.Panel will work, but the general rule-of-thumb that I usually go by is if the new widget is going to be composed of one or more other widgets, then use a wx.Panel as the base. If the widget is going to be totally self-drawn then use a wx.Control as the base. There are examples of both cases in the wx.lib package.

Subclassing native controls like wx.Slider is usually a bad idea, because they often have things that you can not override.

Yes. For the most part, everything that is not located in the wx.lib package or subpackages is implemented in C++ with wrappers in the Python extension modules (*.pyd or *.so) in the root wx package folder.

Thanks Robin, so in the particular custom slider code shown in the first post I should rather subclass from wx.Control instead on wx.Panel, if I understand your guideline, because the PAINT event handler is drawing the slider graphics. Is that correctly understood?

I was a little unsure of what the core.pyi file actually represented because it looks like python syntax and I could not see the actual C code wrapper for control class.

Yes

The .pyi files are used by tools like IDEs to get more information about the classes implemented in the extension modules. Python’s introspection tools can’t provide the same level of information for the extension module code as can be gleaned from actual Python code. So the IDEs will scan the .py files, if they are present, in order to get that info.