TextInput.undo()

2025-05-26

Qt QuickのQML要素であるTextInputは、ユーザーがテキストを入力するための単一行のテキストフィールドを提供します。このTextInput要素は、内部的に編集履歴を保持しており、undo()メソッドを呼び出すことで、その履歴を遡って以前の状態に戻すことができます。

具体的には、以下のような操作がundo()の対象となります。

  • テキストの切り取り
  • テキストの貼り付け
  • テキストの削除
  • テキストの入力

例えば、ユーザーがテキストフィールドに「Hello World」と入力し、その後「 World」を削除して「Hello」だけになったとします。この状態でTextInput.undo()を呼び出すと、削除操作が元に戻され、再び「Hello World」と表示されます。

import QtQuick 2.0
import QtQuick.Controls 2.0

ApplicationWindow {
    visible: true
    width: 400
    height: 200
    title: "TextInput Undo Example"

    Column {
        spacing: 10
        anchors.centerIn: parent

        TextInput {
            id: myTextInput
            width: 200
            placeholderText: "何か入力してください..."
            onAccepted: console.log("Text accepted:", text)
        }

        Button {
            text: "元に戻す (Undo)"
            onClicked: myTextInput.undo()
            // canUndo プロパティがtrueの場合のみボタンを有効にする
            enabled: myTextInput.canUndo
        }

        Text {
            text: "現在のテキスト: " + myTextInput.text
        }
    }
}


TextInput.undo()は便利な機能ですが、期待通りに動作しない場合もあります。ここでは、よくある問題とその解決策を説明します。

undo()が全く効かない、または一部の操作しか元に戻せない

考えられる原因

  • acceptableInputまたはvalidatorの使用
    TextInputacceptableInputプロパティやvalidator(例えばIntValidatorなど)を使用して入力制限を設けている場合、不正な入力がそもそも履歴として記録されなかったり、undoしても制限によって元の状態に戻せないことがあります。
  • カスタム入力処理
    TextInputのデフォルトの入力処理をオーバーライドするようなカスタムロジック(例えば、onTextChangedハンドラ内でテキストを強制的に変更するなど)を使用している場合、それがundo/redoの履歴に正しく反映されないことがあります。
  • フォーカスがTextInputにない
    undo()が呼び出された時点で、対象のTextInputがアクティブ(フォーカスがある)でない場合、操作が適用されないことがあります。
  • 編集履歴が記録されていない、または失われている
    TextInputは、編集内容を自動的に履歴として記録します。しかし、何らかの理由で履歴がクリアされたり、特定の操作が履歴として認識されない場合があります。

トラブルシューティング

  • Qtバージョンの確認
    稀に、古いQtバージョンや特定のプラットフォームでundo/redo機能にバグがある場合があります。Qtのドキュメントやバグトラッカーを確認し、該当する問題がないか調べます。
  • undo()が呼び出されるタイミングの確認
    ユーザーが意図したときにundo()が呼び出されているか、誤って別の操作と競合していないか確認します。
  • カスタムロジックの見直し
    もしTextInputのテキスト内容をプログラムで操作している箇所があれば、それがundo/redoの履歴に悪影響を与えていないか確認してください。可能な限り、ユーザーの直接的な入力操作に任せるか、プログラムによる変更が履歴に正しく統合されるような設計を検討します。
  • デバッグ出力の活用
    TextInputonTextChangedonEditingFinishedなどのシグナルにデバッグ出力を追加し、テキストがどのように変更されているか、そしてundo()が呼び出されたときに何が起きているかを追跡します。
  • canUndoプロパティの確認
    Text {
        text: "Can Undo: " + myTextInput.canUndo
    }
    Button {
        text: "Undo"
        onClicked: myTextInput.undo()
        enabled: myTextInput.canUndo // これを確認
    }
    
    canUndofalseの場合、そもそも元に戻せる操作がないことを意味します。なぜfalseなのか、原因を探る手助けになります。
  • 簡単なテストケースで確認
    最小限のQMLコードで、TextInputundo()ボタンのみを配置し、シンプルなテキスト入力・削除操作でundo()が機能するかを確認します。これにより、問題が複雑なUIロジックにあるのか、基本的なTextInputの動作にあるのかを切り分けられます。

