Maya window adding a row dynamically with python - python

I've been working on this for a while and I can't find any information about adding a row to a window. I seen it done with pyside2 and qt, witch would work but the users are using multiple versions of Maya (2016 = pyside, 2017=pyside2).
I want it like adding a widget in in pyside. I done it where adding a row is a function like add row 1, add row 2, and add row 3 but the script get to long. I need to parent to rowColumnLayout and make that unique in order to delete that later. Also I have to query the textfield in each row. Maybe a for loop that adds a number to the row? I really don't know but this is what I have so far:
from maya import cmds
def row( ):
global fed
global info
item=cmds.optionMenu(mygroup, q=True, sl=True)
if item == 1:
cam=cmds.optionMenu(mygroup, q=True, v=True)
fed=cmds.rowColumnLayout(nc = 1)
cmds.rowLayout(nc=7)
cmds.text(l= cam )
cmds.text(l=u'Frame Range ')
start = cmds.textField('textField3')
cmds.text(l=u' to ')
finish = cmds.textField('textField2')
cmds.button(l=u'render',c='renderTedd()')
cmds.button(l=u'delete',c='deleteRow()')
cmds.setParent (fed)
def deleteRow ():
cmds.deleteUI(fed, layout=True)
if item == 2:
print item
global red
cam1=cmds.optionMenu(mygroup, q=True, v=True)
red = cmds.rowColumnLayout()
cmds.rowLayout(nc=7)
cmds.text(l= cam1 )
cmds.text(l=u'Frame Range ')
start = cmds.textField('textField3')
cmds.text(l=u' to ')
finish = cmds.textField('textField2')
cmds.button(l=u'render',c='renderTedd()')
cmds.button(l=u'delete',c='deleteRow2()')
cmds.setParent (red)
def deleteRow2 ():
cmds.deleteUI(red, control=True)
def cameraInfo():
info=cmds.optionMenu(mygroup, q=True, sl=True)
print info
def deleteRow ():
cmds.deleteUI(fed, control=True)
def getCamera():
layers=pm.ls(type="renderLayer")
for layer in layers:
pm.editRenderLayerGlobals(currentRenderLayer=layer)
cameras=pm.ls(type='camera')
for cam in cameras:
if pm.getAttr(str(cam) + ".renderable"):
relatives=pm.listRelatives(cam, parent=1)
cam=relatives[0]
cmds.menuItem(p=mygroup,label=str (cam) )
window = cmds.window()
cmds.rowColumnLayout(nr=10)
mygroup = cmds.optionMenu( label='Colors', changeCommand='cameraInfo()' )
getCamera()
cmds.button(l=u'create camera',aop=1,c='row ()')
cmds.showWindow( window )

This is totally doable with cmds. The trick is just to structure the code so that the buttons in each row know and can operate on the widgets in that row; once that works you can add rows all day long.
To make it work you want to do two things:
Don't use the string form of callbacks. It's never a good idea, for reasons detailed here
Do use closures to make sure your callbacks are referring to the right widgets. Done right you can do what you want without the overhead of a class.
Basically, this adds up to making a function which generates both the gui items for the row and also generates the callback functions -- the creator function will 'remember' the widgets and the callbacks it creates will have access to the widgets. Here's a minimal example:
def row_test():
window = cmds.window(title='lotsa rows')
column = cmds.columnLayout()
def add_row(cameraname) :
cmds.setParent(column)
this_row = cmds.rowLayout(nc=6, cw6 = (72, 72, 72, 72, 48, 48) )
cmds.text(l= cameraname )
cmds.text(l=u'Frame Range')
start = cmds.intField()
finish = cmds.intField()
# note: buttons always fire a useless
# argument; the _ here just ignores
# that in both of these callback functions
def do_delete(_):
cmds.deleteUI(this_row)
def do_render(_):
startframe = cmds.intField(start, q=True, v=True)
endframe = cmds.intField(finish, q=True, v=True)
print "rendering ", cameraname, "frames", startframe, endframe
cmds.button(l=u'render',c=do_render)
cmds.button(l=u'delete',c=do_delete)
for cam in cmds.ls(type='camera'):
add_row(cam)
cmds.showWindow(window)
row_test()
By defining the callback functions inside of add_row(), they have access to the widgets which get stored as start and finish. Even though start and finish will be created over and over each time the function runs, the values they store are captured by the closures and are still available when you click a button. They also inherit the value of cameraname so the rendering script can get that information as well.
At the risk of self-advertising: if you need to do serious GUI work using cmds you should check out mGui -- a python module that makes working with cmds gui less painful for complex projects.

