【MayaPySide】ちょっとおしゃれなUIメソッド【2日目】

python,qt,PySide,maya,PySide2

こんにちはMayaPython Advent Calender 2017の7日目の記事です
全記事一覧です

【MayaPySide】ちょっとおしゃれなUIメソッド【1日目】
【MayaPySide】ちょっとおしゃれなUIメソッド【2日目】
【MayaPySide】ちょっとおしゃれなUIメソッド【3日目】

ちょっとおしゃれなUIメソッドの二日目です
今回は実は予定していなかったものなのですがフレームレスの需要が高まっている中
Windowのresizeや閉じるときのアニメーションの需要があるようなので今回記事にしました。

リサイズはこんな感じの簡単なものを想定していたのですが

ysさんが素敵なサンプルを用意してくださったので
それを利用して今回のUIを作成していきます
前回までのコードをベースに作っていきましょう

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

import maya.cmds as cmds
import pymel.core as pm
from maya import OpenMayaUI

from Qt.QtWidgets import *
from Qt.QtGui import *
from Qt.QtCore import *

try:
    import shiboken
except:
    import shiboken2 as shiboken

ptr = OpenMayaUI.MQtUtil.mainWindow()
parent = shiboken.wrapInstance(long(ptr), QWidget)


class setMainWindow(QMainWindow):
    def __init__(self):
        super(setMainWindow, self).__init__(parent)
    def mouseReleaseEvent(self, pos):
        self.mc_x = pos.x()
        self.mc_y = pos.y()

    def mousePressEvent(self, pos):
        self.mc_x = pos.x()
        self.mc_y = pos.y()

    def mouseMoveEvent(self, pos):
        winX = pos.globalX() - self.mc_x
        winY = pos.globalY() - self.mc_y
        self.move(winX, winY)

class Example(setMainWindow):
    def __init__(self):
        super(Example, self).__init__()
        self.initUI()
        self.setButton()
        self.paletteUI()

    def initUI(self):
        self.setWindowOpacity(0.85)
        self.setGeometry(300, 300, 200, 150)
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)

    def setButton(self):
        btn = QPushButton("Close", self)
        btn.move(50,50)
        btn.clicked.connect(self.close)

    def paletteUI(self):
        setColors = ['#54354e', '#6a86c7']
        
        Palette = QPalette()
        gradient = QLinearGradient(QRectF(
                self.rect()).topLeft(),
                QRectF(self.rect()).topRight()
            )
        gradient.setColorAt(0.0, setColors[0])
        gradient.setColorAt(1.0, setColors[1])
        Palette.setBrush(QPalette.Background, QBrush(gradient))
        self.setPalette(Palette)

        path = QPainterPath()
        path.addRoundedRect(self.rect(), 10, 10)
        region = QRegion(path.toFillPolygon().toPolygon())
        self.setMask(region)


def Example_UI():
    global Example_UI_ex
    app = QApplication.instance()
    Example_UI_ex = Example()
    Example_UI_ex.show()
    sys.exit()
    app.exec_()

Example_UI()

まず最初にクローズボタンのカスタマイズからしていきましょう
クローズボタンのカスタマイズは閉じる際にフェードアウトして消えていく、というアニメーションを付けます
使用するのはQTimerQPropertyAnimationです
QTimerというのは繰り返すとシングルショットタイマーを使用することができるものです
1000の単位で1秒となります。
では作っていきましょう。
まず最初にCloseButton専用のclassを作っていきます

class SysButton(QPushButton):
    closed = Signal()

    def __init__(self):
        super(SysButton, self).__init__()
        self.clicked.connect(self.__closed)

    def __closed(self):
        self.closed.emit()

PySideにはシグナルとスロットというものがあります。
シグナルclosedを作成し、Close専用のQPushButtonを用意しました。
次にWidgetを用意します

class MainWidget(QWidget):
    closed = Signal()
    def __init__(self):
        super(MainWidget, self).__init__()
        self.sysButton = SysButton()
        self.sysButton.setText('Close')
        self.sysButton.closed.connect(self.__close)
        self.mainLayout = QVBoxLayout(self)
        self.mainLayout.addWidget(self.sysButton)
        self.mainLayout.addStretch(True)

    def __close(self):
        self.closed.emit()

