がれすたさんのDIY日記

電子回路、Python、組み込みシステム開発、自作エフェクターを語るblog

MCUXpressoSDK USB Device StackでMIDIStreamingSubClassを使う1

MCUXpressoSDK USB Stackの中のAudioクラスにMIDIストリーミングサブクラスを追加してPCからUSB MIDI機器としてとして認識できるようになったという感じです。
めちゃめちゃ長ったらしい説明ですがSDKにもともと入っているaudio_generator_lite_bmというサンプルのusb_device_descriptor.cを修正したというだけです。

公式のサンプルだとサブクラスにAudioコントロールとAudioストリーミングしかマクロとして登録されていないのでMIDIストリーミングサブクラス(0x03)を追加して以下固有ディスクリプタの定義をしたという感じ。
コードは長かったのでGistに上げておきました。

https://gist.github.com/GSMCustomEffects/13318627e3e1ca1ab04aea41609c8a43

数値の意味などはコメントで表記しています。

USBTreeviewというソフトウエアで認識を確認しています。
f:id:gsmcustomeffects:20191014230251p:plain

参考


シンセ・アンプラグドさんはこれ以外にもUSBMIDI関連で多数の記事を書かれていて非常に参考になります。
今回ほとんどコピペさせてもらってます。

MIMXRT10xxのUART DMAメモ

前回は割り込みについてやったので今回はDMAについてメモを書く
参考には公式サンプルのlpuart_edma_transferを使うのでそれを参考にしてください。

公式サンプルが動作コードなので使い方的なのは解説しません

DMA部分のメモ

ポーリング、割り込みとは違いDMAMUXEDMAAPIが新しく出てくるのでそれの説明からしていく。

    /*Initはクロックの供給をしている*/
    DMAMUX_Init(EXAMPLE_LPUART_DMAMUX_BASEADDR);

 /*ソースとチャンネルの設定*/
    DMAMUX_SetSource(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_TX_DMA_CHANNEL, LPUART_TX_DMA_REQUEST);
    DMAMUX_SetSource(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_RX_DMA_CHANNEL, LPUART_RX_DMA_REQUEST);

 /*チャンネルの有効*/
    DMAMUX_EnableChannel(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_TX_DMA_CHANNEL);
    DMAMUX_EnableChannel(EXAMPLE_LPUART_DMAMUX_BASEADDR, LPUART_RX_DMA_CHANNEL);


f:id:gsmcustomeffects:20191012214331p:plain
DMAMUX_SetSourceの設定だが引数がbase,channel,sourceである。
channelはDMAのチャンネルなので好きなチャンネルを設定すればいい
sourceはペリフェラル側なので決まっている値使うペリフェラルごとにMIMXRT1052.hで定義されている。
f:id:gsmcustomeffects:20191012214721p:plain

