Qt プログラミングTips: QPlainTextEdit マウス移動イベントの活用事例集

2025-04-26

QPlainTextEdit::mouseMoveEvent() とは

QPlainTextEdit::mouseMoveEvent() は、QtのGUIフレームワークにおいて、QPlainTextEdit ウィジェット(複数行のプレーンテキストを表示・編集するためのウィジェット)内でマウスカーソルが移動した際に発生するイベントを処理するための仮想関数(virtual function)です。

この関数は、QPlainTextEdit クラスを継承したカスタムクラス内で再実装(override)することで、マウス移動時の特別な動作を定義することができます。

イベント発生のタイミング

mouseMoveEvent() イベントは、以下の条件が満たされた場合に発生します。

  1. マウスボタンが押されていない状態、またはマウスボタンが押された状態でマウスが移動した場合。
  2. QPlainTextEdit ウィジェットがマウスカーソル下にあり、かつマウスイベントを受け付ける状態であること。
  3. QWidget::setMouseTracking(true) が呼び出されている場合(マウスボタンが押されていない状態でもイベントが発生するようになります)。setMouseTracking(false) がデフォルトです。

関数のシグネチャ

mouseMoveEvent() 関数の基本的なシグネチャは以下の通りです。

void mouseMoveEvent(QMouseEvent *event);
  • event: QMouseEvent クラスのポインタです。このオブジェクトには、マウスイベントに関する様々な情報が含まれています。例えば、

    • event->pos(): イベントが発生した時点でのマウスカーソルのウィジェット内での座標(QPoint 型)。
    • event->globalPos(): イベントが発生した時点でのマウスカーソルのスクリーン座標(QPoint 型)。
    • event->buttons(): イベント発生時に押されていたマウスボタンの状態(Qt::MouseButtons 型のビットマスク)。
    • event->modifiers(): イベント発生時に押されていた修飾キー(Shift, Ctrl, Alt など)の状態(Qt::KeyboardModifiers 型のビットマスク)。

再実装の目的と利用例

mouseMoveEvent() を再実装することで、マウスカーソルの移動に応じて様々な処理を行うことができます。以下は一般的な利用例です。

  • 描画処理
    マウスの動きに合わせて、ウィジェット上に線や図形を描画する(ただし、QPlainTextEdit は主にテキスト表示・編集用なので、直接的な描画は別のウィジェットと組み合わせることが多いかもしれません)。
  • ホバー効果の実装
    マウスカーソルが特定の要素(例えば、テキスト内の特定の単語や行番号領域)の上に来たときに、ハイライト表示などの視覚的な効果を与える。
  • カスタムカーソルの表示
    マウスカーソルの位置に応じて、表示するカーソルアイコンを変更する。
  • ドラッグ&ドロップの実装
    マウスが特定の領域に入ったときや、特定の条件を満たしたときにドラッグ操作を開始する。
  • マウスカーソル位置の追跡
    マウスカーソルの現在位置を常に取得し、ステータスバーに表示したり、他の処理で使用したりする。

基本的な再実装の例

以下は、mouseMoveEvent() を再実装して、マウスカーソルの座標をコンソールに出力する簡単な例です。

#include <QPlainTextEdit>
#include <QMouseEvent>
#include <QDebug>

class MyPlainTextEdit : public QPlainTextEdit {
protected:
    void mouseMoveEvent(QMouseEvent *event) override {
        // 親クラスの mouseMoveEvent を呼び出し、デフォルトの処理を行わせる
        QPlainTextEdit::mouseMoveEvent(event);

        // 現在のマウスカーソル位置を取得して出力
        QPoint localPos = event->pos();
        qDebug() << "マウスカーソル位置 (ローカル):" << localPos;

        // 必要に応じて、他の処理を追加
    }
};

