问题
Question: I would like to create a listbox that would appear and disappear below label1 but it should not affect the position of label2, when label1 is clicked. How can this be done?
Background: I have written a test code (see below) to show my problem. On 1st mouse click, listbox appears but pushes label2 downwards. On 2nd mouse click, listbox hides but label2 remains in new location and does not return to original position. I have tried using .grid_forget() but got the same behaviour. Repeated mouse clicks simply displays and hides the listbox.
I think i cannot get the desired widget behaviour is because the listbox is grid on the same plane/window as the other widgets. Is so, I think the top-level window is the other widget that allows an out-of-plane widget to exist? If so, how do I do that? How to position it below label1? Also, a top-level windows has a window title bar. I don't want that. How to remove that? Is my thought process correct to getting what I want?
#!/usr/bin/python3
# -*- coding: utf-8 -*-
try:
import tkinter as tk # Python 3 tkinter modules
except ImportError:
import Tkinter as tk # Python 2 tkinter modules
class App(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.grid(row=0, column=0, sticky='nsew')
self.toggle = True
self.label1 = tk.Label(self, text = 'Click me to see Names of Famous Folks')
self.lbframe = tk.Frame(self)
self.label2 = tk.Label(self, text = 'The World of Famous Folks! Welcome!')
self.label1.grid(row=0, column=0, sticky='nsew', pady=10)
self.lbframe.grid(row=1, column=0, sticky='nsew')
self.label2.grid(row=2, column=0, sticky='nsew')
nlist1 = ['Peter', 'Scotty', 'Walter', 'Scott', 'Mary']
self.lbframe.list_values = tk.StringVar()
self.lbframe.list_values.set(nlist1)
self.lbframe.list= tk.Listbox(self.lbframe, height=5, width= 10,
listvariable=self.lbframe.list_values)
self.lbframe.list.grid(row=0, column=0, sticky='nsew')
self.lbframe.list.grid_remove()
self.label1.bind("<Button-1>", self.ShowHideListbox)
def ShowHideListbox(self, event):
if self.toggle: # Show
self.toggle=False
self.lbframe.list.grid()
self.lbframe.list.lift(aboveThis=None)
else: # Hide
self.toggle=True
self.lbframe.list.grid_remove()
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
app.mainloop()
回答1:
If you want to position an widget such that it doesn't affect any other widgets, you should use place
. One of the nice things about place
is that it both lets you place widgets relative to other widgets, and set the width and height relative to other widgets.
For example, to place the listbox window (including its containing frame) immediately below the widget that triggered it to appear, you could use place
like this:
self.lbframe.place(in_=event.widget, x=0, rely=1, relwidth=1.0, anchor="nw")
The relwidth
option says that we want the frame containing the listbox to be exactly as wide as the entry widget. This is optional, but it illustrates the type of control you have with place
. The in_
parameter specifies which widget the width is relative to.
Because place
doesn't affect other children of the same parent, you must make sure that the parent window is large enough to show the list. Since list is an embedded widget, it won't float above the window. In your case that won't happen because your App
instance doesn't fill the whole window. The reason it doesn't is because you neglect to give any rows or columns in the root window any weight.
grid
is great at what it does, but sometimes it is the wrong tool for the job. For example, since App
is the only widget inside the root window, using pack
means you can get it to fill the window with a single command. Using grid
requires at lest three (two of which you forgot to call). For that reason, I recommend using pack
to put App
inside root
. That's not necessary for getting place
to work, it just makes your code easier to understand.
I also recommend not having App
be responsible for placing itself in its parent. You can, and again, it won't affect how place
works, but it will make your code easier to understand because you don't have one function (App.__init__
) responsible for laying out both the main window and its children.
With all that in mind, the following is a working solution based off of your code. Note: there is also a bug in how you are assigning values to the listbox, but fixing that is beyond the scope of this question.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
try:
import tkinter as tk # Python 3 tkinter modules
except ImportError:
import Tkinter as tk # Python 2 tkinter modules
class App(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.toggle = True
self.label1 = tk.Label(self, text = 'Click me to see Names of Famous Folks')
self.label2 = tk.Label(self, text = 'The World of Famous Folks! Welcome!')
self.label1.grid(row=0, column=0, sticky='nsew', pady=10)
self.label2.grid(row=2, column=0, sticky='nsew')
nlist1 = ['Peter', 'Scotty', 'Walter', 'Scott', 'Mary']
self.lbframe = tk.Frame(self)
self.lbframe.list_values = tk.StringVar()
self.lbframe.list_values.set(nlist1)
self.lbframe.list= tk.Listbox(self.lbframe, height=5, width= 10,
listvariable=self.lbframe.list_values)
self.lbframe.list.pack(fill="both", expand=True)
self.label1.bind("<Button-1>", self.ShowHideListbox)
def ShowHideListbox(self, event):
if self.toggle: # Show
self.toggle=False
self.lbframe.place(in_=event.widget, x=0, rely=1, relwidth=1.0, anchor="nw")
else: # Hide
self.toggle=True
self.lbframe.place_forget()
if __name__ == '__main__':
root = tk.Tk()
root.geometry("400x200")
app = App(root)
app.pack(fill="both", expand=True)
app.mainloop()
来源:https://stackoverflow.com/questions/41811921/tkinter-how-to-create-a-listbox-that-can-appear-and-disappear-over-another-wid