LPUARTの場合は送受信で2,3を使っている。
DMAMUX_EnableChannelのほうは0,1をチャンネルとして設定している。

    /*デフォルト設定の読み込み*/
    EDMA_GetDefaultConfig(&config);

    /*EDMAの初期化*/
    EDMA_Init(EXAMPLE_LPUART_DMA_BASEADDR, &config);

    /*ハンドル設定/
    EDMA_CreateHandle(&g_lpuartTxEdmaHandle, EXAMPLE_LPUART_DMA_BASEADDR, LPUART_TX_DMA_CHANNEL);
    EDMA_CreateHandle(&g_lpuartRxEdmaHandle, EXAMPLE_LPUART_DMA_BASEADDR, LPUART_RX_DMA_CHANNEL);

EDMAに関してはInitしてHandleの登録するだけ。

LPUART_TransferCreateHandleEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, LPUART_UserCallback, NULL, &g_lpuartTxEdmaHandle,
                                    &g_lpuartRxEdmaHandle);

最後にLPUART_TransferCreateHandleEDMAでUART用のハンドルを作成する。
コールバックやEDMAHandleを登録する。

あとは送受信APIを呼ぶだけ。

LPUART_ReceiveEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &receiveXfer);
LPUART_SendEDMA(DEMO_LPUART, &g_lpuartEdmaHandle, &sendXfer);

コールバックに関してはサンプルと同様にstatus比較で処理分岐するのがいいと思う

void LPUART_UserCallback(LPUART_Type *base, lpuart_edma_handle_t *handle, status_t status, void *userData)
{
    userData = userData;

    if (kStatus_LPUART_TxIdle == status)
    {
        txBufferFull = false;
        txOnGoing = false;
    }

    if (kStatus_LPUART_RxIdle == status)
    {
        rxBufferEmpty = false;
        rxOnGoing = false;
    }
}

まとめ

LPUARTでDMAの使い方をまとめた。
LPUARTにDMA専用APIが用意されているので比較的楽に記述できる点が良いと感じた。

MIMXRT10xxのUART割り込みメモ

UARTの割り込みについて少し迷ったのでメモ代わりに残しておく。
コードに関しては公式のサンプル内にあるlpuart_interrupt_rb_transferってやつを参考に解説しているので全容はそっちを見てください。

送受信APIの確認

まず割り込みには以下に示すの二つのAPIを使うことになる。

LPUART_TransferReceiveNonBlocking(LPUART_Type *base,
                              lpuart_handle_t *handle,
                              lpuart_transfer_t *xfer,
                              size_t *receivedBytes)
LPUART_TransferSendNonBlocking(LPUART_Type *base,
                              lpuart_handle_t *handle, 
                              lpuart_transfer_t *xfer)

handleはLPUART1のような感じでLPUARTモジュールのハンドルを渡す。
xferはデータ構造そのもので

typedef struct _lpuart_transfer
{
    uint8_t *data;   /*!< The buffer of data to be transfer.*/
    size_t dataSize; /*!< The byte count to be transfer. */
} lpuart_transfer_t;

のように定義している。

実際の使い方

実際の使い方はこんな感じ

LPUART_GetDefaultConfig(&config);//デフォルト設定を読み込む
config.baudRate_Bps = BOARD_DEBUG_UART_BAUDRATE;//このマクロだと115200
config.enableTx = true;//送信有効
config.enableRx = true;//受信有効
LPUART_Init(DEMO_LPUART, &config, DEMO_LPUART_CLK_FREQ);//初期化はポーリングと同じ


/*
 *ハンドルの生成
 *この時点でコールバック関数も登録する。
 *この例ではUserdataがNULLになっているがコールバックに何かデータ渡したいならここにポインタを入れる
 */
LPUART_TransferCreateHandle(DEMO_LPUART, &g_lpuartHandle, LPUART_UserCallback, NULL);


/*
 *リングバッファの有効
 *これの有無でLPUART_TransferReceiveNonBlocking内部での処理が変わる(後述)
 */
LPUART_TransferStartRingBuffer(DEMO_LPUART, &g_lpuartHandle, g_rxRingBuffer, RX_RING_BUFFER_SIZE);

/*
 *xfer構造体にバッファの登録をする。
 *データ構造はこの構造体が決めるのでデータプロトコルが複数ある場合は複数用意しておくと可視性が上がる
*/
xfer.data = g_tipString;//バッファは各自用意する
xfer.dataSize = sizeof(g_tipString) - 1;//サイズも各自編集

/*
 * ノンブロッキング送信API
 * 送信完了後コールバックに飛ぶ
 * あとはxfer構造体のバッファーにデータをセットしてこいつをコールすればいい
 *
*/
LPUART_TransferSendNonBlocking(DEMO_LPUART, &g_lpuartHandle, &xfer);

/*
 * ノンブロッキング受信API
 * 受信完了後コールバックに飛ぶ
*/
LPUART_TransferReceiveNonBlocking(DEMO_LPUART, &g_lpuartHandle, &receiveXfer, &receivedBytes);

リングバッファの話についてだがLPUART_TransferReceiveNonBlockingの中にコメント解説がある。

/* How to get data:

       1. If RX ring buffer is not enabled, then save xfer->data and xfer->dataSize
          to lpuart handle, enable interrupt to store received data to xfer->data. When
          all data received, trigger callback.
       2. If RX ring buffer is enabled and not empty, get data from ring buffer first.
          If there are enough data in ring buffer, copy them to xfer->data and return.
          If there are not enough data in ring buffer, copy all of them to xfer->data,
          save the xfer->data remained empty space to lpuart handle, receive data
          to this empty space and trigger callback when finished. 
*/