Related

Build a fixed-size data table with tkinter / tksheet in Python -- bug in sheet_display_dimensions

I am building a user interface with several (as many as the user wants) tabular (spreadsheet-like) forms of user-specified size (but the size won't change once initialized). The user populates these tables either by copy-pasting data (usually from excel) or directly typing data to the cells. I am using the Tksheet Tkinter add-on.
It seems that there are several options in Tksheet to achieve the goal of opening an empty table of i rows and j columns:
a) set_sheet_data_and_display_dimensions(total_rows = None, total_columns = None).
This routine throws a TypeError. The error is raised in:
GetLinesHeight(self, n, old_method = False)
The subroutine expects the parameter n to be an integer, but receives a tuple.
The calling routine is sheet_display_dimensions, and the relevant line is:
height = self.MT.GetLinesHeight(self.MT.default_rh).
MT.default_rh is apparently a complex object, it can be an integer, but also a string or a tuple. Other routines that use it in Tksheet perform elaborate manipulation to make sure it is handed to the subroutine in integer form, but not so sheet_display_dimensions.
b) sheet_data_dimensions(total_rows = None, total_columns = None)
This seems to work programmatically, but does not display the table to the user.
One may add the line sheet_display_dimensions(i,j) but--you guessed it--this raises an error...
Sample code:
from tksheet import Sheet
import tkinter as tk
from tkinter import ttk
# This class builds and displays a test table. It is not part of the question but merely used to illustrate it
class SeriesTable(tk.Frame):
def __init__(self, master):
super().__init__(master) # call super class init to build frame
self.grid_columnconfigure(0, weight=1) # This configures the window's escalators
self.grid_rowconfigure(0, weight=1)
self.grid(row=0, column=0, sticky="nswe")
self.sheet = Sheet(self, data=[[]]) # set up empty table inside the frame
self.sheet.grid(row=0, column=0, sticky="nswe")
self.sheet.enable_bindings(bindings= # enable table behavior
("single_select",
"select_all",
"column_select",
"row_select",
"drag_select",
"arrowkeys",
"column_width_resize",
"double_click_column_resize",
"row_height_resize",
"double_click_row_resize",
"right_click_popup_menu",
"rc_select", # rc = right click
"copy",
"cut",
"paste",
"delete",
"undo",
"edit_cell"
))
# Note that options that change the structure/size of the table (e.g. insert/delete col/row) are disabled
# make sure that pasting data won't change table size
self.sheet.set_options(expand_sheet_if_paste_too_big=False)
# bind specific events to my own functions
self.sheet.extra_bindings("end_edit_cell", func=self.cell_edited)
self.sheet.extra_bindings("end_paste", func=self.cells_pasted)
label = "Change column name" # Add option to the right-click menu for column headers
self.sheet.popup_menu_add_command(label, self.column_header_change, table_menu=False, index_menu=False, header_menu=True)
# Event functions
def cell_edited(self, info_tuple):
r, c, key_pressed, updated_value = info_tuple # break the info about the event to individual variables
'''
updated_value checked here
'''
# passed tests
pass # go do stuff
def cells_pasted(self, info_tuple):
key_pressed, rc_tuple, updated_array = info_tuple # break the info about the event to individual variables
r, c = rc_tuple # row & column where paste begins
err_flag = False # will be switched if errors are encountered
'''
updated_array is checked here
'''
# passed tests
if err_flag: # error during checks is indicated
self.sheet.undo() # undo change
else:
pass # go do stuff
def column_header_change(self):
r, c = self.sheet.get_currently_selected()
col_name = sd.askstring("User Input", "Enter column name:")
if col_name is not None and col_name != "": # if user cancelled (or didn't enter anything), do nothing
self.sheet.headers([col_name], index=c) # This does not work - it always changes the 1st col
self.sheet.redraw()
# from here down is test code
tk_win = tk.Tk() # establish the root tkinter window
tk_win.title("Master Sequence")
tk_win.geometry("600x400")
tk_win.config(bg='red')
nb = ttk.Notebook(tk_win) # a notebook in ttk is a [horizontal] list of tabs, each associated with a page
nb.pack(expand=True, fill='both') # widget packing strategy
settings_page = tk.Frame(nb) # initiate 1st tab object in the notebook
nb.add(settings_page, text = "Settings") # add it as top page in the notebook
test = SeriesTable(nb) # creates a 1 row X 0 column table
nb.add(test, text = "Table Test") # add it as second page in the notebook
i = 4
j = 3
#test.sheet.set_sheet_data_and_display_dimensions(total_rows=i, total_columns=j) # raises TypeError
#test.sheet.sheet_data_dimensions(total_rows=i, total_columns=j) # extends the table to 4 X 3, but the display is still 1 X 0
#test.sheet.sheet_display_dimensions(total_rows=i, total_columns=j) # raises TypeError
test.sheet.insert_columns(j) # this works
test.sheet.insert_rows(i - 1) # note that we insert i-1 because table already has one row
test.mainloop()
I figured out a work-around with:
c)
insert_columns(j)
insert_rows(i - 1)
Note that you have to insert i-1 rows. This is because the sheet object is initiated with 1 row and 0 columns. (But does it say so in the documentation? No it does not...)

