[maya][python] [ UI ] acessing controls from external functions

So heres my functional UI:

import maya.cmds as cmds
import os
from functools import partial

fruits=['apples','oranges','bananas']
veggies=['carrots','onions','celery']

def populateTextScrollList(textScrollList,itemlist,*args):
	cmds.textScrollList(textScrollList,edit=True,ra=True)
	for item in itemlist:
		cmds.textScrollList(textScrollList,edit=True,a=item)

def UI():

	#check if window exists
	if cmds.window('UI', exists=True):
		cmds.deleteUI('UI')
	window=cmds.window('UI', title='UI',minimizeButton=False , maximizeButton=False, sizeable=False)
	layout=cmds.columnLayout('masterLayout',w=300)

	#Produce frame
	cmds.frameLayout(label='Produce',parent=layout)
	produceList=cmds.textScrollList('produceList')
	produceRadioBtnGrp=cmds.radioButtonGrp(labelArray2=['fruit','vegetable'], numberOfRadioButtons=2,select=1,on1=partial(populateTextScrollList,produceList,fruits),on2=partial(populateTextScrollList,produceList,veggies))
	populateTextScrollList(produceList,fruits)
	cmds.button(label='confirm list')
	cmds.text(label='')


#launch UI
UI()


I fee like the radioButtonGroup() command, with those partial() commands is just too cumbersome.
I had tried to use a simpler function callback:


import maya.cmds as cmds
import os
from functools import partial

fruits=['apples','oranges','bananas']
veggies=['carrots','onions','celery']

def populateTextScrollList(textScrollList,itemlist,*args):
	print 'populateTextScrollList(list)...'
	cmds.textScrollList(textScrollList,edit=True,ra=True)
	for item in itemlist:
		cmds.textScrollList(textScrollList,edit=True,a=item)

def updateTheList(*args):
	selectedRadioButton=cmds.radioButtonGrp(produceRadioBtnGrp,q=True,select=True)
	populateTextScrollList(produceList,selectedRadioButton)

def UI():

	if cmds.window('UI', exists=True):
		cmds.deleteUI('UI')
	window=cmds.window('UI', title='UI',minimizeButton=False , maximizeButton=False, sizeable=False)
	layout=cmds.columnLayout('masterLayout',w=300)

	#Produce frame
	cmds.frameLayout(label='Produce',parent=layout)
	produceList=cmds.textScrollList('produceList')
	produceRadioBtnGrp=cmds.radioButtonGrp(labelArray2=['fruit','vegetable'], numberOfRadioButtons=2,select=1,cc=updateTheList)
	populateTextScrollList(produceList,fruits)
	cmds.button(label='confirm list')
	cmds.text(label='')
	#launch UI
	cmds.showWindow(window)
UI()


and use an external function updateTheList() to repopulate the textScrollList
but the two functions ( UI() and updateTheList() ) are ignorant of each other, so the updateTheList() function can’t find any of the controls created in UI().
I can’t query the state of the radioButtonGrp or populate the textScrollList from an external function,
because the function updateTheList() throws an error that the controls are undefined.

how is this typically done?
how do I query and update UI controls from external functions?

I got this working with help from a colleague, who identified some beginner level errors:

[ul]
[li]naming controls wrong
[/li][li]naming controls the same as the variable that holds them
[/li][li]accesing controls by variable name instead of the control name
[/li][li]naming things with maya.cmds keywords
[/li][/ul]

the sort answer to 'how do I query /update controls form external functions? ’
Is: name any control you need to access when you create it, and use that name,
for example:


variable_used_to_create_control=cmds.textScrollList('name_used_by_external_functions',...)

def doStuffWithMyTextScrollList():
	cmds.textScrollList('name_used_by_external_functions',query=True,...)
	cmds.textScrollList('name_used_by_external_functions',edit=True,...)

Probably noob stuff to most here, but this tidbit of info is remarkably hard to find in the maya docs or google.

Don’t refer to your controls by name. When you run them in the script editor all the names live in the main namespace so things will play nicely, but when you import the module, everything will be an attribute of the module, so name_used_by_external_functions will become my_module.name_used_by_external_functions and you won’t be able to access it with your code.

Instead put all of your UI code and callbacks in a class. You can set the layouts/controls you need to access as class attributes and then refer to them in each callback that needs to query/modify them.

import maya.cmds as cmds
import os
from functools import partial