用はLPUART_TransferStartRingBufferをコールして有効にしていると受信レジスタとxfer->dataの間にリングバッファが挟まるという感じです。
直訳するとリングバッファーに十分なデータがある場合は、それらをxfer-> dataにコピーして戻る。
リングバッファーに十分なデータがない場合は、それらすべてをxfer-> dataにコピーし、xfer-> dataの空きスペースをlpuartハンドルに保存。そのあとこの空きスペースにデータを受信し、終了時にコールバックをトリガーします。

LPUART_TransferReceiveNonBlockingで設定したデータ数が受信されるまでリングバッファとxfer-> data内でやりくりしてくれる。
あとは公式サンプルのようにrxOnGoing変数を監視してLPUART_TransferReceiveNonBlockingをコールしてもいいし

LPUART_TransferReceiveNonBlockingの返り値がステータスなのでそいつを頼りにしてBusyなら再コールするなりで対応すればいいと思います。


タイマーで定期的に受信したいなら

if(g_lpuartHandle.rxState == kStatus_LPUART_RxIdle){........}

みたいな書き方でもいいと思う

PySide2でQtQuick(qml)使うメモ3

今回はPySide2とPySerialを組み合わせて使うメモ

やることとしてはQMLで作成したGUI側でイベントを発生させてシリアルで何か送信してマイコンを制御するという感じ
つくったのはこんな感じのやつ

f:id:gsmcustomeffects:20191005005003p:plain

COMの選択をしてオープンをすると5~7のオブジェクトが表示される仕組み クローズすると消えるようになってる

f:id:gsmcustomeffects:20191005010331p:plain

コード全文は下に貼るのでちょい特殊な部分だけ解説していく流れで行きます。

尚基礎に関しては一番下に過去の記事貼っていますのでそちらを参考に環境構築などして下さい

Python側実装説明

class SerialComport(QtCore.QObject):
    def __init__(self, parent=None):
        super(SerialComport, self).__init__(parent)
        self.flag = 0
        self.instance = []

    @QtCore.Slot(result = 'QVariant')
    def comlist(self):
        self.ports = list_ports.comports()
        self.devices = [info.device for info in self.ports]
        self.instance.clear()                                   #clear list
        for i in range(len(self.devices)):
            self.instance.append(serial.Serial(port=self.devices[i],baudrate=9600))#instance packing
            self.instance[i].close()
        return self.devices

self.ports = list_ports.comports()でCOMの取得をしている。
self.instance.appendってのでインスタンスを配列みたいに管理できる。
これのおかげで self.instance[1].closeみたいな感じでアクセス可能になるわけ.
めんどくさかったのでボーレートは9600固定(GUI側でボーレート指定すれば実現は可能)
return self.devicesでQt側にlistで返している('QVariant'なので色々返せる)

Pyserialのほうは殆どAPI使ってるだけなので特にないけど送信はこんな感じで書いてる

    @QtCore.Slot(str,int,int,int)
    def slider_changed(self, arg1,r,g,b):
        for i in range(len(self.devices)):
            if (self.instance[i].port == arg1):
                self.instance[i].write(b.to_bytes(1, 'big'))
                self.instance[i].write(g.to_bytes(1, 'big'))
                self.instance[i].write(r.to_bytes(1, 'big'))

Qt(qml)側の説明

function comupdate(){
        comboBox.model.clear()
        var device = SerialComport.comlist();
        for(var key in device){
            comboBox.model.append({text:device[key]});
        }
        comboBox.currentIndex = 1;
    }

起動時の処理を毎度書くのがめんどかったのでfunctionにした。
あとはcomboBoxのmodelプロパティにアクセスしCOMリストを更新している感じ

function part_visible(){
        radioDelegate.visible = true
        radioDelegate1.visible = true
        radioDelegate2.visible = true
        radioDelegate3.visible = true
        slider.visible = true
        slider1.visible = true
        slider2.visible = true
        rectangle.visible = true
    }

    function part_hide(){
        radioDelegate.visible = false
        radioDelegate1.visible = false
        radioDelegate2.visible = false
        radioDelegate3.visible = false
        slider.visible = false
        slider1.visible = false
        slider2.visible = false
        rectangle.visible = false
    }

これはプロパティ表示/非表示の命令であり、各種コンポーネントのvisibleプロパティにtrue/falseしているだけ。

まとめ

