Search code examples
python-3.xcsvuser-interfacewxpython

wxPython - can't call class method within __init__


I know this is probably a basic question, but I'm just getting into GUI building and haven't needed to dabble much in classes for that matter. I have some code (see below) to handle the GUI portion of the program (the CLI version is already running perfectly, but management runs for the hills whenever they see a CLI...), but I can't quite get it to work out. FYI running python 3.9.1 and wxPython 4.1.1 Phoenix (though wx.version also spits out wxWidgets 3.1.5, if that's relevant) on Windows 10

A lot of the code was based around examples / other folks asking stuff here, and unfortunately I can't find the original posts to credit them, but this is really just for learning and once I have a grasp of what I'm doing I'll be writing everything from scratch. So to the original authors, I apologize, but I thank you enormously!

The program is to display a "start" screen (frame), with just a title and single button. On clicking the button, a modal file dialog box opens where the user selects the .csv data file to use, the start frame disappears, and a new frame appears with a view of the data in the right pane (in a grid sizer) and some drop downs and radio buttons in the left pane. The last drop-down (self.inputNum) will be bound to an event handler which will add a number of additional drop-downs for the user to select the different targets to analyze (the number of course being determined by their selection in the self.inputNum combobox). However, when running the script, I get an error

Traceback (most recent call last):
  File "C:\Python\Scripts\wxLOD_2class_panelbased.py", line 98, in _OnStart
    self.frm2 = Frame2(None)
  File "C:\Python\Scripts\wxLOD_2class_panelbased.py", line 154, in __init__
    self.CreateGrid(datalist, cols)
AttributeError: 'Frame2' object has no attribute 'CreateGrid'

I searched around here, and found someone with a similar issue (calling a class method within the init of the same class), but the 'correct' answer was to call the method using self.classmethodname(args) (OP had been calling it as just classmethodname(args), which I'm already doing.

Apologies in advance for the sloppy formatting. I tried to clean it up as much as I could, but since I don't know where the error is coming from I didn't want to stray too far from the (working) examples I used to build the mess I currently have. See below for my script, the slightly-modified script I'm basing my "Frame2" around, and screenshots.

My code (I know importing modules in a single line is frowned upon, but there are so many I compressed it for this post):

import os, sys, csv, time, math, warnings, collections, wx, wx.grid, scipy
import numpy as np
import matplotlib.pyplot as plt
import win32gui as wg
import pandas as pd
from datetime import datetime
from scipy.stats import norm
from tkinter import filedialog
from tkinter import *
from decimal import *
pd.options.mode.chained_assignment = None  # default='warn'

class StartFrame(wx.Frame):
    """App controller class"""
    FRAME_MIN_SIZE = (900,600)
    def __init__(self, parent):
        wx.Frame.__init__(self, parent=parent,
         id=wx.ID_ANY, title="LOD Calculator", size=wx.Size(900,600),
         style=wx.CAPTION|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SYSTEM_MENU|wx.TAB_TRAVERSAL)
        self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)

        self.startpnl = wx.Panel(self)
        self.startvsizer = wx.BoxSizer(wx.VERTICAL)
        self.startbtnsizer = wx.BoxSizer(wx.HORIZONTAL)
        # make title and subtitle, format fonts
        self.st = wx.StaticText(self.startpnl, style=wx.ALIGN_CENTER, label="DCB LoD Calculator")
        self.font = self.st.GetFont()
        self.font.PointSize += 10
        self.font = self.font.Bold()
        self.st.SetFont(self.font)
        self.stsub = wx.StaticText(self.startpnl, style=wx.ALIGN_CENTER, label="Probit/Linear Regression Method")
        self.fontsub = self.stsub.GetFont()
        self.fontsub.PointSize += 2
        self.stsub.SetFont(self.fontsub)

        # make the "begin" button to start the script
        self.btn = wx.Button(self.startpnl, wx.ID_ANY, "Start Analysis",\
         size = (200,60))
        self.btn.Bind(wx.EVT_BUTTON, self._OnStart)
        self.startbtnsizer.AddStretchSpacer()
        self.startbtnsizer.Add(self.btn, 0, wx.CENTER)
        self.startbtnsizer.AddStretchSpacer()

        self.startvsizer.Add(self.st, wx.SizerFlags().Expand().Border(wx.ALL, 25))
        self.startvsizer.Add(self.stsub, wx.SizerFlags().Expand().Border(wx.ALL, 25))
        self.startvsizer.AddStretchSpacer()
        self.startvsizer.Add(self.startbtnsizer, wx.SizerFlags().Expand().Border(wx.ALL, 25))
        self.startvsizer.AddStretchSpacer()
        self.startpnl.SetSizerAndFit(self.startvsizer)

        # create a menu & status bar
        self.makeMenuBar()
        self.CreateStatusBar()
        self.SetStatusText("PLACEHOLDER -- STATUS BAR")
        self.Center(wx.BOTH)

    def makeMenuBar(self):
        fileMenu = wx.Menu()
        helloItem = fileMenu.Append(-1, "&Hello...\tCtrl-H",
                "Help string shown in status bar for this menu item")
        fileMenu.AppendSeparator()
        exitItem = fileMenu.Append(wx.ID_EXIT)
        helpMenu = wx.Menu()
        aboutItem = helpMenu.Append(wx.ID_ABOUT)
        menuBar = wx.MenuBar()
        menuBar.Append(fileMenu, "&File")
        menuBar.Append(helpMenu, "&Help")
        self.SetMenuBar(menuBar)
        self.Bind(wx.EVT_MENU, self.OnHello, helloItem)
        self.Bind(wx.EVT_MENU, self.OnExit,  exitItem)
        self.Bind(wx.EVT_MENU, self.OnAbout, aboutItem)

    def OnExit(self, event):
        """Close frame & terminate app"""
        self.Close(True)

    def OnHello(self, event):
        """oh hey what's up"""
        wx.MessageBox("Stuff would go here",\
         "The dialog with the stuff would have this title")

    def OnAbout(self, event):
        """for displaying 'about' dialog"""
        wx.MessageBox("Main dialog box text for About",\
         "About title text", wx.OK|wx.ICON_INFORMATION)

    def _OnStart(self,event):
        self.Hide()
        self.frm2 = Frame2(None)
        self.frm2.Show()

