Maya PySide2 / PySide チュートリアル 初級編 ⑤ – データフォーマットを扱ってみる

python,qt,PySide,PySide2,Tutorial

このチュートリアルでは PySide でjsonやiniなどのデータフォーマットを利用し、UIの情報を書き換えたり、保存したりする方法を学びます

データフォーマットとは
複合的なデータを記録・伝送する際に、個々の要素の記法や長さ、順番など、データの記述方法を定めたものをデータフォーマット(データ形式)という。特に、ストレージに記録されるファイルの体裁を取るものはファイルフォーマット(ファイル形式)という。
引用 - IT用語辞典 e-Words - フォーマット 【format】

ゲーム業界ではいろいろなデータフォーマットを扱うことが多いかと思います。fbxをはじめ、pngやpsdなどこれらは画像データや2D情報を含んだデータですが、今回扱うデータフォーマットはPySideのUI情報を記録したデータフォーマットになります。
今回のチュートリアルではゲーム業界でもよく利用されるJSONFormat(以下json)とQtが提供しているIniFormat(以下ini)の二つを利用してみます
ザックリを解説していきます

{
     "key" : "value"
}

jsonはPythonでいうところのDictつまり、辞書型と呼ばれる形式でKeyとValueで管理します

[Section1]
key1=value1
key2=value2
key3=value3
[Section2]
key1=value1
key2=value2
key3=value3

iniはSection Key Valueで分類して、管理します

今回、jsonは規定の値を保持させ、iniは最後に閉じたときのUIの情報を保持するというような使い方をしてみます

まずはQtDeisgnerでpushButton_All05とpushButton_All1の二種類を用意します

20220605_01

ObjectNamesetText説明
pushButton_All05All 0.5RGBAの値をそれぞれ0.5にする
pushButton_All1All 1RGBAの値をそれぞれ1にする

metadata.jsonを用意します
C:\Users\<USERNAME>\Documents\maya\scripts\sample
metadetaというフォルダを作成し、その中にtextを作成し、metadata.jsonにリネームしてください
C:\Users\\Documents\maya\scripts\sample\ui\metadata.json
作成したmetadata.jsonにスクリプトエディタやテキストエディタなどで以下のように記述します

{
    "All 1": [
        1,
        1,
        1,
        1
    ],
    "All 0.5": [
        0.5,
        0.5,
        0.5,
        0.5
    ]
}

keyはQPushButtonに設定したテキストの文字列で
valueはリストの要素のインデックスがRGBAの順に対応しています

iniに関しては自動で生成されるので作成しなくても問題ありません

Code

今回のサンプルコードです

# !/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import json

import maya.cmds as cmds
import maya.OpenMayaUI as OpenMayaUI

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
    from PySide2.QtUiTools import QUiLoader
    import shiboken2 as shiboken
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *
    from PySide.QtUiTools import QUiLoader
    import shiboken


ptr = OpenMayaUI.MQtUtil.mainWindow()
mayaMainWindow = shiboken.wrapInstance(long(ptr), QMainWindow)


class vtxMainWindow(QMainWindow):
    __currentPath = os.path.dirname(__file__)
    __uiFilePath = os.path.join(__currentPath, "ui", "vtxMain.ui")
    __geometries = []
    __settingPath = os.path.join(__currentPath, "metadata", "setting.ini")
    __setting = QSettings(__settingPath, QSettings.IniFormat)
    __metaDataPath = os.path.join(__currentPath, "metadata", "metadata.json")
    with open(__metaDataPath) as f:
        __metadata = json.load(f)

    def __init__(self, parent=None):
        super(vtxMainWindow, self).__init__(parent)
        loader = QUiLoader()
        uiWidget = loader.load(self.__uiFilePath)
        self.setCentralWidget(uiWidget)
        self.resize(400, 270)
        self.setWindowTitle(".ui MainWindow")
        self.installEventFilter(self)
        self.centralWidget().listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.__initSignalSlot()

    def __initSignalSlot(self):
        self.centralWidget().button_Apply.clicked.connect(self.setVertexColor)
        self.centralWidget().listWidget.itemClicked.connect(self.selectItem)
        self.centralWidget().checkBox_IsSelect.stateChanged.connect(self.selectItem)
        self.centralWidget().pushButton_All05.clicked.connect(self.setValues)
        self.centralWidget().pushButton_All1.clicked.connect(self.setValues)

    def setValues(self):
        values = self.__metadata[self.sender().text()]
        self.centralWidget().doubleSpinBox_R.setValue(values[0])
        self.centralWidget().doubleSpinBox_G.setValue(values[1])
        self.centralWidget().doubleSpinBox_B.setValue(values[2])
        self.centralWidget().doubleSpinBox_A.setValue(values[3])

    def setVertexColor(self):
        current = cmds.ls(sl=True)
        meshList = []
        if self.centralWidget().checkBox_IsSelect.isChecked():
            for item in self.centralWidget().listWidget.selectedItems():
                meshList.append(item.text())
        else:
            meshList = cmds.ls(sl=True)

        for mesh in meshList:
            cmds.select(mesh)
            cmds.polyColorPerVertex(
                r=self.centralWidget().doubleSpinBox_R.value(),
                g=self.centralWidget().doubleSpinBox_G.value(),
                b=self.centralWidget().doubleSpinBox_B.value(),
                a=self.centralWidget().doubleSpinBox_A.value()
            )
            cmds.setAttr("%s.displayColors" % mesh, 1)

        cmds.select(current)

    def selectItem(self):
        if self.centralWidget().checkBox_IsSelect.isChecked():
            cmds.select(cl=True)
            for item in self.centralWidget().listWidget.selectedItems():
                cmds.select(item.text(), add=True)

    def eventFilter(self, object, event):
        if event.type() == QEvent.WindowActivate \
                or event.type() == QEvent.FocusOut:
            self.widgetReload()
        else:
            pass
        super(vtxMainWindow, self).eventFilter(object, event)

    def widgetReload(self):
        self.centralWidget().listWidget.clear()
        for count, geometry in enumerate(self.geometries):
            self.centralWidget().listWidget.insertItem(count, geometry)

    @property
    def geometries(self):
        self.__geometries = []
        meshlist = cmds.ls(type="mesh")
        for mesh in meshlist:
            self.__geometries.append(cmds.listRelatives(mesh, fullPath=True, parent=True, type="transform")[0])
        return self.__geometries

    def closeEvent(self, event):
        self.saveSettings()
        super(vtxMainWindow, self).closeEvent(event)

    def showEvent(self, event):
        self.loadSettings()
        super(vtxMainWindow, self).showEvent(event)

    def loadSettings(self):
        widget = None
        for i in dir(self.centralWidget()):
            try:
                exec("widget = self.centralWidget().%s" % i)
                widgetType = type(widget)
                objectName = widget.objectName()
                value = self.__setting.value(objectName)
                if widgetType == QDoubleSpinBox:
                    widget.setValue(value)
                elif widgetType == QCheckBox:
                    widget.setChecked(value)
            except BaseException:
                pass
        self.restoreGeometry(self.__setting.value("geometry"))

    def saveSettings(self):
        widget = None
        for i in dir(self.centralWidget()):
            try:
                exec("widget = self.centralWidget().%s" % i)
                widgetType = type(widget)
                objectName = widget.objectName()
                if widgetType == QDoubleSpinBox:
                    self.__setting.setValue(objectName, widget.value())
                elif widgetType == QCheckBox:
                    self.__setting.setValue(objectName, widget.isChecked())
            except BaseException:
                pass
        self.__setting.setValue("geometry", self.saveGeometry())


def main():
    app = QApplication.instance()
    mainWin = vtxMainWindow(parent=mayaMainWindow)
    mainWin.show()
    sys.exit()
    app.exec_()

まずはjsonとiniのパスや読み込み先をクラスに定義します

# !/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import json

import maya.cmds as cmds
# ~~~ 略

mayaMainWindow = shiboken.wrapInstance(long(ptr), QMainWindow)


class vtxMainWindow(QMainWindow):
    __currentPath = os.path.dirname(__file__)
    __uiFilePath = os.path.join(__currentPath, "ui", "vtxMain.ui")
    __geometries = []
    __metaDataPath = os.path.join(__currentPath, "metadata", "metadata.json")
    with open(__metaDataPath) as f:
        __metadata = json.load(f)

