I am trying to create a Python script to highlight specific patterns in a .txt file. To do this, I have altered a script which used Tkinter to highlight a given set of data. However, the files I tend to get it to process are around 10000 lines, which results in slow scrolling as I think it renders everything - whether it is on the screen or not (correct me if I'm wrong). Is it possible to alter my code such that it renders the output in a more efficient way? I have tried searching for a means to do this, but have not found anything myself.
My code is as follows:
from Tkinter import *
class FullScreenApp(object):
def __init__(self, master, **kwargs):
self.master=master
pad=3
self._geom='200x200+0+0'
master.geometry("{0}x{1}+0+0".format(
master.winfo_screenwidth()-pad, master.winfo_screenheight()-pad))
master.bind('<Escape>',self.toggle_geom)
def toggle_geom(self,event):
geom=self.master.winfo_geometry()
print(geom,self._geom)
self.master.geometry(self._geom)
self._geom=geom
root = Tk()
app = FullScreenApp(root)
t = Text(root)
t.pack()
#Import file
with open('data.txt') as f:
for line in f:
t.insert(END, line)
#Search terms - Leave blank if not required
search_term0 = '0xCAFE'
search_term1 = '0x0011'
search_term2 = '0x961E'
search_term3 = '0x0000'
search_term4 = ''
#Assigns highlighted colours for terms not blank
t.tag_config(search_term0, background='red')
if search_term1 != '':
t.tag_config(search_term1, background='red')
if search_term2 != '':
t.tag_config(search_term2, background='red')
if search_term3 != '':
t.tag_config(search_term3, background='red')
if search_term4 != '':
t.tag_config(search_term4, background='red')
#Define search
#Requires text widget, the keyword, and a tag
def search(text_widget, keyword, tag):
pos = '1.0'
while True:
idx = text_widget.search(keyword, pos, END)
if not idx:
break
pos = '{}+{}c'.format(idx, len(keyword))
text_widget.tag_add(tag, idx, pos)
#Search for terms that are not blank
search(t, search_term0, search_term0)
if search_term1 != '':
search(t, search_term1, search_term1)
if search_term2 != '':
search(t, search_term2, search_term2)
if search_term3 != '':
search(t, search_term3, search_term3)
if search_term4 != '':
search(t, search_term4, search_term3)
root.mainloop()
An example of the data in a file is given in the following link: here
Many thanks for your time, it is really appreciated.
Assuming MCVE is the following:
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt.pack()
root.mainloop()
Analysis
Based on this I don't think it is a rendering issue. I think it's an issue with having a fixed rate of registering <KeyPress> events. Meaning that the number of events registered per second is fixed, even though the hardware may be capable of registering at a faster rate. A similar regulation should be true also for the mouse-scroll event.
Solutions for rendering
Perhaps slicing text for a buffer proportion of txt['height'] would help. But isn't that how Tk supposed to be rendering anyway?
Solutions for rendering unrelated issue
If a step would be defined as the cursor's movement to the previous or the next line, for every registered event of Up or Down; then scrolling_speed = step * event_register_frequency.
By increasing the step size
An easy workaround would be to simply increase the step size, as in increasing the number of lines to jump, for each registration of the key bind.
But there's already such default behavior, assuming the page length > 1 line, Page Up or Page Down has a step size of a page. Which makes the scrolling speed increase, even though the event registration rate remains the same.
Alternatively, a new event handler with a greater step size may be defined to call multiple cursor movements for each registration of Up and Down, such as:
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
def step(event):
if txt._step_size != 1:
_no_of_lines_to_jump = txt._step_size
if event.keysym == 'Up':
_no_of_lines_to_jump *= -1
_position = root.tk.call('tk::TextUpDownLine', txt, _no_of_lines_to_jump)
root.tk.call('tk::TextSetCursor', txt, _position)
return "break"
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt._step_size = 12
txt.bind("<Up>", step)
txt.bind("<Down>", step)
txt.pack()
root.mainloop()
By mimicking keypress event registry rate increase:
As mentioned in here actually modifying keypress registry rate is out of scope of Tk. Instead, it can be mimicked:
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
def step_up(*event):
_position = root.tk.call('tk::TextUpDownLine', txt, -1)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_up)
return "break"
def step_down(*event):
_position = root.tk.call('tk::TextUpDownLine', txt, 1)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_down)
return "break"
def stop(*event):
if txt._repeat_on:
txt._repeat_on = False
root.after(txt._repeat_freq + 1, stop)
else:
txt._repeat_on = True
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt._repeat_freq = 100
txt._repeat_on = True
txt.bind("<KeyPress-Up>", step_up)
txt.bind("<KeyRelease-Up>", stop)
txt.bind("<KeyPress-Down>", step_down)
txt.bind("<KeyRelease-Down>", stop)
txt.pack()
root.mainloop()
By both increasing step-size and mimicking registry rate increase
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
def step_up(*event):
_no_of_lines_to_jump = -txt._step_size
_position = root.tk.call('tk::TextUpDownLine', txt, _no_of_lines_to_jump)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_up)
return "break"
def step_down(*event):
_no_of_lines_to_jump = txt._step_size
_position = root.tk.call('tk::TextUpDownLine', txt, _no_of_lines_to_jump)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_down)
return "break"
def stop(*event):
if txt._repeat_on:
txt._repeat_on = False
root.after(txt._repeat_freq + 1, stop)
else:
txt._repeat_on = True
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt._step_size = 1
txt._repeat_freq = 100
txt._repeat_on = True
txt.bind("<KeyPress-Up>", step_up)
txt.bind("<KeyRelease-Up>", stop)
txt.bind("<KeyPress-Down>", step_down)
txt.bind("<KeyRelease-Down>", stop)
txt.pack()
root.mainloop()
So this is solved by something called multi threading. A computer can do multiple tasks at once, otherwise, your web experience wouldn't be the same. Here is a simple function that demonstrates mulit-threading
from threading import Thread
def execOnDifferentThread(funct=print, params=("hello world",)):
t = Thread(target=funct, args=params)
t.start()
Now note, this might not be the best example, but all you have to do to run a function in parallel now, is execOnDifferentThread(funct=A, params=B) where A is a function name, and B is a tuple of arguments that will be passed on to your function. Now, I don't want to write your code for you, but using this function, you can multi-thread certain parts of your code to make it faster. If you are truly stuck, just comment where and ill help. But please try on your own first, now that you have the power of multi-threading on your hands
Related
I got a problem with my code, here it is : I want to return the closest left and right child of a node, but whenever I try to, it returns me ALL the children of this node.
I separated the code in two files, one contains the binary tree class (not BST, just a simple BT), and the other contains tkinter things, that suppose to show the node and his children. See below for the code, and sorry for some of the parts in french, if u need words to be translated i'm up.
binary tree file :
class ArbreBinaire:
def __init__(self, valeur):
self.valeur = valeur
self.enfant_gauche = None
self.enfant_droit = None
def __str__(self):
return f"{self.valeur}, ({self.enfant_gauche}, {self.enfant_droit})"
[enter image description here](https://i.stack.imgur.com/KJ3Gr.png)
def insert_gauche(self, valeur):
if self.enfant_gauche == None:
self.enfant_gauche = ArbreBinaire(valeur)
else:
new_node = ArbreBinaire(valeur)
new_node.enfant_gauche = self.enfant_gauche
self.enfant_gauche = new_node
def insert_droit(self, valeur):
if self.enfant_droit == None:
self.enfant_droit = ArbreBinaire(valeur)
else:
new_node = ArbreBinaire(valeur)
new_node.enfant_droit = self.enfant_droit
self.enfant_droit = new_node
def get_valeur(self):
return self.valeur
def get_gauche(self):
return self.enfant_gauche
def get_droit(self):
return self.enfant_droit
tkinter and main parts file :
from arb import ArbreBinaire
from tkinter import *
def classe_arb():
global racine, b_node, f_node, c_node, g_node, h_node
arb = ArbreBinaire
racine = ArbreBinaire('A')
racine.insert_gauche('B')
racine.insert_droit('F')
b_node = racine.get_gauche()
b_node.insert_gauche('C')
b_node.insert_droit('D')
f_node = racine.get_droit()
f_node.insert_gauche('G')
f_node.insert_droit('H')
c_node = b_node.get_gauche()
c_node.insert_droit('E')
g_node = f_node.get_gauche()
g_node.insert_gauche('I')
h_node = f_node.get_droit()
h_node.insert_droit('J')
return arb
def accueil():
global fenetre
fenetre = Tk()
fenetre.title("Bienvenue dans l'énigme du manoir")
fenetre.geometry("1000x500")
bt_jouer= Button(fenetre, text="Jouer", fg="green", command=lambda: valeur_bouton(1))
bt_jouer.pack()
bt_quitter = Button(fenetre, text="Quitter", fg="red", command=quit)
bt_quitter.pack()
fenetre.mainloop()
def jeu(phrase, rep1, rep2):
global run
run = True
while run == True:
global wd_jeu
wd_jeu = Tk()
wd_jeu.title("L'énigme du manoir")
wd_jeu.geometry("1000x500")
label = Label(wd_jeu, text=phrase, bg = "blue", fg = "white", font = "Castellar")
label.pack()
option1 = Button(wd_jeu, text=rep1, fg="black")
option1.pack()
option2 = Button(wd_jeu, text=rep2, fg="black")
option2.pack()
wd_jeu.mainloop()
def valeur_bouton(nb):
if nb == 0:
None
if nb == 1:
fenetre.destroy()
jeu('Hello', racine.valeur, racine.enfant_gauche)
classe_arb()
accueil()
Okay, so basically the problem is in the last function : "racine.valeur" and "racine.enfant_gauche". The first one works very well, it returns me the node with no problem. The second one "racine.enfant_gauche" is supposed to return the closest left child of the A node (B in this case), but it prints me all the children : "B, (C, (None, E, (None, None)), D, (None, None))".
I tried many things like this, like getting the value with the method "get_gauche" but it doesnt work as well. Thanks in advance for your help.
The __str__ method is called recursively because the format string f"{self.valeur}, ({self.enfant_gauche}, {self.enfant_droit})" will use the __str__ method of each child to resolve the embedded text.
To avoid the recursion, you could define method to get values of the left and right (e.g. valeur_gauche(), valeur_droite()) and use those instead in the format string. You could also get the values within your __str__ function and assemble the resulting string accordingly.
The expression racine.enfant_gauche evaluates to an instance of ArbreBinaire, and so when you pass it as argument to jeu and pass it to the Button constructor, the node's __str__ method will be called to retrieve the string that must be printed. This __str__ method will create a string representation that includes all descendants.
The expression racine.valeur on the other hand is a string (a single letter in your example), and so when you pass that to jeu, which passes it to the Button constructor, it will render as that string (as expected).
If you want the same behaviour for the left child, you should also take the .valeur attribute, like so:
jeu('Hello', racine.valeur, racine.enfant_gauche.valeur)
Honestly, I do not feel like this should be happening, but it is.
self.marketList.bind('<<ListboxSelect>>', self.market_selected)
self.jobsList.bind('<<ListboxSelect>>', self.job_selected)
There really isn't any more interaction between these two functions. When you click on an item in the marketList box, it's supposed to bring up the jobs in the jobsList box. Currently, it is applying the binding two both boxes. When I click on a job entry in the jobsBox, it clears the jobs and my troubleshooting is showing that it's calling market_selected. I'm not sure why this is happening, but it's really messing with what I'm trying to do with it.
How can I ensure that my binding is on only one widget, and won't be applied to multiple widgets?
edit:
I'm told that this isn't enough code to reproduce the error.
This is all the relevant code.
As I said previously, self.market_selected is called when I click on anything in the jobsList
edit #2
I uploaded the entire script.
import MarketWizard
import JobWizard
import SpanWalkerDocuments as swd
import tkinter as tk
from tkinter import *
from tkinter import ttk
import SpanWalkerDocuments as SpanWalker
class SpanWalker:
def __init__(self):
self.root = tk.Tk()
self.root.title('Luke Spanwalker')
self.root.resizable(True, True)
self.MainFrame = Frame(self.root, bg='red')
self.sidebarFrame = Frame(self.root, bg='blue')
self.tabControl = ttk.Notebook(self.sidebarFrame)
self.marketFrame = Frame(self.tabControl)
self.clientFrame = Frame(self.tabControl)
self.jobsFrame = Frame(self.tabControl)
self.polesFrame = Frame(self.MainFrame, height=100, width=50)
self.tabControl.add(self.marketFrame, text="Markets")
self.tabControl.add(self.jobsFrame, text="Jobs")
self.tabControl.add(self.clientFrame, text="Clients")
#self.tabControl.add(self.polesFrame, text="Poles")
self.tabControl.pack(expand=1, fill="both")
self.MainFrame.grid(row=0, column=1)
self.sidebarFrame.grid(row=0, column=0)
#Awesome, our tabbed control is ready.
#Now, we need to make the listBox widgets that will actually display our data.
self.marketList = Listbox(self.marketFrame)
self.jobsList = Listbox(self.jobsFrame)
self.polesList = Listbox(self.polesFrame)
#Binding functions! Yay for binding functions!
self.marketList.bind('<<ListboxSelect>>', self.market_selected)
self.jobsList.bind('<<ListboxSelect>>', self.job_selected)
self.curMarket = ""
self.markets = []
self.jobs = []
self.poles = []
def UpdateMarkets (self):
if len(self.markets) > 0:
self.markets.clear()
self.marketList.delete(0, END)
for mark in swd.Market.objects:
self.markets.append(mark)
for m in range(0, len(self.markets)):
self.marketList.insert(m, self.markets[m].title)
def OpenMarketWizard(self):
mw = MarketWizard.MarketWizard()
mw.RunMarketWizard(self.root)
def market_selected(self, event):
print("Market Selected")
selection = self.marketList.curselection()
selectedMarket = ",".join([self.marketList.get(i) for i in selection])
self.PopulateJobs(selectedMarket)
def PopulateJobs(self, market):
self.jobs.clear()
self.jobsList.delete(0, END)
self.GetJobs(market)
for j in range(0, len(self.jobs)):
self.jobsList.insert(j, self.jobs[j].jobName)
def GetJobs(self, market):
marketJobs = []
jobs = []
if market =="":
return
for j in swd.Job.objects:
jobs.append(j)
print("Market = {0}".format(market))
for i in jobs:
if i.market == market:
self.jobs.append(i)
def job_selected(self, event):
print("Job Selected")
selection = self.jobsList.curselection()
selectedJob = ",".join([self.jobsList.get(i) for i in selection])
print("The selected job is: {0}".format(selectedJob))
def PopulateMarkets(self):
self.marketList.destroy()
for m in range(0, len(self.markets)):
marketList.insert(m, self.markets[m].title)
def OpenJobWizard(self):
jw = JobWizard.JobWizard()
jw.RunJobWizard(self.root)
#We need to pack everything that belongs in our tabbed function.
def marketListDisplay(self, show):
if show == True:
self.marketList.pack(fill="both")
self.UpdateMarkets()
self.newButton=Button(self.marketFrame, text="Open Market Wizard", command=lambda:self.OpenMarketWizard())
self.newButton.pack()
self.refreshButton=Button(self.marketFrame, text="Refresh Markets", command=lambda:self.UpdateMarkets())
self.refreshButton.pack()
else:
self.marketList.forget_pack()
def jobsListDisplay(self, show):
if show==True:
self.jobsList.pack()
self.newButton = Button(self.jobsFrame, text="Create New Job / Open Job Wizard", command=lambda:self.OpenJobWizard())
self.newButton.pack()
else:
self.jobsList.forget_pack()
def polesListDisplay(self, show):
if show==True:
self.polesList.pack()
else:
self.polesList.forget_pack()
#Query, why not put them all in a function?
sp = SpanWalker()
sp.marketListDisplay(True)
sp.polesListDisplay(True)
sp.jobsListDisplay(True)
The answer was given by acw1668. I just added exportselection=0 during the creation process for the listboxes, and it worked perfectly.
Sorry for the confusion, y'all, I've never really asked for help on here before.
Thanks for the help!
I have a problem in implementing a fast reader program with tkinter, and that is because it hangs after ~5 seconds.
I know that is because of the while loop but I didn't figured out a way to solve it. Maybe looping outside of the root mainloop?
Here is the code:
import tkinter
import tkinter.ttk
import tkinter.filedialog
import tkinter.messagebox
class FastReader(object):
SLEEP_TIME = 250
def __init__(self):
self.filename = self.getFile()
self.running = True
def window(self):
self.root = tkinter.Tk()
self.root.wm_title("Fast Reader.")
self.text = tkinter.ttk.Label(self.root, text = "word")
self.text.grid(row = 0, column = 0)
# Stop button
self.stop = tkinter.ttk.Button(self.root, text = "Stop", command = lambda: self.close())
self.stop.grid(row = 1, column = 1, columnspan = 2)
while self.running:
for word in self.getWords():
self.text.after(FastReader.SLEEP_TIME)
self.text.config(text = word)
self.text.update_idletasks()
self.root.update_idletasks()
self.root.mainloop()
def close(self):
self.running = False
def getFile(self):
file_ = tkinter.filedialog.askopenfilename()
return file_
def getWords(self):
with open(self.filename) as file_:
for line in file_:
for word in line.strip("\n").split(" "):
yield word
if __name__ == "__main__":
fr = FastReader()
fr.window()
Your program appears to hang because you process all of the words before you give the event loop a chance to update the display, then you put a call to the event loop in an infinite loop.
A good rule of thumb is to never have your own infinite loop in a Tkinter GUI. You already have an infinite loop running, so take advantage of it.
What you should do is write a function that puts the next word in the label, and then calls itself again in the future. It looks somewhat recursive, but it isn't quite. You're simply adding something on to the list of things that the event loop must do. Because it's not strictly recursive, you don't have to worry about running out of stack space.
A simple first step is to first read in all of the words at once. Then, your program removes the first word in the list and displays it in the label. If the list is not empty after that, schedule the command to run again after a delay. It looks something like this:
def showNextWord(self):
word = self.words.pop(0)
self.text.configure(text=word)
if len(self.words) > 0:
self.root.after(self.SLEEP_TIME, self.showNextWord)
You can then replace your while statement with two statements: one to get the complete list of words, and one to display the first word:
self.words = self.getWords()
self.showNextWord()
Of course, you need to modify your getWords() to return the whole list at once. You can use a generator, but it adds a tiny bit of complexity that doesn't seem necessary, unless you're planning on displaying millions of words (which, at four per second, would run for more than several days).
I've recently needed to do some GUI work with Python and stumbled on Tkinter. For the most part, I like it; it's clean and mostly intuitive and brief. There is, however, one little sticking point for me: sometimes it crashes out of nowhere. The program will run normally five times in a row and then the sixth time, halfway through it will freeze and I will get the error
Tcl_AsyncDelete: async handler deleted by the wrong thread
My efforts to find a solution to this problem, both on this website and others, all have to do with multithreading, but I don't use multiple threads. Not explicitly, anyway. I suspect the fact that I have a timer in the GUI is to blame, but I have not been able to figure out why the error pops up, or indeed why it is so infrequent.
What follows is the shortest tkinter program I've written. This happens in all of them, so I suspect the problem will be easiest to see here. Any and all help is appreciated, just don't point me toward another solution without telling me how it applies to my code, because I assure you, I have looked at it, and it either doesn't apply or I didn't understand how it did. I don't mind having missed the answer, but I'm new to tkinter (and to multithreading in general) so I might need it told more explicitly.
This code is for a simulation of a game show I saw online. Most of it can probably be safely ignored, but I'm pasting it all here because I don't know where the error is from.
import re
from tkinter import Tk, Frame, DISABLED, Button, Label, font, NORMAL, ttk
from random import choice
import winsound
class Question:
def __init__(self, t, a, o):
self.text = t.strip().capitalize() + "?"
self.answer = a.strip().title()
self.options = o
self.firstIsRight = self.answer.lower() == self.options[0].lower()
assert self.firstIsRight or self.answer.lower() == self.options[1].lower(), self
def __eq__(self, other):
return self.text == other.text
def __repr__(self):
return "{1} or {2}, {0}".format(self.text, self.options[0], self.options[1])
class Application(Frame):
def __init__(self, master=None):
self.setup()
Frame.__init__(self, master)
self.grid()
self.customFont = font.Font(family="Times New Roman", size=30)
self.createWidgets()
def setup(self):
self.questions = []
with open("twentyone.txt",'r') as file:
for line in file:
groups = re.split("[,\.\?]",line)
answers = re.split(" or ",groups[0])
self.questions.append(Question(groups[1], groups[2], answers))
def createWidgets(self):
self.gamePanel = Frame(self)
self.gamePanel.grid(column=0,row=0)
self.displayPanel = Frame(self)
self.displayPanel.grid(column=0,row=1)
self.buttonPanel = Frame(self)
self.buttonPanel.grid(column=0,row=2)
self.QUIT = Button(self.buttonPanel,text="QUIT",font=self.customFont,command=self.quit)
self.QUIT.grid(row=0,column=2)
self.BEGIN = Button(self.buttonPanel, text="BEGIN",font=self.customFont, command = self.begin)
self.BEGIN.grid(row=0,column=0)
self.STOP = Button(self.buttonPanel, text="STOP",font=self.customFont, command = self.stop)
self.STOP.grid(row=0,column=1)
self.STOP["state"] = DISABLED
self.TITLE = Label(self.gamePanel,text="21 Questions Wrong",font=self.customFont,bg="Black",fg="White")
self.TITLE.grid(columnspan=2)
self.questionText = Label(self.gamePanel,text="Questions go here",font=self.customFont,wraplength=400)
self.questionText.grid(row=1,columnspan=2)
self.leftChoice = Button(self.gamePanel,text="Option 1",font=self.customFont)
self.leftChoice.grid(row=2,column=0)
self.rightChoice = Button(self.gamePanel,text="Option 2",font=self.customFont)
self.rightChoice.grid(row=2,column=1)
self.timerText = Label(self.displayPanel, text="150",font=self.customFont)
self.timerText.grid(row=0)
self.progress = ttk.Progressbar(self.displayPanel, length=100,maximum=22)
self.progress.grid(row=0,column=1,padx=10)
def begin(self):
self.timer(250)
self.asked = []
self.STOP["state"] = NORMAL
self.leftChoice["state"] = NORMAL
self.rightChoice["state"] = NORMAL
self.restart = False
self.askNewQuestion()
def askNewQuestion(self):
if self.restart:
self.currentQuestion = self.asked[int(self.progress["value"])]
else:
self.currentQuestion = choice([i for i in self.questions if i not in self.asked])
self.asked.append(self.currentQuestion)
self.questionDisplay()
def questionDisplay(self):
self.questionText["text"] = self.currentQuestion.text
self.leftChoice["text"] = self.currentQuestion.options[0]
self.rightChoice["text"] = self.currentQuestion.options[1]
if self.currentQuestion.firstIsRight:
self.leftChoice["command"] = self.correct
self.rightChoice["command"] = self.wrong
else:
self.leftChoice["command"] = self.wrong
self.rightChoice["command"] = self.correct
def correct(self):
self.progress.step()
if self.progress["value"] >= 21:
self.gameOver(True, 21)
else:
if self.progress["value"] >= len(self.asked):
self.restart = False
self.askNewQuestion()
def wrong(self):
self.restart = True
self.progress["value"] = 0
winsound.Beep(750,700)
self.askNewQuestion()
def stop(self):
self.after_cancel(self.timerAfter)
self.BEGIN["state"] = NORMAL
self.STOP["state"] = DISABLED
def gameOver(self, success, longest):
self.after_cancel(self.timerAfter)
self.BEGIN["state"] = NORMAL
self.STOP["state"] = DISABLED
self.questionText["text"] = "Congratulations!" if success else "Too Bad!"
self.leftChoice["text"] = "Game"
self.leftChoice["state"] = DISABLED
self.rightChoice["text"] = "Over"
self.rightChoice["state"] = DISABLED
self.showPoints(success, longest)
def showPoints(self, s, l):
if s:
timeTaken = max(0, 100-int(self.timerText["text"]))
print("You scored {0} points".format(1000-10*timeTaken))
else:
print("You scored no points")
def timer(self, time):
self.BEGIN["state"] = DISABLED
self.STOP["state"] = NORMAL
self.runTimer(time)
def runTimer(self, current=None, resume=False):
if current is not None:
self.current = current
self.timerText["text"] = self.current
if self.current == 0:
self.gameOver(False, len(self.asked)-1)
else:
self.current -= 1
self.timerAfter = self.after(1000,self.runTimer)
root = Tk()
app = Application(master=root)
app.master.title("Score Calculator!")
app.anchor("center")
root.mainloop()
root.destroy()
I am trying to extend the ttk combobox class to allow autosuggestion. the code I have far works well, but I would like to get it to show the dropdown once some text has been entered without removing focus from the entry part of the widget.
The part I am struggling with is finding a way to force the dropdown, in the python docs I cannot find any mention of this, however in the tk docs I did find a post method I believe is supposed to do this, except it doesn't seem to be implemented in the python wrapper.
I also tried generating a down arrow key event once the autosuggest has taken place, however while this does show the dropdown it removes focus, and trying to set the focus after this event doesn't seem to work either (focus does not return)
Is anyone aware of a function I can use to achieve this?
The code I have is for python 3.3 using only standard libs:
class AutoCombobox(ttk.Combobox):
def __init__(self, parent, **options):
ttk.Combobox.__init__(self, parent, **options)
self.bind("<KeyRelease>", self.AutoComplete_1)
self.bind("<<ComboboxSelected>>", self.Cancel_Autocomplete)
self.bind("<Return>", self.Cancel_Autocomplete)
self.autoid = None
def Cancel_Autocomplete(self, event=None):
self.after_cancel(self.autoid)
def AutoComplete_1(self, event):
if self.autoid != None:
self.after_cancel(self.autoid)
if event.keysym in ["BackSpace", "Delete", "Return"]:
return
self.autoid = self.after(200, self.AutoComplete_2)
def AutoComplete_2(self):
data = self.get()
if data != "":
for entry in self["values"]:
match = True
try:
for index in range(0, len(data)):
if data[index] != entry[index]:
match = False
break
except IndexError:
match = False
if match == True:
self.set(entry)
self.selection_range(len(data), "end")
self.event_generate("<Down>",when="tail")
self.focus_set()
break
self.autoid = None
You do not need to inherit ttk.Combobox for this event; simply use event_generate to force the dropdown:
box = Combobox(...)
def callback(box):
box.event_generate('<Down>')
A workaround that achieves this UX using tooltips is demonstrated below. This is implemented using PySimpleGUI, but should be easily adaptable to "pure" tkinter.
from functools import partial
from typing import Callable, Any
from fuzzywuzzy import process, fuzz
import PySimpleGUI as sg
# SG: Helper functions:
def clear_combo_tooltip(*_, ui_handle: sg.Element, **__) -> None:
if tt := ui_handle.TooltipObject:
tt.hidetip()
ui_handle.TooltipObject = None
def show_combo_tooltip(ui_handle: sg.Element, tooltip: str) -> None:
ui_handle.set_tooltip(tooltip)
tt = ui_handle.TooltipObject
tt.y += 40
tt.showtip()
def symbol_text_updated(event_data: dict[str, Any], all_values: list[str], ui_handle: sg.Element) -> None:
new_text = event_data[ui_handle.key]
if new_text == '':
ui_handle.update(values=all_values)
return
matches = process.extractBests(new_text, all_values, scorer=fuzz.ratio, score_cutoff=40)
sym = [m[0] for m in matches]
ui_handle.update(new_text, values=sym)
# tk.call('ttk::combobox::Post', ui_handle.widget) # This opens the list of options, but takes focus
clear_combo_tooltip(ui_handle=ui_handle)
show_combo_tooltip(ui_handle=ui_handle, tooltip="\n".join(sym))
# Prepare data:
all_symbols = ["AAPL", "AMZN", "MSFT", "TSLA", "GOOGL", "BRK.B", "UNH", "JNJ", "XOM", "JPM", "META", "PG", "NVDA", "KO"]
# SG: Layout
sg.theme('DarkAmber')
layout = [
[
sg.Text('Symbol:'),
sg.Combo(all_symbols, enable_per_char_events=True, key='-SYMBOL-')
]
]
# SG: Window
window = sg.Window('Symbol data:', layout, finalize=True)
window['-SYMBOL-'].bind("<Key-Down>", "KeyDown")
# SG: Event loop
callbacks: dict[str: Callable] = {
'-SYMBOL-': partial(symbol_text_updated, all_values=all_symbols, ui_handle=window['-SYMBOL-']),
'-SYMBOL-KeyDown': partial(clear_combo_tooltip, ui_handle=window['-SYMBOL-']),
}
unhandled_event_callback = partial(lambda x: print(f"Unhandled event key: {event}. Values: {x}"))
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Exit'):
break
callbacks.get(event, unhandled_event_callback)(values)
# SG: Cleanup
window.close()
This solution was inspired by this gist and this discussion.