Pyserial + PySide2で簡単なCOMアプリケーションを構成することができた。
結構手抜き実装なのでちゃんとやるならtryとか入れたほうがいいと思う。あとはisOpenのケアをもっとまじめにするとかね。

エンディアンとかあんまし気にしてないので間違ってるかも。なのでそこはPyserialのドキュメントちゃんと読んでください。
最後に動作例の動画でも貼っときます


マイコン側参考コード

ハードはKinetis FRDM-K22FでソフトはMbedを利用して書いてます。

/* mbed Microcontroller Library
 * Copyright (c) 2018 ARM Limited
 * SPDX-License-Identifier: Apache-2.0
 */

#include "mbed.h"
#include "stats_report.h"

PwmOut led1(LED1);//r
PwmOut led2(LED2);//g
PwmOut led3(LED3);//b


#define SLEEP_TIME                  500 // (msec)
#define PRINT_AFTER_N_LOOPS         20
Serial pc(USBTX, USBRX);
// main() runs in its own thread in the OS
int main()
{
    SystemReport sys_state( SLEEP_TIME * PRINT_AFTER_N_LOOPS /* Loop delay time in ms */);

    int count = 0;
    while (true) {
        char b = pc.getc();
        char g = pc.getc();
        char r = pc.getc();
            led1 = 1.0-1.0/255.0f*(float)r;//R
            led2 = 1.0-1.0/255.0f*(float)g;//G
            led3 = 1.0-1.0/255.0f*(float)b; //B   
        
        
    }
}

参考用コード全文(Python側)

import sys
import os
from PySide2 import QtCore, QtWidgets, QtQml
from serial.tools import list_ports
import serial

class SerialComport(QtCore.QObject):
    def __init__(self, parent=None):
        super(SerialComport, self).__init__(parent)
        self.flag = 0
        self.instance = []

    @QtCore.Slot(result = 'QVariant')
    def comlist(self):
        self.ports = list_ports.comports()
        self.devices = [info.device for info in self.ports]
        self.instance.clear()                                   #clear list
        for i in range(len(self.devices)):
            self.instance.append(serial.Serial(port=self.devices[i],baudrate=9600))#instance packing
            self.instance[i].close()
        return self.devices

    @QtCore.Slot(str)
    def comopen(self,arg1):
        for i in range(len(self.devices)):
            if(self.instance[i].port==arg1):
                self.instance[i].open()                         #COM open

    @QtCore.Slot(str)
    def comclose(self, arg1):
        for i in range(len(self.devices)):
            if (self.instance[i].port == arg1):
                self.instance[i].close()                        #COM close

    @QtCore.Slot(str,result='int')
    def comlistchanged(self,arg1):
        for i in range(len(self.devices)):
            if(self.instance[i].port==arg1):                    #checking which COM port is selected.
                if(self.instance[i].isOpen() == True):          #open or close check
                    self.flag = 1                               #if checked COM open, set flag.
                    print(self.instance[i].port + ":open")
                else:
                    self.flag = 0                               #if checked COM close, clear flag.
                    print(self.instance[i].port + ":close")
            else:
                if(self.instance[i].isOpen() == True):
                    print(self.instance[i].port + ":open")
                else:
                    print(self.instance[i].port + ":close")
        return self.flag

    @QtCore.Slot()
    def debug_alert(self):
        print("call!")                                         #QtQuick test function

    @QtCore.Slot(str)
    def ledred(self,arg1):
        for i in range(len(self.devices)):
            if (self.instance[i].port == arg1):
                packet = []
                packet.append(0x00)#b
                packet.append(0x00)#g
                packet.append(0xff)#r
                self.instance[i].write(packet)

    @QtCore.Slot(str)
    def ledblue(self, arg1):
        for i in range(len(self.devices)):
            if (self.instance[i].port == arg1):
                packet = []
                packet.append(0xff)#b
                packet.append(0x00)#g
                packet.append(0x00)#r
                print(packet)
                self.instance[i].write(packet)

    @QtCore.Slot(str)
    def ledgreen(self, arg1):
        for i in range(len(self.devices)):
            if (self.instance[i].port == arg1):
                packet = []
                packet.append(0x00)
                packet.append(0xff)
                packet.append(0x00)
                self.instance[i].write(packet)

    @QtCore.Slot(str)
    def ledorange(self, arg1):
        for i in range(len(self.devices)):
            if (self.instance[i].port == arg1):
                packet = []
                packet.append(0x00)
                packet.append(0x45)
                packet.append(0xff)
                self.instance[i].write(packet)

    @QtCore.Slot(str,int,int,int)
    def slider_changed(self, arg1,r,g,b):
        for i in range(len(self.devices)):
            if (self.instance[i].port == arg1):
                self.instance[i].write(b.to_bytes(1, 'big'))
                self.instance[i].write(g.to_bytes(1, 'big'))
                self.instance[i].write(r.to_bytes(1, 'big'))





