QPlainTextEditのワードラップを究める:wordWrapModeの代替手段とカスタム折り返しロジック

2025-05-26

QPlainTextEditは、Qtにおけるプレーンテキスト(装飾されていないテキスト)の表示・編集ウィジェットです。このウィジェットは、大量のテキストを効率的に扱うように最適化されています。

wordWrapModeは、QPlainTextEditに表示されるテキストが、ウィジェットの幅を超えた場合にどのように折り返されるかを制御するプロパティです。

wordWrapModeが表すもの

このプロパティは、QTextOption::WrapModeという列挙型(enum)の値を取ります。主な設定値は以下の通りです。

  • QTextOption::WrapAnywhere:

    • 単語の区切りを気にせず、ウィジェットの幅を超えたらすぐに改行します。
    • これにより、行の長さは常にウィジェットの幅内に収まりますが、単語が途中で分断される可能性があります。
  • QTextOption::WrapAtWordBoundaryOrAnywhere:

    • 基本的にWordWrapと同じですが、単語の境界で折り返すことができない(例えば、非常に長い単語やURLなど)場合に、単語の途中でも改行します。
    • これにより、どのような状況でもテキストがウィジェットの幅に収まることが保証されます。
  • QTextOption::WordWrap:

    • デフォルトの挙動です。
    • ウィジェットの幅に合わせて、単語の境界でテキストを折り返します。
    • これにより、単語の途中で改行されることなく、テキスト全体が可視範囲に収まるようになります。
    • 一般的なテキストエディタや文章表示に適しています。
  • QTextOption::NoWrap:

    • テキストは折り返されず、1行が非常に長くなる可能性があります。
    • ウィジェットの幅を超えた部分は、水平スクロールバーを使って表示されます。
    • ログビューアなど、1行が非常に長い場合に便利です。

wordWrapModelineWrapModeの違い

QPlainTextEditにはlineWrapModeという似たようなプロパティもありますが、これらは異なる役割を持っています。

  • lineWrapMode (QPlainTextEdit::LineWrapMode):
    • これは、テキストがウィジェットの幅に対してどのように「折り返されるか」という、より大まかな挙動を決定します。
    • QPlainTextEdit::NoWrap:完全に折り返さない(水平スクロールバーが表示される)
    • QPlainTextEdit::WidgetWidth:ウィジェットの幅に合わせて折り返す。
    • setLineWrapMode(QPlainTextEdit::WidgetWidth)を設定した場合に、具体的に「どのように」折り返すかを詳細に制御するのがwordWrapModeです。

つまり、lineWrapModeが「折り返すかどうか、そして折り返すならウィジェット幅に合わせるか」を決め、wordWrapModeが「ウィジェット幅に合わせる場合に、単語の区切りをどうするか」を決めると考えるとわかりやすいでしょう。

C++での設定例です。

#include <QApplication>
#include <QPlainTextEdit>
#include <QVBoxLayout>
#include <QWidget>
#include <QTextOption> // QTextOption::WrapMode を使うために必要

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

    QWidget window;
    QVBoxLayout *layout = new QVBoxLayout(&window);
    QPlainTextEdit *textEdit = new QPlainTextEdit(&window);

    // 長いテキストを設定
    textEdit->setPlainText("これは非常に長いテキストで、QPlainTextEdit のワードラップの挙動をテストするために書かれています。このテキストは、ウィジェットの幅を超えるとどのように折り返されるかを示します。特に、日本語のテキストの場合、単語の概念が英語とは異なるため、WordWrap の挙動が重要になることがあります。");

    // WordWrapMode を設定
    // textEdit->setWordWrapMode(QTextOption::NoWrap);                // 折り返さない
    textEdit->setWordWrapMode(QTextOption::WordWrap);               // 単語の境界で折り返す(デフォルト)
    // textEdit->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); // 単語の境界か、どこでも
    // textEdit->setWordWrapMode(QTextOption::WrapAnywhere);           // どこでも折り返す

    layout->addWidget(textEdit);
    window.setLayout(layout);
    window.resize(400, 300);
    window.show();

    return a.exec();
}