この例では、まず親クラスの mouseMoveEvent() を呼び出すことで、QPlainTextEdit の基本的なマウス移動処理を維持しています。その後、event->pos() を使ってウィジェット内でのマウスカーソル座標を取得し、qDebug() で出力しています。

  • マウスイベントを処理する際には、親クラスの同名のイベントハンドラを適切に呼び出すことが重要です。そうしないと、ウィジェットの基本的な動作が失われる可能性があります。
  • setMouseTracking(true) を使用すると、マウスボタンが押されていない状態でも mouseMoveEvent() が頻繁に発生するため、パフォーマンスに影響を与える可能性があります。必要な場合にのみ有効にすることを推奨します。


一般的なエラーとトラブルシューティング

QPlainTextEdit::mouseMoveEvent() を使用する際に遭遇しやすいエラーや問題点、そしてその解決策について解説します。

setMouseTracking(true) の設定忘れ

  • 解決策
    QPlainTextEdit オブジェクトに対して setMouseTracking(true) を呼び出すことで、マウスボタンが押されていなくてもマウス移動イベントが発生するようになります。
  • 原因
    デフォルトでは、マウスボタンが押されていない状態でのマウス移動イベントは発生しません。
  • エラー内容
    マウスボタンを押さずにカーソルを移動しても mouseMoveEvent() が呼ばれない。
MyPlainTextEdit *plainTextEdit = new MyPlainTextEdit(this);
plainTextEdit->setMouseTracking(true);

親クラスの mouseMoveEvent() の呼び出し忘れ

  • 解決策
    再実装した mouseMoveEvent() の先頭で、必ず QPlainTextEdit::mouseMoveEvent(event); を呼び出すようにします。
  • 原因
    再実装した mouseMoveEvent() 内で、親クラス (QPlainTextEdit) の mouseMoveEvent() を呼び出していないため、デフォルトのイベント処理が行われなくなっている。
  • エラー内容
    mouseMoveEvent() を再実装した際に、テキストの選択やカーソルの移動など、QPlainTextEdit 本来の基本的なマウス操作が機能しなくなる。
void MyPlainTextEdit::mouseMoveEvent(QMouseEvent *event) override {
    QPlainTextEdit::mouseMoveEvent(event); // 親クラスの処理を呼び出す
    // ここにカスタムの処理を追加
}

イベントオブジェクト (QMouseEvent *event) の誤った使用

  • 解決策
    • event->pos() はウィジェット内での相対座標、event->globalPos() はスクリーン座標であることを理解して使い分ける。
    • event->buttons() はイベント発生時に押されていたボタンの状態のビットマスクであり、特定のボタンが押されているかを確認するにはビット演算 (&) を使用する。
    • event->modifiers() も同様に、修飾キーの状態を確認するにはビット演算を使用する。
    • イベントが発生した時点での情報を取得しているので、イベントハンドラ内で即座に利用する。
  • 原因
    QMouseEvent オブジェクトのメソッド (pos(), globalPos(), buttons(), modifiers() など) の使い方を間違えている、または意図しないタイミングでこれらの情報を参照している。
  • エラー内容
    マウスカーソルの位置やボタンの状態などを正しく取得できない。
void MyPlainTextEdit::mouseMoveEvent(QMouseEvent *event) override {
    QPlainTextEdit::mouseMoveEvent(event);
    QPoint localPos = event->pos();
    if (event->buttons() & Qt::LeftButton) {
        qDebug() << "左ボタンを押しながら移動中:" << localPos;
    }
}

不要な処理によるパフォーマンス低下

  • 解決策
    • mouseMoveEvent() 内では、できるだけ軽量な処理のみを行うようにする。
    • 重い処理が必要な場合は、タイマー (QTimer) を利用して一定間隔で処理を行う、またはマウスボタンが押されたなどの特定の条件でのみ実行するようにする。
    • イベントフィルタ (eventFilter()) を使用して、より広範なイベント処理を行うことも検討する。
  • 原因
    mouseMoveEvent() はマウスが少しでも動くと頻繁に呼び出されるため、複雑な計算や時間のかかる処理を行うと、描画や他のイベント処理が滞ってしまう。
  • エラー内容
    mouseMoveEvent() 内で重い処理を実行すると、GUIの反応が悪くなる。