class UI(object):
    def __init__(self):
        self.winname = 'UI'
        self.fruits = ['apples','oranges','bananas']
        self.veggies = ['carrots','onions','celery']

        self.build_ui()

    def populateTextScrollList(self, textScrollList, itemlist, *args):
        cmds.textScrollList(textScrollList, edit=True, ra=True)
        for item in itemlist:
            cmds.textScrollList(textScrollList, edit=True, a=item)

    def build_ui(self):
        #check if window exists
        if cmds.window(self.winname, exists=True):
            cmds.deleteUI(self.winname)
        win = cmds.window(self.winname, title='UI', minimizeButton=False,
                                  maximizeButton=False, sizeable=False)
        cmds.columnLayout('masterLayout', w=300)

        #Produce frame
        cmds.frameLayout(label='Produce')
        self.produceList = cmds.textScrollList('produceList')
        self.produceRadioBtnGrp = cmds.radioButtonGrp(labelArray2=['fruit', 'vegetable'], 
                                                numberOfRadioButtons=2, 
                                                select=1, 
                                                on1=partial(self.populateTextScrollList, self.produceList, self.fruits), 
                                                on2=partial(self.populateTextScrollList, self.produceList, self.veggies))
        self.populateTextScrollList(self.produceList, self.fruits)
        cmds.button(label='confirm list')
        cmds.text(label='')

        cmds.showWindow(win)

UI()

A few random points:
After you create a layout, anything you create after that will be created as a child of that layout, until you call setParent and then everything after that will be a child of the next parent up in the stack. You don’t need to specify the -parent flag when creating the frameLayout, it will automatically become a child of the columnLayout.
Put spaces between assignment operators (fruits = [] not fruits=[]), and after spaces in lists/function calls ([1, 2], not [1,2]); it will make your code easier to read.

Capper’s answer is the true and correct path.

A perennial topic:

The key, as capper points out, is to keep things in a nice tidy local (class) scope and use that to manage things instead of relying on the names of individual controls. It’s cleaner, more modular, and doesn’t involve you in scary stuff like global variables…

Thanks all for your responses.
Let me be sure I get this then:

so by using a class to define my UI, I can effectively query/edit controls by referring to them attributes of the UI object.

I have a question . Using Capper’s helpful example code, this all works as I expect:

myUI = UI()
print myUI.fruits
myUI.fruits = [ 'oranges', 'grapefuits', 'papayas' ]
print myUI.fruits
myUI.populateTextScrollList ( myUI.produceList, myUI.fruits)

but if I toggle the radio buttons to veggie and back to fruit, the list resets, losing the updated myUI.fruits list I just delcared.
Even though print myUI.fruits continues to return my new list.
Is it therefore necessary to include, as a class method ,some function for updating any variables declared in init?
how do I keep those variable from resetting back to their initialized value?

In the abstract there are two basic ways to do it:

  1. The class basically manages references to the UI elements. It exposes these to other code as method calls or python properties:

import maya.cmds as cmds
import random

class DirectUIExample(object):
    def __init__(self, *items):
            self.__items = list(items) # private __name on purpose.
            self.Window = None
    def create_UI(self):
        self.Window = cmds.window()
        _col = cmds.columnLayout()
        self.List = cmds.textScrollList()
        for e in self.__items:
            cmds.textScrollList(self.List, e=True, a=e)
        _btn = cmds.button("Add", c = self.add_item)

    def add_item(self, *ignore):
         dummy_values = ['kumquat', 'papaya', 'durian', 'kohlrabi']
         r = random.randint(0,3)
         cmds.textScrollList(self.List, e=True, a = dummy_values[r])
         
    def show(self):
         self.create_UI()
         cmds.showWindow(self.Window)
         
    def get_items(self):
        if not self.Window: return []
        return cmds.textScrollList(self.List, q=True, ai=True)
    
    Items = property(get_items)
    
        
         
ex = DirectUIExample('apple','orange', 'pear')
ex.show()
print ex.get_items()
print ex.Items  # the property construct is really just disguising the method call
ex.add_item()
print ex.Items

#[u'apple', u'orange', u'pear']
#[u'apple', u'orange', u'pear']
#[u'apple', u'orange', u'pear', u'kumquat']

  1. You maintain the internal list as the primary data store and you make sure to update it from every UI event that could touch it:

