wxPython - 无法在 __init__ 中调用 class 方法

wxPython - can't call class method within __init__

我知道这可能是一个基本问题,但我刚刚开始构建 GUI,因此不需要涉足 classes。我有一些代码(见下文)来处理程序的 GUI 部分(CLI 版本已经 运行 完美,但只要他们看到 CLI,管理就会跑山......),但我不能完全可以解决。仅供参考 运行 python 3.9.1 和 wxPython 4.1.1 Phoenix(尽管 wx.version 也吐出 wxWidgets 3.1.5,如果相关的话)在 Windows 10

很多代码都是基于例子/其他人在这里问的东西,不幸的是我找不到原始的 posts 来感谢他们,但这真的只是为了学习,一旦我掌握我在做什么 我将从头开始编写所有内容。所以对于原作者,我深表歉意,但我非常感谢你们!

该程序将显示一个“开始”屏幕(框架),只有一个标题和一个按钮。单击该按钮时,将打开一个模态文件对话框,用户 select 在其中使用要使用的 .csv 数据文件,开始框架消失,并出现一个新框架,并在右窗格中显示数据(在一个 grid sizer)和左窗格中的一些下拉菜单和单选按钮。最后一个 drop-down (self.inputNum) 将绑定到一个事件处理程序,该事件处理程序将为用户添加一些额外的 drop-down 以 select 要分析的不同目标(课程数量由 self.inputNum 组合框中的 selection 决定)。但是,当 运行 脚本时,我得到一个错误

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'

我在这里搜索了一下,发现有人有类似的问题(在同一个 class 的 init 中调用了一个 class 方法),但是'correct' 答案是使用 self.classmethodname(args) 调用该方法(OP 一直将其称为 classmethodname(args),我已经在这样做了。

提前为草率的格式道歉。我尝试尽可能多地清理它,但由于我不知道错误来自何处,我不想偏离我用来构建当前混乱的(工作)示例太远。请参阅下面的我的脚本、我的“Frame2”所基于的 slightly-modified 脚本和屏幕截图。

我的代码(我知道在一行中导入模块是不受欢迎的,但是有这么多我为此压缩了它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()

slightly-edited-but-working 我的“Frame2”代码基于以下内容(不确定他为什么将其分成 3 个 class,或者 csv_view(wx.App) [=53 的目的是什么=] 服务):

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()

我的应用程序启动时的屏幕(在您尝试 select 由“开始分析”按钮生成的模态对话框中的 .csv 文件之前一直有效),我用来引导第二帧的应用程序打开时,以及导入 csv 后的相同应用程序。

此外,任何 improvements/suggestions/corrections 不直接涉及我的问题的人都非常欢迎。我是这方面开发的新手,需要我能得到的所有帮助!

对不起各位,当函数定义为 createGrid() 时,我的咖啡前大脑正在调用 CreateGrid()。代码中还有一些其他错误(引用我不再实现的按钮的事件处理程序,从不同的 class 调用 StartFrame.makeMenuBar 而没有导入调用的函数 StartFrame.makeMenuBar,等等).

但仍然可以随时指出我正在做的低效和冗余以及其他各种非 pythonic 的事情。还在学习:)