見出し画像

Qt for Python リファレンスガイド QWidget 概説編

QWidgetを研究する


このノートはQt for pythonリファレンスQWidgetのページを日本語に翻訳し、個人的な解釈を付け加えたものです。目的はリファレンスの意味を明確化し、実際に有用なコードを付け加えることにより、内容即コード化のページを作ることです。まだまだ不十分な点は多くありますが、個人の備忘録という意味を兼ねて編集いたしました。収穫不問 只問耕耘の精神で望みたいと思います。

 QWidget 文書詳細 

https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#qwidget 

 
 ここでは、QtWidgetsの根幹部分であるQWidgetについて、リファレンスの内容をなぞりながら勉強していきたいと思います。日本語訳は「」でくくっています。途中、私なりに訳してみて、日本語にしてもよくわからない部分については、自分なりに注釈を入れています。

 理論的な所はおいておき、とりあえずQWidgetを表示してみましょう。

from PySide6.QtWidgets import QApplication,QWidget
import sys
def main():
    app = QApplication()
    w = QWidget()
    w.show()
    sys.exit(app.exec())
if __name__ == "__main__":
    main()
app = QApplication([])

PyQtの場合はQApplicationの初期化メソッドに、リスト型オブジェクトを渡す必要があります。空でも問題ありません。ここには、本来コマンドライン引数が入りますが、それはリストで入れ込まれます。空のリストということは、コマンドライン引数は全くないということを意味します。

なお、これからはQWidgetと書いた時はQWidgetというクラスを、ウィジェットと書いた時は、QWidgetおよびQWidgetを継承したクラスがインスタンス化された場合の、オブジェクト全体を指すものとします。
 

ウィジェット

灰色の画面が現れました。これがQWidgetです。

The widget is the atom of the user interface: it receives mouse, keyboard and other events from the window system, and paints a representation of itself on the screen. Every widget is rectangular, and they are sorted in a Z-order. A widget is clipped by its parent and by the widgets in front of it.

QWidget - Qt for Python

「QWidgetはユーザーインターフェースの核です。マウス、キーボード、そして他のイベントをウィンドウシステムから受け取ります。そしてスクリーン上にそれ自体の表示を描画します。全てのウィジェットは矩形であり、Zオーダーで整列されます。①あるウィジェットはその前にあるウィジェットにより、②あるいはその親により領域の外の部分を切り取られます。」

 ①重ね合わせになります。

 ②クリッピングと言います。 

ウィジェットは後からくわえられたものが前に来るx,y軸で、前後はzで表す

A widget that is not embedded in a parent widget is called a window. Usually, windows have a frame and a title bar, although it is also possible to create windows without such decoration using suitable window flags . In Qt, QMainWindow and the various subclasses of QDialog are the most common window types.

QWidget - Qt for Python

「親ウィジェット内に埋め込まれていないウィジェットはウィンドウと呼ばれます。たいてい、ウィンドウは一つのフレームとタイトルバーを持っています。①適切なWindowFlagをつかって、そのような装飾をすることなく、ウィンドウを作ることも可能です。

をしてみます。…は省略記号です。ウィンドウという言葉はさりげなくリファレンスのそこかしこで利用されています。こうした用語をしっかりと理解しておくと、リファレンスの解読精度が向上します。

...
def main():
    app = QApplication()
    w = QWidget(windowFlags=Qt.FramelessWindowHint)
...

フレームレスウィンドウ

これはフレームがない(Framelessな)ウィンドウです。フレームやタイトルバーを持ちません。今までのように×ボタンがないので、Alt+F4キーを押すのがベストではないかと思います。このキーはアプリケーションを終了させる一般的なキーコンビネーションです。

PyQtの場合は、Qt.WindowType.FramelessWindowHintという形で、名前空間をしっかりと繋げておかないとエラーがおきます。PySideは現状このようにしなくてもエラーは起きませんが、PyQtと同じようにしておくことが推奨されています。 推奨の根拠はこちらです。Considerations - Qt for Python

Every widget’s constructor accepts one or two standard arguments:
QWidget *parent = nullptr is the parent of the new widget. If it is None (the default), the new widget will be a window. If not, it will be a child of parent, and be constrained by parent's geometry (unless you specify Window as window flag).

QWidget - Qt for Python

「全てのウィジェットは、一つか二つの標準的な引数を受けます。①QWidget *parent=nullptrは新しいウィジェットの親です。これにNoneが渡されると、そのウィジェットはウィンドウになります。もしそうでなければ、親の子になります。そして②親のジオメトリ(幾何領域)に拘束されます。」

 ①PySideやPyQtではparent=Noneとされていう引数です。

 ②Qtでいうジオメトリとは、パソコンの左上の角(矩形の左上隅の座標位置をトップレフトと言います。)を(0,0)の原点とする矩形範囲を意味します。右方向にx座標が、下方向にy座標が+となる座標軸です。QRect型です。

 子ウィジェットは、さらに親ウィジェットのトップレフトを(0, 0)としたジオメトリとなります。「親のジオメトリ(幾何領域)に拘束されます。」とはそういう意味です。setGeometry()でセットし、QRectオブジェクトを取ります。ジオメトリはgeometry()で取得でき、QRect型オブジェクトを戻します。

ウィジェットは画面のトップレフトを(0,0)として、右方向にx、下方向にyの場所にジオメトリを設定する。さらに子ウィジェットは、親ウィジェットのトップレフトを(0,0)として、同様にする。入れ子構造になっている。子ウィジェットは親ウィジェットの中に複数加えることができる。

 では、QLabelを作り、それをQWidgetの子として表示してみましょう。


from PySide6.QtWidgets import QApplication,QWidget, QLabel
from PySide6.QtGui import QFont
from PySide6.QtCore import Qt

…

def main():

    app = QApplication()

    w = QWidget(parent=None, windowFlags=Qt.Window)

    w.setFont(QFont("Times New Roman", 18))

    l = QLabel("label", parent=w)

    w.show()

…

 

QLabel しかし全体が小さくなりました。

 このコードではQWidgetのほかにQLabelをインポートしています。QLabelのインスタンスを作り、QLabelのparent引数にQWidgetのインスタンスを指定して、QWidgetの子としています。
 子として加える方法は3通りあります。1つ目は、parent引数に親としたいウィジェットのインスタンスを指定すること。2つ目は、setParentメソッドを呼び出して、親のインスタンスを指定すること。3つ目は、ウィジェットをレイアウトに加え、他のウィジェットがそのレイアウトをセットすると、そのレイアウトに加えられているウィジェットは全てそのウィジェットの子となります。
 親ウィジェットの大きさが子ウィジェットの大きさにリサイズされています。これはリファレンスにもそうなる旨書いてありますが、もう少し後で書かれています。

親子関係の構築方法


 ①子ウィジェットのparent引数に親を渡す

 ②子ウィジェットのsetParentメソッドを使って親を指定

 ③レイアウトにウィジェットを加え、親ウィジェットがそのレイアウト 
  をセットすると、レイアウトに加えられたウィジェットは子ウィジェッ  
  トになる。


 ①の場合と、②の場合とでは、結果として表示される内容が異なります。①の場合、子ウィジェットの要求するスペースに親ウィジェットも合わせられますが、②だと、親ウィジェットの大きさは子ウィジェットの要求するスペースに従いません。③の場合、子ウィジェットはレイアウトのルールに従います。親ウィジェットの領域によって影響を受ける場合もあります。

それでは、parentにNoneを渡した場合を見てみましょう。


#parent=Noneの場合。

l = QLabel(“label”, parent=None)

l.show()

親子関係解消

この場合は親を持たないわけですから、QWidgetと同様にウィンドウになります。子であれば親ウィジェットのshow関数の効果が伝播するのですが、そうでなければQLabel自体のshow関数も使わなければなりません。

ただ、QLabelをこのように親と独立して表示するということはまずないと思います。独立表示を行うのは、たいていQMainWindowやQDialog等であり、Qt側があらかじめQt.Windowとしてフラグをセットしています。

Noneはparent引数の仮引数として渡される役割のほか、親子関係解消のための手段として用いられます。

さらに、コードの中で呼び出されているsetFontメソッドは、QWidgetが呼び出すsetFontメソッドで、QLabelはQWidgetの子でも何でもないので、setFontの効果はQLabelには伝わりません。show関数と同じ理屈です。この場合にもQLabelのフォントを変更したいのであれば、QLabel側でsetFontを呼び出す必要があります。このように、親のプロパティが子に伝わる仕組みをプロパゲーション(propagation)(伝播)と言います。

w.setFont(QFont(‘Times New Roman’, 18))の代わりに、

l.setFont(QFont(‘Times New Roman’, 18))

とします。このコードだと、普通QWidgetが親なので、QWidgetにセットしたプロパティは子であるQLabelに伝播します。しかし、親子関係がなければ、そのようなことはありません。

 If not, it will be a child of parent, and be constrained by parent's geometry (unless you specify Window as window flag).

QWidget - Qt for Python

「parentに渡されるのがNoneでなければ、それの子になり、そして親のジオメトリに制限されることになります。①(もしあなたがウィンドウフラグとしてQt::Windowを指定しなければ・・・の話ですが)」

①というわけで、親を指定しながらも、Qt.Windowを指定してみましょう。

#親を指定、しかしQt.Windowをフラグとして指定

...
l = QLabel("label", parent=w, windowFlags=Qt.Window)

l.show()#結果として必要
...

とした場合。


ラベルはウィジェットを親としているが、Qt.Windowをフラグとしてセットしている

 QLabelの親としてQWidgetをセットしたのにもかかわらず、QLabelはウィンドウになっていますね。しかも、親子関係を設定したにもかかわらず、ラベルのフォントは変更されていません。ということは、Qt.Windowにフラグをセットすると、それは親子関係をそもそも持たせない、解消するという意味を含むということになります。

 Qt::WindowFlags f = { } (where available) sets the window flags; the default is suitable for almost all widgets, but to get, for example, a window without a window system frame, you must use special flags.

QWidget - Qt for Python

「(2つの標準的な引数のうち)parentとは別のもう一つの引数はQt::WindowsFlag ={}です。初期値は、ほとんどすべてのウィジェットにとって適切なものです。しかし、例えばウィンドウシステムフレイムなしのウィンドウを得るには、特別なフラグを使わなければなりません。」

Qt for PythonではQt.WindowFlagsまたはQt.WindowType.WindowFlags, PyQtでは、Qt.WindowType.WindowFlagsです。

今まで、Qt.WindowFlagsにはFramelessWindowHintとWindowフラグを渡す例を見てきました。他にもフラグはたくさんあります。そこで、皆さんにご紹介したいのがこちらのイグザンプルです。

 Window Flags Example - Qt for Python

 ここに表示されているイグザンプルは、C++からの変換途中のようです。だいぶC++のコードが混じっていますので、そのままでは使えません。 

これを私なりにPythonに変換したものを書きましたので、確かめてみられてください。

from PySide6.QtWidgets import (QWidget, QApplication,
                               QHBoxLayout, QCheckBox,
                               QGroupBox, QRadioButton,
                               QGridLayout, QTextEdit,
                               QPushButton, QVBoxLayout)
from PySide6.QtCore import Qt
import sys

class ControllerWindow(QWidget):

    def __init__(self, parent=None):
        
        super().__init__(parent)

        self.previewWindow = PreviewWindow(self)
        self.createTypeGroupBox()
        self.createHintsGroupBox()
        quitButton = QPushButton(self.tr("Quit"))
        quitButton.clicked.connect(
                QApplication.quit)
        bottomLayout = QHBoxLayout()
        bottomLayout.addStretch()
        bottomLayout.addWidget(quitButton)
        mainLayout = QHBoxLayout()
        mainLayout.addWidget(self.typeGroupBox)
        mainLayout.addWidget(self.hintsGroupBox)
        mainLayout.addLayout(bottomLayout)
        self.setLayout(mainLayout)
        self.setWindowTitle(self.tr("Window Flags"))
        self.updatePreview()

    def updatePreview(self):

        flags = None
        
        if self.windowRadioButton.isChecked():
            flags = Qt.Window
        elif self.dialogRadioButton.isChecked():
            flags = Qt.Dialog
        elif self.sheetRadioButton.isChecked():
            flags = Qt.Sheet
        elif self.drawerRadioButton.isChecked():
            flags = Qt.Drawer
        elif self.popupRadioButton.isChecked():
            flags = Qt.Popup
        elif self.toolRadioButton.isChecked():
            flags = Qt.Tool
        elif self.toolTipRadioButton.isChecked():
            flags = Qt.ToolTip
        elif self.splashScreenRadioButton.isChecked():
            flags = Qt.SplashScreen

        if self.msWindowsFixedSizeDialogCheckBox.isChecked():
            flags |= Qt.MSWindowsFixedSizeDialogHint
        if self.x11BypassWindowManagerCheckBox.isChecked():
            flags |= Qt.X11BypassWindowManagerHint
        if self.framelessWindowCheckBox.isChecked():
            flags |= Qt.FramelessWindowHint
        if self.windowTitleCheckBox.isChecked():
            flags |= Qt.WindowTitleHint
        if self.windowSystemMenuCheckBox.isChecked():
            flags |= Qt.WindowSystemMenuHint
        if self.windowMinimizeButtonCheckBox.isChecked():
            flags |= Qt.WindowMinimizeButtonHint
        if self.windowMaximizeButtonCheckBox.isChecked():
            flags |= Qt.WindowMaximizeButtonHint
        if self.windowCloseButtonCheckBox.isChecked():
            flags |= Qt.WindowCloseButtonHint
        if self.windowContextHelpButtonCheckBox.isChecked():
            flags |= Qt.WindowContextHelpButtonHint
        if self.windowShadeButtonCheckBox.isChecked():
            flags |= Qt.WindowShadeButtonHint
        if self.windowStaysOnTopCheckBox.isChecked():
            flags |= Qt.WindowStaysOnTopHint
        if self.windowStaysOnBottomCheckBox.isChecked():
            flags |= Qt.WindowStaysOnBottomHint
        if self.customizeWindowHintCheckBox.isChecked():
            flags |= Qt.CustomizeWindowHint

        self.previewWindow.setWindowFlags(flags)

        pos = self.previewWindow.pos()
        if pos.x() < 0:
            pos.setX(0)
        if pos.y() < 0:
            pos.setY(0)
        self.previewWindow.move(pos)
        self.previewWindow.show()
        

    def createTypeGroupBox(self):
        self.typeGroupBox = QGroupBox(self.tr("Type"))
        self.windowRadioButton = self.createRadioButton(self.tr("Window"))
        self.dialogRadioButton = self.createRadioButton(self.tr("Dialog"))
        self.sheetRadioButton = self.createRadioButton(self.tr("Sheet"))
        self.drawerRadioButton = self.createRadioButton(self.tr("Drawer"))
        self.popupRadioButton = self.createRadioButton(self.tr("Popup"))
        self.toolRadioButton = self.createRadioButton(self.tr("Tool"))
        self.toolTipRadioButton = self.createRadioButton(self.tr("Tooltip"))
        self.splashScreenRadioButton = self.createRadioButton(self.tr("Splash screen"))
        self.windowRadioButton.setChecked(True)
        layout = QGridLayout()
        layout.addWidget(self.windowRadioButton, 0, 0)
        layout.addWidget(self.dialogRadioButton, 1, 0)
        layout.addWidget(self.sheetRadioButton, 2, 0)
        layout.addWidget(self.drawerRadioButton, 3, 0)
        layout.addWidget(self.popupRadioButton, 0, 1)
        layout.addWidget(self.toolRadioButton, 1, 1)
        layout.addWidget(self.toolTipRadioButton, 2, 1)
        layout.addWidget(self.splashScreenRadioButton, 3, 1)
        self.typeGroupBox.setLayout(layout)

    def createHintsGroupBox(self):

        self.hintsGroupBox = QGroupBox("Hints")

        self.msWindowsFixedSizeDialogCheckBox = self.createCheckBox("MS Windows fixed size dialog")
        self.x11BypassWindowManagerCheckBox = self.createCheckBox("X11 bypass window manager")
        self.framelessWindowCheckBox = self.createCheckBox("Frameless window")
        self.windowNoShadowCheckBox = self.createCheckBox("No drop shadow")
        self.windowTitleCheckBox = self.createCheckBox("Window Title")
        self.windowSystemMenuCheckBox = self.createCheckBox("Window system menu")
        self.windowMinimizeButtonCheckBox = self.createCheckBox("Window minimize button")
        self.windowMaximizeButtonCheckBox = self.createCheckBox("Window maximize button")
        self.windowCloseButtonCheckBox = self.createCheckBox("Window close button")
        self.windowContextHelpButtonCheckBox = self.createCheckBox("Window context help button")
        self.windowShadeButtonCheckBox = self.createCheckBox("Window shade button")
        self.windowStaysOnTopCheckBox = self.createCheckBox("Window stays on top")
        self.windowStaysOnBottomCheckBox = self.createCheckBox("Window stays on bottom")
        self.customizeWindowHintCheckBox = self.createCheckBox("Customize window")

        layout = QGridLayout()
        layout.addWidget(self.msWindowsFixedSizeDialogCheckBox, 0, 0)
        layout.addWidget(self.x11BypassWindowManagerCheckBox, 1, 0)
        layout.addWidget(self.framelessWindowCheckBox, 2, 0)
        layout.addWidget(self.windowNoShadowCheckBox, 3, 0)
        layout.addWidget(self.windowTitleCheckBox, 4, 0)
        layout.addWidget(self.windowSystemMenuCheckBox, 5, 0)
        layout.addWidget(self.customizeWindowHintCheckBox, 6, 0)
        layout.addWidget(self.windowMinimizeButtonCheckBox, 0, 1)
        layout.addWidget(self.windowMaximizeButtonCheckBox, 1, 1)
        layout.addWidget(self.windowCloseButtonCheckBox, 2, 1)
        layout.addWidget(self.windowContextHelpButtonCheckBox, 3, 1)
        layout.addWidget(self.windowShadeButtonCheckBox, 4, 1)
        layout.addWidget(self.windowStaysOnTopCheckBox, 5, 1)
        layout.addWidget(self.windowStaysOnBottomCheckBox, 6, 1)
        self.hintsGroupBox.setLayout(layout)
        
    def createCheckBox(self, text):
        checkBox = QCheckBox(text)
        checkBox.clicked.connect(self.updatePreview)
        return checkBox

    def createRadioButton(self, text):
        button = QRadioButton(text)
        button.clicked.connect(self.updatePreview)
        return button

