Related
I have the following bits of code that creates a toplevel window and parses a dictionary into a Text widget:
def escrito(**kwargs):
write_window = Toplevel(root)
#write_window.title(kwargs) (problematic code)
writing_box = tk.Text(write_window, font = ("calibri", 20), width = 60, height = 15, wrap=WORD)
writing_box.pack(expand = tk.YES, fill = tk.X)
writing_box.grid(row = 0, column = 0, sticky = 'nswe')
texto = '\n'.join(key + ":\n" + value for key, value in kwargs.items())
writing_box.insert("1.0", texto)
def septic_osteo():
escrito(**infections.Septic_arthritis)
Septic_arthritis = {
'Empirical Treatment':
'Flucloxacillin 2g IV 6-hourly',
'If non-severe penicillin allergy':
'Ceftriaxone IV 2g ONCE daily',
'If severe penicillin allergy OR if known to be colonised with
MRSA':
'Vancomycin infusion IV, Refer to Vancomycin Prescribing
Policy',
'If systemic signs of sepsis': 'Discuss with Consultant
Microbiologist'
}
So when I run the code, the escrito functions parses the dictionary and writes its content onto a text widget contained on a Toplevel window. What I would like to know is how to dynamically rename the Toplevel window with the dicitonary's name. I do know that I can do this:
def septic_osteo():
escrito(**infections.Septic_arthritis)
write_window.title('Septic_arthritis)
but I do have like 100 functions like the one above, so, aside from labour intensive, I am not sure is the more pythonic way, so, is there a way that the window can be renamed with the dictionary name? (i.e. 'Septic_arthritis)
Thanks
If your data is in an object named infections, with attributes such as Septic_arthritis, the most straight-forward solution is to pass the data and the attribute as separate arguments, and then use getattr to get the data for the particular infection.
It would look something like this:
def escrito(data, infection):
write_window = Toplevel(root)
write_window.title(infection)
writing_box = tk.Text(write_window, font = ("calibri", 20), width = 60, height = 15, wrap="word")
writing_box.pack(expand = tk.YES, fill = tk.X)
writing_box.grid(row = 0, column = 0, sticky = 'nswe')
texto = '\n'.join(key + ":\n" + value for key, value in getattr(data, infection).items())
writing_box.insert("1.0", texto)
The important bit about the above code is that it uses getattr(data, infection) to get the data for the given infection.
If you want to create a button to call this function, it might look something like this:
button = tk.Button(..., command=lambda: escrito(infections, "Septic_arthritis"))
This will call the command escrito with two arguments: the object that contains all of the infections, and the key to the specific piece of information you want to display.
This question already has answers here:
Why does Tkinter image not show up if created in a function?
(5 answers)
Closed 2 years ago.
I'm creating a little program to show if something is ok, but I have a problem with the images. It was working until I create a function to generate my page
Without images
def genTout(matInput):
#images
vert_off = PhotoImage(file=r".\asset\vert_off.png")
rouge_off = PhotoImage(file=r".\asset\rouge_off.png")
vert_on = PhotoImage(file=r".\asset\vert_on.png")
rouge_on = PhotoImage(file=r".\asset\rouge_on.png")
#frame
rightFrame = Frame(highlightbackground="black",highlightthickness=5 ,bg="grey")
buttonControl = Frame(rightFrame,highlightbackground="black",highlightthickness=5 ,bg="grey")
for i in range(0,4):
Label(buttonControl,text=i+1).grid(row=0,column=i+2)
Label(buttonControl,image=(vert_on if matInput[i*2] == 0 else vert_off)).grid(row=1,column=i+2)
Label(buttonControl,image=(rouge_on if matInput[i*2+1] == 0 else rouge_off)).grid(row=2,column=i+2)
return frame
When i take my code on the main it's working but if I put the code inside a function no images appear
Here is my main where I get the return
root = Tk()
PanelView = PanedWindow(width=100, bd=5,relief="raised",bg="grey")
PanelView.pack(fill=BOTH,expand=1)
#my side bar code
...
rightFrame = genTout(matInput)
PanelView.add(rightFrame)
I saw that problem often already here : Your PhotoImage objects are getting garbage collected when the function finishes / returns. Tkinter somehow doesn't like that (for example Button's can get garbage collected, Tkinter somehow deals with it, but that's not the case for PhotoImage's). You must somehow save these images, for example by creating the frame first thing in your function and then making the images attributes of the frame, like this:
def genTout(matInput):
#frame
rightFrame = Frame(highlightbackground="black", highlightthickness=5 , bg="grey")
buttonControl = Frame(rightFrame, highlightbackground="black", highlightthickness=5, bg="grey")
#images
rightFrame.vert_off = PhotoImage(file=r".\asset\vert_off.png")
rightFrame.rouge_off = PhotoImage(file=r".\asset\rouge_off.png")
rightFrame.vert_on = PhotoImage(file=r".\asset\vert_on.png")
rightFrame.rouge_on = PhotoImage(file=r".\asset\rouge_on.png")
for i in range(0, 4):
Label(buttonControl, text=i + 1).grid(row=0, column=i + 2)
Label(buttonControl, image=(rightFrame.vert_on if matInput[i * 2] == 0 else rightFrame.vert_off)).grid(row=1, column=i+2)
Label(buttonControl, image=(rightFrame.rouge_on if matInput[i * 2 + 1] == 0 else rightFrame.rouge_off)).grid(row=2, column=i + 2)
return frame
I've been given a lot of text and asked to display it in a tkinter app. The text has a lot of html tags like <em>...<\em>, and <sup>...<\sup> where the text needs to be italicized or superscript.
Is there any way built into tkinter to do this? If not, is it even possible to write a function to, for example, italicize all text between <em> tags, then delete the tags?
I know I would be able to remove the tags by doing something like:
for tag in ["<em>", "<\em>", "<sup>", "<\sup>"]:
text = "".join(text.split(tag))
But I really need to, at least, italicize the text between <em> tags before removing them.
I'm new to tkinter, and I've been watching a lot of tutorials and googling for solutions, but it seems like tkinter can't naturally use html tags, and I can't find any solution.
EDIT:
I need to display this in a regular tkinter text widget.
I know I can use tkinter's font method with slant=italic to set text in a text box to italic. I just need to know a way to set the parameters to everything between <em> tags.
So, I worked this out myself over the last few days. First you have find the places in the text that you want to italicize, removing the html tags from the text as you go along, next you have to put the tag-free text into a text widget, then you have to identify the points in the widget's text to italicize.
It's a bit finicky because identifying points in the text-widget's text requires a decimal input where the number before the decimal point represents the line number, and the number after the decimal represents the index of the character in that line. This means you need to identify line numbers for each index, so you need a way of knowing exactly where one line ends and another begins. Also, line 2, character 4 is 2.4, and line 2, character 40 is 2.40 so Float(f"{line_number}.{character_number}") won't work as it will remove any trailing zeros, you have to use Decimal(f"{line_number}.{character_number}").
For example, in the text alphabet = 'abcd efg hijk\nlmnop qrs tuv wx yz', if you want to italicize all of the letters from "h" to "p" you first have to get an index for "h" to start italicizing at, start = alpha.find("h"), then after p to stop italicizing at, end = alphabet.find("p") + 1. Next you have to find which line the start point and end point are on and translate the indices (9 and 19 respectively) to decimal format (1.9 and 2.5):
start_line = alphabet[:start].count("\n") + 1
end_line = alphabet[:end].count("\n") + 1
line_start_point = len(alphabet[alphabet[:start].rfind("\n") + 1: start])
line_end_point = len(alphabet[alphabet[:end].rfind("\n") + 1: end])
start_point = Decimal(f"{start_line}.{line_start_point}")
end_point = Decimal(f"{end_line}.{line_end_point}")
Anyway, here's all of the code I ended up using to remove the unnecessary <sup>...</sup> tags and anything between them, and to italicize the everything between <em>...</em> tags:
from decimal import Decimal
from tkinter import *
from tkinter import font
def em_points(text):
suppat = re.compile(r'<sup>\w*</sup>')
suppatiter = suppat.findall(text)
if suppatiter:
for suptag in suppatiter:
text = "".join(text.split(suptag))
finds = list()
if "<em>" in text:
find_points = list()
emcount = text.count("<em>")
for _ in range(emcount):
find_open = text.find("<em>")
text = text[:find_open] + text[find_open + 4:]
find_close = text.find("</em>")
text = text[:find_close] + text[find_close + 5:]
find_points.append([find_open, find_close])
for points in find_points:
finds.append(text[points[0]: points[1]])
return [text, finds]
def italicize_text(text_box, finds):
italics_font = font.Font(text_box, text_box.cget("font"))
italics_font.configure(slant="italic")
text_box.tag_configure("italics", font=italics_font)
text_in_box = text_box.get(1.0, END)
used_points = list()
for find in finds:
if find not in text_in_box:
raise RuntimeError(f"Could not find text to italicise in textbox:\n {find}\n {text_in_box}")
else:
start_point = text_in_box.find(find)
end_point = start_point + len(find)
found_at = [start_point, end_point]
if found_at in used_points:
while found_at in used_points:
reduced_text = text_in_box[end_point:]
start_point = end_point + reduced_text.find(find)
end_point = start_point + len(find)
found_at = [start_point, end_point]
used_points.append(found_at)
text_to_startpoint = text_in_box[:start_point]
text_to_endpoint = text_in_box[:end_point]
start_line = text_to_startpoint.count("\n") + 1
end_line = text_to_endpoint.count("\n") + 1
if "\n" in text_to_startpoint:
line_start_point = len(text_in_box[text_to_startpoint.rfind("\n") + 1: start_point])
else:
line_start_point = start_point
if "\n" in text_to_endpoint:
line_end_point = len(text_in_box[text_to_endpoint.rfind("\n") + 1: end_point])
else:
line_end_point = end_point
start_point = Decimal(f"{start_line}.{line_start_point}")
end_point = Decimal(f"{end_line}.{line_end_point}")
text_box.tag_add("italics", start_point, end_point)
em_text = em_points(text)
clean_text = em_text[0]
em_list = em_text[1]
text_box = Text(root, width=80, height=5, font=("Courier", 12))
text_box.insert(1.0, clean_text)
italicize_text(text_box, em_list)
In some cases (demo below) the value shown on the OpenMenu widget does not match that used by the program, this causes option B to be done when the user is expecting option A - causing a WTF?? reaction by the user.
Unfortunately the OptionMenu widget does not have the "command" option that I've used with other widgets to easily handle the problem (e.g. "A_Button" widget in demo). I've tried using bindings but so far I haven't a "magic bullet" that fixes the problem.
I've checked the usual places (NMT, effbot, here, etc.) and found close to no useful documention on this widget, especially when it comes to working with the items on the dropdown list. (Knowing how to determine the number of items in the list, the position/index of the currently selected value in the list and how to include the widget in the GUI's tab sequence would be useful).
My application is multilingual; when the language changes the displayed value and the dropdown list of the OptionMenu widget must change accordingly. (BTW, multilingualism means that you cannot directly use the results of .get() in the code, especially if another language gets added. To get language independence I'm using an index created by matching the .get() value to the values in the option menu - is there a better method? )
In the demo code the chosen language determines the values shown by the OptionMenu widget. "Use the date" (a Button widget) is exactly how the real application gets launched (and why it's mandatory that the GUI and program values match at all times - which is is not always happening). In contrast, "What day is it??" (also a Button widget) uses the command option to implement the expected/correct behaviour - as has been successfully done many times in my application.
To see the problem run the demo, selecting any language. Without changing languages change the day several times. Note that the printed value (used by my application) is always one selection behind that shown on the GUI widget. Next, without changing days, select a different language (the new language gets printed). The OptionMenu dropdown values do not change until after the mouse leaves the OptionMenu widget - and its displayed value never gets "translated" into the new language.
What am I overlooking/missing/doing wrong?
from tkinter import Button, IntVar, Label, OptionMenu, Radiobutton, Tk, StringVar
# You can skip over this section, it's basic gruntwork needed to set up the demo (values used have no impact on the problem).
English = 0
French = 1
Spanish = 2
DayNumbers = 3
DefaultDay = 2 # Corresponds to Tuesday, emulates the user's choice of day (on the GUI)
DefaultLanguage = English
Languages = [ "English", "French", "Spanish", "Day Numbers" ] # for use on GUI
EnglishWeekdays = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]
FrenchWeekdays = [ "dimanche", "lundi", "mardi", "mecredi", "jeudi", "vendredi", "samedi" ]
SpanishWeekdays = [ "domingo", "lunes", "martes", "miercoles", "jeuves", "viernes", "sabado" ]
NumberedWeekdays = [ "Day 0", "Day 1", "Day 2", "Day 3", "Day 4", "Day 5", "Day 6" ]
DayNames = [ EnglishWeekdays, FrenchWeekdays, SpanishWeekdays, NumberedWeekdays ]
# The variables
LanguageInUse = DefaultLanguage
Weekdays = DayNames[ LanguageInUse ]
Today = DefaultDay # Isolates application code from language on GUI
#-------------------------------------------------------------------------------
def ChooseLanguage( ParentFrame ) :
global LanguageInUse, DropdownMenu
GUI_Language = IntVar( value = LanguageInUse )
#---------------------------------------------------------------------------
def SwitchLanguage():
global LanguageInUse , Weekdays
LanguageInUse = GUI_Language.get()
print( "SwitchLanguage sets language index to", LanguageInUse, "(" + Languages[ LanguageInUse ] + ")" )
Weekdays = DayNames[ LanguageInUse ]
DropdownMenu[ 'menu' ][ 'title' ] = Weekdays[ Today ]
for i, DayName in enumerate( Weekdays ) :
DropdownMenu[ 'menu' ].entryconfig( i )['label' ] = DayName
return
#---------------------------------------------------------------------------
LanguageButton = []
for LanguageIndex, Language in enumerate( Languages ) :
LanguageButton = LanguageButton + [ Radiobutton( ParentFrame,
indicatoron = False, width = 12,
variable = GUI_Language, command = lambda: SwitchLanguage(),
text = Language, value = LanguageIndex ) ]
LanguageButton[ LanguageIndex ].grid( row = 0 , column = LanguageIndex )
return
#-------------------------------------------------------------------------------
def GetDayIndex() :
global Today, DropdownMenu
Today = 0
for Index, DayName in enumerate( Weekdays ) :
if ( GUI_Value.get() == DayName ) :
Today = Index
break
print( "GetDayIndex sets weekday index to", Today, "(" + Weekdays[ Today ] + ")" )
for i, j in enumerate( Weekdays ) :
DropdownMenu[ 'menu' ].entryconfig( i , label = j )
return
#-------------------------------------------------------------------------------
def DoSomethingUseful() :
print( " Program uses " + str( Today ) + " (" + Weekdays[ Today ] +")" )
return
#-------------------------------------------------------------------------------
# The mainline
root = Tk()
GUI_Value = StringVar( value = Weekdays[ Today ] )
Widget1 = Label( root, text = "Today is" )
Widget1.grid( row = 1, column = 0 )
DropdownMenu = OptionMenu( root, GUI_Value, *DayNames[ LanguageInUse ] ) # NOT in TAB key sequence !!!
DropdownMenu.grid( row = 1, column = 1 )
DropdownMenu.bind( "<Leave>", lambda _ : GetDayIndex() )
#OptionMenu_Configuration( DropdownMenu )
A_Button = Button( root, text = "What day is it??", command = lambda : GetDayIndex() )
B_Button = Button( root, text = "Use the date", command = lambda: DoSomethingUseful() )
A_Button.grid( row = 1, column = 2 )
B_Button.grid( row = 1, column = 3 )
ChooseLanguage( root ) # creates/manages the language choice widgets
root.mainloop()
Your program's logic isn't clear for me, especially '<Leave>' binding, but lets try to answer your questions in general:
Unfortunately the OptionMenu widget does not have the "command" option that I've used with other widgets to easily handle the problem (e.g. "A_Button" widget in demo).
You're wrong, because it's has that option:
import tkinter as tk
def dropdown_callback(selected=None):
print('Currently selected value is:\t%s' % selected)
root = tk.Tk()
str_var = tk.StringVar()
dropdown_menu = tk.OptionMenu(root, str_var, *['foo', 'bar', 'baz'], command=dropdown_callback)
dropdown_menu.pack()
root.mainloop()
Also, you can specify a separate command for each of your entries (rarely useful):
import tkinter as tk
def dropdown_callback(event=None):
print('Currently selected value is:\t%s' % event)
def dropdown_callback_foo():
print('Called callback is:\t%s' % dropdown_callback_foo.__name__)
def dropdown_callback_bar():
print('Called callback is:\t%s' % dropdown_callback_bar.__name__)
root = tk.Tk()
str_var = tk.StringVar()
dropdown_menu = tk.OptionMenu(root, str_var, *['foo', 'bar', 'baz'], command=dropdown_callback)
dropdown_menu._OptionMenu__menu.entryconfig(0, command=dropdown_callback_foo)
dropdown_menu._OptionMenu__menu.entryconfig(1, command=dropdown_callback_bar)
dropdown_menu.pack()
root.mainloop()
...the position/index of the currently selected value in the list...
And again, there's an option for that:
import tkinter as tk
def dropdown_callback(selected=None):
selected_index = root.tk.call(dropdown_menu.menuname, 'index', selected)
print('Currently selected value is:\t%s\t\ton position:\t%d' % (selected, selected_index))
root = tk.Tk()
str_var = tk.StringVar()
dropdown_menu = tk.OptionMenu(root, str_var, *['foo', 'bar', 'baz'], command=dropdown_callback)
dropdown_menu.pack()
root.mainloop()
...determine the number of items in the list...
It's also achievable, since number of items is just a last index + 1:
import tkinter as tk
def dropdown_callback(selected=None):
selected_index = root.tk.call(dropdown_menu.menuname, 'index', selected)
total_count = root.tk.call(dropdown_menu.menuname, 'index', 'end') + 1
print('Currently selected value is:\t%s\t\ton position:\t%d\t\twith total count:\t%d'
% (selected, selected_index, total_count))
root = tk.Tk()
str_var = tk.StringVar()
dropdown_menu = tk.OptionMenu(root, str_var, *['foo', 'bar', 'baz'], command=dropdown_callback)
dropdown_menu.pack()
root.mainloop()
From this point I think, that now your confusions with OptionMenu are solved. Except a stacking order, for sure, but you can replace your OptionMenu with ttk.Combobox at any time. There's no such <Tab>, behavior for Menu widgets, because they react differently on commands like lift. Of course, it's achievable too, but it's another question, because of many and more of "what if"'s!
That's all!
I'm trying to implement system where when the user points to an object, a text box appears with certain information which I haven't implemented yet, then disappears when they move their mouse away. I'm trying to do that by binding the < Enter > and < Leave > commands, but nothing happens when I run the following code, except that in the terminal it says that destroy requires two arguments, so I know it is calling the functions.
from tkinter import *
xhig, yhig = 425,325
bkgnclr = '#070707'
currentmouseoverevent = ''
c = Canvas(master, width=xhig*2, height=yhig*2, bg=bkgnclr, cursor = 'crosshair',)
def mouseovertext(event):
mouseover = "Jack"
currentmouseoverevent = event
c.create_rectangle(bbox=(event.x,event.y, (event.x + 5), (event.y +len(mouseover)*5)),outline="white", fill=bkgnclr, width= len(mouseover))
c.create_text(position=(event.x,event.y),text=mouseover, fill="white", currentmouseoverevent=event)
def closemouseover(x):
c.destroy(currentmouseoverevent)
c.bind("<Enter>", mouseovertext)
c.bind("<Leave>", closemouseover)
What arguments does destroy take, and why is the rectangle not being created?
A bounding box (bbox) in tkinter is a 4-tuple which stores the bounds of the rectangle. You are only passing in the mouse location, which is a 2-tuple.
Also, you are never actually assigning to the variable "currentmouseoverevent" before using it in the code you show, so your closemouseover function will fail.
The corrected code is as follows.
It turns out I was calling bbox wrong. Instead of passing the coords as a tuple, I should have passed them as the first four agrguments of create_rectangle. c.destroy is only for objects like canvas, entry or textbox, instead I used c.delete for deleting items, and used the event number returned by c.create_rectangle and c.create_text.
from tkinter import *
xhig, yhig = 425,325
bkgnclr = '#070707'
currentmouseoverevent = ['','']
c = Canvas(master, width=xhig*2, height=yhig*2, bg=bkgnclr, cursor = 'crosshair',)
def mouseovertext(event):
mouseover = "Jack"
if currentmouseoverevent[0] != '':
closemouseover()
currentmouseoverevent[0]=''
return
currentmouseoverevent[0] = c.create_rectangle(event.x,event.y, (event.x + 5), (event.y +len(mouseover)*5),outline="white", fill=bkgnclr, width= len(mouseover))
currentmouseoverevent[1] = c.create_text(event.x,event.y,text=mouseover, fill="white", currentmouseoverevent=event,anchor=NW)
def closemouseover(x):
c.delete(currentmouseoverevent[0])
c.delete(currentmouseoverevent[1])
c.bind("<Button-3", mouseovertext)