So I've been trying to make some basic GUIs with tkinter (not te be confused with Tkinter) and I ran into a problem for which I know no solution and can't really find anything on the almighty Google...
I have a small SQLite database with a table of directories on my pc. I would like to draw all directorypaths into a label and add a 'rempve' button next to that label. The button should be able to remove directory from the database and also remove it from the GUI. I also have a 'add' button where one can add directories to the database and this new directory should be shown in the GUI. This is my basic layout:
---------------
| ADD |
|dir1 REMOVE|
|dir2 REMOVE|
---------------
I use the gridlayout to show the buttons and labels. Most things work, all database related stuff works. Also when starting the GUI the current directories and 'remove'-buttons are shown nicely. BUT... when using the 'remove' button the directory does not disappear from the GUI even though it is not in the database anymore, restarting the GUI fixes it of course. Adding a label works... but I'm not sure if I'm doing it correctly...
How can I somehow 'repaint' the GUI with the new information?
This is my code for the GUI:
class GUI():
def __init__(self,db):
self.root = Tk()
self.root.title("Example")
self.frame = ttk.Frame(self.root, padding="3 3 12 12")
self.frame.rowconfigure(5, weight=1)
self.frame.columnconfigure(5, weight=1)
self.frame.grid(sticky=W+E+N+S)
lbl = ttk.Label(self.frame, text="", width=17)
lbl.grid(row=0, column=2, sticky=W)
ttk.Button(self.frame, text="Add directory", command=lambda:self.load_file(db), width=30).grid(row=0, column=0, sticky=W, padx=(500,50))
ttk.Button(self.frame, text="Sort files", command=lambda:self.sort(db,lbl), width=17).grid(row=0, column=1, sticky=W)
self.draw(db)
self.root.mainloop()
def load_file(self,db):
fname = filedialog.askdirectory()
db.addPath(fname)
self.draw(db)
def remove_dir(self,db,pid):
db.removePath(pid)
self.draw(db)
def sort(self,db,lbl):
lbl['text'] = 'Sorting...'
sortFiles.moveFiles(db)
lbl['text'] = 'Done!'
def draw(self,db):
i = 0
paths = db.getPaths()
for path in paths:
ttk.Label(self.frame,text=path[1]).grid(row=1+i,column=0,sticky=W)
ttk.Button(self.frame, text="Remove directory", command=lambda:self.remove_dir(db,path[0]), width=17).grid(row=1+i,column=1, sticky=E)
i = i+1
for child in self.frame.winfo_children(): child.grid_configure(padx=5, pady=5)
if i == 0:
ttk.Label(self.root,text='No directories added yet').grid(row=1,column=0,sticky=W)
If you prefer to redraw the GUI every time you add or delete something, you need to first destroy any old widgets before creating new ones. For example:
def draw(self, db):
# first, delete any existing widgets
for child in self.frame.winfo_children():
child.destroy()
# next, redraw all the widgets
paths = db.getPaths()
for path in paths:
...
You have another bug, which is how you're using lambda. As it stands with the code in the question, all of your callbacks will see the same value. By specifying the value as a keyword argument to the lambda you'll get the right value:
ttk.Button(..., command=lambda p=path[0]:self.remove_dir(db, p)...)
Unrelated to the actual problem, I don't think you need to be passing db around. Assuming you only use a single db, I recommend you do self.db = db in your GUI constructor. That will make your code just a little easier to maintain because your method signatures will be simplified.
Finally, there's really no need to completely redraw the GUI when you delete one item. You can delete just one label and button at a time. This requires that you spend a little more time thinking about how you manage data in your program. If, for example, you keep a reference to each label and button, you can delete it when you delete the path from the database. Your removeDir function might look something like:
def removeDir(self, pid):
label, button = self.widgets(pid)
label.destroy()
button.destroy()
Related
I have an app that I would like to restyle using Customtkinter. I originally used Tkinter.
However I have a ListBox widget connected to a scrollbar and when I run the code it says that Customtkinter does not have CTkListBox as attribute, or that ListBox is not defined.
I have tried o look for the issue online and it seems that other people have asked to the person who created he module if he could add ListBox widgets (the link below).
https://github.com/TomSchimansky/CustomTkinter/issues/69
He did answer, but as I am new to coding, I do not feel very confident with the code attached.
I hope someone can help.
Thank you
This is my original code:
# create listbox and put it on screen
list1 = Listbox(window, height=6, width=35)
list1.grid(row=2, column=0, rowspan=6, columnspan=2)
#create scrollbar and put it on screen
sb1 = Scrollbar(window)
sb1.grid(row=2, column=2, rowspan=6)
# apply configure methods to make list and scrollbar work together
list1.configure(yscrollcommand = sb1.set)
sb1.configure(command=list1.yview)
# bind method takes two args: type of event and function.
list1.bind("<<ListboxSelect>>", get_selected_row)
The loop in the program created a number of buttons with the same name. How to destroy them?
For example:
for i in range(5):
global btn
btn=Button(text=name,command=startfile_)
btn.place(x=5,y=5)
def destroy_it():
btn.destroy()#Its destroying only 1
destroy_btn(text=name,command=destroy_it)
It strongly depends on your use case. By the way, I suggest you to ensure your code is reproducible as-is, so that we can adapt our answers to your specific case.
Here some ways to address your problem.
1. save all buttons in a list and iterate over the list to delete them
pros: very easy to use and understand
cons: you need to pass your list along the code to work on it
import tkinter as tk
root = tk.Tk()
buttons = []
for i in range(5):
btn = tk.Button(root, text=f'test{i}', command=None) # TODO fill with your command
btn.pack()
buttons.append(btn)
def destroy_it(buttons):
# You must know the list of buttons to destroy
for btn in buttons.copy():
btn.destroy()
buttons.remove(btn) # also delete buttons from the list
tk.Button(root, text="Destroy all buttons", command=lambda: destroy_it(buttons)).pack()
root.mainloop()
2. destroy all widgets that satisfy a specific rule (i.e. Buttons with a specific text)
pros: you have a large flexibility on the widgets you are going to delete
cons: you may accidentally delete widgets, you must correctly deal with it
import tkinter as tk
root = tk.Tk()
for i in range(5):
btn = tk.Button(root, text=f'test{i}', command=None) # TODO fill with your command
btn.pack()
def destroy_it():
# Iterate over any widget and destroy it if it is a button with text "test....."
for child in root.winfo_children():
if isinstance(child, tk.Button) and child['text'].startswith('test'):
child.destroy()
tk.Button(root, text="Destroy all buttons", command=destroy_it).pack()
root.mainloop()
3. Give your buttons a name and address them by name
pros: easy to include in the code and to understand
cons: you must remember the names you used and you may get KeyError
import tkinter as tk
root = tk.Tk()
for i in range(5):
btn = tk.Button(root, name=f'btn{i}', text=f'test{i}', command=None) # TODO fill with your command
btn.pack()
def destroy_it():
# Get each button by its name
for i in range(5):
btn = root.nametowidget(f'.btn{i}')
btn.destroy()
tk.Button(root, text="Destroy all buttons", command=destroy_it).pack()
root.mainloop()
There are probably many other ways to achieve it, such as associating an "autodestroy" method to each button that is triggered by command, or include your buttons in a Frame and destroy the frame at once... But you may start from the examples above
I am currently trying to make a scrollable list of entries using tkinter in Python 3. While looking for some documentation, I found this: Adding a scrollbar to a group of widgets in Tkinter. It is really great and works really fine with Labels, but it doesn't seem to work with Entries. You'll find right here my code which creates a 2x25 list of entries through which I would like to be able to scroll:
import tkinter as tk
class MainPage(tk.Frame):
def __init__(self, racine):
super().__init__(master=racine)
self.grid()
self.entriesCanvas = tk.Canvas(self, borderwidth=0, background="white")
self.entriesFrame = tk.Frame(self.entriesCanvas, background="white")
self.scrollbar = tk.Scrollbar(self, command=self.entriesCanvas.yview)
self.entriesCanvas.configure(yscrollcommand=self.scrollbar.set)
self.entriesCanvas.grid(column=0, row=2, columnspan=2)
self.scrollbar.grid(column=3, row=2, sticky='ns')
# self.entriesFrame.grid()
self.entriesCanvas.create_window((0, 0), window=self.entriesFrame,
anchor='nw', tags='self.entriesFrame')
# self.entriesFrame.grid()
self.entriesCanvas.bind('<Configure>', self.onFrameConfigure)
self.entries = []
for i in range(50):
self.entries.append(tk.Entry(self.entriesFrame, font=('arial', 30)))
self.entries[i].grid(column=i % 2, row=i//2)
def onFrameConfigure(self, event):
self.entriesCanvas.configure(scrollregion=self.entriesCanvas.bbox("all"))
if __name__ == "__main__":
root = tk.Tk()
mainPage = MainPage(root)
root.mainloop()
Notice I commented two lines out. If you "activate" the first line, there will be a scrollbar and one can scroll through the entries, but it is strangely zoomed. On the other hand, if you "activate" the second line, the GUI will be as I would like it to be, but without the possibility to scroll, and it seems to show all entries (even if there are 1000 entries, therefore making a window which is 20 times the size of your screen).
Do you know where my mistake is?
Okay, so I found a way to have my program doing what I want. I just changed the method
def onFrameConfigure(self, event):
self.entriesCanvas.configure(scrollregion=self.entriesCanvas.bbox("all"))
by
def onFrameConfigure(self, event):
self.entriesCanvas.configure(scrollregion=self.entriesCanvas.bbox("all"), width=self.entriesFrame.winfo_width())
(I basically only added a parameter to ask the canvas to be the width of the frame.)
I don't know if it is perfect (as I still use .grid() istead of .pack()), but it works.
Thank you for giving me the source Tkinter: How to get frame in canvas window to expand to the size of the canvas?, it really helped me find where my mistake was.
I truely apologise for my English, I know I make a lot of mistakes.
I want to be able to also use the <Shift-Up> keys to select through the items in the treeview. No errors are given but the selection is late with one element. In my code, I am intending for the selection to be the same as the focus, but the selection ends up being on element down the list.
My guess is that the default binding for Arrow-Up key is being run after my binding.
I have searched for a virtual event to replace the <Up> in my binding - something that has a similar functionality to <<TreeviewSelect>> as opposed to <ButtonPress-1> - but with no luck.
Any idea how to synchronize the selection and focus when <Shift-Up> is pressed?
Note: the selectmode for the tree is set to none as in my main application I need the selection to be done slightly different than the default.
import tkinter as tk
from tkinter import ttk
class Treeview_Select:
def __init__(self, tree):
tree.bind('<Shift-Up>', self.ShiftUp, add='+')
def ShiftUp(self, event):
if event.widget.index(event.widget.focus()) is not '':
print(event.widget.index(event.widget.focus()))
event.widget.selection_set(event.widget.focus())
app = tk.Tk()
tree = ttk.Treeview(app)
v_scrollbar = ttk.Scrollbar(app, orient='vertical', command=tree.yview)
tree.config(selectmode='none', yscrollcommand=v_scrollbar.set)
tree.grid(row=0, column=0, sticky='nesw')
v_scrollbar.grid(row=0, column=1, sticky='nes')
Treeview_Select(tree)
for i in range(14):
tree.insert("", 'end', text=i)
app.mainloop()
You should use tree.prev(item) for this kind of action. If you want to implement a <Shift-Down>, use tree.next(item).
def ShiftUp(self, event):
if event.widget.index(event.widget.focus()) is not '':
previous = event.widget.prev(event.widget.focus()) #use current focus to get previous one
event.widget.selection_set(previous)
event.widget.see(previous) #make sure the new selection is shown
I'm trying to create new entry boxes when the "ADD entry" is used for my program. I do realize that the 'pack codes' works when I run it individually, but when I combine with existing program which is in grid(), the window is not showing when I run my program.
I also understand that we should not to use both .pack() when I have other things using .grid() in the same program. Hence, my question is, how do I create new entry boxes in grid.
I have tried finding elsewhere but they all suggested pack. For instance: I have looked here here, etc etc, to name a few; but couldn't find anything similar to mine. I would like to add entry boxes below the current entry boxes which is at row 3.
I'm kind of new to Python; (am using Python 2.7 and tkinter module in this program. Thank you very much for the help!
My simplified error codes are as follows:
from Tkinter import *
import tkFileDialog
import tkMessageBox
import Tkinter
import os
class Window:
def __init__(self, master):
self.filename3=""
csvfile=Label(root, text="NUMBERS").grid(row=3, column=0)
bar=Entry(master).grid(row=3, column=3)
self.entryText3 = StringVar()
self.bar = Entry(root, textvariable=self.entryText3).grid(row=3, column=3)
#BUTTONS
self.cbutton= Button(root, text="OK", command=self.process_csv)
self.cbutton.grid(row=15, column=6, sticky = W + E)
####PROBLEM FROM HERE#####
all_entries = []
addboxButton = Button(root, text='ADD', fg="BLUE", command=self.addBox)
addboxButton.pack()
#I have also tried replacing the last 2 lines with the following 2 lines instead but to no avail:
#self.addboxButton = Button(root, text='ADD THA-ID', fg="BLUE", command=self.addBox)
#self.addboxButton.grid(row=3, column=6)
frame_for_boxes = Frame(root)
frame_for_boxes.pack()
def addBox(self):
print "ADD"
next_row = len(all_entries)
lab = Label(frame_for_boxes, text=str(next_row+1))
lab.grid(row=next_row, column=0)
ent = Entry(frame_for_boxes)
ent.grid(row=next_row, column=0)
all_entries.append( ent )
def process_csv(self):
#own program here
print "program"
root = Tk()
window=Window(root)
root.mainloop()
There are few issues with your program other than the one you stated:
Inside the initializer (__init__()) you attached the widgets to root which is not defined within the scope of your Window class. The reasonable way to fix this issue is when you use an instance of Tk(), id est root id est master in Window class, make it as an instance variable. This means the first thing you have to do in the inititializer is this : self.master = master. This will result in you having to replace all root occurrences within __init__() by self.master
The second issue to fix is the one you specified in your question's title: you can not mix the grid() and pack() layout managers for the same widget container. You have to decide which one. Since you placed most of the widgets using grid(), I suggest you to get rid of pack(). This means:
addboxButton.pack() becomes, for example, addboxButton.grid(row=0, column=1)
frame_for_boxes.pack() becomes, for example, frame_for_boxes.grid(row=0, column=0)
The previous list item fixes the problem but it will make you discover other issues within your program which are related:
NameError: global name 'all_entries' is not defined
NameError: global name 'frame_for_boxes' is not defined
This is because those widget variables are not reachable within the scope of addBox() function. To resolve this issue, you have to make those 2 elements as instance variables. This means:
all_entries = [] becomes self.all_entries = []
frame_for_boxes = Frame(root) becomes self.frame_for_boxes = Frame(self.master) (remember we replaced root by self.master in 1.)
The consequence of this error fixing is that you have to use all over inside your program:
self.all_entries instead all_entries
self.frame_for_boxes instead of frame_for_boxes
For scalability reasons, I think you will have at least to make the rest of widgets as instance variables (i.d. prefix them with the self keyword)
As your real project is more complicated than what you show in this MCVE, I encourage you to adopt the SaYa idiom when creating and placing widget elements. This means you will need to replace:
csvfile=Label(root, text="NUMBERS").grid(row=3, column=0)
by
self.csvfile = Label(self.master, text="NUMBERS")
self.csvfile.grid(row=3, column=0)
To avoid unexpected bugs in your program, you must do the same for the remaining widgets you declared in the inititialzer.
There are also other things I would like to mention, but most of them are available on PEP8
What you have to do it to create a command which creates the entries and stores the new entries inside of a variable.
In my case, I use Entry_i and store in Entries but you can use self.Entries to make communication easier. (python 3.5)
def Make_Entry(self, root, Entries, x, y):
Entry_i = Entry(root, bd = 5)
Entry_i.grid(row = x, column = y, sticky = W+E+N+S)
Entries.append(Entry_i)
return(Entries, x+1, y+1)