QSpinBox::validate()だけじゃない!Qtで入力検証を行う代替メソッド

2025-05-31

QValidator::Stateとは?

QValidator::Stateは、入力された文字列が以下の3つの状態のいずれかであることを示します。

  • QValidator::Acceptable: 入力された文字列が、最終的な結果として完全に有効である場合。
  • QValidator::Intermediate: 入力された文字列が、まだ完全ではないものの、あと少し編集すれば有効になる可能性のある状態である場合。例えば、10~99の範囲を受け付けるQSpinBoxに「4」と入力された場合、これはIntermediateとなり、ユーザーが「42」のように続きを入力すればValidになる可能性があります。
  • QValidator::Invalid: 入力された文字列が、そのバリデータ(検証器)のルールに従って明らかに無効である場合。例えば、数値のみを受け付けるはずのQSpinBoxに「abc」と入力された場合などです。

QSpinBox::validate()の役割

QSpinBoxは数値(整数)を扱うウィジェットであり、通常は最小値と最大値の範囲内でしか値を設定できません。ユーザーがQSpinBoxのテキスト部分に直接値を入力する際、QSpinBoxは内部的にこのvalidate()関数を呼び出して、入力されたテキストが有効な数値として解釈できるかどうかを確認します。

QSpinBox::validate()は、QAbstractSpinBox::validate()を再実装したものであり、具体的には以下のことを行います。

  1. 入力テキストの整形: プレフィックス(接頭辞)やサフィックス(接尾辞)、空白文字などを取り除き、純粋な数値部分のテキストを抽出します。
  2. 数値への変換: 抽出したテキストを数値(int型)に変換しようと試みます。
  3. 範囲チェック: 変換された数値が、QSpinBoxに設定された最小値 (minimum()) と最大値 (maximum()) の範囲内にあるかを確認します。

これらのチェックの結果に基づいて、QValidator::Stateのいずれかを返します。

通常、QSpinBoxをそのまま使用する場合、このvalidate()関数を明示的に呼び出す必要はほとんどありません。QSpinBoxが自動的に入力の検証を行います。

しかし、以下のような特殊なケースでQSpinBoxの振る舞いをカスタマイズしたい場合に、QSpinBoxを継承し、validate()関数をオーバーライドすることがあります。

  • より複雑な入力規則を適用したい場合: 特定のパターン(例: 偶数のみ、特定の倍数のみなど)に従う入力を強制したい場合、QSpinBoxのデフォルトの検証では対応できないため、validate()をオーバーライドしてカスタムロジックを組み込むことができます。
  • 16進数入力など、デフォルトとは異なる数値形式を扱いたい場合: 例えば、0から255までの16進数を入力できるスピンボックスを作成する場合、textFromValue()(数値をテキストに変換)とvalueFromText()(テキストを数値に変換)と共にvalidate()を再実装して、16進数としての入力が有効かどうかを判断する必要があります。
#include <QSpinBox>
#include <QValidator>

class MyCustomSpinBox : public QSpinBox
{
    Q_OBJECT
public:
    MyCustomSpinBox(QWidget *parent = nullptr) : QSpinBox(parent)
    {
        setRange(0, 100); // 例として範囲を設定
    }

protected:
    // validate関数をオーバーライドして、カスタムの検証ロジックを実装
    QValidator::State validate(QString &input, int &pos) const override
    {
        // まず、QSpinBoxのデフォルトの検証を呼び出す
        QValidator::State defaultState = QSpinBox::validate(input, pos);

        // デフォルトの検証でInvalidでなければ、さらにカスタムルールを適用
        if (defaultState != QValidator::Invalid) {
            bool ok;
            int value = input.toInt(&ok); // 入力テキストを整数に変換

            // 変換が成功し、かつ値が偶数でなければInvalidとする
            if (ok && (value % 2 != 0)) {
                return QValidator::Invalid;
            }
            // その他の場合はデフォルトのステータスを返す (Acceptable or Intermediate)
            return defaultState;
        }
        return defaultState; // デフォルトがInvalidならそのままInvalidを返す
    }