class Frame2(wx.Frame):
    """Data load frame"""
    FRAME_MIN_SIZE = (900,600)
    def __init__(self, parent):
        wx.Frame.__init__(self, parent=parent,
         id=wx.ID_ANY, title="LOD Calculator", size=wx.Size(900,600),
         style=wx.CAPTION|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SYSTEM_MENU|wx.TAB_TRAVERSAL)

        self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
        self.dirname = os.getcwd()

        filedlg = wx.FileDialog(self, 'Choose a file', os.getcwd(),\
         '', 'CSV files (*.csv)|*.csv|All files(*.*)|*.*', wx.FD_OPEN)

        if filedlg.ShowModal() == wx.ID_OK:
            self.dirname = filedlg.GetDirectory()
            self.filename = filedlg.GetFilename()
            self.path = os.path.join(self.dirname, self.filename)
            self.file = open(self.path, 'r')

            dialect = csv.Sniffer().sniff(self.file.read(1024),\
             delimiters = ";|,")
            self.file.seek(0)
            csvfile = csv.reader(self.file, dialect)
            filedata = []
            filedata.extend(csvfile)
            self.file.seek(0)
            datasample = self.file.read(2048)
            self.file.seek(0)

            if csv.Sniffer().has_header(datasample):
                self.cols = next(csvfile)
                self.datalist = []
                self.datalist.extend(filedata[1:len(filedata)])
            else:
                self.cols = []
                for idx in range(len(next(csvfile))):
                    self.cols.append(f'col_{i}')
                self.file.seek(0)
                self.datalist = filedata

        self.file.close()


        # Create Sizers
        Sizer1 = wx.BoxSizer(wx.HORIZONTAL)
        paraLsizer = wx.BoxSizer(wx.VERTICAL)
        paraRsizer = wx.BoxSizer(wx.VERTICAL)

        # Create Panels
        self.paraLpnl = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition,\
         wx.DefaultSize, wx.TAB_TRAVERSAL)
        self.paraRpnl = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition,\
         wx.DefaultSize, wx.TAB_TRAVERSAL)

