本文へスキップします。

本文へ

【全Qt】
【全・Qt】SRAロゴ
H1

技術記事:ウィジェット - シングルクリックとダブルクリックの区別

技術記事

ウィジェット - シングルクリックとダブルクリックの区別

(掲載 2024年6月13日)

デバッグ

今回は、連載の話題を変更して、QPushButton でシングルクリックとダブルクリックを区別する方法について説明します。


シングルクリックとダブルクリック

QPushButton でシングルクリックとダブルクリックを区別して扱う方法を考えてみましょう。つまり、シングルクリック時に clicked() シグナルを送信し、ダブルクリック時に doubleClicked() シグナルを送信するようにし、ダブルクリック時には、clicked() シグナルを送信しないようにします。

シングルクリックが短時間に連続して発生したときにダブルクリックが発生します。従って、最初のシングルクリックを検知せずに、ダブルクリックを検知することはできません。最初のシングルクリックを無視するかどうかを判断するために、ダブルクリックが発生するかどうかを予測することは不可能です。予測する代わりに、ダブルクリックのタイムアウトを確認するまで、シングルクリックの送信を延期することはできます。

Qt Quick の TapHandler には、Qt 6.5 で exclusiveSignals プロパティーが追加され、シングルタップとダブルタップを区別して扱えます。QPushButon で同様な機能を実装するのが目標です。参考にするのは、KDAB の YouTube チャンネルの以下の投稿です。

Adding Double Click to a QPushButton

引用するサンプルコードは、以下から入手できます。

https://github.com/KDABLabs/kdabtv/tree/master/Qt-Widgets-and-more/double-click-push-button

説明は、5 つのパートに分かれています。パート毎にコードが改良され、パート 5 で シングルクリックとダブルクリックが区別できるようになります。今回の記事では、サ ンプルコードを引用し、要点を説明します。詳しい説明は、YouTube の解説とその文字 起こしなどを参照してください。

パート 1 - mouseDoubleClickEvent のオーバーライド

DoubleClickButton1.h:
cclass DoubleClickButton1 : public QPushButton
{
    Q_OBJECT

public:
    using QPushButton::QPushButton;

signals:
    void doubleClicked();

protected:
    void mouseDoubleClickEvent(QMouseEvent *event) override;
};

DoubleClickButton1.cpp:
void DoubleClickButton1::mouseDoubleClickEvent(QMouseEvent * /*event*/)
{
    emit doubleClicked();
}

part1.cpp:
void part1()
{
    auto button = new DoubleClickButton1("Click Me, please");
    QObject::connect(button, &DoubleClickButton1::clicked, []() { qDebug() << "Thanks"; });
    QObject::connect(button, &DoubleClickButton1::doubleClicked,
                     []() { qDebug() << "Hey not so fast"; });
    button->show();
}

QPushButton クラスを継承して DoubleClickButton クラスを作成し、doubleClicked() を新たに用意します。ダブルクリック発生を検知するイベントハンドラーmouseDoubleClickEvent() が用意されているので、再実装して doubleClicked() シグナルを送信します。

実行して、クリックすると「Thanks」と表示されます。ダブルクリックすると「Thanks」と「Hey not so fast」が表示されます。

パート 2 シングルクリックシグナルを延期するための QTimer の導入

DoubleClickButton2.h:
class DoubleClickButton2 : public QPushButton
{
    Q_OBJECT

public:
    DoubleClickButton2(const QString &text, QWidget *parent = nullptr);

signals:
    void doubleClicked();

protected:
    void mouseReleaseEvent(QMouseEvent *event) override;
    void mouseDoubleClickEvent(QMouseEvent *event) override;

private:
    class QTimer *m_timer;
};

DoubleClickButton2.cpp:
DoubleClickButton2::DoubleClickButton2(const QString &text, QWidget *parent)
    : QPushButton(text, parent)
{
    m_timer = new QTimer(this);
    m_timer->setSingleShot(true);
    m_timer->setInterval(QApplication::doubleClickInterval());
    connect(m_timer, &QTimer::timeout, this, [this] { emit clicked(); });
}

void DoubleClickButton2::mouseReleaseEvent(QMouseEvent * /*event*/)
{
    m_timer->start();
    // Do not call superclass!
}

void DoubleClickButton2::mouseDoubleClickEvent(QMouseEvent * /*event*/)
{
    emit doubleClicked();
    m_timer->stop();
}

最初のクリック時に、ダブルクリックが発生するかどうかを知る必要があります。予測はできません。しかし、ダブルクリックが発生するか、または発生しないことが確認できるまで、クリックシグナルの送信を延期することはできます。