    // 必要に応じてtextFromValueやvalueFromTextもオーバーライドする
    // QString textFromValue(int value) const override;
    // int valueFromText(const QString &text) const override;
};


入力値が期待通りに検証されない(範囲外の値が入力できてしまうなど)

原因

  • textFromValue() / valueFromText() との不整合
    validate()は入力されたテキストを検証しますが、そのテキストがQSpinBoxの内部値と一致しない場合(textFromValue()で変換されたテキストがvalueFromText()で元の値に戻らない場合など)、検証が正しく行われないことがあります。
  • validate()の誤ったオーバーライド
    QSpinBoxを継承し、validate()をオーバーライドした場合、元のQSpinBox::validate()を呼び出していないか、独自のロジックに誤りがある可能性があります。例えば、範囲チェックを適切に含めていないなど。
  • minimum() / maximum() の設定ミス
    QSpinBoxの許容範囲が正しく設定されていない可能性があります。

トラブルシューティング

  • textFromValue()とvalueFromText()の整合性
    validate()をオーバーライドしている場合は、通常textFromValue()valueFromText()もオーバーライドしてカスタムフォーマットを処理しているはずです。これらの関数が相互に値を正しく変換できることを確認してください。例えば、ある値をtextFromValue()で文字列にし、その文字列をvalueFromText()で元の値に戻せるかテストします。
  • オーバーライドしたvalidate()のデバッグ
    validate()関数をオーバーライドしている場合、ブレークポイントを設定して、input文字列、pos、そして返されるQValidator::Stateが期待通りであるかをステップ実行で確認してください。特に、QSpinBox::validate()を基底クラスとして呼び出しているか(例: QSpinBox::validate(input, pos))を確認し、その結果を考慮に入れているかを確認してください。
  • setRange()の確認
    QSpinBox::setRange(int minimum, int maximum)関数で、期待する最小値と最大値が正しく設定されていることを確認してください。

入力中にQValidator::Intermediateの状態が適切に処理されない

原因

  • validate()のオーバーライドの誤り
    部分的な入力(例: 「4」と入力して、後で「42」にする場合)がIntermediateとして正しく認識されず、すぐにInvalidと判定されてしまう場合があります。これは、入力が完全でなくても将来的に有効になる可能性のある状態をvalidate()が認識していない場合に起こります。

トラブルシューティング

  • QValidator::Intermediateのロジックの見直し
    カスタムロジックでIntermediateを返す条件をより柔軟に設定してください。例えば、入力が数値として部分的に有効であれば、すぐにInvalidとせずIntermediateを返すようにします。
  • QSpinBox::validate()の挙動を理解する
    デフォルトのQSpinBox::validate()は、部分的な入力に対してはIntermediateを適切に返します。例えば、最小値0、最大値99の場合、「5」と入力するとIntermediate、「55」と入力するとAcceptableを返します。カスタムvalidate()を実装する際は、このデフォルトの挙動を模倣または考慮に入れる必要があります。

特殊な文字(プレフィックス、サフィックスなど)を含む入力の検証問題

原因

  • prefix() / suffix() の不適切な使用
    QSpinBox::setPrefix()QSpinBox::setSuffix()を使用している場合、validate()はこれらの文字を考慮して数値部分を抽出します。しかし、カスタムvalidate()を実装する際に、これらのプレフィックス/サフィックスを正しく取り除かずに検証ロジックを適用してしまうと問題が発生します。

トラブルシューティング

  • 手動での文字列操作
    どうしても手動で文字列操作を行う場合は、QString::mid(), QString::indexOf(), QString::remove()などを使用して、プレフィックスとサフィックスを正確に取り除いてから数値を解析するようにしてください。

valueFromText() との連携不足

原因

  • QSpinBoxの値をテキストとして表示(textFromValue())し、ユーザーが編集したテキストを値に変換(valueFromText())する際、validate()がテキストの有効性を判断しますが、これらの3つの関数が連携していないと問題が発生します。特に、validate()Acceptableと判断したテキストをvalueFromText()が正しく数値に変換できない場合、ユーザーは有効な入力をしたにもかかわらず、QSpinBoxの値が更新されないなどの問題が生じます。

