ただのメモ

開発で得た知識のアウトプット

【Qt】複数のウィジェットを含むカスタムウィジェットを作る方法

今回は、Qtについての備忘録です。
前回と同様、学習のアウトプット目的で書いています。
ご指摘等ありましたら、コメントに書いていただけると幸いです。

動作環境は以下の通りです。
【OS】Windows10 64bit
【CPU】Intel core i5-4590
【Qt】5.11.2 64-bit for Desktop (MSVC 2017)
【Qt Creator】4.7.1 (Community)

では、本題です。

カスタムウィジェットとは

Qtには既存のウィジェットに独自の機能を持たせた
    カスタムウィジェット
という機能があります。

基本的にカスタムウィジェットは、
既存のウィジェットから派生したクラスを登録することで扱えるようになります。


登録方法としては、二つ方法があります。

1. 格上げ
2. Custom Widget Plugin

1は比較的簡単で、環境もそのままで行えます。

2は難易度としては高いですが、より細かいカスタムが行えます。
しかしWindowsの場合、環境構築が大変です。

Pluginに関して、詳しくは以下をご覧ください。
QtのCustom Widget Pluginを斬る! - Qiita
Custom Widget Plugin Example | Qt Designer Manual
c++ - how do i create a custom (widget) plugin for qt designer with cmake ( and visual studio ) - Stack Overflow

今回は、1の格上げを用いることにします。

格上げを用いてカスタムウィジェットを登録する

格上げにはQt Creatorを用いて行います。

まずは、カスタムウィジェットのクラスを作ります。

ここでつくるカスタムウィジェットは、
できるだけそのウィジェット内で完結するようにしたいので、
ボタンが押されている間、ボタンにcliked!!と表示
といった簡単なものにしたいと思います。

プロジェクトは、Qt ウィジェットアプリケーションです。
新規プロジェクトに以下のヘッダファイルを追加しています。

コードはこちら。

//custombutton.hpp
#ifndef CUSTOMBUTTON_HPP
#define CUSTOMBUTTON_HPP

#include <QPushButton>

class CustomButton : public QPushButton
{
    Q_OBJECT

public:
    CustomButton( QWidget* parent = nullptr ) :
        QPushButton( parent )
    {
        connect( this, SIGNAL( pressed() ), this, SLOT( ChangeText() ) ) ;
        connect( this, SIGNAL( released() ), this, SLOT( ClearText() ) ) ;
    }

private slots:
    void ChangeText() {
        setText( "clicked!!" ) ;
    }
    void ClearText() {
        setText( "" ) ;
    }
};

#endif // CUSTOMBUTTON_HPP

QPushButtonが押されたとき → ChangeText関数
QPushButtonが離されたとき → ClearText関数

といった具合にconnectしているだけです。

シグナルはスロットでキャッチできます。

次にこのクラスを登録してみましょう。
まずはプロジェクトの.uiファイルを開きます。
Qt Designerでもよいです。

次に基本クラスにあたるウィジェットを置き、その上で右クリックをします。
ここで格上げ先を指定...を選択。

f:id:pit-ray:20181105002553j:plain

表示されたダイアログに
カスタムウィジェットの基本クラス
カスタムウィジェットのクラス名
カスタムウィジェットクラスがあるヘッダファイル名
を入力し、追加します。
f:id:pit-ray:20181105002556j:plain

最後に選択し、格上げすればokです。
f:id:pit-ray:20181105002559j:plain

これで実行すると以下のような動作になります。
ここで注意したいのは、これは
ウィジェットの機能としての動作
であるということです。
f:id:pit-ray:20181105011848g:plain

このように、派生クラスを登録することで、任意の機能を持たせたウィジェットを作れます。

ここで元のウィジェットが複数存在する場合は、どうしたらいいのでしょうか。
正直なところ、一つのウィジェットの中に複数のウィジェットがあるというのは、設計的に賛否がありそうです。
しかし、極めて密接に関係していて、そのウィジェットが複数ある場合、
同じクラス内に複数のウィジェットを配置して、内部でデータのやり取りをしたほうがよさそうです。その方がオブジェクト指向のカプセル化を実現できます。

複数のウィジェットを扱うカスタムウィジェットをつくる

結論からいうと、
QWidgetを継承することで内部に複数のウィジェットを配置できます。
内部のウィジェットはポインタとして作成し、コード上でレイアウト等を決めます。

Qtのウィジェットは大抵、QWidgetクラスを継承しています。
QWidgetは例えるならば、画用紙のようなものです。

実際に.uiファイルや、Qt Designerで確認してみるとわかりますが、
Widgetオブジェクトは透明な枠だけのものです。

f:id:pit-ray:20181105203950j:plain

早速ですが、QPushButtonQLabelを用いて、カウンターを作りたいと思います。
ヘッダは以下のようになります。

//counter.hpp
#ifndef COUNTER_HPP
#define COUNTER_HPP

#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QString>

class Counter : public QWidget
{
    Q_OBJECT
public:
    explicit Counter( QWidget* parent = nullptr ) :
        QWidget( parent ),
        button( new QPushButton( this ) ),
        label( new QLabel( this ) ),
        counter( 0 )
    {
        InitLayout() ;

        //buttonからのclickedシグナルをこのクラスのIncrement()に接続
        connect( button, SIGNAL( clicked() ), this, SLOT( Increment() ) ) ;
    }

    ~Counter() {
        delete button ;
        delete label ;
    }

private:
    QPushButton* button ;
    QLabel*      label ;
    int counter ;

    void UpdateLabel() {
        label->setText( QString::number( counter ) ) ;
    }

    void InitLayout() {
        //縦方向のレイアウト生成
        QVBoxLayout* layout = new QVBoxLayout( this ) ;

        layout->addWidget( button ) ;
        layout->addWidget( label ) ;

        //ウィジェットを中央揃えにする
        layout->setAlignment( button, Qt::AlignCenter ) ;
        layout->setAlignment( label, Qt::AlignCenter ) ;

        setLayout( layout ) ;

        //ウィジェットのサイズを設定。
        //サイズはQt Designer, Qt Creatorのプロパティから確認できる。
        button->setMinimumSize( 180, 60 ) ;
        button->setText( "Increment" ) ;

        label->setMinimumSize( 180, 60 ) ;
        label->setAlignment( Qt::AlignCenter ) ;
        label->setFont( QFont( "Arial", 20, QFont::Bold ) ) ;
        UpdateLabel() ;
    }

private slots:
    void Increment() {
        counter ++ ;
        UpdateLabel() ;
    }
};

#endif // COUNTER_HPP

このクラスは、基本クラスがQWidgetであるので、
格上げ元はWidgetオブジェクトにします。

ここで注意したいのは、レイアウトです。
この場合、配置はソースコード上で設定する必要があります。
レイアウトには様々な種類があり、目的に合ったものを選んでください。
Layout Management | Qt Widgets 5.15.2
Qt Namespace | Qt Core 5.15.2

Layoutオブジェクトをつくり、setLayoutで渡します。
ここでは暗黙的にdeleteしています。
privateメンバとして保持し、コンストラクタでnew, デストラクタで明示的にdeleteでもよいです。
setAlignmentは第一引数は、内部ウィジェットのポインタです。
QLayout Class | Qt 4.8

以上のようにすることでこのように動作します。
f:id:pit-ray:20181105203012g:plain

このようにQWidgetを用いることで、
複数のウィジェットを組み合わせたカスタムウィジェットを作ることができます。


最初にも申しましたが、ご指摘は歓迎いたします。

今回の記事が参考になれば幸いです。
ご閲覧ありがとうございました。