if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"              #set Material theme(QtQuick)
    app = QtWidgets.QApplication(sys.argv)
    myconnect = SerialComport()                                     #Create instance


    engine = QtQml.QQmlApplicationEngine()                          #GUI
    ctx = engine.rootContext()
    ctx.setContextProperty("SerialComport", myconnect)              #Connect GUi to SerialComport Class
    engine.load('mypyside2.qml')                                    #qml file load.
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec_())

コード全文(qml側)

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3


Window {

    function init(){
        comupdate();
    }

    function comupdate(){
        comboBox.model.clear()
        var device = SerialComport.comlist();
        for(var key in device){
            comboBox.model.append({text:device[key]});
        }
        comboBox.currentIndex = 1;
    }

    function part_visible(){
        radioDelegate.visible = true
        radioDelegate1.visible = true
        radioDelegate2.visible = true
        radioDelegate3.visible = true
        slider.visible = true
        slider1.visible = true
        slider2.visible = true
        rectangle.visible = true
    }

    function part_hide(){
        radioDelegate.visible = false
        radioDelegate1.visible = false
        radioDelegate2.visible = false
        radioDelegate3.visible = false
        slider.visible = false
        slider1.visible = false
        slider2.visible = false
        rectangle.visible = false
    }

    objectName: "a"
    visible: true
    width: 640
    height: 480
    color: "#e2dde2"
    title: qsTr("Controller")
    Component.onCompleted:{
        init();
    }
    Button {
        id: button
        x: 255
        y: 28
        text: qsTr("COM ")
        font.pointSize: 13
        onClicked:{
            comupdate();
        }
    }

    ComboBox {
        id: comboBox
        x: 25
        y: 28
        width: 198
        height: 40
        visible: true
        editable: false
        model: ListModel {
            id: model
        }
        onCurrentIndexChanged:{
            var flag = SerialComport.comlistchanged(comboBox.model.get(comboBox.currentIndex).text);
            if(flag == 1){
                part_visible();
            }
            else{
                part_hide();
            }

        }
    }

    Button {
        id: button1
        x: 369
        y: 28
        text: qsTr("OPEN")
        font.pointSize: 13
        onClicked:{
            SerialComport.comopen(comboBox.model.get(comboBox.currentIndex).text);
            part_visible();
        }
    }

    Button {
        id: button2
        x: 475
        y: 28
        text: qsTr("CLOSE")
        font.pointSize: 13
        onClicked:{
            SerialComport.comclose(comboBox.model.get(comboBox.currentIndex).text);
            part_hide();
        }
    }

    RadioDelegate {
        id: radioDelegate
        visible:true
        x: 81
        y: 106
        text: qsTr("Red")
        onClicked:{
            SerialComport.ledred(comboBox.model.get(comboBox.currentIndex).text)
        }
    }

    RadioDelegate {
        id: radioDelegate1
        visible:true
        x: 187
        y: 106
        text: qsTr("Blue")
        onClicked:{
            SerialComport.ledblue(comboBox.model.get(comboBox.currentIndex).text)
        }
    }

    RadioDelegate {
        id: radioDelegate2
        visible:true
        x: 293
        y: 106
        text: qsTr("Green")
        onClicked:{
            SerialComport.ledgreen(comboBox.model.get(comboBox.currentIndex).text)
        }
    }

    RadioDelegate {
        id: radioDelegate3
        x: 420
        y: 106
        text: qsTr("Orange")
        visible: true
        onClicked:{
            SerialComport.ledorange(comboBox.model.get(comboBox.currentIndex).text)
        }
    }

    Slider {
        id: slider
        visible:true
        x: 81
        y: 215
        width: 439
        height: 40
        stepSize: 1
        wheelEnabled: true
        to: 255
        font.family: "Courier"
        font.capitalization: Font.AllLowercase
        value: 50
        onValueChanged:{
            SerialComport.slider_changed(comboBox.model.get(comboBox.currentIndex).text,slider.value,slider1.value,slider2.value)
            rectangle.color = Qt.rgba(slider.value/slider.to,slider1.value/slider.to,slider2.value/slider.to,1)
        }
    }

    Slider {
        id: slider1
        visible:true
        x: 81
        y: 261
        width: 439
        height: 40
        stepSize: 1
        wheelEnabled: true
        to: 255
        value: 50
        onValueChanged:{
            SerialComport.slider_changed(comboBox.model.get(comboBox.currentIndex).text,slider.value,slider1.value,slider2.value)
            rectangle.color = Qt.rgba(slider.value/slider.to,slider1.value/slider.to,slider2.value/slider.to,1)
        }
    }

    Slider {
        id: slider2
        visible:true
        x: 81
        y: 307
        width: 439
        height: 40
        stepSize: 1
        wheelEnabled: true
        to: 255
        value: 50
        onValueChanged:{
            SerialComport.slider_changed(comboBox.model.get(comboBox.currentIndex).text,slider.value,slider1.value,slider2.value)
            rectangle.color = Qt.rgba(slider.value/slider.to,slider1.value/slider.to,slider2.value/slider.to,1)
        }
    }

    Rectangle {
        id: rectangle
        visible:true
        x: 541
        y: 261
        width: 56
        height: 46
        color:Qt.rgba(0,0,0,1)
    }
}

