Hovertip/Tooltip for each item in Python ttk combobox - python-3.x

This is about ttk combobox.
I need to show help-info via Tooltip for each item (text) in ttk.combobox.
The minimum example is following:
import tkinter as tk
from tkinter import ttk
from idlelib.tooltip import Hovertip
names =["One", "Two", "Three"]
root = tk.Tk()
root.title("Combo Test GUI")
Frame1 = ttk.Frame(root, padding="3 3 12 12")
Frame1.grid(column= 0, row = 0)
label_1 = ttk.Label(Frame1, text="Select Item:", anchor=tk.W)
label_1.grid(column= 0, row = 1)
item1 = ttk.Combobox(Frame1, state="readonly", values=names, width=35)
item1.grid(column= 0, row = 2)
m1 = Hovertip(item1, "One test")
m2 = Hovertip(label_1, "Another test")
root.mainloop()
The problem: I want to bind the Hovertip to the text in the combo list, i.e. "One", "Two", "Three".
When the user hovers mouse over "One", the description "This is option One", should be shown in the Hovertip/Tooltip; and same for the other items "Two" and "Three" also.
I have searched everywhere, on Stackoverflow, in the reference docs, other websites, but it seems that Hovertip/Tooltip can only be used with widget, and not with simple text in the combobox.
Is there any way at all to make it possible?

The idea is to make a class similar to Hovertip but that works with listbox items instead of widgets. The current item is stored in self._current_item and when the mouse moves in the listbox, the displaying of the tooltip is rescheduled if the item changes. The text for each item is stored in a dictionary self.tips = {item index: text, ...}.
The main issue is that the Listbox displaying the Combobox's choices is not directly accessible with python. So I had to use some Tcl commands to do it:
proc callback {y} {
event generate .!combobox <<OnMotion>> -y $y
}
set popdown [ttk::combobox::PopdownWindow .!combobox]
bind $popdown.f.l <Motion> {callback %y}
The above code generates a virtual event <<OnMotion>> in the combobox when the mouse moves (where .!combobox is the name of the Combobox widget and y is the mouse relative y coordinate in the widget).
Then I bind the _on_motion() method of the combobox to this <<OnMotion>> event to check if the current item has changed. To get the current item I use the Listbox method nearest(y) but from Tcl.
I also modified the get_position() method to display the tooltip just below the current item and showcontents() to display the text corresponding to the item.
Here is the full code:
import tkinter as tk
from tkinter import ttk
from idlelib.tooltip import OnHoverTooltipBase
class ComboboxTip(OnHoverTooltipBase):
def __init__(self, combobox_widget, hover_delay=1000):
super(ComboboxTip, self).__init__(combobox_widget, hover_delay=hover_delay)
self.tips = {}
self._current_item = 0
combobox_widget.tk.eval("""
proc callback {y} {
event generate %(cb)s <<OnMotion>> -y $y
}
set popdown [ttk::combobox::PopdownWindow %(cb)s]
bind $popdown.f.l <Motion> {callback %%y}
""" % ({"cb": combobox_widget}))
self._id4 = combobox_widget.bind("<<OnMotion>>", self._on_motion)
def _on_motion(self, event):
current_item = int(self.anchor_widget.tk.eval("$popdown.f.l nearest %i" % event.y))
if current_item != self._current_item:
self._current_item = current_item
self.hidetip()
if current_item in self.tips:
self.schedule()
else:
self.unschedule()
def __del__(self):
try:
self.anchor_widget.unbind("<<OnMotion>>", self._id4)
except tk.TclError:
pass
super(ComboboxTip, self).__del__()
def add_tooltip(self, index, text):
self.tips[index] = text
def get_position(self):
"""choose a screen position for the tooltip"""
try:
h = self.anchor_widget.winfo_height()
bbox = self.anchor_widget._getints(self.anchor_widget.tk.eval("$popdown.f.l bbox %i" % self._current_item))
return bbox[0] + bbox[2], bbox[1] + bbox[-1] + h
except Exception:
return 20, self.anchor_widget.winfo_height() + 1
def showcontents(self):
label = tk.Label(self.tipwindow, text=self.tips[self._current_item], justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1)
label.pack()
names = ["One", "Two", "Three"]
root = tk.Tk()
cb = ttk.Combobox(root, values=names)
cb.pack()
t = ComboboxTip(cb)
t.add_tooltip(0, "This is One")
t.add_tooltip(1, "This is Two")
t.add_tooltip(2, "This is Three")
root.mainloop()

Related

In tkinter entry widget, failed to type in Indian language using sanskrit/hindi keyboard

