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.
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
[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]