Attaching a class to a ui element such as uiButton (I am desperate)
I know this maybe a mute point for many, But I really would love a way to attach a class (a normal class, inheriting from object) to a ui object. In a generic way. Yes, I could write a custom class to encapsulate the ui element inside a custom class. However 2 things bother me about this approach, memory and performance overhead as well as losing the ability to code my custom class as I would say a normal button. Well at least I think that's the case. I tried to be smart and stuff my class reference into the transform attribute. But got a error of expecting a ui.transform object (makes sense). I had a glimmer of hope that if I could stuff the reference in there, maybe the underlying code only acknowledges its presence if that objects method are called explicitly on the object(I haven't seen class casting, maybe it's possible). Yeah, slim chance to none I guess. I Had to try. But again, I find my code gets so dirty and unwieldy quickly with this limitation when dealing with lots of ui objects. I accept it could be just my inexperience. I mentioned memory and performance overhead above. But I mention it because one view I present is a calendar. Each day is made up of 5 buttons and 2 views. Could be more by the time I am finished. But that alone is 155 buttons and 62 views on the screen for 31 days. Then I have a header for the days of the week, then a status bar. I also need filler bars for the previous months days etc...
Anyway, getting close to 250++ items on the view. Again, maybe this is not a problem. I just really don't know.
Sorry, I know this is a long winded post, but it's difficult to explain why I really want to be able to attach my own class to a ui element rather than encapsulating it in a custom class. I did briefly give some thought to trying to use the name of the ui element then trying to do something with eval, but I can see big potential problems with the garbage collector with reference counts and other things I don't even understand. But I have seen you guys do some amazing tricky code here before. I was hoping someone maybe able to pull a rabbit out of the hat that will enable me to attach and recover a class to a ui object generically.
Big thank you in advance!
A screen shot of the calendar I make reference to above. This pic has no days of the week or status bars.
https://www.dropbox.com/s/2ga2ndwmfioh9x7/file 6-07-2015 19 11 40.png?dl=0
No, buttons don't have a delegate, this solution of course only works with objects that have a delegate. I've never had to store extra data on delegate-less elements, so that always worked for me.
Gcarver's solution uses a custom class, which probably looks something like this:
class CustomAction(object): def __init__(self, action=None): self.action = action def __call__(self, sender): return self.action(sender)
__call__method makes instances of
CustomActioncallable like a function:
def real_action(self, sender): print("Eureka!") fake_action = CustomAction(real_action) button = ui.Button() button.action = fake_action button.action.random_attr = 42 # No error!
button.actionis an instance of a Python class, you can assign new attributes to it.
Now let's look at what happens when
- You tap the button
button.actionis not a function, but it has a
__call__method, so that gets called instead
Like the delegate variant, this only works on views that have an
@dgelessus, just return home after a hard night out. I think i follow the CustomAction code. I will give it a try in in some hours from now. 1:05am here already. But really thank you, it looks promising
neat idea with the
__call__... I learned something new!
yet another variation on gcarvers approach, which could be combined above: generally if you have extra data, you are going to be modifying it outside of an elements
action... so it is not necessary to access the custom object from within the ui element -- you probably already have a custom View containing class which can store the data object in a more accessible form(such as a list, or dict, or named attribute). if you use custom @property getters/setters, the underlying data is accessible, and can also update the ui element when the data changes.
here is a simple counter which has a data object, which also acts as a controller, as you can update the count which then automatically updates the view. this is a trivial example, but you could easily imagine a complicated object that updates a complicated bit of view... for instance, a Day controller object might have multiple representations, such as might be shown on a whole month vs single day. the higher level view doesn't need to know about how to draw a Day, the Day object can take care of that, and only triggers updates when the data changes
import ui class counter(object): def __init__(self): self._count=0 self.button=None def getButton(self): '''return the ui button object associated with this object, or cate a new instance''' if self.button is None: self.button=ui.Button(bg_color=(0,0,1)) self.button.action=self.button_action self.button.title=str(self._count) self.button.width=100 self.button.height=100 return self.button @property def count(self): return self._count @count.setter def count(self,value): '''update count attribute, and update the button title to show current count''' self._count=value if self.button: self.button.title=str(self._count) def button_action(self,sender): '''increment count in the underlying model. the @property takes care of updating the ui''' self.count+=1 b=counter() v=ui.View(bg_color='white') v.add_subview(b.getButton()) v.present() #note, if you set b.count from outside the view, the button title gets updated.
Why not create a
ButtonViewclass that is a ui.View that embeds a ui.Button inside and attach the Button frame size and action to the ButtonView frame size and action? This provides an object which can be customized to hold any data and have complex functionality yet it is a ui.View so it behaves nicely (resizing, hiding, colors, etc.) within a hierarchy of ui elements. By making all the dates on your calendar ButtonViews, you could have custom ui elements that held rich data and functionality yet in a ui view hierarchy, they behave the way that you expect all ui elements to behave.
Guys, thanks for all the input. I have had some friends turn up to visit (I live in a tourist town), I haven't had time to try much, but I will try each solution. I can not just read each solution and understand all the ramifications. Just don't have the experience yet. Just wanted to let you all know I appreciate all your solutions and I will definitely try them all.
import ui class CustomAction(object): def __init__(self): self.action = self.real_action self.myid = 666 def __call__(self, sender): return self.action(sender) def real_action(self, sender): print("Eureka!") btn = ui.Button(title = 'test') btn.action = CustomAction() btn.action.random_attr = 42 # No error! print btn.action.myid, btn.action.random_attr v = ui.View() v.add_subview(btn) v.present('sheet')
The ui elements that have action
Button has action method
ButtonItem has action method
SegmentedControl has action method
Slider has action method
Switch has action method
TextField has action method
DatePicker has action method
After playing around, and reading, my previous post would seem like the best solution for me. Of course it would be better if every single ui element had an action, but it seems the elements I normally deal with today are covered. But ultimately the real solution will come from omz. Just takes one common user property across all ui classes. But it is still a fantastic discussion. Brings out a lot of innovation from you guys.
Have to say it's very nice, the CustomAction(object) approach....
import ui import uuid import datetime class CustomAction(object): def __init__(self): self.action = self.real_action self.uuid = uuid.uuid4() def __call__(self, sender): return self.action(sender) def real_action(self, sender): print("Eureka!") if __name__ == '__main__': btn = ui.Button(title = 'test') btn.action = CustomAction() btn.action.random_attr = 42 # No error! btn.action.today = datetime.date.today() print btn.action.uuid, btn.action.random_attr print btn.action.today td = datetime.timedelta(days = 1) btn.action.today += td print btn.action.today print '*' * 59 print 'dir btn' print dir(btn) print '*' * 59 print 'dir btn.action' print dir (btn.action) v = ui.View() v.add_subview(btn) v.present('sheet')
Seems to work very generically.
I did some more playing around with the CustomAction Class. I was able to use isinstance to verify if an action has a CustomAction, also if no action is provided from the calling class, a function in the CustomAction is called.
One thing I am not sure about is the getting a reference to the parent object that the CustomAction is attached to. At the moment, I pass the parent object in the init of the CA Class. I Would appreciate if anyone could tell me how I could get a reference without having to pass it as a param. Seems to me I am missing something easy here. This would just clean it up a little.
I think the rest of my code is ok. Just trying to get a basic template in place.
import ui import uuid def make_btn(title, use_custom_action = False, action = None): btn = ui.Button(title = title) if use_custom_action: btn.action = CustomAction(btn, action) else: btn.action = action btn.border_width = 1 btn.width , btn.height = 100, 32 return btn class MyCustomClass(ui.View): def __init__(self): self.background_color = 'white' # make a btn with a CustomAction, with action btn = make_btn('Custom',True, self.btn_act) self.add_subview(btn) btn.x , btn.y = 100, 100 # make a btn with a CustomAction, # with no action, will call CustomAction action btn = make_btn('Custom 2',True, None) self.add_subview(btn) btn.x , btn.y = 100, 150 # referencing the CustomAction inline self.ext(btn).group_id = 666 # create a button without the CustomAction btn = make_btn('Normal',False , self.btn_act) btn.x , btn.y = 100, 200 self.add_subview(btn) # pass though reference to the CustomAction def ext(self, obj): return obj.action def btn_act(self, sender): # check to see if sender has a CustomAction if isinstance(sender.action, CustomAction): ca = self.ext(sender) print ca.index, ca.group_id, ca.alt_text, ca.uuid else: print sender.title class CustomAction(object): def __init__(self, parent, action = None, group_id = None ): # i think i need to pass in the parent obj in # the init to be able to recover it in my code self.obj = parent # if no action is passed on init, use a function in the CustomActionClass if not action: action = self.fallback_act self.action = action #some vars i think will be useful self.index = -1 self.group_id = group_id self.alt_text = None # may or may not be useful later self.uuid = uuid.uuid4() def __call__(self, sender): #the magic thanks to pythonista Forums return self.action(sender) # this func is called if no action is supplied # in the init def fallback_act(self, sender): print 'called in the CustomAction' print self, sender if __name__ == '__main__': x = MyCustomClass() x.present('sheet')
That is correct. Without storing the "parent" object as an attribute, there is no way to tell what an object's "parent object" is. The reason for that is simple - a single object can have more than one name. For example:
class Useless(object): def __init__(self, attr=None): self.attr = attr test_list =  useless1 = Useless(test_list) useless2 = Useless(test_list) # Now both useless1 and useless2 have test_list as their parent. # Proof: useless1.attr.append('hi there') print(useless2.attr) # --> ['hi there']
There is no single "parent object" in this case. So yes, you need to store the parent as an attribute on
CustomAction. In most cases the
senderparameter should be enough though, unless you need to use it outside of
@dgelessus, ok thanks. I thought I might have been missing some trick. But makes sense what you say. I think it's better just to pass the parent each time rather than speculating when you actually might need to use the parent outside call. So I will just do that.