communication between tkinter toplevels - python

I'm working on a little project and made a little on-screen keyboard as a tkinter Toplevel
my application is buildt like this:
Root-Window (Tk-Widget)
input 1 (Entry-Widget)
input 2 (Entry-Widget)
input 3 (Text-Widget)
on_screen-keyboard (Toplevel-Widget)
the Toplevel-Widget contains Buttons, with callbacks that should enter text in the entries (just like keyboard-Buttons)
What I want is a communication between children of the keyboard/the keyboard and the last active input-Widget. My Problem is, that I don't know, how to say the keyboard, to which input-Widget it should send the message.
import tkinter as tk
from tkinter import ttk
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.active_input = tk.Variable(value=None)
ttk.Button(self, text="show Keyboard", command=lambda: Keyboard(self)).pack()
self.text = tk.StringVar(value="")
self.input1 = ttk.Entry(self)
self.input1.bind("<FocusIn>", lambda e: self.active_input.set(self.input1))
self.input2 = ttk.Entry(self)
self.input2.bind("<FocusIn>", lambda e: self.active_input.set(self.input2))
self.input3 = tk.Text(self, height=3, width=15)
self.input3.bind("<FocusIn>", lambda e: self.active_input.set(self.input3))
self.input1.pack()
self.input3.pack()
self.input2.pack()
class Keyboard(tk.Toplevel):
OPENED = False
NAME = "- Keyboard -"
NUM = [{"text":"1", "width":1},
{"text":"2", "width":1},
{"text":"3", "width":2}]
CHAR= [{"text":"A", "width":1},
{"text":"B", "width":1},
{"text":"C", "width":2}]
def __init__(self, master):
if not Keyboard.OPENED:
Keyboard.OPENED = True
print("keyboard opened!")
self.master = master
tk.Toplevel.__init__(self, master)
self.title(self.NAME)
self.protocol("WM_DELETE_WINDOW", self.close)
self.keyb_nb = ttk.Notebook(self)
self.keyb_nb.pack()
self.num_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.num_tab, Keyboard.NUM,2)
self.keyb_nb.add(self.num_tab, text="123")
self.char_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.char_tab, Keyboard.CHAR, 2)
self.keyb_nb.add(self.char_tab, text="ABC")
def createPad(self, master, pad:list, max_col):
self.co_count = 0
self.ro = 1
for button in pad:
button["id"] = ttk.Button(master, width=6*button["width"], text=button["text"], command=self.bclicked(button))
if self.co_count >= max_col:
self.ro = self.ro + 1
self.co_count = 0
button["id"].grid(row=self.ro, columnspan=button["width"], column=self.co_count)
self.co_count = self.co_count+button["width"]
def bclicked(self, button:dict):
"""
reciver = self.master.active_input #I think the Problem here is, that the variable contains a string, not a widget
reciever.focus_force()
reciever.insert(index=tk.INSERT, string=button["text"])
"""
pass
def close(self):
Keyboard.OPENED = False
self.destroy()
print("keyboard closed!")
root = MainWindow()
root.mainloop()
Here the init of the Mainwindow and the bclicked of the Keyboard class are important...
the code is debug-ready
I would prefer a solution, similar to the communication in the internet (sender=button, receiver-id, message), but very welcome every working solution
btw: I'm also looking for a solution, how I don't have to force the input to focus and the Toplevel stays an the highest layer of the screen (that if I focus the Tk-Widget/one of the inputs/the button, the keyboard will stay in front of it)
SUMMARY: how do I find out, which of the 3 input-widgets was active at last, when the keyboard-toplevel has already the focus?

I may made more changes than needed, but mainly focus on the method keyboard_triger() and pass_key_to_master(), this two use the idea that the variable master implements, having access to call methods out of scope.
Olso the method set_focused_object() stores a reference to the last object beeng focused, note than it stores the widget and not the event, it's easyer than searching each time the object
import tkinter as tk
from tkinter import ttk
class MainWindow(tk.Tk):
def keyboard_triger(self, key):
# to identify wath object is just use
# isinstance(self.active_input, ttk.Entry)
self.active_input.insert(tk.END, key)
def new_keyboard(self):
Keyboard(self)
def set_focused_object(self, event):
self.active_input = event.widget
def __init__(self):
tk.Tk.__init__(self)
self.active_input = None
ttk.Button(self, text="Show Keyboard", command=self.new_keyboard).pack()
self.input1 = ttk.Entry(self)
self.input1.bind("<FocusIn>", self.set_focused_object)
self.input1.pack()
self.input2 = ttk.Entry(self)
self.input2.bind("<FocusIn>", self.set_focused_object)
self.input2.pack()
self.input3 = tk.Text(self, height=3, width=15)
self.input3.bind("<FocusIn>", self.set_focused_object)
self.input3.pack()
class Keyboard(tk.Toplevel):
def pass_key_to_master(self, key):
self.master.keyboard_triger(key)
def __init__(self, master):
tk.Toplevel.__init__(self, master)
self.master = master
self.title('Keyboard')
# this way of agruping keys stores the kwags
# of the drawing method
keys = {
'A': {'x': 0, 'y': 0},
'B': {'x': 20, 'y': 20},
'C': {'x': 50, 'y': 50}
}
# expected structure
# {string key: reference to the button}
self.buttons = {}
for i in keys:
self.buttons[i] = tk.Button( # i=i is required to make a instance
self, text=i, command=lambda i=i: self.pass_key_to_master(i)
)
self.buttons[i].place(**keys[i])
if __name__ == '__main__':
root = MainWindow()
root.mainloop()