class PreviewWindow(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.textEdit = QTextEdit()
        self.textEdit.setReadOnly(True)
        self.textEdit.setLineWrapMode(QTextEdit.NoWrap)

        self.closeButton = QPushButton("&Close")
        self.closeButton.clicked.connect(self.close)

        layout = QVBoxLayout()
        layout.addWidget(self.textEdit)
        layout.addWidget(self.closeButton)
        self.setLayout(layout)

        self.setWindowTitle("Preview")

    def setWindowFlags(self, flags):

        super().setWindowFlags(flags)
        text = ""
        type =  flags & Qt.WindowType_Mask
        if type == Qt.Window:
            text = "Qt.Window"
        elif type == Qt.Dialog:
            text = "Qt.Dialog"
        elif type == Qt.Sheet:
            text = "Qt.Sheet"
        elif type == Qt.Drawer:
            text = "Qt.Drawer"
        elif type == Qt.Popup:
            text = "Qt.Popup"
        elif type == Qt.Tool:
            text = "Qt.Tool"
        elif type == Qt.ToolTip:
            text = "Qt.ToolTip"
        elif type == Qt.SplashScreen:
            text = "Qt.SplashScreen"
        if flags & Qt.MSWindowsFixedSizeDialogHint:
            text += "\n| Qt.MSWindowsFixedSizeDialogHint"
        if flags & Qt.X11BypassWindowManagerHint:
            text += "\n| Qt.X11BypassWindowManagerHint"
        if flags & Qt.FramelessWindowHint:
            text += "\n| Qt.FramelessWindowHint"
        if flags & Qt.NoDropShadowWindowHint:
            text += "\n| Qt.NoDropShadowWindowHint"
        if flags & Qt.WindowTitleHint:
            text += "\n| Qt.WindowTitleHint"
        if flags & Qt.WindowSystemMenuHint:
            text += "\n| Qt.WindowSystemMenuHint"
        if flags & Qt.WindowMinimizeButtonHint:
            text += "\n| Qt.WindowMinimizeButtonHint"
        if flags & Qt.WindowMaximizeButtonHint:
            text += "\n| Qt.WindowMaximizeButtonHint"
        if flags & Qt.WindowCloseButtonHint:
            text += "\n| Qt.WindowCloseButtonHint"
        if flags & Qt.WindowContextHelpButtonHint:
            text += "\n| Qt.WindowContextHelpButtonHint"
        if flags & Qt.WindowShadeButtonHint:
            text += "\n| Qt.WindowShadeButtonHint"
        if flags & Qt.WindowStaysOnTopHint:
            text += "\n| Qt.WindowStaysOnTopHint"
        if flags & Qt.WindowStaysOnBottomHint:
            text += "\n| Qt.WindowStaysOnBottomHint"
        if flags & Qt.CustomizeWindowHint:
            text += "\n| Qt.CustomizeWindowHint"
        self.textEdit.setPlainText(text)

def main():
    app = QApplication()
    controller = ControllerWindow()
    controller.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

リファレンスのQt.WindowTypeについても調べてみられてください。

 https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.WindowType(Qt - Qt for Python)

 

QWidget has many member functions, but some of them have little direct functionality; for example, QWidget has a font property, but never uses this itself. There are many subclasses which provide real functionality, such as QLabel , QPushButton , QListWidget , and QTabWidget .

QWidget - Qt for Python

「QWidgetは実に多くのメンバ関数を持っていますが、そのなかにはQWidget自体にとって機能するものはほとんどありません。例えば、QWidgetはフォントプロパティを持ちますけれども、QWidgetでこれを利用しても全く役に立たないのです。これらを利用して意味があるのは、QWidgetを継承して作られる多くのサブクラスです。QLabel, QPushButton, QListWidget, QTabWidgetなどです。」
(なぜならば、これらのサブクラスはテキスト情報をもち、フォントはそのテキストを装飾するのに使われるからです。) 

トップレベルウィジェットと子ウィジェット

https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#top-level-and-child-widgets(QWidget - Qt for Python)

A widget without a parent widget is always an independent window (top-level widget). For these widgets, setWindowTitle() and setWindowIcon() set the title bar and icon respectively.

QWidget - Qt for Python

「親ウィジェットがないウィジェットは常に独立したウィンドウです。(トップレベルウィジェットと呼びます。)これらのウィジェットは、setWindowTitleでタイトルバーにタイトルを表示したり、setWindowIconでアイコンをそれぞれ表示したりします。」

 では、タイトルやアイコンを表示してみましょう。…は省略記号です。

from PySide6.QtGui import QFont, QIcon

…

def main():

…

    w.setWindowTitle("title")

    w.setWindowIcon(QIcon("jp.png"))


アイコンは日の丸 タイトルは[title]


 日本の国旗をアイコンとし、titleというタイトルをセットしてみました。QIconをインポートします。コードと同じファイルにアイコンがあれば、QIconのコンストラクタに名前を指定するだけでファイルをロードでき、アイコンオブジェクトを作ることができます。もしそれ以外のファイルにアイコンがある場合等は、絶対パスを指定するなどの工夫が必要になります。また、ここでは述べませんが、Qtのリソースシステムを活用するのが便利です。 皆さんもお好きなアイコンをセットしてみてください。iconとして利用できるのは、pngやjpg, ico拡張子などがついた画像ファイルです。

Non-window widgets are child widgets, displayed within their parent widgets. Most widgets in Qt are mainly useful as child widgets. For example, it is possible to display a button as a top-level window, but most people prefer to put their buttons inside other widgets, such as QDialog .

QWidget - Qt for Python

ウィンドウではないウィジェットは子ウィジェットです。親ウィジェット内に表示されています。Qt内のほとんどのウィジェットは主に子ウィジェットとして利用価値があります。例えば、トップレベルウィンドウとしてボタンを表示することができますが、ほとんどの人は、それを他のウィジェット内の子ウィジェットとして利用したいと考えるでしょう。」

親子関係説明ダイアグラム

QWidget Class | Qt Widgets 6.5.0

(より画像をいただきました。)

The diagram above shows a QGroupBox widget being used to hold various child widgets in a layout provided by QGridLayout . The QLabel child widgets have been outlined to indicate their full sizes.

QWidget - Qt for Python

「このダイアグラムはQGroupBoxウィジェットを表示しています。QGridLayoutで提供されるレイアウト内に様々な子ウィジェットを持つために使われています。①QLabelの子ウィジェットは、それらのフルサイズを示すために縁取りの線が表示されています。」

 ①「フルサイズを示すために縁取りの線」→赤くて四角い枠のことです。

では、これと同じものを作ってみましょう。まだ見たこともないクラスが含まれていますが、今は意識する必要はありません。

from PySide6.QtWidgets import (QApplication,QWidget, QGroupBox,
                                QLabel, QDateEdit, QTimeEdit,
                               QLineEdit, QTextEdit,
                               QFormLayout,
                               QVBoxLayout)

from PySide6.QtGui import QFont, QIcon
from PySide6.QtCore import Qt

import sys

def main():
    
    app = QApplication()
    w = QWidget()
    groupBox = QGroupBox(title="Appointment Details")

    dateEdit = QDateEdit()
    timeEdit = QTimeEdit()
    lineEdit = QLineEdit(text="Meeting room 1")

    f = QFormLayout()
    f.addRow("&Date:", dateEdit)
    f.addRow("&Time:", timeEdit)
    f.addRow("&Location:", lineEdit)

    t = QTextEdit(text="""<b>Developer meeting</b>
                        <p>A brief meeting to check the
                          status of each project in the
                          development department.</p>""")

    g = QVBoxLayout()
    g.addLayout(f)
    g.addWidget(t)
    groupBox.setLayout(g)
    
    groupBox.setParent(w)
    groupBox.move(50, 50)
    w.show()  
    sys.exit(app.exec())
    
if __name__ == "__main__":
    main()

from PyQt6.QtWidgets import (QApplication,QWidget, QGroupBox,
                                QLabel, QDateEdit, QTimeEdit,
                               QLineEdit, QTextEdit,
                               QFormLayout,
                               QGridLayout)

from PyQt6.QtGui import QFont, QIcon
from PyQt6.QtCore import Qt

import sys

def main():
    
    app = QApplication([])
    w = QWidget()
    groupBox = QGroupBox(title="Appointment Details")

    dateEdit = QDateEdit()
    timeEdit = QTimeEdit()
    lineEdit = QLineEdit(text="Meeting room 1")

    f = QFormLayout()
    
    f.setWidget(0, QFormLayout.ItemRole.SpanningRole, dateEdit)
    f.addRow("&Time:", timeEdit)
    f.addRow("&Location:", lineEdit)

    t = QTextEdit(html="""<b>Developer meeting</b>
                        <p>A brief meeting to check the
                          status of each project in the
                          development department.</p>""")
  
    f.addRow(t)
    
 
    groupBox.setLayout(f)
    
    groupBox.setParent(w)
    groupBox.move(50, 50)
    w.show()  
    sys.exit(app.exec())
    
if __name__ == "__main__":
    main()


ダイアグラム

PyQtの場合、QTextEditに渡す仮引数の名前は、htmlになります。 (PySideでもhtml引数は使えるようです)

 レイアウトはウィジェットの配置を一定のルールで決定する仕組みです。これのおかげでsetGeometryやmove関数等を使って一つ一つの子ウィジェットの位置をハードコードする徒労から解放されます。

 リファレンスでは、このダイアグラムを作成するためにQGridLayoutを利用していると述べてありますが、私がここで利用しているレイアウトはQFormLayoutです。左にラベル、右に具体的なコンポーネント(フィールドと言います)を配置するレイアウトです。横方向を列(カラム=column)、縦方向を行(ロー=row)と言います。左にはstr型を与えるだけで、QLabelがセットされたことになるので便利です。
 昔のQtではQGridLayoutが用いられていたようですが、2列複数行のレイアウトは非常に多いため、より簡潔に書けるようにこうしたクラスが作られたようです。便利な機能も追加されています。全体的、部分的に1列表示もできます。

 他にも、QVBoxLayout,QHBoxLayoutという頻繁に利用されるレイアウトクラスがあります。普通はこの二つから紹介されます。

 If you want to use a QWidget to hold child widgets you will usually want to add a layout to the parent QWidget . See Layout Management for more information.

QWidget - Qt for Python

「もしあなたが子ウィジェットを持つQWidgetを使いたいならば、大抵は親のQWidgetへレイアウトを加えたいでしょう。詳しくはレイアウトマネジメント(Layout Management)を拝見してください。」

レイアウトマネジメントについては、この記事では詳しく扱いません。QWidgetは全リファレンスの中でボリュームが最大といっていいクラスなので、長くなりすぎるからです。

コンポジットウィジェット

https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#composite-widgets(QWidget - Qt for Python)

 When a widget is used as a container to group a number of child widgets, it is known as a composite widget. These can be created by constructing a widget with the required visual properties - a QFrame , for example - and adding child widgets to it, usually managed by a layout. The above diagram shows such a composite widget that was created using Qt Designer.

QWidget - Qt for PythonLayout Management

①あるウィジェットがたくさんの子ウィジェットのコンテナとして利用されるとき、それをコンポジットウィジェットと言います。②これはQFrame等、必要なビジュアルプロパティを備えたウィジェットと、それに対して子ウィジェットを加えることによって作られるものです。たいてい、レイアウトによって管理されています。」

 ①コンテナという言葉はプログラミングではよく使われる言葉です。現実にも、コンテナという単語はよく使われます。たくさんの物資を一つの箱の中に詰めているように、たくさんの子ウィジェットやレイアウトを一つのウィジェットの中にのせているので、そういう役割をしているウィジェットをコンテナと呼んでいます。ただ、そうしてできたウィジェットを、コンポジットウィジェットというようです。

 ②ウィジェットはQtWidgetsというモジュールに属したクラスであり、かつQObjectとQPaintDeviceというクラスを継承したものです。この条件があれば、ウィジェットの視覚化に関するプロパティを持っています。(visibleプロパティです。)

こちらはインタプリタ上でウィジェットを表示するプログラムです。show関数やsetVisible関数ではなく、setPropertyを使っています。


>>>from PySide6.QtWidgets import QWidget, QApplication
>>>from PySide6.QtCore import Qt
>>>import sys
>>>app = QApplication()
>>>w = QWidget(windowFlags=Qt.WindowType.WindowStaysOnTopHint)
>>>w.setProperty("visible", True)
True
>>>sys.exit(app.exec())

 これに対して、QLayoutItemというクラスがあり、これもQtWidgetsというモジュールに属しているものの、QPaintDeviceを継承していないため、視覚化のためのプロパティはありません。

 「ビジュアルプロパティを備えた」、とは、はっきりと書かれてはいないものの、視覚化するためのプロパティであるvisibleを備えているウィジェットのことを言うと理解しています。

 「コンポジットウィジェットは、QWidgetやQFrameなどを継承し、①その中で必要なレイアウトや子ウィジェットをコンストラクタ内で加えることによっても作成できます。多くのサンプルコードがこの手法をとっています。そして、QtWidgetsチュートリアル(https://doc.qt.io/qtforpython-6/overviews/widgets-tutorial.html#widgets-tutorial(Widgets Tutorial - Qt for Python)でもこれはカバーされています。」

from PySide6.QtWidgets import QApplication, QWidget, QPushButton
import sys

class Widget(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        self._button = QPushButton(text="test", parent=self)
        
        
def main():

    app = QApplication([])
    w = Widget()
    w.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

例えばこのように、QWidgetを継承したクラスのコンストラクタ内部でプッシュボタンを子として加えています。これが①の意味です。普通はもっといろいろなウィジェットが加えられ、多くの場合はレイアウトによって管理されることになります。

カスタムウィジェットとペインティング

https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#composite-widgets(QWidget - Qt for Python)

 Since QWidget is a subclass of QPaintDevice , subclasses can be used to display custom content that is composed using a series of painting operations with an instance of the QPainter class.

QWidget - Qt for Python

「QWidgetはQPaintDeviceのサブクラスなので、サブクラスはQPainterクラスのインスタンスで①一連のペイント処理を使って構成されるカスタム化した内容を表示するために使われることができます。」

①QPainterは、ペンやブラシをセットして、ウィジェット上に図形やテキストのような2次元グラフィックを描画する機能を持っています。何かを描く前に、ペン、ブラシ、線の太さといった情報をQPainterにセットしておきます。それから描画処理を開始します。私たちは、QWidgetのpaintEvent関数をオーバーライドし、そのQWidget自体の上に自分たちの好きなグラフィックを描くことができます。この瞬間、ウィジェットはカスタム化した内容を持つことになります。
 それが、「QPainterクラスのインスタンスで一連のペイント処理を使って構成されるカスタム化した内容を表示する」という意味です。カスタムに対する言葉は標準的な、というものだと思います。カスタムであれ、標準的であれ、可視化できるウィジェットは皆QPainterで描かれています。Qt側がそのようにしているからです。私たちが自分達でQPainterのインスタンスを作っていないにも関わらず、表示ができているということは、それが標準の(カスタム化していない)内容です。もし私たちがQPainterを使って何かを描いたのであれば、その瞬間カスタム化した内容になります。

This approach contrasts with the canvas-style approach used by the Graphics View Framework where items are added to a scene by the application and are rendered by the framework itself.

QWidget - Qt for Python

「このアプローチは、GraphicsViewFrameworkによって使われる、アプリケーションによってアイテムがシーンへ追加され、フレームワークそれ自体によって表示されるようなキャンバススタイルとは対照的です。」

GraphicsViewFrameworkについては、今はわからなくても問題ありません。

Each widget performs all painting operations from within its paintEvent() function. This is called whenever the widget needs to be redrawn, either as a result of some external change or when requested by the application.

QWidget - Qt for Python

 「それぞれのウィジェットは①paintEvent関数内から全てのペインティングオペレーションを実行します。これはウィジェットが再描画される必要があるときはいつでも呼ばれます。②他の外部の変化の結果として、あるいは、アプリケーションによって要求された時なども同様です。」 

 それでは、ちょっと試しに何かを描いてみましょう。ここでは、正方形を描く例です。 

from PySide6.QtWidgets import QApplication,QWidget
from PySide6.QtGui import QPainter
from PySide6.QtCore import Qt

import sys

class PaintWidget(QWidget):

 
    def paintEvent(self, event):
        print("event is called")
        painter = QPainter()
        painter.begin(self)
        painter.drawRect(150, 100, 300, 300)
        painter.end()
        return super().paintEvent(event)    

    def keyPressEvent(self, event):

        if event.key() == Qt.Key_W:
            self.repaint()
            return super().keyPressEvent(event)

        elif event.type() == Qt.Key_Q:
            self.update()
            return super().keyPressEvent(event)

def main():    
    app = QApplication()
    w = PaintWidget()
    w.show()  
    sys.exit(app.exec())    
if __name__ == "__main__":
    main()    

    
        


square paint example

paintEventを継承してその中でQPainterを呼び出していますね。それが「(paintEvent関数内から全てのペインティングオペレーションを実行します。)という意味です。
②「他の外部の変化の結果として」というのは、いろいろ考えられますが、例えばウィジェットのサイズが変化したときには、paintEventが再度呼ばれます。また、repaintメソッドやupdate()を使ったときも、paintEventが呼ばれます。repaintは即時再描画です。updateは即時ではなく、一連の流れが終わった後になります。square paint exampleではWを押した時にrepaintを、Qを押した時にupdateを呼び出すように書いていますが、この時点ではupdateを呼び出してもpaintEventは呼ばれないようです。updateについてはupdateの項目を読んでください。

The Analog Clock example shows how a simple widget can handle paint events.

QWidget - Qt for Python

「アナログクロックサンプル(Analog Clock example )が、シンプルなウィジェットがペイントイベントをどのように扱うことができるのかを見せてくれます。」

Analog Clock ExampleもC++のコードしかない様子なので、ここで載せておきます。

from PySide6.QtWidgets import QWidget, QApplication
from PySide6.QtGui import QPainter, QColor
from PySide6.QtCore import QTimer, QTime, QPoint, Qt
import sys

class AnalogClock(QWidget):
    
    def __init__(self, parent=None):
        super().__init__(parent)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(1000)

        self.setWindowTitle(self.tr("Analog Clock"))
        self.resize(200, 200)


    def paintEvent(self, event):

        hourHand = [QPoint(7, 8),
                    QPoint(-7, 8),
                    QPoint(0, -40)
                    ]
        minuteHand = [
            QPoint(7, 8),
            QPoint(-7, 8),
            QPoint(0, -70)
            ]

        hourColor = QColor(127, 0, 127)
        minuteColor = QColor(0, 127, 127, 191)

        side = min(self.width(), self.height())

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.translate(self.width()/2, self.height()/2)
        painter.scale(side /200.0, side/200.0)

        painter.setPen(Qt.NoPen)
        painter.setBrush(hourColor)

        time = QTime.currentTime()

        painter.save()
        painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0)))
        painter.drawConvexPolygon(hourHand)
        painter.restore()

        painter.setPen(hourColor)

        for i in range(12):
            painter.drawLine(80, 0, 96, 0)
            painter.rotate(30.0)


        painter.setPen(Qt.NoPen)
        painter.setBrush(minuteColor)

        painter.save()
        painter.rotate(6.0 * (time.minute() + time.second() / 60.0))
        painter.drawConvexPolygon(minuteHand)
        painter.restore()

        painter.setPen(minuteColor)

        for j in range(60):
            if j%5 != 0:
                painter.drawLine(92, 0, 96, 0)
            painter.rotate(6.0)
  
