Now I understand that there is already a similar question Python Tkinter: OptionMenu modify dropdown list width however this does not help me.
I am trying to make the width of the drop down menu from the OptionMenu widget responsive. Meaning that the width will always match the width of the OptionMenu. As shown in the code below, I've tried a few things but they don't apply to the submenu and it will always stay at a fixed width. Is there no way to change it?
import tkinter as tk
root = tk.Tk()
var = tk.StringVar()
var.set('First')
option = tk.OptionMenu(root, var, 'First', 'Second', 'Third')
option.configure(indicatoron = False)
option.pack(expand = True, fill = tk.X)
# Sub-menu config
submenu = option['menu']
submenu.configure(width = 50) # Can't use width
submenu.pack_configure(expand = True, fill = tk.X) # Can't use pack_configure
root.mainloop()
while there is no way to explicitly set the width, if you really must use tkinter then it is possible to add hacky workarounds to pad these things out. and example of this would be:
import tkinter as tk
from tkinter import font as tkFont
def resizer(event=None):
print("Resize")
widget = event.widget
menu = widget['menu']
req_width = widget.winfo_width()-10
menu_width = menu.winfo_reqwidth()
cur_label = menu.entrycget(0, "label")
cur_label = cur_label.rstrip() # strip off existing whitespace
font = tkFont.Font() # set font size/family here
resized = False
while not resized:
difsize = req_width - menu_width # check how much we need to add in pixels
tempsize = 0
tempstr = ""
while tempsize < difsize:
tempstr += " " # add spaces into a string one by one
tempsize = font.measure(tempstr) #measure until big enough
menu.entryconfigure(0, label=cur_label + tempstr) # reconfigure label
widget.update_idletasks() # we have to update to get the new size
menu_width = menu.winfo_reqwidth() # check if big enough
cur_label = menu.entrycget(0, "label") # get the current label for if loop needs to repeat
if menu_width >= req_width: # exit loop if big enough
resized = True
root = tk.Tk()
var = tk.StringVar()
var.set('First')
option = tk.OptionMenu(root, var, 'First', 'Second', 'Third')
option.bind("<Configure>", resizer) # every time the button is resized then resize the menu
option.configure(indicatoron = False)
option.pack(expand = True, fill = tk.X)
root.mainloop()
this essentially just pads out the first menu item until the menu is big enough. however there does seem to be some discrepancy in the widths reported back by tkinter hence my req_width = widget.winfo_width()-10 offset near the top.
however this will not always be a perfect match size wise, while testing my a space seems to take 3 pixels of width, so it could be 1 or 2 pixels out at any time.
Related
This is an extension to the question and answers here.
I want to create two Comboboxes, with the items in the second Combobox depending on the selection in the first Combobox. Furthermore, I would like the dropdown listbox to resize to fit the text in the list, as in the answer here. However, I'm having some difficulties with this second part. I'd like to solve this problem using Python.
I've had varying results using .pack() and .forget() instead of .place() and .place_forget(), however I haven't been able to create a robust solution. Using .place is preferable to .pack or .grid if possible.
As an MWE, I've extended the code from one of the answers in the previous question. The dropdown listbox of c2 resizes fine, however that of c1 does not.
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont
def on_combo_configure(event):
combo = event.widget
style = ttk.Style()
# check if the combobox already has the "postoffest" property
current_combo_style = combo.cget('style') or "TCombobox"
if len(style.lookup(current_combo_style, 'postoffset'))>0:
return
combo_values = combo.cget('values')
if len(combo_values) == 0:
return
longest_value = max(combo_values, key=len)
font = tkfont.nametofont(str(combo.cget('font')))
width = font.measure(longest_value + "0") - event.width
if (width<0):
# no need to make the popdown smaller
return
# create an unique style name using widget's id
unique_name='Combobox{}'.format(combo.winfo_id())
# the new style must inherit from curret widget style (unless it's our custom style!)
if unique_name in current_combo_style:
style_name = current_combo_style
else:
style_name = "{}.{}".format(unique_name, current_combo_style)
style.configure(style_name, postoffset=(0,0,width,0))
combo.configure(style=style_name)
def update_c1_list(event):
c1.place_forget()
_ = c.get()
if _ == "fruit":
c1['values'] = ('apples are the best', 'bananas are way more better')
elif _ == "text":
c1['values'] = ("here's some text", "and here's some much longer text to stretch the list")
else:
pass
c1.place(x=10,y=40)
root = tk.Tk()
root.title("testing the combobox")
root.geometry('300x300+50+50')
c = ttk.Combobox(root, values=['fruit','text'], state="readonly", width=10)
c.bind('<Configure>', on_combo_configure)
c.bind('<<ComboboxSelected>>', update_c1_list)
c.place(x=10,y=10)
c1 = ttk.Combobox(root, state="readonly", width=10)
c1.bind('<Configure>', on_combo_configure)
c1.place(x=10,y=40)
c2 = ttk.Combobox(root, state="readonly", width=10)
c2.bind('<Configure>', on_combo_configure)
c2.place(x=10,y=70)
c2['values']=('this list resizes fine','because it is updated outside of the function')
root.mainloop()
Any help is welcome, thanks.
After playing around I came up with a function which updates the listbox width of a combobox "on the fly". However, it's a bit like fixing a window with a hammer and causes some issues.
def Change_combo_width_on_the_fly(combo,combo_width):
style = ttk.Style()
# check if the combobox already has the "postoffest" property
current_combo_style = combo.cget('style') or "TCombobox"
combo_values = combo.cget('values')
if len(combo_values) == 0:
return
longest_value = max(combo_values, key=len)
font = tkfont.nametofont(str(combo.cget('font')))
width = font.measure(longest_value + "0") - (combo_width*6+23)
if (width<0):
# no need to make the popdown smaller
return
# create an unique style name using widget's id
unique_name='Combobox{}'.format(combo.winfo_id())
# the new style must inherit from curret widget style (unless it's our custom style!)
if unique_name in current_combo_style:
style_name = current_combo_style
else:
style_name = "{}.{}".format(unique_name, current_combo_style)
style.configure(style_name, postoffset=(0,0,width,0))
combo.configure(style=style_name)
As a MWE, the code can be used as follows:
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont
root = tk.Tk()
c1 = ttk.Combobox(root, state="readonly", width=10)
c1.place(x=10,y=40)
Change_combo_width_on_the_fly(c1,10)
root.mainloop()
While the function does the job, it causes problems elsewhere in my code. In particular, it messes with a previously packed widget (scrollbar). I think it is changing the style the last placed widget, but I don't know how to fix this.
I am trying to add a menu bar on a Canvas widget. I am currently testing it using some demo code I found online, before I implement it in an application that I am writing. Currently the code shows the window, but the Menu bar appears at the bottom of the page, instead of the top.
Also more of a side note: Is there a way that I can use a function in a seperate python file to draw a shape without having it create an entire new window?
my code:
import tkinter
from tkinter import *
from tkinter import messagebox
def option():
print("Options")
top = Tk()
mb = Menubutton(top, text = "condiments", relief = RAISED)
C = Canvas(top, bg = "blue", height = 250, width = 250)
C.grid()
mb.grid()
mb.menu = Menu(mb, tearoff = 0)
mb["menu"] = mb.menu
mb.menu.add_command(label = "mayo", command = option)
mb.menu.add_command(label = "ketchup", command = option)
coord = 10,50.240, 210
coord1 = 10,50,20,60
arc = C.create_arc(coord, start = 0, extent = 150, fill = "red")
line = C.create_line(coord, fill = "white")
oval = C.create_oval(coord1, fill = "black")
top.mainloop()
By default, grid will automatically increase the row and column unless you specify otherwise. You could also simply reorder the code so that the defaults match up with your expectation.
The Zen of Python says that "explicit is better than implicit". If you explicitly define the row and column, the code will be easier to understand and you can put the menubar wherever you want.
mb.grid(row=0, column=0)
C.grid(row=1, column=0)
I'm trying to equally distribute three objects/widgets across one row in a ttk notebook tab, but the three objects only expand half of the window.
I'm not sure what controls the number of columns within a tab since columnsspan in tab.grid(row=0, columnspan=3) doesn't appear to change anything. I've also tried various values for rows and columns for every object with .grid. This is only an issue for notebook tabs, rather than a single window.
#!/usr/bin/env python3
from tkinter import ttk
from tkinter import *
root = Tk()
root.title('Title')
root.resizable(width=FALSE, height=FALSE)
root.geometry('{}x{}'.format(750, 750))
nb = ttk.Notebook(root)
nb.grid(row=0, column=0)
# Add first tab
tab1 = ttk.Frame(nb)
#tab1.grid(row=0, column=0)
nb.add(tab1, text='Setup')
# Add row label
lb1 = ttk.Label(tab1, text = 'Parent Directory:')
lb1.grid(row = 1, column = 1)
# Add text entry
txt1 = ttk.Entry(tab1)
txt1.grid(row = 1, column = 2)
# Add selection button
btn1 = ttk.Button(tab1, text="Select")
btn1.grid(row=1, column=3)
root.mainloop()
I'm expecting the columns to span the full length of the window, instead of half the length of the window.
In order to do this using grid you need to use the Frame.columnconfigure([column#], minsize=[minsize]) function.
If you want the text box and button to stretch to fill the space, use the sticky option. (Sticky doesn't really do anything with the label)
Code:
#!/usr/bin/env python3
from tkinter import ttk
from tkinter import *
root = Tk()
root.title('Title')
root.resizable(width=FALSE, height=FALSE)
root.geometry('{}x{}'.format(750, 750))
nb = ttk.Notebook(root, width=750)
nb.grid(row=0, column=0)
# Add first tab
tab1 = ttk.Frame(nb)
#tab1.grid(row=0, column=0)
nb.add(tab1, text='Setup')
# Change the sizes of the columns equally
tab1.columnconfigure(1, minsize=250)
tab1.columnconfigure(2, minsize=250)
tab1.columnconfigure(3, minsize=250)
# Add row label
lb1 = ttk.Label(tab1, text = 'Parent Directory:')
lb1.grid(row = 1, column = 1,sticky=(E,W))
# Add text entry
txt1 = ttk.Entry(tab1)
txt1.grid(row = 1, column = 2,sticky=(E,W))
# Add selection button
btn1 = ttk.Button(tab1, text="Select")
btn1.grid(row=1, column=3,sticky=(E,W))
root.mainloop()
Image of result
I'm learning Tkinter at the moment. From my book, I get the following code for producing a simple vertical scrollbar:
from tkinter import * # Import tkinter
class ScrollText:
def __init__(self):
window = Tk() # Create a window
window.title("Scroll Text Demo") # Set title
frame1 = Frame(window)
frame1.pack()
scrollbar = Scrollbar(frame1)
scrollbar.pack(side = RIGHT, fill = Y)
text = Text(frame1, width = 40, height = 10, wrap = WORD,
yscrollcommand = scrollbar.set)
text.pack()
scrollbar.config(command = text.yview)
window.mainloop() # Create an event loop
ScrollText() # Create GUI
which produces the following nice output:
enter image description here
However, when I then try to change this code in the obvious way to get a horizontal scrollbar, it's producing a weird output. Here's the code I'm using
from tkinter import * # Import tkinter
class ScrollText:
def __init__(self):
window = Tk() # Create a window
window.title("Scroll Text Demo") # Set title
frame1 = Frame(window)
frame1.pack()
scrollbar = Scrollbar(frame1)
scrollbar.pack(side = BOTTOM, fill = X)
text = Text(frame1, width = 40, height = 10, wrap = WORD,
xscrollcommand = scrollbar.set)
text.pack()
scrollbar.config(command = text.xview)
window.mainloop() # Create an event loop
ScrollText() # Create GUI
and here's what I get when I run this:
enter image description here
You're assigning horizontal scrolling, xscrollcommand, to a vertical scrollbar. You need to modify Scrollbar's orient option to 'horizontal' which is by default 'vertical'.
Try replacing:
scrollbar = Scrollbar(frame1)
with:
scrollbar = Scrollbar(frame1, orient='horizontal')
def red():
frame3.output_display.config(fg = 'red', font=root.customFont1)
def blue():
frame3.output_display.config(fg = 'darkblue', font=root.customFont2)
def green():
frame3.output_display.config(fg = 'darkgreen',font=root.customFont3)
def black():
frame3.output_display.config(fg = 'black',font=root.customFont4)
from tkinter import *
from tkinter import ttk
import tkinter.font
from tkinter.scrolledtext import ScrolledText
root = Tk()
root.title("Change Text")
root.geometry('700x500')
# change font size and family: not used currently because of resizing issue
root.customFont1 = tkinter.font.Font(family="Handwriting-Dakota", size=12)
root.customFont2 = tkinter.font.Font(family="Comic sans MS", size=14)
root.customFont3 = tkinter.font.Font(family="Script MT", size=16)
root.customFont4 = tkinter.font.Font(family="Courier", size=10)
# FRAME 3
frame3 = LabelFrame(root, background = '#EBFFFF', borderwidth = 2, text = 'text entry and display frame', fg = 'purple',bd = 2, relief = FLAT, width = 75, height = 40)
frame3.grid(column = 2, row = 0, columnspan = 3, rowspan = 6, sticky = N+S+E+W)
#frame3.grid_rowconfigure(0, weight=0)
#frame3.grid_columnconfigure(0, weight=0)
frame3.grid_propagate(True)
frame3.output_display = ScrolledText(frame3, wrap = WORD)
frame3.output_display.pack( side = TOP, fill = BOTH, expand = True )
frame3.output_display.insert('1.0', 'the text should appear here and should wrap at character forty five', END)
#frame3.output_display.config(state=DISABLED) # could be used to prevent modification to text (but also prevents load new file)
# draws all of the buttons,
ttk.Style().configure("TButton", padding=6, relief="flat",background="#A52A2A", foreground='#660066')
names_colour=(('Red',red),('Blue',blue),('Green',green),('Black',black))
root.button=[]
for i,(name, colour) in enumerate(names_colour):
root.button.append(ttk.Button(root, text=name, command = colour))
row,col=divmod(i,4)
root.button[i].grid(sticky=N+S+E+W, row=6, column=col, padx=1, pady=1)
root.mainloop()
In the GUI when the text font face and font size is changed, the textbox resizes and obscures the buttons. In my naivety I thought that the textbox would remain the same size and the text would simply wrap within the constraints of the textbox. At least taht is what I would like to achieve. Obviously there is some concept in font size or in textbox , tkinter that I do not understand
Thanks
The width of the text widget is defined in units of character widths rather than pixels, and it tries to use its configured width as its minimum width whenever possible. The widget will be wider for wider fonts, and narrower for narrow fonts. Thus, if you give it a wide font it will try to make itself wider to remain X characters wide.
So, how do you solve this?
One solution is to set the width and height to something small. For example, if you set the width and height to 1 (one), the widget will only ever try to force itself to be one character wide and tall. Unless you're using absolutely huge fonts, you'll barely see the widget grow as you enlarge the font.
Then you will need to rely on the pack, grid, or place algorithm to stretch the widget to the desired dimensions. If you're using grid, this usually means you need to make sure that column and row weights are set appropriately, along with setting the sticky attribute.
The downside to this is that you have to make sure your GUI has the right size, rather than depending on it just magically happening based on the preferred size of each widget.
As a quick hack, you can see this in your program by adding these lines after the widgets have been created:
frame3.output_display.configure(width=1, height=1)
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(2, weight=1)
When I run your code with the above additional lines, the text widget remains a fixed size and the text wraps at different places with each font.