from tkinter import *
from tkinter import ttk
lst = ['रामायणसारः', 'देवयाज्ञिकपद्धतिः', 'तर्कसङ्ग्रहः',]
def check_input(event):
value = event.widget.get()
if value == '':
data = lst
else:
data = []
for item in lst:
if value.lower() in item.lower():
data.append(item)
update(data)
def update(data):
# Clear the Combobox
menu.delete(0, END)
# Add values to the combobox
for value in data:
menu.insert(END, value)
def fillout(event):
combo_box.delete(0, END)
combo_box.insert(0, menu.get(menu.curselection()))
combo_box = Entry(root, width=10, font=("Times", 12))
combo_box.place(relx=0.20, rely=0.4, relwidth=0.45, relheight=0.1)
combo_box.bind('<KeyRelease>', check_input)
menu = Listbox(root)
menu.place(relx=0.20, rely=0.5, relwidth=0.45, relheight=0.1)
menu.bind("<<ListboxSelect>>", fillout)
update(lst)
while typing Indian language(Sanskrit/Hindi) text in entry box using sanskrit/hindi keyboard, it shows question mark, please help to rectify the issue.
ref
I have also tried encoding('utf-8') with entry widget, but it doesn't worked.

Change the width of an updated listbox in a ttk Combobox

This is an extension to the question and answers here.
I want to create two Comboboxes, with the items in the second Combobox depending on the selection in the first Combobox. Furthermore, I would like the dropdown listbox to resize to fit the text in the list, as in the answer here. However, I'm having some difficulties with this second part. I'd like to solve this problem using Python.
I've had varying results using .pack() and .forget() instead of .place() and .place_forget(), however I haven't been able to create a robust solution. Using .place is preferable to .pack or .grid if possible.
As an MWE, I've extended the code from one of the answers in the previous question. The dropdown listbox of c2 resizes fine, however that of c1 does not.
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont
def on_combo_configure(event):
combo = event.widget
style = ttk.Style()
# check if the combobox already has the "postoffest" property
current_combo_style = combo.cget('style') or "TCombobox"
if len(style.lookup(current_combo_style, 'postoffset'))>0:
return
combo_values = combo.cget('values')
if len(combo_values) == 0:
return
longest_value = max(combo_values, key=len)
font = tkfont.nametofont(str(combo.cget('font')))
width = font.measure(longest_value + "0") - event.width
if (width<0):
# no need to make the popdown smaller
return
# create an unique style name using widget's id
unique_name='Combobox{}'.format(combo.winfo_id())
# the new style must inherit from curret widget style (unless it's our custom style!)
if unique_name in current_combo_style:
style_name = current_combo_style
else:
style_name = "{}.{}".format(unique_name, current_combo_style)
style.configure(style_name, postoffset=(0,0,width,0))
combo.configure(style=style_name)
def update_c1_list(event):
c1.place_forget()
_ = c.get()
if _ == "fruit":
c1['values'] = ('apples are the best', 'bananas are way more better')
elif _ == "text":
c1['values'] = ("here's some text", "and here's some much longer text to stretch the list")
else:
pass
c1.place(x=10,y=40)
root = tk.Tk()
root.title("testing the combobox")
root.geometry('300x300+50+50')
c = ttk.Combobox(root, values=['fruit','text'], state="readonly", width=10)
c.bind('<Configure>', on_combo_configure)
c.bind('<<ComboboxSelected>>', update_c1_list)
c.place(x=10,y=10)
c1 = ttk.Combobox(root, state="readonly", width=10)
c1.bind('<Configure>', on_combo_configure)
c1.place(x=10,y=40)
c2 = ttk.Combobox(root, state="readonly", width=10)
c2.bind('<Configure>', on_combo_configure)
c2.place(x=10,y=70)
c2['values']=('this list resizes fine','because it is updated outside of the function')
root.mainloop()
Any help is welcome, thanks.
After playing around I came up with a function which updates the listbox width of a combobox "on the fly". However, it's a bit like fixing a window with a hammer and causes some issues.
def Change_combo_width_on_the_fly(combo,combo_width):
style = ttk.Style()
# check if the combobox already has the "postoffest" property
current_combo_style = combo.cget('style') or "TCombobox"
combo_values = combo.cget('values')
if len(combo_values) == 0:
return
longest_value = max(combo_values, key=len)
font = tkfont.nametofont(str(combo.cget('font')))
width = font.measure(longest_value + "0") - (combo_width*6+23)
if (width<0):
# no need to make the popdown smaller
return
# create an unique style name using widget's id
unique_name='Combobox{}'.format(combo.winfo_id())
# the new style must inherit from curret widget style (unless it's our custom style!)
if unique_name in current_combo_style:
style_name = current_combo_style
else:
style_name = "{}.{}".format(unique_name, current_combo_style)
style.configure(style_name, postoffset=(0,0,width,0))
combo.configure(style=style_name)
As a MWE, the code can be used as follows:
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont
root = tk.Tk()
c1 = ttk.Combobox(root, state="readonly", width=10)
c1.place(x=10,y=40)
Change_combo_width_on_the_fly(c1,10)
root.mainloop()
While the function does the job, it causes problems elsewhere in my code. In particular, it messes with a previously packed widget (scrollbar). I think it is changing the style the last placed widget, but I don't know how to fix this.