トラブルシューティング

  • 三者の整合性
    validate()Acceptableを返すすべての入力文字列が、valueFromText()によって意図した数値に変換できることを確認してください。また、textFromValue()が生成するすべての文字列が、validate()によってAcceptableと判断され、valueFromText()によって元の数値に変換できることを確認してください。これは、ユニットテストで網羅的にテストすべき点です。

カスタムの検証ロジックが複雑すぎる、またはパフォーマンスが悪い

原因

  • validate()関数はユーザーが入力するたびに頻繁に呼び出される可能性があるため、複雑な計算や遅い処理を含むとUIの応答性が低下する可能性があります。
  • 最小限のチェック
    validate()では、入力が「有効であるか、有効になる可能性があるか」を素早く判断することに焦点を当て、詳細なビジネスロジックの検証はeditingFinished()シグナルが発せられた後など、より適切なタイミングで行うことを検討してください。
  • パフォーマンス最適化
    複雑な処理が必要な場合は、キャッシュの使用、正規表現の効率的な記述、不必要な計算の回避など、パフォーマンス最適化の一般的な手法を適用してください。
  • ロジックの簡素化
    可能であれば、validate()内のロジックを簡素化してください。


ここでは、QSpinBox::validate()に関連するプログラミング例をいくつか示します。

デフォルトのQSpinBoxの挙動(オーバーライドなし)

QSpinBoxを単にインスタンス化して使用する場合、validate()関数を明示的に記述する必要はありません。Qtが提供するデフォルトの実装が自動的に機能します。

// main.cpp
#include <QApplication>
#include <QSpinBox>
#include <QWidget>
#include <QVBoxLayout>

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

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

    QSpinBox *spinBox = new QSpinBox(&window);
    spinBox->setRange(0, 100); // 0から100の範囲を設定
    spinBox->setPrefix("Value: "); // プレフィックスを設定
    spinBox->setSuffix(" units");  // サフィックスを設定

    layout->addWidget(spinBox);
    window.setWindowTitle("Default QSpinBox Example");
    window.show();

    return a.exec();
}

この例では、ユーザーが「Value: 50 units」のように入力したり、スピンボタンで値を変更したりできます。QSpinBoxは自動的に「Value: 」と「 units」を無視して数値を検証し、範囲外の入力は許可しません。例えば、「Value: 120 units」と入力しようとすると、値は100に制限されます。

QSpinBox::validate()をオーバーライドしてカスタム検証を追加する例(偶数のみ許可)

ここでは、偶数のみを受け付けるQSpinBoxを作成するためにvalidate()をオーバーライドする例を示します。

MyEvenSpinBox.h

// MyEvenSpinBox.h
#ifndef MYEVENSPINBOX_H
#define MYEVENSPINBOX_H

#include <QSpinBox>
#include <QValidator>

class MyEvenSpinBox : public QSpinBox
{
    Q_OBJECT
public:
    explicit MyEvenSpinBox(QWidget *parent = nullptr);

protected:
    // QSpinBox::validate()をオーバーライド
    QValidator::State validate(QString &input, int &pos) const override;

    // QSpinBox::textFromValue()とQSpinBox::valueFromText()もオーバーライドすることが一般的です。
    // 今回は偶数のみを扱うため、表示と内部値の変換はデフォルトのままでも問題ないですが、
    // 複雑な検証ではこれらもカスタマイズする必要があります。
    // QString textFromValue(int value) const override;
    // int valueFromText(const QString &text) const override;
};

#endif // MYEVENSPINBOX_H

MyEvenSpinBox.cpp

// MyEvenSpinBox.cpp
#include "MyEvenSpinBox.h"
#include <QDebug> // デバッグ出力用

MyEvenSpinBox::MyEvenSpinBox(QWidget *parent) : QSpinBox(parent)
{
    setRange(0, 100); // 例として範囲を設定
    setSingleStep(2); // 上下ボタンでの増減も偶数に
    setValue(0);      // 初期値を偶数に設定
}

