QPlainTextEdit スクロール制御:contentOffset() とスクロールバーの連携(Qt)

2025-04-26

QPlainTextEdit::contentOffset() とは

QPlainTextEdit::contentOffset() は、QPlainTextEdit ウィジェットのコンテンツ(テキスト)の描画位置のオフセットを返す関数です。具体的には、ビューポート(表示領域)の左上隅を基準とした、コンテンツの左上隅の座標(QPointF オブジェクト)を返します。

何のために使うのか

この関数は、主に以下のような目的で使用されます。

  • ビューポートとの相対位置計算
    コンテンツ内の特定の位置が、現在のビューポートのどこに表示されているかを計算するために利用できます。
  • カスタム描画
    QPlainTextEdit の上にカスタムなグラフィックスや注釈を描画する際に、コンテンツのスクロール位置に合わせて描画位置を調整するために使用します。
  • コンテンツのスクロール位置の把握
    現在、QPlainTextEdit がどの程度スクロールされているかを知ることができます。返される QPointFx() 成分は水平方向のスクロール量、y() 成分は垂直方向のスクロール量を示します。

戻り値

QPlainTextEdit::contentOffset()QPointF 型の値を返します。この QPointF オブジェクトは以下の情報を持っています。

  • y()
    垂直方向のオフセット量(上方向へのスクロール量)。値が正の場合は、コンテンツが上にスクロールしていることを示します。
  • x()
    水平方向のオフセット量(左方向へのスクロール量)。値が正の場合は、コンテンツが左にスクロールしていることを示します。

簡単な例

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

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

    QPlainTextEdit plainTextEdit;
    plainTextEdit.setPlainText("これは長いテキストです。\n複数行にわたっています。\nスクロールバーが表示されるでしょう。\nさらにテキストを追加します。\nまだまだ追加します。\nこれで十分でしょう。");
    plainTextEdit.show();

    // スクロールバーを操作した後などに、現在のコンテンツオフセットを取得して表示
    QObject::connect(plainTextEdit.verticalScrollBar(), &QScrollBar::valueChanged,
                     [&]() {
                         QPointF offset = plainTextEdit.contentOffset();
                         qDebug() << "垂直オフセット:" << offset.y();
                     });

    QObject::connect(plainTextEdit.horizontalScrollBar(), &QScrollBar::valueChanged,
                     [&]() {
                         QPointF offset = plainTextEdit.contentOffset();
                         qDebug() << "水平オフセット:" << offset.x();
                     });

    return a.exec();
}

この例では、QPlainTextEdit の垂直および水平スクロールバーの値が変更されるたびに、contentOffset() を呼び出して現在のオフセットを表示しています。スクロールバーを動かすと、コンソールにオフセットの値が変化していくのが確認できるでしょう。



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

QPlainTextEdit::contentOffset() 自体が直接エラーを発生させることは比較的少ないですが、その使い方や関連する処理において、意図しない動作や誤解が生じることがあります。以下に、よくあるケースと対処法を挙げます。

オフセットの単位の誤解

  • トラブルシューティング
    返り値はピクセル単位であることを念頭に置いて、他の座標計算や描画処理を行いましょう。
  • 説明
    contentOffset() は、ビューポートの左上隅を基準としたコンテンツの左上隅のピクセル単位のオフセットを返します。浮動小数点数 (qreal) で返されるため、より細かいスクロール位置を表すことができます。
  • エラー
    contentOffset() が返す値の単位をピクセルではないと誤解している。

スクロールバーの値との混同

  • トラブルシューティング
    • スクロールバーの値は、コンテンツの全体的なサイズとビューポートのサイズによって決まる範囲を持ちます。
    • contentOffset() は、より直接的に描画位置を示します。
    • スクロール位置に基づいて何かを計算する場合は、どちらの値が適切か理解する必要があります。一般的には、描画位置に直接関係する場合は contentOffset() が適しています。
  • 説明
    • contentOffset() は、コンテンツ全体の描画位置のオフセットです。
    • QScrollBar::value() は、スクロールバーの現在の位置(通常は 0 から最大値までの整数)です。
  • エラー
    contentOffset() の値と QScrollBar::value() の値を同じものとして扱ってしまう。

