PySide

【PySide】使いやすいフラットUI のダイアログボックスを考える

使いやすいダイアログボックスが欲しかったので
記事を書きながら作ってみたいと思います。

下記のボタンで起動するダイアログボックスのコードを書いていきます。

コードは上記のページで作ったものを流用しています。
そのため スタンドアロン・Maya・Houdini・Blender に対応しています。
一番下の呼び出しのコメント文を切り替えることで各環境 に対応します。

今のところ QSS を当てたプッシュボタンを追加しただけなので
クリックしても何も起こりません。

ダイアログの基本設計

こんな感じのダイアログボックスを作りました。
クリックすると再生されます。

OK をクリックすれば 1、キャンセルを押せば 0 が返ってきています。
2値を返すダイアログボックスとしての最低限の機能をつけました。

dialog = Dialog()
dialog.setUI()
response = dialog.exec_()

上記のコードでダイアログボックスが立ち上がります。
ボタンのスロットとして記載しています。

メインのウィンドウと合わせた全文のコードが下記です。
一番下のコメント部分の環境さえ変えればコピペで起動するはずです。

ボタンがひとつのダイアログボックスを作る

設定を変えることで「OK」ボタンのみをもつダイアログボックスを作ります。

確認だけでキャンセルが必要ないときに出すタイプのダイアログボックスです。

ダイアログボックスを呼び出す際に
設定を変更することで切り替えられるようにしました。

self.two_Btns = True

69行目にインスタンス変数を追加しました。

# ボタンを二つ配置する
if self.two_Btns:
    self.btn_space = QtWidgets.QLabel()
    self.btn_space.setFixedWidth(10)
    self.btn_Area.addWidget(self.btn_space)

    # Reject ボタン
    # ----------------------------------------------------------------------------------------------------------
    self.reject_Btn = QtWidgets.QPushButton(self.reject_Btn_text)
    self.btn_style(self.reject_Btn, self.reject_Btn_color)
    self.reject_Btn.clicked.connect(self.return_reject)
    self.btn_Area.addWidget(self.reject_Btn)

125行目で追加したインスタンス変数で分岐させています。

def btn_clicked(self):
    dialog = Dialog()
    dialog.two_Btns = False
    dialog.setUI()
    response = dialog.exec_()
    print(response)

167行目でボタンを一つにする設定を行っています。
dialog.two_btns = Falseの部分です。
この一文を消すか、True にするとボタン二つのダイアログボックスで立ち上がります。

表示内容を自由に設定できるようにする

事前にウィジェットを追加できるレイアウトを用意しておき、
ダイアログを呼び出す際に後からウィジェットを追加できるようにしておきます。

下記のように起動時にラベルを追加しています。

def btn_clicked(self):
    dialog = Dialog()
    dialog.two_Btns = False

    lbl = QtWidgets.QLabel('後からここにウィジェットを追加できます')
    lbl.setFont(QtGui.QFont('游ゴシック'))
    lbl.setAlignment(Qt.AlignCenter)
    lbl.setStyleSheet(
        '''
        font-size:20px;
        color:#cccccc;
        font-weight:bold;
        '''
    )
    dialog.content_Area.addWidget(lbl)

    dialog.setUI()
    response = dialog.exec_()
    print(response)

ダイアログを呼び出す際にcontent_Areaにウィジェットを追加することで
そのウィジェットが設置された状態でダイアログボックスが立ち上がります。

self.root_Area.addStretch()

self.root_Area.addLayout(self.content_Area)

self.root_Area.addStretch()

76行目で self.content_Areaを設置しています。
ここにウィジェットが入ることになります。

入力型のダイアログボックスにする

下記のような感じの入力型のダイアログボックスも作れます。

今回は下記のコードで呼び出しています。

def btn_clicked(self):

    dialog = Dialog()
    dialog.two_Btns = False
    dialog.height = 250

    input_Area = QtWidgets.QHBoxLayout()
    dialog.content_Area.addLayout(input_Area)


    # ラベル
    input_Lbl = QtWidgets.QLabel('入力:')
    input_Lbl.setFont(QtGui.QFont('游ゴシック'))
    input_Lbl.setStyleSheet(
        '''
        font-size:20px;
        color:#cccccc;
        font-weight:bold;
        margin-left:30px;
        '''
    )
    input_Area.addWidget(input_Lbl)



    # ラインエディット
    self.input_Line = QtWidgets.QLineEdit('入力欄')
    self.input_Line.setFixedHeight(40)
    self.input_Line.setFont(QtGui.QFont('游ゴシック'))
    self.input_Line.setStyleSheet(
        '''
        font-size:20px;
        color:#cccccc;
        font-weight:bold;
        background-color:#2c2c2c;
        border: 2px solid #4d4d4d;
        padding-left: 15px;
        border-radius:20px;
        '''
    )
    input_Area.addWidget(self.input_Line)

    dialog.setUI()

    # ダイアログボックスの起動
    response = dialog.exec_()

    # 値が返ってきたときの処理
    # --------------------------------------------------------------------------------------------------------------
    if response:
        print(self.input_Line.text())