QValidator::State MyEvenSpinBox::validate(QString &input, int &pos) const
{
    // まず、基底クラスのvalidate()を呼び出し、基本的な数値と範囲の検証を行う
    QValidator::State defaultState = QSpinBox::validate(input, pos);

    // defaultStateがInvalidでなければ、さらに偶数であるかの検証を行う
    if (defaultState != QValidator::Invalid) {
        bool ok;
        // input文字列から純粋な数値を抽出(プレフィックスやサフィックスを考慮)
        // QSpinBox::cleanText()は、QLineEditのテキストからプレフィックス/サフィックスを
        // 取り除いたものを取得するのに使えますが、validate()のinputはすでに
        // 数値部分に近い文字列になっていることが多いため、ここでは直接toInt()を試みます。
        // より堅牢にするには、QSpinBoxの内部処理(textFromValue/valueFromText)を
        // 模倣する必要がありますが、シンプルなケースではこれで十分です。
        int value = input.toInt(&ok);

        // 数値変換に成功し、かつ数値が偶数でなければInvalidとする
        if (ok) {
            if (value % 2 != 0) {
                qDebug() << "Invalid: Not an even number" << value;
                return QValidator::Invalid;
            } else {
                qDebug() << "Acceptable/Intermediate: Even number" << value;
                return defaultState; // 偶数であれば、基底クラスのAcceptableまたはIntermediateを返す
            }
        } else {
            // 数値に変換できない場合、基底クラスのStateを返す(通常はInvalidまたはIntermediate)
            qDebug() << "Not a number or incomplete number, default state:" << defaultState;
            return defaultState;
        }
    }

    qDebug() << "Invalid by default validation, input:" << input;
    return defaultState; // 基底クラスがInvalidと判断した場合は、そのままInvalidを返す
}

main.cpp (MyEvenSpinBoxの使用)

// main.cpp
#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include "MyEvenSpinBox.h" // 作成したカスタムスピンボックスをインクルード

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

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

    MyEvenSpinBox *evenSpinBox = new MyEvenSpinBox(&window);
    evenSpinBox->setPrefix("Even: ");
    evenSpinBox->setSuffix(" units");

    layout->addWidget(evenSpinBox);
    window.setWindowTitle("Custom Even SpinBox Example");
    window.show();

    return a.exec();
}

この例では、MyEvenSpinBoxクラスがQSpinBoxを継承し、validate()関数をオーバーライドしています。

  • 偶数であれば、基底クラスのvalidate()が返したAcceptableまたはIntermediateの状態をそのまま返します。
  • もし奇数であれば、QValidator::Invalidを返します。
  • その結果がInvalidでなければ、入力された数値が偶数であるかをチェックします。
  • まず、基底クラスのQSpinBox::validate()を呼び出し、通常の数値範囲チェックを行います。

これにより、ユーザーは偶数のみを直接入力できるようになります。例えば、「Even: 3 units」と入力しようとすると、入力が無効と判断され、受け付けられません。

validate()をオーバーライドする場合、多くの場合、表示形式(textFromValue())と内部値への変換(valueFromText())もカスタマイズする必要があります。ここでは、「X dpi」のようなフォーマットを扱うスピンボックスの例を示します。

MyDpiSpinBox.h

// MyDpiSpinBox.h
#ifndef MYDPISPINBOX_H
#define MYDPISPINBOX_H

#include <QSpinBox>
#include <QRegularExpressionValidator>

class MyDpiSpinBox : public QSpinBox
{
    Q_OBJECT
public:
    explicit MyDpiSpinBox(QWidget *parent = nullptr);

protected:
    QString textFromValue(int value) const override;
    int valueFromText(const QString &text) const override;
    QValidator::State validate(QString &input, int &pos) const override;

private:
    QRegularExpressionValidator *m_validator;
};

#endif // MYDPiSPINBOX_H

MyDpiSpinBox.cpp

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

MyDpiSpinBox::MyDpiSpinBox(QWidget *parent) : QSpinBox(parent)
{
    setRange(72, 600); // 一般的なDPI範囲
    setSingleStep(10); // 10刻みで変更
    setValue(96);      // 初期値

    // 正規表現バリデータを使用して、"数値 dpi" の形式を検証
    // \\d+ : 1桁以上の数字
    // \\s* : 0個以上の空白
    // dpi$ : "dpi"で終わる
    // QRegularExpressionValidatorは、入力全体が正規表現にマッチするかを検証します。
    // QValidator::Acceptable, QValidator::Intermediate, QValidator::Invalid を適切に返します。
    m_validator = new QRegularExpressionValidator(QRegularExpression("^\\d+\\s*dpi$"), this);
}