Your code maybe could have a better construction.(But I didn't revise your code construction.)
Followed by your code,I use a global variable.And fix some errors in your code.And it could work normally in my computer.
import tkinter as tk
from tkinter import ttk
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.active_input = tk.Variable(value=None)
ttk.Button(self, text="show Keyboard", command=lambda: Keyboard(self)).pack()
global focusedWidget
focusedWidget = None
self.text = tk.StringVar(value="")
self.input1 = ttk.Entry(self)
self.input1.bind("<FocusIn>", self.getFocusWidget)
self.input2 = ttk.Entry(self)
self.input2.bind("<FocusIn>", self.getFocusWidget)
self.input3 = tk.Text(self, height=3, width=15)
self.input3.bind("<FocusIn>", self.getFocusWidget)
self.input1.pack()
self.input3.pack()
self.input2.pack()
def getFocusWidget(self,event): # this function could be a static function
global focusedWidget
focusedWidget = event.widget
class Keyboard(tk.Toplevel):
OPENED = False
NAME = "- Keyboard -"
NUM = [{"text":"1", "width":1},
{"text":"2", "width":1},
{"text":"3", "width":2}]
CHAR= [{"text":"A", "width":1},
{"text":"B", "width":1},
{"text":"C", "width":2}]
def __init__(self, master):
if not Keyboard.OPENED:
Keyboard.OPENED = True
print("keyboard opened!")
self.master = master
tk.Toplevel.__init__(self, master)
self.title(self.NAME)
self.protocol("WM_DELETE_WINDOW", self.close)
self.keyb_nb = ttk.Notebook(self)
self.keyb_nb.pack()
self.num_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.num_tab, Keyboard.NUM,2)
self.keyb_nb.add(self.num_tab, text="123")
self.char_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.char_tab, Keyboard.CHAR, 2)
self.keyb_nb.add(self.char_tab, text="ABC")
def createPad(self, master, pad:list, max_col):
self.co_count = 0
self.ro = 1
for button in pad:
button["id"] = ttk.Button(master, width=6*button["width"], text=button["text"], command=lambda button=button:self.bclicked(button)) # this lambda expression has some errors.
if self.co_count >= max_col:
self.ro = self.ro + 1
self.co_count = 0
button["id"].grid(row=self.ro, columnspan=button["width"], column=self.co_count)
self.co_count = self.co_count+button["width"]
def bclicked(self, button:dict):
global focusedWidget
"""
reciver = self.master.active_input #I think the Problem here is, that the variable contains a string, not a widget
reciever.focus_force()
reciever.insert(index=tk.INSERT, string=button["text"])
"""
if not focusedWidget: # If user hasn't click a entry or text widget.
print("Please select a entry or text")
return
if focusedWidget.widgetName=='ttk::entry': # use if statement to check the type of selected entry.
focusedWidget.insert(index=tk.INSERT,string=button["text"])
else:
focusedWidget.insert("end",button["text"])
def close(self):
Keyboard.OPENED = False
self.destroy()
print("keyboard closed!")
root = MainWindow()
root.mainloop()

Related

How to set background color of tk.simpledialog?

