Фабрика вложенных классов с tkinter - PullRequest
8 голосов
/ 27 мая 2020

Я пытаюсь создать сценарий для импорта в мои будущие проекты. Этот скрипт должен создать несколько tk.Frames в tk.Frame и позволить мне редактировать созданные в main .

Я думаю, лучший способ добиться этого - создать Holder_frame class и поместите в него несколько вложенных классов. Чтобы я мог вызывать их в моем main с помощью Holder_frame.F1. Я перепробовал много кода и в итоге создал себе учетную запись. В любом случае, вот где я:

import tkinter as tk
from tkinter import Frame,Button

class BaseClass(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.master = master
        self.pack()


class Holder_frame(tk.Frame):
    Names = []
    def __init__(self, master, frames=2):
        tk.Frame.__init__(self, master)
        self.master = master
        frame_names = Holder_frame.Names
        for i in range(0,frames):
            frame_names.append("F"+str(i+1))
        print(frame_names)
        Holder_frame.factory()
    def factory():
        print(Holder_frame.Names)
        print(type(BaseClass))
        for idex,i in enumerate (Holder_frame.Names):
            print(i)
            class NestedClass(BaseClass):
                pass

            NestedClass.__name__ = i
            NestedClass.__qualname__ = i

if __name__ == "__main__":
    root = tk.Tk()
    def raise1():
        Holder_frame.F1.tkraise()
    def raise2():
        Holder_frame.F2.tkraise()

    holder=Holder_frame(root,frames=2)
    holder.grid(row=1,column=0)
    b1 = tk.Button(root, text='1', command=raise1)
    b1.grid(row=0,column=0)
    b2 = tk.Button(root, text='2', command=raise2)
    b2.grid(row=0,column=1)


    root.mainloop()

Все работает нормально, пока я не попытаюсь вызвать Frame. (Объект AttributeError 'Holder_frame' не имеет атрибута 'F1') Я думаю, моя проблема связана со структурой, но мне нужна помощь для ее решения.

Есть предложения?

Ответы [ 2 ]

3 голосов
/ 31 мая 2020

Если я правильно понимаю, я думаю, вы имеете в виду иметь какой-то базовый класс , который имеет некоторую конфигурацию, которая является общей для набора кадров, например, вы хотите иметь 10 кадров 300x400 геометрия и общий фон коричневый , а затем еще один набор кадров с другой конфигурацией , доступ к которым может быть организован. Тогда я бы сказал, что у вас есть интересный способ, но я бы предпочел использовать список или словарь.

Вот несколько подходов для достижения этой цели.

Подход 1

В этом подходе я создал функцию, которая возвращает словарь со всеми фреймами, созданными и содержащимися в нем, как в формате ({..., 'F20': tkinter.frame, ...})

import tkinter as tk

def get_base_frames(num, master, cnf={}, **kw):
    """
    Create list of frames with common configuration options.

    Args:
        num (int): Number of frames to be created.
        master (tk.Misc): Takes tkinter widget or window as a parent for the frames.
        cnf (dict): configuration options for all the frames.
        kw: configuration options for all the frames.

    Return:
        Dictionary of frames ({..., 'F20': tkinter.frame, ...}).
    """
    return {f'F{n+1}': tk.Frame(master, cnf=cnf, **kw) for n in range(num)}

if __name__ == "__main__":
    root = tk.Tk()
    frame_holder = get_base_frames(10, root, width=50, height=50, bg='brown')

    # Frames can be accessed through their names like so.
    print(frame_holder.get('F1'))

Подход 2

Здесь я использовал класс и объекты. Где я сделал этот класс Frames, хотя вы можете называть его как хотите. Я также добавил некоторые важные методы, такие как cget() и configure(), с помощью этих методов, когда можно получить значение параметра и настроить параметры для всех кадров соответственно. Есть более полезные методы, такие как bind() и bind_all(), если они вам нужны, просто измените этот класс в соответствии с вашими потребностями.

import tkinter as tk

class Frames(object):
    def __init__(self, master=None, cnf={}, **kw):
        super().__init__()
        num = cnf.pop('num', kw.pop('num', 0))
        for n in range(num):
            self.__setattr__(f'F{n+1}', tk.Frame(master, cnf=cnf, **kw))

    def configure(self, cnf={}, **kw):
        """Configure resources of a widget.

        The values for resources are specified as keyword
        arguments. To get an overview about
        the allowed keyword arguments call the method keys.
        """
        for frame in self.__dict__:
            frame = self.__getattribute__(frame)
            if isinstance(frame, tk.Frame):
                if not cnf and not kw:
                    return frame.configure()
                frame.configure(cnf=cnf, **kw)
    config = configure

    def cget(self, key):
        """Return the resource value for a KEY given as string."""
        for frame in self.__dict__:
            frame = self.__getattribute__(frame)
            if isinstance(frame, tk.Frame):
                return frame.cget(key)
    __getitem__ = cget


if __name__ == "__main__":
    root = tk.Tk()
    frame_holder = Frames(root, num=10, width=10, 
                          bd=2, relief='sunken', bg='yellow')

    # Frames can be accessed through their naems like so.
    print(frame_holder.F4) 
    print(frame_holder['bg'])
    frame_holder.config(bg='blue')
    print(frame_holder['bg'])

Подход 3

Если вы хотите иметь по-разному настроенных фреймов, содержащихся в одном классе, , где все эти фреймы имеют какой-то общий метод или какой-то общий атрибут.

import tkinter as tk

class BaseFrame(tk.Frame):
    def __init__(self, master=None, cnf={}, **kw):
        super().__init__(master=master, cnf={}, **kw)

    def common_function(self):
        """This function will be common in every 
        frame created through this class."""
        # Do something...

class FrameHolder(object):
    def __init__(self, master=None, cnf={}, **kw):
        kw = tk._cnfmerge((cnf, kw))
        num = kw.pop('num', len(kw))

        for n in range(num):
            name = f'F{n+1}'
            cnf = kw.get(name)
            self.__setattr__(name, BaseFrame(master, cnf))

if __name__ == "__main__":
    root = tk.Tk()

    holder = FrameHolder(root, 
                    F1=dict(width=30, height=40, bg='black'),
                    F2=dict(width=50, height=10, bg='green'),
                    F3=dict(width=300, height=350, bg='blue'),
                    F4=dict(width=100, height=100, bg='yellow'),
                    )
    print(holder.F1)
    print(holder.__dict__)

Подход 4

Это подход, которого пытается достичь OP.

import tkinter as tk


class BaseClass(tk.Frame):
    def __init__(self, master, cnf={}, **kw):
        kw = tk._cnfmerge((cnf, kw))
        cnf = [(i, kw.pop(i, None))
               for i in ('pack', 'grid', 'place') if i in kw]
        tk.Frame.__init__(self, master, **kw)
        self.master = master

        if cnf:
            self.__getattribute__(cnf[-1][0])(cnf=cnf[-1][1])


class Container(tk.Frame):
    """Container class which can contain tkinter widgets. 
    Geometry (pack, grid, place) configuration of widgets 
    can also be passed as an argument.

    For Example:-

    >>> Container(root, widget=tk.Button,
              B5=dict(width=30, height=40, bg='black',
                      fg='white', pack=(), text='Button1'),
              B6=dict(width=50, height=10, bg='green', text='Button2',
                      place=dict(relx=0.5, rely=1, anchor='s')))
    """
    BaseClass = BaseClass

    def __init__(self, master=None, cnf={}, **kw):
        kw = tk._cnfmerge((cnf, kw))
        wid = kw.pop('widget', tk.Frame)
        for name, cnf in kw.items():
            geo = [(i, cnf.pop(i, None))
                   for i in ('pack', 'grid', 'place') if i in cnf]
            setattr(Container, name, wid(master, cnf))
            if geo:
                manager, cnf2 = geo[-1]
                widget = getattr(Container, name)
                getattr(widget, manager)(cnf=cnf2)


if __name__ == "__main__":
    root = tk.Tk()

    Container(root, widget=Container.BaseClass,
              F1=dict(width=30, height=40, bg='black', relief='sunken',
                      pack=dict(ipadx=10, ipady=10, fill='both'), bd=5),
              F2=dict(width=50, height=10, bg='green',
                      pack=dict(ipadx=10, ipady=10, fill='both')),
              )

    Container(root, widget=tk.Button,
              B5=dict(width=30, height=40, bg='black',
                      fg='white', pack={}, text='Button1'),
              B6=dict(width=50, height=10, bg='green', text='Button2',
                      place=dict(relx=0.5, rely=1, anchor='s')),
              )

    print(Container.__dict__)
    root.mainloop()

Многое может быть сделано и может быть изменено в соответствии с вашими потребностями, эти это всего лишь некоторые подходы, которые, как мне кажется, будут очень хорошо работать для автоматизации и сохранения набора фреймов в форме и вместе.

Может быть несколько способов сделать это или, может быть, что-то лучше и эффективнее, чем эти, не стесняйтесь давать предложения и делиться чем-то новым.

2 голосов
/ 30 мая 2020

Одно решение этой проблемы, я думаю, поскольку я не полностью понимаю ваш вопрос, но вот мое решение:

import tkinter as tk
from tkinter import Frame,Button

class BaseClass(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.master = master
        self.pack()


class Holder_frame(tk.Frame):
    def __init__(self, master, frames=2):
        tk.Frame.__init__(self, master)
        self.master = master
        self.frame_names = []
        for i in range(frames):
            Holder_frame.create_frames("F"+str(i+1), self)

    @classmethod
    def create_frames(cls, name, master):
        setattr(cls, name, tk.Frame(master))

if __name__ == "__main__":
    root = tk.Tk()
    def raise1():
        print(type(Holder_frame.F1))
    def raise2():
        print(type(Holder_frame.F2))

    holder=Holder_frame(root,frames=2)
    holder.grid(row=1,column=0)
    b1 = tk.Button(root, text='1', command=raise1)
    b1.grid(row=0,column=0)
    b2 = tk.Button(root, text='2', command=raise2)
    b2.grid(row=0,column=1)
    print(Holder_frame.__dict__.items())

    root.mainloop()

Использование setattr позволяет добавлять переменные в класс, как если бы вы вводили функцию в код. Это позволяет вам получать доступ к кадрам извне класса как к «глобальной переменной»

Я использовал файл, чтобы проверить, работает ли он снаружи как импортированный модуль:

# main.py
from nested_class import Holder_frame
import tkinter as tk

root = tk.Tk()
holder=Holder_frame(root,frames=1000)
holder.grid(row=1,column=0)
print(Holder_frame.__dict__.items())

root.mainloop()

I Надеюсь, это ответит на ваш вопрос,

Джеймс

РЕДАКТИРОВАТЬ:

Подумав, я думаю, есть более чистая система для того, что вы хотите. С кодом из этого сообщения можно увидеть, что ваша моя письменная система может быть заменена на ttk.Notebook, и, удалив верхнюю панель с помощью style.layout('TNotebook.Tab', []), можно увидеть, что вы получите фрейм виджет, внутри которого могут быть виджеты фреймов:

import tkinter as tk
import tkinter.ttk as ttk

class multiframe_example:
    def __init__(self, master):
        self.master = master

        style = ttk.Style()
        style.layout('TNotebook.Tab', [])   
        notebook = ttk.Notebook(self.master)
        notebook.grid(row=0, column=0)

        self.master.grid_rowconfigure(0, weight=1)
        self.master.grid_columnconfigure(0, weight=1)

        tab1 = tk.Frame(self.master,  width=500, height=500, background="green")
        tab2 = tk.Frame(self.master,  width=500, height=500)
        tab3 = tk.Frame(self.master,  width=500, height=500)


        notebook.add(tab1)
        notebook.add(tab2)
        notebook.add(tab3)

        notebook.select(0) # select tab 1
        notebook.select(1) # select tab 2
        notebook.select(2) # select tab 3

def main():
    root = tk.Tk()
    root.geometry("500x500")
    multiframe_example(root)
    root.mainloop()

if __name__ == '__main__':
    main()

Надеюсь, этот код поможет вам и сделает все, что вам нужно!

...