On 20.01.2005, at 12:24, Mark English wrote:
I'd like to write a Tkinter app which, given a class, pops up a
window(s) with fields for each "attribute" of that class. The user could
enter values for the attributes and on closing the window would be
returned an instance of the class. The actual application I'm interested
in writing would either have simple type attributes (int, string, etc.),
or attributes using types already defined in a c-extension, although I'd
prefer not to restrict the functionality to these requirements.
I am working on nearly the same thing! Small sort of generic attribute editor
in Tkinter (and Tix). Altough my implementation is still very unpythonic
(== ugly) in many places, it can edit the attributes of instances and classes,
as well as generate new instances / classes. I would like to create new
attributes or delete them as well, but haven't done it so far.
A still faraway dream would be, to allow the user to enter source code, that
will be compiled into byte code and assign it to the instance, so the user can
modify code at run time (if it is possible to disallow access to insecure
modules while code is compiled).
Secondly, the code won't know exactly how to initialise the class instance used to determinte the attributes. Do I need to make it a prerequesite that all instances can be created with no arguments ?
I did it this way, too.
Maybe you can provide a parameterless __new__ in addition to the __init__.
I imagine this is the way that e.g. pickle does the job. Well, I don't
know. Let this sentence just be an invitation for an expert to write something
here.
Should I force/allow the user to pass an instance instead of a class ?
However you like. I prefer passing classes, otherwise you end up in a situation where you need to create dummy instances that are only used as "copying templates". If you get an instance: foo = type(bar)() would give you an instance of the same class as bar. But this just looks like a hack to me when compared to foo = Bar().
Passing an instance allows you to look at its __dict__ of course. But you have no assurance that the variables you find there are present in all instances of that class. Phython is just to dynamic for that.
Should I be using inspect.getargspec on the class __init__ method and then a loop with a try and a lot of except clauses, or is there a nicer way to do this ? Presumably the pickling code knows how do serialise/deserialise class instances but I'm not sure how I'd use this without already having a class instance to hand.
Personally, I ended up writing a class Attribute that provides access to
the attributes and allows more finetuning than a generic approach would
do (think of e.g. validation). My overall setting works like this:
For each attribute that you want to appear in the GUI you define an Attribute
class Attribute(object) : def __init__(self, name, # name of the attribute type, # specifies the widget type values=None, # then OptionMenu is used instead validate=None, # if given, a callable that returns # either True or False get=None, # callable to get the actual value # None: generic getattr() widget_options={} # passed to the Tix widget used ) : pass # [...]
The "controller" of an editable object gets the object and a list
of those Attribute()'s. There is a generic one that handles validation
and generic setattr(), but can be overwritten to allow more sophisticated
stuff.
When creating a new instance, I ask for initial arguments which I know (like __name__ and __doc__ if its a class that I create) in nearly the same way.
If you already have your instance, it is possible to generate the Attribute()-list from the dict of this instance:
attributes = [
Attribute(name,type(getattr(instance,name))) for name in dir(instance)
]
Lastly, does such an app already exist ?
As you see, my solution is not as general as what you might have in mind.
I needed to finetune so much of the underlying generic plan, that it
looks more like declaration-based now.
Anyway, if you are interested: This is the edit part of my app (It's only
the mapping from attributes to widgets, so no instance creation in this part).
As this was only a short snippet in the project I am doing, I could, never
give it the time it deserved. So I am sure that my code is not the best sollution,
but maybe it serves as a starting point for further discussion.
All the best,
- harold -
import Tkinter import Tix
class Attribute(object) : def __init__(self, name, type, values=None, validate=None, get=None, widget_options={} ) : if not get : get = lambda obj : getattr(obj,name)
self.name = name self.type = type self.values = values self.get = get self.validate = validate self.widget_options = widget_options self.label = None self.widget = None
class Edit(Tkinter.Frame) : import types
def __init__(self,obj,items) : self.obj = obj self.items = items
def create_widgets(self,master,**options) : Tkinter.Frame.__init__(self,master,**options) for item in self.items : if item.values : widget = Tix.OptionMenu(master,label=item.name) for v in item.values : widget.add_command(str(v),label=str(v)) item.label = None item.widget = widget else : widget = self.widget_types[item.type](master) widget.config(label=item.name,**item.widget_options) item.label = widget.label item.widget = widget.entry widget.pack(side=Tkinter.TOP,fill=Tkinter.X) self.pack()
def update_widget(self,item) : if isinstance(item.widget,Tix.OptionMenu) : item.widget.set_silent(item.get(self.obj)) else : item.widget.delete(0,Tkinter.END) item.widget.insert(0,item.get(self.obj))
def update_widgets(self) : for item in self.items : self.update_widget(item)
def validate(self,item) : if isinstance(item.widget,Tix.OptionMenu) : return True value = item.widget.get() if item.validate : valid = item.validate(value) elif item.values : valid = value in item.values else : try : valid = item.type(value) except ValueError : valid = False if valid : item.label.config(fg="black") else : item.label.config(fg="red") item.widget.focus_set() return valid def isvalid(self) : for item in self.items : if not self.validate(item) : return False return True
def apply(self) : if self.isvalid() : for item in self.items : try : value = item.type(item.widget.get()) except AttributeError : value = item.type(item.widget.cget("value")) if value != item.get(self.obj) : setattr(self.obj,item.name,value)
widget_types = { types.StringType : Tix.LabelEntry, types.IntType : Tix.Control, types.FloatType : Tix.LabelEntry, }
if __name__ == "__main__" : class myClass : string = "hello world" float = 1. int = 0
proxy = Edit( myClass, [ Attribute("string",type(str())), Attribute("float",type(float())), Attribute("int",type(int()),validate=lambda x : int(x) in [-1,0,1] ) ] )
win = Tix.Tk(None) proxy.create_widgets(win) proxy.update_widgets()
button1 = Tkinter.Button( win, text="apply", command=proxy.apply ) button1.pack() button2 = Tkinter.Button( win, text="update", command=proxy.update_widgets ) button2.pack() win.mainloop()
-- Man will occasionally stumble over the truth, but most of the time he will pick himself up and continue on. -- Winston Churchill
-- http://mail.python.org/mailman/listinfo/python-list