tkinter Loop Through List On Key Press - python

Im am trying to create a command history with the tkinter Entry widget and Up/Down arrow keys. It is for a very basic MUD client that I am trying to come up with in my spare time.
# The list that hold the last entered commands.
self.previousCommands = []
# Binding the up arrow key to the Entry widget.
self.view.Tabs.tab1.E.bind('<Up>', self.checkCommandHistory)
# The function that should cycle through the commands.
def checkCommandHistory(self, event):
comm = self.previousCommands[-1]
self.view.Tabs.tab1.E.delete(0, END)
self.view.Tabs.tab1.E.insert(0, comm)
Basically what I am trying to do is cycle through a list that contains the history of the last entered commands by using the up and down arrow keys. This behavior is common in most MUD clients but I cannot see exactly how this is achieved.
Using the code above I am able to bind the Up arrow key press to the Entry widget and on pressed it does insert the last entered command. If I were to keep pressing the up arrow key I would like it to keep cycling through the last entered commands in the list.

Question: cycle the elements in a list by pressing Up/Down arrow key bound to Entry widget
Create a class object inherited from tk.Entry.
import tkinter as tk
class EntryHistory(tk.Entry):
def __init__(self, parent, init_history=None):
super().__init__(parent)
self.bind('<Return>', self.add)
self.bind('<Up>', self.up_down)
self.bind('<Down>', self.up_down)
self.history = []
self.last_idx = None
if init_history:
for e in init_history:
self.history.append(e)
self.last_idx = len(self.history)
def up_down(self, event):
if not self.last_idx is None:
self.delete(0, tk.END)
if event.keysym == 'Up':
if self.last_idx > 0:
self.last_idx -= 1
else:
self.last_idx = len(self.history) -1
elif event.keysym == 'Down':
if self.last_idx < len(self.history) -1:
self.last_idx += 1
else:
self.last_idx = 0
self.insert(0, self.history[self.last_idx])
def add(self, event):
self.history.append(self.get())
self.last_idx = len(self.history) - 1
if __name__ == "__main__":
root = tk.Tk()
entry = EntryHistory(root, init_history=['test 1', 'test 2', 'test 3'])
entry.grid(row=0, column=0)
root.mainloop()
Tested with Python: 3.5

Here is a non-OOP based version.
import tkinter as tk
root = tk.Tk()
history = []
history_index = -1
def runCommand(event):
command = cmd.get()
print("Running command: {}".format(command))
cmd.set("")
history.append(command)
history_index = -1
print(history)
def cycleHistory(event):
global history_index
if len(history):
try:
comm = history[history_index]
history_index -= 1
except IndexError:
history_index = -1
comm = history[history_index]
cmd.set(comm)
cmd = tk.StringVar(root)
e = tk.Entry(root,textvariable=cmd)
e.grid()
e.bind("<Return>",runCommand)
e.bind("<Up>",cycleHistory)
e.focus()
root.mainloop()
Basically, you just need to keep an record of which item from history you should show next time the user presses the up arrow. I use the history_index field to do this. history_index is set to -1 initially and each time it is accessed it is decremented by 1.
I use the except IndexError exception to reset the index to -1 once there is no more history to read from the list and to start from the beginning again.
Pressing the return key, runs the command, adds it to the history and resets the index to -1.

Related

Python tkinter "Select All" checkbox and alignment