初期値の扱い

  • トラブルシューティング
    初期状態のオフセットも考慮して処理を行う必要がある場合は、contentOffset() の値を実際に取得して確認しましょう。
  • 説明
    テキストの内容や初期状態によっては、わずかにオフセットが発生している場合があります。例えば、最初の行が完全に表示されないような場合などです。
  • 誤解
    QPlainTextEdit が最初に表示されたとき、contentOffset() が常に (0, 0) であると期待している。

コンテンツの変更によるオフセットの変化

  • トラブルシューティング
    コンテンツが変更される可能性がある場合は、その都度 contentOffset() を再取得して最新の状態を把握するようにしましょう。QPlainTextEdittextChanged() シグナルなどを利用して、コンテンツ変更時に必要な処理をトリガーできます。
  • 説明
    コンテンツのサイズが変わると、スクロール範囲や描画位置も変わるため、以前のオフセット値は現在の状態を正しく反映しない可能性があります。
  • 問題
    テキストの追加や削除など、コンテンツが変更された後に、以前に取得したオフセット値が意味を持たなくなる。

カスタムビューポートとの連携

  • トラブルシューティング
    カスタムビューポートのドキュメントや実装をよく理解し、contentOffset() の値がどのように影響するかを確認しましょう。
  • 説明
    カスタムビューポートを使用している場合でも、contentOffset()QPlainTextEdit 自身のコンテンツ描画位置のオフセットを返します。カスタムビューポートの描画処理と連携させる場合は、それぞれの座標系を正しく理解し、変換する必要があります。
  • 問題
    QPlainTextEdit にカスタムのビューポートを設定している場合に、contentOffset() の意味合いを正しく理解していない。

スクロール処理との競合

  • トラブルシューティング
    スクロール処理の結果に基づいて何かを行う場合は、QScrollBar::valueChanged() シグナルなどを利用して、スクロールが完了したタイミングで処理を行うようにしましょう。
  • 説明
    スクロール処理は非同期的に行われる場合があり、setValue() を呼び出した直後に描画が完了しているとは限りません。
  • 問題
    プログラムから強制的にスクロール処理(例えば verticalScrollBar()->setValue() など)を行った直後に contentOffset() を呼び出すと、期待した値が得られないことがある。

浮動小数点数の比較

  • トラブルシューティング
    浮動小数点数を比較する場合は、一定の許容範囲(イプシロン)を設けて比較するようにしましょう。例えば、qAbs(offset1.x() - offset2.x()) < epsilon のようにします。
  • 説明
    浮動小数点数は、内部表現の都合上、完全に一致するとは限りません。
  • 問題
    contentOffset() が返す QPointF の値を直接 == 演算子などで比較すると、わずかな誤差によって期待通りに動作しないことがある。
  • グラフィックデバッガの利用
    Qt Creator に付属しているグラフィックデバッガを使用すると、ウィジェットの描画状態や座標変換などを視覚的に確認できます。
  • qDebug() の活用
    contentOffset() の値を頻繁に qDebug() で出力して、実際の動作を確認しましょう。スクロール操作やコンテンツの変更に応じて値がどのように変化するかを観察することが重要です。


例1: スクロール位置の追跡と表示