Is there a way to set the background color of a tkinter simpledialog window?
I am trying to implement a darkmode theme by changing background colors and am struggling with getting the simpledialog & filedialog windows to follow suit.
My current approach is to check right after creating a new toplevel, whether darkmode should apply, but I couldn't find any information in the Tkinter Dialogs documentation regarding options for the simpledialogs
top = tk.Toplevel()
if self.config["global"]["darkmode"] == "True":
top.configure(bg='black')
Simpledialog call:
cur_row = tk.simpledialog.askinteger("Row Selection", msg_string, parent = parent)
There isn't an existing argument, but I did some digging through the code for tkinter.simpledialog and it turns out that askinteger is based on the Toplevel class anyway so if you change tkinter/simpledialog.py to the code at the end of this answer, it adds a bg parameter to simpledialog that you can use as follows:
cur_row = tk.simpledialog.askinteger("Row Selection", msg_string, parent = parent, bg = "black")
I hope this helps :)
Here is the full code:
#
# An Introduction to Tkinter
#
# Copyright (c) 1997 by Fredrik Lundh
#
# This copyright applies to Dialog, askinteger, askfloat and asktring
#
# fredrik#pythonware.com
# http://www.pythonware.com
#
"""This modules handles dialog boxes.
It contains the following public symbols:
SimpleDialog -- A simple but flexible modal dialog box
Dialog -- a base class for dialogs
askinteger -- get an integer from the user
askfloat -- get a float from the user
askstring -- get a string from the user
"""
from tkinter import *
from tkinter import messagebox
import tkinter # used at _QueryDialog for tkinter._default_root
class SimpleDialog:
def __init__(self, master,
text='', buttons=[], default=None, cancel=None,
title=None, class_=None):
if class_:
self.root = Toplevel(master, class_=class_)
else:
self.root = Toplevel(master)
if title:
self.root.title(title)
self.root.iconname(title)
self.message = Message(self.root, text=text, aspect=400)
self.message.pack(expand=1, fill=BOTH)
self.frame = Frame(self.root)
self.frame.pack()
self.num = default
self.cancel = cancel
self.default = default
self.root.bind('<Return>', self.return_event)
for num in range(len(buttons)):
s = buttons[num]
b = Button(self.frame, text=s,
command=(lambda self=self, num=num: self.done(num)))
if num == default:
b.config(relief=RIDGE, borderwidth=8)
b.pack(side=LEFT, fill=BOTH, expand=1)
self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
self._set_transient(master)
def _set_transient(self, master, relx=0.5, rely=0.3):
widget = self.root
widget.withdraw() # Remain invisible while we figure out the geometry
widget.transient(master)
widget.update_idletasks() # Actualize geometry information
if master.winfo_ismapped():
m_width = master.winfo_width()
m_height = master.winfo_height()
m_x = master.winfo_rootx()
m_y = master.winfo_rooty()
else:
m_width = master.winfo_screenwidth()
m_height = master.winfo_screenheight()
m_x = m_y = 0
w_width = widget.winfo_reqwidth()
w_height = widget.winfo_reqheight()
x = m_x + (m_width - w_width) * relx
y = m_y + (m_height - w_height) * rely
if x+w_width > master.winfo_screenwidth():
x = master.winfo_screenwidth() - w_width
elif x < 0:
x = 0
if y+w_height > master.winfo_screenheight():
y = master.winfo_screenheight() - w_height
elif y < 0:
y = 0
widget.geometry("+%d+%d" % (x, y))
widget.deiconify() # Become visible at the desired location
def go(self):
self.root.wait_visibility()
self.root.grab_set()
self.root.mainloop()
self.root.destroy()
return self.num
def return_event(self, event):
if self.default is None:
self.root.bell()
else:
self.done(self.default)
def wm_delete_window(self):
if self.cancel is None:
self.root.bell()
else:
self.done(self.cancel)
def done(self, num):
self.num = num
self.root.quit()
class Dialog(Toplevel):
'''Class to open dialogs.
This class is intended as a base class for custom dialogs
'''
def __init__(self, parent, title = None, bg = None):
'''Initialize a dialog.
Arguments:
parent -- a parent window (the application window)
title -- the dialog title
'''
Toplevel.__init__(self, parent, bg = bg)
self.withdraw() # remain invisible for now
# If the master is not viewable, don't
# make the child transient, or else it
# would be opened withdrawn
if parent.winfo_viewable():
self.transient(parent)
if title:
self.title(title)
self.parent = parent
self.result = None
body = Frame(self)
self.initial_focus = self.body(body)
body.pack(padx=5, pady=5)
self.buttonbox()
if not self.initial_focus:
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self.cancel)
if self.parent is not None:
self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
parent.winfo_rooty()+50))
self.deiconify() # become visible now
self.initial_focus.focus_set()
# wait for window to appear on screen before calling grab_set
self.wait_visibility()
self.grab_set()
self.wait_window(self)
def destroy(self):
'''Destroy the window'''
self.initial_focus = None
Toplevel.destroy(self)
#
# construction hooks
def body(self, master):
'''create dialog body.
return widget that should have initial focus.
This method should be overridden, and is called
by the __init__ method.
'''
pass
def buttonbox(self):
'''add standard button box.
override if you do not want the standard buttons
'''
box = Frame(self)
w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
w.pack(side=LEFT, padx=5, pady=5)
w = Button(box, text="Cancel", width=10, command=self.cancel)
w.pack(side=LEFT, padx=5, pady=5)
self.bind("<Return>", self.ok)
self.bind("<Escape>", self.cancel)
box.pack()
#
# standard button semantics
def ok(self, event=None):
if not self.validate():
self.initial_focus.focus_set() # put focus back
return
self.withdraw()
self.update_idletasks()
try:
self.apply()
finally:
self.cancel()
def cancel(self, event=None):
# put focus back to the parent window
if self.parent is not None:
self.parent.focus_set()
self.destroy()
#
# command hooks
def validate(self):
'''validate the data
This method is called automatically to validate the data before the
dialog is destroyed. By default, it always validates OK.
'''
return 1 # override
def apply(self):
'''process the data
This method is called automatically to process the data, *after*
the dialog is destroyed. By default, it does nothing.
'''
pass # override
# --------------------------------------------------------------------
# convenience dialogues
class _QueryDialog(Dialog):
def __init__(self, title, prompt,
initialvalue=None,
minvalue = None, maxvalue = None,
parent = None, bg = None):
if not parent:
parent = tkinter._default_root
self.prompt = prompt
self.minvalue = minvalue
self.maxvalue = maxvalue
self.initialvalue = initialvalue
Dialog.__init__(self, parent, title, bg = bg)
def destroy(self):
self.entry = None
Dialog.destroy(self)
def body(self, master):
w = Label(master, text=self.prompt, justify=LEFT)
w.grid(row=0, padx=5, sticky=W)
self.entry = Entry(master, name="entry")
self.entry.grid(row=1, padx=5, sticky=W+E)
if self.initialvalue is not None:
self.entry.insert(0, self.initialvalue)
self.entry.select_range(0, END)
return self.entry
def validate(self):
try:
result = self.getresult()
except ValueError:
messagebox.showwarning(
"Illegal value",
self.errormessage + "\nPlease try again",
parent = self
)
return 0
if self.minvalue is not None and result < self.minvalue:
messagebox.showwarning(
"Too small",
"The allowed minimum value is %s. "
"Please try again." % self.minvalue,
parent = self
)
return 0
if self.maxvalue is not None and result > self.maxvalue:
messagebox.showwarning(
"Too large",
"The allowed maximum value is %s. "
"Please try again." % self.maxvalue,
parent = self
)
return 0
self.result = result
return 1
class _QueryInteger(_QueryDialog):
errormessage = "Not an integer."
def getresult(self):
return self.getint(self.entry.get())
def askinteger(title, prompt, **kw):
'''get an integer from the user
Arguments:
title -- the dialog title
prompt -- the label text
**kw -- see SimpleDialog class
Return value is an integer
'''
d = _QueryInteger(title, prompt, **kw)
return d.result
class _QueryFloat(_QueryDialog):
errormessage = "Not a floating point value."
def getresult(self):
return self.getdouble(self.entry.get())
def askfloat(title, prompt, **kw):
'''get a float from the user
Arguments:
title -- the dialog title
prompt -- the label text
**kw -- see SimpleDialog class
Return value is a float
'''
d = _QueryFloat(title, prompt, **kw)
return d.result
class _QueryString(_QueryDialog):
def __init__(self, *args, **kw):
if "show" in kw:
self.__show = kw["show"]
del kw["show"]
else:
self.__show = None
_QueryDialog.__init__(self, *args, **kw)
def body(self, master):
entry = _QueryDialog.body(self, master)
if self.__show is not None:
entry.configure(show=self.__show)
return entry
def getresult(self):
return self.entry.get()
def askstring(title, prompt, **kw):
'''get a string from the user
Arguments:
title -- the dialog title
prompt -- the label text
**kw -- see SimpleDialog class
Return value is a string
'''
d = _QueryString(title, prompt, **kw)
return d.result
if __name__ == '__main__':
def test():
root = Tk()
def doit(root=root):
d = SimpleDialog(root,
text="This is a test dialog. "
"Would this have been an actual dialog, "
"the buttons below would have been glowing "
"in soft pink light.\n"
"Do you believe this?",
buttons=["Yes", "No", "Cancel"],
default=0,
cancel=2,
title="Test Dialog")
print(d.go())
print(askinteger("Spam", "Egg count", initialvalue=12*12))
print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
maxvalue=100))
print(askstring("Spam", "Egg label"))
t = Button(root, text='Test', command=doit)
t.pack()
q = Button(root, text='Quit', command=t.quit)
q.pack()
t.mainloop()
test()
I just added by own buttonbox method, found the children and configured them.
class bug_dialog(tk.simpledialog.Dialog):
def buttonbox(self):
super().buttonbox()
for _ in self.children.values(): _.configure(bg='light green')
self.configure(bg='light green')

