Custom calendar :: refresh problem

Patrick J. Anderson pat.j.anderson at gmail.com
Sat Nov 3 14:43:15 PDT 2007


I'm trying to build a custom planner and being new to wxPython and 
desktop programming in general, I'm still learning how to do things.

Below is a little test I wrote. Its a frame with a splitter window. On 
the left there is a calendar control which I use to change dates and on 
the right, I have a notebook with 3 pages: 'Day', 'Week', 'Month', which 
correspond to the date selected in the calendar on the left.

I have created the daily, weekly and monthly views of the planner with a 
wx.FlexGridSizer and mostly wx.Panels.

I decided to recreate the properties and layout of each notebook page. 
I'm not sure if this is the right approach to building such widgets, as 
for example the CalendarCtrl is drawn on the screen and not combined from 
individual panels, but this is just a prototype.

However, as you may notice if you run it, I notice that the notebook 
pages flicker and quite often I see a small panel in the top left corner, 
which I believed shows when I loop through the days to create the display.

Any way this could be avoided and the transition made smoother?

Here's the full code:


#!/usr/bin/env python
# -*- coding: UTF-8 -*-

from calendar import Calendar, day_name, day_abbr
from datetime import datetime, date

import wx
import wx.calendar as cal
from wx.lib.scrolledpanel import ScrolledPanel

FRAME_SIZE = (960,700)

PLANNER_COLOURS = {
    'bg_header_normal': '#330',
    'fg_header_normal': '#cc9',
    
    'bg_header_today': '#000',
    'fg_header_today': '#ffc',
    
    'bg_hour_nonworking': '#cc9',
    'fg_hour_nonworking': '#996',
    
    'bg_hour_working': '#eeb',
    'fg_hour_working': '#330',
    
    'bg_day_normal': '#cc9',
    'fg_day_normal': '#996',
    
    'bg_day_first': '#dfdfaf',
    'fg_day_first': '#3f3f0f',
    
    'bg_day_today': '#ffc',
    'fg_day_today': '#330',
    
    'bg_day_busy': '#fff', 
    'fg_day_busy': '#000',   
}


class MainFrame(wx.Frame):

    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)
        planner = PlannerSplitterWindow(self, style = wx.SP_3D)
        hbox = wx.BoxSizer(wx.HORIZONTAL)
        hbox.Add(planner, 1, wx.EXPAND)
        self.statusbar = self.CreateStatusBar()
        self.SetAutoLayout(True)
        self.SetSizer(hbox)


class PlannerSplitterWindow(wx.SplitterWindow):

    proportion = 0.75

    def __init__(self, *args, **kwds):
        wx.SplitterWindow.__init__(self, *args, **kwds)
        
        hbox = wx.BoxSizer(wx.HORIZONTAL)

        self.left_pane = CalendarCtrlPanel(self, wx.NewId(), name = 
'PlannerCalendarCtrl')
        self.right_pane = PlannerBookPanel(self, wx.NewId(), name = 
'PlannerBook')

        self.SplitVertically(self.left_pane, self.right_pane)
        self.SetMinimumPaneSize(300)

        hbox.Add(self, 1, wx.ALL|wx.EXPAND, 0)
        self.SetAutoLayout(True)
        self.SetSizer(hbox)

        if not 0 < self.proportion < 1:
            raise ValueError, "proportion value for ProportionalSplitter 
must be between 0 and 1."
        self.ResetSash()
        self.Bind(wx.EVT_SIZE, self.OnReSize)
        self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.OnSashChanged, 