作成したWidgetを前回作ったsetButtonをsetWindget名前を変え、Widgetを埋め込むものに作り替えます
具体的にはclass Example(setMainWindow)CentralWidgetMainWidgetを埋め込みます
そしてgui_Close`という関数を作ります
内容はQTimerは0.6秒後にWindowをCloseさせ、QPropertyAnimationは0.6秒かけてWindowのOpacityを0にするという機能です
そしてその機能とSignalをコネクションします。

    def setWidget(self):
        mainWidget = MainWidget()
        mainWidget.closed.connect(self.gui_Close)
        self.setCentralWidget(mainWidget)

    def gui_Close(self):
        self.timer = QTimer()
        self.timer.setInterval(600)
        self.timer.timeout.connect(self.close)
        self.timer.start()
        self.fade = QPropertyAnimation(self, "windowOpacity")
        self.fade.setStartValue(0.85)
        self.fade.setEndValue(0.0)
        self.fade.setKeyValueAt(0.5, 0.0)
        self.fade.setEasingCurve(QEasingCurve.InOutCubic)
        self.fade.setDuration(600)
        self.fade.start()

次にWindowをリサイズする機能をsetMainWindowに追加していきます

class setMainWindow(QMainWindow):
    def __init__(self):
        super(setMainWindow, self).__init__(parent)
        self.installEventFilter(self)
        self.resize_mode = None

    def mouseReleaseEvent(self, pos):
        self.mc_x = pos.x()
        self.mc_y = pos.y()

    def mousePressEvent(self, pos):
        self.mc_x = pos.x()
        self.mc_y = pos.y()
        borderWidth = 8
        resizeWidth = self.minimumWidth() != self.maximumWidth()
        resizeHeight = self.minimumHeight() != self.maximumHeight()

        self.pre_size_x = self.size().width()
        self.pre_size_y = self.size().height()
        self.size_w = self.size().width()
        self.size_h = self.size().height()
        self.sub_w = self.size_w - self.mc_x
        self.sub_h = self.size_h - self.mc_y
        resize_mode = None

        if resizeWidth is True:
            if self.mc_x < borderWidth:
                resize_mode = "left"
            if self.sub_w < borderWidth:
                resize_mode = "right"

        if resizeHeight is True:
            if self.mc_y < borderWidth:
                resize_mode = "top"
                
            if self.sub_h < borderWidth:
                resize_mode = "bottom"

        if resizeWidth is True and resizeHeight is True:
            if self.mc_x <= borderWidth and self.mc_y <= borderWidth:
                resize_mode = 'top_left'
            if self.sub_w <= borderWidth and self.sub_h <= borderWidth:
                resize_mode = 'bottom_right'
            if self.sub_w <= borderWidth and self.mc_y <= borderWidth:
                resize_mode = 'top_right'
            if self.mc_x <= borderWidth and self.sub_h <= borderWidth:
                resize_mode = 'bottom_left'
            if self.mc_x >= borderWidth and self.mc_y >= borderWidth\
                and self.sub_w >= borderWidth\
                and self.sub_h >= borderWidth:
                resize_mode = 'center'
        
        self.resize_mode = resize_mode
        return self.resize_mode

        
    def eventFilter(self, obj, event):
        if event.type() == QEvent.Type.Enter:
            QApplication.setOverrideCursor(Qt.ArrowCursor)
        if event.type() == QEvent.Type.HoverMove:
            pos = event.pos()
            cur_dict = {
                'right': Qt.SizeHorCursor,
                'left': Qt.SizeHorCursor,
                'top': Qt.SizeVerCursor,
                'bottom': Qt.SizeVerCursor,
                'top_left': Qt.SizeFDiagCursor,
                'bottom_right': Qt.SizeFDiagCursor,
                'top_right': Qt.SizeBDiagCursor,
                'bottom_left': Qt.SizeBDiagCursor,
                'center': Qt.ArrowCursor,
                None: Qt.ArrowCursor
                }
            cur = cur_dict[self.resize_mode] 
            current_cur = QApplication.overrideCursor().shape()
            cur_set = cur_dict[self.resize_mode]
            if cur != current_cur:
                QApplication.changeOverrideCursor(cur_set)
    
        if event.type() == QEvent.Type.Leave:
            QApplication.restoreOverrideCursor()

    def mouseMoveEvent(self, pos):
        self.win_x = pos.globalX() - self.mc_x
        self.win_y = pos.globalY() - self.mc_y
        
        if self.resize_mode is None:
            self.resize_mode = 'center'
        
        if self.resize_mode is 'center':
            self.move(self.win_x, self.win_y)

        else:
            self.re_w = self.size().width()
            self.re_h = self.size().height()
            if 'right' in self.resize_mode:
                self.re_w = pos.x() + self.sub_w
                self.resize(self.re_w, self.re_h)

            if 'bottom' in self.resize_mode:
                self.re_h = pos.y() + self.sub_h
                self.resize(self.re_w, self.re_h)

            if 'left' in self.resize_mode:
                self.resub_w = pos.x() - self.mc_x
                self.re_w = self.re_w - self.resub_w
                self.resize(self.re_w, self.re_h)
                if self.size().width() != self.pre_size_x:
                    self.win_x = pos.globalX() - self.mc_x
                    self.move(self.win_x, self.pos().y())
                self.pre_size_x = self.size().width() 

            if 'top' in self.resize_mode:
                self.resub_h = pos.y() - self.mc_y
                self.re_h = self.re_h - self.resub_h
                self.resize(self.re_w, self.re_h)
                if self.size().height() != self.pre_size_y:
                    self.win_y = pos.globalY() - self.mc_y
                    self.move(self.pos().x(), self.win_y)
                self.pre_size_y = self.size().height()

def mousePressEvent(self, pos)には上下左右斜のそれぞれ端から8pxの位置にマウスを押した場合、どの位置にあるかを取得する機能を設定しています
そして、def mouseMoveEvent(self, pos)でどの位置にあるかによってresize処理をどう実行させるかを設定しています。
また、PySideはeventFilter関数をつかうことでeventを受け取れる仕組みがあります。
eventを受け取るボタンをinstallEventFilterという関数でeventに登録し、event関数である eventFilterを定義し、そのeventを監視することができます
def eventFilter(self, obj, event)はWindowをResizeする際にマウスの見た目を変更する処理を入れています
def eventFilter(self, obj, event)を登録するためにself.installEventFilter(self)を読み込んでいます。
しかし、このまま実行するとUIがおかしくなります
なぜかというとmaskのサイズが変更されていないからです
ですのでサイズを大きくすれば変に伸びて、形が切れていきます
これを防ぐためにdef paletteUI(self)をベースに作った、maskUIという新しいクラスを用意します

class maskUI(QMainWindow):
    def __init__(self):
        super(maskUI, self).__init__(parent)
    
    def PaletteUI(self):
        setColors = ['#54354e', '#6a86c7']
        Palette = QPalette()
        gradient = QLinearGradient(QRectF(
                self.rect()).topLeft(),
                QRectF(self.rect()).topRight()
            )
        gradient.setColorAt(0.0, setColors[0])
        gradient.setColorAt(1.0, setColors[1])
        Palette.setBrush(QPalette.Background, QBrush(gradient))

        path = QPainterPath()
        path.addRoundedRect(self.rect(), 3, 3)
        region = QRegion(path.toFillPolygon().toPolygon())

        self.setMask(region)
        self.setPalette(Palette)

class setMainWindow(maskUI):
    def __init__(self):
        super(setMainWindow, self).__init__()

内容は一緒です
そしてsetMainWindowの継承をmaskUIに変更します
コンストラクタはsetMainWindowでmayaをparentしているので空にしておきます。
そして、PaletteUIをmouseMoveEvent、Exmple、それぞれに読み込みます
内容は一緒です

def mouseMoveEvent(self, pos):
    self.PaletteUI()
# ~~~~~~~~~~~~~~~~~~~~~~~~
class Example(setMainWindow):
    def __init__(self):
        super(Example, self).__init__()
        self.initUI()
        self.setWidget()
        self.PaletteUI()

次回はUIのデザイン部分をしていきます。


追記

イベントをフィルタリングする場合Trueを返し、それ以外の場合はFalseを返さないといけません
その処理を入れ忘れていたため、# RuntimeWarning:が出ていました
そのため、def eventFilter(self, obj, event)の処理を以下に変更する必要があります

def eventFilter(self, obj, event):
    if event.type() == QEvent.Type.Enter:
        QApplication.setOverrideCursor(Qt.ArrowCursor)
        return True
    if event.type() == QEvent.Type.HoverMove:
        pos = event.pos()
        cur_dict = {
            'right': Qt.SizeHorCursor,
            'left': Qt.SizeHorCursor,
            'top': Qt.SizeVerCursor,
            'bottom': Qt.SizeVerCursor,
            'top_left': Qt.SizeFDiagCursor,
            'bottom_right': Qt.SizeFDiagCursor,
            'top_right': Qt.SizeBDiagCursor,
            'bottom_left': Qt.SizeBDiagCursor,
            'center': Qt.ArrowCursor,
            None: Qt.ArrowCursor
            }
        cur = cur_dict[self.resize_mode] 
        current_cur = QApplication.overrideCursor().shape()
        cur_set = cur_dict[self.resize_mode]
        if cur != current_cur:
            QApplication.changeOverrideCursor(cur_set)
            return True
        return True

    if event.type() == QEvent.Type.Leave:
        QApplication.restoreOverrideCursor()
        return True
    else:
        return False

【MayaPySide】ちょっとおしゃれなUIメソッド【3日目】の記事でUIの見た目を設定していきます


MayaPython Advent Calender 2017の8日目の記事はSEN_AさんのMaya Python API 2.0 を使ってCurveからclosestPointを取得するノードを作ろう!です