#---------------------------------------------------------------------------- # Name: wxroses.py # Purpose: wxPython GUI using clroses.py to display a classic graphics # hack. # # Author: Ric Werme, Robin Dunn. # WWW: http://WermeNH.com/roses # # Created: June 2007 # CVS-ID: $Id$ # Copyright: Public Domain, please give credit where credit is due. # License: Sorry, no EULA. #---------------------------------------------------------------------------- # This module is responsible for everything involving GUI usage # as clroses knows nothing about wxpython, tkintr, etc. import wx import clroses import wx.lib.colourselect as cs # Class SpinPanel creates a control that includes both a StaticText widget # which holds the the name of a parameter and a SpinCtrl widget which # displays the current value. Values are set at initialization and can # change via the SpinCtrl widget or by the program. So that the program # can easily access the SpinCtrl, the SpinPanel handles are saved in the # spin_panels dictionary. class SpinPanel(wx.Panel): def __init__(self, parent, name, min_value, value, max_value, callback): wx.Panel.__init__(self, parent, -1) if "wxMac" in wx.PlatformInfo: self.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) self.st = wx.StaticText(self, -1, name) self.sc = wx.SpinCtrl(self, -1, "", size = (70, -1)) self.sc.SetRange(min_value, max_value) self.sc.SetValue(value) self.sc.Bind(wx.EVT_SPINCTRL, self.OnSpin) self.callback = callback sizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.st, 0, wx.ALIGN_CENTER_VERTICAL) sizer.Add((1,1), 1) sizer.Add(self.sc) self.SetSizer(sizer) global spin_panels spin_panels[name] = self # Called (generally through spin_panels{}) to set the SpinCtrl value. def SetValue(self, value): self.sc.SetValue(value) # Called when user changes the SpinCtrl value. def OnSpin(self, event): name = self.st.GetLabel() value = self.sc.GetValue() if verbose: print 'OnSpin', name, '=', value self.callback(name, value) # Call MyFrame.OnSpinback to call clroses # This class is used to display the current rose diagram. It keeps a # buffer bitmap of the current display, which it uses to refresh the # screen with when needed. When it is told to draw some lines it does # so to the buffer in order for it to always be up to date. class RosePanel(wx.Panel): def __init__(self, *args, **kw): wx.Panel.__init__(self, *args, **kw) self.InitBuffer() self.resizeNeeded = False self.useGCDC = False self.useBuffer = True # set default colors self.SetBackgroundColour((51,51,51)) # gray20 self.SetForegroundColour((164, 211, 238)) # lightskyblue2 # connect the size and paint events to handlers self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_IDLE, self.OnIdle) def InitBuffer(self): size = self.GetClientSize() self.buffer = wx.EmptyBitmap(max(1, size.width), max(1, size.height)) def Clear(self): dc = self.useBuffer and wx.MemoryDC(self.buffer) or wx.ClientDC(self) dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.Clear() if self.useBuffer: self.Refresh(False) def DrawLines(self, lines): if len(lines) <= 1: return dc = self.useBuffer and wx.MemoryDC(self.buffer) or wx.ClientDC(self) if self.useGCDC: dc = wx.GCDC(dc) dc.SetPen(wx.Pen(self.GetForegroundColour(), 1)) dc.DrawLines(lines) if self.useBuffer: self.Refresh(False) def TriggerResize(self): self.GetParent().TriggerResize(self.buffer.GetSize()) def TriggerRedraw(self): self.GetParent().TriggerRedraw() def OnSize(self, evt): self.resizeNeeded = True def OnIdle(self, evt): if self.resizeNeeded: self.InitBuffer() self.TriggerResize() if self.useBuffer: self.Refresh() self.resizeNeeded = False def OnPaint(self, evt): dc = wx.PaintDC(self) if self.useBuffer: dc.DrawBitmap(self.buffer, 0,0) else: self.TriggerRedraw() # A panel used to collect options on how the rose is drawn class OptionsPanel(wx.Panel): def __init__(self, parent, rose): wx.Panel.__init__(self, parent) self.rose = rose sizer = wx.StaticBoxSizer(wx.StaticBox(self, label='Options'), wx.VERTICAL) self.useGCDC = wx.CheckBox(self, label="Use GCDC") sizer.Add(self.useGCDC, 0, wx.BOTTOM|wx.LEFT, 2) self.useBuffer = wx.CheckBox(self, label="Use buffering") sizer.Add(self.useBuffer, 0, wx.BOTTOM|wx.LEFT, 2) def makeCButton(label): btn = cs.ColourSelect(self, size=(20,22)) lbl = wx.StaticText(self, -1, label) sizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(btn) sizer.Add((4,4)) sizer.Add(lbl, 0, wx.ALIGN_CENTER_VERTICAL) return sizer, btn s, self.fg = makeCButton('foreground') sizer.Add(s) s, self.bg = makeCButton('background') sizer.Add(s) self.SetSizer(sizer) self.Bind(wx.EVT_CHECKBOX, self.OnUseGCDC, self.useGCDC) self.Bind(wx.EVT_CHECKBOX, self.OnUseBuffer, self.useBuffer) self.Bind(wx.EVT_IDLE, self.OnIdle) self.Bind(cs.EVT_COLOURSELECT, self.OnSetFG, self.fg) self.Bind(cs.EVT_COLOURSELECT, self.OnSetBG, self.bg) def OnIdle(self, evt): if self.useGCDC.GetValue() != self.rose.useGCDC: self.useGCDC.SetValue(self.rose.useGCDC) if self.useBuffer.GetValue() != self.rose.useBuffer: self.useBuffer.SetValue(self.rose.useBuffer) if self.fg.GetValue() != self.rose.GetForegroundColour(): self.fg.SetValue(self.rose.GetForegroundColour()) if self.bg.GetValue() != self.rose.GetBackgroundColour(): self.bg.SetValue(self.rose.GetBackgroundColour()) def OnUseGCDC(self, evt): self.rose.useGCDC = evt.IsChecked() self.rose.TriggerRedraw() def OnUseBuffer(self, evt): self.rose.useBuffer = evt.IsChecked() self.rose.TriggerRedraw() def OnSetFG(self, evt): self.rose.SetForegroundColour(evt.GetValue()) self.rose.TriggerRedraw() def OnSetBG(self, evt): self.rose.SetBackgroundColour(evt.GetValue()) self.rose.TriggerRedraw() # MyFrame is the traditional class name to create and populate the # application's frame. The general GUI has control/status panels on # the right side and a panel on the left side that draws the rose # # This class also derives from clroses.rose so it can implement the # required interfaces to connect the GUI to the rose engine. class MyFrame(wx.Frame, clroses.rose): def __init__(self): def makeSP(name, labels, statictexts = None): panel = wx.Panel(self.side_panel, -1) box = wx.StaticBox(panel, -1, name) sizer = wx.StaticBoxSizer(box, wx.VERTICAL) for name, min_value, value, max_value in labels: sp = SpinPanel(panel, name, min_value, value, max_value, self.OnSpinback) sizer.Add(sp, 0, wx.EXPAND) if statictexts: for name, text in statictexts: st = wx.StaticText(panel, -1, text) spin_panels[name] = st # Supposed to be a SpinPanel.... sizer.Add(st, 0, wx.EXPAND) panel.SetSizer(sizer) return panel wx.Frame.__init__(self, None, title="Roses in wxPython") self.rose_panel = RosePanel(self) self.side_panel = wx.Panel(self) # The cmd panel is four buttons whose names and foreground colors # change. Plop them in a StaticBox like the SpinPanels. self.cmd_panel = wx.Panel(self.side_panel, -1) box = wx.StaticBox(self.cmd_panel, -1, 'Command') sizer = wx.StaticBoxSizer(box, wx.VERTICAL) global ctrl_buttons border = 'wxMac' in wx.PlatformInfo and 3 or 0 for name, color, handler in ( ('Redraw', 'red', self.OnRedraw), ('Forward', 'magenta', self.OnForward), ('Go', 'dark green', self.OnGo), ('Back', 'blue', self.OnBack)): button = wx.Button(self.cmd_panel, -1, name) button.SetForegroundColour(color) ctrl_buttons[name] = button button.Bind(wx.EVT_BUTTON, handler) sizer.Add(button, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, border) self.cmd_panel.SetSizer(sizer) # Now make the rest of the control panels... # The order of creation of SpinCtrls and Buttons is the order that # the tab key will step through, so the order of panel creation is # important. # In the SpinPanel data (name, min, value, max), value will be # overridden by clroses.py defaults. self.coe_panel = makeSP('Coefficients', (('Style', 0, 100, 3600), ('Sincr', -3600, -1, 3600), ('Petal', 0, 2, 3600), ('Pincr', -3600, 1, 3600))) self.vec_panel = makeSP('Vector', (('Vectors' , 1, 399, 3600), ('Minimum' , 1, 1, 3600), ('Maximum' , 1, 3600, 3600), ('Skip first', 0, 0, 3600), ('Draw only' , 1, 3600, 3600)), (('Takes', 'Takes 0000 vectors'), )) self.tim_panel = makeSP('Timing', (('Vec/tick' , 1, 20, 3600), ('msec/tick', 1, 50, 1000), ('Delay' , 1, 2000, 9999))) self.opt_panel = OptionsPanel(self.side_panel, self.rose_panel) # put them all on in a sizer attached to the side_panel panelSizer = wx.BoxSizer(wx.VERTICAL) panelSizer.Add(self.cmd_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5) panelSizer.Add(self.coe_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5) panelSizer.Add(self.vec_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5) panelSizer.Add(self.tim_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5) panelSizer.Add(self.opt_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5) self.side_panel.SetSizer(panelSizer) # and now arrange the two main panels in another sizer for the frame mainSizer = wx.BoxSizer(wx.HORIZONTAL) mainSizer.Add(self.rose_panel, 1, wx.EXPAND) mainSizer.Add(self.side_panel, 0, wx.EXPAND) self.SetSizer(mainSizer) # bind event handlers self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer) # Determine appropriate image size. # At this point, the rose_panel and side_panel will both report # size (20, 20). After mainSizer.Fit(self) they will report the # same, but the Frame size, self.GetSize(), will report the desired # side panel dimensions plus an extra 20 on the width. That lets # us determine the frame size that will display the side panel and # a square space for the diagram. Only after Show() will the two # panels report the accurate sizes. mainSizer.Fit(self) rw, rh = self.rose_panel.GetSize() sw, sh = self.side_panel.GetSize() fw, fh = self.GetSize() h = max(600, fh) # Change 600 to desired minimum size w = h + fw - rw if verbose: print 'rose panel size', (rw, rh) print 'side panel size', (sw, sh) print ' frame size', (fw, fh) print 'Want size', (w,h) self.SetSize((w, h)) self.SupplyControlValues() # Ask clroses to tell us all the defaults self.Show() # Command button event handlers. These are relabled when changing between auto # and manual modes. They simply reflect the call to a method in the base class. # # Stop/Redraw button def OnRedraw(self, event): if verbose: print 'OnRedraw' self.cmd_stop() # Skip/Forward def OnForward(self, event): if verbose: print 'OnForward' self.cmd_step() # Redraw/Go def OnGo(self, event): if verbose: print 'OnGo' self.cmd_go() # Reverse/Back def OnBack(self, event): if verbose: print 'OnBack' self.cmd_reverse() # The clroses.roses class expects to have methods available that # implement the missing parts of the functionality needed to do # the actual work of getting the diagram to the screen and etc. # Those are implemented here as the App* methods. def AppClear(self): if verbose: print 'AppClear: clear screen' self.rose_panel.Clear() def AppCreateLine(self, line): # print 'AppCreateLine, len', len(line), 'next', self.nextpt self.rose_panel.DrawLines(line) # Here when clroses has set a new style and/or petal value, update # strings on display. def AppSetParam(self, style, petals, vectors): spin_panels['Style'].SetValue(style) spin_panels['Petal'].SetValue(petals) spin_panels['Vectors'].SetValue(vectors) def AppSetIncrs(self, sincr, pincr): spin_panels['Sincr'].SetValue(sincr) spin_panels['Pincr'].SetValue(pincr) def AppSetVectors(self, vectors, minvec, maxvec, skipvec, drawvec): spin_panels['Vectors'].SetValue(vectors) spin_panels['Minimum'].SetValue(minvec) spin_panels['Maximum'].SetValue(maxvec) spin_panels['Skip first'].SetValue(skipvec) spin_panels['Draw only'].SetValue(drawvec) def AppSetTakesVec(self, takes): spin_panels['Takes'].SetLabel('Takes %d vectors' % takes) # clroses doesn't change this data, so it's not telling us something # we don't already know. def AppSetTiming(self, vecPtick, msecPtick, delay): spin_panels['Vec/tick'].SetValue(vecPtick) spin_panels['msec/tick'].SetValue(msecPtick) spin_panels['Delay'].SetValue(delay) # Command buttons change their names based on the whether we're in auto # or manual mode. def AppCmdLabels(self, labels): for name, label in map(None, ('Redraw', 'Forward', 'Go', 'Back'), labels): ctrl_buttons[name].SetLabel(label) # Timer methods. The paranoia about checking up on the callers is # primarily because it's easier to check here. We expect that calls to # AppAfter and OnTimer alternate, but don't verify that AppCancelTimer() # is canceling anything as callers of that may be uncertain about what's # happening. # Method to provide a single callback after some amount of time. def AppAfter(self, msec, callback): if self.timer_callback: print 'AppAfter: timer_callback already set!', # print 'AppAfter:', callback self.timer_callback = callback self.timer.Start(msec, True) # Method to cancel something we might be waiting for but have lost # interest in. def AppCancelTimer(self): self.timer.Stop() # print 'AppCancelTimer' self.timer_callback = None # When the timer happens, we come here and jump off to clroses internal code. def OnTimer(self, evt): callback = self.timer_callback self.timer_callback = None # print 'OnTimer,', callback if callback: callback() # Often calls AppAfter() and sets the callback else: print 'OnTimer: no callback!' resize_delay = 300 def TriggerResize(self, size): self.resize(size, self.resize_delay) self.resize_delay = 100 def TriggerRedraw(self): self.repaint(10) # Called when data in spin boxes changes. def OnSpinback(self, name, value): if verbose: print 'OnSpinback', name, value if name == 'Style': self.SetStyle(value) elif name == 'Sincr': self.SetSincr(value) elif name == 'Petal': self.SetPetals(value) elif name == 'Pincr': self.SetPincr(value) elif name == 'Vectors': self.SetVectors(value) elif name == 'Minimum': self.SetMinVec(value) elif name == 'Maximum': self.SetMaxVec(value) elif name == 'Skip first': self.SetSkipFirst(value) elif name == 'Draw only': self.SetDrawOnly(value) elif name == 'Vec/tick': self.SetStep(value) elif name == 'msec/tick': self.SetDrawDelay(value) elif name == 'Delay': self.SetWaitDelay(value) else: print 'OnSpinback: Don\'t recognize', name verbose = 0 # Need some command line options... spin_panels = {} # Hooks to get from rose to panel labels ctrl_buttons = {} # Button widgets for command (NE) panel app = wx.App(False) MyFrame() if verbose: print 'spin_panels', spin_panels.keys() print 'ctrl_buttons', ctrl_buttons.keys() app.MainLoop()