I am using the grid geometry manager of Tkinter to generate a table with horizontally scrollable text in column 2. I tried creating a Listbox widget that lives in column 2 (as a child of the overall frame) and spans all the rows. This seemed promising, until it became apparent that the lines of text in the Listbox are not aligned with the rows of the parent grid. I've been searching in vain for a way to pad each row of text in the Listbox so that the rows match up; but even if that were possible, I would prefer a more general, less kludgy solution.
I recently stumbled across a description of Gridded Geometry Management that alludes to a setgrid option for a widget. It purports to do exactly what I want: that is, "[determine] whether this widget controls the resizing grid for its top-level window." I tried enabling this option in my Listbox widget, but to no avail. Am I somehow misunderstanding the purpose/usage of setgrid?
(In order to see the problem with the code below, use the Select File or Select Folder buttons to load multiple audio files into the file list.)
#! /usr/bin/env python
#################################################
# This tool allows the user to select audio files
# (or folders containing audio files) and subject
# them to loudness analysis.
#################################################
import sys
import os
import codecs
import re
import Tkinter
from Tkinter import *
import tkFileDialog
from os import walk
from os import path
from Tkinter import Tk, Text, BOTH, W, N, E, S
from ttk import Frame, Button, Label, Style, Progressbar
from ScrolledText import *
from progressbar import ProgressBar
class Leveler_tk(Frame):
fileList = []
allowedExtensions = ['mp3','mp2','m4a','aiff','wav']
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initialize()
def initialize(self):
self.style = Style()
self.style.theme_use("default")
self.pack(fill=BOTH, expand=1)
self.columnconfigure(1, weight=1)
self.columnconfigure(2, weight=1)
self.columnconfigure(2, pad=250)
self.columnconfigure(3, weight=1)
self.columnconfigure(4, weight=1)
self.columnconfigure(5, weight=1)
lbl1 = Label(self, text="Analyze")
lbl1.grid(pady=4, padx=5,row=0,column=0)
lbl2 = Label(self, text="Adjust")
lbl2.grid(pady=4, padx=5,row=0,column=1)
lbl3 = Label(self, text="File")
lbl3.grid(pady=4, padx=5,row=0,column=2)
lbl4 = Label(self, text="Integrated\nLoudness")
lbl4.grid(pady=4, padx=5,row=0,column=3)
lbl5 = Label(self, text="LRA")
lbl5.grid(pady=4, padx=5,row=0,column=4)
lbl6 = Label(self, text="Peak")
lbl6.grid(pady=4, padx=5,row=0,column=5)
lbl7 = Label(self, text="Progress")
lbl7.grid(pady=4, padx=5,row=0,column=6)
lbl8 = Label(self, text="Meets\nSpecs?")
lbl8.grid(sticky=W, pady=4, padx=5,row=0,column=7)
file_btn = Button(self, text="Select File",command=self.selectFile)
file_btn.grid(row=1,rowspan=2, column=8,padx=5,pady=4)
folder_btn = Button(self, text="Select Folder", command=self.selectFolder)
folder_btn.grid(row=3, rowspan=2, column=8,padx=5,pady=4)
def render(self):
count = 0
filebox = Listbox(self,selectmode=EXTENDED,setgrid=1)
scrollbar = Scrollbar(filebox, orient=HORIZONTAL)
scrollbar.config(command=filebox.xview)
filebox.grid(row=1, column=2, rowspan=len(self.fileList), columnspan=1, sticky=N+S+E+W)
filebox.config(xscrollcommand=scrollbar.set)
scrollbar.pack(side=BOTTOM, fill=X)
for file in self.fileList:
analyze = IntVar()
adjust = IntVar()
Radiobutton(self, text="", variable=analyze, value=count, borderwidth=0).grid(row=count+1, column=0)
Radiobutton(self, text="", variable=adjust, value=count, borderwidth=0).grid(row=count+1, column=1)
filebox.insert(END, file + "\n")
Progressbar(self, orient=HORIZONTAL,length=100, mode='determinate').grid(row=count+1, column=6)
count += 1
def addToList(self, name):
dot = re.search("\.(?=[^.]*$)",name)
extension = name[dot.end():]
if extension in self.allowedExtensions and not name in self.fileList:
self.fileList.append(name)
def selectFile(self):
input = tkFileDialog.askopenfilename(filetypes = [('MP3', '*.mp3'), ('MP2', '*.mp2'), ('M4A', '*.m4a'), ('AIFF', '*.aiff'), ('WAV', '*.wav')], multiple = 1)
for el in input:
if os.path.isfile(el) and ".DS_Store" not in el:
try:
self.addToList(el)
except:
tkMessageBox.showerror("Some error")
self.render()
def selectFolder(self):
input = tkFileDialog.askdirectory()
for (dirpath, dirnames, filenames) in walk(input):
for name in filenames:
if name != ".DS_Store":
self.addToList(dirpath + "/" + name)
self.render()
def main():
root = Tk()
app = Leveler_tk(root)
root.mainloop()
if __name__ == "__main__":
main()
I think you are misunderstanding setgrid.
It is so that a widget with a natural size that is based on something other than a pixel (such as a text widget, whose size is based on characters) can prevent the parent from setting it to an unnatural size (eg: 20.5 characters). With setgrid, when you interactively resize the window it will resize in grid units (eg: character height or width) rather than pixels.
setgrid doesn't prevent resizing so much as that it makes sure resizing happens at multiples of some other unit.
Here is the complete, definitive reference for the setgrid option, from the tcl/tk manual:
Specifies a boolean value that determines whether this widget controls
the resizing grid for its top-level window. This option is typically
used in text widgets, where the information in the widget has a
natural size (the size of a character) and it makes sense for the
window's dimensions to be integral numbers of these units. These
natural window sizes form a grid. If the setGrid option is set to true
then the widget will communicate with the window manager so that when
the user interactively resizes the top-level window that contains the
widget, the dimensions of the window will be displayed to the user in
grid units and the window size will be
constrained to integral numbers of grid units. See the section GRIDDED
GEOMETRY MANAGEMENT in the wm manual entry for more details.
Related
My problem with this code is that the text box that appears when you hover over a button resizes the entire window. What I want is for this frame to have the same size as the Text field which will place the information there.
import tkinter
import re
from tkinter import *
from tkinter import ttk, filedialog
from ctypes import windll
from tkinter.scrolledtext import ScrolledText
class MainApp(Tk):
def __init__(self):
super().__init__()
#self.geometry("500x300")
self.title("My Bioinformatics Toolbox")
def fasta_button_hover(hover):
instructions_field.config(text="This function will allow the user to open, "
"read and see the contents of a .FASTA file. It "
"will also allow the user to save their own sequence "
"as a FASTA file")
def fasta_button_leave(hover):
instructions_field.config(text="")
# Create the menu bar
menu_bar = tkinter.Menu(self)
file_menu = tkinter.Menu(menu_bar, tearoff=0)
# The File cascade in the menu bar
menu_bar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Exit", command=self.destroy)
self.config(menu=menu_bar)
# Create the buttons frame
button_frame = ttk.Frame(self)
#button_frame.configure(height=button_frame["height"], width=button_frame["width"])
#button_frame.grid_propagate(0)
button_frame.pack(side=LEFT)
# Create the text field frame
text_frame = ttk.Frame(self, width=500)
text_frame.pack(side=RIGHT)
# Create the instructions field
instructions_field = Message(text_frame, width=300)
instructions_field.grid(column=0, row=0)
# Create the buttons
fasta_button = ttk.Button(button_frame, text="Read FASTA", width=12)
fasta_button.bind("<Enter>", fasta_button_hover)
fasta_button.bind("<Leave>", fasta_button_leave)
fasta_button.grid(column=0, row=0)
dna_to_rna_button = ttk.Button(button_frame, text="DNA to RNA", width=12)
dna_to_rna_button.grid(column=0, row=1)
example_button = ttk.Button(button_frame, text="Example", width=12)
example_button.grid(column=0, row=2)
example_button2 = ttk.Button(button_frame, text="Example2", width=12)
example_button2.grid(column=0, row=3)
example_button3 = ttk.Button(button_frame, text="Example3", width=12)
example_button3.grid(column=0, row=4)
# A separator line ("rowspan" is set to a safe number of rows)
button_separator = ttk.Separator(button_frame, orient="vertical")
button_separator.grid(column=1, row=0, rowspan=100, sticky="ns")
# Fix for windows DPI scaling
windll.shcore.SetProcessDpiAwareness(1)
app = MainApp()
app.mainloop()
I want my window to have this size full size app at all time, but when I'm not hovering it has this size small puny app
My guess is that I'm missing something from the text_frame, but I can't figure it out...
Edit to update indentation
This amendment to your code seems to achieve this:
# Create the instructions field
instructions_field = ttk.Label(text_frame, width=50, wraplength=300)
instructions_field.grid(column=0, row=0)
The changes made:
Replaced the Message widget with a Label widget
Set both the width and wraplength keyword arguments in the Label
Manually adjusted the wraplength (taken as a number of pixels before wrapping) to fit all of the text inside the widget
It is arguably not the most elegant solution but a working and reliable one nonetheless
Note: The width keyword argument in the Label is taken as a number of characters wide
I want to create a grid of pictures and comments like a detail view in a folder. However, when I don't load the image (the "magic two lines" after XXX), the layout collapses and I see only the borders, not even Label widgets. But of course it should work in both cases. Here is some simplified version:
import os.path as osp
import tkinter as tk
from PIL import Image, ImageTk
IMG_FILE = '/your/magic/image/path/name.png'
class Comparator:
def __init__(self):
self.root = tk.Tk()
self.root.geometry('1350x730+0+0')
self.ctrl = ctrl = tk.Frame(self.root, padx=10, pady=10)
ctrl.grid(row=0, column=0)
lframe = tk.Frame(self.root)
lframe.grid(row=0, column=1, rowspan=2)
inner_frame = tk.Frame(lframe, bg='#444', padx=7, pady=7)
inner_frame.grid(row=0, column=0)
# group of widgets
tk_img = ImageTk.PhotoImage(Image.open(IMG_FILE))
for num in range(23):
yard = tk.Frame(inner_frame, bg='#222', padx=6, pady=6)
ya_col = num % 4
ya_row = num // 4
yard.grid(column = ya_col, row = ya_row)
stone = tk.Label(yard, text='Img name', width=179, height=179)
stone.grid(row=0, column=0)
# XXX magic two lines that generate the right layout:
stone.image = tk_img
stone.config(image=stone.image)
# end magic two lines
tk.Label(yard, text='%d : %d %d' % (num, ya_row, ya_col)).grid(row=1, column=0)
self.root.mainloop()
Comparator()
There are a few redundant tk.Frame instances because it's just a simplified version; for instance ctrl is supposed to be filled with a lot of buttons etc. Actually, I also wanted the inner_frame to be in a scrollable tk.Canvas, but let's say that's another story...
I don't understand what happens. Any suggestions would be very nice :-)
[edit] Maybe it depends on the fact that width is interpreted either in pixels or in number of chars? How do I fix that?
Why I cannot stretch Text widget inside Notebook widget(Tab) with sticky?
How to get fixed Text widget size while changing font, while grid_propagate doesn't give results.
How that same window can again be resizable (weight) altogether?
Thanks
import tkinter as tk
from tkinter import *
from tkinter import ttk, font
class TextInsideNotebook:
def __init__(self, main):
self.main = main
self.fontSizePx = -20
# Font
self.fontspecs = font.Font(family="consolas", size=self.fontSizePx)
# Notebook
self.tab = ttk.Notebook(main, width=800, height=600)
self.tab.grid(row=0, column=0, sticky="nsew")
# Tab
self.tab_frame = Frame(self.tab)
self.tab.grid(row=0, column=0, sticky="nsew")
self.tab.add(self.tab_frame, text=' NEW FILE ')
# Text Area
self.textarea = tk.Text(self.tab_frame, font=self.fontspecs)
self.textarea.grid(row=0, column=0, sticky="nsew")
self.tab_frame.grid_propagate(False)
# weights
main.columnconfigure(0, weight=1)
main.rowconfigure(0, weight=1)
# Bind
self.main.bind('<Control-MouseWheel>', self.new_font_size)
def new_font_size(self, event):
if event.delta > 0:
self.fontSizePx = self.fontSizePx - 2
else:
self.fontSizePx = self.fontSizePx + 2
self.fontspecs.config(size=self.fontSizePx)
if __name__ == "__main__":
main = tk.Tk()
agc = TextInsideNotebook(main)
main.mainloop()
Why I cannot stretch Text widget inside Notebook widget(Tab) with sticky?
The text does stick to the edges of the area you've allocated to it. However, you haven't given any rows or columns inside self.tab_frame a weight so that row and column is only as wide and tall as the text widget.
If you're only putting the text widget in the frame, it's much easier to use pack than grid since it takes only one line of code rather than three:
self.textarea.pack(fill="both", expand=True)
If you wish to stick to using grid for some reason, you must give the row and column that contains the text widget a non-zero weight
self.tab_frame.grid_rowconfigure(0, weight=1)
self.tab_frame.grid_columnconfigure(0, weight=1)
self.textarea.grid(row=0, column=0, sticky="nsew")
As a rule of thumb you should always give at least one row and one column a positive non-zero weight in any widget that has children managed by grid.
How to get fixed Text widget size while changing font
What I recommend is to give the widget a small size, such as 1x1, and then let the geometry manager (pack, place, or grid) stretch it to fit the space allocated to it.
self.textarea = tk.Text(..., width=1, height=1)
How that same window can again be resizable (weight) altogether?
I don't understand that question. It's never not resizable.
I have very simple grid layout with two columns, where first column should display some text, and the second to show tree view:
#! python3
from random import randint
import tkinter as tk
from tkinter import ttk
from tkinter.constants import *
class Application(ttk.Frame):
def __init__(self, root):
self.root = root
self.root.resizable(0, 0)
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_columnconfigure(1, weight=3)
self.init_widgets()
self.arrange_grid()
def init_widgets(self):
self.text_frame = ttk.Labelframe(self.root, text='Info')
self.button = ttk.Button(self.root, text='Process', command=self.on_button)
self.tree = ttk.Treeview(self.root)
self.scroll = ttk.Scrollbar(self.root, orient=HORIZONTAL, command=self.tree.xview)
self.tree.configure(xscrollcommand=self.scroll.set)
def arrange_grid(self):
self.text_frame.grid(row=0, sticky=NSEW)
self.button.grid(row=0, sticky=N, pady=32)
self.tree.grid(row=0, column=1, sticky=NSEW)
self.scroll.grid(row=0, column=1, sticky=(S, W, E))
def on_button(self):
headers = list(range(20))
rows = [[randint(0, 100)] * len(headers) for i in headers]
self.tree["columns"] = headers
for i, row in enumerate(rows):
self.tree.insert("", i, values=row)
if __name__ == '__main__':
root = tk.Tk()
app = Application(root)
root.mainloop()
When I click on a "Process" button, tree view is populated with data, but at the same time it resizes the root window and fills whole space.
How can I instruct ttk tree view, to remain it's size after populating with data?
The treeview will grow to fit all of its columns, unless constrained by the window. The window will grow to fit all of it children unless you give it a fixed size. What is happening is that you're giving the treeview many columns, causing it to grow. Because it grows, the window grows because you haven't constraint its growth.
There are several solutions. Perhaps the simplest solution is to put the tree in a frame so that you can give it an explicit width and height. The key to this is to make the frame control the size of its children rather than the other way around. This is done by turning geometry propagation off.
First, start by creating a frame, and then putting the tree in the frame. We can also put the scrollbar in the frame so that we can treat the tree and scrollbar as a single unit.
self.tree_frame = tk.Frame(self.root, width=400, height=200)
self.tree = ttk.Treeview(self.treeframe)
self.scroll = ttk.Scrollbar(self.tree_frame, orient=HORIZONTAL, command=self.tree.xview)
self.tree.configure(xscrollcommand=self.scroll.set)
Next, add the treeview and scrollbar to the frame. You can use any of pack, place or grid; I find pack superior for a top-to-bottom layout. We also use pack_propagate to turn off geometry propagation (meaning: the frame width and height are honored):
self.tree_frame.pack_propagate(0)
self.scroll.pack(side="bottom", fill="x")
self.tree.pack(side="top", fill="both", expand=True)
With that, you need to modify your arrange_grid to put the frame in the root window, and then ignore the scrollbar since it's already packed in the frame:
def arrange_grid(self):
self.text_frame.grid(row=0, sticky=NSEW)
self.button.grid(row=0, sticky=N, pady=32)
self.tree_frame.grid(row=0, column=1, sticky=NSEW)
Note: you've turned off the ability for the user to resize the window. I recommend avoiding this -- the user usually knows better what size they want the window. Instead, you should configure your GUI to properly resize when the user resizes the window.
Since you're using grid, all you have to do is tell tkinter which rows and columns get any extra space caused by the user resizing the window. Since everything is in a single row, you merely need to give that row a weight:
root.grid_rowconfigure(0, weight=1)
I'm building an application that provides viewports for an internal data file. The files are somewhat complex, so I've created three sub-windows that handle different aspects of the file. The upper left provides an outline of the different sections, the upper right provides a series of text widgets containing errors found in the data file, and the lower provides a view of the datafile itself. To facilitate all of this, I wrote a small class that serves as a frame for each of these sections and can be populated with labels, textboxes, etc. (Code below.)
The problem I'm running into is that the text widgets in the upper right and lower sections do not expand with their containing frame. Based on various searches of effbot.org, Stackoverflow, and others, I think I have the settings correct, but obviously something is wrong. If I enlarge the main window, each section adapts as it should, but the text widgets don't expand left to right to fill the new subwindow dimensions.
Any tips are greatly appreciated.
Here's the class that provides functionality for the subwindows:
import Tkinter as tk
class ScrollingChildFrame(tk.Frame):
'''
A Tkinter class creating a scrollable window that can be used
in a docked multiple document interface form. The window created here
allows addition of widgets to create scrolling tables (spreadsheet like),
groups of text fields, etc.
'''
def __init__(self, root):
self.count = 0
tk.Frame.__init__(self)
self.root = root
self.canvas = tk.Canvas(self, height=self.winfo_height(), width=self.winfo_width() )
self.canvas.grid(row=0, column=0, sticky='nsew')
self.vsb = tk.Scrollbar(self, orient='vertical', command=self.canvas.yview)
self.vsb.grid(row=0,column=1,sticky='ns')
self.hsb = tk.Scrollbar(self, orient='horizontal', command=self.canvas.xview)
self.hsb.grid(row=1,column=0,sticky='ew')
self.intframe = tk.Frame(self.canvas)
self.intframe.config(height=self.winfo_height(), width=self.winfo_width())
self.canvas.configure(yscrollcommand=self.vsb.set, xscrollcommand=self.hsb.set)
self.canvas.create_window(0, 0, window=self.intframe, anchor='nw')
#set event bindings
self.bind('<Configure>', self.OnFrameConfigure)
self.intframe.bind('<Configure>', self.OnIntFrameConfigure)
def OnFrameConfigure(self, event=None):
'''
adjust canvas when main frame adjusts
'''
self.canvas.configure(width=event.width - self.vsb.winfo_width()-2,
height=event.height - self.hsb.winfo_height()-2)
def OnIntFrameConfigure(self, event=None):
'''
adjust the scrolling window when the internal frame is adjusted
'''
self.canvas.configure(scrollregion=self.canvas.bbox(tk.ALL))
Here's an example of how I'm using it with textboxes that don't expand:
import Tkinter as tk
from scrollingchildframe import *
class Vis_GUI:
'''
The main GUI class
'''
def __init__(self):
#tkinter stuff
self.root = tk.Tk()
self.root.geometry('500x500')
self.create_frames()
self.root.mainloop()
def create_frames(self):
'''
Build the GUI frames
'''
self.root.columnconfigure(0,weight=1)
self.root.columnconfigure(1,weight=3)
self.root.rowconfigure(0,weight=1)
self.root.rowconfigure(1,weight=3)
#data blocks
self.block_frame = ScrollingChildFrame(self.root)
self.block_frame.config(height=200, width=200)
##error list
self.error_frame = ScrollingChildFrame(self.root)
self.error_frame.config(height=200, width=300)
##data
self.data_frame = ScrollingChildFrame(self.root)
self.data_frame.config(height=300, width=500)
##populate with empty cells
self.PopulateEmpty()
##place them on the grid
self.block_frame.grid(row=0, column=0, padx=2, pady=2, sticky='nsew')
self.error_frame.grid(row=0,column=1, padx=2, pady=2, sticky='nsew')
self.data_frame.grid(row=1, column=0, columnspan=2, padx=2,pady=2, sticky='nsew')
def PopulateEmpty(self):
'''
Populate the frames with empty contents so it doesn't look quite so empty.
'''
z = tk.Text(self.data_frame.intframe)
z.insert(tk.INSERT, 'blah\nblah\nblah')
height = float(z.index(tk.END))
z.config( height=height, state=tk.DISABLED, wrap=tk.NONE)
z.pack(anchor='nw', expand=1, fill=tk.X)
z = tk.Text(self.error_frame.intframe, height=1)
z.pack(anchor='w', expand = 1, fill=tk.X)
z = tk.Label(self.block_frame.intframe, text = 'No file open')
z.pack(anchor='w')
if (__name__=="__main__"):
wv = Vis_GUI()
The Frame also has to have expand and fill options set (and you will have to check on what ScrollingChildFrame does-and this is not a complaint about incomplete code, just pointing out the next step). Using just pack() for the Frame in the following code will not allow it to expand. You can uncomment it and comment the other pack if you want to see the difference.
try:
import Tkinter as tk ## Python 2.x
except ImportError:
import tkinter as tk ## Python 3.x
top=tk.Tk()
## use separate frame instead of top
fr=tk.Frame(top)
##fr.pack() ## does not expand
fr.pack(anchor='nw', expand=1, fill=tk.X)
z = tk.Text(fr)
insert_text="%s" % ("blah"*25) + 'blah\nblah\nblah'
z.insert(tk.INSERT, insert_text)
height = float(z.index(tk.END))
z.config( height=height, state=tk.DISABLED, wrap=tk.NONE)
z.pack(anchor='nw', expand=1, fill=tk.X)
top.mainloop()