Qt初心者向け:QPlainTextEdit::blockBoundingGeometry()でテキストブロックの境界を操作する

2025-04-26

具体的には、以下の点が重要です。

  • 用途
    このメソッドは、テキストブロックの位置やサイズに基づいて何かをしたい場合に役立ちます。例えば、特定の行に注釈やハイライトを追加したり、テキストブロックの位置に合わせて他のウィジェットを配置したりする場合に使用できます。
  • 座標系
    返される矩形の座標系は、QPlainTextEditのビューポート(表示領域)の座標系に基づいています。つまり、ビューポートの左上隅が原点(0, 0)となります。
  • ジオメトリ
    返される値はQRectFオブジェクトであり、これは浮動小数点数で定義された矩形を表します。この矩形は、テキストブロックの描画領域を囲む境界を示します。
  • テキストブロック(行)の境界
    QPlainTextEditは、テキストを複数のブロック(通常は行)に分割して管理します。blockBoundingGeometry()は、これらのブロックの境界を矩形(長方形)として返します。

コード例(簡単な説明付き)

#include <QApplication>
#include <QPlainTextEdit>
#include <QDebug>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QPlainTextEdit textEdit;
    textEdit.setPlainText("Hello,\nWorld!\nThis is Qt.");
    textEdit.show();

    QTextBlock block = textEdit.document()->findBlockByLineNumber(1); // 2行目のブロックを取得(0から始まる)
    if (block.isValid()) {
        QRectF rect = textEdit.blockBoundingGeometry(block);
        qDebug() << "Block bounding geometry:" << rect;
    }

    return app.exec();
}

この例では、QPlainTextEditにテキストを設定し、2行目のテキストブロックの境界矩形を取得してデバッグ出力しています。

  • qDebug()は、デバッグ情報を出力するためのQtの関数です。
  • QRectFは、浮動小数点数で定義された矩形を表すクラスです。位置とサイズを保持します。
  • QTextBlockは、テキストブロック(行)を表すクラスです。QTextDocumentからfindBlockByLineNumber()などのメソッドを使用して取得できます。


無効なブロック (QTextBlock) を渡した場合

  • トラブルシューティング
    • QTextBlock::isValid()を使用して、ブロックが有効かどうかを確認する。
    • QTextDocument内の行数を確認し、有効な行番号を使用する。
    • ブロックを取得する前に、QPlainTextEditにテキストが設定されていることを確認する。
    • コード内のQTextBlock変数のライフサイクルを確認する。
  • 原因
    • QTextDocument::findBlockByLineNumber()などで取得したブロックが、存在しない行を参照している。
    • QTextBlockオブジェクトが初期化されていない、または既に削除されている。
  • エラー
    blockBoundingGeometry()に無効なQTextBlockオブジェクトを渡すと、予期しない結果(不正な矩形やクラッシュ)が発生する可能性があります。

座標系の誤解

  • トラブルシューティング
    • ビューポートの座標系を理解し、必要に応じて座標変換を行う。
    • QPlainTextEdit::verticalScrollBar()QPlainTextEdit::horizontalScrollBar()を使用して、スクロール位置を取得し、座標を調整する。
    • QPlainTextEdit::viewport()->mapToGlobal()などの関数でグローバル座標系に変換する。
  • 原因
    • blockBoundingGeometry()が返す矩形の座標系が、QPlainTextEditのビューポートの座標系に基づいていることを理解していない。
    • スクロールやズームなどのビューポートの変換を考慮していない。
  • エラー
    返される矩形の位置が期待と異なる。

テキストの変更による影響

  • トラブルシューティング
    • テキストを変更した後、必要に応じてQPlainTextEdit::document()->blockSignals(true);QPlainTextEdit::document()->blockSignals(false);でシグナルをブロックして、処理後に再計算する。
    • テキスト変更のシグナル(QPlainTextEdit::textChanged())に接続し、ジオメトリを再計算する。
    • QPlainTextEdit::update()QPlainTextEdit::viewport()->update()を呼び出して、表示を更新する。
  • 原因
    • テキストの変更後に、ブロックのジオメトリを再計算していない。
    • QPlainTextEditの内容が動的に変化する場合、タイミングがずれる。
  • エラー
    テキストを変更した後、blockBoundingGeometry()が返す矩形が更新されない、または不正な値になる。

フォントやテキストのスタイルによる影響

  • トラブルシューティング
    • フォントやスタイルの変更後に、ジオメトリを再計算する。
    • QFontMetricsFクラスを使用して、テキストの描画サイズを正確に計算する。
  • 原因
    • フォントやスタイルによってテキストの描画サイズが変化することを考慮していない。
  • エラー
    フォントやテキストのスタイルを変更した後、矩形のサイズが期待と異なる。

パフォーマンスの問題

  • トラブルシューティング
    • 必要な場合にのみblockBoundingGeometry()を呼び出す。
    • ジオメトリをキャッシュし、不要な再計算を避ける。
    • 表示する範囲のみ計算する。
  • 原因
    • blockBoundingGeometry()は、ブロックのジオメトリを計算するため、コストがかかる。
  • エラー
    大量のテキストに対してblockBoundingGeometry()を頻繁に呼び出すと、パフォーマンスが低下する。
  • シンプルなテストケースを作成し、問題を再現させる。
  • デバッガを使用して、コードの実行をステップ実行し、変数の値を監視する。
  • qDebug()を使用して、blockBoundingGeometry()が返す矩形の値や、関連する変数の値を出力する。


例1: 指定された行の境界矩形をハイライト表示する