How to make a label cycle through preset words or phrases when a button is pressed in tkinter

So I'm trying to make it cycle through each letter of the alphabet when the button is clicked.
I have tried the method i am showing now.
I have also tried many others and i couldn't get anything to work.
If you do have a solution please try keep it simple i am kinda new too this.
from tkinter import *
win = Tk()
win.title('ab')
a = 0
def changetext():
a = a+1
if a == 1:
lbl.config(text='b')
def changetext():
if a == 2:
lbl.config(text='c')
lbl = Label(win,text='a')
lbl.grid(row=1,column=1)
btn = Button(win,text='u', command =changetext)
btn.grid(row=2,column=1)
win.mainloop()```
In python, variables inside functions are local, which means that if you define a variable a = 0 outside the function, then do a = 1 in the function, the a equals 1 inside the function but it still equals 0 outside. If you want to change the value of a outside the function from inside the function, you need to declare a as a global variable (see code below).
import tkinter as tk # avoid import * to because it leads to naming conflicts
win = tk.Tk()
win.title('ab')
i = 0
letters = "abcdefghijklmnopqrstuvwxyz"
def changetext():
global i # change value of i outside function as well
i += 1
i %= 26 # cycle through the alphabet
lbl.configure(text=letters[i])
lbl = tk.Label(win, text='a')
lbl.grid(row=1, column=1)
btn = tk.Button(win,text='u', command=changetext)
btn.grid(row=2, column=1)
win.mainloop()
You can use itertools.cycle to create a cycle list and then use next() function to get the next item in the cycle list:
import tkinter as tk
from itertools import cycle
words = cycle(['hello', 'world', 'python', 'is', 'awesome'])
root = tk.Tk()
lbl = tk.Label(root, text=next(words), width=20)
lbl.pack()
tk.Button(root, text='Next', command=lambda: lbl.config(text=next(words))).pack()
root.mainloop()
I actually used the first method and adapted it by making the variable global because then it will update it for all the functions making my first method work
from tkinter import *
win = Tk()
win.title('ab')
i = 0
def changetext():
global i
i = i + 1
if i == 1:
lbl.config(text='word 2')
if i == 2:
lbl.config(text='word 1 ')
lbl = Label(win,text='a')
lbl.grid(row=1,column=1)
btn = Button(win,text='u', command =changetext)
btn.grid(row=2,column=1)
win.mainloop()

How Do I Wrap Text In A Tkinter Python Notebook Tab And Keep The Width Of Tkinter Notebook Tab Constant

I have a pretty simple question really. So I am making a To - Do List, and it requires having a Tkinter Notebook element that will have tabs on the side with the title and date of people's notes, and on the right will be the text editor where the user will store their notes.
However, what is happening is that I cannot set a width and height that is the same for each of the tabs. The tabs keep increasing and decreasing in size based on the user's note title, but what I want to happen is either only some of the title to appear and dissappear when the title's text goes over the size of the width of the tabs. Otherwise, what I would like to happen is for the text to wrap and the height to increase (but in no case the width).
In the following code the titles are the one one one, two two two, and three three three text. I have tried to use .grid_propagate(FALSE) with no luck.
from tkinter import *
import datetime
from tkinter import ttk
from tkinter.scrolledtext import ScrolledText
import tkinter as tk
root = Tk()
root.columnconfigure(2, weight=1)
root.rowconfigure(1, weight=1)
root.title("To - Do List")
root.geometry("1200x600")
root.configure(background = "white")
# Variable list:
style = ttk.Style()
style.configure(root, tabposition = "wn")
TasksList = ttk.Notebook(root)
Task1 = tk.Frame(TasksList, bg='white', width=600, height=500)
TasksList.add(Task1,text = 'One One One One One One')
Task1.grid_propagate(FALSE)
Task2 = tk.Frame(TasksList, bg='white', width=600, height=200)
TasksList.add(Task2, text = 'Two Two Two Two Two Two')
Task2.grid_propagate(FALSE)
Task3 = tk.Frame(TasksList, bg = "white", width = 600, height = 200)
TasksList.add(Task3, text = "Three Three Three Three Three Three Three")
Task3.grid_propagate(FALSE)
I don't think you can do anything about the widths of the tabs. Given the design you are trying to achieve, the simplest solution is to not use the notebook widget. Instead, just use a single text widget, with a listbox to display the list of notes.
In the following example, notes are stored in a dictionary (self.notes), with the keys being the note titles. For the sake of simplicity this doesn't handle the case of notes with identical titles, and new notes are appended without sorting. It also only saves a note when you switch notes.
import tkinter as tk
class Example():
def __init__(self):
root = tk.Tk()
self.lb = tk.Listbox(root, width=10)
self.text = tk.Text(root)
self.lb.pack(side="left", fill="y")
self.text.pack(side="right", fill="both", expand=True)
self.current = None
self.notes = {}
self.lb.bind("<<ListboxSelect>>", self.show_item)
def show_item(self, event=None):
selection = self.lb.curselection()
if selection:
index = selection[0]
title = self.lb.get(index)
# save current note
if self.current:
self.notes[self.current] = self.text.get("1.0", "end-1c")
# show new item
self.current = title
self.text.delete("1.0", "end")
self.text.insert("1.0", self.notes[title])
def add_note(self, title, body):
self.notes[title] = body
self.lb.insert("end", title)
example = Example()
example.add_note("note one", "this is the body of note one")
example.add_note("Note with a really long title",
"this is the body of the note with a long title")
example.add_note("x", "this is the body of note x")
tk.mainloop()

iterate through a list and get user response to each item using tkinter GUI

I am being particularly obtuse. I am iterating through a list of technical italian words and wanting to insert a translation using a tkinter interface. There is no problem doing this without the GUI: My problem is that I cannot figure out how to do an iteration, load a word into a ttk.Label and wait for a user entry in a ttk.Entry field. I have searched and found explanations, but I am at a loss how to apply the suggestions. This is my code using a trivial list of words:
from tkinter import ttk
import tkinter as tk
def formd():
list_of_terms = ['aardvark', 'ant','zombie', 'cat', 'dog', 'buffalo','eagle', 'owl','caterpiller', 'zebra', 'orchid','arum lily' ]
discard_list = []
temp_dict={}
list_of_terms.sort()
for item in list_of_terms:
listKey.set(item)
# need to wait and get user input
translation =dictValue.get()
temp_dict[item]=translation
discard_list.append(item)
# check if it has worked
for key, value in temp_dict.items():
print(key, value)
# GUI for dict from list
LARGE_FONT= ("Comic sans MS", 12)
root = tk.Tk()
root.title('Nautical Term Bilingual Dictionary')
ttk.Style().configure("mybtn.TButton", font = ('Comic sans MS','12'), padding = 1, foreground = 'DodgerBlue4')
ttk.Style().configure('red.TButton', foreground='red', padding=6, font=('Comic sans MS',' 10'))
ttk.Style().configure('action.TLabelframe', foreground = 'dodger blue3')
#.......contents frames.....................
nb = ttk.Notebook(root)
page5 = ttk.Frame(nb)
# declare variables
listKey= tk.StringVar()
dictValue = tk.StringVar()
# widgets
keyLabel =ttk.Label( page5, text = "key from list", font=LARGE_FONT).grid(row=3, column = 0)
Keyfromlist =ttk.Label(page5, width = 10, textvariable = listKey).grid(row = 3, column = 1)
valueLabel =ttk.Label( page5, text = "enter translation", font=LARGE_FONT).grid(row=3, column = 2)
listValue =ttk.Entry(page5, textvariable =dictValue, width = 15).grid(row = 3, column = 3)
#listValue.delete(0,'end')
#listValue.focus_set()
# add buttons
b1 = ttk.Button(page5, text="add to dictionary",style = "mybtn.TButton", command = formd)
b1.grid(row = 5, column = 0)
b5 = ttk.Button(page5, text="clear entry", style ="mybtn.TButton")
b5.grid(row = 5, column = 2)
nb.add(page5, text='From List')
nb.pack(expand=1, fill="both")
for child in root.winfo_children():
child.grid_configure(padx =5, pady=5)
if __name__ == "__main__":
root.mainloop()
I wonder whether someone could take the time to suggest a solution, please. How to stop a while loop to get input from user using Tkinter? was the one suggestion that I cannot figure how to use in my example
tkinter doesn't "play nice" with while loops.
Fortunately for you, you don't need to use one.
You can do something like the below:
from tkinter import *
class App:
def __init__(self, root):
self.root = root
self.list = ['aardvark', 'ant','zombie', 'cat', 'dog', 'buffalo','eagle', 'owl','caterpiller', 'zebra', 'orchid','arum lily' ]
self.text = Label(self.root, text="aardvark")
self.entry = Entry(self.root)
self.button = Button(self.root, text="Ok", command=self.command)
self.text.pack()
self.entry.pack()
self.button.pack()
def command(self):
print(self.text.cget("text")+" - "+self.entry.get())
try:
self.text.configure(text = self.list[self.list.index(self.text.cget("text"))+1])
except IndexError:
self.entry.destroy()
self.button.destroy()
self.text.configure(text = "You have completed the test")
root = Tk()
App(root)
root.mainloop()
This essentially uses the Button widget to iterate to the next text and get the next input.

Resources