undo()で元に戻る回数が少ない、または多すぎる

考えられる原因

  • プログラムによる変更との競合
    前述の通り、プログラム的にテキストを変更すると、undo履歴が混乱することがあります。
  • undoスタックのサイズ制限
    TextInputのundo履歴の最大サイズは、通常、内部的に制限されています。非常に多くの操作を行った場合、古い履歴から順に削除されてしまうことがあります。

トラブルシューティング

  • 手動での履歴クリア
    myTextInput.clear() // テキストと同時にundo履歴もクリアされることが多い
    
    これはトラブルシューティングというより、意図的に履歴をリセットしたい場合に使います。
  • 操作の粒度の理解
    TextInputがどのような粒度でundo履歴を記録するかを理解し、ユーザーの期待に沿ったUI設計を心がけます。例えば、非常に短い間隔で行われる一連の編集(例:複数文字のタイプ)は、通常1つのアンドゥ操作として扱われます。
  • undoスタックの制御(QMLでは限定的)
    QMLのTextInputでは、C++のQTextDocumentのように直接undoスタックのサイズを制御するAPIは公開されていません。もし詳細な制御が必要な場合は、C++でカスタムのテキスト編集ウィジェットを作成し、QMLに公開することを検討する必要があります。

キーボードショートカット (Ctrl+Z / Cmd+Z) が機能しない

考えられる原因

  • プラットフォーム固有の問題
    稀に、特定のOS環境や入力メソッドでショートカットが正しく機能しない場合があります。
  • 別の要素がショートカットを捕捉している
    アプリケーション内で他の要素(例:Keys.onPressedハンドラ、Shortcut要素)がCtrl+ZCmd+Zを先に処理してしまい、TextInputにイベントが到達していない可能性があります。
  • フォーカスがない
    やはり、TextInputにフォーカスがない状態でショートカットを押しても機能しません。

トラブルシューティング

  • QML Keys.onPressedハンドラの確認
    TextInputの親要素や祖先要素で、Keys.onPressedハンドラがCtrl+Zを処理していないか確認します。処理している場合は、そのロジックを見直すか、event.accepted = falseを設定してイベントを伝播させます。
  • 競合するShortcut要素の調査
    アプリケーション全体で定義されているShortcut要素を検索し、Ctrl+ZCmd+Zと競合するものがないか確認します。もしあれば、そのショートカットを削除するか、TextInputが優先されるように調整します。
  • イベントフィルタリング
    もしApplicationWindowや他のルート要素で広範囲にキーイベントを捕捉している場合、event.accepted = falseとして、イベントをさらに下位の要素に伝播させるようにします。
  • フォーカスの確認
    ショートカットを押す前に、TextInputにフォーカスが当たっていることを確認します。デバッグ目的で、TextInputonActiveFocusChangedシグナルを監視して、フォーカス状態を確認できます。

TextInput.undo()とTextInput.redo()の同期の問題

TextInput.redo()は、undo()で元に戻した操作を再び適用するためのメソッドです。これら2つの操作が期待通りに連携しない場合もあります。

考えられる原因

  • 複雑なカスタムロジック
    前述の通り、カスタムロジックがundo/redo履歴に影響を与えることがあります。
  • undo()後に新しい操作が行われた
    undo()を実行した後、新しいテキスト入力や編集操作を行うと、それ以降のredo履歴は失われます。これは一般的なundo/redoシステムの動作です。