この例では、QPlainTextEdit のスクロールバーの値が変更されるたびに contentOffset() を呼び出し、現在の水平および垂直オフセット値をコンソールに出力します。

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

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

    QPlainTextEdit plainTextEdit;
    plainTextEdit.setPlainText("これは非常に長いテキストです。\n複数行にわたっており、スクロールバーが表示されます。\nさらに多くのテキストを追加して、スクロール範囲を広げます。\nまだまだ続きます。\nこれで十分でしょう。");
    plainTextEdit.show();

    // 垂直スクロールバーの値が変更されたときの処理
    QObject::connect(plainTextEdit.verticalScrollBar(), &QScrollBar::valueChanged,
                     [&]() {
                         QPointF offset = plainTextEdit.contentOffset();
                         qDebug() << "垂直スクロールオフセット:" << offset.y();
                     });

    // 水平スクロールバーの値が変更されたときの処理
    QObject::connect(plainTextEdit.horizontalScrollBar(), &QScrollBar::valueChanged,
                     [&]() {
                         QPointF offset = plainTextEdit.contentOffset();
                         qDebug() << "水平スクロールオフセット:" << offset.x();
                     });

    return a.exec();
}

説明

  • 取得した QPointF オブジェクトの y() 成分(垂直オフセット)と x() 成分(水平オフセット)を qDebug() で出力します。
  • ラムダ関数の中で plainTextEdit.contentOffset() を呼び出し、現在のコンテンツオフセットを取得します。
  • QScrollBar::valueChanged シグナルにラムダ関数を接続します。このシグナルは、スクロールバーの値が変更されるたびに発行されます。
  • verticalScrollBar()horizontalScrollBar() を使って、それぞれのスクロールバーオブジェクトを取得します。
  • QPlainTextEdit に長いテキストを設定し、スクロールバーが表示されるようにします。

実行結果

QPlainTextEdit のスクロールバーを操作すると、コンソールにスクロール量に応じたオフセット値がリアルタイムに表示されます。

例2: スクロール位置に基づいてカスタム描画を行う

この例では、QPlainTextEdit の上に、現在のスクロール位置に合わせて移動する赤い四角形を描画します。

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

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

protected:
    void paintEvent(QPaintEvent *event) override
    {
        QPlainTextEdit::paintEvent(event); // デフォルトの描画処理を行う

        QPainter painter(viewport());
        painter.setBrush(Qt::red);

        QPointF offset = contentOffset();
        QRectF rect(10 - offset.x(), 10 - offset.y(), 50, 50); // オフセットを考慮して四角形の位置を調整
        painter.drawRect(rect);
    }
};

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

    CustomPlainTextEdit plainTextEdit;
    plainTextEdit.setPlainText("これは非常に長いテキストです。\n複数行にわたっており、スクロールバーが表示されます。\nさらに多くのテキストを追加して、スクロール範囲を広げます。\nまだまだ続きます。\nこれで十分でしょう。");
    plainTextEdit.show();

    return a.exec();
}

説明

  • painter.drawRect(rect) で四角形を描画します。
  • 描画する赤い四角形の QRectF を作成する際に、取得したオフセット値を x()y() 成分から減算しています。これにより、スクロールに合わせて四角形が相対的に同じ位置に描画されるように見えます。
  • contentOffset() を呼び出して現在のスクロールオフセットを取得します。
  • 次に QPainter を作成し、ビューポートに対して描画を行います。
  • paintEvent() の中で、まず QPlainTextEdit::paintEvent(event) を呼び出してデフォルトのテキスト描画を行います。
  • CustomPlainTextEdit クラスは QPlainTextEdit を継承し、paintEvent() をオーバーライドしています。

実行結果

QPlainTextEdit をスクロールすると、赤い四角形がテキストと一緒に移動するように見えます。実際には、四角形は常にビューポート内の固定された座標に描画されていますが、コンテンツのオフセットに合わせて描画位置を調整することで、そのような効果を実現しています。

例3: 特定のテキスト位置がビューポートに表示されているか確認する

この例は少し複雑になりますが、contentOffset() を利用して、QPlainTextEdit 内の特定のテキスト位置(例えば、特定の行の先頭)が現在のビューポートに表示されているかどうかを判定する考え方を示します。