### FOLLOWING LINE SPITS THE ERROR 
### HAVE TRIED USING BOTH self.datalist/self/cols AND JUST datalist/cols - NO EFFECT
        self.CreateGrid(self.datalist, self.cols)

        # Create Widgets
        self.reImportButton = wx.Button(self.paraLpnl,\
         wx.ID_ANY, u"Import New CSV File", wx.DefaultPosition,\
         wx.DefaultSize, 0)
        self.paraInput = wx.StaticText(self.paraLpnl,\
         label="Select input column:", style = wx.ALIGN_CENTER_HORIZONTAL)
        self.inputcb = wx.ComboBox(self.paraLpnl, wx.ID_ANY, value=cols[0],\
         choices=cols, style = wx.CB_READONLY)
        self.logbtn = wx.RadioButton(self.paraLpnl, wx.ID_ANY, label='Check button\
         if input is in log10(conc.)')
        self.logbtn.SetValue(False)
        numInputs = ['1','2','3','4','5','6','7','8','9']
        self.inputNumLabel = wx.StaticText(self.paraLpnl, \
         label="Select number of targets", style = wx.ALIGN_CENTER_HORIZONTAL)
        self.inputNum = wx.ComboBox(self.paraLpnl, wx.ID_ANY, value='', \
         choices = numInputs, style = wx.CB_READONLY)

        # Add stuff to sub-sizers, set them, and fit stuff
        paraLsizer.Add(self.reImportButton, 0, wx.ALL, 5)
        paraLsizer.Add(self.paraInput, 0, wx.ALL, 5)
        paraLsizer.Add(self.inputcb, 0, wx.ALL, 5)
        paraLsizer.Add(self.logbtn, 0, wx.ALL, 5)
        paraLsizer.Add(self.inputNumLabel, 0, wx.ALL, 5)
        paraLsizer.Add(self.inputNum, 0, wx.ALL, 5)
        self.paraLpnl.SetSizer(paraLsizer)
        self.paraLpnl.Layout()
        paraLsizer.Fit(self.paraLpnl)

        paraRsizer.Add(self.grid, 1, wx.EXPAND)
        self.paraRpnl.SetSizer(paraRsizer)
        self.paraRpnl.Layout()
        paraRsizer.Fit(self.paraRpnl)

        # Add panels (containing sub-sizers) to main sizer
        Sizer1.Add(self.paraLpnl, 0, wx.EXPAND |wx.BOTTOM, 5)
        Sizer1.Add(self.paraRpnl, 1, wx.EXPAND |wx.ALL, 5)

        # Set main sizer for the window; add status and menu bars
        self.SetSizer(Sizer1)
        StartFrame.makeMenuBar(self)
        StartFrame.CreateStatusBar(self)
        StartFrame.SetStatusText(self, "PLACEHOLDER -- STATUS BAR")

        # Finally, lay the whole window out and center it.
        self.Layout()
        self.Centre(wx.BOTH)

    #create the grid
    def createGrid(self, datalist, colnames):
        if getattr(self, 'grid', 0): self.grid.Destroy()
        self.grid = wx.grid.Grid(self.paraRpnl, 0)
        self.grid.CreateGrid(len(datalist), len(colnames)) #create grid, same size as file (rows, cols)

        #fill in headings
        for i in range(len(colnames)):
            self.grid.SetColLabelValue(i, colnames[i])

        #populate the grid
        for row in range(len(datalist)):
            for col in range(len(colnames)):
                try:
                    self.grid.SetCellValue(row,col,datalist[row][col])
                except:
                    pass

        self.grid.AutoSizeColumns(False) # size columns to data (from cvsomatic.py)
        self.twiddle()

    def twiddle(self):
        x,y = self.GetSize()
        self.SetSize((x, y+1))
        self.SetSize((x,y))

    def Exit(self, event):
        if getattr(self, 'file',0):
            self.file.close()
            self.Close(True)

import wx.lib.mixins.inspection
app = wx.App()
frm = StartFrame(None)
frm.Show()
wx.lib.inspection.InspectionTool().Show()
app.MainLoop()

The slightly-edited-but-working code I based my "Frame2" off of follows (not sure why he split it up into 3 classes, or what purpose the csv_view(wx.App) class serves):

import wx, os, sys, csv
import numpy as np
import wx.grid

class MyFrame3(wx.Frame):

    def __init__(self, parent):
        wx.Frame.__init__(self, parent, id = wx.ID_ANY,\
         title=wx.EmptyString, pos=wx.DefaultPosition, size=wx.Size(900,600),\
         style=wx.CAPTION|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SYSTEM_MENU|wx.TAB_TRAVERSAL)

        self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
        Sizer1 = wx.BoxSizer(wx.HORIZONTAL)

        self.Left_Panel = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition,\
         wx.DefaultSize, wx.TAB_TRAVERSAL)
        LeftSizer = wx.BoxSizer(wx.VERTICAL)

        self.ImportButton = wx.Button(self.Left_Panel,\
         wx.ID_ANY, u"Import CSV File", wx.DefaultPosition, wx.DefaultSize, 0)
        LeftSizer.Add(self.ImportButton, 0, wx.ALL, 5)

        self.Left_Panel.SetSizer(LeftSizer)
        self.Left_Panel.Layout()
        LeftSizer.Fit(self.Left_Panel)
        Sizer1.Add(self.Left_Panel, 0, wx.EXPAND |wx.ALL, 5)
        self.Right_Panel = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition,\
         wx.DefaultSize, wx.TAB_TRAVERSAL)
        RightSizer = wx.BoxSizer(wx.VERTICAL)

        self.Right_Panel.SetSizer(RightSizer)
        self.Right_Panel.Layout()
        RightSizer.Fit(self.Right_Panel)
        Sizer1.Add(self.Right_Panel, 1, wx.EXPAND |wx.ALL, 5)

        self.SetSizer(Sizer1)
        self.Layout()
        self.menubar = wx.MenuBar(0)
        self.fileMenu = wx.Menu()
        self.importMenu = wx.MenuItem(self.fileMenu,\
         wx.ID_ANY, u"Import", wx.EmptyString, wx.ITEM_NORMAL)
        self.fileMenu.Append(self.importMenu)

        self.menubar.Append(self.fileMenu, u"&File") 
        self.SetMenuBar(self.menubar)
        self.Centre(wx.BOTH)

        # Connect Events
        self.ImportButton.Bind(wx.EVT_BUTTON, self.ImportFunc)
        self.Bind(wx.EVT_MENU, self.ImportFunc, id = self.importMenu.GetId())

