problems getting a ScrolledPanel's scrollbar(s) to appear when custom child panel is re-sized

Hello guys,

I'm attempting to build my first "proper" custom control and I would
appreciate a few pointers along the way.

I have created a custom panel which attempts to blend the
functionality of a StaticBox, CheckBox, & FoldPanelBar into one
control.

The original idea was to add extra functionality to a StaticBox to
allow me to blend a StaticBox's background appropriately when using a
different background color then the parent. When setting a default
StaticBox's background color, it does not appear to restrict to the
StaticBox's outline, which hinders the control's ability to naturally
blend in with a different color. Although I am sure there are other
ways to get around this issue by overriding OnPaint, I decided to go
ahead and try creating my own custom control.

To start, I added the ability to set the StaticBox's background color,
as well as the panel's background color, which allows for a more
natural blending around the box's label and corners when using
multiple colors.

I have since extended it's functionality by adding a CheckBox to the
left of the StaticBox's label which toggles the StaticBox item(s)
visibility on click. When the box is unchecked, the items are hidden
and the control will appear to be more like a standard CheckBox. Once
checked, The box will expand to show the panel's item(s), constrained
within a StaticBox outline. The control is still in the works and
there are many more functions to be built so please bear with the
partial code, it's all I got so far. :wink:

Now here comes the current issues.. I have been able to get the
functionality working as needed but I have run into one really
annoying issue that I can't seem to figure out. When I add this custom
panel to a ScrolledPanel's sizer, the scroll bars do not appear as
needed when one of my box's are checked, although the bars will appear
if you re-size the frame manually. I have played around with
Refresh(), Layout(), SetAutoLayout(), etc. but I guess I haven't been
able to figure out the correct use for them in this case. Any
suggestions would be appreciated, the demo code below should run as-
is, just click a CheckBox and you will see no scroll bars, then re-
size the frame manually and you will see them appear.

Another question I have is whether it is possible to restrict the
position of the client area of a panel so that I may force any child
controls/sizers to start at an offset position? Currently the root
panel contains a sizer which has a spacer as it's first item, and a
child panel for the controls. The spacer compensates for the space at
the top which contains the checkbox/label of the control. Borders have
also been set to constrain the child panel within the StaticBox's
outline. Imho, It seems a bit overdone to use two panels within the
one control but I am unable to figure out how to set position
constraints to the client area. If there is a way, I would be able to
remove the child panel, which would help remove the need to create
extra steps.

Being it that this is my first attempt at building a "proper" custom
control, If you see any "rules of thumb" that I may have broken along
the way I would appreciate any suggestions you might have that will
help me structure this to be more like an "official" control.

Thanks :smiley:

[code]

import wx
from wx.lib.scrolledpanel import ScrolledPanel

class StaticBoxPanel(wx.Panel):

    def __init__(self, parent, id=-1, title="", panel=None,
pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.NO_BORDER | wx.CLIP_CHILDREN,
name="StaticBoxPanel"):

        self._title = title
        self._isChecked = False
        self._hasFocus = False
        self._checkPosX = 0
        self._checkPosY = 0
        self._labelFont = wx.Font(8, wx.FONTFAMILY_DEFAULT,
wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, "MS Shell Dlg 2",
wx.FONTENCODING_DEFAULT)
        self._panelColour = parent.GetBackgroundColour()
        self._backColour = parent.GetBackgroundColour()
        self._lineColour = (213, 223, 229, 255)#(255, 255,
