TextInput.undo()
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の使用
TextInput
のacceptableInput
プロパティやvalidator
(例えばIntValidator
など)を使用して入力制限を設けている場合、不正な入力がそもそも履歴として記録されなかったり、undoしても制限によって元の状態に戻せないことがあります。 - カスタム入力処理
TextInput
のデフォルトの入力処理をオーバーライドするようなカスタムロジック(例えば、onTextChanged
ハンドラ内でテキストを強制的に変更するなど)を使用している場合、それがundo/redoの履歴に正しく反映されないことがあります。 - フォーカスがTextInputにない
undo()
が呼び出された時点で、対象のTextInput
がアクティブ(フォーカスがある)でない場合、操作が適用されないことがあります。 - 編集履歴が記録されていない、または失われている
TextInput
は、編集内容を自動的に履歴として記録します。しかし、何らかの理由で履歴がクリアされたり、特定の操作が履歴として認識されない場合があります。
トラブルシューティング
- Qtバージョンの確認
稀に、古いQtバージョンや特定のプラットフォームでundo/redo機能にバグがある場合があります。Qtのドキュメントやバグトラッカーを確認し、該当する問題がないか調べます。 - undo()が呼び出されるタイミングの確認
ユーザーが意図したときにundo()
が呼び出されているか、誤って別の操作と競合していないか確認します。 - カスタムロジックの見直し
もしTextInput
のテキスト内容をプログラムで操作している箇所があれば、それがundo/redoの履歴に悪影響を与えていないか確認してください。可能な限り、ユーザーの直接的な入力操作に任せるか、プログラムによる変更が履歴に正しく統合されるような設計を検討します。 - デバッグ出力の活用
TextInput
のonTextChanged
やonEditingFinished
などのシグナルにデバッグ出力を追加し、テキストがどのように変更されているか、そしてundo()
が呼び出されたときに何が起きているかを追跡します。 - canUndoプロパティの確認
Text { text: "Can Undo: " + myTextInput.canUndo } Button { text: "Undo" onClicked: myTextInput.undo() enabled: myTextInput.canUndo // これを確認 }
canUndo
がfalse
の場合、そもそも元に戻せる操作がないことを意味します。なぜfalse
なのか、原因を探る手助けになります。 - 簡単なテストケースで確認
最小限のQMLコードで、TextInput
とundo()
ボタンのみを配置し、シンプルなテキスト入力・削除操作で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+Z
やCmd+Z
を先に処理してしまい、TextInput
にイベントが到達していない可能性があります。 - フォーカスがない
やはり、TextInput
にフォーカスがない状態でショートカットを押しても機能しません。
トラブルシューティング
- QML Keys.onPressedハンドラの確認
TextInput
の親要素や祖先要素で、Keys.onPressed
ハンドラがCtrl+Z
を処理していないか確認します。処理している場合は、そのロジックを見直すか、event.accepted = false
を設定してイベントを伝播させます。 - 競合するShortcut要素の調査
アプリケーション全体で定義されているShortcut
要素を検索し、Ctrl+Z
やCmd+Z
と競合するものがないか確認します。もしあれば、そのショートカットを削除するか、TextInput
が優先されるように調整します。 - イベントフィルタリング
もしApplicationWindow
や他のルート要素で広範囲にキーイベントを捕捉している場合、event.accepted = false
として、イベントをさらに下位の要素に伝播させるようにします。 - フォーカスの確認
ショートカットを押す前に、TextInput
にフォーカスが当たっていることを確認します。デバッグ目的で、TextInput
のonActiveFocusChanged
シグナルを監視して、フォーカス状態を確認できます。
TextInput.undo()とTextInput.redo()の同期の問題
TextInput.redo()
は、undo()
で元に戻した操作を再び適用するためのメソッドです。これら2つの操作が期待通りに連携しない場合もあります。
考えられる原因
- 複雑なカスタムロジック
前述の通り、カスタムロジックがundo/redo履歴に影響を与えることがあります。 - undo()後に新しい操作が行われた
undo()
を実行した後、新しいテキスト入力や編集操作を行うと、それ以降のredo履歴は失われます。これは一般的なundo/redoシステムの動作です。
トラブルシューティング
- canRedoプロパティの確認
canRedo
がtrue
の場合にのみredo()
ボタンを有効にするなど、UI側で適切に制御します。 - undo/redoの基本的な動作を理解する
ユーザーがundo()
した後に新しい編集を行った場合、その新しい編集が起点となり、それ以前のredo履歴は消去されるのが一般的です。これはバグではなく、仕様です。
- QMLデバッガの活用
Qt Creatorに組み込まれているQMLデバッガを使用すると、プロパティの値、シグナルの発火、コンポーネントのライフサイクルなどを視覚的に確認でき、問題の特定に非常に役立ちます。 - Qtドキュメントの参照
TextInput
の公式ドキュメントには、プロパティ、メソッド、シグナルに関する詳細な情報が記載されています。 - シンプルなケースから始める
問題が発生した場合は、関連するコードを最小限に絞ったテストケースを作成し、そこから徐々に複雑なロジックを追加していくことで、問題の切り分けが容易になります。
TextInput.undo()
は、主にQt QuickのQMLでTextInput
要素と共に使用されます。C++でQt Widgetsを使用している場合は、通常QTextEdit
やQLineEdit
のundo()
スロットを使用します。
例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
の状態をコンソールに出力しています。これにより、内部的な状態の変化を追跡できます。Button
のenabled
プロパティをmyTextInput.canUndo
とmyTextInput.canRedo
にバインドすることで、元に戻せる操作がない場合はボタンを無効にし、ユーザーにフィードバックを与えています。Button
のonClicked
ハンドラでそれぞれmyTextInput.undo()
とmyTextInput.redo()
を呼び出しています。TextInput
にid: myTextInput
を設定し、他の要素から参照できるようにしています。
例2: ショートカットキー (Ctrl+Z
, Ctrl+Y
) との連携
一般的なアプリケーションでは、Undo/Redoはキーボードショートカット(Ctrl+Z
/ Cmd+Z
、Ctrl+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履歴への影響
プログラムでTextInput
のtext
プロパティを直接変更した場合、その変更は通常、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();
のような直接的なメソッドはありませんが、QLineEdit
はQTextDocument
ベースではないため、より単純な履歴管理を行います。多くの場合、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 を使用した高度な履歴管理
TextInput
やQTextEdit
が提供する自動Undo/Redo機能は便利ですが、より複雑なアプリケーションで複数の操作(テキスト編集、図形描画、プロパティ変更など)を横断的にUndo/Redoしたい場合や、Undo履歴の粒度を細かく制御したい場合は、QTextDocument
とQUndoStack
を直接利用するのが強力な方法です。
-
QUndoStack
QUndoStack
は、一般的なUndo/Redoシステムを実装するためのフレームワークです。- 各操作(コマンド)は
QUndoCommand
のサブクラスとして定義され、スタックにプッシュされます。 QUndoStack::undo()
を呼び出すと、スタックのトップにあるコマンドのundo()
メソッドが実行されます。QUndoStack::redo()
を呼び出すと、直前にUndoされたコマンドのredo()
メソッドが実行されます。- 複数の異なる種類の操作を単一のUndo/Redo履歴で管理する(例:テキスト変更と図形移動を同時にUndoする)場合に非常に強力です。
-
QTextDocument
QTextDocument
は、テキストとリッチテキストのコンテンツを表現するクラスです。QTextEdit
やTextInput
の内部でも使用されています。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++の連携の可能性
- C++でカスタムな
QObject
を作成し、QUndoStack
を管理する。 - このカスタム
QObject
をQMLに公開する(QML_ELEMENT
などを使用)。 - QML側で
TextInput
のonTextChanged
などのシグナルを監視し、テキストが変更されたときにC++側のQUndoStack
に新しいQUndoCommand
(例えば、テキスト変更を表すカスタムコマンド)をプッシュする。 - QML側から、公開されたC++オブジェクトの
undo()
やredo()
メソッドを呼び出す。
これはより高度なシナリオであり、アプリケーションの設計全体に影響します。
カスタムの履歴管理ロジック (低レベル)
特定のニッチな要件(例:非常に軽量な履歴、特定の操作のみをUndo対象にするなど)がある場合、自分でテキストの変更履歴を管理するロジックを実装することも不可能ではありません。
redo()
のために、Undoされた操作の履歴も別に管理する。undo()
が呼び出されたら、リストから前の状態を取り出してTextInput
に設定する。- ユーザーが何か入力するたびに、現在のテキストをリストに追加する。
QList<QString>
やQStack<QString>
のようなデータ構造を使って、テキストの各状態を保存する。
欠点
- リッチテキストの書式設定など、テキスト以外の要素のUndoは非常に困難になります。
- パフォーマンスやメモリ使用量に注意が必要です。
- この方法は、
TextInput
やQTextDocument
が提供する組み込みのUndo/Redo機能に比べて、はるかに複雑でバグを起こしやすくなります。
推奨されるシナリオ
- 本当に特殊な、
TextInput
やQTextDocument
の標準機能では実現できない要件がある場合のみ検討すべきです。ほとんどの場合、既存のQtメカニズムで十分です。
- カスタム履歴管理
最後の手段であり、推奨されません。 - 複雑なUndo/Redo
アプリケーション全体で一貫したUndo/Redoシステムを構築したい場合や、テキスト以外の変更もUndoしたい場合は、C++でQUndoStack
を導入するのが最も柔軟で堅牢な方法です。 - Qt Widgets
QLineEdit
やQTextEdit
のundo()
/redo()
スロットを利用します。特にQTextEdit
は強力です。 - QMLのTextInput
組み込みのundo()
/redo()
メソッドとcanUndo
/canRedo
プロパティをそのまま使うのが最も簡単で推奨される方法です。