QtWebEngineのWebページからホスト側の処理を呼び出す

この記事は[Takumi Akashiro ひとり Advent Calendar 2020](https://qiita.com/advent-calendar/2020/takumi_akashiro)の2日目の記事になります。Yah!


## 前置き

どうも！皆さんは最近、PySide2を触ってますか？  
あ、この質問、[昨日の記事](https://qiita.com/takumi_akashiro/private/b3e0825649e78eb2195f)でもしましたね！

茶番はさておき、今日はホストアプリケーション、
すなわちPythonの処理をQtWebEngineのブラウザ側から呼び出す方法について説明いたします。


## QWebChannel

本日使うのはこれ、**QWebChannel！**

QWebChannelはQtWebWidgetsにあるクラスです。

QWebChannelにQObjectに登録し、QWebEngine側に紐づけ、
ブラウザ側でQWebChannel.jsを使うことで
QWebChannelに登録していたQObjectの処理を呼び出すことが出来ます。

……字面だと分かりづらいですね。
シーケンス図で書くとざっくりこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294868/8495bf4e-ffa4-b16c-a9c8-ee9d667dd4bf.png)

こんな図見るより、コード読めば一発ですよ一発！

## 試してみた

```python
#!python3
# encoding:utf-8

from PySide2 import QtCore
from PySide2 import QtWidgets
from PySide2 import QtWebEngineWidgets
from PySide2 import QtWebChannel

html_text = """
<!DOCTYPE html>
<html>
    <head>
        <!--Bootstrap (Option)-->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
        <!--QWebChannel-->
        <script src="qrc:///qtwebchannel/qwebchannel.js"></script>

        <script>
            window.onload = function() {
                new QWebChannel(qt.webChannelTransport, function(channel) {
                    window.manager = channel.objects.web_manager;
                    
                    var $element = document.getElementById("HelloWorld_btn");
                    $element.onclick = function(e) {
                        manager.print_f(e.target.innerText);
                    };

                    var $element = document.getElementById("Number_input");
                    $element.onchange = function(e) {
                        manager.set_number(e.target.value);
                    };
                });
            }
        </script>
    </head>
    <body>
        <button class="btn w-100 btn-primary" id="HelloWorld_btn">Hello World!</button>
        <input class="form-control w-25" type="number" value="0" id="Number_input">
    </body>
</html>
"""


class WebDemo(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setWindowTitle('Web Demo')

        hbox = QtWidgets.QHBoxLayout()

        self.webview = QtWebEngineWidgets.QWebEngineView()

        page = self.webview.page()

        self.channel = QtWebChannel.QWebChannel()
        web_manager = WebManager(self)
        self.channel.registerObject("web_manager", web_manager)
        page.setWebChannel(self.channel)

        page.setHtml(html_text)

        hbox.addWidget(self.webview)
        self.setLayout(hbox)


class WebManager(QtCore.QObject):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.number_ = 0

    @QtCore.Slot(str)
    def print_f(self, text):
        print(f"called: print_f() / text: {text} / number_: {self.number_}")

    @QtCore.Slot(int)
    def set_number(self, num):
        print(f"called: set_number() / number_: {self.number_}")
        self.number_ = num


if __name__ == "__main__":
    app = QtWidgets.QApplication()
    webdemo = WebDemo()
    webdemo.show()

    exit(app.exec_())

```

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294868/d8acaf60-c15f-d3cf-8664-5e7e082434cc.gif)

## Mayaでも試してみた