255, 255)
        self._checkboxBorder = 5
        self._checkboxFlags = wx.EXPAND | wx.ALL
        self._checkboxOrientation = wx.VERTICAL
        self._checkboxProportion = 0

        wx.Panel.__init__(self, parent, id, pos, size, style, name)
        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_MOTION, self.OnMotion)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnChecked)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
        self.Bind(wx.EVT_SIZE, self.OnSize)

        #self.SetAutoLayout(1)
        self.SetCheckBitmaps()

    def SetPanel(self, panel=None):
        if panel: self._panelCtrl = panel
        else: self._panelCtrl = wx.Panel(self, -1)

        self._panelCtrl.SetAutoLayout(1)
        self._panelCtrl.SetBackgroundColour(self._panelColour)
        self._boxSizer = wx.BoxSizer(self._checkboxOrientation)
        self._boxSizer.Add((-1, self.GetLabelExtent()[1]), 0,
wx.EXPAND)
        self._boxSizer.Add(self._panelCtrl, self._checkboxProportion,
self._checkboxFlags, self._checkboxBorder + 3)
        self._boxSizer.Hide(1)
        self.SetSizer(self._boxSizer)
        self.Layout()
        self.Refresh()
        return panel

    def OnSize(self, event):
        print self.GetParent().GetSize()
        self.GetParent().Layout() # Needed, doesn't refresh properly
unless called?
        self.Refresh()
        event.Skip()

    def OnLeaveWindow(self, event):
        if self._isChecked:
            self._bmpCurrentBitmap = self._bmpCheckedNormal
        else:
            self._bmpCurrentBitmap = self._bmpUncheckedNormal
        self.Refresh()
        event.Skip()

    def SetPanelBackColour(self, colour):
        self._panelCtrl.SetBackgroundColour(colour)
        self._panelColour = colour

    def SetBackColour(self, colour):
        self.SetBackgroundColour(colour)
        self._backColour = colour
        self.SetCheckBitmaps()

    def SetCheckBitmaps(self):
        self._bmpCurrentBitmap = self.CreateBitmap()
        self._bmpUncheckedNormal = self.CreateBitmap(flag=0)
        self._bmpUncheckedSelected = self.CreateBitmap(flag=0 |
wx.CONTROL_CURRENT)
        self._bmpCheckedNormal =
self.CreateBitmap(flag=wx.CONTROL_CHECKED | 0)
        self._bmpCheckedSelected =
self.CreateBitmap(flag=wx.CONTROL_CHECKED | wx.CONTROL_CURRENT)

    def SetLabelFont(self, font):
        self._labelFont = font

    def GetPanel(self):
        return self._panelCtrl

    def GetLabelExtent(self):
        dc = wx.ClientDC(self)
        dc.SetFont(self._labelFont)
        extent = dc.GetTextExtent(self._title)
        del dc
        return extent

    def OnMotion(self, event):
        obj = event.GetEventObject()
        x, y = event.GetPosition()
        if self._isChecked:
            self._bmpCurrentBitmap = self._bmpCheckedNormal
        else:
            self._bmpCurrentBitmap = self._bmpUncheckedNormal

        if obj is self:
            if x in range(self._checkPosX, self._checkPosX + 17):
                if y in range(self._checkPosY, self._checkPosY + 17):
                    if self._isChecked:
                        self._bmpCurrentBitmap =
self._bmpCheckedSelected
                    else:
                        self._bmpCurrentBitmap =