QString MyDpiSpinBox::textFromValue(int value) const
{
    // 内部値を「数値 dpi」の形式に変換して表示
    return QString("%1 dpi").arg(value);
}

int MyDpiSpinBox::valueFromText(const QString &text) const
{
    // 表示テキストから数値部分を抽出し、内部値に変換
    QString cleanedText = text;
    cleanedText.remove("dpi", Qt::CaseInsensitive); // "dpi"を削除(大文字小文字を区別しない)
    cleanedText = cleanedText.trimmed(); // 前後の空白を削除

    bool ok;
    int value = cleanedText.toInt(&ok);
    if (ok) {
        return value;
    }
    return minimum(); // 変換に失敗したら最小値を返すなど、適切なフォールバック処理
}

QValidator::State MyDpiSpinBox::validate(QString &input, int &pos) const
{
    // まず、カスタムの正規表現バリデータで検証を行う
    QValidator::State state = m_validator->validate(input, pos);

    // 正規表現でInvalidの場合、それ以上は検証しない
    if (state == QValidator::Invalid) {
        qDebug() << "Invalid by Regex:" << input;
        return QValidator::Invalid;
    }

    // 正規表現でAcceptableまたはIntermediateの場合、数値として解釈し、範囲を検証
    bool ok;
    // input文字列から数値部分を抽出するためにvalueFromTextを使用
    // ここでvalueFromTextを使うことで、textFromValueとvalidateの連携が良くなります。
    int value = valueFromText(input);

    // 範囲チェック
    if (value >= minimum() && value <= maximum()) {
        qDebug() << "Acceptable/Intermediate by Range:" << input << "Value:" << value;
        return state; // 正規表現のステータス(Acceptable or Intermediate)を維持
    } else {
        // 範囲外の場合、中間状態として扱うか、Invalidとするかを決定
        // 例: 7と入力された場合(範囲72-600)。
        // valueFromTextで7になり、範囲外だが、後ろに続く可能性があるのでIntermediate。
        // ここでは、デフォルトのQSpinBox::validate()の挙動に近い形で
        // 範囲外だが、まだ編集途中の可能性がある場合はIntermediate、
        // 明らかに範囲外で完結している場合はInvalidと判断するロジックを検討。

        // よりシンプルな実装として、ここでは範囲外であればInvalidとする
        // 実際のQSpinBoxのvalidateは、入力途中(例: "7")でIntermediateを返す
        // ロジックが複雑になるため、ここでは単純化します。
        // inputが完全に数値として解釈でき、かつ範囲外であればInvalidとする
        if (ok) { // valueFromTextが数値を完全に変換できた場合
            qDebug() << "Invalid: Value out of range" << value;
            return QValidator::Invalid;
        } else {
            // まだ数値として完全に解釈できないが、regexがIntermediateを返した場合は、
            // そのままIntermediateとして返す
            qDebug() << "Intermediate: Possibly out of range but incomplete" << input;
            return QValidator::Intermediate;
        }
    }
}

main.cpp (MyDpiSpinBoxの使用)

// main.cpp
#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include "MyDpiSpinBox.h"

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

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

    MyDpiSpinBox *dpiSpinBox = new MyDpiSpinBox(&window);
    layout->addWidget(dpiSpinBox);
    window.setWindowTitle("Custom DPI SpinBox Example");
    window.show();

    return a.exec();
}