tkinter treeview open state is not up to date when callback is triggered

I am currently working on an tkinter application that uses a treeview element to display data in a hierarchy. In part of this application, I have the need to update data for items. Unfortunately, getting this data is expensive (in time and processing), so I now only update items that are currently visible.
The problem I am having is that when I bind a (left click) event to an item and then query the treeview for open items to try and determine what is visible, previously opened items are returned, but the currently opened item is not (until a subsequent item is clicked).
I saw this question about querying (and setting) the open state, but it doesn't seem to cover my needs.
I have the following simplified code, which demonstrates the problem:
## import required libraries
import tkinter
import tkinter.ttk
import random
import string
## event handler for when items are left clicked
def item_clicked(event):
tree = event.widget
## we assume event.widget is the treeview
current_item = tree.item(tree.identify('item', event.x, event.y))
print('Clicked ' + str(current_item['text']))
## for each itewm in the treeview
for element in tree.get_children():
## if it has elements under it..
if len(tree.get_children(element)) > 0:
## get the element name
element_name = tree.item(element)['text']
## check if it is open
if tree.item(element)['open'] is True:
print('Parent ' + element_name + ' is open')
else:
print('Parent ' + element_name + ' is closed')
## make the window and treeview
root = tkinter.Tk()
root.title('Treeview test')
tree = tkinter.ttk.Treeview(root, selectmode = 'browse', columns = ('num', 'let'))
tree.heading('#0', text = 'Name')
tree.heading('num', text = 'Numbers')
tree.heading('let', text = 'Letters')
tree.bind('<Button-1>', item_clicked)
tree.pack()
## populate the treeview with psuedo-random data
parents = ['']
for i in range(100):
id = str(i)
## higher probability that this won't be under anything
if random.randrange(50) > 40:
parent = random.choice(parents)
else:
parent = ''
## make up some data
tree.insert(parent, 'end', iid = id, text = 'Item ' + id)
tree.set(id, 'num', ''.join(random.choices(string.digits, k=10)))
tree.set(id, 'let', ''.join(random.choices(string.ascii_uppercase, k=10)))
parents.append(id)
## main event loop (blocking)
root.mainloop()
This code will generate a treeview with 100 items in a random heirachy. Clicking on any item will print a list of items that have children and are currently expanded (open). The issue is, as with my main code, not reporting the latest item opened (i.e. it is one step behind).
Adding update() to the treeview before querying its state (at the start of the item_clicked function) does not resolve the issue.
How can I accurately get the open state of the items in the event handler of the code above?
Is it an issue that the event is being fired before the item actually expands (opens), and if so, why is an update() not fixing this?
Does an alternative function exist for treeview widgets to return a list of currently visible items?
Running on Windows 10 with Python 3.7.3 (Tk version 8.6.9).
Changing the event binding from <Button-1> to <ButtonRelease-1> in the code above resolves the issue.
It should be noted in the above code that the open/close state will only be printed for items with children that have no parent (i.e. it won't descend in to the tree). To resolve this, you simply need to call tree.get_children(item_id) on each item with children recursively.

Why are multiple functions being called for this tkinter event binding? (solved)

The code snippet below creates two listboxes. I've written the code so that:
An item selected from the 'Available' list is transferred to the 'Selected' list upon clicking the item (controlled via the OnASelect method)
The index of the item that is transferred is conserved in the 'Available' list (it is replaced with an empty string)
An item selected from the 'Selected' list is transferred BACK to the 'Available' list upon clicking the item
Because the index of the 'Available' list is conserved, items are always transferred back in the original order regardless of what order they are clicked.
Here's my issue:
Despite the widget working as intended, I'm still getting an unnecessary function call to OnASelect upon transferring items BACK to the 'Available' list, which causes an Index Error. This widget is intended for a larger project I'm working on and I want to avoid this causing problems down the line.
For troubleshooting purposes, I've written my code so that when a transfer occurs, a print statement is sent out displaying either 'The A>S function was activated' or 'The S>A function was activated'.
Output upon transferring an item from 'Available' to 'Selected':
The A>S function was activated
Output upon transferring an item from 'Selected' to 'Available':
The S>A function was activated
The A>S function was activated
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\ProgramData\Anaconda3\lib\tkinter\__init__.py", line 1883, in __call__
return self.func(*args)
File "x:/Mouselight Data Management/GUI_Branch/GUI_Menu_Related/Test/test2.py", line 29, in OnASelect
AselectionIndex = int(event.widget.curselection()[0])
IndexError: tuple index out of range
PS X:\Mouselight Data Management\GUI_Branch> The A>S function was activate
I've wasted way too much time trying to figure out why it's doing this and I'm stumped. Any help with this issue would be much appreciated!
Ps. I know that adding an 'if event.widget.curselection():' at the start of the OnASelect method bypasses the error, but I want to prevent the multiple function call to begin with.
The full code:
from tkinter import *
class DependentLists(Tk):
def __init__(self):
Tk.__init__(self)
self.mainframe = Frame(self)
self.mainframe.place(relx=0.5, rely=0.25, anchor=CENTER)
completelabel = Label(self.mainframe, text = "Available:")
completelabel.grid(row=2, column = 0)
selectedlabel = Label(self.mainframe, text = "Selected:")
selectedlabel.grid(row=2, column = 1)
self.Available = Listbox(self.mainframe)
self.Available.grid(row=3, column = 0)
self.AList = []
for i in range(6):
self.Available.insert(END,i)
self.AList.append(i)
self.Available.bind('<<ListboxSelect>>', self.OnASelect)
self.Selected = Listbox(self.mainframe)
self.Selected.grid(row=3, column = 1)
self.Selected.bind('<<ListboxSelect>>', self.OnSSelect)
def OnASelect(self, event):
print('The A>S function was activated')
AselectionIndex = int(event.widget.curselection()[0])
Aselection = event.widget.get(AselectionIndex)
self.Available.delete(AselectionIndex)
self.Available.insert(AselectionIndex,'')
self.Selected.insert(END, Aselection)
def OnSSelect(self, event):
print('The S>A function was activated')
SselectionIndex = int(event.widget.curselection()[0])
Sselection = event.widget.get(SselectionIndex)
self.Selected.delete(ANCHOR)
self.Available.delete(self.AList.index(Sselection))
self.Available.insert(self.AList.index(Sselection), Sselection)
App = DependentLists()
screen_width = App.winfo_screenwidth()
screen_height = App.winfo_screenheight()
window_height = screen_height - 800
window_width = screen_width - 1400
x_cordinate = int((screen_width/2) - (window_width/2))
y_cordinate = int((screen_height/2) - (window_height/2))
App.geometry("{}x{}+{}+{}".format(window_width, window_height, x_cordinate, y_cordinate))
App.mainloop()
Edit: Solution is in BryanOakley's answer (thank you!) and the subsequent comments under that answer.
<<ListboxSelect>> is triggered whenever the selection changes. This can happen when the user clicks on an item, but it can also happen at other times, such as a user using the keyboard to traverse the listbox, or your code deleting something that was selected.
If your UI should only work on a mouse click, you should be binding to that rather than <<ListboxSelect>>.

One checkbox controls other checkboxes in Maya with Python

I am making a UI in Maya with Python. My goal is to make the master checkbox be able to control other checkboxes so that when the master checkbox is checked, all the other checkboxes are checked and vice versa. The function is as same as the Game Exporter/Animation Clips section in Maya. You can add multiple clips and use the master checkbox to control all clips. Does anyone know how to do that with Python?
The example (image) of what I want to achieve:
I've tried to use changeCommand, onCommand, and offCommand to edit other checkboxes, but it didn't work.
The code is like this (clipOn is multiple checkboxes, and masterCheckbox is the master checkbox that can control others):
clipOn = cmds.checkBox(label = '')
masterCheckbox = cmds.checkBox(label = '', onCommand = lambda x: cmds.checkBox(clipOn, editable = True, onCommand = True))
It's late and this is untested, but something like the following should work. Maya's 'onCommand'/ 'offCommand' parameters require a string or command to be returned. I used enumerate with list comprehension so that it should return a string for each value as it loops through and edits the checkboxes (rather than a single list). Of course you can always just call a regular function, which is arguably an easier and more straightforward approach.
master = cmds.checkBox(label='All')
check1 = cmds.checkBox(label='One')
check2 = cmds.checkBox(label='Two')
check3 = cmds.checkBox(label='Three')
checkAll = lambda state: [cmds.checkBox(c, edit=1, value=state) for i, c in enumerate([check1,check2,check3])][i]
cmds.checkBox(master, edit=1, onCommand=checkAll(True), offCommand=checkAll(False))
I would have thought:
checkAll = lambda state: cmds.checkBox([check1, check2, check3], edit=1, value=state)
would have worked. But it appears the checkBox command doesn't allow you to pass in multiple objects.
By the way, if you want to edit the 'onCommand' cmds.checkBox(clipOn, editable = True, onCommand = True)), you would pass a new command in not a bool value. Lastly, there is a 'checkBoxGrp' MEL command that allows creating and editing groups of checkboxes, however ultimately, I don't think it does anything different then what you can accomplish with the standard 'checkBox' command.
I would advise something like this :
from functools import partial
clipList = []
def addClip(*args):
newClip = cmds.checkBox(label='clip', p=mainLayout)
global clipList
clipList.append(newClip)
def setAllClips(ckb_master, *args):
value = cmds.checkBox(ckb_master, q=True, v=True)
if clipList:
for clip in clipList:
cmds.checkBox(clip, e=True, v=value)
mainLayout = cmds.columnLayout()
master = cmds.checkBox(label='All', p=mainLayout, cc=partial(setAllClips, master))
addClip()