self._bmpUncheckedSelected
                    self._boxSizer.Layout()
                    self.Refresh()
        event.Skip()

    def OnPaint(self, event):
        obj = event.GetEventObject()
        if obj is self:
            dc = wx.AutoBufferedPaintDC(obj)
            self.Draw(dc)
        event.Skip()

    def OnChecked(self, event):
        obj = event.GetEventObject()
        x, y = event.GetPosition()
        if obj is self:
            if x in range(self._checkPosX, self._checkPosX + 17):
                if y in range(self._checkPosY, self._checkPosY + 17):
                    if self._isChecked:
                        self._isChecked = False
                        self._boxSizer.Hide(1)
                    else:
                        self._isChecked = True
                        self._boxSizer.Show(1)
                    self.SetSize(self.GetEffectiveMinSize())
                    self.SetClientSize(self.GetEffectiveMinSize())
                    self.Clicked(obj, x, y, self._isChecked)
                    #self.GetParent().Layout()
                    self.Refresh()
        event.Skip()

    def Clicked(self, dc, posX, posY, selected):
        print "Clicked: %s, %s" % (posX, posY)
        if selected:
            self._bmpCurrentBitmap = self._bmpCheckedSelected
        else:
            self._bmpCurrentBitmap = self._bmpUncheckedSelected

    def CreateBitmap(self, size=(16, 16), flag=0):
        bmp = wx.EmptyBitmap(*size)
        dc = wx.MemoryDC(bmp)
        dc.SetBackground(wx.Brush(self._backColour))
        dc.Clear()
        wx.RendererNative.Get().DrawCheckBox(self, dc, (0, 0, size[0],
size[1]), flag)
        dc.SelectObject(wx.NullBitmap)
        return bmp

    def Draw(self, dc):
        w, h = self.GetClientSize()
        dc.SetBackground(wx.Brush(self._backColour))
        dc.SetFont(self._labelFont)
        self._labelExtent = dc.GetTextExtent(self._title)
        dc.SetBrush(wx.Brush(self._panelColour))
        dc.SetPen(wx.Pen(self._lineColour))
        dc.Clear()

        self._checkPosX = 0
        y = self._labelExtent[1] / 2

        if self._isChecked:
            self._checkPosX = 13
            dc.DrawRoundedRectangle(0, y, w, h-y, 5)
            dc.DrawRoundedRectangle(1, y+1, w-2, h-y-2, 4)
            for i in range (y, 17):
                dc.SetPen(wx.Pen(self._backColour))
                dc.DrawLinePoint((10, i), (10 + 16 +
self._labelExtent[0] + 6, i))
                if self._backColour != self._panelColour:
                    dc.SetPen(wx.Pen(self._lineColour))
                    dc.DrawPointPoint((9, i))
                    dc.DrawPointPoint((10, i))
                    dc.DrawPointPoint((10 + 16 + self._labelExtent[0]
+ 6, i)) # Needs cleanup: 10=offset, 16=icon width, 6=border
                    dc.DrawPointPoint((11 + 16 + self._labelExtent[0]
+ 6, i))
                    dc.SetPen(wx.Pen(self._backColour))
                    dc.DrawLinePoint((11, i), (10 + 16 +
self._labelExtent[0] + 6 , i))
            if self._backColour != self._panelColour:
                dc.SetPen(wx.Pen(self._lineColour))
                dc.DrawLinePoint((9, i), (10 + 16 +
self._labelExtent[0] + 8, i))
                dc.DrawLinePoint((9, i+1), (10 + 16 +
self._labelExtent[0] + 8, i+1))
            if self._checkPosY > y:
                self._checkPosY = 0
            else:
                self._checkPosY = y - 16 / 2
        dc.DrawBitmap(self._bmpCurrentBitmap, self._checkPosX,
self._checkPosY)
        dc.DrawText(self._title, self._checkPosX + 16, 0)

···

####################################################################################################

class Frame(wx.Frame):

    def __init__(self, parent, id=-1, title="Default Frame",
pos=wx.DefaultPosition, size=wx.DefaultSize,
style=wx.DEFAULT_FRAME_STYLE):
        wx.Frame.__init__(self, parent, id, title, pos, size, style)
        self.CenterOnScreen()

        labelData = {0: ("StaticBoxPanel - Same Colours 1", (121,
141, 166, 255), (121, 141, 166, 255)),
                     1: ("StaticBoxPanel - Same Colours 2", (154,
180, 211, 255), (154, 180, 211, 255)),
                     2: ("StaticBoxPanel - Mixed Colours 1", (121,
141, 166, 255), (154, 180, 211, 255)),
                     3: ("StaticBoxPanel - Mixed Colours 2", (154,
180, 211, 255), (121, 141, 166, 255))}

        self.Panel = ScrolledPanel(self, -1)
        self.Panel.SetBackgroundColour((121, 141, 166, 255))#((154,