id = self.GetId())
        ##hack to set sizes on first paint event
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.firstpaint = True
        
        

    def SplitHorizontally(self, win1, win2):
        if self.GetParent() is None: 
            return False
        return wx.SplitterWindow.SplitHorizontally(self, win1, win2, 
            int(round(self.GetParent().GetSize().GetHeight() * 
self.proportion)))

    def SplitVertically(self, win1, win2):
        if self.GetParent() is None:
            return False
        return wx.SplitterWindow.SplitVertically(self, win1, win2,
            int(round(self.GetParent().GetSize().GetWidth() * 
self.proportion)))

    def GetExpectedSashPosition(self):
        if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
            total = max(self.GetMinimumPaneSize(),self.GetParent
().GetClientSize().height)
        else:
            total = max(self.GetMinimumPaneSize(),self.GetParent
().GetClientSize().width)
        return int(round(total * self.proportion))

    def ResetSash(self):
        self.SetSashPosition(self.GetExpectedSashPosition())

    def OnReSize(self, event):
        """Window has been resized, so we need to adjust the sash based 
on self.proportion."""
        self.ResetSash()
        event.Skip()

    def OnSashChanged(self, event):
        """We'll change self.proportion now based on where user dragged 
the sash."""
        position = float(self.GetSashPosition())
        if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
            total = max(self.GetMinimumPaneSize(),self.GetParent
().GetClientSize().height)
        else:
            total = max(self.GetMinimumPaneSize(),self.GetParent
().GetClientSize().width)
        self.proportion = position / total
        event.Skip()

    def OnPaint(self,event):
        if self.firstpaint:
            if self.GetSashPosition() != self.GetExpectedSashPosition():
                self.ResetSash()
            self.firstpaint = False
        event.Skip()


class CalendarCtrlPanel(wx.Panel):

    def __init__(self, *args, **kwargs):
        wx.Panel.__init__(self, *args, **kwargs)
        self.SetAutoLayout(True)
        self.layout = self._doLayout()


    def _doLayout(self):
        vbox = wx.BoxSizer(wx.VERTICAL)
        hbox = wx.BoxSizer()
        
        self.calendar = MiniCalendar(self, wx.NewId(), wx.DateTime_Now())
        self.Bind(cal.EVT_CALENDAR_SEL_CHANGED, 
self.calendar.OnCalSelChanged, self.calendar)
                
        vbox.Add(hbox, 0, wx.ALL | wx.GROW, 1)
        vbox.Add(self.calendar, 0, wx.ALL | wx.GROW, 1)

        self.SetSizer(vbox)



class MiniCalendar(cal.CalendarCtrl):

    def __init__(self, *args, **kwargs):
        cal.CalendarCtrl.__init__(self, *args, **kwargs)

    def OnCalSelChanged(self, event):
        cal = event.GetEventObject()
        dp = wx.FindWindowByName('DayPlanner')
        wp = wx.FindWindowByName('WeekPlanner')
        mp = wx.FindWindowByName('MonthPlanner')
        dp.onDateChanged(cal.GetDate())
        wp.onDateChanged(cal.GetDate())
        mp.onDateChanged(cal.GetDate())


class PlannerBookPanel(wx.Panel):

    def __init__(self, *args, **kwargs):
        wx.Panel.__init__(self, *args, **kwargs)
        
        self.today = datetime.today()
        self.date = self.today

        self.nb = wx.Notebook(self, wx.NewId(), 
                style = wx.NB_BOTTOM | 
                wx.TAB_TRAVERSAL |
                wx.SUNKEN_BORDER)
        
        self.createPanels()
        self.addPanels()
        self.doLayout()


    def createPanels(self):
        self.pane_day = DayPlannerPanel(self.nb, wx.NewId(), 
            name = 'DayPlanner', style = wx.EXPAND)
        self.pane_week = WeekPlannerPanel(self.nb, wx.NewId(), 
            name = 'WeekPlanner', style = wx.EXPAND)
        self.pane_month = MonthPlannerPanel(self.nb, wx.NewId(), 
            name = 'MonthPlanner', style = wx.EXPAND)


    def addPanels(self):
        self.nb.AddPage(self.pane_day, 'Day')
        self.nb.AddPage(self.pane_week, 'Week')
        self.nb.AddPage(self.pane_month, 'Month')


    def doLayout(self):
        self.hbox = wx.BoxSizer(wx.HORIZONTAL)
        self.hbox.Add(self.nb, 1, wx.ALL|wx.EXPAND, 1)
        self.SetAutoLayout(True)
        self.SetSizer(self.hbox)