I want to create a toolbar that when clicking a button, change the frame

I created a program that when clicking on one of the buttons, it changes the frame, which contains other functions.
I tried to use ['frame'] .config, a simple function of the same, but nothing works.
from tkinter import *
from Item import Item
from Monstro import Monster
class Ferramentas:
def __init__(self,master,Item,Monster):
self.Item = Item
self.Monster = Monster
self.ferramentas = Frame(master)
self.ferramentas.grid(row=0,column=0)
self.b1 = Button(self.ferramentas,text='CALCULADORA')
self.b2 = Button(self.ferramentas,text='MONSTROS',command=self.Monstros)
self.b3 = Button(self.ferramentas,text='ITENS',command=self.Itens)
self.b1.grid(row=0,column=0)
self.b2.grid(row=0,column=1)
self.b3.grid(row=0,column=2)
self.frame_mestre = Frame(master)
self.frame_mestre.grid(row=1,column=0)
def Itens(self):
self.frame_mestre = self.Item.frame_2
self.Item.frame_2.grid(row=1,column=0,sticky="nsew")
def Monstros(self):
self.frame_mestre = self.Monster.frame_1
self.Monster.frame_1.grid(row=1,column=0,sticky="nsew")
janela = Tk()
c = Monster(janela)
b = Item(janela)
a = Ferramentas(janela,b,c)
janela.mainloop()
class Item:
def __init__(self,master):
self.frame_2 = Frame(master)
self.item_nome = Label(self.frame_2, text='NOME')
self.item_nome.grid(row=0,column=1)
class Monster:
def __init__(self, master):
self.frame_1 = Frame(master)
self.nome = Label(self.frame_1, text='NOME', relief='raised')
self.nome.grid(row=0,column=1, columnspan=2)
I hope that by clicking on the respective button, the frames change, without one overlapping the other

