# abb 6/28/2012: separated the WX gui stuff to this module. # -*- coding: utf-8 -*- # above encoding is for non-ascii in source comments """ Andy at FosteringCourtImprovement.org, 12/21/2011, version 1.0 release WX Python GUI script reads multiple AFCARS files, sorts and classifies rows, displays color-coded records in a search-enabled grid, saves to AFCARS file with 2 added fields: 1. removal episode id, and 2. record class. History: abb 5/18/2012: crashing bc I didn't check for unicode in ExcessData, fixed, version 1.1. abb 5/19/2012: epid format was %20.20s, should be %21.21s, fixed, version 1.1. """ import os, sys from afcarslinker1p2_data import * import wx, wx.grid #=========================================================== # GUI data handling objects: # quicky function to output stats table: def dict2html( d1, h1 ): if len(d1) > 0: t1 = '
' + ' | '.join(h1) + ' | |
---|---|---|
' + k + ' | ' + ' | '.join(v) + ' |
No valid records in files?
' return( t1 ) # This GridDataTable supplies the wx.DataFrame with the data to put in its wx.grid. class GridDataTable( wx.grid.PyGridTableBase ): _rowlabels = _collabels = _data = None _irow = None # index to rows that are displayed, used for filtering & toggling display of classes of rows _firstfield = None # row classes should have bg, fg, & font associated with them. best structure for that? # dict, collections.namedtuple, class with __slots__? # http://dev.svetlyak.ru/using-slots-for-optimisation-in-python-en/ rowclasslabels = RecordClass.recordlabels rowclassbgcols = RecordClass.recordcolors rowclassshow = [True] * len(rowclasslabels) colclasslabels = ( 'Field Change in Episode', ) colclassbgcols = ( '#b000b0', ) rowclasses = None # working copy of class of each row in _data defaultfont = boldfont = strikethroughfont = None fieldfilterfield = None fieldfilterpat = None def GetRowLabelValue(self, row): if row < len(self._irow): row = self._irow[row] return self._rowlabels[row] if self._rowlabels and row < len(self._rowlabels) else None else: return None def GetRowLabelValues(self): return self._rowlabels[self._irow] if self._irow else self._rowlabels def GetColLabelValue(self, col): return self._collabels[col] if self._collabels else None def GetColLabelValues(self): return self._collabels def GetNumberRows(self): return len(self._irow) if self._irow else 0 def GetNumberCols(self): return len(self._collabels) if self._collabels else 0 def GetValue(self, row, col): #print >> sys.stderr, 'GetValue: ', row, col, len(self._data), len(self._data[0]) if row < len(self._irow): row = self._irow[row] # linux-only bug: producing "list index out of range" when 2nd file opened # and first file had fewer lines than screen. caching in grid? # this works around, but something somewhere still out of range? if self._data and row < len(self._data) and col < len(self._data[row]): return self._data[row][col] else: return '' else: return '' def GetAttr(self, row, col, kind): attr = wx.grid.GridCellAttr() if row < len(self._irow): row = self._irow[row] if self.rowclasses and row < len(self.rowclasses): attr.SetBackgroundColour( self.rowclassbgcols[self.rowclasses[row]] ) if self.rowclasses[row] == RecordClass.MostRecentEpisode and self.boldfont: attr.SetFont( self.boldfont ) elif self.rowclasses[row] == RecordClass.Duplicate and self.strikethroughfont: attr.SetFont( self.strikethroughfont ) # Long test here, because not all rows have same # of cols. #if col==20 and self._data[row][self._firstfield].find('GBOSK4VJTH2L') >= 0: if row < (len(self._data)-1) and col > 4 and col < len(self._data[row]) and col < len(self._data[row+1]) and self._data[row][self._firstfield] == self._data[row+1][self._firstfield] and self._data[row][col].strip() != self._data[row+1][col].strip(): attr.SetBackgroundColour( self.colclassbgcols[0] ) # highlight changed fields #try: #except: # print >> sys.stderr, 'GetAttr: ', row, col, len(self._data), len(self._data[0]) return attr def SetFirstField( self, ff ): self._firstfield = ff def SetValues( self, dat, rownames=None, colnames=None ): self._rowlabels = list(rownames) or range(len(dat)) self._collabels = list(colnames) or range(len(dat[0])) self._data = dat self._rowlabels0 = self._data0 = None def SetRowClasses( self, classes ): self.rowclasses = classes self.UpdateRowClasses() def SetDefaultCellFont( self, font ): import copy self.defaultfont = copy.copy(font) self.boldfont = wx.Font( font.GetPointSize(), font.GetFamily(), font.GetStyle(), wx.BOLD ) # windows keep using same font no matter what I do? totally unpredictable: switched family to Arial? # various ms-win workarounds below: self.strikethroughfont = wx.Font( font.GetPointSize()-1, font.GetFamily(), font.GetStyle(), wx.NORMAL ) fontdesc = (self.strikethroughfont.GetNativeFontInfoDesc()).split(';') if len(fontdesc) > 8: fontdesc[8] = '1' fi = wx.NativeFontInfo() fi.FromString( ';'.join(fontdesc) ) self.strikethroughfont.SetNativeFontInfo( fi ) # modifies default/boldfont too on mswin? # mswin bug: all the same font, all the time -- workarounds above work on winxp & win7. def ToggleRows( self, rc, show ): self.rowclassshow[rc] = show self.UpdateRowClasses() def SetFieldFilterField( self, fieldname ): self.fieldfilterfield = self._collabels.index(fieldname) def GetFieldFilterField( self ): return self._collabels[self.fieldfilterfield] if self.fieldfilterfield else None def SetFieldFilter( self, pat ): self.fieldfilterpat = pat.upper() def GetFieldFilter( self ): return self.fieldfilterpat def UpdateRowClasses( self ): import re irow = [] if self.rowclasses: #print >> sys.stderr, 'UpdateRowClasses: ', self.fieldfilterfield, self.fieldfilterpat, '.' pat = re.compile( self.fieldfilterpat ) if self.fieldfilterpat else None for row, rc in enumerate(self.rowclasses): #if self.rowclassshow[rc] and ( (not self.fieldfilterfield) or (not self.fieldfilterpat) or (self._data[row][self.fieldfilterfield].upper().find(self.fieldfilterpat) >= 0) ): if self.rowclassshow[rc] and ( (not self.fieldfilterfield) or (not pat) or (pat.search(self._data[row][self.fieldfilterfield].upper())) ): irow.append( row ) self._irow = irow """ Can only set background color with generic StaticText class, not wx.StaticText for some reason. http://wxpython-users.1045709.n5.nabble.com/wx-StaticText-problem-td2361234.html from wx.lib.stattext import GenStaticText as StaticText st2 = StaticText(self, wx.ID_ANY, text ) #, wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_LEFT, '' ) st2.SetFont(wx.Font(8, wx.SWISS, wx.NORMAL, wx.BOLD, False)) Can't set checkbox bg color on GTK systems (Linux) and no generic checkbox class? One option is cb with no label, hsizer with a statictext box, but need to pass through to cb methods ... Too much trouble. Just reverse fg & bg under GTK, since fg color works. """ #=========================================================== # A custom status bar subclass that appends static text fields to traditional status text field. # Derived from demo/StaticText.py. class MyStatusBar(wx.StatusBar): def __init__(self, parent, nfields=0, fieldwidth=100): wx.StatusBar.__init__(self, parent, -1) self.SetFieldsCount(nfields+1) self._widgets = [None] * (nfields+1) self._callbacks = [None] * (nfields+1) # Preserve field 0 as normal, stretchy statusbar field. self.SetStatusWidths( [-1] + [fieldwidth]*nfields ) self.sizeChanged = True self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_IDLE, self.OnIdle) def SetLabel( self, pos, text, fg='black', bg=None, callback=None, font=None ): if self._widgets[pos]: self.RemoveChild(self._widgets[pos]) self._widgets[pos].Destroy() st2 = wx.CheckBox( self, wx.ID_ANY, text ) self.Bind(wx.EVT_CHECKBOX, self.OnToggle, st2) st2.SetValue(True) if callback: self._callbacks[pos] = callback else: st2.Enable(False) if font: st2.SetFont( font ) if st2.GetGtkWidget(): # better test if we're on gtk, and can't set cb bg color? st2.SetForegroundColour( bg ) else: st2.SetBackgroundColour( bg ) st2.SetForegroundColour( fg ) self._widgets[pos] = st2 self.Reposition() return st2 def OnToggle(self, evt): #print >> sys.stderr, self._widgets.index(evt.GetEventObject()), '.' pos = self._widgets.index( evt.GetEventObject() ) if pos and self._callbacks[pos]: self._callbacks[pos]( pos, self._widgets[pos].GetLabelText(), self._widgets[pos].GetValue() ) def OnSize(self, evt): self.Reposition() # for normal size events # Set a flag so the idle time handler will also do the repositioning. # It is done this way to get around a buglet where GetFieldRect is not # accurate during the EVT_SIZE resulting from a frame maximize. self.sizeChanged = True def OnIdle(self, evt): if self.sizeChanged: self.Reposition() # reposition the widgets def Reposition(self): for pos, widget in enumerate(self._widgets): if widget: rect = self.GetFieldRect(pos) widget.SetPosition((rect.x+2, rect.y+2)) widget.SetSize((rect.width-4, rect.height-4)) self.sizeChanged = False #=========================================================== # A custom search bar to remember searches. # Derived from demo/ToolBar.py class MySearchCtrl(wx.SearchCtrl): maxSearches = 20 def __init__(self, parent, id=-1, value="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, prompt='search', doSearch=None): style |= wx.TE_PROCESS_ENTER wx.SearchCtrl.__init__(self, parent, id, value, pos, size, style) self.ShowCancelButton( True ) self.Bind(wx.EVT_TEXT_ENTER, self.OnTextEntered) self.Bind(wx.EVT_MENU_RANGE, self.OnMenuItem, id=1, id2=self.maxSearches) self.Bind( wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnClear ) self.SetDescriptiveText( prompt ) self.doSearch = doSearch self.searches = [] def OnTextEntered(self, evt): text = self.GetValue() if self.doSearch(text) and len(text) > 0 and (not( text in self.searches)): self.searches.append(text) if len(self.searches) > self.maxSearches: del self.searches[0] self.SetMenu(self.MakeMenu()) def OnClear(self, evt): self.SetValue("") self.OnTextEntered( None ) def OnMenuItem(self, evt): text = self.searches[evt.GetId()-1] self.SetValue(text) self.doSearch(text) def MakeMenu(self): menu = wx.Menu() item = menu.Append(-1, "Recent Searches") item.Enable(False) for idx, txt in enumerate(self.searches): menu.Append(1+idx, txt) return menu # http://www.blog.pythonlibrary.org/2008/06/11/wxpython-creating-an-about-box/ # alternate: http://www.daniweb.com/software-development/python/threads/128350/921519#post921519 import wx.html class AboutDlg(wx.Frame): def __init__(self, parent, msg): wx.Frame.__init__(self, parent, wx.ID_ANY, title="About", size=(700,500)) html = wxHTML(self, parent) html.SetPage( msg ) class wxHTML(wx.html.HtmlWindow): def __init__(self, parent, grandma): wx.html.HtmlWindow.__init__(self, parent) self.parent = grandma def OnLinkClicked(self, link): # webbrowser.open(link.GetHref()) # ugly, should make a parent.Search( 'RecNum', link.GetHref() ) ... self.parent.cb1.SetStringSelection( 'RecNum' ) self.parent.data.SetFieldFilterField( 'RecNum' ) self.parent.sc1.SetValue( link.GetHref() ) self.parent.sc1.OnTextEntered( None ) #print >> sys.stderr, link.GetHref() #=========================================================== # Top level window holding my data grid: # started from http://stackoverflow.com/questions/1866321/highlight-a-row-in-wxgrid-with-wxpython-when-a-cell-in-that-row-changes-progra class DataFrame(wx.Frame): datafile = None def __init__(self): wx.Frame.__init__(self, None, size=( wx.DisplaySize()[0], 600 )) self.title = 'AFCARS File Linker' self.SetTitle( self.title ) import os ip = os.path.join( ScriptPath().d, 'fci.ico' ) if os.path.isfile( ip ): try: ib = wx.IconBundle() ib.AddIconFromFile( ip, wx.BITMAP_TYPE_ANY ) self.SetIcons(ib) except: pass # Holds the actual data: self.data = GridDataTable() # Renders the data in a Grid: self.grid = wx.grid.Grid(self) self.grid.SetTable(self.data) # Small font font = self.grid.GetDefaultCellFont() font.SetPointSize( 8 ) # fixed and copied throughout self.grid.SetDefaultCellFont( font ) self.data.SetDefaultCellFont( font ) self.grid.SetColLabelTextOrientation( wx.VERTICAL ) self.grid.SetColLabelAlignment( wx.ALIGN_LEFT, wx.ALIGN_CENTRE ) self.grid.SetColLabelSize( 120 ) #wx.GRID_AUTOSIZE ) # fixed at this self.grid.SetRowMinimalAcceptableHeight( 0 ) # To hide rows, need to set this to zero? self.grid.SetRowLabelSize( 120 ) # fit to data later self.grid.SetRowLabelAlignment( wx.ALIGN_RIGHT, wx.ALIGN_CENTRE ) # No top menu. # Create toolbar with filter box. self.tb = self.CreateToolBar( wx.TB_HORIZONTAL | wx.RAISED_BORDER ) #self.tb.SetMarginsXY( 3, 3 ) tsize = (24,24) bmp = wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize) self.tb.AddLabelTool( wx.ID_FILE, 'Open', bmp, shortHelp='Open File', longHelp='Add Another AFCARS File to the Linking') self.Bind( wx.EVT_TOOL, self.OnOpenFile, id=wx.ID_FILE ) bmp = wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize) self.tb.AddLabelTool( wx.ID_SAVEAS, 'Save As', bmp, shortHelp='Save File', longHelp='Save Linked Records as a CSV or Fixed-Width Text File') self.Bind( wx.EVT_TOOL, self.OnSaveAs, id=wx.ID_SAVEAS ) bmp = wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, tsize) self.tb.AddLabelTool( wx.ID_ABOUT, 'About', bmp, shortHelp='Statistics', longHelp='Version and Statistics') self.Bind( wx.EVT_TOOL, self.OnAbout, id=wx.ID_ABOUT ) self.tb.AddSeparator() self.cb1 = wx.Choice( self.tb, wx.ID_ANY, size=(1.4*self.grid.GetColLabelSize(),-1), name='Filter Field' ) self.Bind( wx.EVT_CHOICE, self.OnFieldFilterChoice ) self.tb.AddControl( self.cb1 ) self.sc1 = MySearchCtrl( self.tb, size=(200,-1), prompt='filter records', doSearch=self.OnFieldFilter ) self.tb.AddControl( self.sc1 ) #self.CreateStatusBar(3) # Statusbar at bottom self.sb = MyStatusBar(self, len(self.data.rowclasslabels)+len(self.data.colclasslabels), 170) # Add checkbox controls to bottom statusbar. font.SetWeight(wx.BOLD) pos = 1 for i in range(0, len(self.data.rowclasslabels)): self.sb.SetLabel( pos, self.data.rowclasslabels[i], 'black', self.data.rowclassbgcols[i], callback=self.OnToggleRows, font=font ) pos = pos + 1 for i in range(0, len(self.data.colclasslabels)): self.sb.SetLabel( pos, self.data.colclasslabels[i], 'black', self.data.colclassbgcols[i], font=font ) pos = pos + 1 self.SetStatusBar(self.sb) # Copy handler (copy to Excel?) self.grid.Bind( wx.EVT_KEY_DOWN, self.OnKey ) # Data grid fills a sizer for positioning. self.sizer = wx.BoxSizer(wx.VERTICAL) # don't have to add toolbar to sizer. self.sizer.Add(self.grid, 1, wx.EXPAND, 0 ) self.SetSizer(self.sizer) self.tb.Realize() self.grid.SetFocus() #=========================================================== # Event Handling def OnKey( self, event ): if event.ControlDown() and event.GetKeyCode() == 67 and self.grid.IsSelection(): self.CopyRangeToClipboard() # Skip other Key events if event.GetKeyCode(): event.Skip() return def OnAbout( self, event=None, msg='' ): version = '1.2' title = 'Potential record number linkage errors. Try these searches:\n
Select a file first.
' l1 = '' aboutDlg = AboutDlg( self, title+msg+t1+l1 ) aboutDlg.Show() def OnExit( self, event ): self.Close(True) # Close this frame. def LinkAlert( self, filenames ): # check if any of the just-opened files link to prior files at less than 20%. if self.datafile.filestats: fs2 = self.datafile.filestats.ToPct() if len(fs2) > 1 and filenames: for fn in reversed(sorted(fs2.keys())[1:]): if fn in filenames and fs2[fn][FileStats.ilinks2prior] < 20: print >> sys.stderr, 'WARNING: Links in '+fn+' < 20%.' msg = '