180, 211, 255))

        fszr = wx.BoxSizer(wx.VERTICAL)
        pszr = wx.BoxSizer(wx.VERTICAL)

        fszr.Add(self.Panel, 1, wx.EXPAND)

        self.StaticBox, self.BoxPanel, bszr = {}, {}, {}
        for index, data in enumerate(labelData.values()):
            self.StaticBox[index] = StaticBoxPanel(self.Panel, -1,
data[0])

self.StaticBox[index].SetPanel(wx.Panel(self.StaticBox[index], -1))
            self.StaticBox[index].SetBackColour(data[1])
            self.StaticBox[index].SetPanelBackColour(data[2])
            self.BoxPanel[index] = self.StaticBox[index].GetPanel()

            bszr[index] = wx.BoxSizer(wx.HORIZONTAL)
            bszr[index].Add(wx.StaticText(self.BoxPanel[index], -1,
"Item #1:", size=(-1, 14)), 0, flag=wx.ALIGN_CENTER_VERTICAL |
wx.RIGHT, border=3)
            bszr[index].Add(wx.TextCtrl(self.BoxPanel[index], -1, "",
size=(-1, 20)), 1, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=3)
            bszr[index].Add(wx.StaticText(self.BoxPanel[index], -1,
"Item #2", size=(-1, 14)), 0, flag=wx.ALIGN_CENTER_VERTICAL |
wx.RIGHT, border=3)
            bszr[index].Add(wx.TextCtrl(self.BoxPanel[index], -1, "",
size=(-1, 20)), 0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=3)

            self.BoxPanel[index].SetSizer(bszr[index])
            pszr.Add(self.StaticBox[index], 0, flag=wx.EXPAND | wx.ALL

wx.ALIGN_CENTER_VERTICAL, border=5)

        self.Panel.SetSizer(pszr)
        self.SetSizer(fszr)

        fszr.Layout()
        self.Refresh()
        self.Panel.SetupScrolling()

if __name__ == '__main__':
    app = wx.PySimpleApp()
    frame = Frame(None, -1, "DEMO - StaticBoxPanel", size=(800, 130),
style=wx.DEFAULT_FRAME_STYLE)
    frame.Show()
    #import wx.lib.inspection
    #wx.lib.inspection.InspectionTool().Show()
    app.MainLoop()

[/code]

<snip>

Being it that this is my first attempt at building a "proper" custom
control, If you see any "rules of thumb" that I may have broken along
the way I would appreciate any suggestions you might have that will
help me structure this to be more like an "official" control.

Thanks :smiley:

<SNIP a lot of code>

The most obvious rule of thumb you have broken is the news group rule that code should always be posted as attachments to the email not posted within the message. There are a couple of very good reasons for this:
1/ File->SaveAttachements is a lot easier for people to do than (select the code and only the code)(copy the code)(open an editor)(new file)(paste code)File->SaveAs
2/ A lot of e-mail clients word wrap, (usually at inconsistent lengths), which often makes the code impossible to run without further editing.
3/ People are a lot more likely to try your code if they don't have to work too hard to do so.

Gadget/Steve

Hey,

I will keep that in mind for the next time.... I do not usually post to
groups, I'm one of those stubborn guys that normally has to figure
everything out. As well, I did not see any information regarding posting
code when I was submitting it off the google group site, might I suggest
some information get added to the page so that this mistake can be avoided
in the future??

Regards,

[mailto:wxpython-users@googlegroups.com] On Behalf Of GadgetSteve@live.co.uk

···

-----Original Message-----
From: wxpython-users@googlegroups.com
Sent: September-16-10 3:00 AM
To: wxpython-users@googlegroups.com
Subject: Re: [wxPython-users] problems getting a ScrolledPanel's
scrollbar(s) to appear when custom child panel is re-sized

<snip>

Being it that this is my first attempt at building a "proper" custom
control, If you see any "rules of thumb" that I may have broken along
the way I would appreciate any suggestions you might have that will
help me structure this to be more like an "official" control.

Thanks :smiley:

<SNIP a lot of code>