この例では、MyDpiSpinBoxは以下の点を考慮しています。

  • validate():
    • まず、QRegularExpressionValidatorを使用して、入力文字列が「数字 dpi」の基本的な形式に合致するかをチェックします。これにより、「abc」のような入力はすぐにInvalidとなります。
    • 正規表現でInvalidでなければ、valueFromText()を使って文字列を数値に変換し、その数値がsetRange()で設定された範囲内にあるかをチェックします。
    • 範囲外であればInvalidを返しますが、入力がまだ途中(例: 「7」と入力したが、最終的に「72 dpi」になる可能性がある)であればIntermediateを返すようなロジックも考慮できます(ただし、その実装はより複雑になります)。上記の例では、簡略化のため、正規表現がAcceptableで数値変換も成功した場合にのみ、範囲チェックを行い、範囲外ならInvalidとしています。
  • valueFromText(): ユーザーが入力した「数値 dpi」という文字列から数値部分を抽出し、int値に変換します。
  • textFromValue(): 内部のint値を「数値 dpi」という表示文字列に変換します。


ここでは、QSpinBox::validate()を直接オーバーライドせずに、同様の検証機能や入力制限を実現するための代替プログラミング方法をいくつか説明します。

QSpinBoxの組み込み機能の活用

多くの場合、QSpinBoxが提供する標準機能で十分です。

  • setPrefix(const QString &prefix) / setSuffix(const QString &suffix): 表示されるテキストに接頭辞や接尾辞を追加します。これらはvalidate()によって自動的に無視され、数値部分のみが検証されます。

    • 利点: 表示形式のカスタマイズが簡単です。
    • 欠点: 検証ロジックそのものには影響しません。
    QSpinBox *spinBox = new QSpinBox();
    spinBox->setPrefix("Count: ");
    spinBox->setSuffix(" items");
    
  • setSingleStep(int val): スピンボックスの上下ボタンが一度に値を増減させる量を設定します。これにより、ユーザーがボタンを使って値を操作する際に、特定の倍数(例: 2、5、10など)に強制できます。

    • 利点: ボタン操作による値の制限が簡単です。
    • 欠点: ユーザーが直接テキストを入力した場合には適用されません。
    QSpinBox *spinBox = new QSpinBox();
    spinBox->setRange(0, 100);
    spinBox->setSingleStep(2); // 上下ボタンで2ずつ増減(偶数のみに誘導)
    
  • setRange(int minimum, int maximum): 最も基本的な検証機能です。この関数で最小値と最大値を設定するだけで、ユーザーは指定された範囲外の数値を入力できなくなります。

    • 利点: 最も簡単で、特別なコーディングは不要です。
    • 欠点: 数値の範囲制限のみに特化しており、それ以外のカスタムな検証(例: 偶数のみ、特定の倍数のみなど)はできません。
    QSpinBox *spinBox = new QSpinBox();
    spinBox->setRange(0, 100); // 0から100の範囲のみ許可
    

QSpinBox::editingFinished() シグナルと手動検証

ユーザーがQSpinBoxの編集を終了したとき(Enterキーを押したり、フォーカスがQSpinBoxから外れたときなど)に発生するeditingFinished()シグナルを利用して、手動で検証を行う方法です。

  • 欠点:
    • ユーザーが入力中にリアルタイムでフィードバック(入力不可の表示など)を得られない。
    • 無効な入力を確定してしまった後に修正を促す必要がある。
  • 利点:
    • QSpinBox::validate()をオーバーライドするよりもシンプルになる場合があります。
    • 検証が「リアルタイム」でなくても良い場合に適しています。
    • エラーメッセージの表示など、ユーザーへのフィードバックを細かく制御できます。
#include <QApplication>
#include <QSpinBox>
#include <QWidget>
#include <QVBoxLayout>
#include <QMessageBox> // エラーメッセージ表示用

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

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

    QSpinBox *spinBox = new QSpinBox(&window);
    spinBox->setRange(0, 100);

    // editingFinished() シグナルにスロットを接続
    QObject::connect(spinBox, &QSpinBox::editingFinished, [&]() {
        int value = spinBox->value(); // 現在のQSpinBoxの値を取得

        // カスタム検証ロジック (例: 偶数のみ許可)
        if (value % 2 != 0) {
            QMessageBox::warning(&window, "Validation Error",
                                 QString("'%1' is not an even number. Please enter an even number between %2 and %3.").arg(value).arg(spinBox->minimum()).arg(spinBox->maximum()));
            // 無効な値を元に戻すか、適切な値を設定する
            spinBox->setValue(spinBox->property("previousValue").toInt()); // 以前の有効な値に戻す
        } else {
            // 検証成功: 次回の検証のために現在の値を保存しておく
            spinBox->setProperty("previousValue", value);
        }
    });

    // 初期値設定時にpreviousValueプロパティも設定しておく
    spinBox->setValue(50);
    spinBox->setProperty("previousValue", 50); // 初期有効値を保存

    layout->addWidget(spinBox);
    window.setWindowTitle("Editing Finished Validation");
    window.show();

    return a.exec();
}

