Is there a way to add undo and redo capabilities in Tkinter Entry widgets or must I use single line Text widgets for this type of functionality?
If the latter, are there any tips I should follow when configuring a Text widget to act as an Entry widget?
Some features that might need tweaking include trapping the Return KeyPress, converting tab keypresses into a request to change focus, and removing newlines from text being pasted from the clipboard.
Check the Tkinter Custom Entry. I have added Cut, Copy, Paste context menu, and undo redo functions.
# -*- coding: utf-8 -*-
from tkinter import *
class CEntry(Entry):
def __init__(self, parent, *args, **kwargs):
Entry.__init__(self, parent, *args, **kwargs)
self.changes = [""]
self.steps = int()
self.context_menu = Menu(self, tearoff=0)
self.context_menu.add_command(label="Cut")
self.context_menu.add_command(label="Copy")
self.context_menu.add_command(label="Paste")
self.bind("<Button-3>", self.popup)
self.bind("<Control-z>", self.undo)
self.bind("<Control-y>", self.redo)
self.bind("<Key>", self.add_changes)
def popup(self, event):
self.context_menu.post(event.x_root, event.y_root)
self.context_menu.entryconfigure("Cut", command=lambda: self.event_generate("<<Cut>>"))
self.context_menu.entryconfigure("Copy", command=lambda: self.event_generate("<<Copy>>"))
self.context_menu.entryconfigure("Paste", command=lambda: self.event_generate("<<Paste>>"))
def undo(self, event=None):
if self.steps != 0:
self.steps -= 1
self.delete(0, END)
self.insert(END, self.changes[self.steps])
def redo(self, event=None):
if self.steps < len(self.changes):
self.delete(0, END)
self.insert(END, self.changes[self.steps])
self.steps += 1
def add_changes(self, event=None):
if self.get() != self.changes[-1]:
self.changes.append(self.get())
self.steps += 1
Disclaimer: these are just thoughts that come into my mind on how to implement it.
class History(object):
def __init__(self):
self.l = ['']
self.i = 0
def next(self):
if self.i == len(self.l):
return None
self.i += 1
return self.l[self.i]
def prev(self):
if self.i == 0:
return None
self.i -= 1
return self.l[self.i]
def add(self, s):
del self.l[self.i+1:]
self.l.append(s)
self.i += 1
def current(self):
return self.l[self.i]
Run a thread that every X seconds (0.5?) save the state of the entry:
history = History()
...
history.add(stringval.get())
You can also set up events that save the Entry's status too, such as the pressure of Return.
prev = history.prev()
if prev is not None:
stringvar.set(prev)
or
next = history.next()
if next is not None:
stringvar.set(next)
Beware to set locks as needed.
Update on using this method for Undo/Redo:
I am creating a GUI with lot of frames and each contains at least ten or more 'entry' widgets.
I used the History class and created one history object for each entry field that I had. I was able to store all entry widgets values in a list as done here.
I am using 'trace' method attached to each entry widget which will call 'add' function of History class and store each changes. In this way, I was able to do it without running any thread separately.
But the biggest drawback of doing this is, we cannot do multiple undos/redos with this method.
Issue:
When I trace each and every change of the entry widget and add that to the list, it also 'traces' the change that happens when we 'undo/redo' which means we cannot go more one step back. once u do a undo, it is a change that will be traced and hence the 'undo' value will be added to the list at the end. Hence this is not the right method.
Solution:
Perfect way to do this is by creating two stacks for each entry widget. One for 'Undo' and one for 'redo'. When ever there is a change in entry, push that value into the undo stack. When user presses undo, pop the last stored value from the undo stack and importantly push this one to the 'redo stack'. hence, when the user presses redo, pop the last value from redo stack.
Based on Evgeny's answer with a custom Entry, but added a tkinter StringVar with a trace to the widget to more accurately track when changes are made to its contents (not just when any Key is pressed, which seemed to add empty Undo/Redo items to the stack). Also added a max depth using a Python deque.
If we're changing the contents of the Entry via code rather than keyboard input, we can temporarily disable the trace (e.g. see in the undo method below).
Code:
class CEntry(tk.Entry):
def __init__(self, master, **kw):
super().__init__(master=master, **kw)
self._undo_stack = deque(maxlen=100)
self._redo_stack = deque(maxlen=100)
self.bind("<Control-z>", self.undo)
self.bind("<Control-y>", self.redo)
# traces whenever the Entry's contents are changed
self.tkvar = tk.StringVar()
self.config(textvariable=self.tkvar)
self.trace_id = self.tkvar.trace("w", self.on_changes)
self.reset_undo_stacks()
# USE THESE TO TURN TRACE OFF THEN BACK ON AGAIN
# self.tkvar.trace_vdelete("w", self.trace_id)
# self.trace_id = self.tkvar.trace("w", self.on_changes)
def undo(self, event=None): # noqa
if len(self._undo_stack) <= 1:
return
content = self._undo_stack.pop()
self._redo_stack.append(content)
content = self._undo_stack[-1]
self.tkvar.trace_vdelete("w", self.trace_id)
self.delete(0, tk.END)
self.insert(0, content)
self.trace_id = self.tkvar.trace("w", self.on_changes)
def redo(self, event=None): # noqa
if not self._redo_stack:
return
content = self._redo_stack.pop()
self._undo_stack.append(content)
self.tkvar.trace_vdelete("w", self.trace_id)
self.delete(0, tk.END)
self.insert(0, content)
self.trace_id = self.tkvar.trace("w", self.on_changes)
def on_changes(self, a=None, b=None, c=None): # noqa
self._undo_stack.append(self.tkvar.get())
self._redo_stack.clear()
def reset_undo_stacks(self):
self._undo_stack.clear()
self._redo_stack.clear()
self._undo_stack.append(self.tkvar.get())
Related
What is the recommended technique for interactively validating content in a tkinter Entry widget?
I've read the posts about using validate=True and validatecommand=command, and it appears that these features are limited by the fact that they get cleared if the validatecommand command updates the Entry widget's value.
Given this behavior, should we bind on the KeyPress, Cut, and Paste events and monitor/update our Entry widget's value through these events? (And other related events that I might have missed?)
Or should we forget interactive validation altogether and only validate on FocusOut events?
The correct answer is, use the validatecommand attribute of the widget. Unfortunately this feature is severely under-documented in the Tkinter world, though it is quite sufficiently documented in the Tk world. Even though it's not documented well, it has everything you need to do validation without resorting to bindings or tracing variables, or modifying the widget from within the validation procedure.
The trick is to know that you can have Tkinter pass in special values to your validate command. These values give you all the information you need to know to decide on whether the data is valid or not: the value prior to the edit, the value after the edit if the edit is valid, and several other bits of information. To use these, though, you need to do a little voodoo to get this information passed to your validate command.
Note: it's important that the validation command returns either True or False. Anything else will cause the validation to be turned off for the widget.
Here's an example that only allows lowercase. It also prints the values of all of the special values for illustrative purposes. They aren't all necessary; you rarely need more than one or two.
import tkinter as tk # python 3.x
# import Tkinter as tk # python 2.x
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# valid percent substitutions (from the Tk entry man page)
# note: you only have to register the ones you need; this
# example registers them all for illustrative purposes
#
# %d = Type of action (1=insert, 0=delete, -1 for others)
# %i = index of char string to be inserted/deleted, or -1
# %P = value of the entry if the edit is allowed
# %s = value of entry prior to editing
# %S = the text string being inserted or deleted, if any
# %v = the type of validation that is currently set
# %V = the type of validation that triggered the callback
# (key, focusin, focusout, forced)
# %W = the tk name of the widget
vcmd = (self.register(self.onValidate),
'%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
self.entry = tk.Entry(self, validate="key", validatecommand=vcmd)
self.text = tk.Text(self, height=10, width=40)
self.entry.pack(side="top", fill="x")
self.text.pack(side="bottom", fill="both", expand=True)
def onValidate(self, d, i, P, s, S, v, V, W):
self.text.delete("1.0", "end")
self.text.insert("end","OnValidate:\n")
self.text.insert("end","d='%s'\n" % d)
self.text.insert("end","i='%s'\n" % i)
self.text.insert("end","P='%s'\n" % P)
self.text.insert("end","s='%s'\n" % s)
self.text.insert("end","S='%s'\n" % S)
self.text.insert("end","v='%s'\n" % v)
self.text.insert("end","V='%s'\n" % V)
self.text.insert("end","W='%s'\n" % W)
# Disallow anything but lowercase letters
if S == S.lower():
return True
else:
self.bell()
return False
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
For more information about what happens under the hood when you call the register method, see Why is calling register() required for tkinter input validation?
For the canonical documentation see the Validation section of the Tcl/Tk Entry man page
After studying and experimenting with Bryan's code, I produced a minimal version of input validation. The following code will put up an Entry box and only accept numeric digits.
from tkinter import *
root = Tk()
def testVal(inStr,acttyp):
if acttyp == '1': #insert
if not inStr.isdigit():
return False
return True
entry = Entry(root, validate="key")
entry['validatecommand'] = (entry.register(testVal),'%P','%d')
entry.pack()
root.mainloop()
Perhaps I should add that I am still learning Python and I will gladly accept any and all comments/suggestions.
Use a Tkinter.StringVar to track the value of the Entry widget. You can validate the value of the StringVar by setting a trace on it.
Here's a short working program that accepts only valid floats in the Entry widget.
try:
from tkinter import *
except ImportError:
from Tkinter import * # Python 2
root = Tk()
sv = StringVar()
def validate_float(var):
new_value = var.get()
try:
new_value == '' or float(new_value)
validate_float.old_value = new_value
except:
var.set(validate_float.old_value)
validate_float.old_value = '' # Define function attribute.
# trace wants a callback with nearly useless parameters, fixing with lambda.
sv.trace('w', lambda nm, idx, mode, var=sv: validate_float(var))
ent = Entry(root, textvariable=sv)
ent.pack()
ent.focus_set()
root.mainloop()
Bryan's answer is correct, however no one mentioned the 'invalidcommand' attribute of the tkinter widget.
A good explanation is here:
http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html
Text copy/pasted in case of broken link
The Entry widget also supports an invalidcommand option that specifies a callback function that is called whenever the validatecommand returns False. This command may modify the text in the widget by using the .set() method on the widget's associated textvariable. Setting up this option works the same as setting up the validatecommand. You must use the .register() method to wrap your Python function; this method returns the name of the wrapped function as a string. Then you will pass as the value of the invalidcommand option either that string, or as the first element of a tuple containing substitution codes.
Note:
There is only one thing that I cannot figure out how to do: If you add validation to an entry, and the user selects a portion of the text and types a new value, there is no way to capture the original value and reset the entry. Here's an example
Entry is designed to only accept integers by implementing 'validatecommand'
User enters 1234567
User selects '345' and presses 'j'. This is registered as two actions: deletion of '345', and insertion of 'j'. Tkinter ignores the deletion and acts only on the insertion of 'j'. 'validatecommand' returns False, and the values passed to the 'invalidcommand' function are as follows: %d=1, %i=2, %P=12j67, %s=1267, %S=j
If the code does not implement an 'invalidcommand' function, the 'validatecommand' function will reject the 'j' and the result will be 1267. If the code does implement an 'invalidcommand' function, there is no way to recover the original 1234567.
Define a function returning a boolean that indicates whether the input is valid.Register it as a Tcl callback, and pass the callback name to the widget as a validatecommand.
For example:
import tkinter as tk
def validator(P):
"""Validates the input.
Args:
P (int): the value the text would have after the change.
Returns:
bool: True if the input is digit-only or empty, and False otherwise.
"""
return P.isdigit() or P == ""
root = tk.Tk()
entry = tk.Entry(root)
entry.configure(
validate="key",
validatecommand=(
root.register(validator),
"%P",
),
)
entry.grid()
root.mainloop()
Reference.
While studying Bryan Oakley's answer, something told me that a far more general solution could be developed. The following example introduces a mode enumeration, a type dictionary, and a setup function for validation purposes. See line 48 for example usage and a demonstration of its simplicity.
#! /usr/bin/env python3
# https://stackoverflow.com/questions/4140437
import enum
import inspect
import tkinter
from tkinter.constants import *
Mode = enum.Enum('Mode', 'none key focus focusin focusout all')
CAST = dict(d=int, i=int, P=str, s=str, S=str,
v=Mode.__getitem__, V=Mode.__getitem__, W=str)
def on_validate(widget, mode, validator):
# http://www.tcl.tk/man/tcl/TkCmd/ttk_entry.htm#M39
if mode not in Mode:
raise ValueError('mode not recognized')
parameters = inspect.signature(validator).parameters
if not set(parameters).issubset(CAST):
raise ValueError('validator arguments not recognized')
casts = tuple(map(CAST.__getitem__, parameters))
widget.configure(validate=mode.name, validatecommand=[widget.register(
lambda *args: bool(validator(*(cast(arg) for cast, arg in zip(
casts, args)))))]+['%' + parameter for parameter in parameters])
class Example(tkinter.Frame):
#classmethod
def main(cls):
tkinter.NoDefaultRoot()
root = tkinter.Tk()
root.title('Validation Example')
cls(root).grid(sticky=NSEW)
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
root.mainloop()
def __init__(self, master, **kw):
super().__init__(master, **kw)
self.entry = tkinter.Entry(self)
self.text = tkinter.Text(self, height=15, width=50,
wrap=WORD, state=DISABLED)
self.entry.grid(row=0, column=0, sticky=NSEW)
self.text.grid(row=1, column=0, sticky=NSEW)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
on_validate(self.entry, Mode.key, self.validator)
def validator(self, d, i, P, s, S, v, V, W):
self.text['state'] = NORMAL
self.text.delete(1.0, END)
self.text.insert(END, 'd = {!r}\ni = {!r}\nP = {!r}\ns = {!r}\n'
'S = {!r}\nv = {!r}\nV = {!r}\nW = {!r}'
.format(d, i, P, s, S, v, V, W))
self.text['state'] = DISABLED
return not S.isupper()
if __name__ == '__main__':
Example.main()
import tkinter
tk=tkinter.Tk()
def only_numeric_input(e):
#this is allowing all numeric input
if e.isdigit():
return True
#this will allow backspace to work
elif e=="":
return True
else:
return False
#this will make the entry widget on root window
e1=tkinter.Entry(tk)
#arranging entry widget on screen
e1.grid(row=0,column=0)
c=tk.register(only_numeric_input)
e1.configure(validate="key",validatecommand=(c,'%P'))
tk.mainloop()
#very usefull for making app like calci
Here's an improved version of #Steven Rumbalski's answer of validating the Entry widgets value by tracing changes to a StringVar — which I have already debugged and improved to some degree by editing it in place.
The version below puts everything into a StringVar subclass to encapsulates what's going on better and, more importantly allow multiple independent instances of it to exist at the same time without interfering with each other — a potential problem with his implementation because it utilizes function attributes instead of instance attributes, which are essentially the same thing as global variables and can lead to problems in such a scenario.
try:
from tkinter import *
except ImportError:
from Tkinter import * # Python 2
class ValidateFloatVar(StringVar):
"""StringVar subclass that only allows valid float values to be put in it."""
def __init__(self, master=None, value=None, name=None):
StringVar.__init__(self, master, value, name)
self._old_value = self.get()
self.trace('w', self._validate)
def _validate(self, *_):
new_value = self.get()
try:
new_value == '' or float(new_value)
self._old_value = new_value
except ValueError:
StringVar.set(self, self._old_value)
root = Tk()
ent = Entry(root, textvariable=ValidateFloatVar(value=42.0))
ent.pack()
ent.focus_set()
ent.icursor(END)
root.mainloop()
This code can help if you want to set both just digits and max characters.
from tkinter import *
root = Tk()
def validate(P):
if len(P) == 0 or len(P) <= 10 and P.isdigit(): # 10 characters
return True
else:
return False
ent = Entry(root, validate="key", validatecommand=(root.register(validate), '%P'))
ent.pack()
root.mainloop()
Responding to orionrobert's problem of dealing with simple validation upon substitutions of text through selection, instead of separate deletions or insertions:
A substitution of selected text is processed as a deletion followed by an insertion. This may lead to problems, for example, when the deletion should move the cursor to the left, while a substitution should move the cursor to the right. Fortunately, these two processes are executed immediately after one another.
Hence, we can differentiate between a deletion by itself and a deletion directly followed by an insertion due to a substitution because the latter has does not change the idle flag between deletion and insertion.
This is exploited using a substitutionFlag and a Widget.after_idle().
after_idle() executes the lambda-function at the end of the event queue:
class ValidatedEntry(Entry):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tclValidate = (self.register(self.validate), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
# attach the registered validation function to this spinbox
self.config(validate = "all", validatecommand = self.tclValidate)
def validate(self, type, index, result, prior, indelText, currentValidationMode, reason, widgetName):
if typeOfAction == "0":
# set a flag that can be checked by the insertion validation for being part of the substitution
self.substitutionFlag = True
# store desired data
self.priorBeforeDeletion = prior
self.indexBeforeDeletion = index
# reset the flag after idle
self.after_idle(lambda: setattr(self, "substitutionFlag", False))
# normal deletion validation
pass
elif typeOfAction == "1":
# if this is a substitution, everything is shifted left by a deletion, so undo this by using the previous prior
if self.substitutionFlag:
# restore desired data to what it was during validation of the deletion
prior = self.priorBeforeDeletion
index = self.indexBeforeDeletion
# optional (often not required) additional behavior upon substitution
pass
else:
# normal insertion validation
pass
return True
Of course, after a substitution, while validating the deletion part, one still won’t know whether an insert will follow.
Luckily however, with:
.set(),
.icursor(),
.index(SEL_FIRST),
.index(SEL_LAST),
.index(INSERT),
we can achieve most desired behavior retrospectively (since the combination of our new substitutionFlag with an insertion is a new unique and final event.
I'm creating a function-specific text editor using tkinter. I would like to add a bookmark capability, and have seen several examples of using .yview()[0] or .index(INSERT) to get the yview first line number or actual line number of the line to which the bookmark should be added. What I don't know is how to adjust the bookmark(s) based on line insertions/deletions to other parts of the text box. I'm using some great code that Bryan Oakley shared to create line numbers for a textbox. This code also provides a "proxy" function which allows for the creation of virtual events to handle different tkinter functions (e.g., insert, delete, replace, etc.) Here is the code (slightly tweaked to handle spacing for different font sizes):
class TextLineNumbers(tk.Canvas):
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs)
self.textwidget = None
self.font_spacing = 0
self.setfontspacing(self.font_spacing)
def attach(self, text_widget):
self.textwidget = text_widget
def setfontspacing(self, spacing):
self.font_spacing = spacing
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("#0,0")
while True :
dline= self.textwidget.dlineinfo(i)
if dline is None: break
y = dline[1] + self.font_spacing
linenum = str(i).split(".")[0].zfill(7)
self.create_text(2,y,anchor="nw", text=linenum)
i = self.textwidget.index("%s+1line" % i)
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
# create a proxy for the underlying widget
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, *args):
# let the actual widget perform the requested action
cmd = (self._orig,) + args
result=None
try:
result = self.tk.call(cmd)
except Exception:
pass
# generate an event if something was added or deleted,
# or the cursor position changed
# print(cmd, args, "=>", result)
if (args[0] in ("insert", "replace", "delete") or
args[0:3] == ("mark", "set", "insert") or
args[0:2] == ("xview", "moveto") or
args[0:2] == ("xview", "scroll") or
args[0:2] == ("yview", "moveto") or
args[0:2] == ("yview", "scroll")
):
self.event_generate("<<Change>>", when="tail")
if (args[0] in ("insert", "replace", "delete") ):
self.event_generate("<<Text_Change>>", when="now")
# return what the actual widget returned
return result
I've bound the Text_Change event to my custom text widget and it allows me to track changes (i.e., to see if changes have occurred and prompt for save on exit, etc.). So that part is working fine.
However, I don't know how to capture the details about lines that were inserted/deleted when these events occur. The print(cmd, args, "=>", result) gives lots of details, and I guess I could figure out a way to calculate deltas based on that information, but it seems like a complicated solution.
I was wondering if anybody had tackled this problem before and might have suggestions. Or maybe there's just a much simpler solution that I'm overlooking.
Thanks
The text widget has the ability to set bookmarks via the mark_set command. Marks can be used just like a text widget index. For example, you could set a mark "footer" toward the end of a long file and then use the see method to make the footer visible.
Marks represent the gap between two indexes. If you set a mark at index "200.0", the mark represents the space between the preceding index and that index. The mark will "stick" to one side of the gap or the other. By default it sticks with the character to the right, but that can be changed with the mark_gravity method which accepts either "left" or "right".
Here is a contrived example that sets a bookmark at line 200, and then provides a button to jump to that mark. Notice that even after the program starts up, you can insert as many lines as you want and the mark will still be "stuck" to the word "This" (or more precisely, the letter "T").
import tkinter as tk
root = tk.Tk()
text = tk.Text(root, height=20, yscrollcommand=lambda *args: vsb.set(*args))
vsb = tk.Scrollbar(root, orient="vertical", command=text.yview)
jump = tk.Button(root, text="Jump to bookmark", command=lambda: text.see("bookmark"))
jump.pack(side="top")
vsb.pack(side="right", fill="y")
text.pack(side="left", fill="both", expand=True)
for i in range(300):
text.insert("end", f"Lorem ipsum dolar set\n")
text.insert("200.0", "This line is bookmarked\n")
text.mark_set("bookmark", "200.0")
root.mainloop()
I have a list named chosenTestHolder (imported from the my_config file) that consists of several objects each with the attribute 'sentence'.
When pressing the button 'Press' for the first time, the attribute 'sentence' of the first object in the chosenTestHolder should be displayed in the text widget. The next time the button 'Press' is pressed the attribute 'sentence' of the second object in chosenTestHolder should be displayed and so on.
I am using lambda event for binding the 'Press' button and tries to use a new sentences as its first arguments after each pressing of the 'Press' button. However, it keeps showing the first sentence.
When searching Stackoverflow I have seen in
Using lambda function to change value of an attribute that you can't use assignments in lambda expressions but by reading that I still have not figured out how to solve my problem.
Grateful for help! Code is below!
main.py
from tkinter import font
import tkinter as tk
import tkinter.ttk as ttk
import my_config
import Testlist as tl
class TestWidgetTest:
def __init__(self):
ram = tk.Frame(root)
ram.grid(in_=root,row=0, column=0)
self.myText = tk.Text(ram, height = 5)
self.myText.grid(row=0,column=1)
my_config.counter = 0
self.myButton = tk.Button(ram, text = 'Press')
self.myButton.grid(row =1, column =0, columnspan =2)
indata =[my_config.chosenTestHolder[my_config.counter] , self.myText]
self.myButton.bind('<ButtonRelease-1>',lambda event, arg=indata : self.TagConfigure(event, arg))
def TagConfigure(self, event, arg):
arg[1].delete('1.0',tk.END)
arg[1].insert('1.0',arg[0].sentence)
my_config.counter += 1
root = tk.Tk()
TestWidgetTest()
root.mainloop()
my_config.py
import Testlist as tl
testListHolder = [ ['Fabian was very tired'],
['Thomas light the fire'],
['Anna eat a red apple ']]
chosenTestHolder = []
count = 0
while count <(len(testListHolder)):
chosenTestHolder.append(tl.Testlist(testListHolder[count][0]))
count += 1
counter = 0
Testlist.py
class Testlist:
def __init__(self, sentence):
self.sentence = sentence
Your issue is the assignment of indata.
You do only assign in init.
To get your code working you need to re-configure your sentecte...
indata =[my_config.chosenTestHolder[my_config.counter] , self.myText]
self.myButton.bind('<ButtonRelease-1>',lambda event, arg=indata : self.TagConfigure(event, arg))
I would advise to keep track of the current sentence as an instance variable.
class Test_widget(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, args, kwargs)
self.sentences=["a", "b", "c", "d"] # the data
self.show = tk.StringVar() # the current displayed data
self.show.set("NULL")
self.counter=0 # the indexer
tk.Label(self, textvariable=self.show).grid(row=0)
tk.Button(self, command=self.click).grid(row=1)
def click(self, event):
self.show.set("%s"%self.sentences[self.counter]) # use the indexer to access the data
self.counter = self.counter + 1 # modify the indexer
if self.counter = len(self.sentences): # make sure you dont run in index-err
self.counter = 0
As you see, there is no need at all for the lambdas.
Edit
As to your questions:
The change in your original code was not intended.
I do not see a use case where you can use a lambda for its use inside your code.
At least none where a lambda is necessary.
Please remember to use lambda only and exclusively if there are
no ( == NULL ) other options.
Using inheritance (thats what the mechanism is called), you can inherit functions, "default" behaviour from other classes. It is a common mechanism in programming and not exclusive to python.
It is used like any normal object except you have to call the constructor of the base class (what I do using tk.Frame.__init__(self, args, kwargs) inside the init method. For more information on inheritance please refer to the uncounted manuals and tutorials available for that topic (google is your friend now that you know what the mechanism is called).
I've wrote an little app to scroll an stock market ticker into a Tkinter window and update the stock values every minute using a threading class.
If I closed the window the main process didn't stops, then I've implemented a method to close the window and stop the thread. But when I close the window the main process delay the stopping to the next call to the theading.run() method.
How can I get that an instant closing process?
The code:
# -*- coding: UTF-8 -*-
import tkinter as tk
import tkinter.messagebox
import time
import threading
from random import randint as randint, uniform as randlimit
# Here starts the program working process, until here was the GUI
# CONSTANTS
CHAR_UP = "\u25B2"
CHAR_DOWN = "\u25BC"
CHAR_EVEN = "="
UPDATE_TIME = 60
# INITIAL DATA, this must be changed to implement the load of a external source
stock_market = [["GOOG", "587.25", CHAR_UP, "(+12.14)"],
["AAPL", "237.14", CHAR_UP, "(+7.25)"],
["GTAT", "87.47", CHAR_DOWN, "(-1.18)"],
["KNDI", "167.32", CHAR_UP, "(+6.85)"],
["ORCL", "482.91", CHAR_DOWN, "(-24.65)"],
["FBOK", "327.67", CHAR_DOWN, "(-11.78)"],
["TWTR", "842.41", CHAR_UP, "(+15.45)"]]
class AplicationTkinter(tk.Frame):
"""
Class of tkinter.Frame subclass, Initializes the GUI
constants:
SPEED, the delay in millisecs of the scroll
attributes:
parent, the root Tk object
"""
SPEED = 250
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.parent = parent
self._initGUI()
self._scroll_ticker()
self.parent.protocol("WM_DELETE_WINDOW",self.stop)
# or toplevel.protocol(...
def _initGUI(self):
"""
initGUI, draws the layout.
"""
# changes the window icon
self.parent.iconbitmap("tabla.ico")
self.parent.title("Stock Exchange Ticker")
# fix a status bar at the bottom of the window, for future improvements
self.status_bar = tk.Label(self.parent, text="", bd=1, relief=tk.SUNKEN,
anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# content Frame for entry, for future improvements
self.frame = tk.Frame(self.parent)
self.frame.pack()
self.var_entry = tk.StringVar()
self.entry = tk.Entry(self.frame, textvariable=self.var_entry)
self.entry.pack()
self.var_entry.set("a default value")
str_ent_1 = self.entry.get()
# content LabelFrame to show the ticker scrolling line of text
self.label_frame = tk.LabelFrame(self.parent,
text="Ventana de Resultados")
self.label_frame.pack()
# creates an instance of the StockMarket class for contents the data
self.market_one = StockMarket(stock_market)
# the scrolling line of Text for show the data
self.txt_ticker_widget = tk.Text(self.label_frame, background='black',
height=1, width=56, wrap="none")
self.txt_ticker_widget.pack(side=tk.TOP, fill=tk.X)
self.txt_ticker_widget.tag_configure("up", foreground="green")
self.txt_ticker_widget.tag_configure("down", foreground="red")
self.txt_ticker_widget.tag_configure("even", foreground="white")
self.tag = {CHAR_DOWN: "down", CHAR_EVEN: "even", CHAR_UP: "up"}
def _scroll_ticker(self):
"""
scroll_ticker, inserts character by character in the Text widget
"""
self.txt_ticker_widget.configure(state=tk.NORMAL)
self.txt_ticker_widget.insert(tk.END,
self.market_one.get_next_character(),
self.tag[self.market_one.get_tag()])
# TODO simplify
self.txt_ticker_widget.see(tk.END)
self.txt_ticker_widget.configure(state=tk.DISABLED)
self.txt_ticker_widget.after(self.SPEED, self._scroll_ticker)
# recursive each interval of millisecs, constant SPEED
def stop(self):
if tk.messagebox.askokcancel("Quit?", "Are you sure you want to quit?"):
# self.destroy()
print("STOPPING !!")
self.market_one.thread_updating.stop()
self.destroy()
self.parent.quit()
class StockTicker():
"""
Class StockTicker, handle each stock symbol and their data
attributes:
symbol, string, the abbreviature of the securitie
price, string, the current price of the securitie
direction, string(1), is a character that indicates its las fix price
went up, down or even
change, string, is the value of the last change surrounded by '()',
the first character is '+' or '-'
"""
def __init__(self, list_data):
self.symbol, self.price, self.direction, self.change = list_data
def update_ticker(self):
"""
update_ticker, update the securitie price, direction and
change with random values
"""
flt_price = float(self.price)
if randint(0, 9) == 0:
self.direction = CHAR_EVEN
else:
increase_percent = randlimit(-5, 5)
# TODO implementar normalvariate(0, 0.02) o gauss(0, 0.02)
flt_change = flt_price * increase_percent / 100
flt_new_price = flt_price + flt_change
self.price = "{:.2f}".format(flt_new_price)
if flt_change < 0:
self.direction = CHAR_DOWN
elif flt_change == 0:
self.direction = CHAR_EVEN
else:
self.direction = CHAR_UP
self.change = "({:+.2f})".format(flt_change)
def ticker_to_text(self):
"""
ticker_to_text, returns a formatted string with all the data of
the securitie.
"""
return " | {} {} {} {} ".format(self.symbol, self.price,
self.direction, self.change)
class StockMarket():
"""
Class StockMarket, creates and handle a list of StockTicker objects,
and provide to the GUI of stuff for the scroll ticker
attributes:
smarket, list of StockTicker objects
thread_actualizar, Thread object to update the stock market
each time interval
"""
def __init__(self, l_inicial):
self.tickers = []
self.index = 0
self._load(l_inicial)
self.current_ticker = self._get_one_ticker()
self.thread_updating = UpdateThread(self)
self.thread_updating.start()
def _load(self, l_inicial):
"""
load_market, load the list with StockTicker object taking the data from
the initial source data.
"""
for data_ticker in l_inicial:
simple_ticker = StockTicker(data_ticker)
self.tickers.append(simple_ticker)
def update_market(self):
"""
update_market, update the objects of the list
"""
for ticker in self.tickers:
ticker.update_ticker()
def _get_one_ticker(self):
"""
get_one_ticker, getter function to return one securitie data in text
format and rotates to the next one
"""
self.one_ticker = self.tickers.pop(0)
self.tickers.append(self.one_ticker)
self.index = 0
return self.one_ticker.ticker_to_text()
def get_next_character(self):
"""
get_next_character, returns a character of one securitie
(if the securitie data is exhausted retrieve another securitie)
data to the GUI.
"""
if self.index == len(self.current_ticker):
self.current_ticker = self._get_one_ticker()
self.index = 0
character_symbol = self.current_ticker[self.index:self.index+1]
self.index += 1
return character_symbol
def get_tag(self):
return self.one_ticker.direction
class UpdateThread(threading.Thread):
"""
Class UpdateThread(), subclass of Thread, handle the time to the next
update of the stock market values
args:
market_1, a StockMarket class object to update
attributes:
my_check, string for debugging purpouses, it'll be implemented the
source data management the_market, StockMarket object that will
be updated
"""
def __init__(self, market_1):
self.my_check = " CHECK " # TODO replace with initial source data.
self.the_market = market_1
self.is_quit = False
threading.Thread.__init__(self)
def run(self):
"""
run, overrides the Thread run method, and calls the update_market
method of StockMarket class each interval
"""
time.sleep(UPDATE_TIME)
self.the_market.update_market()
print(" UPDATED!!!") # for debugging
if not self.is_quit:
self.run()
def stop(self):
"""
stop, for stopping the thread when event occurs
"""
self.is_quit = True
# STARTS THE PROGRAM
def main():
the_window = tk.Tk()
aplication = AplicationTkinter(the_window)
# init the GUI process
the_window.mainloop()
if __name__ == '__main__':
main()
I want to load some part of the very big file into my QListWidget on Python PyQt. When user moves the scrollbar of the QListWidget and reaches the end of the scrollbar, the event is activating and the next part of the file is loading (appends) to the QListWidget. Is there some event for controlling the end position of the scrollbar?
There's no dedicated signal for "scrolled to end", but you can easily check that in the valueChanged signal:
def scrolled(scrollbar, value):
if value == scrollbar.maximum():
print 'reached max' # that will be the bottom/right end
if value == scrollbar.minimum():
print 'reached min' # top/left end
scrollBar = listview.verticalScrollBar()
scrollBar.valueChanged.connect(lambda value: scrolled(scrollBar, value))
EDIT:
Or, within a class:
class MyWidget(QWidget):
def __init__(self):
# here goes the rest of your initialization code
# like the construction of your listview
# connect the valueChanged signal:
self.listview.verticalScrollBar().valueChanged.connect(self.scrolled)
# your parameter "f"
self.f = 'somevalue' # whatever
def scrolled(self, value):
if value == self.listview.verticalScrollBar().maximum():
self.loadNextChunkOfData()
def loadNextChunkOfData(self):
# load the next piece of data and append to the listview
You should probably catch up on the docs for lambda's and the signal-slot framework in general.