class DayPlannerPanel(ScrolledPanel):

    def __init__(self, *args, **kwargs):
        ScrolledPanel.__init__(self, *args, **kwargs)
        self.properties = self.setProperties()
        self.layout = self.doLayout()


    def onDateChanged(self, d):
        self.properties = self.setProperties(d)
        self.layout = self.doLayout()
        #self.Refresh(True)
        print 'Day planner date has changed to %s' % self.date


    def setProperties(self, d = wx.DateTime_Now()):
        self.date = d
        self.SetBackgroundColour(PLANNER_COLOURS['bg_header_normal'])
        self.work_hours = []
        [self.work_hours.append(i) for i in range(9, 22)]


    def doLayout(self):
        self.fbox = wx.FlexGridSizer(0, 2, 1, 1)
        self.fbox.AddGrowableCol(1)

        for i in range(1,24):
            self.fbox.AddGrowableRow(i)

        self.headers = self._createHeaders()
        
        for h in range(1, 12):
            self._createHourRow(h)
        for h in range(12, 24):
            self._createHourRow(h)

        self.SetAutoLayout(True)
        self.SetSizer(self.fbox)
        self.SetupScrolling()


    def _createHeaders(self):

        hp1 = wx.Panel(self, wx.NewId())
        hp1.SetBackgroundColour("%s" % PLANNER_COLOURS
['bg_header_normal'])
        hbox1 = wx.BoxSizer()
        hour_label = wx.StaticText(hp1, wx.NewId(), '',  
wx.DefaultPosition, [50, -1],)
        hbox1.Add(hour_label, 1, wx.ALL|wx.GROW, 5)
        hp1.SetSizer(hbox1)
        self.fbox.Add(hp1, 1, wx.GROW)

        hp2 = wx.Panel(self, wx.NewId())
        hp2.SetBackgroundColour("%s" % PLANNER_COLOURS
['bg_header_normal'])
        hbox2 = wx.BoxSizer()
        self.date_header = wx.StaticText(hp2, wx.NewId(), str(self.date))
        self.date_header.SetForegroundColour('%s' % PLANNER_COLOURS
['fg_header_normal'])
        hbox2.Add(self.date_header, 1, wx.ALL|wx.ALIGN_CENTER|wx.GROW, 5)
        hp2.SetSizer(hbox2)
        self.fbox.Add(hp2, 1, wx.GROW)


    def _createHourRow(self, hour):

        if hour <= 11:
            x = 'am'
        else:
            x = 'pm'

        l_panel = wx.Panel(self, wx.NewId())
        l_box = wx.BoxSizer(wx.VERTICAL)
        h_label = wx.StaticText(l_panel, wx.NewId(), '%i %s' % (hour, x), 
            style = wx.ALIGN_RIGHT)
            
        if hour not in self.work_hours:
            l_panel.SetBackgroundColour('%s' % PLANNER_COLOURS
['bg_hour_nonworking'])
            h_label.SetFont(wx.Font(10, wx.SWISS, wx.ITALIC, wx.LIGHT))
            h_label.SetForegroundColour('%s' % PLANNER_COLOURS
['fg_hour_nonworking'])
        else:
            l_panel.SetBackgroundColour('%s' % PLANNER_COLOURS
['bg_hour_working'])
            h_label.SetFont(wx.Font(10, wx.SWISS, wx.ITALIC, wx.NORMAL))
            h_label.SetForegroundColour('%s' % PLANNER_COLOURS
['fg_hour_working'])

        l_box.Add(h_label, 1, wx.ALL | wx.EXPAND, 5)
        l_panel.SetSizer(l_box)
        self.fbox.Add(l_panel, 1, wx.ALL | wx.EXPAND, 0)

        h_panel = HourPanel(self, wx.NewId())
        if not hour in self.work_hours:
            h_panel.SetBackgroundColour('%s' % PLANNER_COLOURS
['bg_hour_nonworking'])
        else:
            h_panel.SetBackgroundColour('%s' % PLANNER_COLOURS
['bg_hour_working'])
        h_box = wx.BoxSizer(wx.VERTICAL)
        h_panel.SetSizer(h_box)
        self.fbox.Add(h_panel, 1, wx.ALL | wx.EXPAND, 0)


