I have a Toplevel widget which asks the User what widget he want to spawn and then asks for cnf. Whatfor and stuff is not important.
The cnf will be asked in an Scrollframe-Table-something (I don't really know how to describe it ^^'). Extra for that Scrollframe and its Scrollbar I made a Frame, so I can easily pack it left and right. But somehow the Scrollframe is taking the Tk window (root of my toplevel) as master.
Here is the code - I can't find my mistake:
from tkinter import _cnfmerge as cnfmerge
from tkinter import *
class Scrollframe(Frame):
def __init__(self,master=None,height=200,width=200,**kw):
if 'yscrollcommand' in kw:
self.ysc=kw['yscrollcommand']
del kw['yscrollcommand']
else: ysc=None
if 'pad' in kw:
self.pad=kw['pad']
del kw['pad']
else: self.pad=0
Frame.__init__(self,height=height,width=width)
self.scrollframe=Frame(self,**kw)
self.scrollframe.place(x=0,y=0,relwidth=1)
self.config(bg=self.scrollframe['bg'])
self.bind('<Configure>',self.adopt)
self.widgets,self.scrollable={},False
def adopt(self,event=None):
if self.scrollframe.winfo_height()>self.winfo_height():
self.scrollable=True
self.scrollframe.place(y=0)
self.ysc(0,0)
else:
self.scrollable=False
self.ysc(0,1)
def addItem(self,widget=None,cnf={},**kw):
if widget:
cnf=cnfmerge((cnf,kw))
if 'width' in cnf: del cnf['width']
obj=widget(self.scrollframe,cnf)
if len(self.widgets)==0 and self.pad!=0: obj.pack(fill=X)
else: obj.pack(fill=X,pady=(self.pad,0))
id_=str(id(obj))+widget.__name__
obj.bind('<Destroy>',lambda event: self.delItem(id_),'+')
self.widgets[id_]=obj
return id_
def getItem(self,id):
return self.widgets[id]
def delItem(self,id):
try: self.widgets[id].destroy()
except TclError: del self.widgets[id]
except KeyError: pass
def yview(self,*args):
try: delta=int(args[1])
except ValueError: delta=float(args[1])
maxnegscroll=self.winfo_height()-self.scrollframe.winfo_height()
if isinstance(delta,float):
if maxnegscroll<0: self.scrollframe.place(y=int(maxnegscroll*delta))
delta=abs(int(self.scrollframe.place_info()['y'])/maxnegscroll)
self.ysc(delta,delta)
else:
delta=-delta*3
if int(self.scrollframe.place_info()['y'])+delta<maxnegscroll: self.scrollframe.place(y=maxnegscroll)
elif int(self.scrollframe.place_info()['y'])+delta>0: self.scrollframe.place(y=0)
else: self.scrollframe.place(y=int(self.scrollframe.place_info()['y'])+delta)
delta=abs(int(self.scrollframe.place_info()['y'])/maxnegscroll)
self.ysc(delta,delta)
class CreateWindow(Toplevel):
def __init__(self,master=None):
Toplevel.__init__(self,master,height=458,width=400)
self.grab_set()
self.resizable(False,False)
self.title('Neues Item')
self.vars,self.cnf,self.cnfids={},{},{}
cnf=create_dict(bg='gainsboro',width=380)
Frame(self,cnf=cnf,height=39).place(x=10,y=10)
Frame(self,cnf=cnf,height=103).place(x=10,y=59)
Frame(self,cnf=cnf,height=220).place(x=10,y=172)
bottom=Frame(self,cnf=cnf,height=46)
bottom.pack_propagate(False)
bottom.place(x=10,y=402)
var,values,self.oldwidget=StringVar(value='Frame'),list(_tkinter_widgets.keys())[2:],'Frame'
for i in range(len(values)): values[i]=values[i].__name__
Spinbox(self,values=values,textvar=var,state=READONLY,cursor='arrow',command=self.refresh,buttonuprelief=FLAT,buttondownrelief=FLAT,wrap=True).place(x=20,y=20)
self.vars['widget']=var
Label(self,text='Höhe:',bg='gainsboro',anchor=W,bd=1).place(x=20,y=69)
var=StringVar()
Entry(self,textvar=var,justify=CENTER,width=40).place(x=136,y=69)
self.vars['height']=var
Label(self,text='Breite:',bg='gainsboro',anchor=W,bd=1).place(x=20,y=98)
var=StringVar()
Entry(self,textvar=var,justify=CENTER,width=40).place(x=136,y=98)
self.vars['width']=var
var=BooleanVar(value=True)
Checkbutton(self,onvalue=True,offvalue=False,text='Farbe übernehmen (falls vorhanden)',variable=var,cursor='hand2',bg='gainsboro',activebackground='gainsboro').place(x=20,y=127)
self.vars['takecolor']=var
cnfsframe=Frame(self,height=200,width=360)
cnfsframe.pack_propagate(0)
cnfsframe.place(x=20,y=182)
sb=Scrollbar(cnfsframe)
sb.pack(fill=Y,side=RIGHT)
self.cnfs=Scrollframe(master=cnfsframe,width=360-17,height=200,yscrollcommand=sb.set)
self.cnfs.pack(fill=Y,side=LEFT)
sb.config(command=self.cnfs.yview)
for arg in _tkinter_widgets[Frame]:
id=self.cnfs.addItem(Frame,height=19,width=360)
obj=self.cnfs.getItem(id)
var=StringVar()
Entry(obj,width=35,justify=CENTER,textvar=var).place(x=146,y=0)
Label(obj,text=arg,bd=1).place(x=0,y=0)
self.cnf[arg],self.cnfids[arg]=var,id
Button(bottom,text='Bestätigen',command=self.confirm,width=12,height=1).pack(side=LEFT,padx=10,pady=10)
Button(bottom,text='Abbrechen',command=self.destroy,width=12,height=1).pack(side=RIGHT,padx=(0,10),pady=10)
def refresh(self):
self.vars['height'].set(''),self.vars['width'].set(''),self.vars['takecolor'].set(True)
for arg in _tkinter_widgets[eval(self.oldwidget)]:
self.cnfs.delItem(self.cnfids[arg])
del self.cnfids[arg],self.cnf[arg]
for arg in _tkinter_widgets[eval(self.vars['widget'].get())]:
id=self.cnfs.addItem(Frame,height=19,width=360)
obj=self.cnfs.getItem(id)
obj.pack_propagate(False)
var=StringVar()
Entry(obj,width=35,justify=CENTER,textvar=var).pack(side=RIGHT)
Label(obj,text=arg,bd=1).pack(fill=X,side=LEFT)
self.cnf[arg],self.cnfids[arg]=var,id
self.oldwidget=self.vars['widget'].get()
self.focus()
def confirm(self):
raise NotImplementedError #first I'll have to fix that scrollframe issue xD
if __name__=='__main__':
t=Tk()
cw=CreateWindow(t)
Before someone asks what self.scrollable in Scrollframe is for: Its for the MouseWheel binding I'll implement later.
In this line, you are not passing the master to the super-class' __init__:
Frame.__init__(self,height=height,width=width)
Just change it to:
Frame.__init__(self,master=master, height=height,width=width)
That said, it is a general Python recommendation to use super() instead of hardcoding the superclass name:
super().__init__(master=master, height=height, width=width)
Related
I am (for some elaborate setup reasons) trying to retrieve the actual command callback function from tkinter widgets, for example setting up a callback for a button b
import tkinter as tk
root = tk.Tk()
b = tk.Button(root, text='btn', command=lambda:print('foo'))
both
b['command']
b.cget('command')
which I think both are equivalent to
b.tk.call(b._w, 'cget', '-command')
will only return a string like "2277504761920<lambda\>" and not the actual command function. Is there a way to get the actual callback function?
I cannot imagine any case and Im not sure at all if this answers your question but it maybe equivalent for what you are looking for:
The invoke method of the button seems pretty equivalent to me. So solution-1 would be:
import tkinter as tk
def hi():
print('hello')
root = tk.Tk()
b = tk.Button(root, text='test', command=hi)
b.pack()
cmd = b.invoke
#cmd = lambda :b._do('invoke')
root.mainloop()
If this isnt what you looking for you could call the function in tcl level. Solution-2:
import tkinter as tk
def hi():
print('hello')
root = tk.Tk()
b = tk.Button(root, text='test', command=hi)
b.pack()
cmd = lambda :root.tk.call(b['command'])
#cmd= lambda :root.tk.eval(b['command'])
cmd()
root.mainloop()
Solution 3, would be to return your function by invoke:
import tkinter as tk
def hi():
print('hello')
return hi
root = tk.Tk()
b = tk.Button(root, text='test', command=hi)
b.pack()
cmd = b.invoke()
print(cmd) #still a string but comparable
root.mainloop()
This is a more complex solution. It patches Misc._register, Misc.deletecommand and Misc.destroy to delete values from dict tkinterfuncs. In this example there are many print to check that values are added and removed from the dict.
import tkinter as tk
tk.tkinterfuncs = {} # name: func
def registertkinterfunc(name, func):
"""Register name in tkinterfuncs."""
# print('registered', name, func)
tk.tkinterfuncs[name] = func
return name
def deletetkinterfunc(name):
"""Delete a registered func from tkinterfuncs."""
# some funcs ('tkerror', 'exit') are registered outside Misc._register
if name in tk.tkinterfuncs:
del tk.tkinterfuncs[name]
# print('delete', name, 'tkinterfuncs len:', len(tkinterfuncs))
def _register(self, func, subst=None, needcleanup=1):
"""Return a newly created Tcl function. If this
function is called, the Python function FUNC will
be executed. An optional function SUBST can
be given which will be executed before FUNC."""
name = original_register(self, func, subst, needcleanup)
return registertkinterfunc(name, func)
def deletecommand(self, name):
"""Internal function.
Delete the Tcl command provided in NAME."""
original_deletecommand(self, name)
deletetkinterfunc(name)
def destroy(self):
"""
Delete all Tcl commands created for
this widget in the Tcl interpreter.
"""
if self._tclCommands is not None:
for name in self._tclCommands:
# print('- Tkinter: deleted command', name)
self.tk.deletecommand(name)
deletetkinterfunc(name)
self._tclCommands = None
def getcommand(self, name):
"""
Gets the command from the name.
"""
return tk.tkinterfuncs[name]
original_register = tk.Misc.register
tk.Misc._register = tk.Misc.register = _register
original_deletecommand = tk.Misc.deletecommand
tk.Misc.deletecommand = deletecommand
tk.Misc.destroy = destroy
tk.Misc.getcommand = getcommand
if __name__ == '__main__':
def f():
root.after(500, f)
root = tk.Tk()
root.after(500, f)
but1 = tk.Button(root, text='button1', command=f)
but1.pack()
but2 = tk.Button(root, text='button2', command=f)
but2.pack()
but3 = tk.Button(root, text='button3', command=lambda: print(3))
but3.pack()
print(root.getcommand(but1['command']))
print(root.getcommand(but2['command']))
print(root.getcommand(but3['command']))
but3['command'] = f
print(root.getcommand(but3['command']))
root.mainloop()
Looking at tkinter.__init__.py:
class BaseWidget:
...
def _register(self, func, subst=None, needcleanup=1):
"""Return a newly created Tcl function. If this
function is called, the Python function FUNC will
be executed. An optional function SUBST can
be given which will be executed before FUNC."""
f = CallWrapper(func, subst, self).__call__
name = repr(id(f))
try:
func = func.__func__
except AttributeError:
pass
try:
name = name + func.__name__
except AttributeError:
pass
self.tk.createcommand(name, f)
if needcleanup:
if self._tclCommands is None:
self._tclCommands = []
self._tclCommands.append(name)
return name
and
class CallWrapper:
"""Internal class. Stores function to call when some user
defined Tcl function is called e.g. after an event occurred."""
def __init__(self, func, subst, widget):
"""Store FUNC, SUBST and WIDGET as members."""
self.func = func
self.subst = subst
self.widget = widget
def __call__(self, *args):
"""Apply first function SUBST to arguments, than FUNC."""
try:
if self.subst:
args = self.subst(*args)
return self.func(*args)
except SystemExit:
raise
except:
self.widget._report_exception()
We get that tkinter wraps the function in the CallWrapper class. That means that if we get all of the CallWrapper objects we can recover the function. Using #hussic's suggestion of monkey patching the CallWrapper class with a class that is easier to work with, we can easily get all of the CallWrapper objects.
This is my solution implemented with #hussic's suggestion:
import tkinter as tk
tk.call_wappers = [] # A list of all of the `MyCallWrapper` objects
class MyCallWrapper:
__slots__ = ("func", "subst", "__call__")
def __init__(self, func, subst, widget):
# We aren't going to use `widget` because that can take space
# and we have a memory leak problem
self.func = func
self.subst = subst
# These are the 2 lines I added:
# First one appends this object to the list defined up there
# the second one uses lambda because python can be tricky if you
# use `id(<object>.<function>)`.
tk.call_wappers.append(self)
self.__call__ = lambda *args: self.call(*args)
def call(self, *args):
"""Apply first function SUBST to arguments, than FUNC."""
try:
if self.subst:
args = self.subst(*args)
return self.func(*args)
except SystemExit:
raise
except:
if tk._default_root is None:
raise
else:
tk._default_root._report_exception()
tk.CallWrapper = MyCallWrapper # Monkey patch tkinter
# If we are going to monkey patch `tk.CallWrapper` why not also `tk.getcommand`?
def getcommand(name):
for call_wapper in tk.call_wappers:
candidate_name = repr(id(call_wapper.__call__))
if name.startswith(candidate_name):
return call_wapper.func
return None
tk.getcommand = getcommand
# This is the testing code:
def myfunction():
print("Hi")
root = tk.Tk()
button = tk.Button(root, text="Click me", command=myfunction)
button.pack()
commandname = button.cget("command")
# This is how we are going to get the function into our variable:
myfunction_from_button = tk.getcommand(commandname)
print(myfunction_from_button)
root.mainloop()
As #hussic said in the comments there is a problem that the list (tk.call_wappers) is only being appended to. THe problem will be apparent if you have a .after tkinter loop as each time .after is called an object will be added to the list. To fix this you might want to manually clear the list using tk.call_wappers.clear(). I changed it to use the __slots__ feature to make sure that it doesn't take a lot of space but that doesn't solve the problem.
When you assign a command to a widget, or bind a function to an event, the python function is wrapped in a tkinter.CallWrapper object. That wrapper contains a reference to the python function along with a reference to the widget. To get a callback for a widget you can iterate over the instances of the wrapper in order to get back the original function.
For example, something like this might work:
import tkinter as tk
import gc
def get_callback(widget):
for obj in gc.get_objects():
if isinstance(obj, tk.CallWrapper) and obj.widget == widget:
return obj.func
return None
You can then directly call the return value of this function. Consider the following block of code:
import tkinter as tk
import gc
def get_callback(widget):
for obj in gc.get_objects():
if isinstance(obj, tk.CallWrapper) and obj.widget == widget:
return obj.func
def do_something():
print(f"button1: {get_callback(button1)} type: {type(get_callback(button1))}")
print(f"button2: {get_callback(button2)} type: {type(get_callback(button2))}")
root = tk.Tk()
button1 = tk.Button(root, text="do_something", command=do_something)
button2 = tk.Button(root, text="lambda", command=lambda: do_something())
button1.pack(padx=20, pady=20)
button2.pack(padx=20, pady=20)
root.mainloop()
When I click either button, I see this in the console output which proves that the get_callback method returns a callable.
button1: <function do_something at 0x103386040> type: <class 'function'>
button2: <function <lambda> at 0x103419700> type: <class 'function'>
Button is a object you can assign attributes just define your function outside the button and assign the function ass a attribute
func_print = lambda: print("nice")
x = Button(..., command=func_print)
x.my_func = func_print
def something():
x.my_func()
something()
>>> nice
I was looking same problem but I could not find any nice answer then I created mine actually it is very easy
I am working on a data acquisition project, building multiple data-monitoring/controlling programs for different instruments (voltmeter, camera, etc.) using python. I am using python3 and tkinter (due to its open license) as my GUI.
The basic structure for each instrument right now is:
import packages
class all_GUI():
def __init__():
device = volt_device()
functions linking GUI elements to HW calls
mainloop()
class volt_device():
def __init__():
functions to access HW functionality
mainapp = all_GUI()
It more-less works, but there are many calls between GUI and hardware classes all over the code. If I want to reuse GUI part of the code and link it with another hardware board I pretty much have to rewrite the whole thing. As you can imagine this is not very appealing :-)
I suppose class volt_device can be moved into a separate file and loaded as needed. But because GUI calls many functions from HW part, each HW file (supporting different board, for example) would have to have the exact same naming convention. Not terrible, but not the best either I think.
I was looking into separating GUI and HW as much as possible, but had some difficulties. I was looking into a model-view-controller pattern, but could not make it work. My idea was having three programs:
import GUI
import HW
objGUI =
objHW =
link functions to interface objects
mainloop()
class GUI():
def __init__():
build GUI here with all elements
(this is getting sticky since I need to define functions to be executed when GUI values change
or buttons are pushed)
Have multiple hardware files supporting different instruments.
class HW():
def __init__():
define hardware board, have functions to change/monitor values
Ideally, I would have a relatively simple HW file (file 3). To have whole new virtual device I would have to load GUI portion (file 2; unmodified) and write a simple "controller" (file 1) linking GUI elements to HW functions. Sounds simple ...
I got stuck when I tried to link GUI and HW together. I was not sure how to properly address GUI elements and assign them appropriate HW call/function. Perhaps the whole idea is flawed and the GUI/HW separation needs to approached differently ...
I am sure this problem must have been tackled before I just cannot find it ... or figure it out right now. I would greatly appreciate any suggestions and/or coding references you might have.
Thank you.
Radovan
...would have to have the exact same naming convention. Not terrible, but
not the best either I think.
On the contrary, that is probably the best method. In essence you would create a generic interface and have each "board" implement the interface with it's specifics or subclass something that does. Then you create a class for tkinter that can build an interface from the methods and arguments.
Both displays were automatically generated and one way or another everything leads back to the most basic component.
very generic and simplified example:
import tkinter as tk, abc
from typing import List, Tuple, Callable, Iterable, Dict
import inspect
#create formal interface
class IGenericBoard(metaclass=abc.ABCMeta):
#classmethod
def __subclasshook__(cls, subclass):
isinterface = hasattr(subclass, 'read_pin') and callable(subclass.read_pin)
isinterface &= hasattr(subclass, 'write_pin') and callable(subclass.write_pin)
return isinterface
#abc.abstractmethod
def generic_pin_read(self, pin:int) -> int:
raise NotImplementedError
#abc.abstractmethod
def generic_pin_write(self, pin:int, data:int):
raise NotImplementedError
#implement IGenericBoard
class GenericBoard(IGenericBoard):
#property
def model(self):
#the "model type" for this board instance
return type(self).__name__
#property
def prefix(self) -> List:
#the prefix(es) to use when finding functions
return self._prefix if isinstance(self._prefix , (List, Tuple)) else [self._prefix]
#property
def msgvar(self) -> tk.StringVar:
#the output message var
return self._msgvar
#property
def attributes(self) -> Dict:
#get everything in one shot ~ for **kwargs
return dict(
model =self.model ,
prefix=self.prefix,
msgvar=self.msgvar,
)
def __init__(self):
self._prefix = 'generic'
self._msgvar = tk.StringVar()
def generic_pin_read(self, pin:int) -> int:
self._msgvar.set(f'reading pin {pin}')
#... really do this
return 0
def generic_pin_write(self, pin:int, data:int):
self._msgvar.set(f'writing {data} on pin {pin}')
#... really do this
#"final" class
class LEDBoard(GenericBoard):
def __init__(self):
GenericBoard.__init__(self)
self._prefix = self.prefix + ['led']
def led_blink_write(self, pin:int=13):
self.generic_pin_write(pin, 1)
self._msgvar.set(f'blinking on pin {pin}')
#... really do this
''' tkBaseBoard
the baseclass for all "tk[Version]Board" classes
generates form interfaces for methods with the proper prefix(es)
'''
class tkBaseBoard(tk.Frame):
def __init__(self, master, model, msgvar, prefix, **kwargs):
tk.Frame.__init__(self, master, **{'bd':2, 'relief':'raised', **kwargs})
self.grid_columnconfigure(0, weight=1)
#board model label
tk.Label(self, text=model, font="Consolas 12 bold").grid(row=0, column=0, sticky='w')
#message output from board
self.output_ent = tk.Entry(self, width=30, textvariable=msgvar)
self.output_ent.grid(row=2, column=0, sticky='e')
#common feature label configuration
self.lbl_opts = dict(width=6, anchor='w', font='Consolas 10')
#annotation conversion
self.conversion = {
"<class 'int'>" :lambda: tk.IntVar(),
"<class 'str'>" :lambda: tk.StringVar(),
"<class 'bool'>" :lambda: tk.BooleanVar(),
"<class 'float'>":lambda: tk.DoubleVar(),
}
#build a feature for every "feat_" suffixed method
for feature in [func for func in dir(self) if callable(getattr(self, func)) and func.split('_')[0] in prefix]:
self._add_feature(feature)
#create a list of variable values
def __tovalue(self, vars) -> List[int]:
return [v.get() for v in vars]
#dynamically create the gui for a method
def _add_feature(self, feature):
main = tk.Frame(self)
main.grid(sticky='we')
#parse feature components
command = getattr(self, feature)
featcmp = feature.split('_')
if featcmp and len(featcmp) == 3:
_, label, action = featcmp
#create a list of Vars based on command argument types
args, vars = inspect.signature(command).parameters, []
for name in args:
try:
#convert annotations to the proper tk.[Type]Var
vars.append(self.conversion[str(args[name].annotation)]())
except KeyError:
#fallback to StringVar
vars.append(tk.StringVar())
#create label and button for this command
tk.Label(main, text=label, **self.lbl_opts).grid(row=0, column=0, sticky='e')
tk.Button(main, text=action, width=5, command=lambda v=vars: command(*self.__tovalue(v))).grid(row=0, column=1, sticky='w', padx=8)
#create an Entry for every argument in command
for i, v in enumerate(vars):
tk.Entry(main, width=2, textvariable=v).grid(row=0, column=i+2, sticky='w')
#give all the weight to the last row
main.grid_columnconfigure(i+2, weight=1)
else:
#feature name components did not pass expectations
raise ValueError('ValueError: feature component must consist of three underscore-seperated parts as: PREFIX_LABEL_ACTION')
##EXAMPLES OF THE ULTIMATE IMPLEMENTATION ALL OF THE ABOVE ALLOWS
#generate GenericBoard display
class tkGenericBoard(tkBaseBoard, GenericBoard):
def __init__(self, master, **kwargs):
GenericBoard.__init__(self)
tkBaseBoard.__init__(self, master, **self.attributes, **kwargs)
#generate LEDBoard display
class tkLEDBoard(tkBaseBoard, LEDBoard):
def __init__(self, master, **kwargs):
LEDBoard.__init__(self)
tkBaseBoard.__init__(self, master, **self.attributes, **kwargs)
##EXAMPLE BASE USAGE
if __name__ == '__main__':
root = tk.Tk()
root.title('Example')
root.configure(padx=2, pady=2)
tkGenericBoard(root).grid()
tkLEDBoard(root).grid()
root.mainloop()
I'm converting an old tkinter program to wxPython. One of the things from tk that I used liberally was tk.IntVar() and the like. Is there anything in wx that provides similar functionality?
Specifically, I'd like to be able to define module-level variables such as myvar = tk.StringVar(). Then when those variables are updated, have one or more UI elements update based on the new variable value just like what would happen with:
self.score = tk.Entry(self, textvariable=myvar.get())
here is how you would normally organize your app .... globals tend to be a bad idea
class MyNestedPanel(wx.Panel):
def __init__(self,*a,**kw):
...
self.user = wx.TextCtrl(self,-1)
def SetUser(self,username):
self.user.SetValue(username)
class MyMainPanel(wx.Panel):
def __init__(self,*a,**kw):
...
self.userpanel = MyNestedPanel(self,...)
def SetUsername(self,username):
self.userpanel.SetUser(username)
class MainFrame(wx.Frame):
def __init__(self,*a,**kw):
...
self.mainpanel = MyMainPanel(self,...)
def SetUsername(self,username):
self.mainpanel.SetUsername(username)
a = wx.App()
f = MainFrame(...)
f.Show()
a.MainLoop()
although you can make helper functions
def set_widget_value(widget,value):
if hasattr(widget,"SetWidgetValue"):
return widget.SetWidgetValue(value)
if isinstance(widget,wx.Choice):
return widget.SetStringSelection(value)
if hasattr(widget,"SetValue"):
return widget.SetValue(value)
if hasattr(widget,"SetLabel"):
return widget.SetLabel(value)
else:
raise Exception("Unknown Widget Type : %r"%widget)
def get_widget_value(widget):
if hasattr(widget,"GetWidgetValue"):
return widget.GetWidgetValue()
if isinstance(widget,wx.Choice):
return widget.GetStringSelection()
if hasattr(widget,"GetValue"):
return widget.GetValue()
if hasattr(widget,"GetLabel"):
return widget.GetLabel()
else:
raise Exception("Unknown Widget Type : %r"%widget)
class WidgetManager(wx.Panel):
def __init__(self,parent):
self._parent = parent
wx.Panel.__init__(self,parent,-1)
self.CreateWidgets()
def CreateWidgets(self):
#create all your widgets here
self.widgets = {}
def SetWidgetValue(self,value):
if isinstance(value,dict):
for k,v in value.items():
set_widget_value(self.widgets.get(k),v)
else:
raise Exception("Expected a dictionary but got %r"%value)
def GetWidgetValue(self):
return dict([(k,get_widget_value(v))for k,v in self.widgets])
and then use them like this https://gist.github.com/joranbeasley/37becd81ff2285fcc933
This is my code:
def configure_event(self, widget):
if self.is_hiding:
self.window.present()
else:
self.window.iconify()
def delete_event(self, widget, data=None):
gtk.main_quit()
return True
def popup_menu(self):
self.menu = gtk.Menu()
self.about = gtk.MenuItem("about")
if self.is_hiding:
self.expand = gtk.MenuItem("show")
else:
self.expand = gtk.MenuItem("hide")
self.quit = gtk.MenuItem("quit")
self.about.connect("activate", self.about_monitor)
self.expand.connect("activate", self.configure_event)
self.quit.connect("activate", self.delete_event)
self.menu.popup(None, None, gtk.status_icon_position_menu, event_button, event_time, self.tray_icon)
self.menu.append(self.about)
self.menu.append(self.expand)
self.menu.append(self.monitor)
self.menu.append(self.quit)
self.menu.show_all()
delete_event works, but configure_event does not. Why?
look at the signatures of both of them:
def configure_event(self, widget):
def delete_event(self, widget, data=None):
delete_event has a third argument data (that has a default of None) but configure_event only has two.
although i don't know what the exception was i bet that the exception was:
TypeError: configure_event() takes exactly 2 arguments (3 given)
if so changing configure_event's signature to:
def configure_event(self, widget, data):
would fix it. note that i think the default value of None is unneeded as gtk will pass something in always.
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())