この例では、QPlainTextEdit内の指定された行の境界矩形を赤い枠線でハイライト表示します。

#include <QApplication>
#include <QPlainTextEdit>
#include <QPainter>
#include <QDebug>

class HighlightPlainTextEdit : public QPlainTextEdit {
public:
    HighlightPlainTextEdit(QWidget *parent = nullptr) : QPlainTextEdit(parent), highlightLine(-1) {}

    void setHighlightLine(int line) {
        highlightLine = line;
        viewport()->update(); // 再描画を要求
    }

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

        if (highlightLine >= 0 && highlightLine < document()->blockCount()) {
            QTextBlock block = document()->findBlockByLineNumber(highlightLine);
            if (block.isValid()) {
                QRectF rect = blockBoundingGeometry(block);
                QPainter painter(viewport());
                painter.setPen(Qt::red);
                painter.drawRect(rect);
            }
        }
    }

private:
    int highlightLine;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    HighlightPlainTextEdit textEdit;
    textEdit.setPlainText("Line 1\nLine 2\nLine 3\nLine 4");
    textEdit.setHighlightLine(1); // 2行目をハイライト
    textEdit.show();

    return app.exec();
}

説明

  1. HighlightPlainTextEditクラスは、QPlainTextEditを継承し、ハイライト機能を追加します。
  2. setHighlightLine()メソッドは、ハイライトする行番号を設定し、viewport()->update()で再描画を要求します。
  3. paintEvent()メソッドをオーバーライドし、描画処理を行います。
  4. document()->findBlockByLineNumber()で指定された行のQTextBlockを取得します。
  5. blockBoundingGeometry()でブロックの境界矩形を取得します。
  6. QPainterを使用して、赤い枠線で矩形を描画します。

例2: クリックされた行の情報を表示する

この例では、QPlainTextEdit内でクリックされた行の行番号と境界矩形をデバッグ出力します。

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

class ClickInfoPlainTextEdit : public QPlainTextEdit {
public:
    ClickInfoPlainTextEdit(QWidget *parent = nullptr) : QPlainTextEdit(parent) {}

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

        QPoint pos = event->pos();
        QTextBlock block = firstVisibleBlock();
        int lineNumber = block.blockNumber();

        while (block.isValid()) {
            QRectF rect = blockBoundingGeometry(block).translated(contentOffset());
            if (rect.contains(pos)) {
                qDebug() << "Clicked line:" << lineNumber;
                qDebug() << "Block geometry:" << rect;
                break;
            }
            block = block.next();
            lineNumber++;
        }
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    ClickInfoPlainTextEdit textEdit;
    textEdit.setPlainText("Line 1\nLine 2\nLine 3\nLine 4");
    textEdit.show();

    return app.exec();
}

説明

  1. ClickInfoPlainTextEditクラスは、QPlainTextEditを継承し、クリック情報を表示する機能を追加します。
  2. mousePressEvent()メソッドをオーバーライドし、マウスのクリックイベントを処理します。
  3. firstVisibleBlock()で最初に表示されているブロックを取得し、行番号を初期化します。
  4. whileループで各ブロックを調べ、クリックされた座標がブロックの境界矩形内にあるかどうかを判定します。
  5. blockBoundingGeometry(block).translated(contentOffset())でスクロールを考慮した矩形を取得します。
  6. クリックされた行の行番号と境界矩形をqDebug()で出力します。

例3:スクロール位置に追従するウィジェットを表示する。

この例では、QPlainTextEditの特定の行の右側に追従するウィジェットを表示します。

#include <QApplication>
#include <QPlainTextEdit>
#include <QLabel>
#include <QVBoxLayout>
#include <QScrollBar>

class FollowingWidget : public QWidget {
public:
    FollowingWidget(QPlainTextEdit *textEdit, int lineNumber, QWidget *parent = nullptr) : QWidget(parent), textEdit(textEdit), lineNumber(lineNumber) {
        label = new QLabel("Follows Line", this);
        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(label);
        setLayout(layout);
        updatePosition();
        connect(textEdit->verticalScrollBar(), &QScrollBar::valueChanged, this, &FollowingWidget::updatePosition);
    }

private:
    void updatePosition() {
        QTextBlock block = textEdit->document()->findBlockByLineNumber(lineNumber);
        if (block.isValid()) {
            QRectF rect = textEdit->blockBoundingGeometry(block).translated(textEdit->contentOffset());
            move(textEdit->viewport()->mapToGlobal(QPoint(rect.right(), rect.top())));
        }
    }
    QPlainTextEdit *textEdit;
    int lineNumber;
    QLabel *label;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QWidget window;
    QVBoxLayout *mainLayout = new QVBoxLayout(&window);
    QPlainTextEdit *textEdit = new QPlainTextEdit(&window);
    textEdit->setPlainText("Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10\nLine 11\nLine 12\nLine 13\nLine 14\nLine 15\nLine 16\nLine 17\nLine 18\nLine 19\nLine 20");
    mainLayout->addWidget(textEdit);
    FollowingWidget *follower = new FollowingWidget(textEdit, 5, &window);
    follower->show();
    window.show();
    return app.exec();
}
  1. FollowingWidgetクラスは、特定の行に追従するウィジェットを作成します。
  2. updatePosition()メソッドは、スクロール位置に基づいてウィジェットの位置を更新します。
  3. connectを使って、スクロールバーの変更を検知して、updatePosition()を呼び出します。
  4. blockBoundingGeometry()contentOffset()でスクロールを考慮した矩形を取得します。
  5. mapToGlobal()でグローバル座標系に変換し、ウィジェットを移動します。