【MayaPySide】ちょっとおしゃれなUIメソッド【2日目】
こんにちはMayaPython Advent Calendar 2017の7日目の記事です
全記事一覧です
【MayaPySide】ちょっとおしゃれなUIメソッド【1日目】
【MayaPySide】ちょっとおしゃれなUIメソッド【2日目】
【MayaPySide】ちょっとおしゃれなUIメソッド【3日目】
ちょっとおしゃれなUIメソッドの二日目です
今回は実は予定していなかったものなのですがフレームレスの需要が高まっている中
Windowのresize
や閉じるときのアニメーションの需要があるようなので今回記事にしました。
改良した
最小のサイズの設定とマスクの適応 pic.twitter.com/HtjoozAVIj— リンゴ酸@ryota unzai (@unpyside) December 5, 2017
リサイズはこんな感じの簡単なものを想定していたのですが
オシャレウィジェットをリサイズできるサンプルコードを置いときますね。https://t.co/0uMR4BJDjz
OceanTeaBaseMixinクラスをつかうとそのままでリサイズできるようになります。
おシャンティを継承しましょう(´ω`) pic.twitter.com/l3v6H6zZc2— ys (@blindys45) December 5, 2017
ysさんが素敵なサンプルを用意してくださったのでそれを利用して今回のUIを作成していきます。
Python3の対応によりここではysさんのサンプルを使用しない例に変わっています。
前回までのコードをベースに作っていきましょう。
from typing import Any
from maya import OpenMayaUI
from PySide2 import QtCore, QtGui, QtWidgets
from shiboken2 import wrapInstance
class FramelessMainWindow(QtWidgets.QMainWindow):
radius: int = 15
backgroundColorCode: str = "#333333"
backgroundColor = QtGui.QColor(backgroundColorCode)
gradient: QtGui.QLinearGradient = QtGui.QLinearGradient()
gradient.setColorAt(0.0, "#54354e")
gradient.setColorAt(1.0, "#6a86c7")
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
super().__init__(parent)
self.setAutoFillBackground(True)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
def paintEvent(self, event: QtGui.QMouseEvent) -> Any:
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
path = QtGui.QPainterPath()
self.gradient.setStart(QtCore.QRectF(self.rect()).topLeft())
self.gradient.setFinalStop(QtCore. QRectF(self.rect()).topRight())
brush = QtGui.QBrush(self.gradient)
painter.setBrush(brush)
path.addRoundedRect(
0, 0, self.width(), self.height(),
self.radius, self.radius
)
painter.setClipPath(path)
painter.fillPath(path, painter.brush())
return super().paintEvent(event)
class Example(FramelessMainWindow):
pos: QtCore.QPoint
btn: QtWidgets.QPushButton
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
super().__init__(parent)
self.__initUI()
self.setButton()
def __initUI(self) -> None:
self.btn = QtWidgets.QPushButton("Close", self)
self.setWindowOpacity(0.85)
self.setGeometry(300, 300, 500, 350)
self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint)
def setButton(self) -> None:
self.btn.move(50, 50)
self.btn.clicked.connect(self.close)
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
self.pos = event.pos()
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
self.pos = event.pos()
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
winX = event.globalX() - self.pos.x()
winY = event.globalY() - self.pos.y()
self.move(winX, winY)
def main() -> None:
mayaMainWindow = wrapInstance(int(OpenMayaUI.MQtUtil.mainWindow()), QtWidgets.QMainWindow)
ex = Example(mayaMainWindow)
ex.show()
main()
まず最初にクローズボタンのカスタマイズからしていきましょう。クローズボタンのカスタマイズは閉じる際にフェードアウトして消えていく、というアニメーションを付けます。使用するのはQtCore.QTimer
、QtCore.QPropertyAnimation
です。QtCore.QTimer
というのは繰り返すとシングルショットタイマーを使用することができるものです。1000の単位で1秒となります。
では作っていきましょう。
まず最初にCloseButton専用のclassを作っていきます。
class ClosePushButton(QtWidgets.QPushButton):
closed: QtCore.Signal = QtCore.Signal()
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
super().__init__(parent)
self.clicked.connect(self._closed)
def _closed(self) -> None:
self.closed.emit()
PySideにはシグナルとスロットというものがあります。
シグナルclosedを作成し、Close専用のQPushButtonを用意しました。
このclosePushButtonはクリックされたタイミングでcloseが実行されます。(設定(Slot)したファンクションが実行(Emit)されます)
このclosed専用のコードを定義することは実際はあまり意味がありませんがシグナルとスロットの扱いの参考にしてください。
次にclosePushButtonを表示するためのウィジェットを用意します。このウィジェットは後ほど、MainWindowに配置します。
class ExampleCentralWidget(QtWidgets.QWidget):
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
super().__init__(parent)
self.closePushButton = ClosePushButton(self)
self.closePushButton.setText("Close")
self.mainLayout = QtWidgets.QVBoxLayout(self)
self.mainLayout.addWidget(self.closePushButton)
self.mainLayout.addStretch(True)
self.setLayout(self.mainLayout)
先ほど作ったClosePushButton
をインスタンス化して呼び出します。
フェードアウトしながらWindowを閉じる機能を実装をこのウィジェットに適応させていきましょう。
元々あるclose関数をオーバーライドします。フェードアウトの機能はQTimer
とQPropertyAnimation
を使用し、時間をかけてWindowのOpacityを0にしていき、0になったタイミングでWindowをCloseさせます。
そしてその機能をclosePushButtonのCloseとコネクトさせます
元々用意していたbtnは使用しないのでコードから削除します。わかりやすいようにコメントアウトしておきます。
class Example(FramelessMainWindow):
# btn: QtWidgets.QPushButton
timer: QtCore.QTimer = QtCore.QTimer()
fade: QtCore.QPropertyAnimation
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
super().__init__(parent)
self.__initUI()
# self.setButton()
self.setWidget()
def __initUI(self) -> None:
# self.btn = QtWidgets.QPushButton("Close", self)
self.setWindowOpacity(0.85)
self.setGeometry(300, 300, 500, 350)
self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint)
# def setButton(self) -> None:
# self.btn.move(50, 50)
# self.btn.clicked.connect(self.close)
def setWidget(self) -> None:
self.setCentralWidget(ExampleCentralWidget(self))
self.centralWidget().ClosePushButton.closed.connect(self.close)
def close(self) -> None:
self.timer.setInterval(600)
self.timer.timeout.connect(super().close)
self.timer.start()
self.fade = QtCore.QPropertyAnimation(self, b"windowOpacity")
self.fade.setStartValue(0.85)
self.fade.setEndValue(0.0)
self.fade.setKeyValueAt(0.5, 0.0)
self.fade.setEasingCurve(QtCore.QEasingCurve.InOutCubic)
self.fade.setDuration(600)
self.fade.start()
次にWindowを移動させるする機能をFramelessMainWindow
に追加していきます
class FramelessMainWindow(QtWidgets.QMainWindow):
...
isDrag: bool = False
isPress: bool = False
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
self.installEventFilter(self)
...
def paintEvent(self, event: QtGui.QMouseEvent) -> Any:
...
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
self.isPress = False
self.pressPos = event.pos()
super().mouseReleaseEvent(event)
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
self.isPress = True
self.pressPos = event.pos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
if self.isPress or self.isDrag:
self.move(
event.globalX() - self.pressPos.x(), event.globalY() - self.pressPos.y()
)
super().mouseMoveEvent(event)
def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> Any:
if event.type() == QtCore.QEvent.Type.MouseButtonPress:
self.isDrag = True
return super().eventFilter(obj, event)
if event.type() == QtCore.QEvent.Type.MouseButtonRelease:
self.isDrag = False
return super().eventFilter(obj, event)
installEventFilter
でeventFilter
を有効します。mousePressEvent
とmouseReleaseEvent
ではマウスが押されているのかそうでないかをself.isPress
に定義し、eventFilter
ではMouseの位置情報やイベント情報を元に処理、windowを移動するイベントの際はmouseMoveEvent
に処理を書いています。
次に、リサイズを定義していきます。自前でリサイズ計算を定義してもよいのですがQtではQtWidgets.QSizeGrip
と呼ばれるトップレベルのウィンドウのサイズを変更するためのウィジェットが用意されているのでこちらを使用しましょう。
こちらは若干複雑ですがざっくり、四隅、つまり、topLeftやbottomRightなどの角はQtWidgets.QSizeGrip
を使用します。そして、四辺、つまりTopやRightなどの辺に関してはQtWidgets.QWidget
を継承したCornerGrip
というクラスを使用していきます
まず、CornerGripを定義します
class CornerGrip(QtWidgets.QWidget):
cursor_factory = {
QtCore.Qt.LeftEdge: QtCore.Qt.SizeHorCursor,
QtCore.Qt.TopEdge: QtCore.Qt.SizeVerCursor,
QtCore.Qt.RightEdge: QtCore.Qt.SizeHorCursor,
QtCore.Qt.BottomEdge: QtCore.Qt.SizeVerCursor,
}
resizeFunc: Any
mousePosition: QtCore.QPoint = None
def __init__(
self, parent: QtWidgets.QWidget = None, edge: QtCore.Qt.SizeAllCursor = None
):
super().__init__(parent)
cursor = self.cursor_factory.get(edge)
self.setCursor(cursor)
self.setResizeFunction(edge)
def setResizeFunction(self, edge) -> None:
if edge == QtCore.Qt.LeftEdge:
self.resizeFunc = self.resizeLeft
elif edge == QtCore.Qt.TopEdge:
self.resizeFunc = self.resizeTop
elif edge == QtCore.Qt.RightEdge:
self.resizeFunc = self.resizeRight
elif edge == QtCore.Qt.BottomEdge:
self.resizeFunc = self.resizeBottom
@cached_property
def window(self) -> QtWidgets.QWidget:
return super().window()
def resizeLeft(self, delta):
width = max(self.window.minimumWidth(), self.window.width() - delta.x())
geo = self.window.geometry()
geo.setLeft(geo.right() - width)
self.window.setGeometry(geo)
def resizeTop(self, delta):
height = max(self.window.minimumHeight(), self.window.height() - delta.y())
geo = self.window.geometry()
geo.setTop(geo.bottom() - height)
self.window.setGeometry(geo)
def resizeRight(self, delta):
width = max(self.window.minimumWidth(), self.window.width() + delta.x())
self.window.resize(width, self.window.height())
def resizeBottom(self, delta):
height = max(self.window.minimumHeight(), self.window.height() + delta.y())
self.window.resize(self.window.width(), height)
def mousePressEvent(self, event: QtGui.QMouseEvent) -> Any:
if event.button() == QtCore.Qt.LeftButton:
self.mousePosition = event.pos()
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> Any:
if self.mousePosition:
delta = event.pos() - self.mousePosition
self.resizeFunc(delta)
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> Any:
self.mousePosition = None
CornerGripはedgeを定義することで定義したedgeに合わせて、対応したリサイズの関数を呼び出すように定義しています。
そして、四隅と四辺のGripを持ったFrameGrips
を定義します
class FrameGrips:
topLeft: QtWidgets.QSizeGrip
topRight: QtWidgets.QSizeGrip
bottomRight: QtWidgets.QSizeGrip
buttomLeft: QtWidgets.QSizeGrip
left: CornerGrip
top: CornerGrip
right: CornerGrip
bottom: CornerGrip
parent: QtWidgets.QWidget
__qss: str = "background-color: transparent;"
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
self.parent = parent
self.topLeft = QtWidgets.QSizeGrip(parent)
self.topRight = QtWidgets.QSizeGrip(parent)
self.bottomRight = QtWidgets.QSizeGrip(parent)
self.buttomLeft = QtWidgets.QSizeGrip(parent)
self.left = CornerGrip(parent, edge=QtCore.Qt.LeftEdge)
self.top = CornerGrip(parent, edge=QtCore.Qt.TopEdge)
self.right = CornerGrip(parent, edge=QtCore.Qt.RightEdge)
self.bottom = CornerGrip(parent, edge=QtCore.Qt.BottomEdge)
self.topLeft.setStyleSheet(self.__qss)
self.topRight.setStyleSheet(self.__qss)
self.bottomRight.setStyleSheet(self.__qss)
self.buttomLeft.setStyleSheet(self.__qss)
self.left.setStyleSheet(self.__qss)
self.top.setStyleSheet(self.__qss)
self.right.setStyleSheet(self.__qss)
self.bottom.setStyleSheet(self.__qss)
それぞれのウィジェットは透明になるように定義しておきました。
そして、FramelessMainWindow内にFrameGrips
を呼び出し、リサイズできるように定義します。
class FramelessMainWindow(QtWidgets.QMainWindow):
pos: QtCore.QPoint
radius: int = 15
backgroundColorCode: str = "#333333"
backgroundColor = QtGui.QColor(backgroundColorCode)
gradient: QtGui.QLinearGradient = QtGui.QLinearGradient()
gradient.setColorAt(0.0, "#54354e")
gradient.setColorAt(1.0, "#6a86c7")
isDrag: bool = False
isPress: bool = False
cornerLength = 10
frameGrips: FrameGrips
def __init__(self, parent: QtWidgets.QWidget = None) -> None:
super().__init__(parent)
self.setAutoFillBackground(True)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.installEventFilter(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.frameGrips = FrameGrips(self)
self.setContentsMargins(*[self.gripSize] * 4)
@property
def gripSize(self):
return self.cornerLength
def setGripSize(self, size):
if size == self.cornerLength:
return
self.cornerLength = max(2, size)
self.setContentsMargins(*[self.gripSize] * 4)
self.updateGrips()
def updateGrips(self) -> None:
outRect = self.rect()
# an "inner" rect used for reference to set the geometries of size grips
inRect = outRect.adjusted(
self.gripSize, self.gripSize, -self.gripSize, -self.gripSize
)
# top left
self.frameGrips.topLeft.setGeometry(
QtCore.QRect(outRect.topLeft(), inRect.topLeft())
)
# top right
self.frameGrips.topRight.setGeometry(
QtCore.QRect(outRect.topRight(), inRect.topRight()).normalized()
)
# bottom right
self.frameGrips.bottomRight.setGeometry(
QtCore.QRect(inRect.bottomRight(), outRect.bottomRight())
)
# bottom left
self.frameGrips.buttomLeft.setGeometry(
QtCore.QRect(outRect.bottomLeft(), inRect.bottomLeft()).normalized()
)
# left edge
self.frameGrips.left.setGeometry(
0, inRect.top(), self.gripSize, inRect.height()
)
# top edge
self.frameGrips.top.setGeometry(inRect.left(), 0, inRect.width(), self.gripSize)
# right edge
self.frameGrips.right.setGeometry(
inRect.left() + inRect.width(), inRect.top(), self.gripSize, inRect.height()
)
# bottom edge
self.frameGrips.bottom.setGeometry(
self.gripSize, inRect.top() + inRect.height(), inRect.width(), self.gripSize
)
def resizeEvent(self, event):
QtWidgets.QMainWindow.resizeEvent(self, event)
self.updateGrips()
def paintEvent(self, event: QtGui.QMouseEvent) -> Any:
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
path = QtGui.QPainterPath()
self.gradient.setStart(QtCore.QRectF(self.rect()).topLeft())
self.gradient.setFinalStop(QtCore.QRectF(self.rect()).topRight())
brush = QtGui.QBrush(self.gradient)
painter.setBrush(brush)
path.addRoundedRect(0, 0, self.width(), self.height(), self.radius, self.radius)
painter.setClipPath(path)
painter.fillPath(path, painter.brush())
return super().paintEvent(event)
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
self.isPress = False
self.pressPos = event.pos()
super().mouseReleaseEvent(event)
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
self.isPress = True
self.pressPos = event.pos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
if self.isPress or self.isDrag:
self.move(
event.globalX() - self.pressPos.x(), event.globalY() - self.pressPos.y()
)
super().mouseMoveEvent(event)
def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> Any:
if event.type() == QtCore.QEvent.Type.MouseButtonPress:
self.isDrag = True
return super().eventFilter(obj, event)
if event.type() == QtCore.QEvent.Type.MouseButtonRelease:
self.isDrag = False
return super().eventFilter(obj, event)
次回はUIのデザイン部分をしていきます。
イベントをフィルタリングする場合Trueを返し、それ以外の場合はFalseを返さないといけません。その処理を入れ忘れると# RuntimeWarning:が表示されます。
この例ではreturn super().eventFilter(obj, event)
スーパークラスを返すようにしています。
【MayaPySide】ちょっとおしゃれなUIメソッド【3日目】の記事でUIの見た目を設定していきます
MayaPython Advent Calendar 2017の8日目の記事はSEN_AさんのMaya Python API 2.0 を使ってCurveからclosestPointを取得するノードを作ろう!です
ディスカッション
コメント一覧
まだ、コメントがありません