意図しないウィジェットがマウスイベントを受け取っている

  • 解決策
    • ウィジェットの親子の関係やスタック順序を確認する。
    • イベントが他のウィジェットに伝播している場合は、event->ignore()event->accept() を適切に呼び出して、イベントの伝播を制御する。ただし、mouseMoveEvent() の場合は通常 accept() する必要はありません。
  • 原因
    ウィジェットの重なり順序や、マウスイベントの伝播が意図した通りになっていない。
  • エラー内容
    QPlainTextEdit 上でマウスを動かしているのに、期待した動作が起こらない、または別のウィジェットのイベントハンドラが呼ばれている。

カスタムカーソルの設定に関する問題

  • 解決策
    • QApplication::setOverrideCursor() を使用してアプリケーション全体のカーソルを変更するか、ウィジェットに対して setCursor() を使用して特定のウィジェットのカーソルを変更する。
    • カーソルをリセットする必要がある場合は、QApplication::restoreOverrideCursor() または unsetCursor() を呼び出す。
    • mouseMoveEvent() 内で頻繁にカーソルを変更するとパフォーマンスに影響する可能性があるため、必要な場合にのみ行う。
  • 原因
    カーソルの設定タイミングが不適切、または他の場所でカーソルが再設定されている。
  • エラー内容
    mouseMoveEvent() 内でカスタムカーソルを設定しようとしても、意図した通りに表示されない。

ドラッグ&ドロップ関連の問題

  • 解決策
    • ドラッグを開始する条件(通常はマウスボタンが押された状態での移動)を正しく判定する。
    • QDrag オブジェクトを作成し、setMimeData() でドラッグするデータを設定する。
    • start() メソッドを呼び出してドラッグを開始する。
    • ドロップを受け付ける側のウィジェットで、dragEnterEvent(), dragMoveEvent(), dropEvent() などのイベントハンドラを適切に実装する。
  • 原因
    ドラッグの開始条件、ドラッグデータの設定、ドロップ先の処理などが正しく実装されていない。
  • エラー内容
    mouseMoveEvent() を利用してドラッグ&ドロップ機能を実装しようとしたが、正しく動作しない。
  • 簡単なテストコード
    問題を切り分けるために、最小限のコードで再現するテストプログラムを作成する。
  • Qtのドキュメント参照
    QPlainTextEdit クラスや関連するクラス ( QMouseEvent, QWidget など) の公式ドキュメントをよく読み、各メソッドの役割や使い方を理解する。
  • ステップ実行
    デバッガを使用して、mouseMoveEvent() がどのように実行されているかをステップごとに確認する。
  • デバッグ出力
    qDebug() を使用して、マウスイベントが発生しているか、イベントオブジェクトの内容 (座標、ボタンの状態など) が期待通りかを確認する。


例1: マウスカーソル位置の追跡と表示 (ステータスバー)

この例では、QPlainTextEdit 内でマウスカーソルが移動するたびに、その座標をウィンドウのステータスバーに表示します。

#include <QApplication>
#include <QMainWindow>
#include <QPlainTextEdit>
#include <QStatusBar>
#include <QMouseEvent>
#include <QString>

class MainWindow : public QMainWindow {
public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
        plainTextEdit = new QPlainTextEdit(this);
        setCentralWidget(plainTextEdit);
        statusBar()->showMessage("マウスカーソルはここにあります");
        plainTextEdit->setMouseTracking(true); // マウスボタンを押さなくてもイベントを捕捉
    }

protected:
    void mouseMoveEvent(QMouseEvent *event) override {
        // QPlainTextEdit の mouseMoveEvent を明示的に呼び出す必要はありません
        // なぜなら、MainWindow の mouseMoveEvent は QPlainTextEdit の上で発生するからです

        // マウスカーソルのウィジェット内座標を取得
        QPoint localPos = plainTextEdit->mapFromGlobal(event->globalPos());

        // ステータスバーに座標を表示
        QString message = QString("X: %1, Y: %2").arg(localPos.x()).arg(localPos.y());
        statusBar()->showMessage(message);

        // 必要に応じて、QMainWindow のデフォルトの mouseMoveEvent を呼び出すこともできます
        QMainWindow::mouseMoveEvent(event);
    }