I want a select all check box on top of all check boxes by selecting that all check boxes should be selected. After clicking on the NEXT button the second frame will appear with the checkboxes mentioned in the list in the code.
from tkinter import *
def raise_frame(frame):
frame.tkraise()
root = Tk()
f1 = Frame(root)
f2 = Frame(root)
def Quit():
root.destroy()
checkvar1 = IntVar()
checkvar2 = IntVar()
checkvar3 = IntVar()
def checkbox():
raise_frame(f2)
IPR_List=["COMM 662 SCTP PATH UP","COMM 665 SCTP PATH DOWN","COMM 478 GLOBALMTITLE TRANSLATION FAILURE","COMM 275 MTP LEVEL ROUTING ERROR",
"COMM 628 SCTP ASSOCIATION ESTABLISHED","COMM 0629 SCTP ASSOCIATION TERMINATED","COMM 137 CCS7:SRC DPC FAILED","139 CCS7:SRC DPC ACTIVATED",
"COMM 338 CCS7:SCCP SUBSYS--OUT-OF-SERVICE","COMM 339 CCS7:SCCP SUBSYS--IN-OF-SERVICE","363 SCCP:ROUTE on PC/SSN FAILURE",
"COMM 479 FLEX RTNG FILTERING MATCHED","COMM 271 GATEWAY.SS7 MSU REJECTED","COMM 93 CCS7:SEQ TABLE CKSUM MISMATCH",]
j=0
for i in IPR_List:
chkbtn = Checkbutton(f2, text = i, onvalue = 1, offvalue = 0, height = 2, width = 70).grid(row=j,column=1)
j=j+1
for frame in (f1, f2):
frame.grid(row=0, column=0, sticky='news')
sd=StringVar()
pathlabel = Entry(f1,text=sd,font=('times',10,'bold'),fg='blue',bg='white',width=60)
pathlabel.grid(row=1,column=1)
browsebutton = Button(f1, text="Browse")
browsebutton.grid(row=1,column=2)
Button(f1, text='NEXT', command=checkbox).grid(row=2,column=1)
Quit = Button(f1, text="Quit", command=Quit)
Quit.grid(row=2,column=2)
raise_frame(f1)
root.mainloop()
I want to keep all my checkboxes to the left in one line .on top of that I want a select all check box on top of all check boxes by selecting that all check boxes should be selected.
So here is my solution (it probably can be simplified but... a solution it is):
from tkinter import Tk, Checkbutton, Frame, IntVar
class Options:
def __init__(self, parent, name, selection=None, select_all=False):
self.parent = parent
self.name = name
self.selection = selection
self.variable = IntVar()
self.checkbutton = Checkbutton(self.parent, text=self.name,
variable=self.variable, command=self.check)
if selection is None:
self.checkbutton.pack(side='top')
elif select_all:
self.checkbutton.config(command=self.select_all)
self.checkbutton.pack()
for item in self.selection:
item.checkbutton.pack(side='top')
def check(self):
state = self.variable.get()
if state == 1:
print(f'Selected: {self.name}')
if all([True if item.variable.get() == 1 else False for item in self.selection[:-1]]):
self.selection[-1].checkbutton.select()
elif state == 0:
print(f'Deselected: {self.name}')
if self.selection[-1].variable.get() == 1:
self.selection[-1].checkbutton.deselect()
def select_all(self):
state = self.variable.get()
if state == 1:
for item in self.selection[:-1]:
item.checkbutton.deselect()
item.checkbutton.invoke()
elif state == 0:
for item in self.selection[:-1]:
item.checkbutton.select()
item.checkbutton.invoke()
selection = []
root = Tk()
option_frame = Frame(root)
option_frame.pack(side='left', fill='y')
for i in range(5):
selection.append(Options(option_frame, f'Option {i + 1}', selection))
selection.append(Options(option_frame, 'Select All', selection, True))
root.mainloop()
A quick explanation about how this should be set up. If You have a group of checkboxes that You want to add a select all checkbox You have to append them all to a list and put that list as third argument when creating them in a loop like in the example. Then after the loop You want to append another instance but You have to also set the fourth argument to True. That will allow to easily create checkboxes with a select all. If You just want to have a few checkboxes just put them in a loop and add only the first two arguments: parent and name.
Hope that clears up how to set up. About coding part:
Some confusion may arise when in the code it deselects all and then invokes when it should select. The reason is that first all checkboxes need to be cleared because .invoke() toggles them to the opposite and also simulates user input so all the checkboxes will call .check() method, and for deselection it works the opposite way.
def select_all(self):
state = self.variable.get()
if state == 1:
for item in self.selection[:-1]:
item.checkbutton.deselect()
item.checkbutton.invoke()
elif state == 0:
for item in self.selection[:-1]:
item.checkbutton.select()
item.checkbutton.invoke()
Also the code could be made less complex if the function of if all are selected and one or more get deselected by the user also deselect automatically the select all checkbox because it is untrue. (last 2 lines)
elif state == 0:
print(f'Deselected: {self.name}')
if self.selection[-1].variable.get() == 1:
self.selection[-1].checkbutton.deselect()
EDIT 1: added code that selects select_all checkbox if all checboxes are selected by user (and select_all hasn't been selected by the user) (last 2 lines)
if state == 1:
print(f'Selected: {self.name}')
if all([True if item.variable.get() == 1 else False for item in self.selection[:-1]]):
self.selection[-1].checkbutton.select()
I would say the rest is pretty simple python logic, so if You have questions, ask them.
Here is the source I used for Checkbox attributes and methods.
for i in range(len(IPR_LIST)):
Checkbutton(root2,text=IPR_LIST[i],variable=chkboxlist[i]).place(relx=x,rely=y)
y=y+0.05
c=c+1
if c==8:
x=x+0.15
y=0.05
c=0

How to Update Tkinter Label while looking for keyboard input

I am Trying to make a tkinter based Cube Timer Program for which I have done a console based program before I wanted to improve on it by making a GUI for it... I have made some tkinter projects before and I have a decent knowledge of how stuff works the thing is I want the Label in the tkinter window to update in every second while listening for a spacebar key on press I couldn't figure out a way to do it can someone help me?
def on_press(key):
global chk,start,end,timer_label
print('{0} pressed'.format(key))
if key == Key.space:
if chk == True:
end = time.time()
chk = False
messagebox.showinfo('Your Time',"Your Time =>"+str(round(end-start,3)))
def on_release(key):
global chk,start,end,timer_label
if key == Key.space:
if chk == False:
start = time.time()
chk = True
with Listener(on_press=on_press,on_release=on_release) as listener:
main = tk.Tk()
main.title("Cube Timer Mark 4 Prototype")
main.resizable(0,0)
tk.Label(main, text="Cube Time Mk3 Prototype", font=['Consolas',16]).grid(row = 0,column=0)
textbox = tk.Text(main,font=['Consolas',20], height = 1,width = 27)
textbox.grid(row = 1,column=0)
textbox.insert('1.0',scrambler())
timer_frame = tk.Frame(main).grid(row=2,column=0)
global timer_label
timer_label = tk.StringVar()
timer_label.set("0.0")
tk.Label(timer_frame,text = timer_label.get()+"s",font=['Consolas',20,'bold'],pady = 25).grid(row = 2,column = 0)
main.mainloop()
This is what I have tried but didn't work
You need main.after to periodically run a function like updating the label. To listen to keyboard event, you need main.bind. See example below.
Enhancement: use tk.Label(..., textvariable=timer_label) means the label will automatically update when you set timer_label
import tkinter as tk
def key_press(event):
if event.keysym == 'space':
print('pressed')
def key_release(event):
if event.keysym == 'space':
print('released')
def update_label():
# get the time from the string
time = float(timer_label.get()[:-1])
# increment the time and put it back in timer_label
timer_label.set(str(time+1) + 's')
# calling this function again 1000ms later, which will call this again 1000ms later, ...
main.after(1000, update_label)
main = tk.Tk()
main.title("Cube Timer Mark 4 Prototype")
main.resizable(0,0)
tk.Label(main, text="Cube Time Mk3 Prototype", font=['Consolas',16]).grid(row = 0,column=0)
textbox = tk.Text(main,font=['Consolas',20], height = 1,width = 27)
textbox.grid(row = 1,column=0)
#textbox.insert('1.0',scrambler())
timer_frame = tk.Frame(main).grid(row=2,column=0)
timer_label = tk.StringVar()
timer_label.set("0.0s")
# using textvariable, you can simply update timer_label and it will be reflected
tk.Label(timer_frame,textvariable = timer_label ,font=['Consolas',20,'bold'],pady = 25).grid(row = 2,column = 0)
# bind keypress and keyrelease to the functions
main.bind("<KeyPress>", key_press)
main.bind("<KeyRelease>", key_release)
# call update label function for the first time
update_label()
main.mainloop()

How to remove icursor from Tkinter canvas text item?

I'm following along with Effbot's Tkinter page here:
http://effbot.org/zone/editing-canvas-text-items.htm
The trouble I'm having is that after inserting the icursor, I can't seem to get it to go away!
How do I stop editing altogether?
As for examples, the one from the linked page will work:
# File: canvas-editing-example-1.py
#
# editing canvas items
#
# fredrik lundh, december 1998
#
# fredrik#pythonware.com
# http://www.pythonware.com
#
from tkinter import *
#Change to Tkinter to use python 2.x series
class MyCanvas(Frame):
def __init__(self, root):
Frame.__init__(self, root)
self.canvas = Canvas(self)
self.canvas.pack(fill=BOTH, expand=1)
# standard bindings
self.canvas.bind("<Double-Button-1>", self.set_focus)
self.canvas.bind("<Button-1>", self.set_cursor)
self.canvas.bind("<Key>", self.handle_key)
# add a few items to the canvas
self.canvas.create_text(50, 50, text="hello")
self.canvas.create_text(50, 100, text="world")
def highlight(self, item):
# mark focused item. note that this code recreates the
# rectangle for each update, but that's fast enough for
# this case.
bbox = self.canvas.bbox(item)
self.canvas.delete("highlight")
if bbox:
i = self.canvas.create_rectangle(
bbox, fill="white",
tag="highlight"
)
self.canvas.lower(i, item)
def has_focus(self):
return self.canvas.focus()
def has_selection(self):
# hack to work around bug in Tkinter 1.101 (Python 1.5.1)
return self.canvas.tk.call(self.canvas._w, 'select', 'item')
def set_focus(self, event):
if self.canvas.type(CURRENT) != "text":
return
self.highlight(CURRENT)
# move focus to item
self.canvas.focus_set() # move focus to canvas
self.canvas.focus(CURRENT) # set focus to text item
self.canvas.select_from(CURRENT, 0)
self.canvas.select_to(CURRENT, END)
def set_cursor(self, event):
# move insertion cursor
item = self.has_focus()
if not item:
return # or do something else
# translate to the canvas coordinate system
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
self.canvas.icursor(item, "#%d,%d" % (x, y))
self.canvas.select_clear()
def handle_key(self, event):
# widget-wide key dispatcher
item = self.has_focus()
if not item:
return
insert = self.canvas.index(item, INSERT)
if event.char >= " ":
# printable character
if self.has_selection():
self.canvas.dchars(item, SEL_FIRST, SEL_LAST)
self.canvas.select_clear()
self.canvas.insert(item, "insert", event.char)
self.highlight(item)
elif event.keysym == "BackSpace":
if self.has_selection():
self.canvas.dchars(item, SEL_FIRST, SEL_LAST)
self.canvas.select_clear()
else:
if insert > 0:
self.canvas.dchars(item, insert-1, insert)
self.highlight(item)
# navigation
elif event.keysym == "Home":
self.canvas.icursor(item, 0)
self.canvas.select_clear()
elif event.keysym == "End":
self.canvas.icursor(item, END)
self.canvas.select_clear()
elif event.keysym == "Right":
self.canvas.icursor(item, insert+1)
self.canvas.select_clear()
elif event.keysym == "Left":
self.canvas.icursor(item, insert-1)
self.canvas.select_clear()
else:
pass # print event.keysym
# try it out (double-click on a text to enable editing)
c = MyCanvas(Tk())
c.pack()
mainloop()
After you double click on one of the items to be edited, I cannot make the cursor go away; I've tried moving focus and setting the index to -1, but neither seems to work.
self.canvas.focus("")
To remove focus from the item, call this method with an empty string. Reference
you can add the following
self.canvas.focus_set() # move focus to canvas window
self.canvas.focus("") # remove focus from the current item that has it
To
def set_focus(self, event):
if self.canvas.type(CURRENT) != "text":
#Here
return
So when the user double-clicks on any part or item of the canvas that isn't created by canvas.create_text the focus is removed from the current "text" item, hence stopping the edit.
Plus you can add
self.canvas.delete("highlight")
to remove the white rectangle around the text, when focus is removed.

Python Tkinter: Listbox mouse enter event for a specific entry

It is possible to create events for when the mouse pointer enters/leaves the entire Listbox using <Enter>/<Leave>. How can I track when the mouse enters or leaves a specific entry (row) in the Listbox?
I want to color in different color the background of the entry over which the mouse pointer is currently located.
Here's an (half) attempt to do what you want by binding to the <Motion> event instead of the pair <Enter> and <Leave>. This because <Enter> is raised only when we enter the Listbox from outside it, but once we are inside a Listbox with the mouse, no other <Enter> event will be raised, and we cannot keep track of which item the mouse is above.
Calling a function every time the mouse moves might result in an overload of work, so I don't think this feature is worthing doing it (in this way).
The program does not still work perfectly, and I still have to understand why: basically, sometimes the item's background and font color are not changed properly, there's some kind of delay or something.
from tkinter import *
class CustomListBox(Listbox):
def __init__(self, master=None, *args, **kwargs):
Listbox.__init__(self, master, *args, **kwargs)
self.bg = "white"
self.fg = "black"
self.h_bg = "#eee8aa"
self.h_fg = "blue"
self.current = -1 # current highlighted item
self.fill()
self.bind("<Motion>", self.on_motion)
self.bind("<Leave>", self.on_leave)
def fill(self, number=15):
"""Fills the listbox with some numbers"""
for i in range(number):
self.insert(END, i)
self.itemconfig(i, {"bg": self.bg})
self.itemconfig(i, {"fg": self.fg})
def reset_colors(self):
"""Resets the colors of the items"""
for item in self.get(0, END):
self.itemconfig(item, {"bg": self.bg})
self.itemconfig(item, {"fg": self.fg})
def set_highlighted_item(self, index):
"""Set the item at index with the highlighted colors"""
self.itemconfig(index, {"bg": self.h_bg})
self.itemconfig(index, {"fg": self.h_fg})
def on_motion(self, event):
"""Calls everytime there's a motion of the mouse"""
print(self.current)
index = self.index("#%s,%s" % (event.x, event.y))
if self.current != -1 and self.current != index:
self.reset_colors()
self.set_highlighted_item(index)
elif self.current == -1:
self.set_highlighted_item(index)
self.current = index
def on_leave(self, event):
self.reset_colors()
self.current = -1
if __name__ == "__main__":
root = Tk()
CustomListBox(root).pack()
root.mainloop()
Note that I have used from tkinter import * for typing faster, but I recommend you to use import tkinter as tk.
No, you cannot track when it enters/leaves a specific row. However, you can track when it enters/leaves the widget, and you can compute which item the mouse is over by using the index method of the listbox. If you give an index of the form "#x,y", it will return the numerical index.
For example:
self.listbox.bind("<Enter>", self.on_enter)
...
def on_enter(self, event):
index = self.listbox.index("#%s,%s" % (event.x, event.y))
...

Python tkinter: Sorting the contents of an OptionMenu widget

Hello everyone and thanks in advance!
I've searched all around google and read almost every result i got and i still can't figure
it out, so please at least point me at some direction!
I read about pmw but i want to see if there is any way to do it with tkinter first.
I'm writing a simple enough program for DnD dice rolls and i have an OptionMenu
containing some of the dice someone needs to play. I also have an input field for entering
a die that is not included in my default options. My problem is that even though the new
option is added successfully, the options are not sorted.
I solved it at some point by destroying the OptionMenu when the new option was added,
sorting my List and then rebuilding the OptionMenu from scratch, but i was using the
place manager method at that time and i had to rewrite the program later because i had
some resolution problems. I'm using the pack manager now and destroying/rebuilding
is not an option unless i want to "re"pack all my widgets or make exclusive labels for them!
Here is a working sample of my code:
from tkinter import *
class DropdownExample(Frame):
def __init__(self, master = None):
Frame.__init__(self, master)
self.pack(fill = 'both', expand = True)
# Add Option Button
self.addOptBtn = Button(self, text = "Add Option", command = self.add_option)
# Option Input Field
self.newOpt = IntVar()
self.newOpt.set("Type a number")
self.optIn = Entry(self)
self.optIn['textvariable'] = self.newOpt
# Dropdown Menu
self.myOptions = [0, 1, 2]
self.selOpt = IntVar()
self.selOpt.set("Options")
self.optMenu = OptionMenu(self, self.selOpt, *self.myOptions)
# Positioning
self.addOptBtn.pack(side = 'left', padx = 5)
self.optIn.pack(side = 'left', padx = 5)
self.optMenu.pack(side = 'left', padx = 5)
def add_option(self):
self.numToAdd = ""
self.counter = 0
try:
self.numToAdd = int(self.optIn.get()) # Integer validation
while self.counter < len(self.myOptions): # Comparison loop & error handling
if self.numToAdd == self.myOptions[self.counter]:
print("Already exists!")
break;
elif self.numToAdd < 0:
print("No less than 0!")
break;
elif self.counter < len(self.myOptions)-1:
self.counter += 1
else: # Dropdown menu option addition
self.myOptions.append(self.numToAdd)
self.myOptions.sort()
self.optMenu['menu'].add_command(label = self.numToAdd)
self.selOpt.set(self.numToAdd)
print("Added succesfully!")
self.counter += 2
except ValueError:
print("Type ONLY numbers!")
def runme():
app = DropdownExample()
app.master.title("Dropdown Menu Example")
app.master.resizable(0, 0)
app.mainloop()
runme()
I am using Python 3.3 on Windows 7
There is a set of insert_something() methods in Menu. You must keep your list sorted with each insert (bisect module).
from tkinter import *
import bisect
...
else: # Dropdown menu option addition
index = bisect.bisect(self.myOptions, self.numToAdd)
self.myOptions.insert(index, self.numToAdd)
self.optMenu['menu'].insert_command(index, label=self.numToAdd)
self.selOpt.set(self.numToAdd)
print("Added succesfully!", self.myOptions)
self.counter += 2
Replace the line:
self.optMenu['menu'].add_command(label = self.numToAdd)
with:
for dit in self.myOptions:
self.optMenu['menu'].delete(0)
for dat in self.myOptions:
self.optMenu['menu'].add_command(label = dat)
The gotcha is that "add_command" takes the item to add to the menu, while "delete" takes the index of the item.
It seems that whatever way i choose to follow on this one, it all comes down to updating
the widget.
Updating a frame usually redraws a portion or the whole frame if needed.
So i went back to square one. I destroyed the widget, updated the list and then created it again.
As for the position, i used a label as a background only for the OptionMenu.
That way i can delete/replace my OptionMenu as i please, without moving any widgets around, as long as it stays inside it's label and it's the only one in there and of course as long as i don't move any labels.
This probably is a workaround, not a solution, but it get's the job done and with a bit
of optimization it can be used for any other widget having the same problem.
from tkinter import *
class DropdownExample(Frame):
def __init__(self, master = None):
Frame.__init__(self, master)
self.pack(fill = 'both', expand = True)
# Add Option Button
self.addOptBtn = Button(self, text = "Add Option", command = self.add_option)
# Option Input Field
self.newOpt = IntVar()
self.newOpt.set("Type a number")
self.optIn = Entry(self)
self.optIn['textvariable'] = self.newOpt
# Dropdown Menu
# menu's label
self.optMenuLabel = Label(self)
# option menu
self.myOptions = [0, 1, 2]
self.selOpt = IntVar()
self.selOpt.set("Options")
self.optMenu = OptionMenu(self.optMenuLabel, self.selOpt, *self.myOptions)
# Positioning
self.addOptBtn.pack(side = 'left', padx = 5)
self.optIn.pack(side = 'left', padx = 5)
self.optMenuLabel.pack(side = 'left', padx = 5)
self.optMenu.pack(side = 'left', padx = 5)
def add_option(self):
self.numToAdd = ""
self.counter = 0
try:
self.numToAdd = int(self.optIn.get()) # Integer validation
while self.counter < len(self.myOptions): # Comparison loop & error handling
if self.numToAdd == self.myOptions[self.counter]:
print("Already exists!")
break;
elif self.numToAdd < 0:
print("No less than 0!")
break;
elif self.counter < len(self.myOptions)-1:
self.counter += 1
else: # Dropdown menu option addition
self.myOptions.append(self.numToAdd)
self.myOptions.sort()
self.selOpt.set(self.numToAdd)
self.optMenu.destroy()
self.optMenu = OptionMenu(self.optMenuLabel, self.selOpt, *self.myOptions)
self.optMenu.pack(side = 'left', padx = 5)
print("Added succesfully!", self.myOptions)
break;
except ValueError:
print("Type ONLY numbers!")
def runme():
app = DropdownExample()
app.master.title("Dropdown Menu Example")
app.master.resizable(0, 0)
app.mainloop()
runme()

Categories

Resources