class HourPanel(wx.Panel):
    
    def __init__(self, *args, **kwargs):
        wx.Panel.__init__(self, *args, **kwargs)



class WeekPlannerPanel(ScrolledPanel):

    def __init__(self, *args, **kwargs):
        ScrolledPanel.__init__(self, *args, **kwargs)
        self.setProperties()
        self.doLayout()


    def onDateChanged(self, d):
        self.Freeze()
        self.setProperties(d)
        self.removeHeaders()
        self.removeWeekDays()
        self.doLayout()
        #self.Refresh()
        self.Thaw()
        print 'Week Planner date has changed to %s' % self.date


    def setProperties(self, d = wx.DateTime_Now()):
        self.today = datetime.today()
        self.date = d
        py_date = cal._wxdate2pydate(self.date)
        calendar = Calendar(0)
        self.weeks = calendar.monthdatescalendar(py_date.year, 
py_date.month)
        self.num_weeks = len(self.weeks)
        for w in self.weeks:
            if py_date in w:
                self.week = w
        self.SetBackgroundColour(PLANNER_COLOURS['bg_header_normal'])


    def doLayout(self):
        self.fbox = wx.FlexGridSizer(2, 7, 1, 1)
        # expand weekday columns and row
        for c in range(7):
            self.fbox.AddGrowableCol(c)
        self.fbox.AddGrowableRow(1)
        self.addHeaders()
        self.addWeekDays()
        self.SetAutoLayout(True)
        self.SetSizer(self.fbox)
        #self.SetupScrolling()

    

    def addHeaders(self):
        self.headers = []
        i = 0
        for d in self.week:
            self.headers.append(wx.Button(self, wx.NewId(), d.strftime('%
a %d %b')))
            if (d.day == self.today.day 
                and d.month == self.today.month 
                and d.year == self.today.year):
                self.headers[i].SetBackgroundColour(PLANNER_COLOURS
['bg_header_today'])
                self.headers[i].SetForegroundColour(PLANNER_COLOURS
['fg_header_today'])
            else: 
                self.headers[i].SetBackgroundColour(PLANNER_COLOURS
['bg_header_normal'])
                self.headers[i].SetForegroundColour(PLANNER_COLOURS
['fg_header_normal'])
            self.fbox.Add(self.headers[i], 1, wx.ALL|wx.EXPAND, 0)
            i += 1


    def addWeekDays(self):
        self.weekdays = []
        i = 0
        for d in self.week:
            self.weekdays.append(WeekDayPanel(self, wx.NewId()))
            if (d.day == self.today.day 
                and d.month == self.today.month 
                and d.year == self.today.year):
                self.weekdays[i].SetBackgroundColour(PLANNER_COLOURS
['bg_day_today'])
            else:
                self.weekdays[i].SetBackgroundColour(PLANNER_COLOURS
['bg_day_normal'])
            self.fbox.Add(self.weekdays[i], 1, wx.ALL|wx.EXPAND,0)
            i += 1


    def removeHeaders(self):
        [self.fbox.Remove(i) for i in self.headers]

    def removeWeekDays(self):
        [self.fbox.Remove(i) for i in self.weekdays]
    

class WeekDayPanel(wx.Panel):
    
    def __init__(self, *args, **kwargs):
        wx.Panel.__init__(self, *args, **kwargs)


class MonthPlannerPanel(ScrolledPanel):

    def __init__(self, *args, **kwargs):
        ScrolledPanel.__init__(self, *args, **kwargs)
        self.properties = self.setProperties()
        self.layout = self.doLayout()

        #self.Bind(wx.EVT_SIZE, self.OnSize, self)
        #self.Bind(wx.EVT_PAINT, self.OnPaint, self)


    def onDateChanged(self, d):
        self.properties = self.setProperties(d)
        self.layout = self.doLayout()
        self.Refresh()
        print 'Month planner date has changed to %s\n---------' % 
self.date


    def setProperties(self, d = wx.DateTime_Now()):
        self.date = d
        py_date = cal._wxdate2pydate(self.date)
        calendar = Calendar(0)
        self.month_dates = list(calendar.itermonthdates(py_date.year, 
py_date.month))
        self.no_days = len(self.month_dates)
        self.no_rows = self.no_days / 7
        self.SetBackgroundColour(PLANNER_COLOURS['bg_header_normal'])   
        
        
    def doLayout(self):
        self.fbox = wx.FlexGridSizer(0, 7, 1, 1)
        
        # expand all cols and date rows
        for c in range(7):
            self.fbox.AddGrowableCol(c)
        for r in range(1, self.no_rows + 1):
            self.fbox.AddGrowableRow(r)
        
        self.headers = self._createHeaders()
        self.days = self._createMonthDays()

        self.SetAutoLayout(True)
        self.SetSizer(self.fbox)
        self.SetupScrolling()


    def _createHeaders(self):
         # add headers to the calendar
        for h in day_abbr:
            hp = wx.Panel(self, wx.NewId())
            hp.SetBackgroundColour(PLANNER_COLOURS['bg_header_normal'])
            text = wx.StaticText(hp, wx.NewId(), str(h))
            text.SetForegroundColour(PLANNER_COLOURS['fg_header_normal'])
            hbox = wx.BoxSizer(wx.HORIZONTAL)
            hbox.Add(text, 1, wx.ALL, 5)
            hp.SetSizer(hbox)
            self.fbox.Add(hp, 1, wx.GROW)
            wx.Yield()

    def _createMonthDays(self):
        # add calendar day panels
        for d in self.month_dates:
            p = MonthDayPanel(self, wx.NewId(), style = wx.TAB_TRAVERSAL)
            p.create(d)
            self.fbox.Add(p, 1, wx.ALL|wx.GROW, 0)
             

class MonthDayPanel(wx.Panel):
    
    def __init__(self, *args, **kwargs):
        wx.Panel.__init__(self, *args, **kwargs)
        self.today = date.today()

    
    def create(self, date):
        vbox = wx.BoxSizer(wx.VERTICAL)
        
        if (date.day == self.today.day 
              and date.month == self.today.month 
              and date.year == self.today.year):
            self.SetBackgroundColour(PLANNER_COLOURS['bg_day_today'])
            d = wx.StaticText(self, wx.NewId(), str("%s %s" % 
(date.strftime('%b'), date.day)))
            d.SetFont(wx.Font(8, wx.SYS_SYSTEM_FONT, wx.NORMAL, wx.BOLD))
            d.SetForegroundColour(PLANNER_COLOURS['fg_day_today'])
        elif date.day == 1:
            self.SetBackgroundColour(PLANNER_COLOURS['bg_day_first'])
            d = wx.StaticText(self, wx.NewId(), str("%s %s" % 
(date.strftime('%b'), date.day)))
            d.SetFont(wx.Font(8, wx.SYS_SYSTEM_FONT, wx.NORMAL, wx.BOLD))
            d.SetForegroundColour(PLANNER_COLOURS['fg_day_first'])
        else:
            self.SetBackgroundColour(PLANNER_COLOURS['bg_day_normal'])
            d = wx.StaticText(self, wx.NewId(), str(date.day))
            d.SetFont(wx.Font(8, wx.SYS_SYSTEM_FONT, wx.NORMAL, 
wx.NORMAL))
            d.SetForegroundColour(PLANNER_COLOURS['fg_day_normal'])
        
        vbox.Add(d, 0, wx.ALL, 3)

        self.SetSizer(vbox)


class Application(wx.App):

    def __init__(self, *args, **kwargs):
        wx.App.__init__(self, *args, **kwargs)

    def OnInit(self):
        self.frame = MainFrame(None, wx.NewId(), 'Calendar Test', size = 
FRAME_SIZE)
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

    def getFrame(self):
        return self.frame

if __name__ == "__main__":
    app = Application()
    app.MainLoop()





More information about the wxpython-users mailing list