Tkinter 中的流体设计

Fluid design in Tkinter

我正在尝试使用 ttk (tkinter) 创建一个有点 "responsive" 的设计。小部件的基本放置完全没有问题,但是让它随着程序的宽度流动是我无法实现的。在 CSS 中,我知道可以按照“"float: left" for all container”这样的方式来表达,页面会适应屏幕大小。我还没有在 Tkinter 和框架中找到类似的东西。

我的基本测试程序:

#!/usr/bin/python3

import tkinter
from tkinter import ttk
from ttkthemes import ThemedTk, THEMES

class quick_ui(ThemedTk):
    def __init__(self):
        ThemedTk.__init__(self, themebg=True)
        self.geometry('{}x{}'.format(900, 150))
        self.buttons = {}

        self.frame1 = ttk.Frame(self)
        self.frame1.pack(side="left")
        self.frame2 = ttk.Frame(self)
        self.frame2.pack(side="left")

        #------------------------------------------------------- BUTTONS
        i = 0
        while (i < 5):
            i += 1
            self.buttons[i]= ttk.Button(self.frame1,
                                            text='List 1 All ' + str(i),
                                            command=self.dump)
            self.buttons[i].pack(side="left")


        while (i < 10):
            i += 1
            self.buttons[i]= ttk.Button(self.frame2,
                                            text='List 2 All ' + str(i),
                                            command=self.dump)
            self.buttons[i].pack(side="left")

    def dump(self):
        print("dump called")

quick = quick_ui()
quick.mainloop()

这将创建一个 window,其中包含 10 个彼此相邻的按钮。 当我将 window 缩小到按钮不再适合屏幕时,我希望按钮显示在彼此下方

所以我所做的是添加一个调整大小的侦听器并设置以下方法:

    def resize(self, event):
        w=self.winfo_width()
        h=self.winfo_height()
        # print("width: " + str(w) + ", height: " + str(h))

        if(w < 830):
            self.frame1.config(side="top")
            self.frame2.config(side="top")

但是Frame没有属性side,这是给方法pack的参数。所以那也没用。

现在我迷路了。我在这方面花了很长时间,尝试了网格和其他解决方案,但我觉得我错过了一个简单但非常重要的设置。

我创建了一个(hackish)解决方案,这很好,因为它是一个非常内部的程序。由于在此期间没有给出答案,我将在这里提供我的解决方案。它可能有很大的改进空间,但我希望这可以给将来的人一些关于如何解决他(或她)自己的问题的指示。

#!/usr/bin/python3

import re
import sys
import tkinter
from tkinter import filedialog
from tkinter import ttk
from ttkthemes import ThemedTk, THEMES

import subprocess
import os
from tkinter.constants import UNITS
import json
from functools import partial


