#この記事の目的
自作ツールの中身を一つ一つ確認しながら理解を深め、今後のツール開発に役立てます。
今までなんとなくコピペで動いてたからいいやとスルーしてきたことを徹底理解します。
同じようにスクリプトを書いていてなんとなく動いてるからいっか、で終わってる人への一助になればと思います。
今回はPyQt、QTreeView、QStandardItemModelがメインです。
#ツール概要
シーン内のNotesを一覧を表示します。
各ノードにはNotesというアトリビュートが備わっています。AttributeEditorの一番下にこの機能がありますが使っていますか?
ここにメモを残すことでこのノードが何をやっているか把握しやすくなります。僕はMayaファイル内に各ノードのメモを残すときによく使っています。
しかし、すべてのNotesを一覧表示する方法が見つからなかったためツールにしました。
GIF画像のようにシーン内のNotesが残されているノードを一括表示します。
ノードの選択や、Notesの編集が可能です。
動作環境:Maya2019.3,Maya2020.1
Pyside2を使用しているので2016以前では動作しません。
#コード全文
# -*- coding: utf-8 -*-
from maya.app.general.mayaMixin import MayaQWidgetBaseMixin
from PySide2 import QtWidgets, QtCore, QtGui
from maya import cmds
class Widget(QtWidgets.QWidget):
def __init__(self):
super(Widget, self).__init__()
# グリッドレイアウト作成
layout = QtWidgets.QGridLayout(self)
# ツリービュー追加
treeView = TreeView()
layout.addWidget(treeView, 0, 0, 1, -1)
# ツリービューにアイテム設定
self.stockItemModel = StockItemModel()
treeView.setModel(self.stockItemModel)
# ツリービューに選択アイテム設定
self.itemSelectionModel = QtCore.QItemSelectionModel(self.stockItemModel)
treeView.setSelectionModel(self.itemSelectionModel)
# セレクトボタン
button = QtWidgets.QPushButton('Select')
button.clicked.connect(self.select)
layout.addWidget(button, 1, 0)
# リフレッシュボタン
button = QtWidgets.QPushButton('Refresh')
button.clicked.connect(self.refresh)
layout.addWidget(button, 1, 1)
# 初期読み込み
self.refresh()
# セレクトボタンの動作
def select(self):
cmds.select(cl=True)
indexes = self.itemSelectionModel.selectedIndexes()
for index in indexes:
(node, notes) = self.stockItemModel.rowData(index.row())
cmds.select(node, noExpand=True ,add=True)
# リフレッシュボタンの動作
def refresh(self):
# 空の辞書を作成
list = {}
# notesがあるアトリビュートを辞書で取得
for node in cmds.ls(allPaths=True):
if cmds.attributeQuery ('notes',node=node,exists=True):
list[node] = cmds.getAttr(node+'.notes')
# アイテムモデルを空にする
self.stockItemModel.removeRows(0, self.stockItemModel.rowCount())
# 取得した辞書からアイテムを追加する
for note in list:
self.stockItemModel.appendItem(note, list[note])
class TreeView(QtWidgets.QTreeView):
def __init__(self):
super(TreeView, self).__init__()
# 選択の種類を変更する(エクセルみたいな感じ)
self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
# ツリービューの行の色を交互に変更する
self.setAlternatingRowColors(True)
class StockItemModel(QtGui.QStandardItemModel):
def __init__(self):
super(StockItemModel, self).__init__(0, 2)
# ヘッダー設定
self.setHeaderData(0, QtCore.Qt.Horizontal, 'Node Name')
self.setHeaderData(1, QtCore.Qt.Horizontal, 'Notes')
# 編集したときの動作
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role:
# 編集したデータを取得
(node, attr) = self.rowData(index.row())
# Notesアトリビュートを変更
cmds.setAttr(node+'.notes', value, type='string')
# 編集したアイテムを変更
self.item(index.row(), index.column()).setText(value)
# 成功したことを報告
return True
# 指定の行からnode,attrを返す
def rowData(self, row):
# ノードとアトリビュート取得
node = self.item(row, 0).text()
attr = self.item(row, 1).text()
return (node, attr)
# アイテムを追加する
def appendItem(self, nodename, notes):
# ノードは編集不可で作成
nodeItem = QtGui.QStandardItem(nodename)
nodeItem.setEditable(False)
# Notesは編集可で作成
notesItem = QtGui.QStandardItem(notes)
notesItem.setEditable(True)
# 行を追加
self.appendRow([nodeItem, notesItem])
class MainWindow(MayaQWidgetBaseMixin, QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle('List Notes')
self.resize(430, 260)
widget = Widget()
self.setCentralWidget(widget)
def main():
window = MainWindow()
window.show()
if __name__ == "__main__":
main()
#ツールの設計図
おおよそこのような感じ。
ツールの動作を把握するためにも設計図を描くとわかりやすい。
#詳細を見ていく
以前書いた部分に関しては下記参照。新しく出てきた項目について書いて行きます。
【Maya Python】スクリプトの中身を噛み砕く1 ~カメラスピードエディタ編
##ツリービューの見た目設定(QTreeView)
# ツリービュー追加
treeView = TreeView()
layout.addWidget(treeView, 0, 0, 1, -1)
TreeView()
クラスをインスタンスし、グリッドレイアウトに追加します。
ちなみにクラス名は頭文字大文字がPEP8で推奨されています。僕はわかりやすいように、クラス名は頭文字大文字、インスタンス名は頭文字小文字にして区別しています。
少し飛びますが先にTreeView
クラスを見ましょう。
class TreeView(QtWidgets.QTreeView):
def __init__(self):
super(TreeView, self).__init__()
# 選択の種類を変更する(エクセルみたいな感じ)
self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
# ツリービューの行の色を交互に変更する
self.setAlternatingRowColors(True)
QtWidgets.QTreeViewをTreeViewに継承します。
基本的にはそのまま使うのでオーバーライド部分は少ないです。
self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
選択のモード変更を行います。
デフォルトのままだと一つしか選択できないので複数選択できるように変更します。ExtendedSelection
を使用するとエクセルのような飛び石選択ができるようになります。
self.setAlternatingRowColors(True)
行の色を交互に変更します。
こういったツリービューは量が増えると見づらくなるのでゼブラ柄にして見やすくします。
このようにQTreeViewではツリービューの見た目や動作を設定します。
##ツリービューにアイテムを設定(StockItemModel、QItemSelectionModel)
# ツリービューにアイテム設定
self.stockItemModel = StockItemModel()
treeView.setModel(self.stockItemModel)
# ツリービューに選択アイテム設定
self.itemSelectionModel = QtCore.QItemSelectionModel(self.stockItemModel)
treeView.setSelectionModel(self.itemSelectionModel)
StockItemModel()
クラスをself.stockItemModel
という名前でインスタンス
先程作ったツリービューに作成したアイテムをセットします。
QtCore.QItemSelectionModel
を使い、self.stockItemModel
の選択状態を取得できるように設定しておきます。
継承してオーバーライドする必要がないのでそのままインスタンスして使います。
setSelectionModel
でtreeView
にself.itemSelectionModel
を選択モデルとしてセットします。
では、次にStockItemModel()
クラスについて見てみます。
##StockItemModel
###初期化、ヘッダーの設定(init)
class StockItemModel(QtGui.QStandardItemModel):
def __init__(self):
super(StockItemModel, self).__init__(0, 2)
# ヘッダー設定
self.setHeaderData(0, QtCore.Qt.Horizontal, 'Node Name')
self.setHeaderData(1, QtCore.Qt.Horizontal, 'Notes')
QStandarItemModel
をStockItemModel
として継承します。
初期設定でヘッダーの設定をします。
super(StockItemModel, self).__init__(0, 2)
初期化時に、0行、2列のアイテムモデルを作ります。
self.setHeaderData(0, QtCore.Qt.Horizontal, 'Node Name')
0列目に、Node Nameという名前でヘッダーを作成します。
self.setHeaderData(1, QtCore.Qt.Horizontal, 'Notes')
1列目に、Notesという名前でヘッダーを作成します。
QtCore.Qt.Horizontal
は向きの設定、ここではヘッダーなので水平を指定します。
###編集したときの動作を設定(setData)
# 編集したときの動作
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role:
# 編集したデータを取得
(node, attr) = self.rowData(index.row())
# Notesアトリビュートを変更
cmds.setAttr(node+'.notes', value, type='string')
# 編集したアイテムを変更
self.item(index.row(), index.column()).setText(value)
# 成功したことを報告
return True
編集されたときにどういう動作をするか設定します。
index
は編集した場所、value
は編集内容、role
は役割を指定します。QtCore.Qt.EditRole
で編集に適した形式を指定します。
編集したデータのrowから、rowData
クラスを使いnode,attr
を取得します。
rowData
については後述します。
cmds.setAttr(node+'.notes', value, type='string')
Mayaコマンドです。編集したアトリビュートをノードに反映します。
self.item(index.row(), index.column()).setText(value)
編集した部分をツリービューのアイテムにも反映します。
###指定の行からノード、アトリビュートを取得(rowData)
先ほど出てきたrowDataクラスについての説明です。
# 指定の行からnode,attrを返す
def rowData(self, row):
# ノードとアトリビュート取得
node = self.item(row, 0).text()
attr = self.item(row, 1).text()
return (node, attr)
指定した行(row)から、node,attr
(ノード名、アトリビュート内容)として値を返します。
item
クラスは(行、列)で要素の取得ができます。
そのままだとオブジェクトデータになので、.text()
で文字列に変換しています。
###アイテムを追加する(appendItem)
アイテムを追加する機能を作っておきます。
このあとボタン等から呼び出します。
# アイテムを追加する
def appendItem(self, nodename, notes):
# ノードは編集不可で作成
nodeItem = QtGui.QStandardItem(nodename)
nodeItem.setEditable(False)
# Notesは編集可で作成
notesItem = QtGui.QStandardItem(notes)
notesItem.setEditable(True)
# 行を追加
self.appendRow([nodeItem, notesItem])
ノード名、Notesから、アイテムを追加します。
QtGui.QStandardItem(nodename)
アイテムの表示内容を設定します。
nodeItem.setEditable(False)
ノードの方は編集不可にします。
notesItem.setEditable(True)
Notesの方は編集可能にします。
self.appendRow([nodeItem, notesItem])
設定した内容を行(row)に追加します。
appendRow
はQStandardItemModel
のファンクションです。
こういった機能をそのまま使えるのがPySideの強みですね。
##ボタンの作成
# セレクトボタン
button = QtWidgets.QPushButton('Select')
button.clicked.connect(self.select)
layout.addWidget(button, 1, 0)
# リフレッシュボタン
button = QtWidgets.QPushButton('Refresh')
button.clicked.connect(self.refresh)
layout.addWidget(button, 1, 1)
基本的には【Maya Python】スクリプトの中身を噛み砕く1 ~カメラスピードエディタ編と同じです。
次は、ボタンの中身を見ていきます。
###セレクトボタンの動作
# セレクトボタンの動作
def select(self):
cmds.select(cl=True)
indexes = self.selItemModel.selectedIndexes()
for index in indexes:
(node, notes) = self.itemModel.rowData(index.row())
cmds.select(node, noExpand=True ,add=True)
cmds.select(cl=True)
現在の選択状態をなくします。
selectedIndexes()
はQItemSelectionModel
のファンクションで、選択しているインデックスをリストで返してくれます。
for index in indexes:
選択している回数分繰り返します。
self.itemModel.rowData(index.row())
選択した行のノード名、notesを取得。
詳細は先程の指定の行からノード、アトリビュートを取得(rowData)参照
cmds.select(node, noExpand=True ,add=True)
取得したノードを追加選択。
noExpand=True
は選択セットのオブジェクトではなく、選択セットを選択できるようにします。
add=True
追加選択モードに変更します。
###リフレッシュボタンの動作
# リフレッシュボタンの動作
def refresh(self):
# 空の辞書を作成
list = {}
# notesがあるアトリビュートを辞書で取得
for node in cmds.ls(allPaths=True):
if cmds.attributeQuery ('notes',node=node,exists=True):
list[node] = cmds.getAttr(node+'.notes')
# アイテムモデルを空にする
self.stockItemModel.removeRows(0, self.stockItemModel.rowCount())
# 取得した辞書からアイテムを追加する
for note in list:
self.stockItemModel.appendItem(note, list[note])
for...
notesがあるノードを辞書形式でリストアップします。
self.stockItemModel.removeRows(0, self.stockItemModel.rowCount())
0行目から行数分(self.stockItemModel.rowCount()
)まで削除します。
self.stockItemModel.appendItem(note, list[note])
取得した辞書(list)からアイテムを追加します。
先ほど作成したアイテムを追加する(appendItem)を参照。
##初期読み込み
# 初期読み込み
self.refresh()
最後に最初に立ち上げたときにNotesを取得します。
refreshの挙動と同じなのでそれを使いまわします。
#おわりに
今回はPyQtの記述が多かったですが、なかなかネットで情報収集が難しかったです。
公式のドキュメントの見方がもう少しわかりやすければ。。
でもようやくPySideの使い方が少しわかってきた気がする。
#参考一覧