def main():

    app = QApplication([])
    analogClock = AnalogClock()
    analogClock.show()

    sys.exit(app.exec())

if __name__ == "__main__":
    main()


このコードは11:35分~36分に実行したことがわかります。コードを実行して暫く眺めていると分針が動いていることに気が付かれるでしょう。


サイズヒントとサイズポリシー

https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#size-hints-and-size-policies(QWidget - Qt for Python)

 When implementing a new widget, it is almost always useful to reimplement sizeHint() to provide a reasonable default size for the widget and to set the correct size policy with setSizePolicy() .

QWidget - Qt for Python

「新しいウィジェットを実装するとき、ほとんどたいていはsizeHintを再実装することが便利です。①合理的なデフォルトのサイズを提供するためです。そして、②setSizePolicyで、正確なサイズポリシーをセットすることも同様に便利です。」

 では、sizeHintを再実装してみましょう。

from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtCore import Qt, QSize
import sys

class SizeHintWidget1(QWidget):

    def sizeHint(self):

        return QSize(100, 100)

class SizeHintWidget2(QWidget):

    def sizeHint(self):

        return QSize(200, 200)

class SizeHintWidget3(QWidget):

    def sizeHint(self):

        return QSize(300, 300)

def main():
    
    app = QApplication()

    s1 = SizeHintWidget1()
    s2 = SizeHintWidget2()
    s3 = SizeHintWidget3()    
    
    s3.show()
    s2.show()
    s1.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()
        


sizeHint100,200,300

By default, composite widgets which do not provide a size hint will be sized according to the space requirements of their child widgets.

QWidget - Qt for Python

普通はこのようなコードを書くことはありませんが、sizeHintの値によって最初に表示されるウィジェットの大きさが決定されているということがわかっていただけると思います。

「デフォルトでは、コンポジットウィジェットがサイズヒントを提供しない時には、①子ウィジェットの求めるスペースに従ってサイズ決定されるでしょう。」

①「子ウィジェットの求めるスペースに従って」、先ほどQLabelをQWidgetの子として表示したとき、一気に表示されるウィンドウが小さくなったのを覚えていらっしゃいますか?あれは、子ウィジェットであるQLabelの求めるスペースに従って、親であるQWidgetのサイズが決定されたのです。
 もしこの現象を防ぎたいのであれば、コンポジットウィジェット、ここでは親となるQWidgetのサブクラスで、sizeHintをオーバーライドしておく必要があると書かれています。それがない場合は、子ウィジェットの求めるスペースに従います。

The size policy lets you supply good default behavior for the layout management system, so that other widgets can contain and manage yours easily. The default size policy indicates that the size hint represents the preferred size of the widget, and this is often good enough for many widgets.

QWidget - Qt for Python

「サイズポリシーはレイアウト管理システムのためによき初期動作を提供してくれます。デフォルトのサイズポリシーはそのウィジェットのPreferredサイズを表示することを示しており、これはたいてい多くのウィジェットで十分な状態です。」

Note
The size of top-level widgets are constrained to 2/3 of the desktop’s height and width. You can resize() the widget manually if these bounds are inadequate.

QWidget - Qt for Python

 ノート:

トップレベルウィジェットのサイズはデスクトップの高さと幅の3分の2に制限されます。もしこれらの縛りがあることが不十分であるならば、resize()メソッドを使います。」

 実際、何もしなければウィジェットはデスクトップの画面の大きさの3分の2を目安としたサイズになっています。resizeメソッドを使ってサイズ指定をしておけば、そのサイズに変化します。ややこしいですが、sizeHintの戻り値はデフォルトのサイズ、resizeメソッドを使った場合、その後のサイズということです。resizeメソッドをコンストラクタ内で呼び出した場合、あるいはインスタンス生成直後に呼んだ場合には、結局デフォルトの値と変わりがないということになります。

では、サイズポリシーというものをセットしてみましょう。

from PySide6.QtWidgets import (
    QApplication,QWidget, QLabel, QVBoxLayout, QSizePolicy, QPushButton)
from PySide6.QtGui import QPainter, QIcon
from PySide6.QtCore import Qt, QSize
import sys

class SizeHintWidget(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)      
  
        l1 = PushButton(text="text1")
        l2 = PushButton(text="text2")
        v = QVBoxLayout()
        v.addWidget(l1)
        v.addWidget(l2)
        self.setLayout(v)    

    def sizeHint(self):
        return QSize(100, 100)    

class PushButton(QPushButton):
    def __init__(self, icon=QIcon(), text="", parent=None):
        super().__init__(icon, text, parent)        
        self.setSizePolicy(
            QSizePolicy.Fixed, QSizePolicy.Fixed
                    )       
                
    def sizeHint(self):
        rturn QSize(100, 100)  
 
def main():    
    app = QApplication([])
    w = SizeHintWidget()
    w.show()  
    sys.exit(app.exec())    
if __name__ == "__main__":
    main() 
        

QPushButtonが二つ、QVBoxLayoutに入れられています。そして、それぞれのサイズヒントは100,100の正方形です。さらに、サイズポリシーとして、Fixed, Fixed(固定、固定)がセットされています。こうなると、QPushButtonは全くそれ以上大きくなることはなく、小さくなることもありません。サイズヒントによって与えられたサイズで固定する、というのがサイズポリシーのFixed, Fixedの意味です。

 Minimumは、sizeHintのサイズが最小のサイズになるという意味です。試しにMinimum, Minimumに変えてみましょう。そうすると、大きくはなりますが、100,100以下に小さくなることはないはずです。

 Maximumは、sizeHintのサイズが最大のサイズになるという意味です。試しに、Maximum, Maximumに変えてみましょう。そうすると、QPushButtonをのせているウィジェット自体は大きくなります。しかし、ラベルは100,100よりも大きくなりません。最大が100ということは、最小値までは小さくなってくれるはずです。しかし、100以下にはなってくれません。これはminimumSizeHintがsizeHintと同じ値になる仕様だからです。つまり最低も100,100なので、それ以上小さくなることはありません。本当にMaximumサイズポリシーの効果を発揮したいのであれば、minimumSizeHintをオーバーライドして、最低の幅と高さも指定する必要があります。

def minimumSizeHint(self):

    return QSize(15, 0)

 これを追加してみましょう。

つぶれていくボタン

 高さが0なのでこのようにつぶれていきます。最終的には何も表示されなくなります。ところが、高さは0になっても、一定のよりも小さくはならないようです。

 サイズポリシーについては、こちらにわかりやすいサイトがありますので、合わせてご覧ください。

第4回 Qtの基本プログラミング~レイアウトマネージメント | gihyo.jp

 レイアウトは以下のようにして定まるようです。

「・sizeHintとsizePolicyに応じて、スペースの量が初期に割り当てられる。

・そのウィジェットのうちのいずれかにストレッチファクターがセットされるならば、0よりも大きい値で、ファクターの比率に応じてスペースが割り当てられる。

・ストレッチファクターが0であるウィジェットのいくつかは、もし他のウィジェットがスペースを求めていないならば、より多くのスペースを得るだけだ。これらのうち、Expandingサイズポリシーを持っているウィジェットに最初に割り当てられる。

・それらの最小のサイズ(ミニマムサイズ)よりも少ないスペースを割り当てられたウィジェットは、要求しているミニマムサイズが割り当てられる。」