```python

#!python2
# encoding:utf-8

from PySide2 import QtCore
from PySide2 import QtWidgets
from PySide2 import QtWebEngineWidgets
from PySide2 import QtWebChannel

from maya import cmds
from maya import OpenMayaUI

from shiboken2 import wrapInstance

mayaMainWindowPtr = OpenMayaUI.MQtUtil.mainWindow()
mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtWidgets.QWidget)

html_text = """
<!DOCTYPE html>
<html>
    <head>
        <!--Bootstrap (Option)-->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
        <!--QWebChannel-->
        <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
        <script>
            window.onload = function() {
                new QWebChannel(qt.webChannelTransport, function(channel) {
                    window.manager = channel.objects.web_manager;
                    var $element = document.getElementById("polycube_btn");
                    $element.onclick = function(e) {
                        var callback_print = function(nodename) {
                            var select_btn = document.getElementById("select_btn");
                            select_btn.removeAttribute("disabled");
                            select_btn.value = nodename;
                            select_btn.innerText = "Select: " + nodename
                        };
                        manager.gen_polycube(callback_print);
                    };
                    var $element = document.getElementById("select_btn");
                    $element.onclick = function(e) {
                        var node = e.target.value;
                        manager.select_node(node);
                    };
                    var $element = document.getElementById("select_clear_btn");
                    $element.onclick = function() {
                        manager.select_node("");
                    };
                });
            }
        </script>
    </head>
    <body class="m-3">
        <div class="container">
            <div class="row">
                <button class="col btn w-25 btn-primary m-1" id="polycube_btn">polycube!</button>
                <button class="col btn w-25 btn-secondary m-1" id="select_clear_btn">Select Clear</button>
                <button class="col btn w-25 btn-primary m-1" disabled id="select_btn">Select</button>
            </div>
        </div>
    </body>
</html>
"""


class MayaWebDemo(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super(MayaWebDemo, self).__init__(*args, **kwargs)

        self.setWindowFlags(QtCore.Qt.Window)
        self.setWindowTitle('Maya Web Demo')

        hbox = QtWidgets.QHBoxLayout()

        self.webview = QtWebEngineWidgets.QWebEngineView()

        page = self.webview.page()
        self.channel = QtWebChannel.QWebChannel()
        web_manager = MayaWebManager(self)
        self.channel.registerObject("web_manager", web_manager)
        page.setWebChannel(self.channel)

        page.setHtml(html_text)

        hbox.addWidget(self.webview)
        self.setLayout(hbox)


class MayaWebManager(QtCore.QObject):
    def __init__(self, *args, **kwargs):
        super(MayaWebManager, self).__init__(*args, **kwargs)
        self.text_ = ""

    @QtCore.Slot(result=str)
    def gen_polycube(self):
        result = cmds.polyCube()[0]
        return result

    @QtCore.Slot(str)
    def select_node(self, name):
        if name:
            cmds.select(name)
        else:
            cmds.select(cl=True)


if __name__ == "__main__":
    demo = MayaWebDemo(mayaMainWindow)
    demo.show()

```

![output_maya.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294868/8a86f322-2275-1133-0a1b-945062012ee5.gif)

## Houdiniでも試してみた


```python
#!python2
# encoding:utf-8

from PySide2 import QtCore
from PySide2 import QtWidgets
from PySide2 import QtWebEngineWidgets
from PySide2 import QtWebChannel

html_text = """
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
        <script src=" qrc:///qtwebchannel/qwebchannel.js"></script>

        <script>
            window.onload = function() {
                new QWebChannel(qt.webChannelTransport, function(channel) {
                    window.manager = channel.objects.manager;
                    
                    var $element = document.getElementById("create_box");
                    $element.onclick = function() {
                        manager.create_box();
                    };
                });
            }
        </script>
    
    </head>
    <body>
        <button class="btn w-100 btn-primary" id="create_box">create_box</button>
    </body>
</html>
"""

class HoudiniWebDemo(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super(HoudiniWebDemo, self).__init__(*args, **kwargs)

        self.setGeometry(500, 300, 250, 110)
        self.setWindowTitle('Houdini Web Demo')
        
        hbox = QtWidgets.QHBoxLayout()

        self.webview = QtWebEngineWidgets.QWebEngineView()

        page = self.webview.page()
        manager = HoudiniWebManager(self)
        self.channel = QtWebChannel.QWebChannel()
        
        self.channel.registerObject("manager", manager)
        
        page.setWebChannel(self.channel)
        page.setHtml(html_text)
        

        hbox.addWidget(self.webview)
        
        self.setLayout(hbox)

class HoudiniWebManager(QtCore.QObject):
    def __init__(self, *args, **kwargs):
        super(HoudiniWebManager, self).__init__(*args, **kwargs)

    @QtCore.Slot()
    def create_box(self):
        geo = hou.node('/obj/').createNode('geo', 'geo1')
        geo.createNode('box', 'box1')
        geo.moveToGoodPosition()


demo = HoudiniWebDemo()
demo.show()
```

![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294868/3624e584-3e97-730d-4c85-7c8c6fb33102.gif)

## 締め

というわけで、今回紹介した技術の評価としては以下の通りですね。

|評価ラベル|ランク(5段階)|
| --:| :--: |
|おすすめ度|★★★★|
|難易度|★★★|
|ニッチ|★★★★|
|汎用性|★★★|
|キュート度|★★★★★|

Maya, Houdini共に割と簡単に使えたので、個人的にはおすすめ度が高いですね。
おそらく今回のAdventCalenderの中の記事の中で一番実用的かもしれません。



---

記事自体はスクリプトと画像でお茶を濁した感じですが、
使った感じ将来性がありそうな気がしたので、皆様もぜひ活用してみてください！

ではでは～