TKinter/TTK: Multiple elements in a clickable area - python

I was curious if it is possible to create an clickable element that contains multiple elements?
ttk.Button appears to take text or an image.
I would like to have a clickable element that will have 8 text items and 2 images inside it. Clicking anywhere in that element will trigger the same backend method.
Any code examples would be helpful as still wading through TKinter -- only my second project to use it.

Use bindtags and give the same tag for all widgets that you want to be clickable, then use bind_class to bind all the widgets.
Here's an example
import tkinter as tk
def clicked(event):
print("Clicked !")
class ClickableElement(tk.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.bindtags('Click') # or pass a list if you want to have multiple tag names
for x in range(8):
lbl = tk.Label(self, text=f"Text {x}")
lbl.bindtags('Click')
lbl.pack()
root = tk.Tk()
root.geometry("200x200")
frame = ClickableElement(root) # or tk.Frame(root, class_='Click')
frame.pack(fill='both', expand=True)
frame2 = tk.Frame(root, bg='red')
frame2.pack(expand=True, fill='both')
root.bind_class('Click', "<Button-1>", clicked)
root.mainloop()
The above example will make both text and Frame clickable. You can expand on this to include images. Alternatively, You can use bind on each widget inside the frame.

What you can do is get the children of the tk.Frame and bind them to a function.
from tkinter import *
from tkinter import messagebox
class ClickFrame:
def __init__(self,root):
self.root = root
self.root.geometry("700x500")
Label(self.root,text="Clickable Frame!",font=("arial",15)).pack(fill=X,side=TOP)
self.Frame1 = Frame(self.root,bg="light grey")
self.Frame1.pack(fill=BOTH,expand=1,padx=100,pady=100)
for i in range(8):
Label(self.Frame1,text=f"This is Label {i}").pack(pady=5,anchor="w",padx=5)
self.Frame1.bind("<Button-1>",self.detect_click)
for wid in self.Frame1.winfo_children():
wid.bind("<Button-1>",self.detect_click)
def detect_click(self,event,*args):
messagebox.showerror("Clicked",f"Clicked the widget {event.widget}")
print(event.widget,type(event.widget))
root=Tk()
ob=ClickFrame(root)
root.mainloop()
You also can use bind_all() to bind . However, this will bind everything in the window.
from tkinter import *
from tkinter import messagebox
class ClickFrame:
def __init__(self,root):
self.root = root
self.root.geometry("700x500")
Label(self.root,text="Clickable Frame!",font=("arial",15)).pack(fill=X,side=TOP)
self.Frame1 = Frame(self.root,bg="light grey")
self.Frame1.pack(fill=BOTH,expand=1,padx=100,pady=100)
for i in range(8):
Label(self.Frame1,text=f"This is Labe {i}").pack(pady=5,anchor="w",padx=5)
self.Frame1.bind_all("<Button-1>",self.detect_click)
def detect_click(self,event,*args):
messagebox.showerror("Clicked",f"Clicked the widget {event.widget}")
root=Tk()
ob=ClickFrame(root)
root.mainloop()

Related

How to add padding before element name in tkinter listbox or remove underline under selected item?

When inserting elements in listbox I want to add a little padding to their right. For example:
import tkinter as tk
from tkinter import *
root = Tk()
listbox = Listbox(root,width = 50)
listbox.pack(side=LEFT,fill=Y)
for ele in range(1,10):
listbox.insert(END,str(ele))
root.mainloop()
Gives:
I want something like this:
import tkinter as tk
from tkinter import *
root = Tk()
listbox = Listbox(root,width = 50)
listbox.pack(side=LEFT,fill=Y)
for ele in range(1,10):
listbox.insert(END," "+str(ele))
root.mainloop()
Now, I am looking for an in-build way of doing this because I need add extra code to remove those spaces when I do listbox.get(listbox.curselection()). Or for a way to remove the underline under the selected item so that the user can't see my dirty quick fix.
Place your Listbox in a Frame with ipadx.
LiveDemo: reply.it
Reference
The Tkinter Pack Geometry Manager - options
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.geometry('600x300+50+50')
self.configure(bg='blue')
frame = tk.Frame(self, bg='white')
frame.pack(side=tk.LEFT, ipadx=10)
listbox = tk.Listbox(frame, borderwidth=0, highlightthickness=0, bg=frame.cget('bg'), width=50)
listbox.pack(fill=tk.Y)
for ele in range(1,10):
listbox.insert(tk.END, str(ele))
if __name__ == '__main__':
App().mainloop()

Accessing child widgets of a window by their names?

Is there any way in Python/tkinter to access child elements referring by their variable names, but from an other function?
For example in VBA, it is possible to directly refer to an element of an other window by its name.
For example if I have two windows, UserForm1 and UserForm2 I can change the text value of Label1 of UserForm2 by clicking a button on UserForm1.
Private Sub CommandButton1_Click()
UserForm2.Label1.Caption = "Changed"
End Sub
In tkinter I have found the winfo_children() to access child elements. Is there any way to access them by their names?
See my sample code below:
import tkinter
from tkinter import *
#----------------------------------------------------------------------
def new(parent_window):
""""""
parent_window.withdraw()
global main_window
global new_window
new_window = tkinter.Tk()
new_window.title("My App - New")
label1 = tkinter.Label(new_window, text="NEW")
label1.grid(row=0,column=0,columnspan=3,pady=10,padx=10, sticky="nsw")
b1 = tkinter.Button(new_window, text="Change It", command=lambda: showdashboard(new_window))
b1.grid(row=4,column=1,padx=20,pady=10,sticky="nwse")
b2 = tkinter.Button(new_window, text="Quit", command=lambda: quit())
b2.grid(row=5,column=1,padx=20,pady=10,sticky="nwse")
#----------------------------------------------------------------------
def dashboard(parent_window):
""""""
parent_window.withdraw()
global main_window
global dashboard_window
dashboard_window = tkinter.Tk()
dashboard_window.title("My App - Dashboard")
label1 = tkinter.Label(dashboard_window, text="Dashboard")
label1.grid(row=0,column=0,columnspan=3,pady=10,padx=10, sticky="nsw")
b1 = tkinter.Button(dashboard_window, text="New", command=lambda: new(dashboard_window))
b1.grid(row=4,column=1,padx=20,pady=10,sticky="nwse")
#----------------------------------------------------------------------
def showdashboard(parent_window):
""""""
parent_window.withdraw()
dashboard_window.update()
dashboard_window.deiconify()
#This way it works <<<<<<<<<<<<<<------!!!!!!!
byID=dashboard_window.winfo_children()
byID[0].config(text="change the value")
#But I am looking for somethin like this <<<<<<<<<<<<<<------????
dashboard_window.label1.config(text="changed the value")
#----------------------------------------------------------------------
main_window=tkinter.Tk()
main_window.title("MyApp")
label = tkinter.Label(main_window, text="My App")
label.grid(row=0,column=0,pady=10,padx=10,sticky="nwse")
b1 = tkinter.Button(main_window, text="Dashboard", command=lambda:dashboard(main_window))
b1.grid(row=1,column=0,padx=20,pady=10,sticky="nwse")
main_window.mainloop()
winfo_children() returns an instance of the class associated with the type of widget along with the name that tkinter assigned to the actual tk object.
This means that yes, we can refer to the name of widget, although I'm not sure what advantage this would really give you other than not needing to assign the label to a variable.
from tkinter import *
root = Tk()
Label(root, text="Label1").pack()
label2 = Label(root, name="name", text="Label2")
label2.pack()
print(root.winfo_children())
print(root.nametowidget('.!label'))
print(str(label2))
Button(root, text="Delete label2", command=lambda: root.nametowidget(".name").destroy()).pack()
The above will result in two Label widgets and a Button widget appearing in the window. The first Label is not stored in a variable and yet we can quite happily call it inside the print statement. The second is stored in a variable but you can see that in the command of the Button we don't refer to the variable but the name attribute of the Label.
Bryan Oakley has a fantastic answer here explaining this.
I'm not sure what you mean by names in:
"Is there any way in Python/tkinter to access child elements referring by their names?"
You can access widgets simply by their object references:
# Procedural
import tkinter as tk
def change():
object_reference['text'] = "Changed!"
root = tk.Tk()
object_reference = tk.Label(root, text="This is a label for root-window")
object_reference.pack()
another_window = tk.Toplevel(root)
btn_in_other_window = tk.Button(another_window, text="Change")
btn_in_other_window['command'] = change
btn_in_other_window.pack()
root.mainloop()
or if they were to be defined with more of an object-oriented approach, you can make use of the .(dot) notation:
#oop
import tkinter as tk
class App1(tk.Toplevel):
def __init__(self, master):
super().__init__()
self.label = tk.Label(self, text="This is App1's label")
self.label.pack()
class App2(tk.Toplevel):
def __init__(self, master):
super().__init__(master)
self.button = tk.Button(self, text="This is a App2's button")
self.button.pack()
def change(label):
label['text'] = "Changed!"
if __name__ == '__main__':
root = tk.Tk()
root.withdraw()
app1 = App1(root)
app2 = App2(root)
app2.button['command'] = lambda label=app1.label: change(label)
root.mainloop()
As an alternative, it is possible to get a list of the children widgets of the element window with:
root.update()
lista = list(root.children.values())
Then it is possible to refer to the list elements and do whatever with the widget itself. For example to get the width of the first widget do print(lista[0].winfo_width()).
Warning: I am not sure that the list contains the elements in the same order that the widgets appear in the script, although for me it worked in this order. Hopping someone will write in the comments.

Python tkinter 2 frames merge together

Hey guys I have to classes that both create a Frame. The first one contains a button that is supposed to close its frame. The second frame simply contains a Label. My code should first create the frame with the button and when the button is pressed the second window should show up. What happens is that when pressing the button a "merged" window is created that contains the button and the label.
import tkinter as tk
class Window1(tk.Frame):
def __init__(self):
tk.Frame.__init__(self)
self.grid()
self.btn = tk.Button(self,text = "button",command = self.run)
self.btn.grid(row = 0,column = 0)
def run(self):
tk.Frame.quit(self)
class Window2(tk.Frame):
def __init__(self):
tk.Frame.__init__(self)
self.grid()
self.label = tk.Label(self,text = "label ")
self.label.grid(row = 0,column = 0)
w = Window1()
w.mainloop()
v = Window2()
v.mainloop()
The first picture is before you press the button, the next one after you pressed the button. The problem seems that tk.Frame.quit(self) doesn't work correctly. I tried similar ways to close the window such as:
tk.Frame.destroy(self)
but that doesn't help either.
edit: I solved it by inheriting the class from tk.TK instead of tk.Frame
Frame doesn't create window - it only group elements. Tk() creates window.
To close window you have to destroy() object create by Tk(). But you don't creat it manually root = tk.Tk() so tkinter create it automatically, but you have no access to this root to close it.
If widget doesn't have parent then it uses root and your Frame does it too.
import tkinter as tk
class Window1(tk.Frame):
def __init__(self, master):
# send `root` to `Frame` as its parent
tk.Frame.__init__(self, master)
# `Frame` will keep `master as `self.master`
# so we don't have to do `self.master = master` manually
self.grid()
self.btn = tk.Button(self, text="Hello Button", command=self.run)
self.btn.grid(row=0, column=0)
def run(self):
# use `master` (`root`) to destroy it
self.master.destroy()
class Window2(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.grid()
self.label = tk.Label(self, text="Hello Label")
self.label.grid(row=0, column=0)
root = tk.Tk() # create main window as `root`
Window1(root) # send `root` to `Window1` and later to `Frame`
root.mainloop()
root = tk.Tk()
Window2(root)
root.mainloop()

Dependant widgets in tkinter

I have a small GUI app in Python that uses Tk. It has several filters --- a text entries (they set filter values) with checkboxes (which set filter on/off). I create filters as a class inhetrited from ttk's Labelframe:
from tkinter.ttk import Labelframe
import tkinter as tk
class FilterWidget(Labelframe):
def __init__(self, parent, label):
Labelframe.__init__(self, parent, text=label)
self.grid()
self._entry = tk.Entry(self)
self._entry.grid(row=0, column=0)
self._checkbox = tk.Checkbutton(self, command=lambda: print(self))
self._checkbox.grid(row=0, column=1)
Then I create several instances of this class and place them in GUI:
root = tk.Tk()
source_filter = FilterWidget(root, "Source")
source_filter.grid(row=0, column=0)
level_filter = FilterWidget(root, "Severity")
level_filter.grid(row=0, column=1)
root.mainloop()
The widgets are created and correctly displayed. However, when one of the checkboxes is clicked and changes state, the other changes state as well!
When diffrent checkboxes are clicked, print command outputs .!filterwidget and .!filterwidget2, so those are separate objects. It seems that they are somehow implicitly syncronized, but I have no idea how did this happend.
So, the question is: how to remove this dependancy and make checkboxes independant of each other?
As the docs mention,
To use a Checkbutton, you must create a Tkinter variable. To inspect
the button state, query the variable.
Here's your code updated to use an IntVar to store the Checkbutton state.
from tkinter.ttk import Labelframe
import tkinter as tk
class FilterWidget(Labelframe):
def __init__(self, parent, label):
Labelframe.__init__(self, parent, text=label)
self._entry = tk.Entry(self)
self._entry.grid(row=0, column=0)
self._var = tk.IntVar()
callback = lambda: print(self._entry.get(), self._var.get())
checkbox = tk.Checkbutton(self, variable=self._var, command=callback)
checkbox.grid(row=0, column=1)
root = tk.Tk()
source_filter = FilterWidget(root, "Source")
source_filter.grid(row=0, column=0)
level_filter = FilterWidget(root, "Severity")
level_filter.grid(row=0, column=1)
root.mainloop()

Tkinter Scrollbar in frame with multiple textframes

Question
How can I have a scrollbar which moves an entire Tkinter frame? Note: I am using Python 2.7.3.
Code and Explanation
I have this code for defining the scrollbar
scrollbar = Scrollbar(soeg)
scrollbar.pack(side=RIGHT, fill="y")
And this code for defining the textframes
h = 0
s = 0
for i in dom_nodup:
abc = dom_nodup[h]
text = Text(soeg, bg="brown", fg="white", height="10", width="60")
text.insert(INSERT, "%s \n" % abc[0])
text.insert(END, "%s \n\n\n" % abc[1])
text.pack()
h += 1
s += 1
A new textframe is created for each text entity for later easier overview (planning on having a button to show/hide the input).
The scrollbar is present but is not functional
I would recommend that you use the ScrolledText widget. It automatically adds a scrollbar to each text widget, and has the same arguments as Text. Here is a brief example of how to do it.
from Tkinter import * #Import the Tkinter module
from ScrolledText import ScrolledText #import the scrolled text module
message = "I \n am \n scroll \n able. \n\n\n\n\n\n Yes I am!"
class Application(Frame): #Create a frame for the widgets
def __init__(self, master): #initialize the grid and widgets
Frame.__init__(self,master)
self.grid()
self.widgets()
def widgets(self):
self.mytext = ScrolledText(self, width = 10) #Creates the widget
self.mytext.grid() #Places it
root = Tk()
root.title("My Text Example")
#make my screen dimensions work
root.geometry("500x1000")
app = Application(root)
root.mainloop()
For more information, please see the Tkinterbook and this question.
To make a scrollbar functional you must do two things: you must tell it which scrollable widget to scroll, and you must tell the scrollable widget which scrollbar to update with the current position.
scrollbar.configure(command=text.yview)
text.configure(yscrollcommand=scrollbar.set)

Categories

Resources