Qtでのカスタムウィジェットの作り方

Qtには便利なウィジェットが数多く用意されているが、それぞれに適した機能のみが実装されている
描画についてはQt Style Sheetsによって、強力なカスタマイズが可能だが
対応しているプロパティは限定的であり、動的な変更に向かない、また対象環境を考慮しづらいのが欠点である

対象環境を考慮しづらいというのは例えば、QLabelの背景色に白を想定し、枠線色をスタイルシートで黒に設定するとしよう
これがもしユーザーのテーマの背景色が黒だった場合に色が同化し枠線の意味をなくしてしまう
これを防ぐために背景色をスタイルシートで白に固定させると今度は文字色が被り、文字色を黒に設定する必要が・・・となってしまう
左から想定、テーマが黒、背景色を白指定

しかし、カスタムクラス化し描画やイベント時の動作を書き加えることで、より広範囲にカスタマイズすることが可能だ

例としてQSliderをカスタムクラス化し、文字を表示できるよう順を追って説明していく
まず通常が画像のようになっているがこれに文字を追加するにはQSliderのカスタムクラスを作成する
//ヘッダ
class CustomSlider : public QSlider
{
    Q_OBJECT
public:
    explicit CustomSlider(QWidget *parent = nullptr);
    explicit CustomSlider(Qt::Orientation orientation, QWidget *parent = nullptr);
protected:
    void paintEvent(QPaintEvent *e) //描画を行う
};
//ソース
CustomSlider::CustomSlider(QWidget *parent) : QSlider(parent)
{
}
CustomSlider::CustomSlider(Qt::Orientation orientation, QWidget *parent) : QSlider(parent)
{
    setOrientation(orientation);
}
CustomSlider:paintEvent(QPaintEvent *e)
{
    QSlider::paintEvent(e); //これで継承元のQSliderを描画することができる
    QString text = QString::number(value()) + "%"; //スライダの数値
    QPainter painter(this);
    painter.drawText(rect(),Qt::AlignCenter,text); //文字列を描き込む
}

paintEvent内のQSlider::paintEvent(e)で継承元のpaintEventを呼び出すことでスライダの描画をしている
この「継承元の関数を呼び出す」のはpaintEventに限らず、継承元の各ウィジェットで独自に実装された関数を呼び出す場合に使用する

ボタンであれば押下時に表示や動作を更新しているので、mousePressEventとmouseReleaseEventを書き加えた場合は
呼び出しを行わないと押し下げ状態の表示はもちろんのこと、clicked()のシグナルが送られずボタンとして利用できなくなる

それぞれのウィジェットがどの関数を独自に実装しているかは
リファレンスページ内の Reimplemented Protected Functions という項目で確認できる
QSliderの場合は「mouseMoveEvent, mousePressEvent, mouseReleaseEvent, paintEvent」の4つ。

QPainter painter(this)で、このWidgetに直接描画することを示している
QSliderの場合はこれで問題ないが、QListWidgetやQTreeWidgetなどのスクロールバーが内蔵されているものは指定をQPainter painter(viewport())とする必要がある
またその場合、通常再描画はupdate()とするところをviewport()->update()と書く

上記のコードを書き加えQSliderの代わりに配置することでスライダの上にテキストを表示することが出来る
この例ではスライダの数値を表示している
QLabelを上に被せるのとは違い、元通りマウスでの操作が可能
しかしこれでは少々文字が細く文字が見づらい・・・のでフォントを変更する
painter.drawTextの前に以下を追加
    QFont newFont = font(); //現在のフォントを元にQFontを作成
    newFont.setBold(true); //太字にする
    painter.setFont(newFont); //太字に設定したフォントをpainterに設定

太くはなったが、まだ何か見づらいのでテキストにアウトラインを設定する
painter.drawTextを下記に置き換え
    painter.save();
    painter.setPen(QColor(255,255,255,150)); //文字色を指定
    painter.drawText(QRect(-1,-1,width(),height()),Qt::AlignCenter,text);
    painter.drawText(QRect(-1,1,width(),height()),Qt::AlignCenter,text);
    painter.drawText(QRect(1,-1,width(),height()),Qt::AlignCenter,text);
    painter.drawText(QRect(1,1,width(),height()),Qt::AlignCenter,text);
    painter.restore();
    painter.drawText(rect(),Qt::AlignCenter,text);