#include <QApplication>
#include <QPlainTextEdit>
#include <QScrollBar>
#include <QDebug>
#include <QTextBlock>

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

    QPlainTextEdit plainTextEdit;
    QString text = "行1\n行2\n行3\n行4\n行5\n行6\n行7\n行8\n行9\n行10";
    plainTextEdit.setPlainText(text);
    plainTextEdit.show();

    // 特定の行番号 (ここでは5行目)
    int targetLineNumber = 5;

    QObject::connect(plainTextEdit.verticalScrollBar(), &QScrollBar::valueChanged,
                     [&, targetLineNumber]() {
                         QTextBlock block = plainTextEdit.document()->findBlockByLineNumber(targetLineNumber - 1); // 行番号は0から始まる
                         if (block.isValid()) {
                             QRectF blockRect = plainTextEdit.blockBoundingGeometry(block).translated(plainTextEdit.contentOffset());
                             QRectF viewportRect = plainTextEdit.viewport()->rect();

                             if (viewportRect.intersects(blockRect)) {
                                 qDebug() << "行" << targetLineNumber << "は表示されています。";
                             } else {
                                 qDebug() << "行" << targetLineNumber << "は表示されていません。";
                             }
                         }
                     });

    return a.exec();
}

説明

  • スクロールバーの値が変更されるたびに、以下の処理を行います。
    • plainTextEdit.document()->findBlockByLineNumber(targetLineNumber - 1) を使って、指定された行番号の QTextBlock オブジェクトを取得します(行番号は0から始まるため -1 します)。
    • block.isValid() で取得したブロックが有効かどうかを確認します。
    • plainTextEdit.blockBoundingGeometry(block) でブロックのローカル座標における矩形を取得します。
    • translated(plainTextEdit.contentOffset()) を使って、ブロックの矩形を現在のコンテンツオフセットに合わせて移動させ、グローバル座標系における位置を求めます。
    • plainTextEdit.viewport()->rect() でビューポートの矩形を取得します。
    • viewportRect.intersects(blockRect) で、ブロックの矩形がビューポートの矩形と交差しているかどうか(つまり、表示されているかどうか)を判定します。
    • 結果に応じてメッセージを qDebug() で出力します。
  • targetLineNumber で確認したい行番号を指定します。
  • QPlainTextEdit に複数行のテキストを設定します。

実行結果

QPlainTextEdit をスクロールすると、指定した行がビューポートに表示されたり隠れたりするたびに、コンソールにその旨のメッセージが表示されます。



QScrollBar の値を利用する

QPlainTextEdit は内部に垂直 (verticalScrollBar()) および水平 (horizontalScrollBar()) の QScrollBar オブジェクトを持っています。これらのスクロールバーの値を利用することで、スクロールの状態を間接的に知ることができます。

  • QScrollBar::singleStep()
    1ステップ分のスクロール量を取得します。
  • QScrollBar::pageStep()
    1ページ分のスクロール量を取得します。
  • QScrollBar::maximum()
    スクロールバーの最大値を取得します。これはコンテンツのサイズとビューポートのサイズによって動的に変化します。
  • QScrollBar::minimum()
    スクロールバーの最小値(通常は 0)を取得します。
  • QScrollBar::value()
    現在のスクロールバーの位置(整数値)を取得します。通常、0 から maximum() までの範囲の値を取ります。


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

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

    QPlainTextEdit plainTextEdit;
    plainTextEdit.setPlainText("長いテキスト...\n".repeat(100));
    plainTextEdit.show();

    QObject::connect(plainTextEdit.verticalScrollBar(), &QScrollBar::valueChanged,
                     [&](int value) {
                         int maximum = plainTextEdit.verticalScrollBar()->maximum();
                         qDebug() << "垂直スクロールバー値:" << value << " / 最大値:" << maximum;
                         // contentOffset() の y() に対応する情報を計算するには、
                         // テキストの高さや行の高さなどを考慮する必要があります。
                     });

    return a.exec();
}