Receiveing input from Python's tkinter entry widget and displaying it in a label

The program I am working with currently requires input from the user to be displayed on the programs window. I have researched on both the internet and stackoverflow, discovering several solutions to my problem, but none seem to work. My goal is to receive input from the user via Python's tkinter entry widget and display the results in a new label, while taking out my initial one and the entry box, however, the program is rejecting my attempts at an answer.
What strategy, lines of code/library's, or pieces of advice do you have for me to accomplish my goal?
My existing solutions:
.get()
textvariable=self.entdat
Existing code is as follows:
from Tkinter import *
import time
class Input(Frame):
def __init__(self, parent=None, **kw):
Frame.__init__(self, parent, background="white")
self.parent = parent
self.initUI()
self.entdat = StringVar
self.timestr = StringVar()
self.makeWidgets()
def makeWidgets(self):
self.ol = Label(text="Objective:")
self.ol.pack(side=TOP)
self.ew = Entry()
self.ew.pack(side=TOP)
self.b = Button(text="OK", command=self.clicked)
self.b.pack(side=TOP)
def clicked(self):
self.entdat = self.ew.get()
self.dat = Label(textvariable=self.ew.get())
self.dat.pack(side=TOP)
self.hide_Widget()
def hide_Widget(event):
event.ew.pack_forget()
event.ol.pack_forget()
event.b.pack_forget()
def main():
root = Tk()
root.geometry("240x135+25+50")
tm = Input(root)
tm.pack(side=TOP)
root.mainloop()
if __name__ == '__main__':
main()
I amended your code, so that it executes at least, and hopefully in a way you want.
from Tkinter import *
class Input(Frame):
def __init__(self, parent=None, **kw):
Frame.__init__(self, parent, background="white")
self.parent = parent
self.entdat = StringVar()
self.makeWidgets()
def makeWidgets(self):
self.ol = Label(text="Objective:")
self.ol.pack(side=TOP)
self.ew = Entry(textvariable=self.entdat)
self.ew.pack(side=TOP)
self.b = Button(text="OK", command=self.clicked)
self.b.pack(side=TOP)
def clicked(self):
self.dat = Label(self, textvariable=self.entdat )
self.dat.pack(side=TOP)
self.distroy_Widget()
def distroy_Widget(self):
self.ew.destroy()
self.ol.destroy()
self.b.destroy()
def main():
root = Tk()
root.geometry("240x135+25+50")
tm = Input(root)
tm.pack(side=TOP)
root.mainloop()
if __name__ == '__main__':
main()
Hope it helps.

Display message when hovering over something with mouse cursor in Python