wordWrapModeが効かない/期待通りに動作しない

一般的な原因

  • テキストの動的な変更
    テキストを頻繁に動的に追加したり、削除したりする場合、レイアウトの更新が追いつかない、または期待通りのタイミングで再計算されないことがあります。
  • 非常に長い「単語」
    QTextOption::WordWrapは単語の境界で折り返そうとします。もしテキスト中にスペースやハイフンなどの区切り文字がない、非常に長い文字列(例: 長いURL、ファイルパス、ハッシュ値など)が含まれている場合、その文字列全体が1つの単語と見なされ、ウィジェットの幅を超えても折り返されずに水平スクロールバーが表示されることがあります。
  • lineWrapModeの設定が不適切
    wordWrapModeは、lineWrapModeQPlainTextEdit::WidgetWidthに設定されている場合にのみ効果を発揮します。lineWrapModeQPlainTextEdit::NoWrapに設定されていると、テキストは一切折り返されず、wordWrapModeの設定は無視されます。

トラブルシューティング

  1. lineWrapModeを確認する
    textEdit->setLineWrapMode(QPlainTextEdit::WidgetWidth); が設定されていることを確認してください。これがQPlainTextEdit::NoWrapになっていると、wordWrapModeは無効です。

  2. QTextOption::WrapAtWordBoundaryOrAnywhereまたはQTextOption::WrapAnywhereを試す
    非常に長い単語が原因で折り返されない場合は、以下のモードを試してください。

    • textEdit->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); 単語の境界を優先しつつ、どうしても折り返せない場合は単語の途中でも折り返します。
    • textEdit->setWordWrapMode(QTextOption::WrapAnywhere); 単語の区切りを無視して、ウィジェットの幅に収まるように強制的に折り返します。これにより、テキスト全体が可視範囲に収まりますが、単語が途中で分断される可能性があります。
  3. 手動で改行コードを挿入する
    もし特定の場所で確実に改行させたい場合は、プログラム側でテキストに\n(改行コード)を挿入することを検討してください。これは特に、ログ表示など、フォーマットが厳密に決まっている場合に有効です。

  4. QPlainTextEditの代わりにQTextEditを検討する(稀なケース)
    QPlainTextEditはプレーンテキストに特化しており、パフォーマンスが高いですが、テキストのレイアウトや折り返しに関してQTextEditよりも柔軟性に欠ける場合があります。非常に複雑な折り返しルールが必要な場合は、QTextEditのより高度なテキストレイアウト機能を検討することも可能ですが、一般的にはQPlainTextEditで十分です。

  5. テキスト変更後にレイアウトを強制的に更新する
    テキストをsetPlainText()などで設定した後、ウィジェットのサイズが変更された場合などにupdate()repaint()を呼び出すことで、描画を強制的に更新できる場合があります。ただし、Qtのレイアウトシステムは通常自動的にこれを処理します。

パフォーマンスの問題

一般的な原因

  • 頻繁なテキストの追加/更新
    QPlainTextEditにテキストを頻繁に追加する際(例: リアルタイムログ)、毎回レイアウトが再計算されるため、パフォーマンスに影響が出ることがあります。
  • 大量のテキストとQTextOption::WordWrap
    非常に長いテキスト(特に数万行以上)をQTextOption::WordWrapQTextOption::WrapAtWordBoundaryOrAnywhereで表示しようとすると、Qtが単語の境界を計算するために時間がかかり、パフォーマンスが低下する可能性があります。特に、1行が非常に長いテキストが多数ある場合に顕著です。