Display hierarchy of selected object only

I am creating a UI where the user selects an object,
the UI will display its hierarchy of the selected
object.
It is somewhat similar to Outliner but I am unable to
find any documentation/ similar results to what I am
trying to obtain. And btw, I am coding using python...
Even so, is this even possible to do it in the first
place?
Allow me to provide a simple example below:
Say if I selects testCtrl, it will only displays
testCtrl, loc and jnt without showing the Parent (Grp 01)
Eg. Grp 01 --> testCtrl --> loc --> jnt
import maya.cmds as cmds
def customOutliner():
if cmds.ls( sl=True ):
# Create the window/UI for the custom Oultiner
newOutliner = cmds.window(title="Outliner (Custom)", iconName="Outliner*", widthHeight=(250,100))
frame = cmds.frameLayout(labelVisible = False)
customOutliner = cmds.outlinerEditor()
# Create the selection connection network; Selects the active selection
inputList = cmds.selectionConnection( activeList=True )
fromEditor = cmds.selectionConnection()
cmds.outlinerEditor( customOutliner, edit=True, mainListConnection=inputList )
cmds.outlinerEditor( customOutliner, edit=True, selectionConnection=fromEditor )
cmds.showWindow( newOutliner )
else:
cmds.warning('Nothing is selected. Custom Outliner will not be created.')
Make the window:
You want to use the treeView command (documentation) for this. I'm placing it in a formLayout for convenience.
from maya import cmds
from collections import defaultdict
window = cmds.window()
layout = cmds.formLayout()
control = cmds.treeView(parent=layout)
cmds.formLayout(layout, e=True, attachForm=[(control,'top', 2),
(control,'left', 2),
(control,'bottom', 2),
(control,'right', 2)])
cmds.showWindow(window)
Populate the tree view:
For this, we'll use a recursive function so you can build up the hierarchy with nested listRelatives calls (documentation). Start with the result of old faithful ls -sl:
def populateTreeView(control, parent, parentname, counter):
# list all the children of the parent node
children = cmds.listRelatives(parent, children=True, path=True) or []
# loop over the children
for child in children:
# childname is the string after the last '|'
childname = child.rsplit('|')[-1]
# increment the number of spaces
counter[childname] += 1
# make a new string with counter spaces following the name
childname = '{0} {1}'.format(childname, ' '*counter[childname])
# create the leaf in the treeView, named childname, parent parentname
cmds.treeView(control, e=True, addItem=(childname, parentname))
# call this function again, with child as the parent. recursion!
populateTreeView(control, child, childname, counter)
# find the selected object
selection = cmds.ls(sl=True)[0]
# create the root node in the treeView
cmds.treeView(control, e=True, addItem=(selection, ''), hideButtons=True)
# enter the recursive function
populateTreeView(control, selection, '', defaultdict(int))
Comparison of window to outliner.
I've replaced the spaces with X so you can see what's happening. Running this code will use spaces though:
You'll want to read up on the documentation to improve on this, but this should be a great starting point. If you want a live connection to the selection, make a scriptJob to track that, and be sure to clear the treeView before repopulating.

Categories

Resources