class quick_ui(ThemedTk):

    def __init__(self):
        ThemedTk.__init__(self, themebg=True)
        self.minsize(600, 250)
        self.elems = {}
        self.resize_after_id = None


        #------------------------------------------------------- Window menu bar contents
        self.menubar = tkinter.Menu(self)
        self.menubar.add_command(label="Open", command = self.dump)
        self.menubar.add_command(label="Refresh", command = self.dump)
        self.config(menu=self.menubar)

        # Theme menu
        self.themeMenu = tkinter.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="Theme", menu=self.themeMenu)
        self.themeMenu.add_command(label="DEFAULT", command=partial(self.dump, "default"))


        #---------------------------------------------------------------------- top_frame
        self.top_frame = ttk.Frame(self)
        self.top_frame.pack( side = tkinter.TOP, expand='YES', fill='both', padx=10)

        self.top_top_frame = ttk.Frame(self.top_frame)
        self.top_top_frame.pack(side=tkinter.TOP, expand='YES', fill='both')

        self.top_bottom_frame = ttk.Frame(self.top_frame)
        self.top_bottom_frame.pack(side=tkinter.BOTTOM)

        self.top_bottom_top_frame = ttk.Frame(self.top_frame)
        self.top_bottom_top_frame.pack(side=tkinter.TOP)


        self.top_bottom_bottom_frame = ttk.Frame(self.top_frame)
        self.top_bottom_bottom_frame.pack(side=tkinter.BOTTOM)

        #------------------------------------------------------------------- bottom_frame
        self.bottom_frame = ttk.Frame(self, relief="sunken")
        self.bottom_frame.pack( side = tkinter.BOTTOM, 
                                expand='YES', 
                                fill='both', 
                                padx=10, 
                                pady=10 )

        #------------------------------------------------------- BUTTONS
        i = 0
        while (i < 15):
            self.elems[i]=ttk.Button(self.top_bottom_top_frame,
                                                text='List All ' + str(i),
                                                command=self.dump)
            i += 1


        self.label_test_strings1 = ttk.Label(self.top_top_frame, text='Test strings1')
        self.label_test_strings2 = ttk.Label(self.top_bottom_frame, text='Test strings2')
        self.label_test_strings4 = ttk.Label(self.top_bottom_bottom_frame, text='Test strings4')

        self.label_test_strings1.pack(side = tkinter.TOP)
        self.label_test_strings2.pack(side = tkinter.TOP)
        self.label_test_strings4.pack(side = tkinter.TOP)


        self.placeElems()
        # Setup a hook triggered when the configuration (size of window) changes
        self.bind('<Configure>', self.resize)


    def placeElems(self):
        for index in self.elems:
            self.elems[index].grid(row=0, column=index, padx=5, pady=5)


    # ------------------------------------------------------ Resize event handler
    def resize(self, event):
        # Set a low "time-out" for resizing, to limit the change of "fighting" for growing and shrinking
        if self.resize_after_id is not None:
            self.after_cancel(self.resize_after_id)
        self.resize_after_id = self.after(200, self.resize_callback)


    # ------------------------------------------------------ Callback for the resize event handler
    def resize_callback(self):
        # The max right position of the program
        windowMaxRight = self.winfo_rootx() + self.winfo_width()

        # Some basic declarations
        found = False
        willAdd = False
        maxColumn = 0
        currIndex = 0
        currColumn = 0
        currRow = 0
        counter = 0
        last_rootx = 0
        last_maxRight = 0

        # Program is still starting up, so ignore this one
        if(windowMaxRight < 10):
            return

        # Loop through all the middle bar elements
        for child in self.top_bottom_frame.children.values():
            # Calculate the max right position of this element
            elemMaxRight = child.winfo_rootx() + child.winfo_width() + 10

            # If we already found the first 'changable' child, we need to remove the following child's also
            if(found == True):
                # Is the window growing?
                if(willAdd == True):
                    # Check to see if we have room for one more object
                    calcMaxRight = last_maxRight + child.winfo_width() + 20
                    if(calcMaxRight < windowMaxRight):
                        maxColumn = counter + 1
                # Remove this child from the view, to add it again later
                child.grid_forget()

            # If this child doesn't fit on the screen anymore
            elif(elemMaxRight >= windowMaxRight):
                # Remove this child from the view, to add it again later
                child.grid_forget()
                currIndex = counter
                maxColumn = counter
                currRow = 1
                found = True

            else:
                # If this child's x position is lower than the last child
                # we can asume it's on the next row
                if(child.winfo_rootx() < last_rootx):
                    # Check to see if we have room for one more object on the first row
                    calcMaxRight = last_maxRight + child.winfo_width() + 20
                    if(calcMaxRight < windowMaxRight):
                        child.grid_forget()
                        currIndex = counter
                        currColumn = counter
                        maxColumn = counter + 1
                        found = True
                        willAdd = True

            # Save some calculation data for the next run
            last_rootx = child.winfo_rootx()
            last_maxRight = elemMaxRight
            counter += 1

        # If we removed some elements from the UI
        if(found == True):
            counter = 0
            # Loop through all the middle bar elements (including removed ones)
            for child in self.top_bottom_frame.children.values():
                # Ignore the elements still in place
                if(counter < currIndex):
                    counter += 1
                    continue

                # If we hit our maxColumn count, move to the next row
                if(currColumn == maxColumn):
                    currColumn = 0
                    currRow += 1

                # Place this element on the UI again
                child.grid(row=currRow, column=currColumn, padx=5, pady=5)
                currColumn += 1
                counter += 1


    def dump(self):
        print("dump called")


quick = quick_ui()
quick.mainloop()