トラブルシューティング

  1. QTextOption::NoWrapまたはQTextOption::WrapAnywhereを検討する
    ログビューアなど、パフォーマンスが最優先される場合は、QTextOption::NoWrapに設定し、水平スクロールバーで対応するか、QTextOption::WrapAnywhereで単語の区切りを無視して強制的に折り返すことで、レイアウト計算のオーバーヘッドを減らすことができます。

  2. テキストの追加を最適化する

    • 一括で追加する
      多数の行を一度に追加するのではなく、まとまったデータとしてsetPlainText()またはappendPlainText()で追加する方が効率的です。
    • 最大ブロック数を設定する
      textEdit->setMaximumBlockCount(N); を設定することで、N行を超えた古い行が自動的に削除され、メモリ使用量とレンダリング負荷を抑えることができます。これは特にログビューアで有効です。
    • QTextCursorを使って効率的にテキストを挿入する
      頻繁にテキストを挿入する場合、QTextCursorを使用してinsertPlainText()insertBlock()を呼び出す方が、appendPlainText()を繰り返すよりも効率的な場合があります。
    • バックグラウンドでの処理
      非常に大量のテキストを扱う場合は、テキストの読み込みや整形を別スレッドで行い、GUIスレッドでは最終的な表示のみを行うといった工夫も考えられます。

一般的な原因

  • フォントサイズ/行間の影響
    フォントサイズや行間が非常に小さい、または非常に大きい場合に、折り返しが不自然に見えることがあります。
  • カスタムスタイルシート (QSS) の影響
    カスタムのスタイルシート(CSSのようなもの)をQPlainTextEditに適用している場合、それが原因でレイアウトや描画に予期せぬ影響を与えることがあります。

トラブルシューティング

  1. スタイルシートを一時的に無効にする
    スタイルシートが原因かどうかを確認するために、一時的にスタイルシートの適用を解除して、デフォルトの表示と比較してみてください。

  2. フォントと行間を調整する
    QPlainTextEditのフォント (textEdit->setFont(...)) や、テキストドキュメントのデフォルトフォーマット (textEdit->document()->setDefaultStyleSheet(...)textEdit->document()->defaultTextOption().setLineHeight(...)) を調整して、視覚的に問題がないか確認します。



すべての例で共通して使用する基本的なQtアプリケーションの構造を最初に示します。

// main.cpp
#include <QApplication>
#include <QMainWindow>
#include <QPlainTextEdit>
#include <QVBoxLayout>
#include <QWidget>
#include <QLabel>
#include <QComboBox> // ドロップダウンリスト用
#include <QTextOption> // QTextOption::WrapMode を使うために必要

// 長いサンプルテキスト
const QString LONG_TEXT =
    "これは非常に長いテキストであり、QPlainTextEdit のワードラップモードがどのように動作するかを示すために書かれています。ウィジェットの幅が変更されたときに、テキストがどのように折り返されるかに注目してください。特に、日本語のような言語では、スペース以外の場所でも単語の区切りが発生する可能性があるため、WordWrap の挙動は重要です。長いURLやパス、連続した英数字の文字列は、単語と認識されにくいため、WordWrapMode の影響を強く受けます。例えば、https://www.example.com/very/long/url/without/any/spaces/that/might/not/wrap/correctly/with/default/settings.html のような文字列です。";

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

    QMainWindow window;
    window.setWindowTitle("QPlainTextEdit Word Wrap Mode Example");

    QWidget *centralWidget = new QWidget(&window);
    window.setCentralWidget(centralWidget);

    QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);

    QLabel *instructionLabel = new QLabel("ワードラップモードを選択してください:", centralWidget);
    mainLayout->addWidget(instructionLabel);

    QComboBox *wrapModeComboBox = new QComboBox(centralWidget);
    wrapModeComboBox->addItem("WordWrap (デフォルト)", (int)QTextOption::WordWrap);
    wrapModeComboBox->addItem("NoWrap", (int)QTextOption::NoWrap);
    wrapModeComboBox->addItem("WrapAtWordBoundaryOrAnywhere", (int)QTextOption::WrapAtWordBoundaryOrAnywhere);
    wrapModeComboBox->addItem("WrapAnywhere", (int)QTextOption::WrapAnywhere);
    mainLayout->addWidget(wrapModeComboBox);

    QPlainTextEdit *textEdit = new QPlainTextEdit(centralWidget);
    textEdit->setPlainText(LONG_TEXT);
    // wordWrapMode を適用するには、lineWrapMode が WidgetWidth である必要があります
    textEdit->setLineWrapMode(QPlainTextEdit::WidgetWidth);
    mainLayout->addWidget(textEdit);

    // QComboBox の選択が変更されたときに wordWrapMode を更新する
    QObject::connect(wrapModeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
                     [textEdit, wrapModeComboBox](int index){
        QTextOption::WrapMode mode = (QTextOption::WrapMode)wrapModeComboBox->itemData(index).toInt();
        textEdit->setWordWrapMode(mode);
    });

    // 初期モードを設定
    textEdit->setWordWrapMode(QTextOption::WordWrap);

    window.resize(600, 400); // ウィンドウサイズを調整
    window.show();

    return a.exec();
}