この方法では、QSpinBoxにカスタムプロパティ(previousValue)を追加し、有効な値のみを保存するようにしています。無効な入力が確定された場合、ユーザーに警告し、以前の有効な値に戻します。

QAbstractSpinBox::textChanged(const QString &text) シグナルとリアルタイム検証

QSpinBoxQAbstractSpinBoxを継承しており、textChanged()シグナルを発行します。これにより、ユーザーがテキストボックス内で文字をタイプするたびに検証を行うことができます。

  • 欠点:
    • validate()のオーバーライドよりも複雑になる可能性があります。
    • テキストをパースして数値に変換するロジックを自分で書く必要があります。
    • 頻繁に呼び出されるため、パフォーマンスに注意が必要です。
  • 利点:
    • ほぼリアルタイムでのフィードバックが可能です。
    • ユーザーが間違った入力をするのを防ぐことができます。
#include <QApplication>
#include <QSpinBox>
#include <QWidget>
#include <QVBoxLayout>
#include <QLineEdit> // QSpinBoxの内部LineEditにアクセスするため

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

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

    QSpinBox *spinBox = new QSpinBox(&window);
    spinBox->setRange(0, 100);
    spinBox->setPrefix("Value: ");

    // QSpinBoxの内部LineEditにアクセス
    QLineEdit *lineEdit = spinBox->findChild<QLineEdit*>();
    if (lineEdit) {
        QObject::connect(lineEdit, &QLineEdit::textChanged, [&](const QString &text) {
            // プレフィックスとサフィックスを考慮して純粋な数値部分を抽出
            QString cleanedText = text;
            if (spinBox->prefix().length() > 0 && cleanedText.startsWith(spinBox->prefix())) {
                cleanedText.remove(0, spinBox->prefix().length());
            }
            if (spinBox->suffix().length() > 0 && cleanedText.endsWith(spinBox->suffix())) {
                cleanedText.chop(spinBox->suffix().length());
            }
            cleanedText = cleanedText.trimmed();

            bool ok;
            int value = cleanedText.toInt(&ok);

            if (ok) {
                // 有効な数値だが、カスタムルールに反するかチェック (例: 偶数のみ)
                if (value % 2 != 0) {
                    lineEdit->setStyleSheet("color: red;"); // 無効な場合は赤字
                } else {
                    lineEdit->setStyleSheet(""); // 有効な場合は通常色
                    // ここでspinBox->setValue()を呼び出すことも可能だが、
                    // その場合は無限ループに注意(setValueがtextChangedを再度トリガーする)
                    // リアルタイム検証では、色を変えるなどの視覚的なフィードバックが一般的。
                }
            } else {
                // 数値として解釈できない場合
                lineEdit->setStyleSheet("color: red;");
            }
        });
    }

    layout->addWidget(spinBox);
    window.setWindowTitle("Real-time Text Changed Validation");
    window.show();

    return a.exec();
}

この例では、QLineEditのスタイルシートを変更することで、無効な入力を視覚的にフィードバックしています。setValue()を直接呼び出すと無限ループに陥る可能性があるため、注意が必要です。

独自のQValidatorサブクラスとQLineEdit::setValidator()

QSpinBoxの内部にはQLineEditがあり、そのQLineEditに直接QValidatorを設定することはできません(QSpinBox自体が内部でバリデーションを管理しているため)。しかし、QSpinBoxの動作を根本から変えるのではなく、例えば単なる数値入力ウィジェットでカスタム検証を行いたい場合は、QLineEditにカスタムQValidatorをセットするという方法がより適切です。