private:
    QPlainTextEdit *plainTextEdit;
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MainWindow w;
    w.setGeometry(100, 100, 400, 300);
    w.show();
    return a.exec();
}

説明

  1. MainWindow クラス
    QMainWindow を継承し、中央ウィジェットとして QPlainTextEdit を配置しています。
  2. コンストラクタ
    QPlainTextEdit を作成し、setCentralWidget() で設定します。ステータスバーに初期メッセージを表示し、plainTextEdit->setMouseTracking(true) を呼び出して、マウスボタンを押さなくてもマウス移動イベントを捕捉するように設定します。
  3. mouseMoveEvent(QMouseEvent *event)
    QMainWindow クラスで mouseMoveEvent() を再実装しています。マウスカーソルが QPlainTextEdit の上を移動すると、この関数が呼び出されます。
    • plainTextEdit->mapFromGlobal(event->globalPos()) を使用して、グローバル座標 (スクリーン座標) を QPlainTextEdit ウィジェット内でのローカル座標に変換しています。
    • 取得したローカル座標を QString に整形し、statusBar()->showMessage() でステータスバーに表示します。
    • QMainWindow::mouseMoveEvent(event) を呼び出すことで、QMainWindow のデフォルトのマウス移動処理も行われます。

例2: マウスカーソル下の文字のハイライト表示 (簡易版)

この例では、マウスカーソル下の文字を簡易的にハイライト表示します。実際には、テキストドキュメントの構造を考慮したより複雑な処理が必要になる場合があります。

#include <QApplication>
#include <QPlainTextEdit>
#include <QMouseEvent>
#include <QTextCursor>
#include <QTextEdit> // QTextEdit も使用可能

class MyPlainTextEdit : public QPlainTextEdit {
public:
    MyPlainTextEdit(QWidget *parent = nullptr) : QPlainTextEdit(parent) {
        setMouseTracking(true);
    }

protected:
    void mouseMoveEvent(QMouseEvent *event) override {
        QPlainTextEdit::mouseMoveEvent(event);

        // マウスカーソルの位置にあるテキストのカーソルを取得
        QPoint viewportPos = event->pos();
        QTextCursor cursor = cursorForPosition(viewportPos);

        // 現在の選択範囲をクリア
        QTextCursor clearCursor = textCursor();
        clearCursor.clearSelection();
        setTextCursor(clearCursor);

        // カーソルが有効な位置にあるか確認
        if (cursor.isValid()) {
            // カーソルを現在の位置に移動
            setTextCursor(cursor);
            // 単語を選択 (簡易的なハイライト)
            textCursor().select(QTextCursor::WordUnderCursor);
        }
    }
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MyPlainTextEdit w;
    w.setPlainText("これはテストのテキストです。\nマウスを移動してみてください。");
    w.setGeometry(100, 100, 400, 300);
    w.show();
    return a.exec();
}

説明

  1. MyPlainTextEdit クラス
    QPlainTextEdit を継承しています。
  2. コンストラクタ
    setMouseTracking(true) を呼び出し、マウス移動イベントを捕捉するように設定します。
  3. mouseMoveEvent(QMouseEvent *event)
    • QPlainTextEdit::mouseMoveEvent(event) を呼び出し、デフォルトの処理を行います。
    • event->pos() でビューポート内のマウス座標を取得します。
    • cursorForPosition(viewportPos) を使用して、その座標に対応する QTextCursor を取得します。
    • まず、現在の選択範囲をクリアします。
    • 取得したカーソルが有効 (isValid()) であれば、そのカーソルを現在のテキストカーソルに設定し、textCursor().select(QTextCursor::WordUnderCursor) を呼び出して、カーソル下の単語を選択状態にします (簡易的なハイライト)。

注意点
この例は非常に簡易的なハイライトであり、単語の区切りは空白文字などに依存します。より複雑なハイライト処理を行う場合は、テキストドキュメントの構造や書式情報を考慮する必要があります。

例3: マウスドラッグによる矩形選択 (描画)