トラブルシューティング

  • canRedoプロパティの確認
    canRedotrueの場合にのみredo()ボタンを有効にするなど、UI側で適切に制御します。
  • undo/redoの基本的な動作を理解する
    ユーザーがundo()した後に新しい編集を行った場合、その新しい編集が起点となり、それ以前のredo履歴は消去されるのが一般的です。これはバグではなく、仕様です。
  • QMLデバッガの活用
    Qt Creatorに組み込まれているQMLデバッガを使用すると、プロパティの値、シグナルの発火、コンポーネントのライフサイクルなどを視覚的に確認でき、問題の特定に非常に役立ちます。
  • Qtドキュメントの参照
    TextInputの公式ドキュメントには、プロパティ、メソッド、シグナルに関する詳細な情報が記載されています。
  • シンプルなケースから始める
    問題が発生した場合は、関連するコードを最小限に絞ったテストケースを作成し、そこから徐々に複雑なロジックを追加していくことで、問題の切り分けが容易になります。


TextInput.undo()は、主にQt QuickのQMLでTextInput要素と共に使用されます。C++でQt Widgetsを使用している場合は、通常QTextEditQLineEditundo()スロットを使用します。

例1: 基本的なUndo/Redo機能

これは最も基本的な例で、テキストを入力し、UndoボタンとRedoボタンで操作を元に戻したり、やり直したりするものです。

// main.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: "TextInput Undo/Redo Example"

    ColumnLayout {
        anchors.centerIn: parent
        spacing: 10

        TextInput {
            id: myTextInput
            Layout.fillWidth: true
            width: 300 // レイアウトを使用しない場合
            placeholderText: "ここに入力してください..."
            font.pixelSize: 18
            selectByMouse: true // テキスト選択を可能にする

            // テキスト変更時にデバッグ出力
            onTextChanged: {
                console.log("Text changed:", myTextInput.text)
                console.log("Can Undo:", myTextInput.canUndo)
                console.log("Can Redo:", myTextInput.canRedo)
            }
        }

        RowLayout {
            spacing: 5
            Button {
                text: "元に戻す (Undo)"
                onClicked: myTextInput.undo()
                // myTextInput.canUndo が true の場合のみボタンを有効にする
                enabled: myTextInput.canUndo
            }

            Button {
                text: "やり直す (Redo)"
                onClicked: myTextInput.redo()
                // myTextInput.canRedo が true の場合のみボタンを有効にする
                enabled: myTextInput.canRedo
            }
        }

        Text {
            text: "現在のテキスト: " + myTextInput.text
            font.pixelSize: 16
        }

        // undo/redo の状態を視覚的に表示
        Text {
            text: "Undo可能: " + myTextInput.canUndo + ", Redo可能: " + myTextInput.canRedo
            font.pixelSize: 14
        }
    }
}

解説

  • onTextChangedシグナルを使って、テキストが変更されるたびに現在のテキストとcanUndo/canRedoの状態をコンソールに出力しています。これにより、内部的な状態の変化を追跡できます。
  • ButtonenabledプロパティをmyTextInput.canUndomyTextInput.canRedoにバインドすることで、元に戻せる操作がない場合はボタンを無効にし、ユーザーにフィードバックを与えています。
  • ButtononClickedハンドラでそれぞれmyTextInput.undo()myTextInput.redo()を呼び出しています。
  • TextInputid: myTextInputを設定し、他の要素から参照できるようにしています。

例2: ショートカットキー (Ctrl+Z, Ctrl+Y) との連携

一般的なアプリケーションでは、Undo/Redoはキーボードショートカット(Ctrl+Z / Cmd+ZCtrl+Y / Cmd+Y)で行われることがほとんどです。TextInputはデフォルトでこれらのショートカットを処理しますが、明示的に追加することも可能です。