利点

  • スクロール操作に応じた処理を valueChanged() シグナルで簡単にトリガーできます。
  • スクロールバーの状態を直接的に把握できます。

欠点

  • contentOffset() のように、描画位置のオフセットを直接ピクセル単位で取得することはできません。スクロールバーの値からオフセットを計算するには、テキストのレイアウト情報(行の高さなど)を知る必要があります。

QTextCursor と QTextBlock を利用する

テキストの特定の位置に関する情報を取得し、それに基づいてビューポートとの関係を間接的に判断できます。

  • QPlainTextEdit::blockBoundingGeometry(const QTextBlock &block)
    指定された QTextBlock のローカル座標における矩形を取得します。これを contentOffset() と組み合わせることで、ビューポートに対する位置を計算できます。
  • QPlainTextEdit::firstVisibleBlock()
    現在ビューポートの最上部に部分的にでも表示されている最初の QTextBlock を取得します。
  • QPlainTextEdit::cursorRect(const QTextCursor &cursor)
    指定されたテキストカーソルの位置にある矩形(ビューポートに対する相対座標)を取得します。


#include <QApplication>
#include <QPlainTextEdit>
#include <QTextCursor>
#include <QTextBlock>
#include <QDebug>

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

    QPlainTextEdit plainTextEdit;
    QString text = "行1\n行2\n行3\n行4\n行5\n行6\n行7\n行8\n行9\n行10";
    plainTextEdit.setPlainText(text);
    plainTextEdit.show();

    QObject::connect(plainTextEdit.verticalScrollBar(), &QScrollBar::valueChanged,
                     [&]() {
                         QTextBlock firstBlock = plainTextEdit.firstVisibleBlock();
                         if (firstBlock.isValid()) {
                             int lineNumber = firstBlock.blockNumber() + 1;
                             QRectF blockRect = plainTextEdit.blockBoundingGeometry(firstBlock).translated(plainTextEdit.contentOffset());
                             qDebug() << "最初の表示ブロック (行):" << lineNumber << ", Yオフセット:" << blockRect.top();
                         }
                     });

    return a.exec();
}

説明

  • ブロックが有効であれば、その行番号と、blockBoundingGeometry() で取得した矩形を contentOffset() でオフセットして、ビューポートの相対的な Y 座標を取得します。
  • スクロールバーの値が変更されるたびに、firstVisibleBlock() で最初に表示されているテキストブロックを取得します。

利点

  • 特定のテキスト要素がビューポートに表示されているかどうかを判断するのに役立ちます。
  • テキストの構造(ブロック、カーソル位置)に基づいて情報を取得できます。

欠点

  • contentOffset() のようにビューポート全体のオフセットを直接得るわけではありません。特定の要素に関する情報を基に間接的に判断する必要があります。

QAbstractScrollArea のメソッドを利用する (間接的)

QPlainTextEditQAbstractScrollArea を継承しています。QAbstractScrollArea クラスにもスクロールに関連するいくつかのメソッドがありますが、これらは主にスクロールバーの制御やビューポートの操作に関するものです。

  • horizontalScrollBarPolicy() / setVerticalScrollBarPolicy()
    スクロールバーの表示ポリシーを設定します。
  • ensureVisible(int x, int y, int xmargin = 50, int ymargin = 50)
    指定された矩形がビューポートに表示されるようにスクロールします。
  • viewport()->scroll(int dx, int dy)
    ビューポートの内容を相対的にスクロールします。

これらのメソッドは、contentOffset() の値を取得する代替手段というよりは、スクロールの動作を制御するためのものです。しかし、これらのメソッドの動作を理解することで、contentOffset() が示す状態の変化を間接的に把握することができます。

イベントフィルタを利用する (高度な方法)

ビューポートのイベント(例えばペイントイベント)を監視し、その情報からスクロールの状態を推測することも理論的には可能ですが、これはかなり高度な方法であり、通常は contentOffset() やスクロールバーの API を利用する方が簡単です。