【PyQt】Qt Designerで作った画面にQMLを埋め込み、valueをセット

# はじめに
　普段ちょっとしたGUIを作るときに[PyQt](https://ja.wikipedia.org/wiki/PyQt)を使っているのですが、[QML](https://ja.wikipedia.org/wiki/QML)の表示やプロパティの変更に手こずったので、その備忘録です。
　画面作成にはQtDesignerを使っています。QtDesignerの使い方は[こちら](https://qiita.com/grinpeaceman/items/54b439bfa52640c444e1)を参考に。

# 環境
+ Windows 10
+ Qt Designer 5.11.1
+ Python 3.7.3
+ PyQt5 5.13.1

# 作成したサンプル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404447/5b728ea7-59c3-4e94-4211-14e55678fa3c.png)

+ボタンを押すとゲージの値が増え、-ボタンを押すと減るというものです。

# ファイルの構成
+ qml_gauge_sample_gui.py
    + 実行ファイル
+ MyGauge.qml
    + QMLファイル
+ my_gauge.py
    + QQuickWidgetを継承し、ソースにMyGauge.qmlをセット
+ qml_gauge_sample.ui
    + Designerで作成したuiファイル
+ qml_gauge_sample_ui.py
    + uiファイルからpyuicコマンドで作成したPythonファイル

# 各ファイルの詳細
## ①
```qml:MyGauge.qml
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Quick Extras module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Controls.Private 1.0
import QtQuick.Extras 1.4
import QtQuick.Extras.Private 1.0

Gauge {
    minimumValue: 0
    value: gauge.value
    maximumValue: 100
    tickmarkStepSize: 10
    orientation: Qt.Vertical
}

```

QtQuick.ExtrasのGaugeを使用します。
valueの値をgauge.valueとして、プログラムから更新できるようにします。

## ②
```python:my_gauge.py
import os

from PyQt5.QtCore import pyqtProperty
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import QObject
from PyQt5.QtCore import QUrl
from PyQt5.QtQuickWidgets import QQuickWidget


class GaugeProperty(QObject):
    _value_changed_signal = pyqtSignal(int)

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

    @pyqtProperty(int, notify=_value_changed_signal)
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        if self._value == value:
            return
        self._value = value
        self._value_changed_signal.emit(value)


class MyGauge(QQuickWidget):
    def __init__(self, parent):
        super(MyGauge, self).__init__(parent)
        self._gauge = GaugeProperty(self)
        self.setResizeMode(self.SizeRootObjectToView)
        self.engine().rootContext().setContextProperty("gauge", self._gauge)
        directory = os.path.dirname(os.path.abspath(__file__))
        self.setSource(QUrl.fromLocalFile(os.path.join(directory,
                                                       "MyGauge.qml")))

    def increment(self):
        self._gauge.value += 1

    def decrement(self):
        self._gauge.value -= 1

```
　QQuickWidgetを継承したMyGaugeクラスと、ゲージのvalueプロパティを管理するGaugePropertyクラスを作成します。
　MyGaugeクラスは、画面作成時に[格上げ](https://qiita.com/grinpeaceman/items/b3e2652325b5cdc2a42f)の対象先となります。

## ③
```qml_gauge_sample.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>GaugeSampleWidget</class>
 <widget class="QWidget" name="GaugeSampleWidget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>94</width>
    <height>335</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <widget class="MyGauge" name="myGauge" native="true">
   <property name="geometry">
    <rect>
     <x>10</x>
     <y>10</y>
     <width>71</width>
     <height>291</height>
    </rect>
   </property>
  </widget>
  <widget class="QWidget" name="horizontalLayoutWidget">
   <property name="geometry">
    <rect>
     <x>10</x>
     <y>300</y>
     <width>71</width>
     <height>31</height>
    </rect>
   </property>
   <layout class="QHBoxLayout" name="_horizontalLayout">
    <item>
     <widget class="QPushButton" name="_plusButton">
      <property name="font">
       <font>
        <pointsize>9</pointsize>
        <weight>75</weight>
        <bold>true</bold>
       </font>
      </property>
      <property name="text">
       <string>+</string>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QPushButton" name="_minusButton">
      <property name="font">
       <font>
        <weight>75</weight>
        <bold>true</bold>
       </font>
      </property>
      <property name="text">
       <string>-</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <customwidgets>
  <customwidget>
   <class>MyGauge</class>
   <extends>QWidget</extends>
   <header>my_gauge.h</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections>
  <connection>
   <sender>_plusButton</sender>
   <signal>pressed()</signal>
   <receiver>GaugeSampleWidget</receiver>
   <slot>increment_gauge_value()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>28</x>
     <y>314</y>
    </hint>
    <hint type="destinationlabel">
     <x>159</x>
     <y>310</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>_minusButton</sender>
   <signal>pressed()</signal>
   <receiver>GaugeSampleWidget</receiver>
   <slot>decrement_gauge_value()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>68</x>
     <y>313</y>
    </hint>
    <hint type="destinationlabel">
     <x>69</x>
     <y>394</y>
    </hint>
   </hints>
  </connection>
 </connections>
 <slots>
  <slot>increment_gauge_value()</slot>
  <slot>decrement_gauge_value()</slot>
 </slots>
</ui>

```
Qt Designerで作成したuiファイル。
![gauge_sample_widget.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404447/92d4b67d-098c-bfd7-9625-f8657a58c1f4.png)

![promoted_mygauge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404447/efb25cd6-c0fd-5b3b-b497-96fc38e35f93.png)

MyGaugeクラスに格上げされたQWidgetと、ゲージのインクリメント/デクリメントを行うボタンが二つ配置されています。

## ④
```python:qml_gauge_sample_ui.py
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'qml_gauge_sample.ui'
#
# Created by: PyQt5 UI code generator 5.13.1
#
# WARNING! All changes made in this file will be lost!


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_GaugeSampleWidget(object):
    def setupUi(self, GaugeSampleWidget):
        GaugeSampleWidget.setObjectName("GaugeSampleWidget")
        GaugeSampleWidget.resize(94, 335)
        self.myGauge = MyGauge(GaugeSampleWidget)
        self.myGauge.setGeometry(QtCore.QRect(10, 10, 71, 291))
        self.myGauge.setObjectName("myGauge")
        self.horizontalLayoutWidget = QtWidgets.QWidget(GaugeSampleWidget)
        self.horizontalLayoutWidget.setGeometry(QtCore.QRect(10, 300, 71, 31))
        self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
        self._horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
        self._horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self._horizontalLayout.setObjectName("_horizontalLayout")
        self._plusButton = QtWidgets.QPushButton(self.horizontalLayoutWidget)
        font = QtGui.QFont()
        font.setPointSize(9)
        font.setBold(True)
        font.setWeight(75)
        self._plusButton.setFont(font)
        self._plusButton.setObjectName("_plusButton")
        self._horizontalLayout.addWidget(self._plusButton)
        self._minusButton = QtWidgets.QPushButton(self.horizontalLayoutWidget)
        font = QtGui.QFont()
        font.setBold(True)
        font.setWeight(75)
        self._minusButton.setFont(font)
        self._minusButton.setObjectName("_minusButton")
        self._horizontalLayout.addWidget(self._minusButton)

        self.retranslateUi(GaugeSampleWidget)
        self._plusButton.pressed.connect(GaugeSampleWidget.increment_gauge_value)
        self._minusButton.pressed.connect(GaugeSampleWidget.decrement_gauge_value)
        QtCore.QMetaObject.connectSlotsByName(GaugeSampleWidget)

    def retranslateUi(self, GaugeSampleWidget):
        _translate = QtCore.QCoreApplication.translate
        GaugeSampleWidget.setWindowTitle(_translate("GaugeSampleWidget", "Form"))
        self._plusButton.setText(_translate("GaugeSampleWidget", "+"))
        self._minusButton.setText(_translate("GaugeSampleWidget", "-"))
from my_gauge import MyGauge

```

`$ pyuic5 qml_gauge_sample.ui > qml_gauge_sample_ui.py`
で作成した、Pythonファイル。

## ⑤
```python:qml_gauge_sample_gui.py
import sys

from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QMainWindow

from qml_gauge_sample_ui import Ui_GaugeSampleWidget


class QmlGaugeSampleGui(QMainWindow, Ui_GaugeSampleWidget):
    def __init__(self, parent=None):
        super(QmlGaugeSampleGui, self).__init__(parent)
        self.setupUi(self)

    @pyqtSlot()
    def increment_gauge_value(self):
        self.myGauge.increment()

    @pyqtSlot()
    def decrement_gauge_value(self):
        self.myGauge.decrement()


if __name__ == '__main__':
    argvs = sys.argv
    app = QApplication(argvs)
    qml_gauge_sample_gui = QmlGaugeSampleGui()
    qml_gauge_sample_gui.show()
    sys.exit(app.exec_())

```
　Designerで作成した、Ui_GaugeSampleWidgetを継承したQmlGaugeSampleGuiクラスを作ります。
　QmlGaugeSampleGuiクラスにはincrement_gauge_value()関数とdecrement_gauge_value()関数を持っており、それぞれ+ボタン、-ボタンが押されると呼ばれるようにDesigner上でSignal/Slotの設定がされています。

あとは、
`$ python qml_gauge_sample_gui.py`
で実行すれば、ゲージQMLが埋め込まれた画面が表示されます。

# 参考URL
+ https://codeday.me/jp/qa/20190428/726754.html
+ https://stackoverflow.com/questions/49930114/how-to-set-values-in-qml-using-pyqt5
+ https://doc.qt.io/qtforpython/PySide2/QtQuickWidgets/QQuickWidget.html#PySide2.QtQuickWidgets.PySide2.QtQuickWidgets.QQuickWidget.ResizeMode
+ https://doc.qt.io/qt-5/qml-qtquick-extras-gauge.html