【Maya Python】スクリプトの中身を噛み砕く2 ~list Notes編

#この記事の目的

自作ツールの中身を一つ一つ確認しながら理解を深め、今後のツール開発に役立てます。
今までなんとなくコピペで動いてたからいいやとスルーしてきたことを徹底理解します。
同じようにスクリプトを書いていてなんとなく動いてるからいっか、で終わってる人への一助になればと思います。
今回はPyQt、QTreeView、QStandardItemModelがメインです。

#ツール概要

シーン内のNotesを一覧を表示します。
各ノードにはNotesというアトリビュートが備わっています。AttributeEditorの一番下にこの機能がありますが使っていますか？
ここにメモを残すことでこのノードが何をやっているか把握しやすくなります。僕はMayaファイル内に各ノードのメモを残すときによく使っています。

しかし、すべてのNotesを一覧表示する方法が見つからなかったためツールにしました。

![listNotes.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507736/142ca130-7935-9329-4a79-e54431c39348.gif)
GIF画像のようにシーン内のNotesが残されているノードを一括表示します。
ノードの選択や、Notesの編集が可能です。

**動作環境：Maya2019.3,Maya2020.1**
Pyside2を使用しているので2016以前では動作しません。

#コード全文

```python
# -*- 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()
```

#ツールの設計図

![listNotes.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507736/51a96084-2e8b-dc01-3272-061fdcaeb4ba.jpeg)

おおよそこのような感じ。
ツールの動作を把握するためにも設計図を描くとわかりやすい。

#詳細を見ていく
以前書いた部分に関しては下記参照。新しく出てきた項目について書いて行きます。
[【Maya Python】スクリプトの中身を噛み砕く1 ~カメラスピードエディタ編](https://qiita.com/elloneil/items/56591ab00c2a9b66ce2a)

##ツリービューの見た目設定（QTreeView）
```python
		# ツリービュー追加
		treeView = TreeView()
		layout.addWidget(treeView, 0, 0, 1, -1)
```
`TreeView()`クラスをインスタンスし、グリッドレイアウトに追加します。
ちなみにクラス名は頭文字大文字が[PEP8](https://qiita.com/simonritchie/items/bb06a7521ae6560738a7#%E3%82%AF%E3%83%A9%E3%82%B9%E5%90%8D)で推奨されています。僕はわかりやすいように、クラス名は頭文字大文字、インスタンス名は頭文字小文字にして区別しています。

少し飛びますが先に`TreeView`クラスを見ましょう。

```python
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`を使用するとエクセルのような飛び石選択ができるようになります。
>[QtのSelectionModeのメモ](http://see-ku.com/wiki/wiki.cgi?page=Qt%A4%CESelectionMode%A4%CE%A5%E1%A5%E2)

`self.setAlternatingRowColors(True)`
行の色を交互に変更します。
こういったツリービューは量が増えると見づらくなるのでゼブラ柄にして見やすくします。

![ListNotes_TreeView.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507736/86e87039-c3ff-d81d-0ce4-e9c2b1649bf3.jpeg)
このようにQTreeViewではツリービューの見た目や動作を設定します。


##ツリービューにアイテムを設定（StockItemModel、QItemSelectionModel）

```python
		# ツリービューにアイテム設定
		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）
```python
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）
```python
	# 編集したときの動作
	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クラスについての説明です。

```python
	# 指定の行から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）
アイテムを追加する機能を作っておきます。
このあとボタン等から呼び出します。

```python
	# アイテムを追加する
	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の強みですね。

##ボタンの作成
```python
		# セレクトボタン
		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 ~カメラスピードエディタ編](https://qiita.com/elloneil/items/56591ab00c2a9b66ce2a)と同じです。
次は、ボタンの中身を見ていきます。

###セレクトボタンの動作

```python
	# セレクトボタンの動作			
	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）](###指定の行からノード、アトリビュートを取得（rowData）)参照
`cmds.select(node, noExpand=True ,add=True)` 取得したノードを追加選択。
`noExpand=True`は選択セットのオブジェクトではなく、選択セットを選択できるようにします。
`add=True`追加選択モードに変更します。


###リフレッシュボタンの動作

```python
	# リフレッシュボタンの動作
	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）](###アイテムを追加する（appendItem）)を参照。

##初期読み込み
```python
		# 初期読み込み
		self.refresh()
```

最後に最初に立ち上げたときにNotesを取得します。
[refresh](###リフレッシュボタンの動作)の挙動と同じなのでそれを使いまわします。


#おわりに
今回はPyQtの記述が多かったですが、なかなかネットで情報収集が難しかったです。
公式のドキュメントの見方がもう少しわかりやすければ。。
でもようやくPySideの使い方が少しわかってきた気がする。


#参考一覧
- [【Maya Python】スクリプトの中身を噛み砕く1 ~カメラスピードエディタ編](https://qiita.com/elloneil/items/56591ab00c2a9b66ce2a)
- [QtのSelectionModeのメモ](http://see-ku.com/wiki/wiki.cgi?page=Qt%A4%CESelectionMode%A4%CE%A5%E1%A5%E2)
- [[Pythonコーディング規約]PEP8を読み解く-クラス名](https://qiita.com/simonritchie/items/bb06a7521ae6560738a7#%E3%82%AF%E3%83%A9%E3%82%B9%E5%90%8D)