// main.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: "TextInput Undo/Redo with Shortcuts"

    ColumnLayout {
        anchors.centerIn: parent
        spacing: 10

        TextInput {
            id: myTextInputShortcuts
            Layout.fillWidth: true
            width: 300
            placeholderText: "Ctrl+Z/Yで操作してください..."
            font.pixelSize: 18
            selectByMouse: true

            // TextInput自体がデフォルトでCtrl+Z/Yを処理しますが、
            // 明示的にKeys要素を使う場合は以下のように設定できます。
            // ただし、通常は不要です。
            Keys.onPressed: (event) => {
                if (event.modifiers === Qt.ControlModifier && event.key === Qt.Key_Z) {
                    if (myTextInputShortcuts.canUndo) {
                        myTextInputShortcuts.undo()
                        event.accepted = true // イベントを消費
                    }
                } else if (event.modifiers === Qt.ControlModifier && event.key === Qt.Key_Y) {
                    if (myTextInputShortcuts.canRedo) {
                        myTextInputShortcuts.redo()
                        event.accepted = true // イベントを消費
                    }
                }
            }
        }

        Text {
            text: "現在のテキスト: " + myTextInputShortcuts.text
            font.pixelSize: 16
        }

        Text {
            text: "Undo可能: " + myTextInputShortcuts.canUndo + ", Redo可能: " + myTextInputShortcuts.canRedo
            font.pixelSize: 14
        }
    }
}

解説

  • もしTextInput以外でグローバルなショートカットを定義したい場合は、ApplicationWindowやルート要素にShortcutコンポーネントを追加するか、Keys.onPressedハンドラを設定します。その場合、TextInputにイベントが到達するように、event.accepted = falseとしてイベントを伝播させる必要がないか確認してください。
  • 上記のコードでは、Keys.onPressedハンドラをTextInputに追加して、明示的にショートカットを捕捉し、undo()/redo()を呼び出す例を示しています。通常、TextInputのデフォルト動作に任せるため、この明示的なKeys.onPressedハンドラは不要です。
  • TextInputは、特に設定しなくてもデフォルトでCtrl+Z(macOSではCmd+Z)とCtrl+Y(macOSではCmd+Shift+Zが一般的なRedo)を内部的に処理し、undo()およびredo()を呼び出します。

例3: プログラムによるテキスト変更とUndo履歴への影響

プログラムでTextInputtextプロパティを直接変更した場合、その変更は通常、1つのundo操作として記録されます。

// main.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: "Programmatic Text Change and Undo"

    ColumnLayout {
        anchors.centerIn: parent
        spacing: 10

        TextInput {
            id: programmaticTextInput
            Layout.fillWidth: true
            width: 300
            placeholderText: "手動で入力後、ボタンを押してください..."
            font.pixelSize: 18
        }

        Button {
            text: "テキストを強制変更"
            onClicked: {
                programmaticTextInput.text = "Hello, Qt World! (プログラムで変更)";
                console.log("プログラムでテキストを変更しました。");
                console.log("Can Undo:", programmaticTextInput.canUndo);
            }
        }

        Button {
            text: "元に戻す (Undo)"
            onClicked: programmaticTextInput.undo()
            enabled: programmaticTextInput.canUndo
        }

        Text {
            text: "現在のテキスト: " + programmaticTextInput.text
            font.pixelSize: 16
        }
    }
}
  • ただし、注意点として、プログラムによる連続した非常に細かい変更が全て独立したundo操作として記録されるかどうかは、Qtの内部実装に依存します。 多数の細かい変更をundo()の粒度で制御したい場合は、QTextDocumentを直接操作するC++コードを検討した方が良い場合があります。
  • このプログラムによる変更も、TextInputのundo履歴に1つの操作として記録されます。したがって、その後に「元に戻す (Undo)」ボタンをクリックすると、プログラムによる変更が元に戻され、前のテキストに戻ります。
  • ユーザーがTextInputに何か入力した後、「テキストを強制変更」ボタンをクリックすると、programmaticTextInput.textが新しい値に設定されます。


