Tuesday, July 14, 2009

wx.MiniPanel

While I was browsing wxPython demo, I found a really nifty window called wx.MiniFrame.


















I thought it was nice for my app but soon realized it's wx.Frame. I wanted it inside my application to draw a graph. I didn't want one of my nodes to travel outside my application window and join Firefox, so I decided to make a similar one as a panel.


















Can you see which one is MiniFrame and which is MiniPanel?











One that cannot' be outside the application window is MiniPanel!
(well, anybody talked about close but something? I don't know... (works on Windows))















I haven't finished minor tunings, e.g. the title is embedded in the code, but it's already resizable.

Jul. 15 added:
I tested on (Python2.6, wxPython-2.8.10.1, Fedora11) and (Python 2.6.2, wxPython 2.8.10.1, WindowsXP). Not tested on Mac.


import wx
import wx.lib.resizewidget as resizewidget
import cStringIO as StringIO

batten = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x11\x00\x00\x00\x10\x08\x02\x00\x00\x00\x7fS\x03\x08\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x00`IDAT8OcdX\xf9\x9f\x81d\x00\xd4C*B\xd6\xf0\x1f7@1\x17\xce\x01\xaa\xc7c!\x8a,\xa6\x1edi8\x1b\x9f\x1e\xb8\xeb\x80f\xa1\xb1\x11\xae\xc0j\x0f\xb2\xbf \n\x08\xb8\r\xcd\x06\xa2\xf4\xa0\x05\x1ea=\xe4\xfb\x07\x7f`2P\x14?\xc8^\xc7L\x0f\xd8\xd3\x01\t\xa9\x8e\x04\xa5\xf0\xa4L\x86\x1e\x00\xbc\xf4\xfe\xa6\xcaO\x9a\x16\x00\x00\x00\x00IEND\xaeB`\x82'

bmpstream = StringIO.StringIO(batten)
try:
image = wx.ImageFromStream(bmpstream)
finally:
bmpstream.close()

class _TitleBarPanel(wx.Panel):
def __init__(self, parent, titleFGColour, titleBGColour, barWidth, fontSize, title):
wx.Panel.__init__(self, parent, pos = (0, 0), size = (0, barWidth))
self.SetBackgroundColour(titleBGColour)
self.SetMaxSize((100000, barWidth))
self.parent = parent
self.gparent = parent.GetParent()
self.restwin = None

#Title static text.
self.titlest = titlest = wx.StaticText(self, -1, title, size = (100000, barWidth))
titlest.SetForegroundColour(titleFGColour)
font = titlest.GetFont()
font.SetPointSize(fontSize)
titlest.SetFont(font)

#Close bitmap button.
bfi = wx.BitmapFromImage(image)
self.closeb = b = wx.BitmapButton(self, -1, bfi, (0, 0), (barWidth, barWidth), style = wx.BU_EXACTFIT)
b.SetToolTipString("Delete this node.")

hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(titlest, 1, wx.EXPAND | wx.CENTER)
hsizer.Add(b, 0, wx.RIGHT)
self.SetSizer(hsizer)

self.titlest.Bind(wx.EVT_BUTTON, self.onClose, b)
self.Bind(wx.EVT_BUTTON, self.onClose, b)
self.titlest.Bind(wx.EVT_LEFT_DOWN, self.onTitleMouseLeftDown)
self.titlest.Bind(wx.EVT_LEFT_UP, self.onTitleMouseLeftUp)
self.Bind(wx.EVT_LEFT_UP, self.onTitleMouseLeftUp)
self.titlest.Bind(wx.EVT_MOTION, self.onTitleMouseMotion)
self.Bind(wx.EVT_MOTION, self.onTitleMouseMotion)
self.titlest.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onTitleMouseCaptureLost)
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onTitleMouseCaptureLost)

def onClose(self, ev):
self.parent.Close()

def onTitleMouseLeftDown(self, ev):
self.titlest.CaptureMouse()
x, y = ev.GetPosition()
sx, sy = self.titlest.GetScreenPosition()
gx, gy = self.gparent.GetPosition()
self.px, self.py = sx + x, sy + y
self.gx, self.gy = gx, gy

def onTitleMouseLeftUp(self, ev):
if self.titlest.HasCapture():
self.titlest.ReleaseMouse()

def onTitleMouseMotion(self, ev):
if ev.Dragging():
x, y = ev.GetPosition()
sx, sy = self.titlest.GetScreenPosition()
px, py = sx + x, sy + y
if self.restwin:
fsx, fsy = self.restwin.GetScreenPosition()
fw, fh = self.restwin.GetSize()
margin = self.margin
if px < fsx + margin or py < fsy + margin or px > fsx + fw - margin or py > fsy + fh - margin:
return
self.gparent.SetPosition((self.gx + px - self.px, self.gy + py - self.py))

def onTitleMouseCaptureLost(self, ev):
self.titlest.ReleaseMouse()

def setDragRestrictWindow(self, window, margin):
self.restwin = window
self.margin = margin

class NodePanel(wx.Panel):
def __init__(self, parent, id = wx.ID_ANY, pos=wx.DefaultPosition, size = wx.DefaultSize,
style = wx.TAB_TRAVERSAL|wx.NO_BORDER, name=wx.PanelNameStr,
borderColour = wx.BLACK, minSize = (0, 0), maxSize = (-1, -1),
bgColour = wx.NullColour, titleFGColour = wx.WHITE, titleBGColour = wx.Colour(80, 167, 255),
barWidth = 13, fontSize = 8, title = 'MiniPanel'):
wx.Panel.__init__(self, parent, id, pos, size, style | wx.FULL_REPAINT_ON_RESIZE, name)
self.borderColour = borderColour

rw = resizewidget.ResizeWidget(parent, pos = pos, style = wx.FULL_REPAINT_ON_RESIZE)
self.SetMinSize(minSize)
self.SetMaxSize(maxSize)
rw.SetManagedChild(self)

self.tb = tb = _TitleBarPanel(self, titleFGColour, titleBGColour, barWidth, fontSize, title)
self.clientPanel = clientPanel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(tb, 0, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border = 3)
sizer.Add(clientPanel, 1, wx.EXPAND | wx.BOTTOM | wx.LEFT | wx.RIGHT, border = 3)

self.SetSizer(sizer)
self.SetBackgroundColour(bgColour)
clientPanel.SetBackgroundColour(bgColour)

rw.SetSize(size)

self.Bind(wx.EVT_PAINT,self.OnPaint)
self.Bind(wx.EVT_IDLE,self.OnPaintOnIdle)

def OnPaintOnIdle(self, ev):
#self._onPaint(wx.ClientDC(self))
pass

def OnPaint(self, ev):
self._onPaint(wx.PaintDC(self))

def _onPaint(self, dc):
#Use double buffering as you like.
#(wxPython-demo dir)/samples/pySketch/pySketch.py has a good example of it.
dc.SetPen(wx.Pen(self.borderColour))
x, y = self.GetSize()
dc.DrawLine(0, 0, x-1, 0)
dc.DrawLine(0, 0, 0, y-1)
dc.DrawLine(x-1, 0, x-1, y-1)
dc.DrawLine(0, y-1, x-1, y-1)

def setDragRestrictWindow(self, window, margin = 0):
#If you don't set this, the Panel will go outside the window. Is there a nice way? :(
self.tb.setDragRestrictWindow(window, margin)

def getClientPanel(self):
return self.clientPanel

def getTitleStaticText(self):
return self.tb.titlest

def Destroy(self):
#The resize handle is the parent of the Panel(reparented). We want to destroy
#the handle too. We'll override Destroy() to make sure it is destroyed.
self.GetParent().Destroy()


if __name__ == '__main__':
myapp=wx.PySimpleApp()
frame = wx.Frame(None, -1, "Title", size = wx.Size(300, 400))
scroll = wx.ScrolledWindow(frame, -1)
scroll.SetScrollbars(50,50,10,10)
np = NodePanel(scroll, pos = (30, 30), size = (200, 200), borderColour = (128, 128, 128))

np.setDragRestrictWindow(scroll, 15)

print np.getTitleStaticText().GetLabel()

def OnClose(ev):
if ev.CanVeto():
dlg = wx.MessageDialog(None, 'Are you sure?', 'msgb', wx.YES_NO)
if dlg.ShowModal() == wx.ID_YES:
np.GetParent().Destroy()
dlg.Destroy()

else:
np.Destroy()


np.Bind(wx.EVT_CLOSE, OnClose, np)


def CloseIt(ev):
np.Close()
clientAreaCloseButton = wx.Button(np.getClientPanel(), -1, "Close Me", pos = (20, 20))
np.Bind(wx.EVT_BUTTON, CloseIt, clientAreaCloseButton)

frame.Show()
myapp.MainLoop()

1 comment:

Kam Yen Cheah said...

Nice sample, am learning wxPython too ... and looking for the wx.lib.resizewidget, your example is working in my environment. Thanks! Good Job!