简体   繁体   中英

How can I set the default container in a decorator class for tkinter.Frame?

I would like to create a contractible panel in a GUI, using the Python package tkinter .

My idea is to create a decorator for the tkinter.Frame class, adding a nested frame and a "vertical button" which toggles the nested frame.

Sketch: (Edit: The gray box should say Parent of contractible panel )

在此处输入图像描述

I got it to toggle just fine, using the nested frame's grid_remove to hide it and then move the button to the left column (otherwise occupied by the frame).

Now I want to be able to use it like any other tkinter.Frame , but let it target the nested frame. Almost acting like a proxy for the nested frame. For example, adding a tkinter.Label (the green Child component in the sketch) to the decorator should add the label to the nested frame component (light yellow tk.Frame in the sketch) not the decorator itself (strong yellow ContractiblePanel in the sketch).


Minimal example: (omitting the toggling stuff and any "formatting"):

( Here's a published (runnable) Repl project )

import tkinter

class ContractiblePanel(tkinter.Frame):

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self._panel  = tkinter.Frame(self)
        self._toggle = tkinter.Button(self, text='<', command=self._toggle_panel)

        self.grid(row=0, column=0, sticky='nsw')
        self._panel.grid(row=0, column=0, sticky='nsw')
        self._toggle.grid(row=0, column=1, sticky='nsw')

    def _toggle_panel(self):
        # ...

if __name__ == '__main__':
    root = tkinter.Tk()
    root.geometry('128x128')
    
    contractible_panel = ContractiblePanel(root)

Forwarding configuration calls is just overriding the config method I guess?

class ContractiblePanel(tkinter.Frame):
    # ...
    def config(self, **kwargs):
        self._panel.config(**kwargs)

# ...
contractible_panel.config(background='blue')

But I would like to be able to add a child component into the nested panel frame by

    label_in_panel = tkinter.Label(contractible_panel, text='yadayada')

How do I get the ContractiblePanel object to act like a proxy to its member _panel , when adding child components?

What other methods/use cases should I consider? I am quite new to tkinter and thus expect the current implementation to break some common practices when developing tkinter GUIs.

This is an interesting question. Unfortunately, tkinter really isn't designed to support what you want. I think it would be less complicated to simply expose the inner frame and add widgets to it.

That being said, I'll present one possible solution. It's not implemented as a python decorator, but rather a custom class.

The difficulty is that you want the instance of the custom class to represent the outer frame in one context (for example, when packing it in your UI) and the inner frame in another context (when adding child widgets to it)

The following solution solves this by making the instance be the inner frame, and then overriding pack , place , and grid so that they operates on the outer frame. This works fine, with an important exception: you cannot use this class directly inside a notebook or embedded in a text widget or canvas.

I've used colors and borders so it's easy to see the individual components, but you can remove the colors in production code, obviously. Also, I used a label instead of a button since I created the screenshot on OSX where the background color of a button can't be changed.

import tkinter as tk

class ContractiblePanel(tk.Frame):
    def __init__(self, parent, **kwargs):
        self._frame = tk.Frame(parent, **kwargs)
        super().__init__(self._frame, bd=2, relief="solid", bg="#EFE4B0")
        self._button = tk.Label(
            self._frame, text="<", bg="#00A2E8", bd=2,
            relief="solid", font=("Helvetica", 20), width=4
        )
        self._frame.grid_rowconfigure(0, weight=1)
        self._frame.grid_columnconfigure(0, weight=1)
        self._button.grid(row=0, column=1, sticky="ns", padx=4, pady=4)
        super().grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
        self._button.bind("<1>", lambda event: self.toggle())

    def collapse(self):
        super().grid_remove()
        self._button.configure(text=">")

    def expand(self):
        super().grid()
        self._button.configure(text="<")

    def toggle(self):
        self.collapse() if self.winfo_viewable() else self.expand()

    def pack(self, **kwargs):
        # override to call pack in the private frame
        self._frame.pack(**kwargs)

    def grid(self, **kwargs):
        # override to call grid in the private frame
        self._frame.grid(**kwargs)

    def place(self, **kwargs):
        # override to call place in the private frame
        self._frame.place(**kwargs)


root = tk.Tk()
root.geometry("400x300")
cp = ContractiblePanel(root, bg="yellow", bd=2, relief="raised")
cp.pack(side="left", fill="y", padx=10, pady=10)

label = tk.Label(cp, text="Child component", background="#22B14C", height=3, bd=2, relief="solid")
label.pack(side="top", expand=True, padx=20, pady=20)

root.mainloop()

截屏

First of all it is kinda gross to use this code and it's very confusing. So I'm really not sure if you really want to take this route. However, it is possible to achieve it.

The basic idea is to have a wrapper and to pretend the wrapper is the actual object you can lie with __str__ and __repr__ about what the class really is. That is not what a proxy means.

class WrapperClass:

    def __init__(self, master=None, **kwargs):
        self._wrapped_frame = tk.Frame(master, **kwargs)
        self._panel = tk.Frame(self._wrapped_frame)
        self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)

        self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
        self._panel.grid(row=0, column=0, sticky='nsw')
        self._toggle.grid(row=0, column=1, sticky='nsw')
        return None

    def _toggle_panel(self):
        print('toggle')

    def __str__(self):
        return self._panel._w
    __repr__ = __str__

You can do even more confusing things by delegate the lookup-chain to the _wrapped_frame inside the WrapperClass this enables you to call on the instance of WrapperFrame() methods like pack or every other method. It kinda works similar for inheritance with the difference that by referring to the object, you will point to different one.

I don't recommend using this code by the way.

import tkinter as tk

NONE = object()
#use an object here that there will no mistake

class WrapperClass:

    def __init__(self, master=None, **kwargs):
        self._wrapped_frame = tk.Frame(master, **kwargs)
        self._panel = tk.Frame(self._wrapped_frame)
        self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)

        self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
        self._panel.grid(row=0, column=0, sticky='nsw')
        self._toggle.grid(row=0, column=1, sticky='nsw')
        return None

    def _toggle_panel(self):
        print('toggle')

    def __str__(self):
        return self._panel._w
    __repr__ = __str__

    def __getattr__(self, name):
        #when wrapper class has no attr name
        #delegate the lookup chain to self.frame
        inreturn = getattr(self._wrapped_frame, name, NONE)
        if inreturn is NONE:
            super().__getattribute__(name)
        return inreturn

root = tk.Tk()
wrapped_frame = WrapperClass(root, bg='red', width=200, height=200)
root.mainloop()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM