Python
Qt
PySide
maya
pyside2

Qt.pyでPySideとPySide2を共通コードで使う

More than 1 year has passed since last update.

この記事はMaya-Python Advent Calendar 2017 12月4日の記事です。

普段それほどアウトプットもしてないので、今年もいくつか勉強がてら書いていこうと思います。

はじめに

Qtももう5.9までバージョンが進みました。
PythonバインディングはQt4系のPyQt,PySide、Qt5系のPyQt5,PySide2があります。

ちなみに弊社はPySideまたはPySide2で統一していますが、githubのプロジェクトを見渡すとビルド環境の関係からか、PySide2ではなくPyQt5を採用しているケースも見受けられます。

Qt4とQt5ではモジュール構成に変更がありました。
QtGuiから多くのウィジェットクラスが新設されたQtWidgetsに移動になってます。
http://doc.qt.io/qt-5/portingguide.html

Autodesk Mayaは2016以前はPySide、2017以降はPySide2となっています。
コードを書き分けるのは何としても回避せねばなりません。
さて、どうしたものか。

普通に書くとどうなるか

PySide

main.py
from PySide import QtCore, QtGui

import myapp.ui.mainwindow


class MainWindow(QtGui.QDialog, myapp.ui.mainwindow.Ui_Form):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setupUi(self)

PySide2

main.py
from PySide2 import QtCore, QtGui, QtWidgets

import myapp.ui.mainwindow


class MainWindow(QtWidgets.QDialog, myapp.ui.mainwindow.Ui_Form):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setupUi(self)

QDialogがQtGuiからQtWidgetsに移動してます。myapp.ui.mainwindowはuiファイルから変換して自動生成したpythonコードです。

PySideのコードでもQtWidgets経由に統一するために以下のようにエイリアスを定義すれば大分ごまかせますが、QtWidgetsに移動になっていないクラスもQtWidgets経由で呼べてしまうので気持ちが悪いです。

from PySide import QtGui as QtWidgets

Qt.pyを使おう

みんな困ってますので、誰かが何かを作っているはず。githubで探してみたらやはりありました。

Qt.py
https://github.com/mottosso/Qt.py

PyPIに上がってるので

python -m pip install qt.py

でインストールできます。virtualenv作って試してみましょう

PySide2がインストールされていればPySide2を使用し、無ければPySideを使用してQtモジュール経由でQtCoreQtGui,QtWidgets等を使用できます。Qt5準拠でクラスを整理整頓して返してくれます。

一時期私が修正したコードが消え去ってたりして、開発の安定性に疑問を感じてたのですが、最近またきれいに整ってきました。今なら強くお勧めできます。

このように書けます。スッキリしました。

main.py
from Qt import QtCore, QtGui, QtWidgets

import myapp.ui.mainwindow


class MainWindow(QtWidgets.QDialog, myapp.ui.mainwindow.Ui_Form):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setupUi(self)

どーしてもPySide2とPySideで処理を分けなければいけない場合は、__binding__にモジュール名が入っているのでそれで処理を分けることも可能です。

if qt.__binding__ == u"PySide2":
    pass

uiファイルから変換したコード

しかしまだ問題は残っています。

uiファイルはあらかじめuiコンパイラーでPythonコードに変換して使用しています。

ちなみにだいぶ昔からこの手法で開発してるのですが、最近「なんで?」と聞かれたんですが答えられませんでした。
確か、uiファイルを動的に読み込むスタイルだと動作が不安定だった?のと、Pythonコードになっていることで全体の挙動が確認しやすいから?だったと思います。
今の時代はエディターのコード補完の恩恵も絶大ですのでこのスタイルを変えることは無いと思います。

PySideのuiコンパイラーとPySide2のuiコンパイラーは当然ながらそれぞれのモジュール構成のpythonコードを出力します。

PySideのuiコンパイラーで生成したコード

mainwindow.py
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created: Wed Jul 05 17:43:52 2017
#      by: pyside-uic 0.2.15 running on PySide 1.2.4
#
# WARNING! All changes made in this file will be lost!

from PySide import QtCore, QtGui

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(251, 188)
...以下省略...

PySide2のuiコンパイラーで生成したコード

mainwindow.py
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created: Wed Jul 05 17:43:52 2017
#      by: pyside2-uic  running on PySide2 2.0.0~alpha0
#
# WARNING! All changes made in this file will be lost!

from PySide2 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(251, 188)
...以下省略...

Qt.pyにはPySideおよびPySide2コードをQt.pyを使用したコードに変換する機能も実験的に実装されていますが、あらゆるケースでうまくいくわけではありませんでした。

python -m Qt --convert ui\mainwindow.py

これは安定まで時間がかかりそうでしたので、早々に諦めました。

力業ですが、PySideとPySide2の両方のコードを自動生成しておくことで解決を図ることにしました。

まとめ

uiからの変換はPySide、PySide2両方で行う。
以下のように無駄にPySide、PySide2というパッケージで分けてます。大文字使ってるのもPEP8に反していますがここは判りやすさを優先して割り切ってます。

myapp/main.py
myapp/ui/__init__.py
myapp/ui/mainwindow.ui
myapp/ui/PySide/__init__.py
myapp/ui/PySide/mainwindow.py
myapp/ui/PySide2/__init__.py
myapp/ui/PySide2/mainwindow.py

自分のコードでは、PySideであれPySide2であれ、Qt.pyを使う。
uiのインポートはImportErrorで読み分ける。try,exceptでの読み分けはとてもジェネリックで判りやすいです。

main.py
from Qt import QtCore, QtGui, QtWidget

try
    import myapp.ui.PySide2.mainwindow as ui_mainwindow
except ImportError:
    import myapp.ui.PySide.mainwindow as ui_mainwindow


class MainWindow(QtWidgets.QDialog, ui_mainwindow.Ui_Form):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setupUi(self)

ということで、Qt.pyのおかげで個人的には満足いくコードの共通化ができています。

Qt.pyにもまだまだ改善の余地もあるのでどんどんプルリクエストを送りましょう。