The most obvious rule of thumb you have broken is the news group rule that
code should always be posted as attachments to the email not posted within
the message. There are a couple of very good reasons for this:
1/ File->SaveAttachements is a lot easier for people to do than (select the
code and only the code)(copy the code)(open an editor)(new file)(paste
code)File->SaveAs
2/ A lot of e-mail clients word wrap, (usually at inconsistent lengths),
which often makes the code impossible to run without further editing.
3/ People are a lot more likely to try your code if they don't have to work
too hard to do so.

Gadget/Steve

--
To unsubscribe, send email to wxPython-users+unsubscribe@googlegroups.com
or visit http://groups.google.com/group/wxPython-users?hl=en

See the first paragraph at http://groups.google.com/group/wxpython-users and also MakingSampleApps - wxPyWiki

···

On 9/16/10 7:31 AM, AWainb wrote:

Hey,

I will keep that in mind for the next time.... I do not usually post to
groups, I'm one of those stubborn guys that normally has to figure
everything out. As well, I did not see any information regarding posting
code when I was submitting it off the google group site, might I suggest
some information get added to the page so that this mistake can be avoided
in the future??

--
Robin Dunn
Software Craftsman

Now here comes the current issues.. I have been able to get the
functionality working as needed but I have run into one really
annoying issue that I can't seem to figure out. When I add this custom
panel to a ScrolledPanel's sizer, the scroll bars do not appear as
needed when one of my box's are checked, although the bars will appear
if you re-size the frame manually.

When the size needed to show the full contents of the scrolled panel changes then you need to let the scrolled panel reset its virtual size to accommodate the new size. So calling SetupScrolling again should take care of it, and then calling Layout again may be needed. This assumes that your panel is set up to play nice with sizers by either having a sizer of its own to manage its children, or sets its minsize, or overrides DoGetBestSize.

Another question I have is whether it is possible to restrict the
position of the client area of a panel so that I may force any child
controls/sizers to start at an offset position? Currently the root
panel contains a sizer which has a spacer as it's first item, and a
child panel for the controls. The spacer compensates for the space at
the top which contains the checkbox/label of the control.

Using a spacer for that is probably the correct way to handle it.

Borders have
also been set to constrain the child panel within the StaticBox's
outline.

Using a wx.StaticBoxSizer for the contents of the box will do that for you automatically.

Imho, It seems a bit overdone to use two panels within the
one control but I am unable to figure out how to set position
constraints to the client area.

You probably don't need the inner panel. Just make all the content widgets be on the same panel as the static box, and use a wx.StaticBoxSizer to position them inside the box.

···

On 9/15/10 9:28 PM, PyNo1 wrote:

--
Robin Dunn
Software Craftsman

Thanks for your reply Robin,

And my apologies for overlooking the obvious :stuck_out_tongue:

Thank you for your suggestions, I will update the code and put the
DoGetBestSize() as soon as I get home.

Just a couple questions regarding your response.

Using a wx.StaticBoxSizer for the contents of the box will do that for
you automatically.

Can I use a wx.StaticBoxSizer when I am not actually using a StaticBox? I am
overriding OnPaint and manually drawing the StaticBox & CheckBox effects to
the root panel, then I assign it a normal BoxSizer and stick another panel
inside it and place my controls on top of the second panel to restrict the
control's dimensions. What I would like to do is remove the second panel
and restrict the client area (or is it virtual?) of the root panel so that
when I add a child control it will not overlap the painted effects. For
instance, if the root panel is 800x600, I would like to restrict the
controls to fit into the dimension (7, 12, 786, 576). This way when a child
control is added with the position of (0, 0), or (-1, -1) for that matter,
the custom panel will know to offset to the defined dimensions.

The reason I am starting this from scratch is because I would like to add
extra functionality to a standard StaticBox. The first issue I discovered is
the fact that the background colour of a StaticBox surpasses the rounded
rectangle border. When using a dark background for the parent panel, and a
light background for the StaticBox, the corners, and label area do not blend
in well with the parent's background, leaving a bad visual experience. I
require two different background colours for the project I am currently
working on and I was unable to find my way around this issue. I also
required a way to show/hide the StaticBox to save space but I wanted to
leave the CheckBox and label in view. From there it has become more of a pet
project more than anything.