C++ の QLineEdit および QTextEdit の undo() / redo() スロット

QMLのTextInputが内部的に利用しているメカニズムに似たものが、Qt Widgetsのテキスト編集ウィジェットにも備わっています。

  • QTextEdit (複数行リッチテキスト編集)

    • QTextEditは、より強力なUndo/Redo機能を提供します。これは、内部的にQTextDocumentを使用しているためです。
    • textEdit->undo(); を呼び出すことで、元に戻せます。
    • textEdit->redo(); を呼び出すことで、やり直せます。
    • QTextEditは、canUndo()canRedo()シグナルも提供しており、Undo/Redoが可能かどうかをUIに反映させるために使用できます。
    • 特徴
      QTextEditは、テキストだけでなく書式設定(フォント、色、配置など)の変更もUndo/Redo履歴に含めることができます。
    • QLineEditには、デフォルトでUndo/Redo機能が備わっています。
    • プログラムから元に戻すには、lineEdit->undo(); を呼び出します。
    • やり直すには、通常lineEdit->redo(); のような直接的なメソッドはありませんが、QLineEditQTextDocumentベースではないため、より単純な履歴管理を行います。多くの場合、undo()の後に新しい操作を行わなければ、元の状態に戻ります。

C++での例 (QTextEdit)

// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void undoTextEdit();
    void redoTextEdit();
    void updateUndoRedoButtons();

private:
    QTextEdit *textEdit;
    QPushButton *undoButton;
    QPushButton *redoButton;
};

#endif // MAINWINDOW_H

// mainwindow.cpp
#include "mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    textEdit = new QTextEdit(this);
    undoButton = new QPushButton("元に戻す (Undo)", this);
    redoButton = new QPushButton("やり直す (Redo)", this);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(textEdit);

    QHBoxLayout *buttonLayout = new QHBoxLayout;
    buttonLayout->addWidget(undoButton);
    buttonLayout->addWidget(redoButton);
    layout->addLayout(buttonLayout);

    QWidget *centralWidget = new QWidget(this);
    centralWidget->setLayout(layout);
    setCentralWidget(centralWidget);

    // シグナルとスロットの接続
    connect(undoButton, &QPushButton::clicked, this, &MainWindow::undoTextEdit);
    connect(redoButton, &QPushButton::clicked, this, &MainWindow::redoTextEdit);

    // QTextEditのundo/redo状態が変化したときにボタンを更新
    connect(textEdit, &QTextEdit::undoAvailable, this, &MainWindow::updateUndoRedoButtons);
    connect(textEdit, &QTextEdit::redoAvailable, this, &MainWindow::updateUndoRedoButtons);

    // 初期状態のボタン更新
    updateUndoRedoButtons();
}

MainWindow::~MainWindow()
{
}

void MainWindow::undoTextEdit()
{
    textEdit->undo();
}

void MainWindow::redoTextEdit()
{
    textEdit->redo();
}

void MainWindow::updateUndoRedoButtons()
{
    undoButton->setEnabled(textEdit->document()->isUndoAvailable());
    redoButton->setEnabled(textEdit->document()->isRedoAvailable());
    // もしくは、QTextEditのシグナル undoAvailable(bool) / redoAvailable(bool) を直接接続する
    // textEdit->undoAvailable は QTextDocument::isUndoAvailable() にマップされます
}

// main.cpp
#include <QApplication>
#include "mainwindow.h"

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

QTextDocument と QUndoStack を使用した高度な履歴管理