jsonを読み込むにはjsonモジュールを利用するのがやりやすいのでimportでjsonを呼び出し
metadata.jsonを読み込んでおきます


    def __initSignalSlot(self):
        self.centralWidget().pushButton_All05.clicked.connect(self.setValues)
        self.centralWidget().pushButton_All1.clicked.connect(self.setValues)

    def setValues(self):
        values = self.__metadata[self.sender().text()]
        self.centralWidget().doubleSpinBox_R.setValue(values[0])
        self.centralWidget().doubleSpinBox_G.setValue(values[1])
        self.centralWidget().doubleSpinBox_B.setValue(values[2])
        self.centralWidget().doubleSpinBox_A.setValue(values[3])

シグナル&スロットでそれぞれのQPushButtonを押した際にQDubleSpinBoxに値が入るように設定します
self.sender()は何らかのシグナルでスロットが呼び出された際に、シグナルを送信したオブジェクトへのポインタを返すもので、ザックリ話すとclickedを押した際にどのWidgetが押されたかがわかる便利なものです
senderで.text()を取得することでここではQPushButton.text()を呼んでいると同じ事をしています
先ほどのjsonにはkeyにPushButtonのテキストを設定していたので、自動的に対応するvalueをインデックス番号で設定しています

20220605_1


class vtxMainWindow(QMainWindow):
    __currentPath = os.path.dirname(__file__)
    __uiFilePath = os.path.join(__currentPath, "ui", "vtxMain.ui")
    __geometries = []
    __settingPath = os.path.join(__currentPath, "metadata", "setting.ini")
    __setting = QSettings(__settingPath, QSettings.IniFormat)
    __metaDataPath = os.path.join(__currentPath, "metadata", "metadata.json")
    with open(__metaDataPath) as f:
        __metadata = json.load(f)

setting.iniの保存先とQSettingsを設定します
QSettings.IniFormatを引数に指定することで保存する際に自動的にIniFormatで保存してくれます
他にもレジストリに書き込むタイプなども指定できますがスタンドアロンでソフトウェアを作成したりしないのであればIniFormatが無難かと思います

    def closeEvent(self, event):
        self.saveSettings()
        super(vtxMainWindow, self).closeEvent(event)

    def saveSettings(self):
        widget = None
        for i in dir(self.centralWidget()):
            try:
                exec("widget = self.centralWidget().%s" % i)
                widgetType = type(widget)
                objectName = widget.objectName()
                if widgetType == QDoubleSpinBox:
                    self.__setting.setValue(objectName, widget.value())
                elif widgetType == QCheckBox:
                    self.__setting.setValue(objectName, widget.isChecked())
            except BaseException:
                pass
        self.__setting.setValue("geometry", self.saveGeometry())

Windowを閉じる際に設定を保存するようにcloseEventにself.saveSettings()呼び出すようにします
saveSettingsではcentralWidgetに設定されているWidgetで値を保持したいものだけを条件分岐し、
vtxMainWindowのgeometry情報だけ最後に保存できるように設定します
条件分岐はwidgetTypeでQDoubleSpinBoxとQCheckBoxの値を保存するようにしています

    def showEvent(self, event):
        self.loadSettings()
        super(vtxMainWindow, self).showEvent(event)

    def loadSettings(self):
        widget = None
        for i in dir(self.centralWidget()):
            try:
                exec("widget = self.centralWidget().%s" % i)
                widgetType = type(widget)
                objectName = widget.objectName()
                value = self.__setting.value(objectName)
                if widgetType == QDoubleSpinBox:
                    widget.setValue(value)
                elif widgetType == QCheckBox:
                    widget.setChecked(value)
            except BaseException:
                pass
        self.restoreGeometry(self.__setting.value("geometry"))

設定を読み込むloadSettingsも同じく
Windowを表示する際に設定を読み込むようにshowEventにself.loadSettings()呼び出すようにします

20220605_2


Maya PySide2 / PySide チュートリアルのこのパートでは、データフォーマットを扱う方法を学びました

前はMaya PySide2 / PySide チュートリアル 初級編 ④ – .uiとPythonのコーディングを組み合わせてみる


※おまけ
フォーマットは色々な形式で扱うことが多く、結局はテキストデータなので拡張子を変更して、プロジェクト独自や会社独自の形式で扱うことがあったりします
metaデータといいつつ実はjsonでしかなかったり、
yamlといった形式を扱うこともあるでしょうし、場合によってはfbxのデータの中身をいじったりすることもあるかもしれません