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()
我正在尝试使用 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()