これは文字色を変更した上で「左上」「左下」「右上」「右下」にずらして文字を描画している
この上で色を戻し元の文字を描画すると文字のアウトラインができる

painter.save()でその時点のペンの色やブラシの色などを保存し
painter.restore()で保存したものを復元する

これでようやく見やすくなった
しかし、上記の例だとスライダの数値を表示しているので問題ないが
これが種類を表示する場合は文字数が多いとスライダの"つかみ"が隠れてしまい、どこを掴めばいいかがわかりづらくなるだろう

これを解消する為に仕組みを考えてみよう
マウスカーソルがウィジェット上にある場合はスライダのつかみを手前に表示することで現在の位置を把握しやすくする
ヘッダのprivateに以下の変数を追加
    bool isHover = false;
同じくヘッダのprotectedに以下を追加
    void enterEvent(QEvent *e);
    void leaveEvent(QEvent *e);
これはウィジェットにマウスカーソルが入った時にenterEventが、出た時にleaveEventが発火される
そしてソースに以下を追加
void CustomSlider::enterEvent(QEvent *e)
{
    isHover = true;
}

void CustomSlider::leaveEvent(QEvent *e)
{
    isHover = false;
}
肝心のpaintEventは大きく変更し
 
void CustomSlider::paintEvent(QPaintEvent *e)
{   
    QString text = "Position";
    QStylePainter painter(this);
    QStyleOptionSlider opt;

    initStyleOption(&opt);
    opt.subControls = QStyle::SC_SliderGroove; //スライダの背景を指定
    painter.drawComplexControl(QStyle::CC_Slider, opt); //スライダの背景を描画

    opt.subControls = QStyle::SC_SliderHandle; //スライダの つかみ を指定
    if (!isHover)
        painter.drawComplexControl(QStyle::CC_Slider, opt); //スライダの つかみ を描画

    painter.save();
    painter.setPen(QColor(255,255,255,150));
    painter.drawText(QRect(-1,-1,width(),height()),Qt::AlignCenter, text);
    painter.drawText(QRect(-1,1,width(),height()),Qt::AlignCenter, text);
    painter.drawText(QRect(1,-1,width(),height()),Qt::AlignCenter, text);
    painter.drawText(QRect(1,1,width(),height()),Qt::AlignCenter, text);
    painter.restore();
    painter.drawText(rect(),Qt::AlignCenter, text);

    if (isHover)
        painter.drawComplexControl(QStyle::CC_Slider, opt);
}

QPainterをQStylePainterに変更しているが
このQStylePainterというのはQSliderなどのQtで用意されたウィジェットの描画をするためのものであり、QPainterを継承しているのでそのままでdrawTextが可能
上記のコードの描画構成は
・スライダの背景
・スライダのつかみ
・文字(アウトライン付き)

仕組みはスライダ内にマウスカーソルが入っていないと(isHover == false)
 背景->つかみ->文字 の順で最初のコードと同じ様に描画し
スライダ内にマウスカーソルが入っていると(isHover == true)
 背景->文字->つかみ の順で描画することでつかみを上に表示することができる

このスライダの「背景」や「つかみ」などはQStyleで定義されており公式のリファレンスで確認できる
今回のスライダは多重構造になっているため、少し特殊でpainter.drawなんちゃらをする前に描き込む対象を指定しなければならない

また、一部のウィジェットでは子ウィジェットが入っているものが存在する
QSpinBoxなどがいい例で、数字を表示する部分はQLineEditになっており
この部分に描画するにはQLineEditをカスタマイズした上で
CustomLineEdit *customLineEdit = new CustomLineEdit();
spinbox->setLineEdit(customLineEdit);
のように割当てなければならない

ちなみに今回作成したカスタムウィジェットをQt Creatorで使用するには
・継承元のウィジェット(上記までの例ではQSlider)を配置
・右クリックメニューで格上げ先を指定を選択し、クラス名とクラスの書かれたヘッダファイルを指定
・追加ボタン、格上げボタンの順に押下すれば完了
また、一度格上げ先を登録しておけば、同じ継承元&継承先であれば右クリックメニューから簡単に格上げできる

コメント

このブログの人気の投稿

Firefoxで改造アドオンのインストール失敗

base64で文字列化された画像のリサイズ用ツール

PlayOnLinuxでインストールリストが表示されない場合の対処法