ignoredポリシーというものがあります。これはsizeHintの値を全く無視するものです。ですから、sizeHintで設定された値はもう読み込まれません。じゃあどうなるのかというと、レイアウトルールが適用された上で、本当にそのウィジェットが入るためのスペースがある場合にだけ表示されるようです。

 レイアウトシステムは、QLayoutItem, QLayout, QBoxLayoutと継承が進みます。私たちはさらに継承が進んだQHBoxLayout, QVBoxLayout, QGridLayout, QFormLayoutの4つを主に利用します。


レイアウトクラスの継承関係
  • sizeHint, minimumSizeHint→レイアウトクラスと共に利用される

  • fixedSize, minimumSize, maximumSize, size→レイアウトクラス関係なくセットされるが、事実上レイアウトの計算と衝突する。

 レイアウト処理に関係なく、というのは、レイアウトを利用しなくてもそのサイズが尊重されるようになる、という意味です。しかしながら、レイアウト処理を利用した場合でも、やはりそのサイズは関わってきます。例えばsetMinimumSizeで最小のサイズを指定した場合、レイアウトのやりくりで最小サイズ以下につぶそうと思っても、指定された最小のサイズより小さくはなりません。setFixedSizeやsetMaximumSizeというのもあり、こうしたメソッドとの関係が初学者を混乱させます。

 maximumSizeはデフォルトだととてつもなく大きな数値です。1677215です。普通はこんなサイズのウィジェットを作るとは思えないので、事実上制約がない、ということになります。制約を付ける場合は、setMaximumSizeメソッドを使って、もっと小さいサイズを指定することになります。

 sizeHintはオーバーライドをして、QSizeオブジェクトを返しておけば自動で読み込まれましたが、minimumSizeやmaximumSizeは、オーバーライドをしただけでは効果がありません。というかオーバーライドはしません。ウィジェットは当然のようにmaximumSizeにアクセスはしないのです。明示的にsetMinimumSize, setMaximumSizeを呼んで、QSizeを引数に渡しましょう。詳しくは、別の記事でご紹介したいと思います。

イベント

 https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#events

(QWidget - Qt for Python)

Widgets respond to events that are typically caused by user actions. Qt delivers events to widgets by calling specific event handler functions with instances of QEvent subclasses containing information about each event.

QWidget - Qt for Python

「①ウィジェットはユーザーのアクションによって典型的に引き起こされるイベントに反応します。②Qtはイベントをそれぞれのイベントについての情報を含んでいるQEventサブクラスのインスタンスで③特有のイベントハンドラ関数をコールすることによってウィジェットへイベントを伝達します。」

①「ユーザーのアクションによって典型的に引き起こされるイベント」

マウスクリックとかキーボードのプッシュとか、マウスが領域内に入ってきたとか出ていったとか、Qtに限らずユーザー側の処理によってOS全体で普通に発生するイベントです。

②「それぞれのイベントについての情報を含んでいるQEventサブクラスのインスタンスで」

例えばマウスイベントには、QMouseEventというクラスがありますし、キーイベントはQKeyEventというクラスがあります。これらはQEventのサブクラスです。マウスがクリックされた時、QMouseEventが作られます。だから、それぞれのイベントについての情報を含んでいるQEventサブクラスのインスタンスで・・・ということになります。

③「特有のイベントハンドラ関数をコールすることによって」

QMouseEventが作られたらmousePressEvent, mouseReleaseEventというイベントハンドラ関数の引数として渡されてきます。つまり、この関数をコールすることになります。


QEventの継承関係 本当はもっとあります。特にQEvent直下のクラスはQInputEvent以外にも多くあります。

 もしeventを伝達したければ、event.acceptメソッドを、無視したければevent.ignoreを呼びます。(デフォルトはacceptです。)伝達するとは、このことを指しています。acceptやignoreが使われるのは、closeEventやdropEvent等ではよく見かけます。

If your widget only contains child widgets, you probably do not need to implement any event handlers. If you want to detect a mouse click in a child widget call the child’s underMouse() function inside the widget’s mousePressEvent() .

QWidget - Qt for Python

「もしあなたのウィジェットが子ウィジェットを持っているだけならば、多分全てのイベントハンドラを導入する必要はありません。①もしあなたが子ウィジェット内でマウスクリックを検出したいのであれば、ウィジェットのmousePressEvent()の内部でunderMouse()をコールします。」 

ちょっと検出してみましょう。

from PySide6.QtWidgets import QWidget, QLabel, QApplication

from PySide6.QtGui import QPixmap, QPainter, QFont, QColor, QPolygon, QPalette, QRegion

from PySide6.QtCore import Qt, QRect, QPoint

import sys

class ChildWidget(QWidget):

    def __init__(self, parent=None):

        super().__init__(parent)

        self.setGeometry(0, 0, 125, 100)

class ParentWidget(QWidget):

    def __init__(self, parent=None):

        super().__init__(parent)

        self.setAttribute(Qt.WA_PaintOnScreen, True)

        self.childWidget = ChildWidget(parent=self)

        self.childWidget.move(100, 100)

        self.childWidget.repaint()

        self.setFont(QFont("Times New Roman", 18))

    def mousePressEvent(self, event):

        if self.underMouse():

            print(self.childWidget.underMouse())

        return super().mousePressEvent(event)

def main():

    app = QApplication()

    w = ParentWidget()

    w.show()
    
    sys.exit(app.exec())

if __name__ == "__main__":

    main()

中央付近でクリックをすると、Trueがかえります。そこに子ウィジェットがあるからです。 

The Scribble example implements a wider set of events to handle mouse movement, button presses, and window resizing.

QWidget - Qt for Python

「Scribbleエグザンプルがマウスムーブイベント、ボタンプレス、そしてウィンドウのリサイズを扱うためのイベントの広い設定を導入しています。」

 このサンプルもPySideのエグザンプルにはないようなので、こちらにコードを載せておきます。

from PySide6.QtWidgets import (QApplication, QColorDialog, QFileDialog,
                            QInputDialog, QMenuBar, QMessageBox,
                               QMainWindow, QWidget, QMenu)
from PySide6.QtGui import (QCloseEvent, QImageWriter, QAction,
                            QKeySequence, QImage, QPen,QColor,
                           QPainter)
from PySide6.QtPrintSupport import QPrinter, QPrintDialog
from PySide6.QtCore import Qt, QPoint, QRect, QSize, QDir
import sys


class MainWindow(QMainWindow):
    
    def __init__(self, parent=None):
        super().__init__(parent)

        self.scribbleArea = ScribbleArea(parent=self)        

        self.saveAsActs = []        
        
        self.createActions()
        self.createMenus()

        self.setWindowTitle("Scribble")
        self.resize(500, 500)

    def closeEvent(self, event):
        if self.maybeSave():
            event.accept()
        else:
            event.ignore()

    def open(self):

        if self.maybeSave():
            fileName, _ = QFileDialog.getOpenFileName(self,
                                                   self.tr("Open File"),
                                                   QDir.currentPath())

            if fileName:
                self.scribbleArea.openImage(fileName)
    def save(self):
        action = self.sender()
        fileFormat = action.data().toByteArray()
        self.saveFile(fileFormat)

    def penColor(self):
        newColor = QColorDialog.getColor(self.scribbleArea.penColor())
        if newColor.isValid():
            self.scribbleArea.setPenColor(newColor)

    def penWidth(self):

        ok = False
        newWidth, ok = QInputDialog.getInt(self, self.tr("Scribble"),
                                       self.tr("Select pen width:"),
                                       self.scribbleArea.penWidth(),
                                       1, 50, 1)
        if ok:
            self.scribbleArea.setPenWidth(newWidth)

    def about(self):

        QMessageBox.about(self, self.tr("About Scribble"),
                          self.tr("<p>The <b>Scribble</b> example shows how to use QMainWindow as the "
               "base widget for an application, and how to reimplement some of "
               "QWidget's event handlers to receive the events generated for "
               "the application's widgets:</p><p> We reimplement the mouse event "
               "handlers to facilitate drawing, the paint event handler to "
               "update the application and the resize event handler to optimize "
               "the application's appearance. In addition we reimplement the "
               "close event handler to intercept the close events before "
               "terminating the application.</p><p> The example also demonstrates "
               "how to use QPainter to draw an image in real time, as well as "
               "to repaint widgets.</p>"))

    def createActions(self):
        self.openAct = QAction(self.tr("&Open..."), self)
        self.openAct.setShortcuts(QKeySequence.Open)
        self.openAct.triggered.connect(self.open)

        imageFormats = QImageWriter.supportedImageFormats()
        for format_ in imageFormats:
            
            text = self.tr("{}...".format(str(format_).upper()))
            action = QAction(text, self)
            action.triggered.connect(self.save)
            self.saveAsActs.append(action)

        self.printAct = QAction(self.tr("&Print..."), self)
        self.printAct.triggered.connect(self.scribbleArea.print)

        self.exitAct = QAction(self.tr("E&xit"), self)
        self.exitAct.setShortcuts(QKeySequence.Quit)
        self.exitAct.triggered.connect(self.close)

        self.penColorAct = QAction(self.tr("&Pen Color..."), self)
        self.penColorAct.triggered.connect(self.penColor)

        self.penWidthAct = QAction(self.tr("Pen &Width"), self)
        self.penWidthAct.triggered.connect(self.penWidth)

        self.clearScreenAct = QAction(self.tr("&Clear Screen"), self)
        self.clearScreenAct.setShortcut(self.tr("Ctrl+L"))
        self.clearScreenAct.triggered.connect(self.scribbleArea.clearImage)

        self.aboutAct = QAction(self.tr("&About"), self)
        self.aboutAct.triggered.connect(self.about)

        self.aboutQtAct = QAction(self.tr("About &Qt"), self)
        self.aboutQtAct.triggered.connect(QApplication.aboutQt)


    def createMenus(self):

        saveAsMenu = QMenu(self.tr("&Save As"), self)
        for action in self.saveAsActs:
            saveAsMenu.addAction(action)

        fileMenu = QMenu(self.tr("&File"), self)
        fileMenu.addAction(self.openAct)
        fileMenu.addMenu(saveAsMenu)
        fileMenu.addAction(self.printAct)
        fileMenu.addSeparator()
        fileMenu.addAction(self.exitAct)

        optionMenu = QMenu(self.tr("&Options"), self)
        optionMenu.addAction(self.penColorAct)
        optionMenu.addAction(self.penWidthAct)
        optionMenu.addSeparator()
        optionMenu.addAction(self.clearScreenAct)

        helpMenu = QMenu(self.tr("&Help"), self)
        helpMenu.addAction(self.aboutAct)
        helpMenu.addAction(self.aboutQtAct)

        self.menuBar().addMenu(fileMenu)
        self.menuBar().addMenu(optionMenu)
        self.menuBar().addMenu(helpMenu)


    def maybeSave(self):

        if self.scribbleArea.isModified():
            ret = QMessageBox.warning(self, self.tr("Scribble"),
                                      self.tr("The image has been modified\
                                              Do yu want to save your changes?"),
                                      QMessageBox.Save|QMessageBox.Discard|QMessageBox.Cancel)
            if ret == QMessageBox.Save:
                return self.saveFile("png")
            elif ret == QMessageBox.Cancel:
                return False
        return True

    def saveFile(self):

        initialPath = QDir.currentPath() + "/untitled." + fileFormat
        fileName = QFileDialog.getSaveFileName(self, self.tr("Save As"),
                                               initialPath,
                                               self.tr(
                                                   "{} Files (*.{})::All Files (*)".format(
                                                       fileFormat.upper(),
                                                        fileFormat)))

        if not fileName:
            return False
        return self.scribbleArea.saveImage(fileName, fileFormat.constData())