I have a GUI made with TKinter in Python. I would like to be able to display a message when my mouse cursor goes, for example, on top of a label or button. The purpose of this is to explain to the user what the button/label does or represents.
Is there a way to display text when hovering over a tkinter object in Python?
I think this would meet your requirements.
Here's what the output looks like:
First, A class named ToolTip which has methods showtip and hidetip is defined as follows:
from tkinter import *
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"Display text in tooltip window"
self.text = text
if self.tipwindow or not self.text:
return
x, y, cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 57
y = y + cy + self.widget.winfo_rooty() +27
self.tipwindow = tw = Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = Label(tw, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
def CreateToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
The widget is where you want to add the tip. For example, if you want the tip when you hover over a button or entry or label, the instance of the same should be provided at the call time.
Quick note: the code above uses from tkinter import *
which is not suggested by some of the programmers out there, and they have valid points. You might want to make necessary changes in such case.
To move the tip to your desired location, you can change x and y in the code.
The function CreateToolTip() helps to create this tip easily. Just pass the widget and string you want to display in the tipbox to this function, and you're good to go.
This is how you call the above part:
button = Button(root, text = 'click mem')
button.pack()
CreateToolTip(button, text = 'Hello World\n'
'This is how tip looks like.'
'Best part is, it\'s not a menu.\n'
'Purely tipbox.')
Do not forget to import the module if you save the previous outline in different python file, and don't save the file as CreateToolTip or ToolTip to avoid confusion.
This post from Fuzzyman shares some similar thoughts, and worth checking out.
You need to set a binding on the <Enter> and <Leave> events.
Note: if you choose to pop up a window (ie: a tooltip) make sure you don't pop it up directly under the mouse. What will happen is that it will cause a leave event to fire because the cursor leaves the label and enters the popup. Then, your leave handler will dismiss the window, your cursor will enter the label, which causes an enter event, which pops up the window, which causes a leave event, which dismisses the window, which causes an enter event, ... ad infinitum.
For simplicity, here's an example that updates a label, similar to a statusbar that some apps use. Creating a tooltip or some other way of displaying the information still starts with the same core technique of binding to <Enter> and <Leave>.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.l1 = tk.Label(self, text="Hover over me")
self.l2 = tk.Label(self, text="", width=40)
self.l1.pack(side="top")
self.l2.pack(side="top", fill="x")
self.l1.bind("<Enter>", self.on_enter)
self.l1.bind("<Leave>", self.on_leave)
def on_enter(self, event):
self.l2.configure(text="Hello world")
def on_leave(self, enter):
self.l2.configure(text="")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand="true")
root.mainloop()
You can refer to this- HoverClass
It is exactly what you need. Nothing more, nothing less
from Tkinter import *
import re
class HoverInfo(Menu):
def __init__(self, parent, text, command=None):
self._com = command
Menu.__init__(self,parent, tearoff=0)
if not isinstance(text, str):
raise TypeError('Trying to initialise a Hover Menu with a non string type: ' + text.__class__.__name__)
toktext=re.split('\n', text)
for t in toktext:
self.add_command(label = t)
self._displayed=False
self.master.bind("<Enter>",self.Display )
self.master.bind("<Leave>",self.Remove )
def __del__(self):
self.master.unbind("<Enter>")
self.master.unbind("<Leave>")
def Display(self,event):
if not self._displayed:
self._displayed=True
self.post(event.x_root, event.y_root)
if self._com != None:
self.master.unbind_all("<Return>")
self.master.bind_all("<Return>", self.Click)
def Remove(self, event):
if self._displayed:
self._displayed=False
self.unpost()
if self._com != None:
self.unbind_all("<Return>")
def Click(self, event):
self._com()
Example app using HoverInfo:
from Tkinter import *
from HoverInfo import HoverInfo
class MyApp(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.grid()
self.lbl = Label(self, text='testing')
self.lbl.grid()
self.hover = HoverInfo(self, 'while hovering press return \n for an exciting msg', self.HelloWorld)
def HelloWorld(self):
print('Hello World')
app = MyApp()
app.master.title('test')
app.mainloop()
Screenshot:
I have a very hacky solution but it has some advantages over the current answers so I figured I would share it.
lab=Label(root,text="hover me")
lab.bind("<Enter>",popup)
def do_popup(event):
# display the popup menu
root.after(1000, self.check)
popup = Menu(root, tearoff=0)
popup.add_command(label="Next")
popup.tk_popup(event.x_root, event.y_root, 0)
def check(event=None):
x, y = root.winfo_pointerxy()
widget = root.winfo_containing(x, y)
if widget is None:
root.after(100, root.check)
else:
leave()
def leave():
popup.delete(0, END)
The only real issue with this is it leaves behind a small box that moves focus away from the main window
If anyone knows how to solve these issues let me know
If anyone is on Mac OSX and tool tip isn't working, check out the example in:
https://github.com/python/cpython/blob/master/Lib/idlelib/tooltip.py
Basically, the two lines that made it work for me on Mac OSX were:
tw.update_idletasks() # Needed on MacOS -- see #34275.
tw.lift() # work around bug in Tk 8.5.18+ (issue #24570)
Here is a simple solution to your problem that subclasses the tk.Button object. We make a special class that tk.Button inherits from, giving it tooltip functionality. The same for tk.Labels.
I don't know what would be cleanest and the easiest way to maintain code for keeping track of the text that goes into the tooltips. I present here one way, in which I pass unique widget IDs to MyButtons, and access a dictionary for storing the tooltip texts. You could store this file as a JSON, or as a class attribute, or as a global variable (as below). Alternatively, perhaps it would be better to define a setter method in MyButton, and just call this method every time you create a new widget that should have a tooltip. Although you would have to store the widget instance in a variable, adding one extra line for all widgets to include.
One drawback in the code below is that the self.master.master syntax relies on determining the "widget depth". A simple recursive function will catch most cases (only needed for entering a widget, since by definition you leave somewhere you once were).
Anyway, below is a working MWE for anyone interested.
import tkinter as tk
tooltips = {
'button_hello': 'Print a greeting message',
'button_quit': 'Quit the program',
'button_insult': 'Print an insult',
'idle': 'Hover over button for help',
'error': 'Widget ID not valid'
}
class ToolTipFunctionality:
def __init__(self, wid):
self.wid = wid
self.widet_depth = 1
self.widget_search_depth = 10
self.bind('<Enter>', lambda event, i=1: self.on_enter(event, i))
self.bind('<Leave>', lambda event: self.on_leave(event))
def on_enter(self, event, i):
if i > self.widget_search_depth:
return
try:
cmd = f'self{".master"*i}.show_tooltip(self.wid)'
eval(cmd)
self.widget_depth = i
except AttributeError:
return self.on_enter(event, i+1)
def on_leave(self, event):
cmd = f'self{".master" * self.widget_depth}.hide_tooltip()'
eval(cmd)
class MyButton(tk.Button, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Button.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class MyLabel(tk.Label, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Label.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.tooltip = tk.StringVar()
self.tooltip.set(tooltips['idle'])
self.frame = tk.Frame(self, width=50)
self.frame.pack(expand=True)
MyLabel(self.frame, '', text='One Cool Program').pack()
self.subframe = tk.Frame(self.frame, width=40)
self.subframe.pack()
MyButton(self.subframe, 'button_hello', text='Hello!', command=self.greet, width=20).pack()
MyButton(self.subframe, 'button_insutl', text='Insult', command=self.insult, width=20).pack()
MyButton(self.subframe, 'button_quit', text='Quit', command=self.destroy, width=20).pack()
tk.Label(self.subframe, textvar=self.tooltip, width=20).pack()
def show_tooltip(self, wid):
try:
self.tooltip.set(tooltips[wid])
except KeyError:
self.tooltip.set(tooltips['error'])
def hide_tooltip(self):
self.tooltip.set(tooltips['idle'])
def greet(self):
print('Welcome, Fine Sir!')
def insult(self):
print('You must be dead from the neck up')
if __name__ == '__main__':
app = Application()
app.mainloop()
The best way I have found to create a popup help window is to use the tix.Balloon. I have modified it below to make it look better and show an example (note the use of tix.Tk):
import tkinter as tk
import tkinter.tix as tix
class Balloon(tix.Balloon):
# A modified tix popup balloon (to change the default delay, bg and wraplength)
init_after = 1250 # Milliseconds
wraplength = 300 # Pixels
def __init__(self, master):
bg = root.cget("bg")
# Call the parent
super().__init__(master, initwait=self.init_after)
# Change background colour
for i in self.subwidgets_all():
i.config(bg=bg)
# Modify the balloon label
self.message.config(wraplength=self.wraplength)
root = tix.Tk()
l = tk.Label(root, text="\n".join(["text"] * 5))
l.pack()
b = Balloon(root.winfo_toplevel())
b.bind_widget(l, balloonmsg="Some random text")
root.mainloop()
OLD ANSWER:
Here is an example using <enter> and <leave> as #bryanoakley suggested with a toplevel (with overridedirect set to true). Use the hover_timer class for easy use of this. This needs the widget and help-text (with an optional delay argument - default 0.5s) and can be easily called just by initiating the class and then cancelling it.
import threading, time
from tkinter import *
class hover_window (Toplevel):
def __init__ (self, coords, text):
super ().__init__ ()
self.geometry ("+%d+%d" % (coords [0], coords [1]))
self.config (bg = "white")
Label (self, text = text, bg = "white", relief = "ridge", borderwidth = 3, wraplength = 400, justify = "left").grid ()
self.overrideredirect (True)
self.update ()
self.bind ("<Enter>", lambda event: self.destroy ())
class hover_timer:
def __init__ (self, widget, text, delay = 2):
self.wind, self.cancel_var, self.widget, self.text, self.active, self.delay = None, False, widget, text, False, delay
threading.Thread (target = self.start_timer).start ()
def start_timer (self):
self.active = True
time.sleep (self.delay)
if not self.cancel_var: self.wind = hover_window ((self.widget.winfo_rootx (), self.widget.winfo_rooty () + self.widget.winfo_height () + 20), self.text)
self.active = False
def delayed_stop (self):
while self.active: time.sleep (0.05)
if self.wind:
self.wind.destroy ()
self.wind = None
def cancel (self):
self.cancel_var = True
if not self.wind: threading.Thread (target = self.delayed_stop).start ()
else:
self.wind.destroy ()
self.wind = None
def start_help (event):
# Create a new help timer
global h
h = hover_timer (l, "This is some additional information.", 0.5)
def end_help (event):
# If therre is one, end the help timer
if h: h.cancel ()
if __name__ == "__main__":
# Create the tkinter window
root = Tk ()
root.title ("Hover example")
# Help class not created yet
h = None
# Padding round label
Frame (root, width = 50).grid (row = 1, column = 0)
Frame (root, height = 50).grid (row = 0, column = 1)
Frame (root, width = 50).grid (row = 1, column = 2)
Frame (root, height = 50).grid (row = 2, column = 1)
# Setup the label
l = Label (root, text = "Hover over me for information.", font = ("sans", 32))
l.grid (row = 1, column = 1)
l.bind ("<Enter>", start_help)
l.bind ("<Leave>", end_help)
# Tkinter mainloop
root.mainloop ()
I wanted to contribute to the answer of #squareRoot17 as he inspired me to shorten his code while providing the same functionality:
import tkinter as tk
class ToolTip(object):
def __init__(self, widget, text):
self.widget = widget
self.text = text
def enter(event):
self.showTooltip()
def leave(event):
self.hideTooltip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
def showTooltip(self):
self.tooltipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1) # window without border and no normal means of closing
tw.wm_geometry("+{}+{}".format(self.widget.winfo_rootx(), self.widget.winfo_rooty()))
label = tk.Label(tw, text = self.text, background = "#ffffe0", relief = 'solid', borderwidth = 1).pack()
def hideTooltip(self):
tw = self.tooltipwindow
tw.destroy()
self.tooltipwindow = None
This class can then be imported and used as:
import tkinter as tk
from tooltip import ToolTip
root = tk.Tk()
your_widget = tk.Button(root, text = "Hover me!")
ToolTip(widget = your_widget, text = "Hover text!")
root.mainloop()

Tkinter Freeze on grid_remove

So I am trying to build a GUI where I enter some information, clear the entry fields, and then add new entry fields. However, when I try to clear the frame from the root via grid_remove, the application freezes. The relevant code is below.
import tkinter
from threading import Thread
class PinGui(tkinter.Frame):
def __init__(self, client):
self.client = client
self.root = client.root
self.add_pin = client.add_pin
self.end = client.end
tkinter.Frame.__init__(self, self.root)
self.grid_widgets()
self.grid_buttons()
self.bind_keys()
self.grid(padx=32, pady=32)
def grid_buttons(self, b1='Add', b2='Reset', b3='Quit'):
self.addButton = tkinter.Button(self, text=b1, command=self.validate)
self.resetButton = tkinter.Button(self, text=b2, command=self.reset)
self.quitButton = tkinter.Button(self, text=b3, command=self.end)
self.buttons = [self.addButton, self.resetButton, self.quitButton]
for i in range(3): self.buttons[i].grid(row=i, column=11)
def grid_widgets(self):
widths = [3,3,4,4,6]
self.pin_vars = []
self.pin_fields = []
for i in range(5):
self.pin_vars.append(tkinter.StringVar())
self.pin_fields.append(
tkinter.Entry(self,width=widths[i], textvariable=self.pin_vars[i])
)
self.pin_fields[i].grid(row=0, column=2*i, padx=3)
self.pin_fields[0].focus_set()
def bind_keys(self):
self.root.bind_all("<Return>", self.validate)
self.root.bind_all("<Escape>", self.end)
def validate(self, args=None):
self.client.pin = []
for field in self.pin_fields:
self.client.pin.append(field.get())
Thread(target=self.add_pin).start()
def ungrid(self):
for field in self.pin_fields: field.grid_remove()
for button in self.buttons: button.grid_remove()
self.display.grid_remove()
And:
class PinClient:
def __init__(self):
self.root = tkinter.Tk()
self.gui = PinGui(self)
self.pins = []
def add_pin(self):
self.gui.reset()
if 'display' in self.__dict__:
self.pins.append(self.pin)
self.display.add_pin(self.pin)
self.ping("Enter PIN for Comp %s:" % len(self.display.col1))
if len(self.display.col1) > 5:
self.end() # THIS IS WHERE IT FREEZES
else:
self.subject = self.pin
self.display = Display(self.root, self.pin)
self.display.grid(row=1, padx=32, pady=32)
self.ping("Enter PIN for Comp 1:")
def ping(self, msg):
self.gui.d_var.set(msg)
def end(self, args=None):
self.gui.ungrid()
class Display(tkinter.Frame):
def __init__(self, master, pin):
tkinter.Frame.__init__(self, master)
self.pin = pin
self.col1 = []
self.col2 = []
self.col1.append(tkinter.Label(self, text="Subject:"))
self.col2.append(tkinter.Label(self, text=self.pin))
self.grid_widgets()
def grid_widgets(self):
self.ungrid()
for i in range(len(self.col1)):
self.col1[i].grid(row=i, column=0)
self.col2[i].grid(row=i, column=1)
def ungrid(self):
for i in range(len(self.col1)):
self.col1[i].grid_remove()
self.col2[i].grid_remove()
def add_pin(self, pin):
self.col1.append(tkinter.Label(self, text="Comp %s:" % len(self.col1)))
self.col2.append(tkinter.Label(self, text=pin))
i = len(self.col1)
self.col1[i-1].grid(row=i, column=0)
self.col2[i-1].grid(row=i, column=1)
It seems to be somehow related to the threading, but I haven't been able to find any reason this should freeze. Any help is greatly appreciated!
Tkinter is not thread safe. If you do anything in a thread other than the main thread that touches a GUI object then you will get unpredictable results. Almost certainly, it is threading that is causing your problem, since you are trying to access widgets from the worker thread.

Categories

Resources