The canvas Widget in Tkinter is very slow at drawing, causing a lot of distortion to the applications visuals when scrolling even when using limited widgets.
I have had a search around but only seem to have answers from people drawing multiple things to a canvas rather than the scrollbar effects.
Is there any issues with my code that would cause this issue or are there any methods to fix the draw times to be more visually smooth. In the application this is meant for each row is a different colour which can make it extremely ugly to look at and hard to find the data the user is looking for.
MVCE:
#python 3.8.6
from tkinter import *
import random
class test:
def __init__(self):
self.words = ["troop","relieve","exact","appeal","shortage","familiar","comfortable","sniff","mold","clay","rack","square","color","book","velvet","address","elaborate","grip","neutral","pupil"]
def scrollable_area2(self, holder):
base_frame = Frame(holder, padx=5, pady=5)
base_frame.pack(fill=BOTH, expand=1)
base_frame.rowconfigure(0, weight=0)
base_frame.columnconfigure(0, weight=1)
can = Canvas(base_frame, bg="white")
can.pack(side=LEFT, expand=1, fill=BOTH)
scrollArea = Frame(base_frame, bg="white", )
scrollArea.pack(side=LEFT, expand=1, fill=BOTH)
can.create_window(0, 0, window=scrollArea, anchor='nw')
Scroll = Scrollbar(base_frame, orient=VERTICAL)
Scroll.config(command=can.yview)
Scroll.pack(side=RIGHT, fill=Y)
can.config(yscrollcommand=Scroll.set)
scrollArea.bind("<Configure>", lambda e=Event(), c=can: self.update_scrollregion(e, c))
return scrollArea, can
def update_scrollregion(self, event, can):
if can.winfo_exists():
can.configure(scrollregion=can.bbox("all"))
def generate(self, count): #generates the rows
for i in range(int(count.get())):
row = Frame(self.holder)
row.pack(side=TOP)
for i in range(9):
a = Label(row, text=self.words[random.randint(0, len(self.words)-1)])
a.pack(side=LEFT)
b = Button(row, text=self.words[random.randint(0, len(self.words)-1)])
b.pack(side=LEFT)
def main(self):
opts = Frame(self.root)
opts.pack(side=TOP)
v= StringVar()
e = Entry(opts, textvariable=v)
e.pack(side=LEFT)
b=Button(opts, text="Run", command=lambda e=Event(), v=v:self.generate(v))
b.pack(side=LEFT)
main_frame=Frame(self.root)
main_frame.pack(side=TOP, fill=BOTH, expand=1)
self.holder, can = self.scrollable_area2(main_frame)
def run(self):
self.root = Tk()
self.main()
self.root.mainloop()
if __name__ == "__main__":
app = test()
app.run()
I have left a box where you can type the number of rows. I have tried from 30 rows to over 300 rows and although the initial render time changes the scroll issue is always the same.
NOTE: sorry about the weird way I am creating a scroll region, its from a more complex piece of code which I have modified to fit here if that ends up being a factor.
Since you are just creating a vertical stack of frames, it will likely be more efficient to use a text widget as the container rather than a canvas and embedded frame.
Here's a simple example that creates 1000 rows similar to how you're doing it with the canvas. On my OSX machine it performs much better than the canvas.
def scrollable_area2(self, parent):
base_frame = Frame(parent, padx=5, pady=5)
base_frame.pack(fill="both", expand=True)
holder = Text(base_frame)
vsb = Scrollbar(base_frame, orient="vertical", command=holder.yview)
holder.configure(yscrollcommand=vsb.set)
holder.pack(side="left", fill="both", expand=True)
vsb.pack(side="right", fill="y")
return holder
...
def generate(self, count): #generates the rows
for i in range(int(count.get())):
row = Frame(self.holder)
self.holder.window_create("end", window=row)
self.holder.insert("end", "\n")
...
def main(self):
...
self.holder = self.scrollable_area2(main_frame)
The above example keeps the inner frames, but you don't really need it. You can insert the text directly in the text widget, making the code even more efficient.
In a comment you said you aren't actually creating a stack of frames but rather a table of values. You can create a table in the text widget by using tabstops to create columns. By inserting text directly in the widget you're creating far fewer widgets which will definitely improve performance.
Here's an example using hard-coded the tabstops, but you could easily compute them based on the longest word in the list.
def scrollable_area2(self, parent):
base_frame = Frame(parent, padx=5, pady=5)
base_frame.pack(fill="both", expand=True)
self.holder = Text(base_frame, wrap="none", tabs=100)
vsb = Scrollbar(base_frame, orient="vertical", command=self.holder.yview)
self.holder.configure(yscrollcommand=vsb.set)
self.holder.pack(side="left", fill="both", expand=True)
vsb.pack(side="right", fill="y")
Your generate function then might look something like this:
def generate(self, count): #generates the rows
for i in range(int(count.get())):
for i in range(9):
text = "\t".join([random.choice(self.words) for x in range(9)])
self.holder.insert("end", text + "\t")
button = Button(self.holder, text=random.choice(self.words))
self.holder.window_create("end", window=button)
self.holder.insert("end", "\n")
Related
I am trying to create a GUI for an application I am making and for some reason that I cannot figure out, the text widget that is inside the message_space frame is increasing the size of the message_space frame and reducing the size of the friends_space frame. I want the friends_space frame to take up 1/4th of the window size and the message_space frame to take up the remaining 3/4ts of the window size.
The red is the friends_space frame, the blue is the message_space frame.
This is how I would like the sizing of the frames to be.
This is what is happening when I add the text box.
Code
from tkinter import *
class app:
def __init__(self, master):
self.master = master
master.title("PyChat")
master.geometry("800x500")
master.configure(bg="grey")
master.resizable(0, 0)
master.grid_columnconfigure(0, weight=1)
master.grid_columnconfigure(1, weight=3)
master.grid_rowconfigure(0, weight=1)
self.friends_space = Frame(master, bg="red")
self.friends_space.grid(row=0, column=0, sticky=NSEW)
self.chat_space = Frame(master, bg="blue")
self.chat_space.grid(row=0, column=1, columnspan=3, sticky=NSEW)
self.message_area = Text(self.chat_space)
self.message_area.grid(row=0, column=0)
root = Tk()
my_gui = app(root)
root.mainloop()
If you're using grid, you divide your UI into four uniform-width columns (using the uniform option), then have the text widget span three.
You should also start with a small text widget that can grow into the space. Otherwise tkinter will try to preserve the large size and start removing space from the other widgets in order to try to make everything fit.
Here's an example based on your original code. However, I'm using pack for the text widget instead of grid because it requires fewer lines of code. I've also reorganized the code a bit. I find that grouping calls to grid together makes layout easier to grok.
I've also removed the restriction on resizing. There's rarely a good idea to limit the user's ability to resize the window. Plus, it allows you to see that the resulting UI is responsive.
from tkinter import *
class app:
def __init__(self, master):
self.master = master
master.title("PyChat")
master.geometry("800x500")
master.configure(bg="grey")
master.grid_columnconfigure((0,1,2,3), uniform="uniform", weight=1)
master.grid_rowconfigure(0, weight=1)
self.friends_space = Frame(master, bg="red")
self.chat_space = Frame(master, bg="blue")
self.friends_space.grid(row=0, column=0, sticky=NSEW)
self.chat_space.grid(row=0, column=1, columnspan=3, sticky=NSEW)
self.message_area = Text(self.chat_space, width=1, height=1)
self.message_area.pack(fill="both", expand=True)
root = Tk()
my_gui = app(root)
root.mainloop()
Hi there (this is my first question)
I am building an app with Tkinter as the GUI. I want multiple frames to expand to fill out the entire root window.
With the code below, I expected the bottom (green) frame to expand all the way up to the top (cyan) frame. Instead, it stays at the bottom, and there is a "frame-less" white area between the two frames.
screenshot of an actual result when code is run
This is the code, I am executing (methods that do not mess with frame layout has been shortened out):
class CreateWindow:
def __init__(self, master, screen):
self.master = master
self.master.geometry('300x400')
self.master.title("THE PROGRAM")
self.screen = screen
self.menu_bar = Menu(self.master)
self.setup_menu = Menu(self.menu_bar)
self.setup_bar()
self.main_menu = Menu(self.menu_bar)
self.main_bar()
self.diary_menu = Menu(self.menu_bar)
self.diary_bar()
self.master.config(menu=self.menu_bar)
# self.master.grid_columnconfigure(0, weight=1) # What is difference between these two and the two below?
# self.master.grid_rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
self.master.rowconfigure(0, weight=1)
self.top_menu(self.master) # TODO: Make this menu actively do stuff
if self.screen == "setup":
setup = SetupScreen(self.master)
elif self.screen == "main":
setup = MainScreen(self.master)
elif self.screen == "diary":
setup = DiaryScreen(self.master)
else:
raise TypeError("wrong screen")
def setup_bar(self): ...
def main_bar(self): ...
def diary_bar(self): ...
def top_menu(self, window): # Defines top frame : placeholder for future menu
top = tk.Frame(window, bg='cyan', pady=5)
top.grid(row=0, sticky='new')
button = tk.Button(top, text="Setup", command=self.do_nothing)
button.grid(row=0, column=0)
button = tk.Button(top, text="Main", command=self.do_nothing)
button.grid(row=0, column=1)
button = tk.Button(top, text="Diary", command=self.do_nothing)
button.grid(row=0, column=2)
top.columnconfigure(0, weight=1)
top.columnconfigure(1, weight=1)
top.columnconfigure(2, weight=1)
def do_nothing(self): ...
def b_exit(self): ...
"""This class contains methods, that create and manage the setup screen.
I want the green frame to expand all the way up to the cyan (top menu) """
class SetupScreen(CreateWindow):
def __init__(self, master):
self.master = master
self.menu = tk.Frame(self.master, bg='green')
self.menu.grid(row=1, sticky='new')
self.menu.columnconfigure(0, weight=1) # Again, what is difference between 'grid_'or not?
self.menu.grid_rowconfigure(1, weight=1) #I have tried setting index to both 0 and 1, no difference
self.create_buttons()
def create_buttons(self): ...
def personal_details(self): ...
def start_new(self):
pass
if __name__ == "__main__":
files = FileHandler() #Class meant to be handling file operations - currently only sets a boolean to false, that makes the app start with setup screen
ap = files.active_program
print(ap)
root = tk.Tk()
if not files.active_program: #based on the boolean from FileHandler class, this starts the setup screen
top_menu = CreateWindow(root, "setup")
else:
top_menu = CreateWindow(root, "main")
root.mainloop()
It looks like you're trying to create a notebook widget with several tabs.
So I would suggest you use ttk.Notebook instead of re-inventing it yourself.
I am using Python to parse entries from a log file, and display the entry contents using Tkinter and so far it's been excellent. The output is a grid of label widgets, but sometimes there are more rows than can be displayed on the screen. I'd like to add a scrollbar, which looks like it should be very easy, but I can't figure it out.
The documentation implies that only the List, Textbox, Canvas and Entry widgets support the scrollbar interface. None of these appear to be suitable for displaying a grid of widgets. It's possible to put arbitrary widgets in a Canvas widget, but you appear to have to use absolute co-ordinates, so I wouldn't be able to use the grid layout manager?
I've tried putting the widget grid into a Frame, but that doesn't seem to support the scrollbar interface, so this doesn't work:
mainframe = Frame(root, yscrollcommand=scrollbar.set)
Can anyone suggest a way round this limitation? I'd hate to have to rewrite in PyQt and increase my executable image size by so much, just to add a scrollbar!
Overview
You can only associate scrollbars with a few widgets, and the root widget and Frame aren't part of that group of widgets.
There are at least a couple of ways to do this. If you need a simple vertical or horizontal group of widgets, you can use a text widget and the window_create method to add widgets. This method is simple, but doesn't allow for a complex layout of the widgets.
A more common general-purpose solution is to create a canvas widget and associate the scrollbars with that widget. Then, into that canvas embed the frame that contains your label widgets. Determine the width/height of the frame and feed that into the canvas scrollregion option so that the scrollregion exactly matches the size of the frame.
Why put the widgets in a frame rather than directly in the canvas? A scrollbar attached to a canvas can only scroll items created with one of the create_ methods. You cannot scroll items added to a canvas with pack, place, or grid. By using a frame, you can use those methods inside the frame, and then call create_window once for the frame.
Drawing the text items directly on the canvas isn't very hard, so you might want to reconsider that approach if the frame-embedded-in-a-canvas solution seems too complex. Since you're creating a grid, the coordinates of each text item is going to be very easy to compute, especially if each row is the same height (which it probably is if you're using a single font).
For drawing directly on the canvas, just figure out the line height of the font you're using (and there are commands for that). Then, each y coordinate is row*(lineheight+spacing). The x coordinate will be a fixed number based on the widest item in each column. If you give everything a tag for the column it is in, you can adjust the x coordinate and width of all items in a column with a single command.
Object-oriented solution
Here's an example of the frame-embedded-in-canvas solution, using an object-oriented approach:
import tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.create_window((4,4), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.bind("<Configure>", self.onFrameConfigure)
self.populate()
def populate(self):
'''Put in some fake data'''
for row in range(100):
tk.Label(self.frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(self.frame, text=t).grid(row=row, column=1)
def onFrameConfigure(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
root=tk.Tk()
example = Example(root)
example.pack(side="top", fill="both", expand=True)
root.mainloop()
Procedural solution
Here is a solution that doesn't use a class:
import tkinter as tk
def populate(frame):
'''Put in some fake data'''
for row in range(100):
tk.Label(frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(frame, text=t).grid(row=row, column=1)
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
frame = tk.Frame(canvas, background="#ffffff")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw")
frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
populate(frame)
root.mainloop()
Make it scrollable
Use this handy class to make the frame containing your widgets scrollable. Follow these steps:
create the frame
display it (pack, grid, etc)
make it scrollable
add widgets inside it
call the update() method
import tkinter as tk
from tkinter import ttk
class Scrollable(tk.Frame):
"""
Make a frame scrollable with scrollbar on the right.
After adding or removing widgets to the scrollable frame,
call the update() method to refresh the scrollable area.
"""
def __init__(self, frame, width=16):
scrollbar = tk.Scrollbar(frame, width=width)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)
self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.canvas.yview)
self.canvas.bind('<Configure>', self.__fill_canvas)
# base class initialization
tk.Frame.__init__(self, frame)
# assign this obj (the inner frame) to the windows item of the canvas
self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)
def __fill_canvas(self, event):
"Enlarge the windows item to the canvas width"
canvas_width = event.width
self.canvas.itemconfig(self.windows_item, width = canvas_width)
def update(self):
"Update the canvas and the scrollregion"
self.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))
Usage example
root = tk.Tk()
header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack()
body.pack()
footer.pack()
ttk.Label(header, text="The header").pack()
ttk.Label(footer, text="The Footer").pack()
scrollable_body = Scrollable(body, width=32)
for i in range(30):
ttk.Button(scrollable_body, text="I'm a button in the scrollable frame").grid()
scrollable_body.update()
root.mainloop()
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and can be used to replace a standard tk.Frame.
import tkinter as tk
class ScrollbarFrame(tk.Frame):
"""
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and
can be used to replace a standard tk.Frame
"""
def __init__(self, parent, **kwargs):
tk.Frame.__init__(self, parent, **kwargs)
# The Scrollbar, layout to the right
vsb = tk.Scrollbar(self, orient="vertical")
vsb.pack(side="right", fill="y")
# The Canvas which supports the Scrollbar Interface, layout to the left
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.canvas.pack(side="left", fill="both", expand=True)
# Bind the Scrollbar to the self.canvas Scrollbar Interface
self.canvas.configure(yscrollcommand=vsb.set)
vsb.configure(command=self.canvas.yview)
# The Frame to be scrolled, layout into the canvas
# All widgets to be scrolled have to use this Frame as parent
self.scrolled_frame = tk.Frame(self.canvas, background=self.canvas.cget('bg'))
self.canvas.create_window((4, 4), window=self.scrolled_frame, anchor="nw")
# Configures the scrollregion of the Canvas dynamically
self.scrolled_frame.bind("<Configure>", self.on_configure)
def on_configure(self, event):
"""Set the scroll region to encompass the scrolled frame"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
Usage:
class App(tk.Tk):
def __init__(self):
super().__init__()
sbf = ScrollbarFrame(self)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
sbf.grid(row=0, column=0, sticky='nsew')
# sbf.pack(side="top", fill="both", expand=True)
# Some data, layout into the sbf.scrolled_frame
frame = sbf.scrolled_frame
for row in range(50):
text = "%s" % row
tk.Label(frame, text=text,
width=3, borderwidth="1", relief="solid") \
.grid(row=row, column=0)
text = "this is the second column for row %s" % row
tk.Label(frame, text=text,
background=sbf.scrolled_frame.cget('bg')) \
.grid(row=row, column=1)
if __name__ == "__main__":
App().mainloop()
I use a Tkinter frame in python, and on one of the pages I need to display 2 sets of entries, one next to the other, where the number of entries is equal to the number in range (in the full program is a changing variable, so each time the number of entries changes). I use the for loop to do it.
However, when I try to pack the entries into the frame, the 2 sets of 3 entries are shown in one single column of 6 entries, instead of showing 2 columns with 3 rows of entries each.
If I adjust the packs to the left and right sides of the frame, each set of entries then is shown in 1 row, and has 3 columns, which is not needed.
When I use .place or .grid instead of .pack, then for each set only one single entry is shown (I guess all 3 entries are just placed in a single defined location ex. (x = 550, y = 80), so that 3 entries "overlap" into one)
I guess I need to write a more sophisticated "for loop" function and use .grid or .place positioning, so that all 3 entries will be displayed in a column one after the other.
Or I'm also thinking that using .pack and inserting the entries into a new frame inside the first frame, and then position these 2 frames one next to another might work. But again, I tried to create an extra frame inside the first page, and it didn't work.
Any observations and tips would be highly appreciated!
Here is the full code, so you might try playing around with it and see the whole picture. (sorry for a mess in imports, I also have to sort it out)
P.S. It is a part of a bigger code, where I need to use more then 1 page, so this code is the smallest that works - if I there would be only a single frame in the program, I would have no problem arranging the entries as I need. The problem is that I don't know how to arrange the entries is this particular structure.
import tkinter as tk
from tkinter import font as tkfont
import traceback
from tkinter import messagebox
from pandastable.core import Table
from pandastable.data import TableModel
from tkinter import *
from tkinter import ttk
import tkinter as Tkinter
import tkinter.ttk as ttk
LARGE_FONT= ("Verdana", 12)
class MyTable(Table):
"""
Custom table class inherits from Table.
You can then override required methods
"""
def __init__(self, parent=None, **kwargs):
Table.__init__(self, parent, **kwargs)
return
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
# the container is where we'll stack a bunch of frames
# on top of each other, then the one we want visible
# will be raised above the others
self.geometry('800x600+200+100')
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartPage, PageTwo):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
# put all of the pages in the same location;
# the one on the top of the stacking order
# will be the one that is visible.
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("StartPage")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is the start page", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button2 = tk.Button(self, text="Go to Page Two",
command=lambda: controller.show_frame("PageTwo"))
button2.place(x = 20, y = 50)
entries = [Entry(self, font =('Calibri', 7 )) for _ in range(3)]
for entry in entries:
#entry.place(x = 400, y = 80)
entry.pack()
entries_date = [Entry(self, font =('Calibri', 7 )) for _ in range(3)]
for entry in entries_date:
#entry.place(x = 550, y = 80)
entry.pack()
class PageTwo(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 2", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=lambda: controller.show_frame("StartPage"))
button.pack()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
If you're trying to create rows and columns, there are two generally accepted ways to do it:
make each row a frame, and pack the entries along a side
use grid to create rows and columns.
The first is best if your columns don't need to line up, the second is best if your columns do need to line up. Though, you also have to take into consideration what else you're putting in the frame -- you can't use both grid and pack on widgets that have the same master.
Note: If you're creating identical widgets in each row, then as a side effect the columns will line up even when you use pack.
Example using pack:
entries = []
entry_frame = Frame(self)
entry_frame.pack(side="top", fill="x")
for column in range(3):
entry = Entry(entry_frame, font=('Calibri', 7))
entry.pack(side="left", fill="both", expand=True)
entries.append(entry)
entries_date = []
entry_date_frame = Frame(self)
entry_date_frame.pack(side="top", fill="x")
for column in range(3):
entry = Entry(entry_date_frame, font=('Calibri', 7))
entry.pack(side="left", fill="both", expand=True)
entries_date.append(entry)
Example using grid:
entries = []
for column in range(3):
entry = Entry(self, font=('Calibri', 7))
entry.grid(row=0, column=column, sticky="ew")
entries.append(entry)
entries_date = []
for column in range(3):
entry = Entry(self, font=('Calibri', 7))
entry.grid(row=1, column=column, sticky="ew")
entries_date.append(entry)
I have read a number of threads and other resources to try to find the correct way to handle this but I have not found anything that works with my application.
Here is what I am trying to accomplish.
When a query is completed and the insert of the data to a Listbox is done I cannot seem to get it to margin the data insert by 1 character space.
I am using pack() and I have read the tkinter manual for this and have tried each example available along with others found on various threads here.
The widget:
output = tkinter.Listbox(window_2, height = 20, font='Times 10',
width=42, bd=1, bg = '#FFD599', fg = '#9A0615', selectmode=SINGLE)
output.pack()
output.place(x=210, y=195)
I have tried padx and pady with pack() without success, although this works successfully with the Text widget. I have also attempted to use a few alternatives that I have found here on the site but all without success in margining the Listbox when the data is inserted.
Any advice?
pack's padx/pady and ipadx/ipady options don't affect the data that is inside the listbox. The listbox itself doesn't have any options to add an internal margin.
To get a margin around the inside of the listbox, what I normally do is give it a zero borderwidth and highlightthickness, and then place it in a frame with the same background color and let the frame be the border. You can then add any padding you want between the border and the listbox.
This is also convenient because you can put a scrollbar inside the frame, giving it the appearance that it is inside the listbox without actually being inside the listbox.
Example:
import tkinter as tk
root = tk.Tk()
root.configure(background="gray")
listbox_border = tk.Frame(root, bd=1, relief="sunken", background="white")
listbox_border.pack(padx=10, pady=10, fill=None, expand=False)
listbox = tk.Listbox(listbox_border, width=20, height=10,
borderwidth=0, highlightthickness=0,
background=listbox_border.cget("background"),
)
vsb = tk.Scrollbar(listbox_border, orient="vertical", command=listbox.yview)
listbox.configure(yscrollcommand=vsb)
vsb.pack(side="right", fill="y")
listbox.pack(padx=10, pady=10, fill="both", expand=True)
for i in range(100):
listbox.insert("end", "Item #{}".format(i))
root.mainloop()
here is a variation on the much appreciated answer by Bryan Oakley.
it uses ttk widgets instead of tk widgets
the scrollbar tracks your position in the list box when you scroll with the mouse
uses the oStyle.theme_use("clam") because it may look more modern...this can be commented out
'
import tkinter as tk
from tkinter import ttk
try: # allows the text to be more crisp on a high dpi display
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
root = tk.Tk()
oStyle = ttk.Style()
oStyle.theme_use("clam")
oStyle.configure('LB.TFrame', bd=1, relief="sunken", background="white")
listbox_border = ttk.Frame(root, style='LB.TFrame')
listbox_border.pack(padx=4, pady=4, fill=None, expand=False)
vsb = ttk.Scrollbar(listbox_border)
vsb.pack(side="right", fill="y")
listbox = tk.Listbox(listbox_border, width=20, height=10, borderwidth=0,
highlightthickness=0, selectmode=tk.SINGLE,
activestyle=tk.NONE)
listbox.pack(padx=6, pady=6, fill="y", expand=True)
listbox.config(yscrollcommand=vsb.set)
vsb.config(command=listbox.yview)
for i in range(100):
listbox.insert("end", "Item #{}".format(i))
root.mainloop()
'
first of all to format chars in a tkinter listbox you need to use a fixed font and .format python funcion....;
So you can do something this
Press Load to load data in the listbox and pay attention to this line code
s = '{0:>8}{1:5}'.format(i[0],i[1])
self.list.insert(tk.END, s)
import tkinter as tk
RS = (('Apple',10),
('Banana',20),
('Peack',8),
('Lemon',6),)
class App(tk.Frame):
def __init__(self,):
super().__init__()
self.master.title("Hello World")
self.init_ui()
def init_ui(self):
self.pack(fill=tk.BOTH, expand=1,)
f = tk.Frame()
sb = tk.Scrollbar(f,orient=tk.VERTICAL)
self.list = tk.Listbox(f,
relief=tk.GROOVE,
selectmode=tk.BROWSE,
exportselection=0,
background = 'white',
font='TkFixedFont',
yscrollcommand=sb.set,)
sb.config(command=self.list.yview)
self.list.pack(side=tk.LEFT,fill=tk.BOTH, expand =1)
sb.pack(fill=tk.Y, expand=1)
w = tk.Frame()
tk.Button(w, text="Load", command=self.on_callback).pack()
tk.Button(w, text="Close", command=self.on_close).pack()
f.pack(side=tk.LEFT, fill=tk.BOTH, expand=0)
w.pack(side=tk.RIGHT, fill=tk.BOTH, expand=0)
def on_callback(self,):
for i in RS:
s = '{0:>8}{1:5}'.format(i[0],i[1])
self.list.insert(tk.END, s)
def on_close(self):
self.master.destroy()
if __name__ == '__main__':
app = App()
app.mainloop()