PySide2でQtQuick(qml)使うメモ2

前回に引き続き今回もPySide2+Qtquick(qml)のメモ

GUIだと結構定番の電卓っぽい奴の実装

f:id:gsmcustomeffects:20190818201337g:plain


Button押下でイベント発生させてTextInputからデータもらってTextInputに返すサンプルだと思ってくれればいい

Pythonコード

import sys
import os
from PySide2 import QtCore, QtWidgets, QtQml

class Connect(QtCore.QObject):
    def __init__(self, parent=None):
        super(Connect, self).__init__(parent)

    @QtCore.Slot(int,int,result = float)
    def sum(self,arg1,arg2):
        return arg1 + arg2

    @QtCore.Slot(int, int, result=float)
    def sub(self, arg1, arg2):
        return arg1 - arg2

    @QtCore.Slot(int, int, result=float)
    def mul(self, arg1, arg2):
        return arg1 * arg2

    @QtCore.Slot(int, int, result=float)
    def div(self, arg1, arg2):
        return arg1 / arg2

if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
    app = QtWidgets.QApplication(sys.argv)
    myconnect = Connect()
    engine = QtQml.QQmlApplicationEngine()
    ctx = engine.rootContext()
    ctx.setContextProperty("Connect", myconnect)
    engine.load('mypyside2.qml')
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

qmlコード

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.0

Window {
    objectName: "a"
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    TextInput {
        id: textInput1
        x: 104
        y: 108
        width: 80
        height: 37
        text: qsTr("12")
        font.pixelSize: 20
    }

    TextInput {

        id: textInput2
        x: 179
        y: 108
        width: 80
        height: 37
        text: qsTr("2")
        font.pixelSize: 20
    }

    TextInput {

        id: textInput3
        x: 270
        y: 108
        width: 80
        height: 37
        text: qsTr("1")
        font.pixelSize: 20
    }

    Row {
        id: row
        x: 104
        y: 186
        width: 427
        height: 119
        spacing: 9

        Button {
            objectName:"button"
            id: button1
            width: 100
            height: 50
            text: qsTr("add")
            font.pointSize: 14
            onClicked:function(){
                textInput3.text = Connect.sum(textInput1.text,textInput2.text)
            }
        }

        Button {
            id: button2
            width: 100
            height: 50
            text: qsTr("sub")
            font.pointSize: 13
            objectName: "button"
            onClicked:function(){
                textInput3.text = Connect.sub(textInput1.text,textInput2.text)
            }
        }

        Button {
            id: button3
            width: 100
            height: 50
            text: qsTr("mul")
            font.pointSize: 12
            objectName: "button"
            onClicked:function(){
                textInput3.text = Connect.mul(textInput1.text,textInput2.text)
            }
        }

        Button {
            id: button4
            width: 100
            height: 50
            text: qsTr("div")
            font.pointSize: 12
            objectName: "button"
            onClicked:function(){
                textInput3.text = Connect.div(textInput1.text,textInput2.text)
            }
        }
    }
}