コンパイルと実行

このコードをコンパイルして実行するには、CMakeLists.txt(またはqmake.proファイル)が必要です。

CMakeLists.txt の例

cmake_minimum_required(VERSION 3.14)
project(WordWrapExample LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 COMPONENTS Widgets REQUIRED)

add_executable(WordWrapExample main.cpp)

target_link_libraries(WordWrapExample PRIVATE Qt6::Widgets)

# 必要であれば、mocなどのビルドステップも追加
qt_wrap_cpp(WordWrapExample_MOC_SRCS main.cpp)
target_sources(WordWrapExample PRIVATE ${WordWrapExample_MOC_SRCS})
  1. 上記の内容でmain.cppCMakeLists.txtを作成します。
  2. プロジェクトディレクトリ内にbuildディレクトリを作成し、移動します。 mkdir build cd build
  3. CMakeを実行します。 cmake ..
  4. ビルドします。 cmake --build .
  5. 実行します。 ./WordWrapExample (macOS/Linux) または ./Debug/WordWrapExample.exe (Windows)

QTextOption::WordWrap (デフォルト)

textEdit->setWordWrapMode(QTextOption::WordWrap);
  • 挙動
    テキストはウィジェットの幅に合わせて、単語の境界で折り返されます。これが最も一般的で読みやすい設定です。

QTextOption::NoWrap

textEdit->setWordWrapMode(QTextOption::NoWrap);
// このモードが設定されている場合、lineWrapMode も NoWrap であるべき
// textEdit->setLineWrapMode(QPlainTextEdit::NoWrap); // 通常はこれも設定
  • 特徴
    ログファイルやコードのように、1行の連続性が重要な場合に適しています。行の途中で強制的に改行されることがないため、元のフォーマットが保たれます。
  • 挙動
    テキストは一切折り返されません。ウィジェットの幅を超えた部分は、水平スクロールバーが表示されて対応されます。

QTextOption::WrapAtWordBoundaryOrAnywhere

textEdit->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
  • 特徴
    長いURLやパスなど、区切り文字がない非常に長い文字列が含まれていても、必ずウィジェットの幅に収まるように折り返されます。これにより、水平スクロールバーの出現を最小限に抑えつつ、可能な限り単語の区切りを尊重します。
  • 挙動
    WordWrapに似ていますが、もし単語が非常に長く、単語の境界で折り返すことができない場合に、単語の途中でも折り返します。

QTextOption::WrapAnywhere

textEdit->setWordWrapMode(QTextOption::WrapAnywhere);
  • 特徴
    最も積極的な折り返しモードで、どのようなテキストでも必ずウィジェットの幅に収まります。ただし、単語が途中で分断される可能性があり、テキストの可読性が低下することがあります。厳密なレイアウトよりも、とにかく全体を可視範囲に収めたい場合に有用です。
  • 挙動
    単語の境界を無視して、ウィジェットの幅を超えたらすぐに改行します。