これはQSpinBoxに直接関連する代替案ではありませんが、「数値入力の検証」というより広い文脈での強力な代替案です。

  • 欠点:
    • スピンボタンの増減機能は自分で実装する必要があります。
    • プレフィックス/サフィックスの処理も自分で実装する必要があります。
  • 利点:
    • 再利用可能な検証ロジックをカプセル化できます。
    • QSpinBoxの複雑なオーバーライドを避けることができます。
    • QLineEditだけでなく、他のテキスト入力ウィジェットにも適用できます。
// MyCustomValidator.h
#include <QValidator>
#include <QRegularExpression>

class MyCustomValidator : public QValidator
{
    Q_OBJECT
public:
    explicit MyCustomValidator(QObject *parent = nullptr);
    QValidator::State validate(QString &input, int &pos) const override;

private:
    QRegularExpression m_regex;
};

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

MyCustomValidator::MyCustomValidator(QObject *parent)
    : QValidator(parent),
      m_regex("^\\d+$") // 1桁以上の数字のみを許可する正規表現
{}

QValidator::State MyCustomValidator::validate(QString &input, int &pos) const
{
    // 入力が空の場合はIntermediate(まだ入力途中なので)
    if (input.isEmpty()) {
        return QValidator::Intermediate;
    }

    // 正規表現でマッチするかチェック
    if (m_regex.match(input).hasMatch()) {
        bool ok;
        int value = input.toInt(&ok);
        if (ok && value >= 0 && value <= 100) { // さらに数値範囲の検証
            if (value % 2 == 0) { // さらに偶数であるかの検証
                qDebug() << "Acceptable (Even):" << input;
                return QValidator::Acceptable;
            } else {
                qDebug() << "Invalid (Odd):" << input;
                return QValidator::Invalid;
            }
        } else {
            qDebug() << "Invalid (Out of range or not int):" << input;
            return QValidator::Invalid; // 数値変換失敗または範囲外
        }
    }

    // 部分的にマッチするかチェック(Intermediateの状態)
    // 例えば "123" は Acceptableだが、"12a" は Invalid
    // QRegularExpressionValidator::validate()の内部ロジックを参考に
    // 部分マッチを正確に判断するにはより複雑なロジックが必要になることがあります。
    // 簡単化のため、ここではregexに完全にマッチしない場合はInvalidとする
    // もしくは、特定のパターン(例: "12")が将来的に有効になる可能性があればIntermediate
    if (m_regex.match(input, 0, QRegularExpression::PartialPreferFirstMatch).hasPartialMatch()) {
        qDebug() << "Intermediate (Partial match):" << input;
        return QValidator::Intermediate;
    }

    qDebug() << "Invalid (No match):" << input;
    return QValidator::Invalid;
}

// main.cpp (QValidatorの利用例)
#include <QApplication>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QWidget>
#include "MyCustomValidator.h"

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

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

    QLineEdit *lineEdit = new QLineEdit(&window);
    MyCustomValidator *validator = new MyCustomValidator(&window);
    lineEdit->setValidator(validator); // カスタムバリデータを設定

    layout->addWidget(lineEdit);
    window.setWindowTitle("Custom QValidator Example");
    window.show();

    return a.exec();
}

この方法は、QSpinBoxの自動的な数値操作機能が不要で、単にテキスト入力の検証が必要な場合に非常に有効です。

  • スピンボックス機能が不要で、テキスト入力の複雑な検証のみが必要: 独自のQValidatorサブクラスを作成し、QLineEdit::setValidator()で設定します。
  • 入力確定後に検証し、エラーメッセージを表示したい: QSpinBox::editingFinished()シグナルを利用します。
  • リアルタイムで強力な入力制限が必要だが、validate()のオーバーライドを避けたい: QSpinBox::validate()をオーバーライドして、内部でQValidatorを使用するか、QLineEdittextChanged()シグナルを利用します。ただし、後者は入力が複雑になると実装も複雑になります。
  • 複雑なカスタム表示形式: QSpinBox::textFromValue()QSpinBox::valueFromText()をオーバーライドします。
  • ボタン操作でのステップ制限: QSpinBox::setSingleStep()も併用します。
  • 最も簡単な範囲制限: QSpinBox::setRange()を使用します。