この例では、マウスボタンを押しながら移動することで矩形を描画する簡単な例です。実際のテキスト選択とは異なります。

#include <QApplication>
#include <QPlainTextEdit>
#include <QMouseEvent>
#include <QPainter>
#include <QRect>

class MyPlainTextEdit : public QPlainTextEdit {
public:
    MyPlainTextEdit(QWidget *parent = nullptr) : QPlainTextEdit(parent) {
        setMouseTracking(true);
    }

protected:
    void mousePressEvent(QMouseEvent *event) override {
        QPlainTextEdit::mousePressEvent(event);
        if (event->button() == Qt::LeftButton) {
            mousePressed = true;
            startPos = event->pos();
            endPos = startPos;
            update(); // 再描画を要求
        }
    }

    void mouseMoveEvent(QMouseEvent *event) override {
        QPlainTextEdit::mouseMoveEvent(event);
        if (mousePressed) {
            endPos = event->pos();
            update(); // 再描画を要求
        }
    }

    void mouseReleaseEvent(QMouseEvent *event) override {
        QPlainTextEdit::mouseReleaseEvent(event);
        if (event->button() == Qt::LeftButton) {
            mousePressed = false;
            update(); // 最終的な描画
        }
    }

    void paintEvent(QPaintEvent *event) override {
        QPlainTextEdit::paintEvent(event);

        if (mousePressed) {
            QPainter painter(viewport());
            painter.setPen(Qt::red);
            painter.setBrush(QColor(255, 0, 0, 50)); // 半透明の赤
            QRect rect(startPos, endPos);
            painter.drawRect(rect.normalized()); // 開始点と終了点を正規化して矩形を描画
        }
    }

private:
    bool mousePressed = false;
    QPoint startPos;
    QPoint endPos;
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MyPlainTextEdit w;
    w.setPlainText("マウスをドラッグしてみてください。赤い矩形が描画されます。");
    w.setGeometry(100, 100, 400, 300);
    w.show();
    return a.exec();
}
  1. MyPlainTextEdit クラス
    QPlainTextEdit を継承しています。
  2. メンバ変数
    mousePressed (マウスボタンが押されているか)、startPos (ドラッグ開始位置)、endPos (現在のマウス位置) を保持します。
  3. mousePressEvent(QMouseEvent *event)
    左ボタンが押されたときに mousePressedtrue に設定し、開始位置を記録し、再描画を要求します。
  4. mouseMoveEvent(QMouseEvent *event)
    mousePressedtrue の間、現在のマウス位置を endPos に記録し、再描画を要求します。
  5. mouseReleaseEvent(QMouseEvent *event)
    左ボタンが離されたときに mousePressedfalse に設定し、最終的な描画のために再描画を要求します。
  6. paintEvent(QPaintEvent *event)
    • まず、親クラスの paintEvent() を呼び出し、テキストの描画を行います。
    • mousePressedtrue の場合、QPainter を作成し、開始点 startPos と終了点 endPos から定義される矩形を描画します。rect.normalized() を使用して、開始点と終了点の順序に関わらず正しい矩形を描画するようにしています。


QPlainTextEdit::cursorPositionChanged() シグナル

  • 利用例
    • 現在のカーソル位置に対応する行番号や文字位置をステータスバーに表示する。
    • カーソル位置に応じて、特定のコードブロックや情報をハイライト表示する。
  • 欠点
    • マウスカーソルが移動してもテキストカーソルが移動しない場合(例えば、テキストがない領域をマウスが移動している場合や、マウスボタンが押されていない状態で setMouseTracking(false) の場合)は通知されません。
    • マウスカーソルの具体的な座標情報は直接的には得られません。
#include <QApplication>
#include <QPlainTextEdit>
#include <QMainWindow>
#include <QStatusBar>
#include <QTextCursor>

class MainWindow : public QMainWindow {
public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
        plainTextEdit = new QPlainTextEdit(this);
        setCentralWidget(plainTextEdit);
        statusBar()->showMessage("カーソル位置:");

        // カーソル位置が変更されたときのシグナルをスロットに接続
        connect(plainTextEdit, &QPlainTextEdit::cursorPositionChanged, this, &MainWindow::updateCursorPosition);