基本的には先ほどと同じような書き方をしています。

ポイントは最後の部分でしょうか。

    # 値が返ってきたときの処理
    # --------------------------------------------------------------------------------------------------------------
    if response:
        print(self.input_Line.text())

自身で足したウィジェットでもダイアログから返ってきた後で読みにいけます。

最終的なコード

アイコンを入れられるようにする

注意や警告などを促すアイコンを入れたいことがあるので、
呼び出す際に自由にアイコン画像を指定できるようにしました。

追加した変数が下記の4つです。

# アイコンの設定
self.icon_path = ''
self.icon_width  = 100
self.icon_height = 100

self.icon_space_size = 20

ダイアログに関しては 85行目で記述しています。

# アイコンを設置する場合
# --------------------------------------------------------------------------------------------------------------
if not self.icon_path == '':
    self.main_Area = QtWidgets.QHBoxLayout()
    self.root_Area.addLayout(self.main_Area)

    self.space = QtWidgets.QLabel()
    self.space.setFixedWidth(self.icon_space_size)
    self.main_Area.addWidget(self.space)

    # アイコンラベル
    self.icon_Area = QtWidgets.QVBoxLayout()
    self.main_Area.addLayout(self.icon_Area)

    self.icon_Lbl = QtWidgets.QLabel()
    self.icon_Lbl.setAlignment(Qt.AlignCenter)
    pixmap = QtGui.QPixmap(self.icon_path)
    pixmap = pixmap.scaled(self.icon_width, self.icon_height, QtCore.Qt.KeepAspectRatio)
    self.icon_Lbl.setPixmap(pixmap)
    self.icon_Area.addWidget(self.icon_Lbl)


    self.space = QtWidgets.QLabel()
    self.space.setFixedWidth(self.icon_space_size)
    self.main_Area.addWidget(self.space)

    # コンテンツエリア の設置
    self.main_Area.addLayout(self.content_Area)

# アイコンを設置しない場合
else:
    # コンテンツエリア の設置
    self.root_Area.addLayout(self.content_Area)

self.root_Area.addStretch()

パスが渡されていればアイコンを設置し、渡されていなければ設置せずにそのままコンテンツを入れます。

呼び出し文は 319行目から。

def btn_clicked(self):

    dialog = Dialog()
    dialog.two_Btns = True
    dialog.height = 300

    dialog.icon_path = 'D:\\icon\\caution.png'
    dialog.icon_width  = 150
    dialog.icon_height = 150

    dialog.icon_space_size = 20


    root_Area = QtWidgets.QVBoxLayout()
    dialog.content_Area.addLayout(root_Area)


    # ラベル
    text = '下記のシーンを開こうとしています。\n' \
           '保存されていないデータは破棄されます。'
    text_Lbl = QtWidgets.QLabel(text)
    text_Lbl.setFont(QtGui.QFont('游ゴシック'))
    text_Lbl.setStyleSheet(
        '''
        font-size:18px;
        color:#cccccc;
        font-weight:bold;
        '''
    )
    root_Area.addWidget(text_Lbl)


    self.space = QtWidgets.QLabel()
    self.space.setFixedHeight(10)
    root_Area.addWidget(self.space)


    # ラインエディット
    self.path_Line = QtWidgets.QLineEdit('D:\\scenes\\hoge.mb')
    self.path_Line.setReadOnly(True)
    self.path_Line.setFixedHeight(40)
    self.path_Line.setFont(QtGui.QFont('Arial'))
    self.path_Line.setStyleSheet(
        '''
        font-size:20px;
        color:#cccccc;
        background-color:#2c2c2c;
        border: 2px solid #4d4d4d;
        padding-left: 15px;
        border-radius:20px;
        '''
    )
    root_Area.addWidget(self.path_Line)

    dialog.setUI()

    # ダイアログボックスの起動
    response = dialog.exec_()

    # 値が返ってきたときの処理
    # --------------------------------------------------------------------------------------------------------------
    if response:
        print('実行する場合の処理')