TextInputQTextEditが提供する自動Undo/Redo機能は便利ですが、より複雑なアプリケーションで複数の操作(テキスト編集、図形描画、プロパティ変更など)を横断的にUndo/Redoしたい場合や、Undo履歴の粒度を細かく制御したい場合は、QTextDocumentQUndoStackを直接利用するのが強力な方法です。

  • QUndoStack

    • QUndoStackは、一般的なUndo/Redoシステムを実装するためのフレームワークです。
    • 各操作(コマンド)はQUndoCommandのサブクラスとして定義され、スタックにプッシュされます。
    • QUndoStack::undo() を呼び出すと、スタックのトップにあるコマンドのundo()メソッドが実行されます。
    • QUndoStack::redo() を呼び出すと、直前にUndoされたコマンドのredo()メソッドが実行されます。
    • 複数の異なる種類の操作を単一のUndo/Redo履歴で管理する(例:テキスト変更と図形移動を同時にUndoする)場合に非常に強力です。
  • QTextDocument

    • QTextDocumentは、テキストとリッチテキストのコンテンツを表現するクラスです。QTextEditTextInputの内部でも使用されています。
    • QTextDocument自体がUndo/Redoスタックを管理する機能を持っています(QTextDocument::undo() / QTextDocument::redo())。
    • setUndoRedoEnabled(bool) で有効/無効を切り替えられます。

QUndoStackの概念図

[QUndoStack]
  |
  +-- [QUndoCommand A (テキスト変更)]
  |       - undo() -> 以前のテキストに戻す
  |       - redo() -> 新しいテキストに進める
  |
  +-- [QUndoCommand B (要素移動)]
  |       - undo() -> 以前の位置に戻す
  |       - redo() -> 新しい位置に進める
  |
  +-- [QUndoCommand C (プロパティ変更)]
          - undo() -> 以前のプロパティ値に戻す
          - redo() -> 新しいプロパティ値に進める

QUndoStackを使ったQMLとC++の連携の可能性

  1. C++でカスタムなQObjectを作成し、QUndoStackを管理する。
  2. このカスタムQObjectをQMLに公開する(QML_ELEMENTなどを使用)。
  3. QML側でTextInputonTextChangedなどのシグナルを監視し、テキストが変更されたときにC++側のQUndoStackに新しいQUndoCommand(例えば、テキスト変更を表すカスタムコマンド)をプッシュする。
  4. QML側から、公開されたC++オブジェクトのundo()redo()メソッドを呼び出す。

これはより高度なシナリオであり、アプリケーションの設計全体に影響します。

カスタムの履歴管理ロジック (低レベル)

特定のニッチな要件(例:非常に軽量な履歴、特定の操作のみをUndo対象にするなど)がある場合、自分でテキストの変更履歴を管理するロジックを実装することも不可能ではありません。

  • redo() のために、Undoされた操作の履歴も別に管理する。
  • undo() が呼び出されたら、リストから前の状態を取り出してTextInputに設定する。
  • ユーザーが何か入力するたびに、現在のテキストをリストに追加する。
  • QList<QString>QStack<QString> のようなデータ構造を使って、テキストの各状態を保存する。

欠点

  • リッチテキストの書式設定など、テキスト以外の要素のUndoは非常に困難になります。
  • パフォーマンスやメモリ使用量に注意が必要です。
  • この方法は、TextInputQTextDocumentが提供する組み込みのUndo/Redo機能に比べて、はるかに複雑でバグを起こしやすくなります。

推奨されるシナリオ

  • 本当に特殊な、TextInputQTextDocumentの標準機能では実現できない要件がある場合のみ検討すべきです。ほとんどの場合、既存のQtメカニズムで十分です。
  • カスタム履歴管理
    最後の手段であり、推奨されません。
  • 複雑なUndo/Redo
    アプリケーション全体で一貫したUndo/Redoシステムを構築したい場合や、テキスト以外の変更もUndoしたい場合は、C++でQUndoStackを導入するのが最も柔軟で堅牢な方法です。
  • Qt Widgets
    QLineEditQTextEditundo()/redo()スロットを利用します。特にQTextEditは強力です。
  • QMLのTextInput
    組み込みのundo()/redo()メソッドとcanUndo/canRedoプロパティをそのまま使うのが最も簡単で推奨される方法です。