class IndirectUIExample(object):
    def __init__(self, *items):
            self.__items = list(items) # private __name on purpose.
            self.Window = None
    def create_UI(self):
        self.Window = cmds.window()
        _col = cmds.columnLayout()
        self.List = cmds.textScrollList()
        _btn = cmds.button("Add", c = self.add_item)
        self.update_display()

    def update_display(self, *ignore):
        cmds.textScrollList(self.List, e=True, ra=True)
        for e in self.__items:
            cmds.textScrollList(self.List, e=True, a=e)

    def add_item(self, *ignore):
        dummy_values = ['kumquat', 'papaya', 'durian', 'kohlrabi']
        r = random.randint(0,3)
        self.__items.append(dummy_values[r])
        self.update_items()       

    def show(self):
         self.create_UI()
         cmds.showWindow(self.Window)
         
    def get_items(self):
       return self.__items
    
    def set_items(self, items):
        self.__items = items
        if self.Window: 
            self.update_items()

    Items = property(get_items, set_items) # in this version there's a property setter too...
    
        
         
ex = IndirectUIExample('apple','orange', 'pear')
ex.show()
print ex.Items
ex.Items = ['banana', 'grape', 'strawberry']
print ex.Items

#[u'apple', u'orange', u'pear']
#[u'banana', u'grape', u'strawberry']

The choice between these is dictated by how complex the updating is. If you have to do really complex stuff to the data (filtering a list, for example) it’s easier to work with the data directly. If you’re mostly exposing the data and letting users mess with it via gui widgets (eg: a list of checkboxes) it’s easier to read the data directly out of the UI widgets. Example 1 is good if all the work is going to happen in the UI, example 2 is better if some other entity might want to update the class and it’s UI from outside.

As for why your updated fruits list wasn’t being used to update the textScrollList when using the radioButtons (I’m also not saying you should use this method, as Theo’s examples are much better; this is just an explanation):

When you use partial, the arguments that you give it (in your case, self.produceList and self.fruits/veggies) are “frozen” in their current state at definition-time and stored in the function object. That way whenever you call the resulting function, it will use the variables as they were when the partial was created.
From the Docs: it “freezes some portion of a function’s arguments and/or keywords resulting in a new object".

To get the behavior you were expecting, you can use lambdas and include the variables you want read at run-time in the statement body:

on1=lambda x:self.populateTextScrollList(self.produceList, self.fruits)

Because objects in the lambda statement are read at run-time, not definition time, the current contents of self.fruits will be passed at the time of each function call.


Great examples Theo. I like the use of a property to remove direct control of the __items list. It’s good to hear (and have detailed) different UI design decisions. It’s something I haven’t given enough thought to and seeing your examples makes me realize that there are probably plenty of ways I could be improving the flow and management of data in my UIs.

Theodox, thanks so much for taking the time to explain all this.
If I understand correctly:

    def show(self):
         self.create_UI()
         cmds.showWindow(self.Window)
         
    def get_items(self):
        if not self.Window: return []
        return cmds.textScrollList(self.List, q=True, ai=True)
    
    Items = property(get_items)

uses the python bif property
to implement Items as a “read/write” property of the ui class.

So if external functions were generating/modifying the list, this would be the preferred way to do it?

Yes. It’s good practice to use properties for class data that you want to expose to other code – that way you can make sure that any cleanup or reactions to value changes can will be handled in a uniform way (in this case, that would mean updating the UI when the list of values changes). It also means calling code doesn’t need to know or care how the value is gotten (in this case, we use it to hide the fact that we’re querying the UI widget – in another, it might hide caching, a database lookup over the network or whatever – the calling code doesn’t know or care where the value comes from)

The basic principle is that you want bits of different code to know as little as possible about each other.

Suppose you did it the old fashioned way and made the calling code query the UI directly:



def get_something_from_the_UI(DirectUI_instance):
    print "the value is',  cmds.textScrollList(DirectUI_instance.List, q=True, ai=True)

def change_directUI_value (DirectUI_instance, new value):
    cmds.textScrollList(DirectUI_instance.List, e=True, ra=True) 
    for x in new_value:
        cmds.textScrollList(DirectUI_instance.List, e=True, ai=x) 
        

It’s all OK until you decide that you’d like to swap out the textScrollList for a iconTextScrollList so you can add icons. Now you’ve got to track down every piece of code everywhere that might have referenced the old textScrollList and change it, plus make sure that they all do the formatting the same way… as you can imagine, that get’s old real fast. Forcing it through the property decorator means (a) you’re telling the world you want them to get-set this value through this-and-only-this doorway and (b) you know every time the value is set or gotten so you can respond in the UI or other code.

Hardcore C#/Java types will often try to use properties exclusively and never use fields at all. In python land that’s kind of overkill, since it’s really easy to convert a field to a property retroactively. But if the data is anything other than a completely dumb bit of data that just sits around, properties are a good way to make sure you can react correctly when they are updated.