class ScribbleArea(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.modified = False
        self.scribbling = False
        self.myPenWidth = 1
        self.myPenColor = Qt.blue
        self.image = QImage()
        self.lastPoint = QPoint()

        self.setAttribute(Qt.WA_StaticContents, True)
        self.resize(500, 500)

    def isModified(self):

        return self.modified

    def penColor(self):

        return self.myPenColor

    def penWidth(self):

        return self.myPenWidth

    def setPenWidth(self, newWidth):

        self.myPenWidth = newWidth

    def openImage(self, fileName):
        import os
        loadedImage = QImage(os.path.join(os.getcwd(), fileName))

        newSize = loadedImage.size().expandedTo(self.size())
        self.resizeImage(loadedImage, newSize)
        self.image = loadedImage
       
        self.modified = False
        self.repaint()
        return True

    def saveImage(self, fileName, fileFormat):

        visibleImage = self.image
        self.resizeImage(visibleImage, self.size())

        if visibleImage.save(fileName, fileFormat):
            self.modified = False
            return True
        return False

    def setPenColor(self, newColor):

        self.myPenColor = newColor

    def clearImage(self):

        self.image.fill(QColor(255, 255, 255))
        self.modified = True
        self.update()


    def mousePressEvent(self, event):

        if event.button() == Qt.LeftButton:
            self.lastPoint = event.position().toPoint()
            self.scribbling = True

        return super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        
        if event.buttons() == Qt.LeftButton and self.scribbling:
            self.drawLineTo(event.position().toPoint())

    def mouseReleaseEvent(self, event):
        
        if event.button() == Qt.LeftButton and self.scribbling:
            self.drawLineTo(event.position().toPoint())
            self.scribbling = False

        return super().mouseReleaseEvent(event)

    def paintEvent(self, event):
        print(event)
        painter = QPainter()
        painter.begin(self)
        dirtyRect = event.rect()
        painter.drawImage(dirtyRect, self.image, dirtyRect)
        painter.end()

        return super().paintEvent(event)

    def resizeEvent(self, event):

        if self.width() > self.image.width() or self.height() > self.image.height():
            newWidth = max(self.width() + 128, self.image.width())
            newHeight = max(self.height() + 128, self.image.height())
            self.resizeImage(self.image, QSize(newWidth, newHeight))
            self.update()
       
        return super().resizeEvent(event)

    def drawLineTo(self, endPoint):

        painter = QPainter(self.image)
        painter.setPen(QPen(self.myPenColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap,
                            Qt.RoundJoin)
                       )
        painter.drawLine(self.lastPoint, endPoint)
        self.modified = True

        rad = (self.myPenWidth / 2) + 2
        self.update(QRect(
            self.lastPoint, endPoint).normalized().adjusted(-rad, -rad, rad, rad))

        self.lastPoint = endPoint

    def resizeImage(self, image, newSize):

        if self.image.size() == newSize:
            return

        newImage = QImage(newSize, QImage.Format_RGB32)
        newImage.fill(QColor(255, 255, 255))
        painter = QPainter(newImage)
        painter.drawImage(QPoint(0, 0), self.image)
        self.image = newImage

    def print(self):

        printer = QPrinter(QPrinter.HighResolution)
        printDialog = QPrintDialog(printer, self)

        if printDialog.exec() == QDialog.Accepted:
            painter = QPainter(printer)
            rect = painter.viewport()
            size = self.image.size()
            size.scale(rect.size(), Qt.KeepAspectRatio)
            painter.setViewport(rect.x(), rect.y(), size.width(), size.height())
            painter.setWindow(self.image.rect())
            painter.drawImage(0, 0, image)

def main():
    app = QApplication([])
    window = MainWindow()
    window.show()
    return sys.exit(app.exec())

if __name__ == "__main__":
    main()
    
    
        

You will need to supply the behavior and content for your own widgets, but here is a brief overview of the events that are relevant to QWidget , starting with the most common ones:

QWidget - Qt for Python

「①独自の動作や内容を提供する必要がありますが、②QWidgetに関連し、最も共通しているものからスタートし、イベントの手短な全体像を次に示します。」

①「独自の動作や内容を提供する必要があります」というのは、あなたがイベントをオーバーライドする以上は、あなた自身がその内容を定義する必要があるということです。
②QWidgetが関わらない範囲でも、イベントは重要な役割を持っています。しかしこれからお話するイベントの範囲は、QWidgetにかかわるものに限定するようです。

paintEvent() is called whenever the widget needs to be repainted. Every widget displaying custom content must implement it. Painting using a QPainter can only take place in a paintEvent() or a function called by a paintEvent() .

QWidget - Qt for Python

「paintEvent()はウィジェットが再描画する必要があるときはいつでも呼ばれます。カスタムな内容を表示する全てのウィジェットはこれを実装しなければなりません。QPainterを使うペインティングは、paintEvent内か、paintEventによって呼び出される関数の中だけで行われます。」

「カスタムな内容を表示」という言葉の意味ですが、先ほども言及しました。paintEventをオーバーライドして、その中で自分がQPainterを使って何かを描いた瞬間、それがカスタムな内容になります。カスタムじゃない内容というのは、私たちが何もしなくともQt側が勝手に描いてくれているQWidget等の領域とかを言います。

resizeEvent() is called when the widget has been resized.

QWidget - Qt for Python

 「resizeEventは、そのウィジェットのサイズが変更された時に呼ばれます。」

 よくあるきっかけは、resizeメソッドを明示的に呼び出したり、ウィジェットの右下を引っ張る(グリップと言います)を行うことで発生します。


 mousePressEvent() is called when a mouse button is pressed while the mouse cursor is inside the widget, or when the widget has grabbed the mouse using grabMouse() . Pressing the mouse without releasing it is effectively the same as calling grabMouse() .

QWidget - Qt for Python

 「mousePressEventはマウスカーソルがウィジェットの中にある時にマウスボタンが押された場合か、①あるいはウィジェットがgrabMouse()を利用してマウスを掴んだ時に呼ばれます。マウスを押してマウスを放さずに押し続けることは、grabMouse()を呼んでいるのと実質的には同じになります。」
①grabMouse()を利用してマウスを掴んだ時ってどういうことでしょうか。

from PySide6.QtWidgets import QApplication,QWidget, QPushButton
from PySide6.QtGui import QPainter
from PySide6.QtCore import Qt

import sys

class MouseGrabWidget(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.pushButton = QPushButton(text= "ungrab", parent=self)      

    def mousePressEvent(self, event):

        print("mouse press event is called")

        return super().mousePressEvent(event)

    def mouseMoveEvent(self, event):

        print("mouse move event is called")

        return super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):

        print("mouse release event is called")

        return super().mouseReleaseEvent(event)    

    def keyPressEvent(self, event):

        if event.key() == Qt.Key_W:

            self.grabMouse()
            self.pushButton.setText("grab")
            return super().keyPressEvent(event)

        elif event.key() == Qt.Key_Q:

            self.releaseMouse()
            self.pushButton.setText("ungrab")
            return super().keyReleaseEvent(event)        

def main():    
    app = QApplication()
    w = MouseGrabWidget()
    w.show()  
    sys.exit(app.exec())    
if __name__ == "__main__":
    main()    

Wを押すとgrabに、Qを押すとgrabは解放されます。grab中は、ウィジェットを閉じようとして×を押そうとしても押せません。詳しいことはgrabMouseの項目を参照してください。簡単に申し上げれば、grabMouse()を呼んだウィジェットでしか、マウスイベントを受け付けなくなります。

 mouseReleaseEvent() is called when a mouse button is released. A widget receives mouse release events when it has received the corresponding mouse press event. This means that if the user presses the mouse inside your widget, then drags the mouse somewhere else before releasing the mouse button, your widget receives the release event. There is one exception: if a popup menu appears while the mouse button is held down, this popup immediately steals the mouse events.

QWidget - Qt for Python

 「マウスリリースイベントはマウスボタンがリリース(解放)された時に呼ばれます。ウィジェットは対応しているマウスプレスイベントを受け取ったときにマウスリリースイベントを受け取ります。つまり、もしユーザーがウィジェット内でマウスを押し、①それからマウスボタンをリリースする前に他のどこかにマウスをドラッグするならば、ウィジェットはリリースイベントを受け取ります。一つ例外があります。マウスボタンが押し続けられているときにポップアップメニューが現れると、このポップアップはマウスイベントを即座にスチールします。」

 ①「それからマウスボタンをリリースする前に他のどこかにマウスをドラッグするならば、ウィジェットはリリースイベントを受け取ります。」の部分なのですが、実験してみたところ、呼ばれませんでした。

mouseDoubleClickEvent() is called when the user double-clicks in the widget. If the user double-clicks, the widget receives a mouse press event, a mouse release event, (a mouse click event,) a second mouse press, this event and finally a second mouse release event. (Some mouse move events may also be received if the mouse is not held steady during this operation.) It is not possible to distinguish a click from a double-click until the second click arrives. (This is one reason why most GUI books recommend that double-clicks be an extension of single-clicks, rather than trigger a different action.)

QWidget - Qt for Python

 「マウスダブルクリックイベントはユーザーがウィジェット内でダブルクリックをする時に呼ばれます。もしユーザーがダブルクリックをするならば、ウィジェットはマウスプレスイベント、マウスリリースイベント、2回目のマウスプレスイベント、そして最後には2回目のマウスリリースイベントを受け取ります。(いくつかのマウスムーブイベントもまた、もしこの実行がなされている間にその場にとどまり続けるのでないのならば、呼ばれるかもしれません。)クリックとダブルクリックの違いは、第2回目のクリックが届くまで区別をつけることができません。(これがほとんどのGUI本がダブルクリックは別のアクションをトリガーするよりもむしろ、シングルクリックの拡張であるとするよう勧めている理由です。)」

Widgets that accept keyboard input need to reimplement a few more event handlers:

QWidget - Qt for Python

 「キーボードインプットを受け取るウィジェットはさらにちょっとだけ多くのイベントハンドラを再実装する必要があります。」

keyPressEvent() is called whenever a key is pressed, and again when a key has been held down long enough for it to auto-repeat. The Tab and Shift+Tab keys are only passed to the widget if they are not used by the focus-change mechanisms. To force those keys to be processed by your widget, you must reimplement event() .

QWidget - Qt for Python

 「keyPressEventはキーが押された時はいつでも呼ばれます。そしてキーが自動的にリピート(オートリピート)するのに十分なくらい長く押され続けた時に再び呼ばれます。①TabとShift+Tabキーはもしフォーカスチェンジのメカニズムによって使われないならば、その時だけウィジェットに渡されます。」

 オートリピートされるのが初期状態のようです。TabとShift+Tabキーは、ウィジェット上で特殊な動きをします。ちょっとコードを見てみましょう。

from PySide6.QtWidgets import QApplication, QWidget, QPushButton

from PySide6.QtGui import QPainter

from PySide6.QtCore import Qt, QRect

import sys

def main():

    app = QApplication()

    w = QWidget()

    p1 = QPushButton(

        parent=w, geometry=QRect( 0, 0, 50, 50)

    )

    p2 = QPushButton(

    parent=w, geometry=QRect(50, 50, 50, 50)

    )

    w.show()

    sys.exit(app.exec())

if __name__ == "__main__":

    main()

TabキーとShift+Tabキーを押してください。フォーカスが切り替わっているのがわかっていただけると思います。今でこそ二つのQPushButtonですが、もっと多くのウィジェットがあることが普通です。フォーカスを受け取る流れをフォーカスチェインと言います。そして、フォーカスがタブで切り替わる順番を、タブオーダーと言います。フォーカスチェンジのメカニズムは、このフォーカスチェインにあるウィジェット間の、タブオーダーにより決まります。QWidgetは、setTabOrderというメソッドを持っていて、これでタブオーダーを決定できます。

タブのオーダーは、ウィジェットをインスタンス化する順番によって決まるようになっています。ですから、インスタンス化する順番を考えて行いましょう。それが一番楽です。

 これは非常に便利な仕組みなのですが、一つ注意があります。その問題はQTextEdit等のテキスト処理系のウィジェットで発生します。QTextEdit上では、タブキーを利用すると、空白スペースが入れ込まれます。そのため、QTextEditにフォーカスが移ると、連鎖があっても次のウィジェットへ移動することがありません。setTabChangesFocusにTrueを渡せばこれを解除できます。

from PySide6.QtWidgets import QTextEdit, QPushButton, QHBoxLayout, QApplication, QWidget

import sys


class Widget(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        t = QTextEdit()
##        t.setTabChangesFocus(True)
        p1 = QPushButton()
        p2 = QPushButton()

        h = QHBoxLayout()

        h.addWidget(p1)
        h.addWidget(t)
        h.addWidget(p2)

        self.setLayout(h)

def main():

    app = QApplication()
    w = Widget()
    w.show()
    

    sys.exit(app.exec())

if __name__ == "__main__":
    main()
        

これが、①「TabとShift+Tabキーはもしフォーカスチェンジのメカニズムによって使われないならば、その時だけウィジェットに渡されます。」が意味するところです。

focusInEvent() is called when the widget gains keyboard focus (assuming you have called setFocusPolicy() ). Well-behaved widgets indicate that they own the keyboard focus in a clear but discreet way.

QWidget - Qt for Python

「focusInEventはウィジェットがキーボードフォーカスを得る時に呼ばれます。(setFocusPolicy()を呼んだとみなして)。①適切に動作するウィジェットは簡潔ではあるが考え抜かれたやり方でキーボードフォーカスを所有するということを示しています。」

 デフォルトでキーボードフォーカスは受け取ります。

①後半の文章の意味が今いちわかりにくいのですが、いいウィジェットは、タブチェンジがいい流れで決まっているものだといいたいのでしょう。

focusOutEvent() is called when the widget loses keyboard focus.

QWidget - Qt for Python

 「focusOutEventは、そのウィジェットがキーボードフォーカスを失うときに呼ばれます。」

 フォーカスの説明を私の言葉で言わせていただくと、作業ウィジェットの決定という意味です。日本語でフォーカスは焦点とか、集中という意味を持ちます。思えば、GUIはコンポジットウィジェットであることがほとんどでありますし、GUI表示中も、そのGUIとは全く無関係なことをしたくなることがよくあります。ネット検索とか文書編集とか。そうしたとき、今どのGUIが作業ウィジェットになっているのかを定めなければなりません。

 例えば、ワードソフトを開きながら、インターネットブラウザでコメントを編集しているとき、フォーカスという仕組みがなければ、ワードにもインターネットのコメント欄にも文字が打ち込まれてしまいかねません。これだと非常に不便ですから、作業するウィジェットを定める必要があるのです。ここは当然すぎて、パソコンに詳しくない方も普通にフォーカスを操って作業してこられたことだと思いますが、GUIを作る側からすれば、フォーカスを定める作業は重要です。Qtの場合は、かなりの部分が自動化されていますので、私たちはフォーカスを考慮する必要は基本的にありません。特にQtDesignerを用いると、この部分がかなり楽に実装できます。

 フォーカスというのはGUIである以上、Qtに限らず存在している一般的な用語です。Qtだけに絞ってお伝えすると、Qt内のGUIにフォーカスが移ることによって、FocusInEventが呼ばれ、フォーカスが無くなるときにFocusOutEventが呼ばれます。

 フォーカスが移るには、クリックやキーボタン、タブキーによる移動等がありますが、フォーカスポリシーをセットすることによって、フォーカスの移動を制限することができます。例えば、setFocusPolicyにNoFocusをセットすると、そのウィジェットにフォーカスが移ることはありません。

from PySide6.QtWidgets import QTextEdit, QPushButton, QHBoxLayout, QApplication, QWidget
from PySide6.QtCore import Qt
import sys


class Widget(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        t = QTextEdit()
        t.setTabChangesFocus(True)
        p1 = QPushButton()
        p1.setFocusPolicy(Qt.NoFocus)
        p1.clicked.connect(lambda: print("clicked"))
        p2 = QPushButton()

        h = QHBoxLayout()

        h.addWidget(p1)
        h.addWidget(t)
        h.addWidget(p2)

        self.setLayout(h)

def main():

    app = QApplication()
    w = Widget()
    w.show()
    

    sys.exit(app.exec())

if __name__ == "__main__":
    main()
        

        

 フォーカスが移らないからといって、そのウィジェットの機能が使えなくなるのではありません。単にフォーカスが移らないだけです。例えばQTextEditにフォーカスがある状態で、QPushButtonをクリックすると、クリックした瞬間、QPushButtonに備え付けられたメソッドなどは発行されますし、ボタンのダウンも起きます。しかし、フォーカスはQTextEditにあるままなので、キーボードをそのまま押せば継続的に編集を行うことができます。もしフォーカスがQPushButtonに移っていると、QPushButtonに対してkeyEventが呼ばれてしまいますから、QTextEditにフォーカスを移さなければ、何も編集することができなくなるのです。

You may be required to also reimplement some of the less common event handlers:

QWidget - Qt for Python

「あまり一般的ではないイベントハンドラのいくつかも実装することを要求されるかもしれません。」

 これからお話するイベントたちが、あまり一般的でないとQt側が考えているイベントのようです。

 mouseMoveEvent() is called whenever the mouse moves while a mouse button is held down. This can be useful during drag and drop operations. If you call setMouseTracking (true), you get mouse move events even when no buttons are held down. (See also the Drag and Drop guide.)

QWidget - Qt for Python

 「マウスムーブイベントはマウスボタンが押されている間にマウスが動くときにはいつでも呼ばれます。これはドラッグ、ドロップ処理の間には役に立ちます。もしあなたがsetMouseTrackingをTrueにセットしているのであれば、全くボタンが押されていなくとも、マウスムーブイベントを得ることになります。(DragとDropのガイドDrag and Drop - Qt for Python(https://doc.qt.io/qtforpython-6/overviews/dnd.html#drag-and-drop)もみてください)。」

keyReleaseEvent() is called whenever a key is released and while it is held down (if the key is auto-repeating). In that case, the widget will receive a pair of key release and key press event for every repeat. The Tab and Shift+Tab keys are only passed to the widget if they are not used by the focus-change mechanisms. To force those keys to be processed by your widget, you must reimplement event() .

QWidget - Qt for Python

「keyReleaseEventはあるキーがリリース(解放)されるときにはいつでも呼ばれます。もしそのキーがオートリピートであるならば、押し続けている間呼ばれます。その場合、ウィジェットはリピートごとにキーリリースとキープレスのペアを受け取ります。①タブとShift+タブキーはもしフォーカス変更のメカニズムを利用していないならば、その時だけウィジェットに通されます。②それらのキーをあなたのウィジェットで手続きされるよう強制するならば、あなたはevent関数を再実装しなければなりません。」

①その時だけウィジェットに通されます、という意味ですが、先ほどQTextEditの例で説明差し上げました。QTextEditはTabを使用して空白を入れ込みます。この機能が有効だと、Tabによるフォーカスチェンジができなくなります。この場合、setTabChangeFocusプロパティがFalseなので、「フォーカス変更のメカニズムを利用していない」ことになりますから、その時には、QTextEdit内のkeyPressEvent, keyReleaseEvent内でも、Tabキーは呼ばれるようになる、ということを意味しています。

from PySide6.QtWidgets import QTextEdit, QPushButton, QHBoxLayout, QApplication, QWidget
from PySide6.QtCore import Qt
import sys


class TextEdit(QTextEdit):

    def __init__(self, parent=None):
        super().__init__(parent)

       
        self.setTabChangesFocus(True)
    
    def keyPressEvent(self, event):

        if event.key() == Qt.Key_Tab:

            print("tab is called") 


        return super().keyPressEvent(event)

def main():

    app = QApplication()
    w = QWidget()
    t = TextEdit()
    p1 = QPushButton()
    p2 = QPushButton()
    h = QHBoxLayout()
    h.addWidget(p1)
    h.addWidget(t)
    h.addWidget(p2)
    
    w.setLayout(h)
    
    w.show()    

    sys.exit(app.exec())

実験の結果、QTextEditだけの場合は、tabChangesFocusの値がどうであれ、keyPressEvent内にQt.Key_Tabは渡されてくるようです。
 

②「それらのキーをあなたのウィジェットで手続きされるよう強制するならば」、ということですが、もしフォーカス変更のメカニズムを利用しているにもかかわらず、QTextEdit内部のkeyPressEvent等でタブキーやShift+タブキーの処理を行いたいならば、ということを言っています。event関数は、keyPressEventが呼ばれる前にイベントが通過する関数なので、そのタイミングであれば、タブキーによるフォーカス変更が起きる前に、私たちがイベント操作できるのです。

from PySide6.QtWidgets import QTextEdit, QPushButton, QHBoxLayout, QApplication, QWidget
from PySide6.QtCore import Qt, QEvent
import sys


class TextEdit(QTextEdit):

    def __init__(self, parent=None):
        super().__init__(parent)

       
        self.setTabChangesFocus(True)

    def event(self, event):

        if event.type() == QEvent.KeyPress:

            if event.key() == Qt.Key_Tab:
                print("tab is called") 

                return True

            if event.key() == Qt.Key_Backtab:
                print("backtab is called")

                return True

        return super().event(event)
    
    def keyPressEvent(self, event):

        if event.key() == Qt.Key_Tab:

            print("tab is called") 


        return super().keyPressEvent(event)

def main():

    app = QApplication()
    w = QWidget()
    t = TextEdit()
    p1 = QPushButton()
    p2 = QPushButton()
    h = QHBoxLayout()
    h.addWidget(p1)
    h.addWidget(t)
    h.addWidget(p2)
    
    w.setLayout(h)
    
    w.show()    

    sys.exit(app.exec())

if __name__ == "__main__":
    main()
        

wheelEvent() is called whenever the user turns the mouse wheel while the widget has the focus.

QWidget - Qt for Python

「wheelEventはウィジェットがフォーカスを持っている間にユーザーがマウスのホイールを回転させるときにはいつでも呼ばれます。」

enterEvent() is called when the mouse enters the widget’s screen space. (This excludes screen space owned by any of the widget’s children.)

QWidget - Qt for Python

「enterEvent()はマウスがウィジェットのスクリーンのスペースに入るといつでも呼ばれます。(これはウィジェットの子のどれかによって所有されているスペースを除きます。)」

leaveEvent() is called when the mouse leaves the widget’s screen space. If the mouse enters a child widget it will not cause a leaveEvent() .

QWidget - Qt for Python

「leaveEvent()はマウスがウィジェットのスクリーンスペースから離れる時にはいつでも呼ばれます。もしそのマウスが子ウィジェットに入るならば、leaveEventは発生しないでしょう。」

moveEvent() is called when the widget has been moved relative to its parent.

QWidget - Qt for Python

「moveEventはウィジェットが①その親と相対的に移動された時に呼ばれます。」

closeEvent() is called when the user closes the widget (or when close() is called).

QWidget - Qt for Python

 ①「その親と」とあるように、このイベントが呼ばれるときは必ず親子関係があります。子が親のウィジェット内部でそのx, y座標を変更したとき、moveEventが発生します。move関数は子の位置を移動する関数です。

closeEvent() is called when the user closes the widget (or when close() is called).

QWidget - Qt for Python

「closeEventはユーザーがウィジェットを閉じるときに呼ばれます。(あるいはclose()がコールされるとき)」

 ここでは書いてありませんが、ウィジェットが表示されるときにはshowEventがコールされます。

There are also some rather obscure events described in the documentation for Type . To handle these events, you need to reimplement event() directly.

QWidget - Qt for Python

 「型によってはドキュメント内に説明されているかなりあいまいなイベントもあります。これらのイベントを扱うには、直接event()メソッドを再実装する必要があります。」

The default implementation of event() handles Tab and Shift+Tab (to move the keyboard focus), and passes on most of the other events to one of the more specialized handlers above.

QWidget - Qt for Python

 「①event()の初期導入はTabとShift+Tab(キーボードフォーカスを動かすための)を扱います。そして他のイベントのほとんどはより特化した上記のハンドラに渡されます。」

 ①先ほどTabとShift+Tabがフォーカスチェインに組み込まれないようにするには、eventメソッドをオーバーライドするという話がありました。それと関係しているのでしょう。だから、(キーボードフォーカスを動かすための)と注意書きがされています。

そして、event関数は、その後各ウィジェットへイベントを伝達するための源流となる関数なので、イベントはやがて各ウィジェットへ伝達されて行きます。「他のイベントのほとんどはより特化した上記のハンドラに渡されます。」、というのは、例えばpaintEventやresizeEvent等、これまでに見た(上記の)ハンドラに渡されて行く、という意味です。aboveが具体的にどこを指しているのかちょっとわかりにくいです。

 こちらがevent関数のソースコードです。TabやBacktabという文字列は見当たりませんでした。

qobject.cpp source code [qtbase/src/corelib/kernel/qobject.cpp] - Codebrowser

Events and the mechanism used to deliver them are covered in The Event System .

QWidget - Qt for Python

 「それらを送るために使われるイベントとそのメカニズムはEvent System(The Event System - Qt for Python) https://doc.qt.io/qtforpython-6/overviews/eventsandfilters.html#the-event-systemの項目で説明します。」

 ここまでがイベントに関係するリファレンスの内容とその翻訳です。

 イベントループはQApplicationをインスタンスにし、exec()を呼び出すことによって発生します。QtのGUIは、基本的にイベント駆動型なので、ユーザーである我々が、マウスをクリックしたり、キーを押したりする何らかの作業を行うまで処理を行いません。

 イベントは、keyPressEventやmousePressEvent等のように個々のメソッドに分けられていますが、元々をたどれば、eventメソッドというところからきています。つまり、keyPressEventやmousePressEventというメソッドをオーバーライドし、その中でイベントをキャッチするよりも前に、イベントをキャッチしておきたいのであれば、eventメソッドをオーバーライドします。ただ、この場合はどのようなタイプのイベントなのかが判然としないため、渡されてくるイベントタイプをチェックしたうえで利用することになります。

 他に、イベントフィルターという仕組みがあります。例えば、親ウィジェットから子ウィジェットへイベントが通過していくのがイベントの基本的な流れになるのですが、ある適当なオブジェクトが、installEventFilterというメソッドを使ったとします。すると、その対象となったクラス内で発生するイベントを、installEventFilterを使ったクラス内でキャッチすることができるようになります。イベントについては、イベントの項目でまとめたいと思います。

ウィジェットのスタイルシート

https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#widget-style-sheets

 QWidget - Qt for Python

In addition to the standard widget styles for each platform, widgets can also be styled according to rules specified in a style sheet . This feature enables you to customize the appearance of specific widgets to provide visual cues to users about their purpose. For example, a button could be styled in a particular way to indicate that it performs a destructive action.

QWidget - Qt for Python

 「①それぞれのプラットフォームのための標準的なウィジェットスタイルに加えて、スタイルシートによって特化したルールに従い装飾することもできます。②この機能を使用すると、特定のウィジェットの外観をカスタマイズして、ユーザーにその目的に関する視覚的な手がかりを提供できます。例えば、ボタンは有害な動作を行うことを示すために、特別な方法で装飾を行うことができます。」

 ①「それぞれのプラットフォームのための標準的なウィジェットスタイル」

from PySide6.QtWidgets import QStyleFactory

print(QStyleFactory.keys())

 私の環境だと、これが返ってきます。

 ['windowsvista', 'Windows', 'Fusion']

 例えば、

...
s = QStyleFactory.create("Windows")

app.setStyle(s)
...

として、QApplicationのスタイルに、スタイルを作ったうえセットしてみます。すると、

windowsvista


windowsvista

windows


windows

Fusion

Fusion


②の最後の意味ですが、押すと危険を意味する☠マークのようなものをボタンにつけておけば、ユーザーに押してはダメだ、押すなら用心しろ、ということを示唆することができる、という程度の意味だと思います。あまり有害な機能を持つものを、おいそれと押しやすい場所に表示することは無いと思いますが、例えば全てのデータを消去するボタンとか、そういうものに危険をにおわせる装飾を施すのはいいアイデアだと思います。ただ、普通はQMessageBox等のダイアログを表示して、事前に警告を行うでしょう。

The use of widget style sheets is described in more detail in the Qt Style Sheets document.

QWidget - Qt for Python

「ウィジェットにはそれぞれのプラットフォームに応じたスタイルがありますが、stylesheetによって装飾することもできます。詳しくは https://doc.qt.io/qtforpython-6/overviews/stylesheet.html#qt-style-sheets(Qt Style Sheets )を参照してください。」 

 QWidgetの時点でご紹介するよりも、別のクラスの説明の時にご紹介したほうが、内容がよりよく伝わると思います。 

透過とダブルバッファリング

 https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#transparency-and-double-buffering(QWidget - Qt for Python)

ちょっとここから解説させていただくリファレンスは、私の知識不足も手伝ってか、わかりにくいですので、ご了承ください。

Since Qt 4.0, QWidget automatically double-buffers its painting, so there is no need to write double-buffering code in paintEvent() to avoid flicker.

QWidget - Qt for Python

「Qt4.0からQtはダブルバッファリングをサポートするようになったので、ちらつきを防ぐために①私たちがダブルバッファリングをする必要はなくなりました。 

 ダブルバッファリングとは - 意味をわかりやすく - IT用語辞典 e-Words

 ①リンク先のダブルバッファリングの意味は、現在表示している画像と、バッファとしてメモリに書き込んでいる隠蔽された画像データの2つをデータとしてとらえます。表示している画像の内容をそのままの状態で変更しようとすると、その変更過程が画面に表示されてしまい、ちらつきなどが目立つようになります。そのため、バッファとしてメモリに書き込んでいる隠蔽された画像データの方に変更を加え、変更が終われば、その結果としての画像を表示するようにします。つまり、作業過程は見えないところで行い、結果だけを表示するようにすれば、ちらつきなどを見せないでOKということです。
 これを行う必要がなくなった、ということは、画像データを内部的にもう一つ自動的に用意してくれて、内部で更新が行われ結果だけ表示されるようなQtの仕組みが整った、ということでしょう。

Since Qt 4.1, the contents of parent widgets are propagated by default to each of their children as long as WA_PaintOnScreen is not set. Custom widgets can be written to take advantage of this feature by updating irregular regions (to create non-rectangular child widgets), or painting with colors that have less than full alpha component.

QWidget - Qt for Python

「①Qt.4.1からは、親ウィジェットの内容はQt::WA_PaintOnScreenがセットされていない限り、それぞれの子に伝達されていきます。②カスタムウィジェットは、不規則な領域を更新する(長方形以外の子ウィジェットを作成する)か、③完全なアルファコンポーネント未満の色でペイントすることで、④この機能を利用するように記述できます。」

言っていることが今いちよくわからないですね。

①Qt.WA_PaintOnScreenをセットすると、親から子に伝播する内容が、伝播されなくなるようです。で、その伝播する内容って何なのでしょうか?
こちらに、Qt.WA_PaintOnScreenの説明が書かれています。

https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#qt

Indicates that the widget wants to draw directly onto the screen. Widgets with this attribute set do not participate in composition management, i.e. they cannot be semi-transparent or shine through semi-transparent overlapping widgets. Note: This flag is only supported on X11 and it disables double buffering. On Qt for Embedded Linux, the flag only works when set on a top-level widget and it relies on support from the active screen driver. This flag is set or cleared by the widget’s author. To render outside of Qt’s paint system, e.g., if you require native painting primitives, you need to reimplement paintEngine() to return 0 and set this flag.

Qt - Qt for Python

「ウィジェットがスクリーン上へ直接描くことを指示します。このアトリビュートを持つウィジェットは合成マネジメントの影響を受けません。それらは、半透明であることはできないし、重ね合わせのウィジェットを通して半透明にもならないし、光沢を発することもありません。注意点:このフラグはX11でのみサポートされています。そして、ダブルバッファリングを無効にします。組み込みLinuxのためのQt上、このフラグはトップレベルウィジェットにセットされるときにのみ動作し、アクティブなスクリーンドライバからのサポートに依存します。このフラグは、ウィジェットの作成者によってセットされるか、クリアされます。Qtのペイントシステム外で表示するために、もしネイティブペイントプリミティブを必要とするのであれば、0を戻すためにpaintEngineメソッドを再実装し、このフラグをセットする必要があります。」

どうもX11だけでサポートされているので、Windowsは関係ないようです。

②そもそも、長方形以外の子ウィジェットを作成する…ということはできるのでしょうか?ウィジェットは必ず全て長方形(あるいは正方形)であるはずです。本書のQWidgetの冒頭にも、QWidgetは全て矩形であるという文言が書かれています。

考えられるとすれば、QRegionによるクリッピングです。QRegionをQt.Ellipseを指定してセットすると、円形のウィジェットを描画できます。

③「完全なアルファコンポーネント未満の色」というのは、例えば、QColorでは、RGBAといって、赤、緑、青、アルファ値(透過情報)の4つのパラメータを指定します。このアルファ値が255だと、不透明。0に近づくほど、透明になります。つまり、完全なアルファコンポーネント未満の色、とは255未満、つまり0~254を指すと推定されます。あるいは0か255以外?

④「この機能を利用するように記述できます。」という意味ですが、この機能はダブルバッファリングのことでしょうか。透過という意味であるならばもう少し意味がわかります。

 ちょっとよくわからない箇所ではあるのですが、コードを書いてみます。本当に適当なコードです。

from PySide6.QtWidgets import QApplication,QWidget,QPushButton

from PySide6.QtGui import QPainter, QPolygon, QColor, QRegion, QFont

from PySide6.QtCore import Qt, QRect, QPoint, QSize

import sys

class ParentWidget(QWidget):

    def __init__(self, parent=None):

        super().__init__(parent)

        self.setAttribute(

        Qt.WA_TranslucentBackground, True

        )

        self.setWindowFlags(Qt.FramelessWindowHint)

        self.setAttribute(Qt.WA_PaintOnScreen, True)

        self.setFont(QFont("Times New Roman", 18))

    def paintEvent(self, event):

        painter = QPainter()

        painter.begin(self)

        painter.drawText(

        self.rect(), "parentparentparent", Qt.AlignTop

        )

        painter.end()

        painter = QPainter()

        painter.begin(self)

        brush = painter.brush()

        brush.setStyle(Qt.BDiagPattern)

        brush.setColor(QColor(0, 0, 255, 40))

        painter.setBrush(brush)

        region = QRegion(QRect(

        self.rect().center()-

        QPoint(250, 250), QSize(500, 500)

        ),

        QRegion.Ellipse)

        painter.setClipRegion(region)

        painter.drawRect(self.rect())

        painter.end()

        return super().paintEvent(event)

class Widget(QWidget):

    def __init__(self, parent=None):

        super().__init__(parent)

        print(self.font())

    def paintEvent(self, event):

        polygon = QPolygon()

        polygon += [QPoint(10, 10), QPoint(30, 30), QPoint(200, 200), QPoint(250, 50),

        QPoint(50, 40), QPoint(10, 10)]

        painter = QPainter()

        painter.begin(self)

        brush = painter.brush()

        brush.setStyle(Qt.SolidPattern)

        brush.setColor(QColor(0, 255, 0, 50))

        painter.setBrush(brush)

        painter.drawPolygon(polygon)

        region = QRegion(QRect(

        self.rect().center()

        -QPoint(30, 50), QSize(60, 60)

        ),

        QRegion.Ellipse)

        painter.setClipRegion(region)

        painter.drawRect(self.rect())

        painter.drawText(

        self.rect(), "childchildchild", Qt.AlignCenter)

        painter.end()

        painter = QPainter()

        painter.begin(self)

        brush = painter.brush()

        brush.setStyle(Qt.FDiagPattern)

        brush.setColor(QColor(255, 0, 0, 40))

        painter.setBrush(brush)

        painter.drawRect(self.rect())

        painter.end()

        super().paintEvent(event)

def main():

    app = QApplication()

    w = ParentWidget()

    w2 = Widget(parent=w)

    w2.resize(300, 300)

    w.resize(500, 500)

    w.show()

    sys.exit(app.exec())

if __name__ == "__main__":
    main()

 自分なりにやりくりしてみたのですが、長方形以外のウィジェットを描く、というのはこれくらいしか思いつきませんでしたし、Qt.WA_TranslucentBackgroundを使うと、長方形以外のウィジェットを描くこともできますが、トップレベルウィジェットにしか効果がなく、子ウィジェットと書かれていることと一致しません。①~④の意味が全部よくわかりませんでした。さらなる研鑽が必要のようです。

The following diagram shows how attributes and properties of a custom widget can be fine-tuned to achieve different effects.

QWidget - Qt for Python

「次の図は、カスタム ウィジェットの属性とプロパティを微調整してさまざまな効果を実現する方法を示しています。」

3つの家?

この図はQWidget - Qt for Pythonよりいただきました。

In the above diagram, a semi-transparent rectangular child widget with an area removed is constructed and added to a parent widget (a QLabel showing a pixmap). Then, different properties and widget attributes are set to achieve different effects:

QWidget - Qt for Python

「上の図では、①領域が削除された半透明の長方形の子ウィジェットが構築され、親ウィジェット(ピックスマップを示すQLabel)に追加されます。次に、さまざまなプロパティとウィジェット属性を設定して、さまざまな効果を実現します。」

領域が削除された半透明の長方形の子ウィジェットが構築され、とありますが、これはどうやって作られたのだろう?というのが最大の疑問です。後で半透明ウィジェットというのは出てきます。Translucentアトリビュートをセットするのですが、半透明にできるのは、トップレベルウィジェットのみです。子ウィジェットは半透明にできないかと思います。また、仮にできたとして、この家の絵の輪郭は描けても、窓やドアに当たる部分を透明にすることはおそらくできないと思います。eraseRectを使ったら、結局autoFillBackgroundの色になるだけです。

何か勘違いをしているのでしょうか。むしろ、緑色の背景部分が子で、家の絵を指しているQLabelが親なのでしょうか。ピックスマップを示すQLabelとあるので、家の絵がピックスマップで作られているとすると、これが親。そして、その後ろに広がっている抹茶色の背景が、逆に言えば子にあたるのでしょうか。ここでうだうだうなっていても仕方がないので、ちょっと保留します。

The left widget has no additional properties or widget attributes set. This default state suits most custom widgets using transparency, are irregularly-shaped, or do not paint over their entire area with an opaque brush.

QWidget - Qt for Python

「左のウィジェットは、追加のプロパティが全くない、あるいは、ウィジェットのアトリビュートが全くない状態です。このデフォルトの状態は、透明色を利用していたり、不規則な形であったり、あるいは不透明なブラシで全体のエリアを塗らない、といったようなほとんどのカスタムなウィジェットに適しています。」

The center widget has the autoFillBackground property set. This property is used with custom widgets that rely on the widget to supply a default background, and do not paint over their entire area with an opaque brush.

QWidget - Qt for Python

「中央のウィジェットはautoFillBackgroundプロパティがセットされています。このプロパティは①デフォルトの背景を提供するウィジェットに依存し、②そして全体の領域を不透明なブラシで塗りつぶさないカスタムウィジェットと共に利用されます。」

①autoFillBackgroundは、QPaletteオブジェクトと関わっています。QWidgetを継承しているクラスは、みなpaletteメソッドを持ち、QPaletteオブジェクトを返します。QPaletteオブジェクトは、GUIのコンポーネントに共通した部品と、その部品に対応する色を格納しておくことのできるコンテナクラスです。共通した部品は、ColorRoleという列挙体で区別されています。Windowとか、WindowTextとかです。Windowは、backgroundRoleとして指定されています。つまり、WindowというColorRoleは、薄灰色が指定されています。したがって、デフォルトの背景は薄灰色になるのです。これが、「デフォルトの背景を提供するウィジェットに依存する」、という意味です。もちろん、WindowというColorRoleに対応する色を変えて、パレットを再セットすれば、autoFillBackgroundで塗られる色は、薄灰色からその色へ変わることになるのでしょう。

②全体の領域を不透明なブラシで塗りつぶさないカスタムウィジェットとは?

例えばpaintEvent内でQPainterを使って何かを描いた瞬間、そのウィジェットはカスタムな内容のウィジェットになることはお伝えいたしました。例えば、全体の領域を不透明なブラシで塗りつぶしてしまうと、その塗りつぶした色しか表示されなくなります。そうなると、autoFillBackgroundの意味がなくなります。透明が0だとすると、不透明は255なので、例えばこの数値が0に近いほど、塗りつぶした色以外の色(autoFillBackgroundの色)も表示されるようになるはずです。つまり、全体の領域を0~254の範囲で塗りつぶしたカスタムウィジェット、あるいは、255でも一部しか塗りつぶしていないウィジェットが、全体の領域を不透明なブラシで塗りつぶさないカスタムウィジェット、ということになります。とはいえ、254はほぼ不透明なので、ほとんどわからないでしょうけれども。

The right widget has the WA_OpaquePaintEvent widget attribute set. This indicates that the widget will paint over its entire area with opaque colors. The widget’s area will initially be uninitialized, represented in the diagram with a red diagonal grid pattern that shines through the overpainted area. The Qt::WA_OpaquePaintArea attribute is useful for widgets that need to paint their own specialized contents quickly and do not need a default filled background.

QWidget - Qt for Python

「右のウィジェットはWA_OpaquePaintEventウィジェットがアトリビュートとしてセットされている場合です。これはウィジェットが全体の領域を不透明な色で塗りつぶすことになります。このウィジェットの領域は、最初は初期化されません。赤い斜線グリッドパターンのダイアグラム内で表現され、上塗りされた領域を通してはっきりと出てきます。」

To rapidly update custom widgets with simple background colors, such as real-time plotting or graphing widgets, it is better to define a suitable background color (using setBackgroundRole() with the Window role), set the autoFillBackground property, and only implement the necessary drawing functionality in the widget’s paintEvent() .

QWidget - Qt for Python

「Qt::QA_OpaquePaintAreaアトリビュートがそれら自身の指定された内容を素早く描く必要がある、①デフォルトで塗りつぶされた背景を必要としないウィジェットのために有用です。」

①ウィジェットは灰色で塗りつぶされています。これがデフォルトの状態です。しかし、この灰色背景の描画にも、一定の時間がかかります。この時間さえも節約したい場合があります。例えば、ビデオストリーミングをその領域に表示する、というような場合です。そういう場合はこのアトリビュートをセットし、背景色を塗りつぶす過程をパスします。QMultimediaWidgetを利用するとわかるように、最初の画面は真っ黒のスクリーンになります。どうしていつも真っ黒なスクリーンになっているのかというと、そこで動画を表示するのにウィジェットの初期背景がいちいち描画されないようにするためなのです。

To rapidly update custom widgets that constantly paint over their entire areas with opaque content, e.g., video streaming widgets, it is better to set the widget’s WA_OpaquePaintEvent , avoiding any unnecessary overhead associated with repainting the widget’s background.

QWidget - Qt for Python

 「単純な背景色でカスタムウィジェットを素早く更新するためには、例えば、プロッティングやグラフウィジェットのようなものですが、WindowロールでsetBackgroundRoleを使い、適切な背景色を定義するのが良いです。後は、ウィジェットのpaintEvent()内で、必要な描画機能を実装するだけです。」

To rapidly update custom widgets that constantly paint over their entire areas with opaque content, e.g., video streaming widgets, it is better to set the widget’s WA_OpaquePaintEvent , avoiding any unnecessary overhead associated with repainting the widget’s background.

QWidget - Qt for Python

 「不透明な内容で全体の領域を継続して上塗りするカスタムウィジェットを素早く更新するには、例えば、ビデオストリーミングのウィジェットなどですが、ウィジェットのWA_OpaquePaintEventをセットするのがよいです。ウィジェットの背景を再ペイントするような不必要なオーバーヘッドを避けることができます。」

 PySide6のexamplesフォルダ内のmultimediaファイルの中に、player.pyというファイルがあります。それを見ていただけると、この状態でウィジェットが表示されることがよくわかります。皆さんがお使いのビデオストリーミング用アプリも、大抵このようになっているのではないでしょうか。
 

player.py

(これがexamplesフォルダ内のplayer.pyの実行結果です。)

If a widget has both the WA_OpaquePaintEvent widget attribute and the autoFillBackground property set, the WA_OpaquePaintEvent attribute takes precedence. Depending on your requirements, you should choose either one of them.

QWidget - Qt for Python

「WA_OpaquePaintEventウィジェットアトリビュートと、autoFillBackgroundプロパティが両方ともセットされているならば、WA_OpaquePaintEventアトリビュートが優先します。どちらか一方を目的に合わせて選択します。」

Since Qt 4.1, the contents of parent widgets are also propagated to standard Qt widgets. This can lead to some unexpected results if the parent widget is decorated in a non-standard way, as shown in the diagram below.

QWidget - Qt for Python

 「Qt4.1以来、①親ウィジェットの内容は標準のQtのウィジェットへ伝播します。これは、②もしその親ウィジェットが標準的ではない方法によって装飾されるのであれば、予想外の結果を引き起こす可能性があります。下のダイアグラムに示されているように。」


①標準=カスタムではない、という意味だとすると、親から子へそのまま伝播する、ということでしょう。

②標準的ではない方法によって装飾される、というのもよくわかりません。つまり、カスタムな内容だということでしょう。最初に疑問として書かせていただきました、親ウィジェットの内容が標準のQtのウィジェットへ伝播します。とか、親ウィジェットの内容が子へ伝播していく、という言葉の意味ですが、本来であれば、子ウィジェットは子ウィジェットとして、右のautoFillBackgroundがセットされている場合のように、独立した背景色を持つはずです。しかし、そうはならず、親の背景色と同じ背景色を子ももつのだ、ということを言っているのでしょう。この図が示してあるように、そう推測するしかありません。

The scope for customizing the painting behavior of standard Qt widgets, without resorting to subclassing, is slightly less than that possible for custom widgets. Usually, the desired appearance of a standard widget can be achieved by setting its autoFillBackground property.

QWidget - Qt for Python

「①サブクラス化に頼らずに標準のQtウィジェットの描画動作をカスタマイズする範囲は、カスタムウィジェットで可能な範囲よりもわずかに小さくなります。通常、標準ウィジェットの望ましい外観は、その autoFillBackground プロパティを設定することで実現できます。」 

①普通はサブクラス化を行い、paintEventをオーバーライドすることで、そこでカスタムな内容を記述できます。これは、サブクラス化をした方が、サブクラス化していないよりも、よりカスタマイズできる範囲が(わずかに)広がりますよ、ということでしょう。
autoFillBackgroundは、クラス外部からでもセットできますし、パレットなども同じでしょうから。

透過とダブルバッファリングまとめ

 私だけかもしれませんが、個人的にここの内容は非常にわかりにくいな~というのが感想でした。 

 全体的に、この3つの図を説明しているだけなのだと思います。例えば、背景のウィジェットの上に、家の絵が描いてある子ウィジェットが載っているとき、背景のウィジェットの背景は、子ウィジェットの背景にもなる。これが左側。もし子ウィジェットにsetAutoFillBackgroundが付けられていると、真ん中のようになり、Qt.WA_OpaquePaintEventがアトリビュートとしてセットされていると、たちまちは背景が真っ黒になります。その後paintEventで描画を行った結果、右の図のようになっているということです。autoFillBackgroundとWA_OpaquePaintEventは競合関係にあり、WA_OpaquePaintEventが優先されるのでした。

では、コードで見てみましょう。

例えば、これがデフォルトの状態です。一番左の時です。

from PySide6.QtWidgets import QWidget, QLabel, QApplication

from PySide6.QtGui import QPixmap, QPainter, QColor, QPolygon, QPalette, QFont

from PySide6.QtCore import Qt, QRect, QPoint, QSize

import sys

class ChildWidget(QLabel):

    def __init__(self, parent=None):

        super().__init__(parent)

    def sizeHint(self):

        return QSize(100, 100)

class Widget(QWidget):

    def __init__(self, parent=None):

        super().__init__(parent)

        self.childWidget = ChildWidget(parent=self)

        self.childWidget.move(100, 100)

        self.childWidget.repaint()

    def paintEvent(self, event):

        painter = QPainter()

        painter.begin(self)

        painter.drawTiledPixmap(

        self.rect(),

        QPixmap("propagation-custom_tile.png")

        )

        painter.end()

        return super().paintEvent(event)

def main():

    app = QApplication()

    w = Widget()

    w.show()

    sys.exit(app.exec())

if __name__ == "__main__":
    main()

利用している画像はこちらです。

propagation-custom_tile.png

  

実は中心に子ウィジェットがあります


これがデフォルトの状態です。この上に子ウィジェットがあるはずなのですが、親の背景と完全に溶け込んでしまっていますね。

 次は、setAutoFillBackgroundを使ってみましょう。

class ChildWidget(…

self.setAutoFillBackground(True)

 これだけを加えてみます。


autoFillBackground

  こうなります。ここに子ウィジェットがあるんだな、というのがはっきりとわかりますね。では次に、WA_OpaquePaintEventをセットしてみましょう。

class ChildWidget(…

self.setAttribute(Qt.WA_OpaquePaintEvent)

 これだけを加えます。


WA_OpaquePaintEvent

 真っ黒になりましたね。つまり、親の背景がいちいち描画されなくなるのです。デフォルトの背景色である灰色も描画されません。ですから、動画を描くときには最適な状態になっている、ということになりそうです。この上でペイントなどを描いてみます。ビデオストリーミングのプレイヤーと同じ画面です。

def paintEvent(self, event):

    painter = QPainter()

    painter.begin(self)

    painter.drawPixmap(self.rect(),self.pixmap)

    painter.end()

    return super().paintEvent(event)


利用された画像

利用した画像はこちらです。


default

これがデフォルトの状態。setAutoFillBackgroundも、Qt.WA_OpaquePaintEventもつけられてない状態です。

 では、それぞれつけて調べてみましょう。もう結果はわかっておられると思いますけれども。


autofillbackground

これがsetAutoBackgroundがTrueの場合ですね。

 お次は、


WA_OpaquePaintevent

 こうですね。

Qtのサンプル画像では、背景がwhiteで塗りつぶされ、赤い斜線がブラシで引かれているものが載せられています。そういう細かい所の説明がないので、ちょっとわかりにくかったかもしれません。

 こういうの、サンプルコードがどこかにあるのでしょうか?!これは出来合いの画像を用いましたが、ペイントなどを駆使すれば、こうした画像が描ける・・・窓とドアの部分が透明になる方法がある、というのであれば、教えていただきたいものですね。

 では、最後にサンプル画像のようにしてしまいましょう。

def paintEvent(self, event):
    painter = QPainter()

    painter.begin(self)

    brush = painter.brush()

    brush.setColor(Qt.white)

    brush.setStyle(Qt.SolidPattern)

    painter.setBrush(brush)

    painter.drawRect(self.rect())

    brush = painter.brush()

    brush.setColor(Qt.red)

    brush.setStyle(Qt.FDiagPattern)

    painter.setBrush(brush)

    painter.drawRect(self.rect())

    painter.drawPixmap(self.rect(),self.pixmap)

    painter.end()

    return super().paintEvent(event)

半透明のウィンドウを作る

 https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#creating-translucent-windows(QWidget - Qt for Python)

Since Qt 4.5, it has been possible to create windows with translucent regions on window systems that support compositing.

QWidget - Qt for Python

Qt4.5以降、構成をサポートするウィンドウシステム上の半透明領域を使ってウィンドウを作ることが可能になりました。」

To enable this feature in a top-level widget, set its WA_TranslucentBackground attribute with setAttribute() and ensure that its background is painted with non-opaque colors in the regions you want to be partially transparent.

QWidget - Qt for Python

 「トップレベルウィジェットでこの特徴を可能にするには、WA_TranslucentBackgroundをセットする。①部分的に透明であってほしいという部分で、不透明ではないカラーでその背景がペイントされることを保証する。」

Platform notes:
X11: This feature relies on the use of an X server that supports ARGB visuals and a compositing window manager.
Windows: The widget needs to have the FramelessWindowHint window flag set for the translucency to work.
macOS: The widget needs to have the FramelessWindowHint window flag set for the translucency to work.

QWidget - Qt for Python

「プラットフォームノート:

X11:この特徴はARGB外観と、構成しているウィンドウマネージャをサポートしているXサーバーの利用によります。

 Windowsでこれを行うには、FramelessWindowHintのフラグをセットする必要があります。

 macOsでこれを行うには、FramelessWindowHintのフラグをセットする必要があります。

こちらにこのフラグの説明があります。

https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#qt

Indicates that the widget should have a translucent background, i.e., any non-opaque regions of the widgets will be translucent because the widget will have an alpha channel. Setting this flag causes WA_NoSystemBackground to be set. On Windows the widget also needs the FramelessWindowHint window flag to be set. This flag is set or cleared by the widget’s author. As of Qt 5.0, toggling this attribute after the widget has been shown is not uniformly supported across platforms. When translucent background is desired, set the attribute early when creating the widget, and avoid altering it afterwards.

https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#qt

「そのウィジェットが半透明の背景を持つことを示す。そのウィジェットがアルファチャンネルを持つために、ウィジェットの不透明ではない部分が半透明になる。①このフラグをセットすると、WA_NoSystemBackgroundがセットされる原因になる。Windows(OS)ではそのウィジェットはFramelessWindowHintのフラグをセットする必要がある。このフラグはウィジェットの作成者によってセットされ、あるいはクリアされる。Qt5.0の時点では、ウィジェットを表示した後でこれを切り替えることは複数のプラットフォームを通して統一的にサポートされていない。半透明の背景を望むのであれば、このウィジェットを作るときに早めにこのアトリビュートをセットし、後からこれを変更することを避ける。」

 芋づる式にまたわからないアトリビュートが出てきましたね。

①WA_NoSystemBackground

Indicates that the widget has no background, i.e. when the widget receives paint events, the background is not automatically repainted. Note: Unlike WA_OpaquePaintEvent, newly exposed areas are never filled with the background (e.g., after showing a window for the first time the user can see “through” it until the application processes the paint events). This flag is set or cleared by the widget’s author.

Qt - Qt for Python

「①ウィジェットが背景を持たないことを示す。②ウィジェットがペイントイベントを受け取るとき、背景は自動的に再描画されない。注意点:③WA_OpaquePaintEventとは違い、新しく晒された領域は決して背景で満たされることは無い。(例えば、最初にウィンドウを表示した後、ユーザーはアプリケーションがペイントイベントを手続きするまでそれを透けてみることができる。)このフラグはウィジェットの作成者によって、セットされるかクリアされる。」

①画面が真っ黒になります。autoFillBackgroundフラグというのがありましたが、普通はデフォルトで背景が灰色に塗りつぶされます。この処理には一定の時間がかかります。よって、そのオーバーヘッドがなくなります。

②paintEventなどを呼ぶとき、一旦autoFillBackgroundで指定された背景が塗られ、そのあとでpaintEventの内容を描画する、という意味で2度手間になっているのがデフォルトの状態です。しかし、このフラグを付けておくと、最初の背景がぬられる段階がなくなるようです。

③似たような機能を持つアトリビュートに、Qt.WA_OpaquePaintEventというのがあります。新しく晒された領域でも決して背景が満たされることはないようです。

 TranslucentBackgroundを使うには、WA_TranslucentBackgroundをセットすること。そして、FramelessWindowHintのフラグをセットする、この2つが必要みたいです。ではやってみましょう。

 

from PySide6.QtWidgets import QApplication,QWidget, QPushButton, QVBoxLayout

from PySide6.QtGui import QPainter, QColor

from PySide6.QtCore import Qt

import sys

class Widget(QWidget):

    def __init__(self, parent=None, flags=Qt.FramelessWindowHint):

        super().__init__(parent, flags)

        self.setAttribute(

        Qt.WA_TranslucentBackground, True

        )

        self.setGeometry(300, 300, 500, 500)

    def paintEvent(self, event):

        painter = QPainter()

        painter.begin(self)

        painter.fillRect(

        self.rect(), QColor(128, 128, 128, 100)

        )

        painter.end()

        return super().paintEvent(event)

    def keyPressEvent(self, event):

        if event.key() == Qt.Key_Escape:

            self.close()

            return

        return super().keyPressEvent(event)

def main():

    app = QApplication()

    w = Widget()

    p1 = QPushButton("Button1")

    p2 = QPushButton("Button2")

    v = QVBoxLayout()

    v.addWidget(p1)

    v.addWidget(p2)

    w.setLayout(v)

    w.show()

    sys.exit(app.exec())

if __name__ == "__main__":

    main()

 

半透明ウィジェット


確かに透明になっています。後ろのネコは、背景画像で、このアプリとは全く関係ありません。Flamelessでクローズボタンがないため、ウィジェットはEscapeキーを押して消してください。半透明と書かれていたので、半分は不透明なんだろうな、と思っていたのですが、完全透明でした。コードを実行し、何もエラーが起きていないのに何も表示されませんでした。そこで、paintEventをオーバーライドして、アルファ値をいじってようやく今のような状態になりました。

①「部分的に透明であってほしいという部分で、不透明ではないカラーでその背景がペイントされることを保証する。」

という言葉の意味が、ここでようやくわかりました。

今は全体を透明にしていますが、「部分的に透明であってほしいという部分で」、という文言から、部分的に透明にするのが本来の目的のようです。

 これがTransluscentの力なのでしょうけれども、フレームがない場合でしか利用ができない、というのはちょっと残念ですね。タイトルバーなどがなくなるのは、今の仕様だと仕方がないようです。そういう場合は、自分で作ってしまいましょう。 

ネイティブウィジェットとエイリアンウィジェット

 https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#native-widgets-vs-alien-widgets(QWidget - Qt for Python)

特定のシステム用であることをネイティブ、それ以外のウィジェットをエイリアンウィジェットというみたいです。

Introduced in Qt 4.4, alien widgets are widgets unknown to the windowing system. They do not have a native window handle associated with them. This feature significantly speeds up widget painting, resizing, and removes flicker.
Should you require the old behavior with native windows, you can choose one of the following options:
Use the QT_USE_NATIVE_WINDOWS=1 in your environment.
Set the AA_NativeWindows attribute on your application. All widgets will be native widgets.
Set the WA_NativeWindow attribute on widgets: The widget itself and all of its ancestors will become native (unless WA_DontCreateNativeAncestors is set).
Call winId to enforce a native window (this implies 3).
Set the WA_PaintOnScreen attribute to enforce a native window (this implies 3).

QWidget - Qt for Python

 「Qt4.4で紹介されたのだが、エイリアンウィジェットはウィンドウのシステムでは知られていないウィジェットである。

 エイリアンウィジェットは、それらに関連しているネイティブハンドルを持たない。この特徴はウィジェットの描画、リサイズ、そしてちらつきを防ぐということで、かなりのスピードアップをすることができる。

 仮にネイティブウィジェットで古い動作を求めるのであれば、次のオプションのうち、どれか一つを使うこと。

1.QT_USE_NATIVE_WINDOWS=1を環境内で使うこと

2.AA_NativeWindowsアトリビュートをアプリケーションにセットすること。全てのウィジェットがネイティブウィジェットになる。

3.WA_NativeWindowアトリビュートをウィジェットにセットすること。ウィジェットそれ自体とその子孫の全てはネイティブウィジェットとなる。(もしWA_DontCreateNativeAncestorsがセットされていないのであれば)

4.winIdをネイティブウィンドウに強制する。idは3を意味する。

5.WA_PaintOnScreenアトリビュートをネイティブウィンドウへ強制する(これは3を意味する)」

 おそらく1はC++のQtでしかできないのではないか…と思われます。

Qt.AA_NativeWindows

Ensures that widgets have native windows.

QWidget - Qt for Python

ウィジェットがネイティブウィンドウを持つことを保証します。

Qt.WA_NativeWindow

Indicates that a native window is created for the widget. Enabling this flag will also force a native window for the widget’s ancestors unless Qt::WA_DontCreateNativeAncestors is set.

QWidget - Qt for Python

「ネイティブウィンドウはウィジェットのために作られることを示します。このフラグを可能にするとQt.WA_DontCreateNativeAncestorsがセットされないのであれば、ウィジェットの継承元クラスがネイティブウィンドウであることを強制します。」

4は、winIdというメソッドがありますから、それをオーバーライドして、3を返せということでしょう。
 このwinIdというのは、作成するウィジェットごとに全く違う番号が割り当てられ、ランダムのようです。OSがウィジェットを識別するためにつけておく番号になります。

5は、既にみていただいたようにX11のみ通用します。Qt.WA_PaintOnScreenによってこの切り替えができるということは、これらのオプションはいずれもX11のみで通用するものなのでしょうか・・・。

今のところ申し上げることができるとすれば、Qtも、Windows等のプラットフォーム固有の仕組みを利用してここまでのコードを積み重ねてきているということです。Qtによらなくても、例えばWindowsのWindows APIを使えば、GUIアプリケーションを作ることができます。

 QWindowを継承して作られたクラスにQRasterWindowというのがあります。リファレンスを読むと、全ての描画がCPUで行われるもののようです。これとほとんど近似した内容のRasterWindow Exampleというのがありますので、これを機会に載せておきたいと思います。

from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QPainter, QGradient, QRasterWindow, QBackingStore,QWindow
from PySide6.QtCore import  QEvent, QRect, QRectF, Qt
import sys

class RasterWindow(QWindow):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.m_backingStore = QBackingStore(self)
        self.setGeometry(100, 100, 300, 200)
        
    def render(self, painter):

        painter.drawText(QRectF(0, 0, self.width(), self.height()),
                         Qt.AlignCenter,
                         "QWindow")        

    def renderLater(self):

        self.requestUpdate()

    def renderNow(self):
        if not self.isExposed():
            return

        rect = QRect(0, 0, self.width(), self.height())
        self.m_backingStore.beginPaint(rect)

        device = self.m_backingStore.paintDevice()
        painter = QPainter(device)

        painter.fillRect(0, 0, self.width(), self.height(), QGradient.NightFade)
        self.render(painter)
        painter.end()

        self.m_backingStore.endPaint()
        self.m_backingStore.flush(rect)
        

    def event(self, event):
        if event.type() == QEvent.UpdateRequest:
            self.renderNow()
            return True
        return super().event(event)

    def resizeEvent(self, event):

        self.m_backingStore.resize(event.size())

    def exposeEvent(self, event):
        if self.isExposed():
            self.renderNow()
            
def main():

    app = QApplication([])
    window = RasterWindow()
    window.show()

    sys.exit(app.exec())

if __name__ == "__main__":
    main()

QWindowは、QWidgetやQGraphicsViewでの描画と異なり、ダブルバッファリングどころか、トリプルバッファリングまで行うので、メモリを使うようです。QBackingStoreというのがQPainterとバッファを管理しているようです。メモリは食うけど高速で動作するタイプのウィンドウなのでしょう。

この章で書かれていることは全体的によくわからないものでした。ここもさらなる研鑽が必要な箇所のようです。

特にWin API等、OS側のGUIの知識があった方が理解しやすいと思います。

まとめ

 ここでQWidgetリファレンスの概要を見てみました。私はQtを学んできて、QWidgetをここまで調べたことはなく、自分の目的とするアプリに向けて必要なクラスをかいつまんでやりくりしてきました。しかし、改めて全てを見通してみると、今までなんとなく理解していたことが多くあることに気が付きました。ただ、この中で初心者にとって頻出で重要な箇所と言えば、親子関係、イベントまでの前半部分です。ここらあたりを最初にしっかりと学んでおくと、後で混乱することがなくてよかったなと思います。ネイティブウィジェットとエイリアンウィジェットのについては、長いことPySideを触ってきた今も、全くと言っていいほど利用したことがありません。透過とダブルバッファリングは、自分はあまり使わなかったのですが、知っておいた方がいいと思います。

もしよろしかったら書籍を読んでくださいね。お勧め書籍です。



この記事が気に入ったらサポートをしてみませんか?