QTextEdit の使用を検討する

QPlainTextEdit は、名前が示す通り「プレーンテキスト」に特化しており、パフォーマンスのために内部的なレイアウト処理が簡略化されています。一方、QTextEdit はリッチテキストをサポートしており、より高度なレイアウトや書式設定が可能です。

  • 欠点
    QPlainTextEditに比べて、非常に大量のテキストを扱う場合のパフォーマンスが劣る可能性があります。
  • 利点
    QTextEditQPlainTextEditよりも柔軟なテキストレイアウト機能を持ちます。特に、段落ごとの異なる折り返し設定や、より複雑なカスタムレイアウトが必要な場合に有利です。

もし、単なるワードラップ以上の、より複雑なテキスト整形や表示のカスタマイズが必要であれば、QTextEdit の使用を検討する価値があります。

テキストに手動で改行コードを挿入する

これは最も直接的で、確実な方法です。表示したいテキストをQPlainTextEditに設定する前に、自分で折り返しロジックを実装し、適切な位置に\n(改行コード)を挿入します。

  • 欠点
    • ウィジェットの幅が変更されるたびに、テキスト全体を再処理して改行を再配置する必要があります。これは動的なリサイズに対応するのが複雑になることを意味します。
    • 単語の区切りや行の長さを正確に計算するために、QFontMetricsなどの低レベルな機能を使用する必要があります。

プログラミング例

#include <QApplication>
#include <QMainWindow>
#include <QPlainTextEdit>
#include <QVBoxLayout>
#include <QWidget>
#include <QFontMetrics>
#include <QDebug> // デバッグ出力用

// 任意の長いテキスト
const QString CUSTOM_TEXT =
    "これは非常に長いテキストであり、QPlainTextEdit のワードラップモードがどのように動作するかを示すために書かれています。ウィジェットの幅が変更されたときに、テキストがどのように折り返されるかに注目してください。特に、日本語のような言語では、スペース以外の場所でも単語の区切りが発生する可能性があるため、WordWrap の挙動は重要です。長いURLやパス、連続した英数字の文字列は、単語と認識されにくいため、WordWrapMode の影響を強く受けます。例えば、https://www.example.com/very/long/url/without/any/spaces/that/might/not/wrap/correctly/with/default/settings.html のような文字列です。";