スタンドアロンの exe化に対応させる

Maya などで使う分には上記のコードで問題ありませんが、
exe化して他の人に exeファイルを渡す場合は下記のようなコードになります。

詳しくは下記を参照してください。

こちらを設定しないと exe化した際にアイコンのパスが通りません。

# リソースパスへ変換
def resourcePath(filename):

    # exe化した際にリソースのパスを返す
    if hasattr(sys, "_MEIPASS"):
        return os.path.join(sys._MEIPASS, filename)

    # 通常時はそのままパスを返す
    else:
        return filename.replace('icon_path/', 'D:/icon/')

341行目のパスは下記のように記載します。

dialog.icon_path = resourcePath('icon_path/caution.png')

ダイアログの中でウィジェット・シグナル・スロットを追加する

これまでの呼び出し方では ダイアログ内にボタンやリストウィジェットを設置する
ことはできますが、シグナルやスロットを設定できません。

ダイアログ内で何か操作を追加したい場合は Diarog クラスを継承した上でそのメソッドを追加します。

ダイアログボックスの中でフォルダを開けるようにしました。
クリックで再生します。

継承して作成したダイアログボックスが下記です。

class InheritanceDialog(Dialog):

    def __init__(self):
        super(InheritanceDialog, self).__init__()

        self.two_Btns = True
        self.height = 300

        self.icon_path = resourcePath('icon_path/caution.png')
        self.icon_width = 150
        self.icon_height = 150

        self.icon_space_size = 20

        root_Area = QtWidgets.QVBoxLayout()
        self.content_Area.addLayout(root_Area)

        root_Area.addStretch()

        # ラベル
        text = '左のアイコンのパスは下記です。'

        text_Lbl = QtWidgets.QLabel(text)
        text_Lbl.setFont(QtGui.QFont('游ゴシック'))
        text_Lbl.setStyleSheet(
            '''
            font-size:18px;
            color:#cccccc;
            font-weight:bold;
            '''
        )
        root_Area.addWidget(text_Lbl)


        self.space = QtWidgets.QLabel()
        self.space.setFixedHeight(10)
        root_Area.addWidget(self.space)

        # ラインエディット
        self.path_Line = QtWidgets.QLineEdit(self.icon_path)
        self.path_Line.setReadOnly(True)
        self.path_Line.setFixedHeight(40)
        self.path_Line.setFont(QtGui.QFont('Arial'))
        self.path_Line.setStyleSheet(
            '''
            font-size:20px;
            color:#cccccc;
            background-color:#2c2c2c;
            border: 2px solid #4d4d4d;
            padding-left: 15px;
            border-radius:20px;
            '''
        )
        root_Area.addWidget(self.path_Line)

        self.space = QtWidgets.QLabel()
        self.space.setFixedHeight(10)
        root_Area.addWidget(self.space)


        # フォルダを開くボタン
        self.open_btn = QtWidgets.QPushButton('フォルダを開く')
        self.open_btn.setFont(QtGui.QFont('遊ゴシック'))
        self.open_btn.setFixedSize(200, 40)
        self.open_btn.setStyleSheet(
            '''
            QPushButton {
                font-size:18px;
                font-weight:bold;
                color:#cccccc;
                border:2px solid #818181;
                border-radius:20px;
            }
            QPushButton:hover {
                background-color:#1E73BE;
                color:#fff;
                border:2px solid #1E73BE;
            }
            QPushButton:pressed {
                border:6px solid #3a3a3a;
            }
            '''
        )
        self.open_btn.clicked.connect(self.btn_clicked)
        root_Area.addWidget(self.open_btn)

        root_Area.addStretch()

    def btn_clicked(self):

        directory_path = os.path.split(self.icon_path)[0].replace('/', '\\')

        import subprocess
        if os.path.isdir(directory_path):
            subprocess.Popen(["explorer", directory_path + '\\'], shell=True)

継承してダイアログボックスを作ったため、その分呼び出しがシンプルになっています。

    def btn_clicked(self):

        dialog = InheritanceDialog()
        dialog.setUI()
        response = dialog.exec_()

        # 値が返ってきたときの処理
        # --------------------------------------------------------------------------------------------------------------
        if response:
            print('実行する場合の処理')

アイコンのパス

スタンドアロン対応に伴いやや複雑になっています。

self.icon_path = resourcePath('icon_path/caution.png')

流用される場合は 286行目の上記のパスと22行目の下記のパスを適当なパスに変更してください。

return filename.replace('icon_path/', 'D:/icon/')

-PySide