class csv_view(wx.App): 
        def OnInit(self): 
                self.frame=MyFrame3(None, -1, 'PyStereo', size=(900,600)) 
                self.SetTopWindow(self.frame) 
                return True

class MyFrame(MyFrame3):
    def __init__(self, parent, size = wx.Size(900,600)):
        MyFrame3.__init__(self, parent)

        self.dirname = os.getcwd()

    # Import/Open CSV
    def ImportFunc(self, event):
        
        dlg=wx.FileDialog(self, 'Choose a file', self.dirname, '','CSV files (*.csv)|*.csv|All files(*.*)|*.*',wx.FD_OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.dirname=dlg.GetDirectory()
            self.filename=dlg.GetFilename()
            self.file=open(os.path.join(self.dirname, self.filename), 'r')

            #check for file format with sniffer
            dialect = csv.Sniffer().sniff(self.file.read(1024), delimiters=";|,")
            self.file.seek(0)

            csvfile=csv.reader(self.file,dialect)
            filedata = [] #put contents of csvfile into a list
            filedata.extend(csvfile)
            self.file.seek(0)

            #grab a sample and see if there is a header
            sample=self.file.read(2048)
            self.file.seek(0)
            if csv.Sniffer().has_header(sample): #if there is a header
                colnames=next(csvfile) # label columns from first line
                datalist=[] # create a list without the header
                datalist.extend(filedata[1:len(filedata)]) #append data without header

            else:
                row1=next(csvfile) #if there is NO header
                colnames=[]
                for i in range(len(row1)):
                    colnames.append('col_%d' % i) # label columns as col_1, col_2, etc
                self.file.seek(0)
                datalist=filedata #append data to datalist

        self.file.close()
        self.createGrid(datalist, colnames)
        grid_sizer = wx.BoxSizer(wx.VERTICAL)
        grid_sizer.Add(self.grid, 1, wx.EXPAND)
        self.Right_Panel.SetSizer(grid_sizer)
        self.Right_Panel.Layout()

    #create the grid

    def createGrid(self, datalist, colnames):
        if getattr(self, 'grid', 0): self.grid.Destroy()
        self.grid=wx.grid.Grid(self.Right_Panel, 0)
        self.grid.CreateGrid(len(datalist), len(colnames)) #create grid, same size as file (rows, cols)

        #fill in headings
        for i in range(len(colnames)):
            self.grid.SetColLabelValue(i, colnames[i])

        #populate the grid
        for row in range(len(datalist)):
            for col in range(len(colnames)):
                try: 
                    self.grid.SetCellValue(row,col,datalist[row][col])
                except: 
                    pass

        self.grid.AutoSizeColumns(False) # size columns to data (from cvsomatic.py)
        self.twiddle()

    def twiddle(self): # from http://www.velocityreviews.com/forums/t330788-how-to-update-window-after-wxgrid-is-updated.html
        x,y = self.GetSize()
        self.SetSize((x, y+1))
        self.SetSize((x,y))

    def Exit(self, event):
        if getattr(self, 'file',0):
            self.file.close()
            self.Close(True)

import wx.lib.mixins.inspection
app = wx.App(0)
Frame_02 = MyFrame(None)
Frame_02.Show()
wx.lib.inspection.InspectionTool().Show()
app.MainLoop()

Screens of my app on startup (works until you try to select a .csv file in the modal dialog box spawned by the Start Analysis button), the app I'm using to guide my second frame on opening, and the same app after importing a csv. My start screen His start screen His screen after selecting a .csv with the left button

Also, any improvements/suggestions/corrections not directly involving my question are more than welcome. I'm new to this side of development, and need all the help I can get!


Solution

  • Sorry folks, my pre-coffee brain was calling CreateGrid() when the function is defined as createGrid(). There were a few other mistakes in the code (event handlers that referred to buttons that I no longer had implemented, calling StartFrame.makeMenuBar from a different class without also importing the functions StartFrame.makeMenuBar called, etc).

    But still feel free to point out inefficiencies and redundancies and other various un-pythonic things I'm doing. Still learning :)