QApplication::doubleClickInterval() で、二度目のクリックがダブルクリックと見なされるまでの時間が分かります。QTimerのシングルショットを使用し、発火したときにclicked() シグナルを送信します。mouseReleaseEvent() では、本来の clicked() シグナルを発生させないようにするために、ベースクラスの mouseReleaseEvent() を呼び出さないようにしています。

実行して、クリックすると「Thanks」と表示されます。ダブルクリックするとと「Hey not so fast」が表示されてから「Thanks」が表示されます。

もうひとつ問題があります。マウスボタンを放してもボタンの見かけが押したままになってしまっています。ベースクラスの mouseReleaseEvent() でボタンの見かけを元に戻しているので、呼び出さないと見かけが押したままになってしまいます。ベースクラスの mouseReleaseEvent() を呼び出して、クリックシグナルを送信しないようにする必要があります。

パート 3 - ボタン状態の描画確認、シグナル発信の抑制

DoubleClickButton3.cpp:
DoubleClickButton3::DoubleClickButton3(const QString &text, QWidget *parent)
    : QPushButton(text, parent)
{
    m_timer = new QTimer(this);
    m_timer->setSingleShot(true);
    m_timer->setInterval(QApplication::doubleClickInterval());
    connect(m_timer, &QTimer::timeout, this, [this] { emit clicked(); });
}

void DoubleClickButton3::mouseReleaseEvent(QMouseEvent *event)
{
    m_timer->start();

    QSignalBlocker dummy(this);
    QPushButton::mouseReleaseEvent(event);
}

void DoubleClickButton3::mouseDoubleClickEvent(QMouseEvent *event)
{
    emit doubleClicked();
    m_timer->stop();
    QPushButton::mouseDoubleClickEvent(event);
}

ベースクラスのメソッドを呼び出すようにし、QSignalBlocker でシグナルをブロックして送信されないようにしました。

実行して、クリックすると「Thanks」と表示されます。ダブルクリックするとと「Hey not so fast」が表示されてから「Thanks」が表示されます。ボタンの見かけは、マウスボタンを放すと元に戻ります。

パート 4 - 遅れて「Thanks」が表示される理由の理解

前パートで「Thanks」が表示されてしまう原因の追求のためにデバッグ文を入れデバッガーで調べています。mouseDoubleClickEvent() がベースクラスの mouseDoubleClickEvent() を呼び出していて、そこでは mouseDoubleClickEvent () を実装していない場合、一連のクリック関連イベントを発生させています。これが clkecked() シグナルを送信してしまっています。

パート 5 - フラグを使用して誤ったイベントをブロックする

DoubleClickButton5.h:
class DoubleClickButton5 : public QPushButton
{
    Q_OBJECT

public:
    DoubleClickButton5(const QString &text, QWidget *parent = nullptr);

signals:
    void doubleClicked();

protected:
    void mousePressEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void mouseDoubleClickEvent(QMouseEvent *event) override;

private:
    class QTimer *m_timer;
    bool m_blockNextRelease = false;
};

DoubleClickButton5.cpp:
DoubleClickButton5::DoubleClickButton5(const QString &text, QWidget *parent)
    : QPushButton(text, parent)
{
    m_timer = new QTimer(this);
    m_timer->setSingleShot(true);
    m_timer->setInterval(QApplication::doubleClickInterval());
    connect(m_timer, &QTimer::timeout, this, [this] { emit clicked(); });
}

void DoubleClickButton5::mousePressEvent(QMouseEvent *event)
{
    QPushButton::mousePressEvent(event);
}

void DoubleClickButton5::mouseReleaseEvent(QMouseEvent *event)
{
    if (!m_blockNextRelease)
        m_timer->start();
    m_blockNextRelease = false;

    QSignalBlocker dummy(this);
    QPushButton::mouseReleaseEvent(event);
}

void DoubleClickButton5::mouseDoubleClickEvent(QMouseEvent *event)
{
    emit doubleClicked();
    m_timer->stop();
    m_blockNextRelease = true;
}

解決策は、ダブルクリック後に発生するリリースからのシグナルをブロックすることです。blockNextRelease フラグを用意して、2 度目のマウスリリースでタイマーを起動しないようにし、cliecked() シグナルを送信しないようにします。

これで、シングルクリックとダブルクリックを区別して、cliecked() シグナルとdoubleClicked() シグナルのどちらか一方だけが送信されるようになりました。