解説的なやつ

python

@QtCore.Slot(int,int,result = float)
    def sum(self,arg1,arg2):
        return arg1 + arg2

addの例で解説する。
@QtCore.Slot(int,int,result = float)で引数の型と返り値の型をqmlに対して公開する。
あとはdefで関数を実装するだけ前回との違いはSlotでreturnしているとこ。
ここで注意が必要なのが書かなくてもエラーは出ないがresult = floatは必須ということ
書かないと何も値が返ってこない

qml側

TextInput {

        id: textInput3
        x: 270
        y: 108
        width: 80
        height: 37
        text: qsTr("1")
        font.pixelSize: 20
    }

Button {
            objectName:"button"
            id: button1
            width: 100
            height: 50
            text: qsTr("add")
            font.pointSize: 14
            onClicked:function(){
                textInput3.text = Connect.sum(textInput1.text,textInput2.text)
            }
        }

こちらもaddの例で説明する。

まずqmlオブジェクトに対してはtextInput3.textのようにid.propertyでアクセスできる。(qmlではオブジェクト以下のパラメータ類のことをPropertyと呼ぶ

次にonclicked部分で押下時の動作を定義している。
textInput3.textにpython側のsumの返り値を代入するということをしているだけだ。

あくまでqmlオブジェクトへの代入はqml側でやるというとこがQtの方針
PythonC++側からも子オブジェクトにアクセスできるがQt公式は推奨していない(参照関係がごちゃごちゃになるため

この関係を守るとUI設計者と内部実装側が完全分業可能なので合理的ではある。

PySide2でQtQuick(qml)使うメモ1

自分用のメモです。

自分の動作環境は

  • PyCharm 無料版
  • Python 3.7.4 or 3.6(仮想で両方で試した)
  • Pycharm内臓のVenvでパッケージ管理

導入

PythonでQtを扱うにはPyQtとPysideの2つがある。

verごとに書くとこんな感じ

  • Qt4 : PyQt4,PyiSde
  • Qt5 : PyQt5,PySide2

という感じになる。
現行で使うならQt5系を利用するほうがいいだろうということでPyQt5,PySide2を選ぶことになる。
選定にあたりいろいろググった結果PySide2ってのがQt公式がサポートするQt5バインディング(Qt for Pythonと呼ばれてるのがこれにあたる)らしいのでこれを使うことにする。

ui -> pyをする使い方

QtDesignerというソフトを使ってグラフィカルにUIをデザインしてそのファイル(.ui)をpyside2-uic.exeを使って.py拡張子ファイルに変換してインポートする手法のこと。
この方法が一番情報も多くて使いやすいと思う。

いろんな人が情報を公開しているのでその辺を読むといいだろう
note.mu

qiita.com

UIファイルの更新のたびにPyCharm側でpyside2-uic.exeを自動で呼び出すこともできる。
qiita.com

QtQuickについて

これについては公式の説明が分かりやすい
Qt Quick 入門 第1回: Qt Quick とは - Qt Japanese Blog

ようはuiファイルではなくqmlというjson風の表記を使ってUIをデザインしていく手法であり比較的楽にかっこいいUIを作成できる。

f:id:gsmcustomeffects:20190818004444p:plain

画像は私が作成したUIであるが5分ぐらいでこのぐらいのGUIを作成できる。

PySide2導入とテスト

早速本題に入る。
まずはPyCharmで新規プロジェクトをつくる。
f:id:gsmcustomeffects:20190818010410p:plain

上部メニューのFile/settingでこの画面が開くのでproject interpreterの画面まで持ってくる。
f:id:gsmcustomeffects:20190818010741p:plain

+ボタンをクリックして必要なモジュールをインストールしてくる。
ここではPySide2を検索して持ってくる。
f:id:gsmcustomeffects:20190818011621p:plain

インストールができたら.pyファイルを作成して以下のリンクのコードを実行してみる。
https://doc.qt.io/qtforpython/tutorials/basictutorial/dialog.html

このような画面が出ていればPySide2の導入はOK
f:id:gsmcustomeffects:20190818012746p:plain

PySide2でQtQuick(qml)ファイルを扱う

次にqmlファイルを作るためにQt Creatorをダウンロードしてくる。
Download Qt: Choose commercial or open source

アカウントとか聞かれますけどスキップで何とかなります。
適当に自分の欲しいものをインストールしてください

CreatorとQt5.x系があればOKです。

Qt Creatorをインストール出来たら開いて新規プロジェクトでUI Prototypeを選ぶ
f:id:gsmcustomeffects:20190818021459p:plain

キットに関しては適当に選ぶといいです。
するとこのような画面になるのでデザインをクリックします。
f:id:gsmcustomeffects:20190818021603p:plain

UIデザイン画面が開くので次にインポートタブを開きます。
f:id:gsmcustomeffects:20190818022028p:plain

QtQuick.ControlsとQtQuick.Controls.Materialsを追加
f:id:gsmcustomeffects:20190818022236p:plain

そうするとエレメントタブにボタン類が入ってくる
f:id:gsmcustomeffects:20190818022427p:plain

ボタンをドラッグしてUIに追加する。
f:id:gsmcustomeffects:20190818022542p:plain

次にできたqmlをPycharmプロジェクトに入れてあげる
f:id:gsmcustomeffects:20190818022837p:plain

そしたらこのコードを実行

import sys
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QUrl

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    engine.load(QUrl("mypyside2.qml"))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

先ほど作ったUIが表示できていればOK
f:id:gsmcustomeffects:20190818023149p:plain

ボタンへのイベント追加

次に配置したウィジェットPythonコードをつないでいく。

python code

import sys
import os
from PySide2 import QtCore, QtWidgets, QtQml

class Connect(QtCore.QObject):
    def __init__(self, parent=None):
        super(Connect, self).__init__(parent)

    @QtCore.Slot()
    def button_clicked(self):
        print("button clicked")



if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
    app = QtWidgets.QApplication(sys.argv)
    myconnect = Connect()
    engine = QtQml.QQmlApplicationEngine()
    ctx = engine.rootContext()
    ctx.setContextProperty("Connect", myconnect)
    engine.load('mypyside2.qml')
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

qml code

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Button {
        id: button
        x: 270
        y: 220
        text: qsTr("Button")
        onClicked:Connect.button_clicked()//python側のクラスとつなぐ
    }
}


実行してボタンをクリックするとprint文が実行されるはず。
f:id:gsmcustomeffects:20190818025353p:plain

説明(Python側)

まずQtCore.QObjectを継承したクラス(Connect)を作成する。
@QtCore.Slotでqml側にメソッドの存在を通知する。これによりqml側でdef以下を呼べるようになる。
@表記はPythont的に言うとデコレータってやつらしい。

os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"

Materialテーマを有効にする記述で特に必要はない
有効にしていると以下のリンクのようなことができる。
https://doc.qt.io/qt-5/qtquickcontrols2-material.html

ctx = engine.rootContext()
ctx.setContextProperty("Connect", myconnect)

engine内部の子ウィジェットを検索可能にする記述(あってるかわかんないけど
QMLにアクセスしたりQMLからアクセスしたりする場合はこの二行が必須っぽい

説明(qml側)

説明するといってもonClickedぐらい

クリックされた時の動作を記述するメンバー
ウィジェットごとにイベントが用意されてるSliderならonValueChangedみたいな感じで書ける。
Ctrl+Spaceで補完できるので何があるかは分かると思う。

メンバーにカーソルを合わせてF1を押すとウィジェットの説明を表示できる
f:id:gsmcustomeffects:20190818031331p:plain


参考文献

rootcontextの記述はC++も同様なので参考になった。
その他qmlとの相互イベント通知の参考になる。
QMLとC++のバインディング - Qiita

今回の記事はこの問題の劣化版みたいなものなのでこっちのスレッド読むともっといいのがつくれると思う
Connect python signal to QML ui slot with PySide2 - Stack Overflow

公式のサンプルとチュートリアル
QMLの例が2個しかないけど書き方の参考にはなる。
https://doc.qt.io/qtforpython/tutorials/index.html