The code should run as-is (and I have attached it as requested :wink: so you
should be able to see the new bordering I have added for now, I also have a
few more border styles in mind to allow a few extra choices for the user,
but that will be for later.

Thanks again,

[mailto:wxpython-users@googlegroups.com] On Behalf Of Robin Dunn

StaticBoxPanel.py (10.4 KB)

···

-----Original Message-----
From: wxpython-users@googlegroups.com
Sent: September-17-10 3:11 PM
To: wxpython-users@googlegroups.com
Subject: Re: [wxPython-users] problems getting a ScrolledPanel's
scrollbar(s) to appear when custom child panel is re-sized

On 9/15/10 9:28 PM, PyNo1 wrote:

Now here comes the current issues.. I have been able to get the
functionality working as needed but I have run into one really
annoying issue that I can't seem to figure out. When I add this custom
panel to a ScrolledPanel's sizer, the scroll bars do not appear as
needed when one of my box's are checked, although the bars will appear
if you re-size the frame manually.

When the size needed to show the full contents of the scrolled panel
changes then you need to let the scrolled panel reset its virtual size
to accommodate the new size. So calling SetupScrolling again should
take care of it, and then calling Layout again may be needed. This
assumes that your panel is set up to play nice with sizers by either
having a sizer of its own to manage its children, or sets its minsize,
or overrides DoGetBestSize.

Another question I have is whether it is possible to restrict the
position of the client area of a panel so that I may force any child
controls/sizers to start at an offset position? Currently the root
panel contains a sizer which has a spacer as it's first item, and a
child panel for the controls. The spacer compensates for the space at
the top which contains the checkbox/label of the control.

Using a spacer for that is probably the correct way to handle it.

Borders have
also been set to constrain the child panel within the StaticBox's
outline.

Using a wx.StaticBoxSizer for the contents of the box will do that for
you automatically.

Imho, It seems a bit overdone to use two panels within the
one control but I am unable to figure out how to set position
constraints to the client area.

You probably don't need the inner panel. Just make all the content
widgets be on the same panel as the static box, and use a
wx.StaticBoxSizer to position them inside the box.

--
Robin Dunn
Software Craftsman

--
To unsubscribe, send email to wxPython-users+unsubscribe@googlegroups.com
or visit http://groups.google.com/group/wxPython-users?hl=en

Thanks for your reply Robin,

And my apologies for overlooking the obvious :stuck_out_tongue:

Thank you for your suggestions, I will update the code and put the
DoGetBestSize() as soon as I get home.

Just a couple questions regarding your response.

Using a wx.StaticBoxSizer for the contents of the box will do that for
you automatically.

Can I use a wx.StaticBoxSizer when I am not actually using a StaticBox?

No. Sorry, I didn't catch that fact before.

I am
overriding OnPaint and manually drawing the StaticBox& CheckBox effects to
the root panel, then I assign it a normal BoxSizer and stick another panel
inside it and place my controls on top of the second panel to restrict the
control's dimensions. What I would like to do is remove the second panel
and restrict the client area (or is it virtual?) of the root panel so that
when I add a child control it will not overlap the painted effects. For
instance, if the root panel is 800x600, I would like to restrict the
controls to fit into the dimension (7, 12, 786, 576). This way when a child
control is added with the position of (0, 0), or (-1, -1) for that matter,
the custom panel will know to offset to the defined dimensions.

You can still do it with the single panel without too much hassle. Just add spacers to the panel's sizer where you your effects to be located, and save the wx.SizerItems returned from the Add method. Then later on you can use sizerItem.GetRect() to know where to draw.

···

On 9/17/10 1:04 PM, AWainb wrote:

--
Robin Dunn
Software Craftsman