动态初始化Tkinter中文件夹中的帧



我正试图将文件夹中的所有帧作为包导入,然后初始化所有这些帧,这样我就可以在按下按钮时raise它们。

这是我的文件夹结构:

+-projectfolder
|--bargraphtutor.py
|-- __init__.py (empty)
|--pages
|--startpage.py
|-- .
|-- .
|--aboutpage.py
|--__init__.py

pages文件夹中的__init__.py具有以下代码,用于将该文件夹中的所有.py文件打包到pages中。这句话取自这个问题。

from os.path import dirname, basename, isfile, join
import glob
pages = glob.glob(join(dirname(__file__), "*.py"))
__all__ = [ basename(f)[:-3] for f in pages if isfile(f) and not f.endswith('__init__.py')]
from . import *

然后我开始做import pages as p。问题是,每个帧都是文件中的一个类,要初始化每个帧,我必须知道每个文件的名称和类名:

示例:bargraphtutor.py 的一部分

self.frames = {}
for F in (p.aboutpage.AboutPage, p.startpage.StartPage): # This is where I'd like to make as 
page_name = F.__name__                               # automated as possible. 
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
# put all of the pages in the same location;
# the one on the top of the stacking order
# will be the one that is visible.
frame.grid(row=0, column=0, sticky="nsew")

示例:startpage.py:

import tkinter as tk   # python 3
from tkinter import ttk
from tkinter.ttk import Label, Button
from tkinter import font

class StartPage(tk.Frame):   # Each Frame is defined via a class
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
titleLabel= ttk.Label(self, text='This is the Startpage')
titleLabel.pack()

那么,我如何在不知道所有类名的情况下导入包,然后迭代bargraphtutor.py中的所有帧呢?我甚至使用了p.__all__,它返回包中所有文件的名称,但我不知道如何从那里继续前进。

编辑:如果我将文件命名为与类相同的名称,我会遇到名称空间的问题吗?

概述

我解决这个问题的方法是让我的所有页面都从基类继承,这样我就可以使用__subclass函数在导入所有子类后获得它们的列表。要导入它们,我会使用python的glob模块来查找文件,并使用importlib模块按文件路径导入文件。

文件结构示例

例如,让我们从这个简单的文件夹结构开始:

.
├── main.py
└── pages
├── PageOne.py
├── PageTwo.py
├── __init__.py
└── basepage.py

__init__.py为空,但让我们将pages视为一个模块。

basepage.py定义类BasePage,所有其他页面都从该类继承。它不需要太多,因为每个页面都将负责页面内的所有内容。它可能看起来像这样:

import tkinter as tk
class BasePage(tk.Frame):
def __init__(self, master, controller):
self.master = master
self.controller = controller
super().__init__(master)

PageOne.pyPageTwo.py包含页面。它们具有相似的结构。例如,PageOne.py可能看起来像这样:

import tkinter as tk
from .basepage import BasePage
class PageOne(BasePage):
def __init__(self, parent, controller):
super().__init__(parent, controller)
label = tk.Label(self, text="This is page one")
label.pack(padx=20, pady=20)

第二页是一样的,只是明显的地方写的是"二"而不是"一"。请注意此页面是如何从BasePage继承的。这很重要,您稍后会看到。

获取要导入的文件列表

您可以使用glob模块获取"pages"子目录中所有文件的列表。它看起来像这样:

import glob
for filename in glob.glob("./pages/*.py"):
...

如果实际文件夹中的文件是页面,而文件不是页面,则可以使用命名约定,以便仅导入页面文件。例如,可以将模式更改为".pages/Page*.py"

按文件名导入文件

Python的importlib模块具有允许我们按文件名导入文件的功能。

例如,在filename中给定一个文件名,我们可以像这样导入该文件:

import importlib.util
from pathlib import Path
path = Path(filename)
module_name = f"pages.{path.stem}"
spec = importlib.util.spec_from_file_location(module_name, path)
spec.loader.exec_module(module)

如果我们传入类似./pages/PageOne.py的东西,它将以名称pages.PageOne加载模块。

获取页面类

一旦我们导入了一个或多个页面,这里就是我们使用BasePage类的地方。还记得每个页面是如何从这个类继承的吗?我们可以使用子类的__subclasses__方法来给我们一个所有子类的列表。

把所有这些放在一起,这里有一个函数,它导入"pages"子文件夹中所有匹配"Page*.py"的文件,然后返回类:

def get_pages(self):
for filename in glob.glob("./pages/*.py"):
path = Path(filename)
module_name = f"pages.{path.stem}"
print(f"module name: {module_name}")
spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return BasePage.__subclasses__()

创建页面实例

在这一点上,创建页面实例非常容易。你的主程序可以做这样的事情:

self.frames = {}
for page_class in self.get_pages():
page = page_class(parent=page_container, controller=self)
page_name = page_class.__name__
self.frames[page_name] = page
page.grid(row=0, column=0, sticky="nsew")

问题:动态初始化Tkinter 中文件夹中的帧

核心点

pages.__init__中获取module,然后获取页面class

cls = getattr(getattr(pages, module_name), page_name)

参考

  • getattr(object, name[, default])
    返回对象的命名属性的值。名称必须是字符串

注意:我不传递controller引用,因为它与class App相同。


import tkinter as tk
import pages

class App(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("200x200")
menubar = tk.Menu(self, tearoff=0)
self.configure(menu=menubar)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.pages = {}
for filename in pages.__all__:
module_name, page_name = filename, filename
cls = getattr(getattr(pages, module_name), page_name)
self.pages[page_name] = frame = cls(self)
frame.grid(row=0, column=0, sticky="nsew")
menubar.add_command(label=page_name, command=frame.tkraise)
# The following statements are equivalent 
self.pages['StartPage'].tkraise()
# From within a page object
# self.master.pages['StartPage'].tkraise()

if __name__ == "__main__":
App().mainloop()

用Python测试:3.5-"TclVersion":8.6"TkVersion":8.6

最新更新