        plainTextEdit->setPlainText("これはテストのテキストです。\nカーソルを移動してみてください。");
    }

private slots:
    void updateCursorPosition() {
        QTextCursor cursor = plainTextEdit->textCursor();
        int line = cursor.blockNumber() + 1; // 行番号 (0始まりなので+1)
        int column = cursor.positionInBlock() + 1; // 行内の文字位置 (0始まりなので+1)
        statusBar()->showMessage(QString("行: %1, 列: %2").arg(line).arg(column));
    }

private:
    QPlainTextEdit *plainTextEdit;
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MainWindow w;
    w.setGeometry(100, 100, 400, 300);
    w.show();
    return a.exec();
}

イベントフィルタ (eventFilter())

  • 利用例
    • アプリケーション全体で特定のマウス操作を監視し、特別な処理を行う。
    • 複数の QPlainTextEdit ウィジェットで共通のマウス移動処理を実装する。
  • 欠点
    • コードが少し複雑になる場合があります。
    • フィルタする対象やイベントの種類を明示的に指定する必要があります。
#include <QApplication>
#include <QPlainTextEdit>
#include <QMainWindow>
#include <QStatusBar>
#include <QMouseEvent>
#include <QDebug>

class MainWindow : public QMainWindow {
public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
        plainTextEdit = new QPlainTextEdit(this);
        setCentralWidget(plainTextEdit);
        statusBar()->showMessage("マウスイベント監視中");
        plainTextEdit->setMouseTracking(true); // イベントフィルタでも通常は必要

        // plainTextEdit にイベントフィルタをインストール
        plainTextEdit->installEventFilter(this);
    }

protected:
    // イベントフィルタ関数をオーバーライド
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (watched == plainTextEdit && event->type() == QEvent::MouseMove) {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
            QPoint localPos = mouseEvent->pos();
            statusBar()->showMessage(QString("マウス移動 (フィルタ): X: %1, Y: %2").arg(localPos.x()).arg(localPos.y()));
            // ここで true を返すとイベントは plainTextEdit に伝播しません
            // false を返すと通常のイベント処理が行われます
            return false;
        }
        // その他のイベントは通常通り処理
        return QMainWindow::eventFilter(watched, event);
    }

private:
    QPlainTextEdit *plainTextEdit;
};

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MainWindow w;
    w.setGeometry(100, 100, 400, 300);
    w.show();
    return a.exec();
}

QGraphicsView と QGraphicsScene

  • 利用例
    • テキストとグラフィック要素が混在するようなエディタ。
    • ダイアグラムやフローチャートの中にテキストを埋め込むようなアプリケーション。
    • カスタムのテキストレンダリングやインタラクションが必要な場合。
  • 欠点
    • QPlainTextEdit ほどテキスト編集に特化していないため、高度なテキスト編集機能(例えば、複雑なテキスト選択、undo/redoなど)を自力で実装する必要がある場合があります。
    • 比較的複雑な構造になるため、簡単なテキスト表示・編集にはオーバーエンジニアリングになる可能性があります。
  • 利点
    • マウスイベントだけでなく、キーボードイベント、ドラッグ&ドロップなど、より柔軟なイベント処理が可能です。
    • グラフィカルな要素(図形、画像、カスタムアイテムなど)とテキストを自由に組み合わせたインタフェースを構築できます。
    • 拡大縮小、回転などのビュー変換が容易です。
  • 利用例
    • テキストが選択されたときに何らかの処理を行う (selectionChanged() シグナル)。
    • 他のウィジェットやアプリケーションからテキストがドラッグされてきたときの処理 (dragEnterEvent(), dragMoveEvent(), dropEvent()).
  • 欠点
    • 特定の目的に限定されるため、汎用的なマウス追跡などには向きません。
  • 利点
    • 特定のタスクに特化したAPIを利用することで、より簡潔で意図が明確なコードを書くことができます。
    • 低レベルなマウス移動イベントを直接扱うよりも、抽象化されたレベルで問題を解決できます。