QString wrapTextManually(const QString& text, int maxWidth, const QFont& font) {
    QFontMetrics fm(font);
    QString wrappedText;
    QString currentLine;
    QStringList words = text.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); // スペースで分割

    for (const QString& word : words) {
        // 現在の行に単語を追加したときの幅を計算
        int testWidth = fm.horizontalAdvance(currentLine + (currentLine.isEmpty() ? "" : " ") + word);

        if (testWidth <= maxWidth) {
            // 幅に収まるなら追加
            if (!currentLine.isEmpty()) {
                currentLine += " ";
            }
            currentLine += word;
        } else {
            // 幅に収まらない場合
            if (currentLine.isEmpty()) {
                // 行が空で、それでも単語が長すぎる場合、単語自体を分割する
                QString tempWord = word;
                while (fm.horizontalAdvance(tempWord) > maxWidth && !tempWord.isEmpty()) {
                    int charCount = 0;
                    while (charCount < tempWord.length() && fm.horizontalAdvance(tempWord.left(charCount + 1)) <= maxWidth) {
                        charCount++;
                    }
                    if (charCount == 0) { // 1文字でも収まらない場合はループを抜ける(非常に稀なケース)
                        break;
                    }
                    wrappedText += tempWord.left(charCount) + "\n";
                    tempWord = tempWord.mid(charCount);
                }
                currentLine = tempWord;
            } else {
                // 新しい行を開始
                wrappedText += currentLine + "\n";
                currentLine = word;
            }
        }
    }
    wrappedText += currentLine; // 最後の行を追加

    return wrappedText;
}

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

    QMainWindow window;
    window.setWindowTitle("Custom Word Wrap Example");

    QWidget *centralWidget = new QWidget(&window);
    window.setCentralWidget(centralWidget);

    QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);

    QPlainTextEdit *textEdit = new QPlainTextEdit(centralWidget);
    textEdit->setLineWrapMode(QPlainTextEdit::NoWrap); // カスタムラップなのでQtのラップは無効に
    textEdit->setReadOnly(true); // 読み取り専用にする場合

    mainLayout->addWidget(textEdit);

    // ウィンドウのリサイズイベントを捕捉してテキストを再ラップ
    QObject::connect(&window, &QMainWindow::resized, [&](QSize size){
        // QPlainTextEdit のビューポート幅を取得 (マージンやスクロールバーを考慮)
        int viewportWidth = textEdit->viewport()->width();
        QString wrapped = wrapTextManually(CUSTOM_TEXT, viewportWidth - 10, textEdit->font()); // マージンを少し引く
        textEdit->setPlainText(wrapped);
    });

    window.resize(600, 400);
    window.show();

    // 初回表示時にラップを適用
    // QPlainTextEdit のサイズが確定してからでないと正しい幅が取得できないため、
    // show() 後に一度シグナルをエミットするか、タイマーで遅延させる
    // 簡単のため、ここでは show() 直後に手動で呼び出す
    int viewportWidth = textEdit->viewport()->width();
    QString wrapped = wrapTextManually(CUSTOM_TEXT, viewportWidth - 10, textEdit->font());
    textEdit->setPlainText(wrapped);


    return a.exec();
}

QTextDocument と QTextLayout を使った低レベルな制御 (複雑)

QPlainTextEditは内部的にQTextDocumentを使ってテキストを管理しています。さらにその下のレイヤーではQTextLayoutが個々のブロック(段落)のレイアウトを担当しています。これらを直接操作することで、非常に詳細なテキストの整形を行うことが可能です。

しかし、これは非常に複雑であり、ほとんどの場合推奨されません。QPlainTextEditのパフォーマンス上の利点を活かせなくなる可能性があり、Qtが自動的に行ってくれる多くのレイアウト処理を手動で管理する必要があるため、バグを導入しやすくなります。

  • 難易度
    非常に高い。Qtのテキストレイアウトエンジンの深い理解が必要です。
  • ユースケース
    • 独自の複雑な改行ルール(例: プログラミング言語の特定の構文で改行する、特定の記号の前後にのみ改行を入れるなど)を実装したい場合。
    • テキストの特定の領域に異なる折り返しポリシーを適用したい場合。
  1. QPlainTextEdit::document()からQTextDocumentを取得します。
  2. QTextDocument::setDocumentLayout()を使用して、QPlainTextDocumentLayoutを継承したカスタムレイアウトクラスを設定します。
  3. カスタムレイアウトクラス内で、blockBoundingRect()drawBlock()などのメソッドをオーバーライドし、テキストブロックの描画とレイアウトを制御します。
  4. QTextLayoutを直接使用して、個々のテキスト行の幅や位置を計算します。
  • 非常に特殊で低レベルなレイアウト制御が必要な場合
    QTextDocumentQTextLayoutを直接操作する方法を検討しますが、これは最終手段と考えるべきです。
  • パフォーマンスを維持しつつ、厳密なカスタム改行ルールが必要な場合
    テキストに手動で改行コードを挿入する方法が最も現実的です。ただし、ウィンドウリサイズ時の再計算ロジックは自分で実装する必要があります。
  • より豊かなテキスト表現や複雑な書式設定が必要な場合
    QTextEditの使用を検討してください。
  • 単純な表示調整で十分な場合
    まずはQPlainTextEdit::wordWrapModeの各種設定(WordWrap, NoWrap, WrapAtWordBoundaryOrAnywhere, WrapAnywhere)を試してください。ほとんどのケースでこれで解決します。