本文へスキップします。

本文へ

【全Qt】
【全・Qt】SRAロゴ
H1

FAQ

Q&A:検索

カテゴリー

Q&A:表示

Qt

「Qt」はどのように発音しますか。

2003 年、当時の Qt 開発元 Trolltech 社への「Qt」の発音の問い合わせで以下の回答を得ています。

You wrote:
> Could I make just simple question? How do you pronounce `Qt'?

There are two alternatives in English: /kjut/ (like the word "cute") or
/kju-ti/ (like the abbreviation "qt").  The latter alternative is generally
seen as more appropriate in an international context.

There are other alternatives in other languages.

Best regards,

Grim Gjonnes
Director of Sales
Trolltech AS
Tel: +47 934 01 002
Email: grim@trolltech.com

この当時は、「キュート」と「キューティー」のふた通りで発音されていて、どちらか一方が公式とは、決まっていませんでした。

2008 年、Trolltech 社が Nokia 社に買収され、Nokia 社より「キュート」が公式の発音と発表され、現在に至っています。

Qt 6 で新しいアプリケーションを作ります。Qt Widgets と Qt Quick は、どのような基準で選べばよいですか。既存の C++ コードを利用できるかも知りたいです。

入力フォームや表・ツリーを中心とするデスクトップアプリケーションでは Qt Widgets、タッチ操作やアニメーション、独自の見た目を重視する画面では Qt Quick を、最初に検討する候補にするとよいです。ただし、これは選定の出発点です。どちらでも作れる画面は多く、必要な操作、既存の画面資産、開発チームの経験、対象機での動作を比較して選びます。

主な比較点は次のとおりです。

比較点Qt WidgetsQt Quick
画面の記述主に C++ と QWidget 派生クラス。Qt Widgets Designer の .ui ファイルも利用できる。主に QML。プロパティーの関係や状態変化を宣言的に記述する。
入力部品QLineEdit、QComboBox、QTableView、QTreeView などを組み合わせる。Qt Quick Controls の TextField、ComboBox、Button などを組み合わせる。
描画と動き標準部品の配置や独自の paintEvent() による描画を組み立てる。シーングラフを使い、アニメーションや画面遷移を QML で表現する。
C++ との連携画面から C++ のロジックやモデルを呼び出す。QObject のプロパティー・メソッド・シグナルや C++ のモデルを QML に公開する。

たとえば、顧客情報の登録画面で、文字入力、候補の選択、一覧表の編集を行う場合を考えます。Qt Widgets では、入力部品とレイアウト、モデル/ビューを組み合わせて画面を構成できます。キーボードでの連続入力や複数選択、メニュー、ドックなど、デスクトップアプリケーションとして必要な操作を洗い出し、既存部品でどこまで実現できるかを確認します。社内に QWidget ベースの入力部品や画面が蓄積されているなら、その再利用も判断材料になります。

一方、装置の操作パネルで、状態に応じて表示を切り替えたり、スワイプでページを移動したり、数値や図形を滑らかに変化させたりする場合は Qt Quick が候補になります。QML のバインディングで表示と値の関係を記述し、状態・アニメーションを組み合わせられます。Qt Quick Controls にはフォーム用の部品もあるので、Qt Quick は動く画面専用というわけではありません。同様に、Qt Widgets でも独自描画やアニメーションは実装できます。必要な画面を、どちらの方法で無理なく構成できるかを比較します。

描画性能は、方式の名前だけでは決まりません。Qt Quick は通常、GPU を利用するシーングラフで描画しますが、ソフトウェア描画の経路もあります。Qt Widgets の通常の部品描画と Qt Quick では処理の仕組みが異なり、画像の大きさ、表示する要素数、エフェクト、更新頻度、対象機の GPU やドライバーなどで結果が変わります。「Qt Quick なら必ず速い」「Qt Widgets なら必ず軽い」とは判断せず、実際に必要な画面で測定します。

既存の C++ コードがあることだけを理由に、Qt Widgets を選ぶ必要はありません。通信、計算、データ保存など、画面から独立した処理は Qt Quick からも利用できます。たとえば、入力値の検証と保存を行う C++ のクラスを残し、QML から呼び出せる QObject の API を用意する構成です。画面は公開された値を表示し、操作をメソッドやシグナルで処理側へ渡します。長時間の処理で GUI スレッドを塞がない設計は、どちらの方式でも必要です。

ただし、再利用するものが C++ の業務ロジックなのか、QWidget を継承した画面部品なのかは区別してください。QWidget の部品をそのまま QML の画面部品として使えるわけではありません。ロジックと画面が強く結び付いている場合は、UI の作り直しだけでなく、分離に必要な作業も見積もります。

既存の Qt Widgets アプリケーションに、Qt Quick の画面を一部分だけ追加するなら QQuickWidget も選択肢です。全面的な置き換えをせず、必要な部分から導入できます。ただし、追加の描画処理やフォーカスの移動、入力の受け渡しなどを確認する必要があります。混在させる方が常に簡単になるとは限りません。

迷う場合は、最も重要な一画面を小さく試作してください。業務フォームなら表の編集、日本語入力、Tab キーによる移動を、操作パネルならタッチ操作、画面遷移、連続更新を試します。そのうえで、必要な操作を実装する手間、対象機での応答性・起動時間・メモリー使用量、今後の画面変更のしやすさを比べると、具体的な根拠を持って選べます。

参考資料

Qt Widgets

Qt Quick

Qt Quick Controls

Overview - QML and C++ Integration

Qt Quick Scene Graph

QQuickWidget Class

Qt 6 のアプリケーションで QNetworkAccessManager を使おうとすると、ヘッダーが見つからない、または未定義参照のリンクエラーになります。Qt Network モジュールを CMake プロジェクトへ追加するには、どこを確認すればよいですか。

Qt のクラスを利用するときは、ソースコードでヘッダーをインクルードするだけでなく、CMake で対応する Qt モジュールを読み込み、そのモジュールのインポートターゲットを実行ファイルやライブラリーへリンクします。

QNetworkAccessManager は Qt Network モジュールのクラスです。最小構成は次のようになります。

CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)

project(NetworkExample VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Network)

qt_standard_project_setup()

qt_add_executable(NetworkExample
    main.cpp
)

target_link_libraries(NetworkExample
    PRIVATE
        Qt6::Network
)
main.cpp:
#include <QCoreApplication>
#include <QNetworkAccessManager>

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    QNetworkAccessManager manager;
    return 0;
}

find_package(Qt6 REQUIRED COMPONENTS Network) は、Qt 6 の Network コンポーネントを探し、Qt6::Network という CMake のインポートターゲットを利用できるようにします。target_link_libraries() は、そのターゲットが必要とするライブラリーだけでなく、インクルードディレクトリーやコンパイル定義などの利用条件も NetworkExample へ伝えます。そのため、Qt のインクルードパスやライブラリーファイルを個別にハードコードする必要はありません。

エラーが出る段階によって、主に次の点を確認します。

CMake の構成時に Network コンポーネントが見つからない場合
使用している Qt に Qt Network の開発ファイルが含まれているか、CMake が意図した Qt を見つけているかを確認します。複数の Qt をインストールしている環境では、Qt のパスを変更した後に古い CMake キャッシュが残っていないかも確認してください。

コンパイル時に QNetworkAccessManager のヘッダーが見つからない場合
クラスの公式リファレンスにある Header と CMake の欄を確認します。Network が find_package() の COMPONENTS にあり、実際にコンパイルしているターゲットが Qt6::Network へリンクされているかを確認します。別の実行ファイルやライブラリーへコードを移した場合は、移動先のターゲットにも必要なリンク指定が要ります。

リンク時に未定義参照が出る場合
ヘッダーが読めても、クラスの実装を含むモジュールがリンクされているとは限りません。target_link_libraries() の対象名が、エラーを出している実行ファイルまたはライブラリーと一致しているかを確認します。Qt6::Network を単に変数へ書いたり、別のターゲットだけへリンクしたりしても、目的のターゲットには反映されません。

自作ライブラリーの公開ヘッダーで Qt Network の型を使う場合は、その依存関係を利用側にも伝える必要があります。その場合は、ライブラリーの設計に応じて Qt6::Network を PUBLIC としてリンクします。Qt Network の型を実装ファイルの中だけで使うなら PRIVATE が基本です。

Qt のモジュール名、CMake のコンポーネント名、インポートターゲット名は対応しています。たとえば Widgets は Qt6::Widgets、Quick は Qt6::Quick、Network は Qt6::Network です。クラスのリファレンスに掲載されている CMake の記述を確認すると、必要なモジュールを特定できます。

参考資料

Qt Network

QNetworkAccessManager Class

Getting started with CMake

Imported targets

qt_standard_project_setup

コア

Qt のクラスを拡張してサブクラスを作成する場合に Q_OBJECT はどのような場合に必要なのでしょうか。

QObject または間接的に QObject を継承しているクラスを継承してサブクラス化をする場合には、いつも Q_OBJECT を記述しておいた方がよいです。 そうしておけば、いろいろな間違いを避けられます。

サブクラスにシグナルとスロットを追加するならば Q_OBJECT の記述が必要です。追加しないならば Q_OBJECT を記述しなくてもコンパイルはできて一応動作はします。 しかし、シグナルやスロットを追加しないからと言って、Q_OBJECT を付けないといろいろな問題が発生します。 翻訳のための tr() がクラス名のコンテキストで翻訳メッセージを検索できなくなります。 qobject_cast がコンパイルエラーになります。metaObject()->className() が正しいクラス名を返さず、ベースクラス名を返してしまいます。

main.cpp:

#include <QCoreApplication>
#include <QTranslator>
#include <QLocale>
#include <QDebug>

class QUnderscoreObject : public QObject
{
    Q_OBJECT

public:
    explicit QUnderscoreObject(QObject* parent = nullptr) : QObject(parent) {
        qDebug() << Q_FUNC_INFO << tr("Hi, there.");
    }
};

class NoQUnderscoreObject : public QObject
{
public:
    explicit NoQUnderscoreObject(QObject* parent = nullptr) : QObject(parent) {
        qDebug() << Q_FUNC_INFO << tr("Hi, there.");
    }
};

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);

    QTranslator translator;

    if (translator.load(":/qunderscore_" + QLocale::system().bcp47Name())) {
        app.installTranslator(&translator);
    }

    QUnderscoreObject qUnderscoreObject{};
    qDebug() << "QUnderscoreObject class has a proper class name:"
             << qUnderscoreObject.metaObject()->className();

    NoQUnderscoreObject noQUnderscoreObject{};
    qDebug() << "NoQUnderscoreObject class does not have a proper class name:"
             << noQUnderscoreObject.metaObject()->className();

    QObject* object = &qUnderscoreObject;
    qDebug() << "qobject_cast<QUnderscoreObject*>(object):" << qobject_cast<QUnderscoreObject*>(object);

    object = &noQUnderscoreObject;
//  qDebug() << "qobject_cast<NoQUnderscoreObject*>(object):" << qobject_cast<NoQUnderscoreObject*>(object); // Compilation error
}

#include "main.moc"

コードの説明

翻訳関数 tr() は、クラス名と実引数文字列をキーにして翻訳辞書を検索し翻訳文字列を返します。 QUnderscoreObject コンストラクター内の tr() は、正しく翻訳日本語文字列を返します。 しかし、NoQUnderscoreObject のコンストラクター内の tr() は、ベースクラスの tr() が呼び出されるので、翻訳日本語文字列を返せません。


日本語翻訳辞書をロードしています。


QMetaObject::className() は、クラス名を返します。QUnderscoreObject の方の metaObject() は、QUnderscoreObject のメタオブジェクトを返すので、className() は 正しいクラス名を返します。 NoQUnderscoreObject の方の metaObject() は、ベースクラスのメタオブジェクトを返すので、className() はベースクラスのクラス名を返してしまいます。


QObject* 型変数に代入した QUnderscoreObject オブジェクトを QUnderscoreObject* に型変換できます。 しかし、NoQUnderscoreObject オブジェクトの型変換は、コンパイルエラーになります。


QUnderscoreObject::QUnderscoreObject(QObject *) "やあ。"
QUnderscoreObject class has a proper class name: QUnderscoreObject
NoQUnderscoreObject::NoQUnderscoreObject(QObject *) "Hi, there."
NoQUnderscoreObject class does not have a proper class name: QObject
qobject_cast<QUnderscoreObject*>(object): QUnderscoreObject(0x7ffeeec96480)

実行結果です。


/usr/local/qt/Qt/6.2.1/macos/lib/QtCore.framework/Headers/qobject.h:451:5: error: static_assert failed due to requirement
      'QtPrivate::HasQ_OBJECT_Macro<NoQUnderscoreObject>::Value' "qobject_cast requires the type to have a Q_OBJECT macro"
    static_assert(QtPrivate::HasQ_OBJECT_Macro<ObjType>::Value,
    ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:46:64: note: in instantiation of function template specialization 'qobject_cast<NoQUnderscoreObject *>' requested here
  qDebug() << "qobject_cast<NoQUnderscoreObject*>(object):" << qobject_cast<NoQUnderscoreObject*>(object); // Compilation error
                                                               ^
1 error generated.
make: *** [main.o] Error 1

NoQUnderscoreObject の方のコメントを外すとコンパイルエラーになります。


サンプルコード

qunderscoreobject.zip

参考資料

Q_OBJECT ドキュメント
qobject_cast ドキュメント
QMetaObject::className ドキュメント
Linux で QThread を特定の CPU コアで動くようにするにはどのようにすればよいでしょうか。

QThread には特定の CPU コアで動くように設定する方法は用意されていないので、プラットフォーム固有の機能を使う必要があります。


main.cpp:
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QElapsedTimer>
#include <cstring>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>

class Thread : public QThread
{
    Q_OBJECT

public:
    explicit Thread(int cpuNumber) : QThread(nullptr), cpuNumber(cpuNumber) {}

protected:
    void run() {
        // Linux では pthread_setaffinity_np() で CPU コアにスレッドを割り当てられます。
        // QThread::currentThreadId() が返す ID は pthread_self() と同じなので、
        // QThread で実行されるサブスレッドの CPU コアを変更できます。
        cpu_set_t cpuset;
        CPU_ZERO(&cpuset);
        CPU_SET(cpuNumber, &cpuset);
        const int status = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

        if (status != 0) {
            qWarning() << "pthread_setaffinity_np:" << strerror(status);
            return;
        }

        qInfo() << "thread" << cpuNumber
                << "started on thread id" << QThread::currentThreadId();

        // スレッド処理の CPU 負荷を CPU コアに応じて変えます。
        // 別の端末から確認しやすいように、スレッドは処理を 60 秒間実行して終了します。
        unsigned long usecs;
        switch (cpuNumber) {
        case 0:
            usecs = 1;
            break;
        case 1:
            usecs = 200;
            break;
        default:
            usecs = 100;
            break;
        }

        QElapsedTimer timer;
        timer.start();
        while (true) {
            if (timer.elapsed() >= 60000) {
                break;
            }
            QThread::usleep(usecs);
        }

        qInfo() << "thread" << cpuNumber << "finished";
    }

private:
    int cpuNumber;
};

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);

    // QThread::idealThreadCount() で CPU コア数が少なくとも 2 つあるのをチェックしています。
    if (QThread::idealThreadCount() < 2) {
        qWarning() << "Ideal thread count is small.";
        exit(EXIT_FAILURE);
    }

    qInfo() << "process id:" << getpid();
    qInfo() << "ideal thread count:" << QThread::idealThreadCount();

    Thread firstThread{0};
    Thread secondThread{1};

    int livingThreads = 2;
    auto threadFinished = [&]() {
        livingThreads -= 1;
        if (livingThreads == 0) {
            // 実行中のスレッド数が 0 になったならばイベントループを抜けてプログラムを終了します。
            QCoreApplication::quit();
        }
    };

    QObject::connect(&firstThread, &QThread::finished, threadFinished);
    QObject::connect(&secondThread, &QThread::finished, threadFinished);

    firstThread.start();
    secondThread.start();

    return app.exec();
}

#include "main.moc"
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)

project(cpuaffinity LANGUAGES CXX)

find_package(Qt6 REQUIRED COMPONENTS Core)
qt_standard_project_setup()

find_package(Threads REQUIRED)

qt_add_executable(cpuaffinity
    main.cpp
)

target_link_libraries(cpuaffinity
    PRIVATE
        Qt6::Core
        Threads::Threads
)

Ubuntu 24.04 では、Qt 自身が Linux で pthread を使うため、Qt6::Core をリンクしていれば Threads::Threads を明示しなくてもビルドできる場合があります。ただし、このサンプルコードは pthread_self() と pthread_setaffinity_np() を直接呼び出しているため、CMakeLists.txt では依存関係として Threads::Threads を明示しています。

実行すると、プログラム自身がプロセス ID とスレッドの開始、終了を表示します。スレッドの行順やスレッド ID は環境によって変わります。

process id: 450390
ideal thread count: 4
thread 0 started on thread id 0x7f8f9bfff6c0
thread 1 started on thread id 0x7f8f9b7fe6c0
thread 1 finished
thread 0 finished

CPU 割り当ては、実行中に別の端末から以下のように確認できます。コマンドに指定する PID は、プログラムが表示した process id: の値に置き換えてください。メインスレッドしか表示されない場合は、サブスレッドがすでに終了しているか、CPU 割り当てに失敗して早期終了しています。

$ ps -C cpuaffinity -L -o comm,pid,lwp,psr,%cpu
COMMAND             PID     LWP PSR %CPU
cpuaffinity      450390  450390   2  0.0
Thread           450390  450391   0  7.5
Thread           450390  450392   1  2.0
$ LANG=C taskset -pac 450390
pid 450390's current affinity list: 0-3
pid 450391's current affinity list: 0
pid 450392's current affinity list: 1
$

参考資料

Ubuntu Manpage: pthread_setaffinity_np, pthread_getaffinity_np
QThread::currentThreadId ドキュメント
QThread:: idealThreadCount ドキュメント
QVector と std::vector の変換をするにはどのようにすればよいでしょうか。

Qt 5.14 からは、範囲コンストラクターを使って QVector と std::vector との相互変換ができます。 Qt 5.14 より前は、toStdVector() と fromStdVector() が使えます。 std::copy でも変換できます。

main.cpp:
#include <QVector>
#include <QDebug>

int main()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
    qDebug() << "Using range constructors.";
    {
        const QVector<int> qVectorInt{1, 2, 3};
        const std::vector<int> stdVectorInt(qVectorInt.cbegin(), qVectorInt.cend());
        auto debug = qDebug();
        for (const auto value : stdVectorInt) {
            debug << value;
        }
    }

    {
        const std::vector<int> stdVectorInt{10, 20, 30};
        const QVector<int> qVectorInt(stdVectorInt.cbegin(), stdVectorInt.cend());
        auto debug = qDebug();
        for (const auto value : qVectorInt) {
            debug << value;
        }
    }

#else
    qDebug() << "Using conversion methods.";
    {
        const QVector<int> qVectorInt{1, 2, 3};
        const std::vector<int> stdVectorInt = qVectorInt.toStdVector();
        auto debug = qDebug();
        for (const auto value : stdVectorInt) {
            debug << value;
        }
    }

    {
        const std::vector<int> stdVectorInt{10, 20, 30};
        const QVector<int> qVectorInt = QVector<int>::fromStdVector(stdVectorInt);
        auto debug = qDebug();
        for (const auto value : qVectorInt) {
            debug << value;
        }
    }
#endif

    qDebug() << "Using std::copy.";
    {
        QVector<int> qVectorInt{1, 2, 3};
        std::vector<int> stdVectorInt;
        std::copy(qVectorInt.cbegin(), qVectorInt.cend(), std::back_inserter(stdVectorInt));
        auto debug = qDebug();
        for (const auto value : stdVectorInt) {
            debug << value;
        }
    }

    {
        std::vector<int> stdVectorInt{10, 20, 30};
        QVector<int> qVectorInt;
        std::copy(stdVectorInt.cbegin(), stdVectorInt.cend(), std::back_inserter(qVectorInt));
        auto debug = qDebug();
        for (const auto value : qVectorInt) {
            debug << value;
        }
    }
}

コードの説明

範囲コンストラクターを使った変換です。


toStdVector() と fromStdVector() を使った変換です。これらのメソッドは非推奨で、Qt 6 で廃止されています。


std::copy() を使った変換です。前述の 2 つの方法の方が簡潔な変換で、定数にもできます。


サンプルコード

vectorconversion.zip

参考資料

QVector 範囲コンストラクタードキュメント
QVector::toStdVector ドキュメント
QVector::fromStdVector ドキュメント
std::vector::コンストラクタ cpprefjp C++日本語リファレンス
std::copy cpprefjp C++日本語リファレンス
QString と char* を相互に型変換するにはどのようにすればよいでしょうか。

QString を char* に型変換するには、まず QString::toLatin1()、QString::toUtf8() や QString::toLocal8Bit() などで QByteArray 型に変換します。 次に data() または constData() を呼出して、QByteArray オブジェクトに保存されている char* 型または const char* データへのポインタを取出します。

逆に、char* を QString に型変換するには、QLatin1String 型または const char* 型の実引数を取る QString コンストラクターを使うか、QString のスタティックメンバー関数 fromLatin1()、fromLocal8Bit() や fromUtf8() などの関数を使います。const char* 型の実引数を取る QString コンストラクターは、fromUtf8() 関数を使って変換するので、UTF-8 でエンコードされている場合に使えます。

main.cpp:
#include <QCoreApplication>
#include <QLocale>
#include <QString>
#include <QByteArray>
#include <QDebug>
#include <iostream>

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);

    const QString nativeCountryName = QLocale::system().nativeCountryName();

    QByteArray ba = nativeCountryName.toLocal8Bit();
    const char* cp = ba.constData();
    std::cout << "toLocal8Bit: " << cp << std::endl;
    QString string = QString::fromLocal8Bit(cp);
    qDebug() << "fromLocal8Bit:" << string;

    ba = nativeCountryName.toUtf8();
    cp = ba.constData();
    std::cout << "toUtf8: " << cp << std::endl;
    string = QString::fromUtf8(cp);
    qDebug() << "fromUtf8:" << string;

    const QString countryName("Japan");
    ba = countryName.toLatin1();
    cp = ba.constData();
    std::cout << "toLatin1: " << cp << std::endl;
    string = QString::fromLatin1(cp);
    qDebug() << "fromLatin1:" << string;
}

コードの説明

QLocale::system() でシステムロケールを取得し、nativeCountryName() を呼び出すと日本語環境では国名を日本語で返します。


QString::toLocal8Bit() 関数は、言語環境のエンコードでバイト列を格納した QByteArray オブジェクトを返します。 QByteArray::constData() は、内部のバイト列のアドレスを const char* 型で返します。

QByteArray::constData() を呼ぶ前に QByteArray 型の変数に一旦保存する必要があることに注意しましょう。 つまり、以下のようにすると cp を参照しようしたときには、データを保存している QByteArray オブジェクトが存在しなくなってしまっているので、ポインターが無効になり、参照すると未定義動作になるためです。必ずクラッシュするとは限らず、偶然読める場合もあります。

const char* cp = nativeCountryName.toLocal8Bit().constData();

逆に、言語環境のエンコードのバイト列 const char* を QString に型変換するには、QString::fromLocal8Bit() を使います。


QString::toUtf8() 関数は、UTF-8 でエンコードされたバイト列を格納した QByteArray オブジェクトを返します。 逆に、UTF-8 でエンコードされたバイト列を QString に型変換するには、QString::fromUtf8() を使います。


QString::toLatin1() 関数は、Latin-1 でエンコードされたバイト列を格納した QByteArray オブジェクトを返します。 逆に、Latin-1 でエンコードされたバイト列を QString に型変換するには、QString::fromLatin1() を使います。


toLocal8Bit: 日本
fromLocal8Bit: "日本"
toUtf8: 日本
fromUtf8: "日本"
toLatin1: Japan
fromLatin1: "Japan"

Unix や Linux などの UTF-8 環境での実行結果です。


toLocal8Bit: 日本
fromLocal8Bit: "日本"
toUtf8: 譌・譛ャ
fromUtf8: "日本"
toLatin1: Japan
fromLatin1: "Japan"

Windows の SJIS 環境での実行結果です。toUtf8 が文字化けしているのは SJIS 環境で UTF-8 文字列を表示しようとしたためです。


サンプルコード
stringconversion.zip

適用条件と確認

Qt 6 / Qt Coreの例です。通信やファイルの仕様がUTF-8なら toUtf8()/fromUtf8() を対にして使います。toLocal8Bit() の結果と端末の表示文字コードは環境依存です。上のWindows出力はSJIS設定時の例で、すべてのWindowsに共通ではありません。

cp は ba が破棄されるか内容変更で内部領域が無効になるまでだけ使用します。非同期処理やAPIがポインターを保存する場合は、その処理が終わるまで QByteArray を保持してください。途中にNULを含むデータは fromUtf8(cp, length) のように長さも渡します。Latin-1では日本語を表現できません。

サンプルを実行し、変換前後のQStringが一致することを確認します。端末の文字化けとバイト列の変換失敗は分けて判断してください。

参考資料

非 Qt スレッドで生成したサブスレッドから Qt スレッドのスロットを安全に呼び出すにはどのようにすればよいでしょうか。

非 Qt スレッドから QMetaObject::invokemethod() で Qt スレッドのスロットを安全に呼び出せます。

std::thread で生成した非 Qt サブスレッドからメインスレッドのスロットを呼び出すサンプルコードです。

main.cpp:

#include <QCoreApplication>
#include <QMetaObject>
#include <QThread>
#include <QTimer>
#include <QDebug>
#include <iostream>

class Object : public QObject
{
    Q_OBJECT

public:
    Object() : QObject(nullptr) {
    }

public slots:
    void slot(const QString& message) {
        qDebug() << Q_FUNC_INFO;
        std::cerr << "    thread:id: " << std::hex << std::this_thread::get_id() << std::endl;
        qDebug() << "    QThread::currentThreadId():" << QThread::currentThreadId();
        qDebug() << "    message:" << message;
        QCoreApplication::quit();
    }
};

void worker(QObject* object)
{
    qDebug() << Q_FUNC_INFO;
    qDebug() << "    Starting";
    std::cerr << "    thread:id: " << std::hex << std::this_thread::get_id() << std::endl;
    qDebug() << "    QThread::currentThreadId():" << QThread::currentThreadId();

    qDebug() << "    Invoke slot";
    const QString message = "Hi there.";
    QMetaObject::invokeMethod(object, "slot", Q_ARG(QString, message));

    qDebug() << "    Exiting";
}

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);

    qDebug() << Q_FUNC_INFO;
    std::cerr << "    thread:id: " << std::hex << std::this_thread::get_id() << std::endl;
    qDebug() << "    QThread::currentThreadId():" << QThread::currentThreadId();

    Object object;

    std::thread nativeThread(worker, &object);

    const int ret = app.exec();

    nativeThread.join();

    return ret;
}

#include "main.moc"

コードの説明

スロットを用意したクラスで、このクラスのオブジェクトのスレッドアフィニティをメインスレッドにし、スロットをサブスレッドから呼び出すようにします。 2 通りの方法で、どのスレッドで実行されているか分かるようにしています。スロットの最後で QCoreApplication::quit() を呼び出してメインイベントループを終了させます。


std::trhead で生成したサブスレッドで実行する関数です。QMetaObject::invokeMethod() でメインスレッドのオブジェクトのスロットを呼び出します。 QMetaObject::invokeMethod() は、スレッドセーフな QCoreApplication::postEvent() を使って、メインスレッドのイベントループにスロットを呼び出すための内部イベントをポストしています。


std::thread で生成した非 Qt サブスレッドで、クラス Object のオブジェクトを渡して worker 関数を動かします。イベントループの終了後に、非 Qt サブスレッドの終了を待つようにしています。


int main(int, char **)
    thread:id: 0x114121dc0
    QThread::currentThreadId(): 0x114121dc0
void worker(QObject *)
    Starting
    thread:id: 0x7000064ac000
    QThread::currentThreadId(): 0x7000064ac000
    Invoke slot
    Exiting
void Object::slot(const QString &)
    thread:id: 0x114121dc0
    QThread::currentThreadId(): 0x114121dc0
    message: "Hi there."

実行結果です。スロットが Qt メインスレッドで実行されているのが分かります。


サンプルコード

stdthreadtomain.zip

参考資料

QMetaObject Struct
QCoreApplication Class
実行中にオブジェクトからそのクラスのオブジェクトを生成できますか。

Q_INVOKABLE を付けたメンバー関数は QMetaObject::invokeMethod() で呼び出しできるようになります。これが Q_INVOKABLE の一般的な使い方です。コンストラクターは特殊メンバー関数で Q_INVOKABLE を付けると、クラスのスタティックメタオブジェクトに対して QMetaObject::newInstance() を呼び出してインスタンス生成ができます。


main.cpp:
#include <QApplication>
#include <QMetaObject>
#include <QPushButton>

class PushButton : public QPushButton
{
    Q_OBJECT

public:
    Q_INVOKABLE PushButton(QWidget* parent = nullptr) : QPushButton(parent) {}
    Q_INVOKABLE PushButton(const QString& text, QWidget* parent = nullptr) : QPushButton(text, parent) {}
    Q_INVOKABLE PushButton(const QIcon& icon, const QString& text, QWidget* parent = nullptr) : QPushButton(icon, text, parent) {}
};

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    const auto someButton = new PushButton("OK?");
    someButton->setMinimumSize(someButton->sizeHint());
    QObject::connect(someButton, &PushButton::clicked, &app, &QApplication::quit);
    someButton->show();

    QObject* const someObject = someButton;

    if (qobject_cast<QPushButton*>(someObject)) {
        QObject* const maybeButton = someObject->metaObject()->newInstance(Q_ARG(QString, "Hi three!"));
        Q_ASSERT(maybeButton);
        maybeButton->setProperty("minimumSize", maybeButton->property("sizeHint"));
        QObject::connect(maybeButton, SIGNAL(clicked()), &app, SLOT(quit()));
        QMetaObject::invokeMethod(maybeButton, "show");
    }

    return app.exec();
}

#include "main.moc"

コードの説明

QPushButton を継承した PushButton のコンストラクターに Q_INVOKABLE を付けます。


PushButton のオブジェクトを QObject のポインター someObject に入れておきます。


qobject_cast で someObject が QPushButton を継承したクラスか判定し、そのスタティックメタオブジェクトを metaObject() から newInstance() を呼び出して someObject と同じクラスのオブジェクトを生成しています。 metaObject() は仮想関数で、PushButton のスタティックメタオブジェクトが返ります。 qobject_cast によって、QPushButton を継承していると分かっているので、QPushButton のプロパティーの参照と設定をし、スロット show() を QMetaObject::invokeMethod() で呼びたせます。


サンプルコード

construct.zip

参考資料

QMetaObject Struct
Qt 6 の QTextStream で、コーデックを eucJP にして使うにはどのようにすればよいでしょうか。

Qt 6では、QTextStream::setCodec() は廃止され、setEncoding() で指定できるのは QStringConverter::Encoding に定義されたエンコーディングです。

ただし、QtがICU対応でビルドされている場合は、QStringDecoder/QStringEncoder でEUC-JPなどの追加コーデックを利用できます。利用可能な名前は QStringConverter::availableCodecs() で確認してください。

以下はQt 6.11以降用のmain.cpp全体です。終端の未完データも拒否するため finalize() を使用します。

main.cpp:

#include <QCoreApplication>
#include <QFile>
#include <QDebug>
#include <QStringDecoder>

int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);
    if (app.arguments().size() != 2) {
        qWarning() << "Usage: eucjp-reader input.txt";
        return 1;
    }
    QFile file(app.arguments().at(1));
    if (!file.open(QIODevice::ReadOnly)) {
        qWarning() << file.errorString();
        return 1;
    }
    QStringDecoder decoder("EUC-JP");
    if (!decoder.isValid()) {
        qWarning() << "EUC-JP is not available in this Qt build";
        return 2;
    }
    const QByteArray bytes = file.readAll();
    if (file.error() != QFileDevice::NoError) {
        qWarning() << file.errorString();
        return 1;
    }
    const QString text = decoder(bytes);
    const bool conversionError = decoder.hasError();
    const auto end = decoder.finalize();
    if (conversionError || end.error != QStringDecoder::FinalizeResult::Error::NoError) {
        qWarning() << "Invalid or incomplete EUC-JP input";
        return 3;
    }
    qDebug().noquote() << text;
    return 0;
}

decoder.isValid() が false の場合、そのQtビルドではEUC-JPを利用できません。その場合は、ICU対応のQtを使用するか、互換性が必要な既存コードではQt5Compatの QTextCodec を利用します。

参考資料

QStringConverter Class
QStringDecoder Class

ビルドはC++17以上とQt Coreを使用します。Qt CreatorでQt 6のコンソール用CMakeプロジェクトを作成し、main.cppを置き換えて実行できます。

確認方法

EUC-JPで保存した既知の日本語ファイルを引数に渡し、元の文章が出ることを確認します。コーデック未対応は終了コード2、不正・未完入力は3です。端末も日本語を表示できる文字コードに設定してください。小さなファイルを一括で読む例なので、大容量ファイルはチャンク読み込みに変更します。

Qt 6.10以前には finalize() がありません。hasError() だけで末尾の未完入力まで検出できるとは限りません。古いQtも対象にする場合は入力全体の検証方法を別途用意してください。QTextStreamを使い続ける必要がある場合は、Qt5Compatによる移行範囲も検討します。

Qt で定義されている列挙子の値を文字列にするにはどのようにすればよいですか。

QMetaEnum を使うのが標準的な方法です。ただし、通常の列挙型と QFlags 型は分けて扱います。Qt::Orientation は列挙型ですが、Qt::Alignment は QFlags<Qt::AlignmentFlag> の別名です。

メタオブジェクトへ登録された列挙型は、次のように QMetaEnum::fromType() と valueToKey() で文字列へ変換できます。


main.cpp:
#include <QCoreApplication>
#include <QDebug>
#include <QMetaEnum>
#include <QString>

#include <type_traits>

template<typename Enum>
QString enumToString(Enum value)
{
    static_assert(std::is_enum_v<Enum>);

    const QMetaEnum metaEnum = QMetaEnum::fromType<Enum>();
    const char *key = metaEnum.valueToKey(static_cast<int>(value));
    return key ? QString::fromLatin1(key) : QString{};
}

QString alignmentToString(Qt::Alignment value)
{
    const int index = Qt::staticMetaObject.indexOfEnumerator("Alignment");
    const QMetaEnum metaEnum = Qt::staticMetaObject.enumerator(index);
    return QString::fromLatin1(metaEnum.valueToKeys(value.toInt()));
}

int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);

    qDebug().noquote() << enumToString(Qt::Horizontal);
    // Horizontal

    qDebug().noquote()
        << alignmentToString(Qt::AlignLeft | Qt::AlignVCenter);
    // AlignLeading|AlignVCenter

    return 0;
}

コードの説明

Qt::Orientation は次のように変換できます。

一方、Qt::Alignment のようなフラグ型では、複数の値を組み合わせられるため valueToKeys() を使います。Qt 名前空間の Alignment メタ列挙子は Qt::staticMetaObject から取得できます。

列挙型がメタオブジェクトへ登録されていない場合、QMetaEnum::fromType() は使用できません。独自の列挙型では Q_ENUM、名前空間内の列挙型では Q_ENUM_NS を使って登録します。フラグ型は Q_DECLARE_FLAGS と Q_FLAG または Q_FLAG_NS で登録します。

表示やデバッグだけが目的なら、登録済みの列挙値を直接 qDebug() へ渡す方法もあります。ただし、保存形式や通信形式として文字列を使用する場合は、列挙子名を将来も変更しないなど、形式の互換性を別途管理してください。

参考資料

QMetaEnum Class

QFlags Class

シグナルが送信された場合、すべてのシグナルをひとつのスロットで処理できますか。例えば、QPushButton と QSlider それぞれが、clicked() や valueChanged(int) などのシグナルを送信したときに、同じスロットでシグナルを受信し、どのウィジェットがどのシグナルを送信したかを区別できるでしょうか。

シグナルが送信されスロットが呼ばれた場合、スロット内で sender() を呼び出すと送信したオブジェクトが返り、senderSignalIndex() でメタメソッドのインデックスが返ります。従って、メタオブジェクトを metaObject() で求め、QMetaObject::method(int) に、そのインデックスを渡して呼び出せば、シグナルのメタメソッドが求まるので、QMetaMethod::methodSignature() でシグナルのシグニチャを取得できます。

sender() と senderSignalIndex() は、モジュール性に関してオブジェクト指向の原則に反します。しかし、ひとつのスロットに多くのシグナルが接続される場合には実用的です。


main.cpp:
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QSlider>
#include <QVBoxLayout>
#include <QMetaMethod>
#include <QDebug>

class Receiver : public QObject {
    Q_OBJECT

public:
    Receiver(QObject* parent = nullptr)
        : QObject(parent) {
    }

public slots:
    void universalSlot() {
        if (sender() == nullptr) {
            return;
        }
        const QMetaMethod metaMethod = sender()->metaObject()->method(senderSignalIndex());
        if (sender()->inherits("QPushButton")) {
            const auto pushButton = qobject_cast<QPushButton*>(sender());
            if (qstrcmp(metaMethod.methodSignature(), "clicked(bool)") == 0) {
                qDebug() << "pushbutton: clicked(bool):" << pushButton->isChecked();
            }
        } else if (sender()->inherits("QSlider")) {
            const auto slider = qobject_cast<QSlider*>(sender());
            if (qstrcmp(metaMethod.methodSignature(), "valueChanged(int)") == 0) {
                qDebug() << "slider: valueChanged(int):" << slider->value();
            }
        }
    }
};

class Harness : public QWidget {
    Q_OBJECT

public:
    Harness() : QWidget(nullptr) {
        const auto pushButton = new QPushButton("Click");
        const auto slider = new QSlider;
        slider->setOrientation(Qt::Horizontal);

        const auto topLayout = new QVBoxLayout(this);
        topLayout->addWidget(pushButton, 0, Qt::AlignHCenter);
        slider->setMinimumWidth(200);
        topLayout->addWidget(slider, 0, Qt::AlignHCenter);

        const auto receiver = new Receiver(this);

        connect(pushButton, &QPushButton::clicked, receiver, &Receiver::universalSlot);
        connect(slider, &QSlider::valueChanged, receiver, &Receiver::universalSlot);
    }
};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    Harness window;
    window.show();
    return app.exec();
}

#include "main.moc"

コードの説明

どのシグナルから呼ばれたかを判断しています。


2 つの異なるシグナルをひとつのスロット universalSlot() に接続しています。


サンプルコード

universalslot.zip

参考資料

QObject::sender ドキュメント
QObject::senderSignalIndex ドキュメント
QMetaMethod ドキュメント

適用条件と確認

Qt 6 / Qt Widgets用のmain.cpp全体です。CMakeではqt_standard_project_setup()でAUTOMOCを有効にしてください。ボタン操作でclicked(bool)、スライダー操作でvalueChanged(int)のログが出ることを確認します。

ここで読んでいるのはウィジェットの現在値であり、任意のシグナル引数を汎用的に取り出しているわけではありません。キュー接続では送信時から値が変わることもあります。送信時の値が必要なら、型付きのスロットや、引数を受け取るラムダから共通処理を呼ぶ構成にします。

この例は送信元と受信先が同じGUIスレッドにある前提です。別スレッドからのDirectConnectionではsender()等の制限があるため、そのまま流用しないでください。通常の関数呼び出しではsender()がないこともあります。

QString 文字列を表示したときの見かけの文字数を求めるにはどのようにすればよいですか。

QString は内部で UTF-16 エンコーディングを用いて文字列を管理しています。したがって、size() や length() が返す値は、必ずしも見た目上の文字数と一致するとは限りません。

QTextBoundaryFinder を使用すれば、見た目上の文字列境界を検出できます。以下はサンプルコードです。


main.cpp:
#include <QTextBoundaryFinder>
#include <QDebug>

using namespace Qt::Literals::StringLiterals;

qsizetype visibleCharCount(QStringView text)
{
    QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);

    int count{0};
    while (finder.toNextBoundary() != -1) {
        count++;
    }

    return count;
}

int main(int argc, char** argv)
{
    auto text = u"いろは𩸽😀"_s;

    if (argc > 1) {
        text = argv[1];
    }

    qDebug().noquote() << u"%1 = %2"_s.arg(text).arg(visibleCharCount(text));
}

visibleCharCount() は、引数として与えられた文字列 text に含まれる「見た目上の文字数」(グラフェム数) を数えて返します。

QTextBoundaryFinder を Grapheme モードで初期化することで、複合文字 (合成文字や複数のコードポイントで表現される文字) を一文字として数えられます。

QTextBoundaryFinder::toNextBoundary() を呼び出して次の見た目上の文字境界へ移動します。返り値が -1 になるまで (つまり、文字列の終端まで) 繰り返し、文字境界の数を数えます。

空白やタブ、改行などの文字も文字数として数えるため、これらを除外する必要があるでしょう。


参考資料

QTextBoundaryFinder Class
qDebug() の出力は、複数スレッド間で競合しますか。

qDebug() などのQtのロギング関数はスレッドセーフです。複数スレッドから呼び出せます。標準のメッセージ処理では、各呼び出しで構築したメッセージは一単位として出力されます。


main.cpp:
#include <QDebug>
#include <QThread>

void writeMessages(QAnyStringView text)
{
    for (int i = 0; i < 3; ++i)
        qDebug().noquote() << text << i;
}

int main()
{
    QThread *first = QThread::create(writeMessages, u"first");
    QThread *second = QThread::create(writeMessages, u"second");

    first->start();
    second->start();
    first->wait();
    second->wait();

    delete first;
    delete second;
}

スレッド間でメッセージの順序は保証されません。順序が必要な場合は、ログを一つのスレッドへ転送するなど、アプリケーション側で制御してください。

また、qInstallMessageHandler() で独自のメッセージハンドラーを設定した場合、そのハンドラーは複数スレッドから並行して呼び出される可能性があります。共有ファイルや共通バッファーへ書き込む処理は、ハンドラー側で排他制御してください。

参考資料

QDebug Class
Debugging Techniques
バイトストリームを読み込みながら文字表示をするときにマルチバイト文字区切りで文字を取得するにはどのようにすればよいでしょうか。

Qt 6では、同じ QStringDecoder インスタンスへ受信したバイト列を順番に渡します。デコーダーは呼び出し間の状態を保持するので、UTF-8の文字途中でチャンクが分かれても、次のチャンクと合わせて変換できます。

以下はQt 6.11以降用のmain.cpp全体です。最後に finalize() を呼び、途中で切れたまま終了した入力を検出します。

#include <QByteArray>
#include <QDebug>
#include <QList>
#include <QStringDecoder>

int main()
{
    QStringDecoder decoder(QStringDecoder::Utf8);
    QString text;
    const QList<QByteArray> chunks = {
        QByteArray::fromHex("e381"),
        QByteArray::fromHex("82e381"),
        QByteArray::fromHex("84")
    };
    for (const QByteArray &chunk : chunks) {
        text += decoder(chunk);
        if (decoder.hasError()) {
            qWarning() << "Invalid UTF-8 input";
            return 1;
        }
    }
    const auto end = decoder.finalize();
    if (end.error != QStringDecoder::FinalizeResult::Error::NoError) {
        qWarning() << "Incomplete UTF-8 input at end";
        return 2;
    }
    qDebug().noquote() << text; // あい
    return 0;
}

コードの説明と確認

この例は「あい」のUTF-8バイト列を文字の途中で分割しています。正常時は「あい」と出力して0で終了します。最後のチャンクを空にすると、未完入力として2で終了します。不正なバイト列を途中に入れた場合は1で終了します。

受信のたびにデコーダーを作り直したり、resetState()を呼んだりしないでください。一つのストリームにつき一つのデコーダーを使い、入力終了時だけfinalize()します。この例は不正・未完入力を拒否する方針です。

Qt 6.10以前もチャンク変換は可能ですが、finalize()はありません。hasError()だけでは末尾の未完入力を検出できない場合があります。終端検証が必要なら、別の検証方法を用意するかQt 6.11以降を使用します。

ビルドはC++17以上とQt Coreを使用します。Qt CreatorでQt 6のコンソール用CMakeプロジェクトを作成し、main.cppを置き換えて実行できます。

参考資料

QEvent::registerEventType() を呼び出すとイベントタイプを発番できますが、固定範囲を発番しないようにできますか。

QEvent::registerEventType() に「この範囲を除外する」というAPIはありません。独自イベント同士の衝突を防ぐ目的なら、固定番号を直接割り当てず、この関数が返した番号を各イベントで共有する設計を推奨します。

hint が QEvent::User~QEvent::MaxUser の範囲内で未登録なら、その値が返ります。既に登録済みなら別の未使用値が返ります。番号を取得できない場合や終了処理中は -1 が返ります。番号の発行順序は公開仕様として保証されていません。

既存の固定範囲を保護する必要がある場合

ほかのモジュールがイベント番号を取得する前の初期化段階で、対象範囲の各値をhintとして登録する方法があります。ただし、返り値が要求した値と一致したことを全件確認してください。一致しない場合は、その番号が既に登録済みである可能性があり、自分のイベントに使ってよいとは判断できません。

登録解除のAPIはありません。途中で失敗しても、それまでの登録や代わりに返された番号を元に戻せないため、実行中に予約を繰り返す設計は避けます。起動順を制御できないプラグインとの共存では、固定範囲の予約より、registerEventType()で取得した番号を渡し合う方式が確実です。

確認方法

同じhintで2回登録した場合、1回目がhintを取得できても、2回目は別番号になることを確認します。-1も必ず処理し、値の大小や連番であることには依存しないでください。Qt 6を対象とする説明です。

参考資料

QPointer はどのような場合に使うとよいでしょうか。

QPointer は、時間をまたいで QObject 派生オブジェクトへの参照を保持する場合に使うとよいです。

ダイアログやウィジェットをメンバー変数として保持しているが、そのオブジェクトが別の箇所で破棄される可能性がある場合です。通常のポインターを保持していると、参照先が破棄された後に不正なアドレスを指すダングリングポインターになります。QPointer で保持しておけば、参照先が破棄されたときに自動的に nullptr になるため、if 文などで生存確認をしてから安全に使えます。

main.cpp:
#include <QApplication>
#include <QLabel>
#include <QPointer>
#include <QTimer>
#include <QDebug>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    app.setQuitOnLastWindowClosed(false);

    QLabel* label = new QLabel("Status");
    QPointer<QLabel> guardedLabel = label;

    label->show();
    label->deleteLater();

    QTimer::singleShot(1000, &app, [guardedLabel, &app]() {
        if (guardedLabel) {
            guardedLabel->setText("Running");
        } else {
            qDebug() << "The label has been deleted";
        }
        app.quit();
    });

    return app.exec();
}

この例では QLabel への参照を QPointer で保持しています。QLabel が deleteLater() によって破棄された後は、guardedLabel は nullptr になります。そのため、タイマーで後から処理を行う場合でも、if (guardedLabel) で確認してからアクセスできます。

一方、関数の引数として一時的に受け取るだけであれば、通常は QPointer ではなく T* を使えば十分です。QPointer は T* に自動変換できるため、関数の引数を QPointer にするよりも、時間をまたいで保持するメンバー変数や遅延実行の中で使う変数に使うのが分かりやすいです。

QPointer は参照先を所有しません。QPointer 自体を破棄しても、参照先の QObject は破棄されません。オブジェクトの所有権は parent の設定や所有者側の処理で管理し、QPointer は「まだ存在しているかを確認できる参照」として使います。

参考資料

QPointer Class | Qt Core | Qt 6

適用条件と確認

Qt 6 / Qt Widgets用のmain.cpp全体です。ウィンドウがなくても確認タイマーが動くよう、この例では setQuitOnLastWindowClosed(false) を設定しています。約1秒後に「The label has been deleted」が出て終了することを確認してください。

ラムダにはQPointerを値で保持し、タイマーのコンテキストをappにしています。QPointerの生存確認は、別スレッドからの同時破棄を防ぐロックではありません。この例のように、GUIオブジェクトの参照・破棄をGUIスレッド内で扱います。

Qt のクラスに定義されているシグナルとスロットをプログラムで調べるにはどのようにすればよいですか。

QMetaObject と QMetaMethod を使います。インスタンスがある場合は object->metaObject()、インスタンスを作らずに調べる場合は QObject::staticMetaObject や QAbstractButton::staticMetaObject などの静的メタオブジェクトを使います。

methodOffset() から methodCount() までを調べると、そのクラス自身で追加されたメソッドだけを列挙できます。基底クラスから継承したものも含めたい場合は 0 から methodCount() までを調べます。

main.cpp:
#include <QAbstractButton>
#include <QAbstractItemModel>
#include <QAbstractSlider>
#include <QApplication>
#include <QMetaMethod>
#include <QMetaObject>
#include <QTextStream>
#include <QTimer>

// QMetaMethod::MethodType は enum なので、そのままでは読みやすい文字列として
// 出力できません。サンプルの出力用に、種類を短い文字列へ変換します。
QString methodTypeName(QMetaMethod::MethodType type)
{
    switch (type) {
    case QMetaMethod::Method: return QStringLiteral("method");
    case QMetaMethod::Signal: return QStringLiteral("signal");
    case QMetaMethod::Slot: return QStringLiteral("slot");
    case QMetaMethod::Constructor: return QStringLiteral("constructor");
    }
    return QStringLiteral("unknown");
}

// 1 つのメタオブジェクトに登録されているメソッドを表示します。
//
// QMetaObject は、QObject 派生クラスのクラス名、プロパティ、シグナル、
// スロット、Q_INVOKABLE メソッドなどの情報を持っています。
void dumpMethods(QTextStream &out, const QMetaObject &metaObject)
{
    out << metaObject.className() << Qt::endl;

    // methodCount() は基底クラスから継承したメソッドも含めた数を返します。
    // methodOffset() は、このクラス自身で追加された最初のメソッドの位置です。
    // ここでは差分が見やすいように、そのクラス自身のメソッドだけを出力します。
    for (int i = metaObject.methodOffset(); i < metaObject.methodCount(); ++i) {
        const QMetaMethod method = metaObject.method(i);

        // methodSignature() は "clicked(bool)" のような正規化済みシグニチャを
        // 返します。methodType() を併せて見ると signal/slot/method を区別できます。
        out << "    " << method.methodSignature() << " "
            << methodTypeName(method.methodType()) << Qt::endl;
    }
}

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    // インスタンスを作らずにクラス情報だけを調べたい場合は staticMetaObject を
    // 使います。QObject 派生クラスで Q_OBJECT が有効なものは、この静的メタ
    // オブジェクトを持っています。
    const QList<const QMetaObject *> metaObjects = {
        &QObject::staticMetaObject,
        &QTimer::staticMetaObject,
        &QAbstractItemModel::staticMetaObject,
        &QAbstractSlider::staticMetaObject,
        &QAbstractButton::staticMetaObject,
    };

    QTextStream out(stdout);

    // 調べたいクラスをリストに追加すれば、同じ処理でメソッド一覧を出力できます。
    for (const QMetaObject *metaObject : metaObjects) {
        dumpMethods(out, *metaObject);
        out << Qt::endl;
    }
}

CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)

project(SignalSlotList VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_standard_project_setup()

qt_add_executable(SignalSlotList main.cpp)

target_link_libraries(SignalSlotList
    PRIVATE
        Qt6::Widgets
)

set_target_properties(SignalSlotList PROPERTIES
    WIN32_EXECUTABLE OFF
    MACOSX_BUNDLE OFF
)

参考資料

QMetaObject Class
QMetaMethod Class
QObject::staticMetaObject

ウィジェット

片方のウィジェットを 2/3、もう一方を 1/3 の大きさで、スプリッターに配置するにはどのようにすればよいでしょうか。

スプリッターに配置したウィジェットの大きさを設定するには QSplitter::setSizes() を使います。


main.cpp:
#include <QApplication>
#include <QSplitter>
#include <QTextEdit>

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QSplitter splitter;
    splitter.addWidget(new QTextEdit);
    splitter.addWidget(new QTextEdit);
    splitter.setSizes( { 200, 100 } );
    splitter.show();

    return app.exec();
}

設定したい割合に応じた幅を指定して QSplitter::setSize() を呼び出します。


実行結果です。


サンプルコード

sizedsplitter.zip

参考資料

QSplitter::setSizes ドキュメント
ボタンにフォーカス枠を付けないようにできますか。

ボタンにフォーカス枠を描画させないようにするには、QProxyStyle をサブクラス化し、drawControl() を再実装して、ボタンに対して State_HasFocus フラグを落とすようにします


main.cpp:
#include <QApplication>
#include <QProxyStyle>
#include <QStyleOptionButton>
#include <QPushButton>
#include <QLayout>
#include <QDebug>

class Style : public QProxyStyle
{
public:
    Style() {}

    void drawControl(ControlElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget = nullptr) const {
        if (element == CE_PushButton) {
            const QStyleOptionButton* styleOptionButton = qstyleoption_cast<const QStyleOptionButton*>(option);
            if (styleOptionButton) {
                QStyleOptionButton handyStyleOptionButton = *styleOptionButton;
                if (handyStyleOptionButton.state & State_HasFocus) {
                    handyStyleOptionButton.state = handyStyleOptionButton.state ^ State_HasFocus;
                }
                QProxyStyle::drawControl(element, &handyStyleOptionButton, painter, widget);
                return;
            }
        }
        QProxyStyle::drawControl(element, option, painter, widget);
    }

};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    const bool isMacOSStyle = (QString::compare(app.style()->name(), "macos", Qt::CaseInsensitive) == 0);
    app.setStyle(new Style);

    QWidget top;
    const auto topLayout = new QVBoxLayout(&top);

    for (const auto title : { "One", "Two", "Three" }) {
        const auto button = new QPushButton(title);
#if defined(Q_OS_MACOS)
        if (isMacOSStyle) {
            button->setAttribute(Qt::WA_MacShowFocusRect, false);
        }
#endif
        topLayout->addWidget(button);
    }

    top.show();

    return app.exec();
}

コードの説明

QPushButton の場合、State_HasFocus ビットをクリアーして、ベースクラス QProxyStyle の drawControl() を呼び出せば、フォーカス枠を付けないようにできます。


macOS スタイルの場合には、スタイルのカスタマイズでキーボードフォーカスを付かないようにできません。 macOS では、ウィジェット属性に Qt::WA_MacShowFocusRect を false にセットしてキーボードフォーカス枠を付かないようにします。


サンプルコード

nofocusrect.zip

参考資料

Styles and Style Aware Widgets ドキュメント
QProxyStyle ドキュメント
QWidget::setAttribute ドキュメント
QTextDocument のテキストのマージンを変更するにはどのようにすればよいでしょうか。

ドキュメント全体へマージンを指定する手順は以降のようになります。

  1. ドキュメントが入っているルートフレームを QTextDocument::rootFrame() を使って取り出します。
  2. QTextFrame::frameFormat() によって QTextFrameFormat を取り出します。
  3. 設定したい値で QTextFrameFormat::setMargin() を呼び出します。
  4. 変更したQTextFrameFormatをQTextFrame::setFrameFormat()で設定し直します。

main.cpp:
#include <QApplication>
#include <QTextEdit>
#include <QTextDocument>
#include <QTextFrame>
#include <QLayout>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QVector>

class Harness : public QWidget
{
    Q_OBJECT

public:
    Harness() : QWidget(nullptr) {
        const QVector<QString> sentences{
            "The QTextEdit is an editor having abundant functions that we can format in rich text.",
            "It can be used to display HTML and Markdown formats."
        };

        const auto topLayout = new QVBoxLayout(this);

        const QVector<QTextEdit*> textEdits{ new QTextEdit, new QTextEdit };
        for (const auto textEdit : textEdits) {
            topLayout->addWidget(textEdit);
            textEdit->setText(sentences.at(0));
            textEdit->append(sentences.at(1));
        }

        // The upper QTextEdit.
        QTextCursor textCursor(textEdits.at(0)->document());
        textCursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
        QTextBlockFormat textBlockFormat = textCursor.blockFormat();
        textBlockFormat.setLeftMargin(50);
        textBlockFormat.setTopMargin(50);
        textCursor.setBlockFormat(textBlockFormat);

        // The lower QTextEdit.
        textCursor = QTextCursor(textEdits.at(1)->document());
        textCursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
        QTextFrame* const textFrame = textCursor.document()->rootFrame();
        QTextFrameFormat textFrameFormat = textFrame->frameFormat();
        textFrameFormat.setMargin(25);
        textFrame->setFrameFormat(textFrameFormat);
    }
};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    Harness window;
    window.resize(640, 400);
    window.show();
    return app.exec();
}

#include "main.moc"

コードの説明

QTextEdit のインスタンスを 2 つ生成し、それぞれに対して 2 つのテキストブロック (パラグラフ) をルートフレームに追加します。


最初にパラグラフへのマージン設定をしてみます。QTextEdit が抱えるテキストドキュメントを取り出します。次に、そのテキストドキュメントを引数に渡してテキストカーソルを生成し、カーソルとアンカーをドキュメントの末端に移動します。カーソルとアンカーは、2 つ目のテキストブロック内の末端にあります。従って、2 つ目のパラグラフの上と左に 50 ピクセルのマージンが取られます。


冒頭で説明した全体へのマージン設定です。ルートのテキストフレームにマージンを設定します。ルートフレームのフレームフォーマットを取り出して、setMargin() で上下左右のマージンを設定し、ルートフレームにフレームフォーマットを設定します。両ブロックの左側に 25 ピクセルのマージンが取られます。1 つ目のブロックの上側には 25 ピクセルのマージンが取られます。


実行結果です。


サンプルコード

textmargins.zip

参考資料

QTextDocument::rootFrame ドキュメント
QTextFrame::frameFormat ドキュメント
QTextFrameFormat::setMargin ドキュメント

適用条件と確認

Qt 6 / Qt Widgets用のmain.cpp全体です。AUTOMOCを有効にしてビルドします。上側では2段落目の上・左に50、下側では文書全体に25のマージンが付くことを確認してください。QTextBlockFormatは段落、QTextFrameFormatはフレームの書式を変更します。取得した書式オブジェクトを変更するだけでは反映されず、対応するset関数で戻す必要があります。

QPainterPath の直線や曲線に沿ってテキストを描くにはどのようするのでしょうか。

ペインターパスの途中にテキストを描くには、QPainterPath::pointAtPercent() で位置を求めて、その位置にテキストを描画します。


main.cpp:
#include <QApplication>
#include <QWidget>
#include <QPainterPath>
#include <QPainter>

class Harness : public QWidget
{
    Q_OBJECT

public:
    Harness () : QWidget(nullptr) {}

private:
    void paintEvent (QPaintEvent*)
    {
        const QString message("Hello world!");

        const int drawWidth = qMax(1, width() / 100);

        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);
        QPen pen = painter.pen();
        pen.setWidth(drawWidth);
        pen.setColor("slateblue");
        painter.setPen(pen);

        QPainterPath path(QPointF(0.0, 0.0));

        const QPointF cp1(width() * 0.2, height() * 0.8);
        const QPointF cp2(width() * 0.8, height() * 0.2);

        path.cubicTo(cp1, cp2 , QPointF(width(), height()));

        painter.drawPath(path);

        QFont font = painter.font();
        font.setPixelSize(drawWidth * 8);
        painter.setFont(font);
        pen.setColor("orangered");
        painter.setPen(pen);

        const qreal step = path.length() / (message.size() + 1);

        for (int i = 0; i < message.size(); i++) {
            const qreal percent = path.percentAtLength(step * (i + 1));
            const QPointF point = path.pointAtPercent(percent);
            painter.drawText(point, QString(message.at(i)));
        }
    }

};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    Harness window;
    window.resize(640, 400);
    window.show();
    return app.exec();
}

#include "main.moc"

コードの説明

まず、ペインターパスで曲線を描きます。


曲線に添って 1 文字ずつ描画します。QPainterPath::pointAtPercent() で文字の位置を求め、その位置に1文字描画します。percentAtLength()で曲線に沿った距離をパラメーターに変換してからpointAtPercent()へ渡すことで、文字の基準点を等距離に配置します。


実行結果です。


サンプルコード

textonpath.zip

参考資料

QPainterPath::pointAtPercent ドキュメント

適用条件と確認

Qt 6 / Qt Widgets用のmain.cpp全体です。AUTOMOCを有効にして実行し、ウィンドウを拡大・縮小して曲線に沿う文字位置を確認します。pointAtPercent()の引数は曲線のパラメーターであり、曲線に沿った距離の割合とは一致しない点に注意してください。

この例はASCII文字の配置方法を示すもので、文字は回転させず水平に描いています。文字幅を考慮した詰め方や、曲線の接線方向への回転は別に設計します。絵文字・結合文字・アラビア文字などをQStringの1要素ずつ描くと崩れるため、一般の文章にはQTextLayoutやグリフ単位の配置を検討してください。

QSpinBox で入力値が空の状態をデフォルト値として扱うにはどのようにすればよいでしょうか。

QSpinBox では、通常は入力値を空にして表示はしません。しかし、textFromValue() を再実装して、ある条件の場合には空の文字列を返すようにすれば、入力値を空にできます。以降のサンプルコードでは、0 の代わりに空の文字列を返すようにしています。


main.cpp:
#include <QApplication>
#include <QSpinBox>
#include <QLineEdit>
#include <QLayout>

class SpinBox : public QSpinBox
{
    Q_OBJECT

public:
    explicit SpinBox(QWidget* parent = nullptr)
        : QSpinBox(parent)
    {
        setRange(-100, 100);
        setValue(1);
        setValue(0);
        connect(lineEdit(), &QLineEdit::textChanged, this, [this]() {
                    if (lineEdit()->text().isEmpty()) {
                        setValue(defaultValue);
                    }
                });
    }

protected:
    QValidator::State validate(QString &text, int &pos) const override
    {
        if (text.isEmpty())
            return QValidator::Acceptable;
        return QSpinBox::validate(text, pos);
    }

    QString textFromValue(int value) const override
    {
        if (value == defaultValue) {
            return QString();
        }
        return QSpinBox::textFromValue(value);
    }

    int valueFromText(const QString& text) const override
    {
        if (text.isEmpty()) {
            return defaultValue;
        }
        return QSpinBox::valueFromText(text);
    }

private:
    int defaultValue = 0;
};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    SpinBox spin;
    spin.show();
    return app.exec();
}

#include "main.moc"

コードの説明

入力値を削除キーで空にしたときにデフォルト値になるようにします。


textFromValue()を再定義して、入力値がデフォルト値の場合に空文字列を返せば、スピンボックスの表示が空になります。


valueFromText()を再定義して、入力が空の場合にデフォルト値を返すようにします。


サンプルコード

defaultemptyspinbox.zip

参考資料

QSpinBox::textFromValue ドキュメント
QSpinBox::valueFromText ドキュメント

適用条件と確認

Qt 6 / Qt Widgets用のmain.cpp全体です。AUTOMOCを有効にしてビルドします。0を空表示する例で、値なしを表す独立した状態ではありません。範囲は-100~100、prefix・suffix・specialValueTextは未設定です。この条件を変える場合は変換・検証も合わせて見直してください。

初期表示が空であること、10を入力できること、全選択して削除するとvalue()が0になること、Enter・フォーカス移動後も空表示になること、上下キーで値を変更できることを確認します。validate()でも空入力を許可しています。真の未入力と数値0を区別する場合は、別の状態フラグや入力部品を用意します。

背景色を付けたときに文字色を黒と白のどちらにするか決める方法は Qt に用意されているでしょうか。

Qt に用意されている方法では関数 qGray() を使う方法があります。qGray は QRgb (unsigned int の typedef) 値から灰色を以下の計算式で求めて返します。

(r * 11 + g * 16 + b * 5) / 32

qGray()による閾値判定は簡易的な方法で、アクセシビリティ基準のコントラスト比を保証するものではありません。


一般に、仕様として説明しやすく再現性も高いのは WCAG のコントラスト比を使う方法です。WCAG では sRGB を線形化して相対輝度 L を計算し、コントラスト比を (L1 + 0.05) / (L2 + 0.05) で評価します。以降が実装方針です。

  1. 背景色 bg を sRGB として相対輝度 L(bg) を計算 (WCAG の定義)
  2. 候補の文字色を 黒/白で試し、それぞれのコントラスト比を計算
  3. どちらかが目標比を満たすなら、より高い方を採用
  4. 両方とも目標比を満たさない場合は背景色を変更するか、文字の背後に単色の領域を設ける。黒と白より有利な中間の文字色を探しても解決しない。

以降は SVG 色で確認をするサンプルコードです。


main.cpp:
#include <QApplication>
#include <QHeaderView>
#include <QTableWidget>
#include <QColor>
#include <QLayout>
#include <QtMath>

using namespace Qt::Literals::StringLiterals;

// sRGB -> linear
static double srgbToLinear(double color)
{
    // WCAG 2.1 の相対輝度の定義に沿う (0.04045 しきい値)
    // https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
    if (color <= 0.04045) {
        return color / 12.92;
    }
    return qPow((color + 0.055) / 1.055, 2.4);
}

// WCAG relative luminance (0..1)
static double relativeLuminance(const QColor& color)
{
    // QColor::redF()/greenF()/blueF() は 0..1 の値を返す
    const double r = srgbToLinear(color.redF());
    const double g = srgbToLinear(color.greenF());
    const double b = srgbToLinear(color.blueF());
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

static double contrastRatio(double L1, double L2)
{
    // L1 >= L2 を想定
    return (L1 + 0.05) / (L2 + 0.05);
}

// WCAG: 背景に対して黒/白のコントラスト比を比較し、より大きい方を採用
static QColor textColorByWcag(const QColor& bg, double* outRatio = nullptr)
{
    const double Lbg = relativeLuminance(bg);
    const double Lwhite = 1.0; // 白
    const double Lblack = 0.0; // 黒

    const double crWhite = contrastRatio(qMax(Lwhite, Lbg), qMin(Lwhite, Lbg));
    const double crBlack = contrastRatio(qMax(Lblack, Lbg), qMin(Lblack, Lbg));

    const bool chooseWhite = (crWhite >= crBlack);
    if (outRatio) {
        *outRatio = chooseWhite ? crWhite : crBlack;
    }

    return chooseWhite ? Qt::white : Qt::black;
}

// qGray: 灰色値 (0..255) をしきい値で二分して黒/白を選択
static QColor textColorByQGray(const QColor &bg, int threshold = 128)
{
    const int gray = qGray(bg.rgb());
    // 背景が暗い(grayが小さい)なら白文字、明るいなら黒文字
    return (gray < threshold) ? Qt::white : Qt::black;
}

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QWidget window;
    auto topLayout = new QVBoxLayout(&window);

    auto table = new QTableWidget;
    topLayout->addWidget(table);
    table->setColumnCount(3);
    table->setHorizontalHeaderLabels({
        u"Color name"_s,
        u"WCAG (black/white)"_s,
        u"qGray() (black/white)"_s
    });

    QStringList names = QColor::colorNames();
    (void)names.removeOne("transparent");
    table->setRowCount(names.size());

    table->verticalHeader()->setVisible(false);
    table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
    table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
    table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);

    table->setAlternatingRowColors(false);
    table->setShowGrid(true);
    table->setWordWrap(false);
    table->setSelectionMode(QAbstractItemView::NoSelection);

    const QString sampleText = u"Sample Text 0123 あいう ABC"_s;

    for (int row = 0; row < names.size(); ++row) {
        const QString &name = names.at(row);
        const QColor bg(name);

        // 0: 名前
        auto nameItem = new QTableWidgetItem(name);
        nameItem->setFlags(Qt::ItemIsEnabled);
        table->setItem(row, 0, nameItem);

        // 1: WCAG
        double ratio = 0.0;
        const QColor fgWcag = textColorByWcag(bg, &ratio);
        auto wcagItem = new QTableWidgetItem(
            u"%1  (CR=%2)"_s
                .arg(sampleText)
                .arg(QString::number(ratio, 'f', 2))
        );
        wcagItem->setTextAlignment(Qt::AlignCenter);
        wcagItem->setFlags(Qt::ItemIsEnabled);
        wcagItem->setBackground(bg);
        wcagItem->setForeground(fgWcag);
        table->setItem(row, 1, wcagItem);

        // 2: qGray
        const QColor fgGray = textColorByQGray(bg);
        auto grayItem = new QTableWidgetItem(
            u"%1  (qGray=%2)"_s
            .arg(sampleText)
            .arg(qGray(bg.rgb()))
        );
        grayItem->setTextAlignment(Qt::AlignCenter);
        grayItem->setFlags(Qt::ItemIsEnabled);
        grayItem->setBackground(bg);
        grayItem->setForeground(fgGray);
        table->setItem(row, 2, grayItem);
    }

    window.setWindowTitle(u"Text color on background: WCAG vs qGray"_s);
    window.resize(780, 720);
    window.show();

    return app.exec();
}

実行結果です。


サンプルコード

colornames.zip

参考資料

QColor Class

適用条件と確認

Qt 6.4以降 / Qt Widgets用です。背景色を不透明なsRGB単色として計算します。半透明色は実際の背景と合成してから評価し、写真・グラデーションは文字の背後の各色を調べます。通常サイズの文字は4.5:1、大きい文字は3:1がWCAG AAの目安です。

黒と白のうちコントラスト比が高い方を選ぶと、この条件では少なくとも約4.58:1になります。7:1など、より高い目標では満たせない背景色もあります。表のCR値と見た目を確認し、qGray列との違いを比較してください。これは色の組み合わせの評価で、アプリ全体のアクセシビリティ適合を保証するものではありません。

参考資料

QMainWindow は statusBar() を呼び出せば QStatusBar を付けられます。QWidget にステータスバーを付けるにはどのようにすれば良いでしょうか。

まず QWidget を親ウィジェットにして QStatusBar のインスタンスを生成します。次に QMainWindow と違ってレイアウトを自分でする必要があります。要点は 2 つです。

  1. QStatusBar を下端に配置する
  2. コンテンツマージンの左右と下を 0 にする

以下がサンプルコードです。ボタンをクリックすると QStatusBar にメッセージが 5 秒間表示されるようにしています。


main.cpp:
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QStatusBar>
#include <QLayout>

class Harness : public QWidget
{
    Q_OBJECT

public:
    Harness() : QWidget(nullptr)
    {
        auto topLayout = new QVBoxLayout(this);
        int topMargin;
        topLayout->getContentsMargins(nullptr, &topMargin, nullptr, nullptr);
        topLayout->setContentsMargins(0, topMargin, 0, 0);
        topLayout->addStretch();

        auto showMessageButton = new QPushButton("Show Message");
        auto buttonLayout = new QHBoxLayout;
        buttonLayout->addStretch();
        buttonLayout->addWidget(showMessageButton);
        buttonLayout->addStretch();
        topLayout->addLayout(buttonLayout);

        topLayout->addStretch();

        statusBar = new QStatusBar(this);
        statusBar->setSizeGripEnabled(true);
        connect(showMessageButton, &QPushButton::clicked,
                [&]() {
                    statusBar->showMessage("Hi there!", 5000);
                });
        topLayout->addWidget(statusBar);
    }

private:
    QStatusBar* statusBar;
};

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    Harness harness;
    harness.show();

    return app.exec();
}

#include "main.moc"

サンプルコード

statusbar.zip
ラベルをクリック可能にするにはどのようにすればよいですか。

QLabel には clicked() シグナルがないため、クリックを検出するサブクラスを作成します。

#include <QApplication>
#include <QLabel>
#include <QMouseEvent>
#include <QVBoxLayout>
#include <QWidget>

class ClickableLabel : public QLabel
{
    Q_OBJECT
public:
    using QLabel::QLabel;

signals:
    void clicked();

protected:
    void mouseReleaseEvent(QMouseEvent *event) override
    {
        QLabel::mouseReleaseEvent(event);
        if (event->button() == Qt::LeftButton
            && rect().contains(event->position().toPoint())) {
            emit clicked();
        }
    }
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QWidget window;
    auto *layout = new QVBoxLayout(&window);
    auto *label = new ClickableLabel("クリックしてください");
    layout->addWidget(label);

    QObject::connect(label, &ClickableLabel::clicked, [] {
        qDebug("label clicked");
    });

    window.show();
    return app.exec();
}

#include "main.moc"

ボタンとして操作させたい場合は、キーボード操作やアクセシビリティも備える QPushButton または QToolButton の使用も検討してください。

QLabel に複数行を表示したときに行間を調整するにはどのようにすればよいでしょうか。

QLabel には行間を直接設定するプロパティーがありません。簡単な表示であれば、リッチテキストの line-height を利用できます。

Qt 6 / Qt Widgets用のmain.cpp全体です。

#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QWidget window;
    auto *layout = new QVBoxLayout(&window);
    for (int percent : {100, 150, 200}) {
        auto *label = new QLabel(&window);
        label->setTextFormat(Qt::RichText);
        label->setText(QString("<p style='line-height:%1%'>%1%: 1行目<br>2行目<br>3行目</p>").arg(percent));
        layout->addWidget(label);
    }
    window.show();
    return app.exec();
}

段落ごとに厳密な書式を制御する場合は、QTextDocument と QTextBlockFormat、または QTextEdit の利用を検討してください。

Qt Quick の Text では lineHeight と lineHeightMode を使用できます。

参考: QTextDocument、Text.lineHeight、QTBUG-33901

確認方法と制約

100%、150%、200%の3つを同じフォントで表示し、行間が変わることを比較します。line-heightはQLabelのスタイルシートではなく、setText()に渡すリッチテキストのp要素に指定します。固定の高さでラベルを切り詰めず、レイアウトが必要な高さを確保できるようにしてください。

QtのリッチテキストはWebブラウザーのHTML/CSS全体を実装していません。使用するQt版の対応表と実際のフォントで確認します。動的な文字列をHTMLへ埋め込む場合はtoHtmlEscaped()でエスケープしてください。

参考資料

QLabel を Qt Designer のフォームに配置しサイズ調整したり、レイアウト機能を使って配置をするとテキストの上下が欠けることがあります。このような場合にコードで sizeHint() にサイズを設定しても欠けたままです。QLabel でテキストを欠けないように表示させるにはどのようにすればよいでしょうか。

まずQLabel自体が小さく切られているのか、特定のフォント・文字だけが欠けるのかを分けて確認します。固定高さ、maximumHeight、レイアウトの制約、スタイルシートのpaddingやborderを確認し、通常はレイアウトにsizeHint()以上の領域を確保させます。

それでも特定の文字だけ欠ける場合は、使用フォントと代替フォント、フォントサイズ、OSの拡大率を固定して再現条件を調べます。ascent()/descent()はフォントの指標であり、すべての文字の描画が必ずその範囲に収まるという保証ではありません。

以下は余白の影響を比較するQt 6 / Qt Widgets用のmain.cpp全体です。実際に欠ける文字列とフォントへ置き換えて使います。

#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QWidget window;
    auto *layout = new QVBoxLayout(&window);
    const QString text = QString::fromUtf8("日本語 Ág あいうえお");
    for (int margin : {0, 8}) {
        auto *label = new QLabel(text);
        label->setTextFormat(Qt::PlainText);
        label->setMargin(margin);
        label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
        layout->addWidget(label);
        qDebug() << "margin:" << margin
                 << "font:" << label->font().family()
                 << "sizeHint:" << label->sizeHint();
    }
    window.show();
    return app.exec();
}

確認と対処

上段は余白0、下段は8論理ピクセルです。下段だけ改善するなら、必要最小限の余白を設定し、レイアウトに任せる方法が候補です。余白8は診断用で、すべてのフォントに共通する推奨値ではありません。

両方とも欠ける場合は、フォントを変えて比較してください。単一行ならQFontMetricsF::boundingRect(text)等も調査材料になりますが、リッチテキスト、折り返し、代替フォントを含む描画を単一の高さ計算で保証することはできません。複雑な書式はQTextDocument等でレイアウトを扱います。

文字やフォント、DPIを変更した後も確認し、setFixedHeight()による固定値の回避策は避けます。再現するQt版・OS・フォント名・文字列・拡大率を揃えると、不具合報告や回避策の適用範囲を明確にできます。

参考資料

QComboBox の setItemData() を使ってフォントや色を設定できます。有効/無効の設定はどのようにすればよいのでしょうか。

Qt の QComboBox のアイテムを有効または無効に設定するには、QStandardItemModel を使用します。QComboBox のアイテムは内部的にモデル(通常は QStandardItemModel)に保存されており、そのモデルを取得して個々のアイテムの状態を操作します。

以下に、具体的な方法を示します。


main.cpp:
#include <QApplication>
#include <QComboBox>
#include <QStandardItemModel>

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QComboBox comboBox;
    comboBox.addItem("Item 1");
    comboBox.addItem("Item 2");
    comboBox.addItem("Item 3");

    // QComboBox のモデルを取得
    QStandardItemModel* const model = qobject_cast<QStandardItemModel*>(comboBox.model());

    if (model) {
        // 2 番目のアイテム(インデックス 1)を無効化
        QStandardItem* const item = model->item(1); // インデックス 1 = "Item 2"
        if (item) {
            item->setEnabled(false);
        }
    }

    comboBox.show();

    return app.exec();
}
  1. モデルの取得
    QComboBox::model() を使用して QStandardItemModel を取得します。 QComboBox は内部的にこのモデルを使用してアイテムを管理しています。
  2. 特定のアイテムを無効化
    モデルからアイテム(QStandardItem)を取得し、 QStandardItem::setEnabled(false) を呼び出します。
  3. 結果
    無効化されたアイテムはグレーアウトされ、選択できなくなります。

参考資料

QStandardItem Class
パレット変更で背景色が反映される理由

setAutoFillBackground(true) を設定したウィジェットでパレットの背景色を変更すると、背景色が自動的に変わるのは、パレットの変更後に再描画が必要になり、その再描画時に QWidgetの描画処理がautoFillBackgroundに従って現在のパレットで背景を塗るためです。QFrameの枠の描画とは区別してください。

このことを確認するためのサンプルコードです。

main.cpp:
#include <QApplication>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QFormLayout>
#include <QLabel>
#include <QSlider>
#include <QPalette>
#include <QEvent>
#include <QPaintEvent>
#include <QPushButton>
#include <QDebug>

// 背景色の変化とイベント配送の流れを観察するための QFrame 派生クラス
class ColorFrame : public QFrame
{
    Q_OBJECT

public:
    explicit ColorFrame(QWidget* parent = nullptr);

    // すべてのイベントがまずここを通る。
    // PaletteChange などのイベント型を確認するために使う。
    bool event(QEvent* event) override;

protected:
    // QWidget 系の「状態変化イベント」を処理する関数。
    // QEvent::PaletteChange は最終的にここにも流れてくる。
    void changeEvent(QEvent* event) override;

    // 実際の再描画処理。
    // autoFillBackground が true の場合は、
    // 基底クラス側の描画処理の中で背景も塗られる。
    void paintEvent(QPaintEvent* event) override;
};

ColorFrame::ColorFrame(QWidget* parent)
    : QFrame(parent)
{
    // 見た目を分かりやすくするため、少し立体的な枠を付ける
    setFrameStyle(QFrame::Panel | QFrame::Raised);
    setLineWidth(2);
}

bool ColorFrame::event(QEvent* event)
{
    // このウィジェットに届いたイベントの型を出力する。
    // たとえば setPalette() のあとには PaletteChange が観察できる。
    qDebug() << "ColorFrame::event" << event->type();

    // 標準のイベント配送を継続する。
    // ここで changeEvent() や paintEvent() などに振り分けられる。
    return QFrame::event(event);
}

void ColorFrame::changeEvent(QEvent* event)
{
    // changeEvent() に到達したことを確認する。
    // パレット変更時には QEvent::PaletteChange がここで観察できる。
    qDebug() << "ColorFrame::changeEvent" << event->type();

    // 既定の changeEvent 処理を実行する。
    QFrame::changeEvent(event);

    // この関数自体で背景を塗るわけではない。
    // 背景の実際の反映は、その後の再描画で行われる。
}

void ColorFrame::paintEvent(QPaintEvent* event)
{
    // 再描画要求が来たことを確認する。
    // setPalette() 直後には、最終的に paintEvent() が呼ばれて見た目が更新される。
    qDebug() << "ColorFrame::paintEvent";

    // QFrame の既定描画を実行する。
    // autoFillBackground が true なら、ここで背景もパレット色で塗られる。
    QFrame::paintEvent(event);
}

// スライダーで RGB を変え、ColorFrame の背景色を更新する確認用画面
class Harness : public QWidget
{
    Q_OBJECT

public:
    Harness();

protected slots:
    // スライダー値から QColor を作り、ColorFrame のパレットへ反映する
    void updateColor();

private:
    ColorFrame* colorFrame{nullptr};
    QSlider* redSlider{nullptr};
    QSlider* greenSlider{nullptr};
    QSlider* blueSlider{nullptr};

    // 手動で PaletteChange イベントを送る確認用ボタン
    QPushButton* postPaletteChangeEvent{nullptr};
};

Harness::Harness() : QWidget{}
{
    auto topLayout{new QVBoxLayout{this}};

    // 色表示用フレーム
    colorFrame = new ColorFrame{};
    colorFrame->setMinimumSize(240, 120);

    // QPalette::Window の色を背景として自動的に塗る設定
    colorFrame->setAutoFillBackground(true);
    topLayout->addWidget(colorFrame);

    auto sliderLayout{new QFormLayout{}};
    sliderLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
    topLayout->addLayout(sliderLayout);

    redSlider = new QSlider{Qt::Horizontal};
    redSlider->setRange(0, 255);
    connect(redSlider, &QSlider::valueChanged, this, &Harness::updateColor);
    sliderLayout->addRow("&Red", redSlider);

    greenSlider = new QSlider{Qt::Horizontal};
    greenSlider->setRange(0, 255);
    connect(greenSlider, &QSlider::valueChanged, this, &Harness::updateColor);
    sliderLayout->addRow("&Green", greenSlider);

    blueSlider = new QSlider{Qt::Horizontal};
    blueSlider->setRange(0, 255);
    connect(blueSlider, &QSlider::valueChanged, this, &Harness::updateColor);
    sliderLayout->addRow("&Blue", blueSlider);

    auto buttonLayout{new QHBoxLayout{}};
    postPaletteChangeEvent = new QPushButton("Send");

    connect(postPaletteChangeEvent, &QPushButton::clicked,
            [this]() {
                qDebug() << "paletteChangeEvent を手動で送る";
                // PaletteChange イベントを手動でポストする。
                auto paletteChangeEvent{new QEvent(QEvent::PaletteChange)};
                QApplication::postEvent(colorFrame, paletteChangeEvent);
            });

    buttonLayout->addWidget(postPaletteChangeEvent);
    buttonLayout->setAlignment(postPaletteChangeEvent, Qt::AlignHCenter);
    topLayout->addLayout(buttonLayout);

    // 初期状態の背景色を設定する
    updateColor();
}

void Harness::updateColor()
{
    qDebug() << "色を更新する";
    // スライダー値から現在色を作る
    const QColor color{redSlider->value(), greenSlider->value(), blueSlider->value()};

    // 現在のパレットを取得し、背景用の Window ロールだけ書き換える
    auto palette = colorFrame->palette();
    palette.setColor(QPalette::Window, color);

    // パレットをウィジェットへ設定する。
    //
    // ここで起きることの要点:
    // 1. ColorFrame のパレットが更新される
    // 2. 必要に応じて PaletteChange 系の通知が行われる
    // 3. 再描画が要求される
    // 4. paintEvent() が呼ばれ、背景色が見た目に反映される
    colorFrame->setPalette(palette);
}

int main(int argc, char** argv)
{
    QApplication app{argc, argv};

    Harness harness{};
    harness.show();

    return app.exec();
}

#include "main.moc"

コードで主に観察したい流れは、次のとおりです。

  • updateColor() で setPalette() を呼ぶ
  • ウィジェットのパレットが変更される
  • ColorFrame::event() に QEvent::PaletteChange が届く
  • ColorFrame::changeEvent() が呼ばれる
  • 再描画が必要になる
  • QEvent::Paint が発生する
  • ColorFrame::paintEvent() が呼ばれる
  • QWidgetによる背景の自動塗りつぶしには変更後のパレットが使われ、QFrameは枠などを描画する

そのため、setAutoFillBackground(true) が有効であれば、アプリ側で背景を明示的に塗らなくても、パレットで指定した背景色が表示に反映されます。

一点だけ重要な補足があります。Send ボタンで QEvent::PaletteChange を手動送信しても、パレットの実データそのものは変わっていません。この場合は、通常の setPalette() のように状態が変わるのではなく、変更通知だけを送っている状態です。

つまり、正確には次のように整理できます。

  • setPalette() は「状態変更 + 通知 + 再描画要求」
  • postEvent(new QEvent(QEvent::PaletteChange)) は「通知イベントだけ」

この違いを意識して見ると、PaletteChange が届くことと、実際に背景色が変わることは同じではない、という点が分かりやすくなります。

適用条件と確認

Qt 6 / Qt Widgets用のmain.cpp全体です。AUTOMOCを有効にしてビルドします。スライダーで色を変更した場合は実際の背景色が変わること、通知だけを送った場合はパレットの値が変わらないことを確認してください。

上記は原因と結果の概略です。描画要求はまとめられる場合があるため、PaletteChangeとPaintのログが常に一対一になるとは限りません。イベントを手動送信する例は観察用で、通常の色変更にはsetPalette()を使います。

autoFillBackgroundは通常backgroundRole()の色を使います。スタイルシートで背景を指定している場合や、ネイティブスタイル・独自paintEvent()が描画する場合は、パレットだけでは意図した結果にならないことがあります。

参考資料

QColorDialog で赤などの色を選んだのに、Windows では透明色として返ることがあります。どう対処すればよいですか。

RGBの色だけでなく、返されたQColorのalphaも確認してください。alphaが0なら、赤のRGB値でも描画結果は透明です。初期値の透明度が色選択後も保持される場合があり、OS名だけから結果を決めつけることはできません。

以下はQt 6 / Qt Widgets用の確認プログラム(main.cpp全体)です。ShowAlphaChannelを指定して透明度も選択できるようにし、キャンセルと有効な透明色を区別します。

#include <QApplication>
#include <QColorDialog>
#include <QDebug>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QColor initial(0, 0, 0, 0);
    if (app.arguments().contains("--opaque-initial"))
        initial.setAlpha(255);
    QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel;
    if (app.arguments().contains("--qt-dialog"))
        options |= QColorDialog::DontUseNativeDialog;
    const QColor selected = QColorDialog::getColor(initial, nullptr, "Fill", options);
    if (!selected.isValid()) {
        qDebug() << "Canceled";
        return 0;
    }
    qDebug() << "Qt:" << qVersion()
             << "initial:" << initial.name(QColor::HexArgb)
             << "selected:" << selected.name(QColor::HexArgb);
    return 0;
}

確認方法

引数なしで透明な初期色から赤を選び、selectedのAARRGGBBを確認します。次に --opaque-initial を指定して不透明な初期色と比較します。さらに --qt-dialog を指定してQt標準ダイアログでも比較し、Qt版・Windows版・使用したオプションを記録してください。ネイティブダイアログが実際に使用されるかも、オプションとプラットフォームによります。

アプリケーションへの適用

「塗りつぶし色は最初は不透明として選ぶ」という仕様なら、ダイアログへ渡すコピーだけalphaを255に補正する方法があります。ただし、透明色をそのまま編集する仕様なら、補正は利用者の意図を変えるため行いません。返された色を無条件に不透明にするのも避けます。

キャンセル時はisValid()がfalseなので、既存の色を更新しません。OS依存の問題として扱う場合は、上の比較で再現したQt/OSの組み合わせを適用条件にしてください。

参考資料

QML/Qt Quick

重ね合わせる方法以外に Rectangle のボーダー幅を 1 ピクセルより細くする方法はありますか。

Qt 6ではRectangleのborder.pixelAlignedをfalseにすると、border.widthへ小数の幅を指定できます。このプロパティーは現在の公式リファレンスに記載されています。

ここで幅の単位は論理ピクセルです。たとえばdevicePixelRatioが2なら、1 / devicePixelRatioは0.5論理ピクセル、つまり1物理ピクセルに相当します。「1ピクセルより細い」は論理ピクセルを指し、物理ピクセルより細い線を保証するものではありません。


Rectangle {
    id: withOverlap

    width: 100
    height: 100

    color: "black"

    Rectangle {
        x: 1 / Screen.devicePixelRatio
        y: x

        width: withOverlap.width - (2 / Screen.devicePixelRatio)
        height: width

        color: "red"
    }
}

重ね合わせによって、1論理ピクセルより細いボーダーを付ける方法です。


Rectangle {
    id: withPixelAligned

    width: 100
    height: 100

    color: "red"
    border.width: 1 / Screen.devicePixelRatio
    border.pixelAligned: false
}

border.pixelAligned を false にすることで 1論理ピクセルより細いボーダーを付ける方法です。


サンプルコード

thinborderrectangle.zip

参考資料

Rectangle QML Type
Screen QML Type

適用条件と確認

上の二つはQt 6のQML部品例です。Qt QuickプロジェクトのWindow内へ配置し、import QtQuickとimport QtQuick.Windowを指定してください。devicePixelRatioが1と2の画面で比較し、画面間の移動や拡大率変更時も確認します。小数幅で滑らかに描画したい場合はantialiasing: trueも検討します。

上段は別のRectangleを重ねる方法、下段は枠線だけで実現する方法です。新しいコードには下段を推奨します。旧版でのドキュメント表記と実装の違いを前提にせず、対象Qt版の公式リファレンスを確認してください。

参考資料

QML のコードで Qt のバージョンを判定するにはどのようにすればよいですか。

QML には Qt のバージョンを取得する機能が用意されていません。C++ のマクロ QT_VERSION_STR や QT_VERSION、関数 qVersion() のいずれかを使って、その値を QML 側で参照できるようにします。

C++側(QMLを読み込む前の設定部分):

QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("QT_VERSION_STRING", QT_VERSION_STR);

QT_VERSION_STR は文字列で "5.15.2" や "6.3.0" という値です。次のQML例では、バージョンの表示と数値による判定を一つにまとめています。

import QtQuick

Text {
    text: QT_VERSION_STRING

    function qtVersionNumberFromString(versionString) {
        let [major, minor, bugfix] = versionString.split(".");
        return Number(major.padStart(2, "0") + minor.padStart(2, "0") + bugfix.padStart(2, "0"));
    }

    Component.onCompleted: {
        if (qtVersionNumberFromString(QT_VERSION_STRING) < 60000) {
            // Qt 6 より前のバージョンで行う処理
        } else {
            // Qt 6 以降で行う処理
        }
    }
}

コードの説明


数値で判定するために、バージョン文字列を数値に変換する JavaScript 関数を定義しています。


この関数は Qt のバージョン文字列を変換して 10 進数で 51502 や 60300 という値にします。上の例では、Component.onCompleted でこの値を判定しています。


参考資料

QQmlContext::setContextProperty ドキュメント
Qt Quick Controls の Button の長押し時間を変更するにはどのようにすればよいですか。

Qt Quick Controls の Button のデフォルトの長押し時間は 800 ミリ秒です。長押し時間を変更するには QStyleHints の mousePressAndHoldInterval プロパティーの設定値を変更します。

以下がサンプルコードです。ボタンを押したままにすると 1.5 秒で Button の onPressAndHold ハンドラーが呼び出されます。


main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QStyleHints>

int main(int argc, char** argv)
{
    QGuiApplication app(argc, argv);

    // 長押し時間 (ミリ秒) を設定
    const int pressAndHoldInterval = 1500; // 1.5秒
    app.styleHints()->setMousePressAndHoldInterval(pressAndHoldInterval);
Rectangle で円形図形を作り MouseArea を適用すると円の外側でも反応してしまいます。円以外にも図形の内側だけで反応するようするにはどのようにすればよいでしょうか。

Item.containmentMask を使います。MouseArea の大きさは矩形のままにし、マスクオブジェクトの contains(point) が true を返す位置だけでマウスやタッチへ反応させます。

円形なら、Qt 6ではC++の補助クラスを作らずQMLだけで実装できます。


main.qml:
import QtQuick
import QtQuick.Window

Window {
    id: window
    visible: true
    width: 300
    height: 300

    Rectangle {
        id: circle
        width: 160
        height: width
        anchors.centerIn: parent
        radius: width / 2
        color: "red"

        MouseArea {
            id: circleMouseArea
            anchors.fill: parent
            hoverEnabled: true

            containmentMask: QtObject {
                function contains(point: point): bool {
                    const dx = point.x - circle.width / 2
                    const dy = point.y - circle.height / 2
                    return dx * dx + dy * dy
                           <= circle.radius * circle.radius
                }
            }

            onPressed: circle.color = "green"
            onReleased: circle.color = "red"
        }
    }
}

contains() に渡される point は、マスク対象アイテムのローカル座標です。この例では円の中心からの距離を計算し、半径以内の場合だけ true を返しています。

より複雑な図形では、Shape に containsMode: Shape.FillContains を設定し、その Shape を containmentMask に指定できます。独自のC++オブジェクトをマスクにする場合は、Q_INVOKABLE bool contains(const QPointF &point) const に相当するメソッドを公開してください。

containmentMask はQt 5.11で追加され、Qt 6でも利用できます。マスクの判定は入力イベントごとに呼ばれるため、contains() では重い処理を避けてください。

参考資料

Item::containmentMask

Shape::containsMode

QML でプロパティーや関数をプライベートにするにはどのようにすればよいでしょうか。

QML は、プロパティーや関数を C++ のようにプライベートにできません。完全ではありませんが部分的にプライベート化する方法が用いられています。QtObject を配置し、その中にプロパティーや関数を閉じ込めます。サンプルコードは以下のようになります。


Circle.qml:
import QtQuick

Rectangle {
    id: root

    property real diameter: d.defaultDiameter

    QtObject {
        id: d

        readonly property real defaultDiameter: 50.0
        readonly property color defaultColor: "red"

        function center() {
            return Qt.point(root.x + root.width / 2, root.y + root.height / 2);
        }

        function setCenter(point: point) {
            const pointInParent = root.mapToItem(root.parent, point.x, point.y);
            root.x = pointInParent.x - root.radius;
            root.y = pointInParent.y - root.radius;
        }
    }

    width: diameter
    height: diameter
    radius: diameter / 2
    color: d.defaultColor
    border {
        width: 0.0;
        color: "grey";
    }

    MouseArea {
        id: mouseAreaOfRoot

        anchors.fill: parent

        onPressed: (mouse) => {
            d.setCenter(Qt.point(mouse.x, mouse.y));
            root.border.width = 1.0;
        }

        onPositionChanged: (mouse) => {
            d.setCenter(Qt.point(mouse.x, mouse.y));
        }

        onReleased: (_) => {
            root.border.width = 0.0;
            print(d.center());
        }
    }
}

QtObject 内に配置したプロパティーや関数は、Circle タイプを使う QML ファイルからは直接参照できなくなります。ただし、Circle 内に配置したタイプからは d を参照してアクセスできることに注意が必要です。


main.qml:
import QtQuick
import QtQuick.Window

Window {
    visible: true
    width: 400
    height: 400

    signal clicked()

    Circle {
        x: 100
        y: 100
        diameter: 100
    }

    Circle {
        x: 180
        y: 225
        color: "green"
    }

    Circle {
        x: 250
        y: 160
        diameter: 30
        color: "blue"
    }

}

Circle を使うサンプルコードです。


サンプルコード

pseudo-private.zip

適用条件と確認

Qt 6用です。Circle.qmlとmain.qmlを同じディレクトリーに置き、Qt Quickプロジェクトからmain.qmlを読み込みます。三つの円が表示され、各円をドラッグできることを確認してください。Qtのqml実行ツールがある環境なら、そのディレクトリーでqml main.qmlでも確認できます。

dはCircle.qml内部のidなので、利用側へ公開したプロパティーではありません。ただし、これはC++のprivateと同等のアクセス制御や、機密情報を隠す仕組みではありません。QtObject自体を外部へ返すプロパティーなどを追加すると内部へアクセスできます。外部から変更してよいものだけをrootのプロパティーや関数として公開します。

同名のプロパティーによる曖昧さを避けるため、内部関数から座標や親を参照する箇所はrootを明示しています。上記掲載コードはQt 6向けに整えた例です。ダウンロード版は従来の参考サンプルです。

画像ファイルを読み込み、画像を加工後にファイルに保存するにはどのようにすればよいですか。

Qt QuickではItem.grabToImage()でアイテムと子アイテムを画像化し、コールバックでsaveToFile()を呼びます。画像の読み込みと画面の表示が終わってから実行してください。

Qt 6用のmain.qml全体です。同じディレクトリーにinput.pngを置き、Qt Quick Controlsを利用できるプロジェクトで読み込みます。枠を重ねた画像を表示し、ボタンで保存します。

import QtQuick
import QtQuick.Window
import QtQuick.Controls

Window {
    id: window
    width: 640
    height: 480
    visible: true
    property bool saving: false

    Image {
        id: image
        anchors.centerIn: parent
        source: "input.png"
        asynchronous: true
        width: Math.min(400, implicitWidth)
        height: implicitWidth > 0 ? implicitHeight * width / implicitWidth : 0
        onStatusChanged: {
            if (status === Image.Error)
                console.error("input.pngを読み込めませんでした")
        }
        Rectangle {
            anchors.fill: parent
            color: "transparent"
            border.color: "gray"
            border.width: 8
        }
    }
    Button {
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        text: window.saving ? "保存中" : "画像を保存"
        enabled: image.status === Image.Ready && image.width > 0
                 && image.height > 0 && !window.saving
        onClicked: {
            window.saving = true
            const accepted = image.grabToImage(function(result) {
                const saved = result.saveToFile("output.png")
                console.log(saved ? "保存しました: output.png" : "保存に失敗しました")
                window.saving = false
            })
            if (!accepted) {
                console.error("画像取得を開始できませんでした")
                window.saving = false
            }
        }
    }
}

コードの説明と確認

Image.Readyになるまで保存ボタンを無効にしています。画像取得の開始失敗はgrabToImage()の返り値、ファイルへの保存失敗はsaveToFile()の返り値で区別します。保存中の連続操作も抑止しています。

保存ボタンを押して、プロセスの作業ディレクトリーにoutput.pngができ、枠も含まれていることを確認します。保存先は書き込み可能な場所にしてください。実アプリでは保存先選択と絶対パスの指定を用意します。

これは表示内容のキャプチャです。表示時の縮小、描画サイズ、画面の倍率などを考慮する必要があり、原画像の画素をそのまま加工する処理とは異なります。原寸の画像処理が目的ならC++側でQImageとQPainterを使う方法も検討してください。

参考資料

Rectangle の任意の四隅を丸める方法はありますか。

Qt 6.7以降なら、RectangleのtopLeftRadius、topRightRadius、bottomLeftRadius、bottomRightRadiusで四隅を個別に指定できます。0を指定した角は丸くなりません。

Qt 6.7以降用のmain.qml全体です。Qt Quickプロジェクトから読み込むか、qml実行ツールがある環境でqml main.qmlを実行します。

import QtQuick
import QtQuick.Window

Window {
    visible: true
    width: 320
    height: 180
    color: "white"
    Rectangle {
        anchors.centerIn: parent
        width: 240
        height: 100
        color: "steelblue"
        topLeftRadius: 24
        topRightRadius: 0
        bottomLeftRadius: 0
        bottomRightRadius: 12
    }
}

確認方法

左上が半径24、右下が半径12で丸まり、右上と左下が直角であることを確認します。単位は論理ピクセルです。個別指定しない角にはradiusの値が使われます。

Qt 6.6以前ではこの四つのプロパティーは使えません。古いQtも対応対象なら、ShapeやCanvasで輪郭を描くか、背景と同色の部品を重ねる方法を検討します。重ねる方法は単色背景を前提にしやすく、透明背景・枠線・影に制約があるため、見た目だけでなく合成結果を確認してください。

参考資料

QML で固定幅フォントを使うにはどのようにすればよいですか。

いくつか方法がありますが QFontDatabase::systemFont() で QFontDatabase::FixedFont を取得するのが確実です。

このサンプルは、Qt (C++ と QML) でシステム標準の等幅フォント (固定幅フォント) を QML 側に渡して表示する最小構成のアプリです。 C++ 側で QFontDatabase::systemFont(QFontDatabase::FixedFont) を取得し、そのフォントファミリー名を systemFixedMonoFamily として QQmlContext に公開します。 QML 側では ApplicationWindow の font.family にそれを指定し、実際に使われているフォント名をラベルに表示します。


main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QFontDatabase>
#include <QQmlContext>
#include <QUrl>

int main(int argc, char** argv)
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    const QFont fixed = QFontDatabase::systemFont(QFontDatabase::FixedFont);
    engine.rootContext()->setContextProperty("systemFixedMonoFamily", fixed.family());

    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(
        &engine, &QQmlApplicationEngine::objectCreated,
        &app,
        [url](QObject* obj, const QUrl& objUrl) {
            if (!obj && objUrl == url)
                QCoreApplication::exit(-1);
        },
        Qt::QueuedConnection);

    engine.load(url);

    return app.exec();
}

main.qml:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
    id: root
    visible: true
    minimumWidth: topLayout.implicitWidth
    minimumHeight: topLayout.implicitHeight
    font.family: systemFixedMonoFamily
    ColumnLayout {
        id: topLayout
        spacing: 12
        Label {
            text: "monospace = " + systemFixedMonoFamily
            font.pointSize: 24
        }
        Button {
            text: "Quit"
            onClicked: {
                Qt.quit();
            }
        }
    }
}

Qt for Python

Qt for Python の Examples のサンプルコード QAbstractListModel in QML の model.py の reset() で resetInternalData() を呼び出している部分に「呼び出さなくても動作するはずですか?」とコメントされている理由を教えてください。

質問中のサンプルでは、endResetModel()から呼ばれるresetInternalData()を、その直前にも直接呼んでいます。PySide6 6.11.2で呼び出し回数を確認すると、直接呼び出した場合は2回、直接呼び出さない場合は1回でした。

以下は質問で取り上げられたmodel.pyの抜粋です。公式サンプルは版によって更新されるため、手元のコードと比較してください。


model.py:
@Slot(result=bool)
def reset(self):
    self.beginResetModel()
    self.resetInternalData()  # should work without calling it ?
    self.endResetModel()
    return True

def resetInternalData(self):
    self.db = [
        {"id": 3, "bgColor": QColor("red"), "ratio": 0.15, "text": "first"},
        {"id": 1, "bgColor": QColor("blue"), "ratio": 0.1, "text": "second"},
        {"id": 2, "bgColor": QColor("green"), "ratio": 0.2, "text": "third"},
    ]

resetInternalData() は仮想関数で、endResetModel() から呼び出されるように実装されています。従って、reset() で resetInternalData() を呼び出すべきではありません。resetInternalData(self) にデバッグ文を入れると、リセット時にこの関数が 2 度呼び出されているのを確認できます。このサンプルのようにresetInternalData()で初期データを作り直す設計なら、reset()は以下の形にできます。

@Slot(result=bool)
def reset(self):
    self.beginResetModel()
    self.endResetModel()
    return True

確認方法

resetInternalData()の先頭にprint("resetInternalData")を入れ、reset()を1回実行します。修正前は2行、修正後は1行になることと、モデルの行数・表示内容が期待どおりに戻ることを確認してください。

一般のモデルでは、beginResetModel()とendResetModel()の間で実データを変更し、resetInternalData()は派生クラスの内部キャッシュなどを片付けるために使う設計もあります。begin/endを呼ぶだけで任意のデータが自動初期化されるわけではありません。データ更新と内部リセットの責任を分け、二重に更新しないようにします。

参考資料

PySide6 で、実行中にウィンドウを開くとすぐに閉じてしまいます。開いたままにするにはどのようにすればよいですか。
 

C++でもローカル変数として作ったウィンドウはスコープを抜けると破棄されます。PySide6では、Pythonの参照とQt側の所有権の両方を確認する必要があります。しかし、PySide6 の場合には、ウィンドウオブジェクトを強参照しないと、Python のガーベージコレクションによってメモリー解放されるために、ウィンドウがすぐに閉じてしまいます。ウィンドウを開いたままにするには、強参照してウィンドウオブジェクトを管理する必要があります。

ウィンドウオブジェクトをリストに入れて強参照を維持することにします。この場合に、ウィンドウオブジェクトの破壊時にリストから取り除く必要があります。Qt/C++ であれば、リストにベースクラスの QObject インスタンスを入れ、destroyed() シグナルに渡されてくる QObject インスタンスで、どれをリストから取り除くかを判断できます。しかし、PySide6 の場合には destroyed() シグナルで渡されてくるのは Shiboken によりラップされた QWidget に相当する Python オブジェクトになるため、シグナル引数のPythonラッパーの同一性や、破棄中のオブジェクトの操作に依存しない管理方法が必要です。PySide6 での 2 通りの対処方法を説明します。


clean_up_by_object.py:
import sys

from PySide6.QtCore import Qt, Slot
from PySide6.QtWidgets import (QApplication, QWidget, QPushButton, QLabel,
                               QVBoxLayout)


class Window(QWidget):
    _count = 0

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        topLayout = QVBoxLayout(self)

        Window._count += 1
        label = QLabel(str(Window._count))
        topLayout.addWidget(label, alignment=Qt.AlignHCenter)

        self.resize(200, 200)


class Harness(QWidget):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self._windows: list[Window] = []

        topLayout = QVBoxLayout(self)

        createButton = QPushButton("Create", clicked=self._createWindow)
        topLayout.addWidget(createButton, alignment=Qt.AlignHCenter)

    @Slot()
    def _createWindow(self):
        window = Window()
        self._windows.append(window)
        window.setAttribute(Qt.WA_DeleteOnClose)

        window.destroyed.connect(lambda: self._cleanUp(window))

        window.show()

    def _cleanUp(self, window):
        if window in self._windows:
            self._windows.remove(window)


def main() -> None:
    app = QApplication(sys.argv)

    harness = Harness()
    harness.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

ラムダ関数でウィンドウオブジェクトを抱えて、destroyed() シグナルで呼び出して判定する方法です。_cleanUp() に渡されてくるオブジェクトは、すでに破壊されている場合があるため、print() での表示などはできないことがあるので、操作に注意が必要です。


clean_up_by_shiboken.py:
import sys

from PySide6.QtCore import Qt, Slot
from PySide6.QtWidgets import (QApplication, QWidget, QPushButton, QLabel,
                               QVBoxLayout)
import shiboken6


class Window(QWidget):
    _count = 0

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        topLayout = QVBoxLayout(self)

        Window._count += 1
        label = QLabel(str(Window._count))
        topLayout.addWidget(label, alignment=Qt.AlignHCenter)

        self.resize(200, 200)


class Harness(QWidget):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self._windows: list[Window] = []

        topLayout = QVBoxLayout(self)

        createButton = QPushButton("Create", clicked=self._createWindow)
        topLayout.addWidget(createButton, alignment=Qt.AlignHCenter)

    @Slot()
    def _createWindow(self):
        window = Window()
        self._windows.append(window)
        window.setAttribute(Qt.WA_DeleteOnClose)
        window.destroyed.connect(self._cleanUp)
        window.show()

    @Slot(object)
    def _cleanUp(self, _) -> None:
        self._windows = [window for window in self._windows
                                  if shiboken6.isValid(window)]


def main() -> None:
    app = QApplication(sys.argv)

    harness = Harness()
    harness.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

_cleanUp() を呼び出し、リスト中の無効オブジェクトを shiboken6.isValid() で判定して、リストから取り除くようにする方法です。


適用条件と確認

PySide6用の二つの独立した完成例です。各コードを別々の.pyファイルとして保存し、PySide6を導入したPythonで実行します。Createを3回押して三つのウィンドウが残ること、閉じたウィンドウがリストから除かれることを確認してください。確認用ログにはlen(self._windows)を出し、破棄済みラッパー自体をprintしないようにします。

WA_DeleteOnCloseを設定しているため、閉じるとQt側のオブジェクトが削除されます。設定しない場合は通常は非表示になるだけで、destroyedはその時点では通知されません。閉じた後に再利用する設計なのか、破棄する設計なのかを先に決めてください。

親のないウィンドウをリストで保持する例です。親を持つQObjectなら親子関係で寿命を管理できる場合があります。一方、Pythonの強参照があっても、Qt側で削除されたオブジェクトを再利用することはできません。アプリ終了時には、管理中のウィンドウの閉じ方と管理者の寿命も揃えます。

参考資料

PySide6 で QVariant プロパティを公開し、QML の var 型として扱うにはどのようにすればよいですか。

PySide6 で QObject のプロパティーとして QVariant を公開し、QML 側で var として読み書きする手順です。Holder.valueで数値・文字列・QVariantMap/List・null/undefined を操作し、シグナル通知・スロット呼び出し・メタ型名の確認までします。ボタンで値を設定し、ログとラベルで可視化します。Python ↔ QML 往復、同値時の通知抑制、undefined が None になる点も確認できます。

以降がサンプルコードです。


main.py:
from typing import Any

import sys
from pathlib import Path

from PySide6.QtCore import QObject, Property, Signal, Slot, QUrl
from PySide6.QtGui import QGuiApplication, QFontDatabase
from PySide6.QtQml import QQmlApplicationEngine


class Holder(QObject):
    # QML 側で onValueChanged(value) で受け取る
    valueChanged = Signal('QVariant', arguments=['value'])

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 任意の値 (Python 側では object, None) を保持
        self._value: object | None = None

    def getValue(self) -> object | None:
        """QML 側から読む getter。QVariant として見える。"""
        return self._value

    def setValue(self, newValue: object) -> None:
        """QML 側から書く setter。同値ならシグナル抑制。"""
        if self._value == newValue:
            return
        self._value = newValue
        self.valueChanged.emit(newValue)

    # QVariant として公開 (QMLでは var として見える)
    value = Property('QVariant', fget=getValue, fset=setValue, notify=valueChanged)

    # 例として使うスロット (QMLから呼びやすいように用意)
    @Slot(int)
    def setInt(self, n: int) -> None:
        self.value = n

    @Slot('QVariant')
    def setVariant(self, value) -> None:
        """QML の undefined は None として届くことが多い。"""
        self.value = value

    @Slot(result='QVariant')
    def getVariant(self) -> Any:
        return self.value


def propertyTypeName(qobj: QObject | None, prop: str) -> str:
    """qobj のプロパティ prop のメタ型名を返す。見つからなければ '(not found)'。"""
    if qobj is None:
        return "(null)"
    metaObject = qobj.metaObject()
    index = metaObject.indexOfProperty(prop)
    if index < 0:
        return "(not found)"
    metaObjectProperty = metaObject.property(index)
    typeName = metaObjectProperty.typeName()
    return (typeName if typeName else "(unknown)")


def main():
    app = QGuiApplication(sys.argv)

    engine = QQmlApplicationEngine()
    holder = Holder(engine)

    # holder.value の QML プロパティー型を確認
    print(f"{propertyTypeName(holder, 'value')=}")

    # QML から "holder" という名前でアクセスできるようにする
    engine.rootContext().setContextProperty("holder", holder)

    # 等幅フォント名を渡してログ表示を見やすく
    fixed = QFontDatabase.systemFont(QFontDatabase.FixedFont)
    engine.rootContext().setContextProperty("systemFixedMonoFamily", fixed.family())

    qml_url = QUrl.fromLocalFile(str(Path(__file__).resolve().parent / "main.qml"))

    engine.objectCreated.connect(
        lambda obj, url: sys.exit(1) if obj is None and url == qml_url else None
    )
    engine.load(qml_url)

    if not engine.rootObjects():
        sys.exit(1)

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

main.qml:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    id: root

    function log(msg) {
        logView.text += (logView.text.length ? "\n" : "") + msg
        console.log(msg)
    }

    function pretty(value) {
        if (value === null) {
            return "null";
        }
        if (value === undefined) {
            return "undefined";
        }
        if (Qt.isQtObject(value)) {
            return "[QObject]";
        }
        if (Array.isArray(value)) {
            try {
                return JSON.stringify(value);
            }
            catch (error) {
                return "[array]";
            }
        }
        if (typeof value === "object") { // プレーンなオブジェクト想定 (QVariantMapなど)
            try {
                return JSON.stringify(value);
            }
            catch (error) {
                return "[object]";
            }
        }
        return String(value); // number/string/boolean など
    }

    visible: true
    height: 420
    title: "QVariant ⇔ QML var デモ"

    ColumnLayout {
        id: topLayout

        anchors.fill: parent
        anchors.margins: 12
        spacing: 8

        // ログ表示
        TextArea {
            id: logView

            readOnly: true

            Layout.fillWidth: true
            Layout.fillHeight: true

            wrapMode: TextArea.Wrap
            font.family: systemFixedMonoFamily
        }

        // 現在の holder.value を常に可視化 (var は何でも入る)
        Label {
            id: valueLabel

            Layout.fillWidth: true

            text: "現在の holder.value: " + root.pretty(holder.value)
        }

        RowLayout {
            id: manipulationButtonLayout

            Layout.fillWidth: true
            spacing: 8

            Button {
                id: setQVariantMapButton

                text: "QVariantMap を設定"
                onClicked: {
                    // JS オブジェクト → QVariantMap
                    holder.value = { name: "Ken", age: 12, nested: { p: 42 } }
                }
            }

            Button {
                id: setQVariantListButton

                text: "QVariantList を設定"
                onClicked: {
                    // JS 配列 → QVariantList
                    holder.value = [10, 20, 30]
                }
            }

            Button {
                id: setIntButton

                text: "int を設定 (スロット)"
                onClicked: holder.setInt(99)
            }

            Button {
                id: logGetVariantButton

                text: "getVariant() をログ"
                onClicked: {
                    const value = holder.getVariant()
                    log("getVariant(): " + root.pretty(value))
                }
            }

            // 追加: null / undefined / 空コンテナの確認ボタン
            Button {
                text: "null を設定"
                onClicked: holder.setVariant(null)
            }
            Button {
                text: "undefined を設定"
                onClicked: holder.setVariant(undefined)
            }
            Button {
                text: "空 Map {}"
                onClicked: holder.value = {}
            }
            Button {
                text: "空 List []"
                onClicked: holder.value = []
            }
        }
    }

    Component.onCompleted: {
        // ウィンドウ幅をすべてのボタンが表示されるように調整
        Qt.callLater(() => {
            root.width = topLayout.implicitWidth
                         + topLayout.anchors.leftMargin
                         + topLayout.anchors.rightMargin;
        });

        // 起動時にいくつか試す
        print(typeof(holder.value));

        // 1) オブジェクト (= QVariantMap)
        holder.value = { a: 1, b: "x", nested: { p: 42 } }
        log("set object -> holder.value.a = " + holder.value.a +
            ", nested.p = " + holder.value.nested.p)

        // 2) 配列 (= QVariantList)
        holder.value = [1, 2, 3]
        log("set array  -> length = " + holder.value.length +
            ", [0] = " + holder.value[0])

        // 3) 数値 (= QVariant(double/int))
        holder.value = 3.14
        log("set number -> " + holder.value)

        // 4) 混在配列 (= QVariantList)
        holder.value = ["alpha", true, 5]
        log("set mixed array -> " + JSON.stringify(holder.value))
    }

    // Python 側の valueChanged('QVariant') に反応してログ
    Connections {
        target: holder
        function onValueChanged(value) {
            log("signal valueChanged: " + root.pretty(value));
        }
    }
}
macOS 上で Qt Creator から PySide6 のプログラムを実行し、 QLocale.system().bcp47Name() を呼び出すと en-POSIX が返され、日本語環境かどう かを判別できません。

まず、macOS の GUI アプリケーションは launchd から直接起動され、シェルを通らないため LANG が設定されません。Qt Creator も GUI アプリケーションとして起動されるため、その子プロセスである実行プログラムも LANG が未設定のまま動作します。

次に、PySide6 の QLocale.system() は Qt/C++ の QLocale::system() とは実装が異なり、PySide6 のドキュメントには次のように記述されています。

PySide6.QtCore.QLocale - Qt for Python

macOS 上の Qt for Python は、Qt/C++ の QLocale::system() を通じてユーザーの地域と言語の設定を反映しません。その代わり、Python の locale モジュールと同様に、POSIX がロケール指定に用いる環境変数を参照します。もしシステムロケールを判定できない場合、つまり環境内で LC_ALL、LC_CTYPE、LANG、LANGUAGE のいずれも設定されていない場合には、POSIX のデフォルトロケール、すなわち C ロケールが返されます。

Qt 6.9 以降では、この C ロケールは BCP47 で en-POSIX という名称で返されます。

What's New in Qt 6.9

QLocale::bcp47Name() は、C のロケールを "en-POSIX "として表すようになりました。これは、単なる "en "と区別し、BCP47 仕様との整合性を高めるためです。また、POSIX というバリアントも、その名前から C ロケールを構築する際に、C ロケールを意味するものとして認識されるようになりました。

つまり PySide6 では、LC_ALL/LC_CTYPE/LANG/LANGUAGE のいずれかが設定されていれば、それに対応するロケールが使われ、どれも設定されていなければ C ロケールとなり、bcp47Name() が en-POSIX を返すという挙動になります。

Qt Creator での回避策です。1 の方法を推奨します。

  1. 実行時の設定」で LANG を追加し ja_JP.UTF-8 を設定する
  2. Qt Creator の実行ファイルを直接ターミナルから起動する
  3. コード側でロケールを固定する
PySide6 で作成したウィンドウやタイマーがすぐに消えたり、動作しなくなったりすることがあります。Python の GC と Qt の親子関係を踏まえて、オブジェクトの寿命をどのように管理すればよいですか。

PySide6 の QObject 派生オブジェクトには、Python のラッパーと C++ の Qt オブジェクトという二つの側面があります。Python の参照と Qt の親子関係の両方を考えて寿命を管理してください。

親のないトップレベルウィンドウをローカル変数だけに保存すると、関数終了後に強参照がなくなり、すぐ閉じることがあります。必要な期間は属性で保持します。

class MainWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._detail_window = None

    @Slot()
    def open_window(self):
        if self._detail_window is None:
            self._detail_window = QWidget()
            self._detail_window.setAttribute(
                Qt.WidgetAttribute.WA_DeleteOnClose)
            self._detail_window.destroyed.connect(
                lambda: setattr(self, "_detail_window", None))
        self._detail_window.show()

メイン画面と同じ寿命でよい QObject には親を設定します。例えば timer = QTimer(self) とすると、タイマーは self の子になり、ローカル変数がスコープを抜けても動作を続けます。

  • 独立したトップレベルウィンドウは属性やリストで強参照を保つ
  • 同じ寿命でよい QObject は親を設定する
  • 所有権を取得しない API に渡すオブジェクトは Python 側でも保持する

親が破棄されると子の C++ オブジェクトも破棄されます。Python 側に参照が残っていても、その無効なラッパーを使用しないでください。

参考: Object ownership - Shiboken、Object Trees & Ownership

PySide6 で重い処理を QtConcurrent や QThreadPool のスレッドに移せば、CPU コアを複数使って処理を速くできますか。GIL との関係と、処理方法の選び方を教えてください。

通常の GIL が有効な CPython では、複数のスレッドがあっても、同時に Python バイトコードを実行できるスレッドは原則として 1 つです。QtConcurrent が関数を別のネイティブスレッドで実行しても、関数の大部分が純粋な Python の計算なら、GIL の制約はなくなりません。

ただし、C++ の Qt Concurrent API と PySide6 で公開されている API は同じではありません。PySide6 6.11.0 では、QtConcurrent.run() と任意の結果型を扱う汎用の QFutureWatcher は Python API に公開されていません。C++ 用の QtConcurrent の例を、そのまま PySide6 のコードへ置き換えられるとは限らないため、使用する PySide6 の API リファレンスと、実際に公開されているクラスや関数を確認してください。

たとえば、次のように Python のループだけで大量の整数計算を行う関数を複数のスレッドへ振り分けても、CPU コア数に比例した高速化は通常期待できません。スレッドの切り替えなどの負担により、かえって遅くなる場合もあります。

例:
def calculate(start, end):
    total = 0
    for value in range(start, end):
        total += value * value
    return total

ただし、「処理が速くなるか」と「GUI が固まらないか」は別の問題です。このような処理を GUI スレッドで直接実行すると、イベント処理が止まり、ウィンドウの再描画や入力への応答ができなくなります。ワーカースレッドへ移せば、計算全体が短くならなくても GUI の応答性を保てることがあります。

スレッドが効果を発揮しやすいのは、次のような処理です。

  • ファイル、ネットワーク、データベースなどの I/O 待ちが多い処理
  • GIL を解放して実行する C/C++ 拡張やライブラリを呼び出す処理
  • GIL を解放する Qt API やネイティブライブラリで時間のかかる処理を行う場合
  • GUI スレッドから待ち時間の長い処理を分離したい場合

一方、純粋な Python で書いた CPU バウンド処理を複数コアで並列化したい場合は、multiprocessing や concurrent.futures.ProcessPoolExecutor などを使い、処理を別プロセスへ分ける方法を検討します。各プロセスは別の Python インタープリターを持つため、通常の GIL の制約をプロセス間で共有しません。ただし、プロセス間で渡すデータのシリアライズ、起動コスト、終了処理が必要になります。

また、ワーカーの完了を待つために GUI スレッドで wait() や result() などのブロッキング処理を呼ぶと、その間は GUI が停止します。PySide6 では、QThread の worker QObject が送るシグナル、または QThreadPool で実行する QRunnable とは別に用意した QObject のシグナルなどを使い、結果を非同期に受け取ります。画面の更新は GUI スレッドで行い、ワーカースレッドから QWidget を直接操作してはいけません。

処理方法は、次の基準で選ぶと分かりやすくなります。

  • GUI の応答性を保ちたい: QThreadPool と QRunnable、または QThread など
  • I/O 待ちを重ねて進めたい: スレッドまたは非同期 I/O
  • 純粋な Python の CPU 計算を並列化したい: 複数プロセス
  • GIL を解放するネイティブ処理を並列化したい: スレッドも候補

Python 3.13 以降には GIL を無効化できる free-threaded build もありますが、標準のビルドではありません。使用する PySide6 や拡張モジュールを含めた対応状況の確認が必要です。通常の GIL 有効 CPython を使う場合は、QtConcurrent に渡したという理由だけで純粋な Python コードが並列実行されるとは考えず、処理の性質に合わせてスレッドとプロセスを選んでください。

参考資料

threading — Thread-based parallelism - Python

Initialization, Finalization, and Threads - Python/C API

QtConcurrent - Qt for Python

Multithreading Technologies in Qt - Qt for Python

QThreadPool - Qt for Python

PySide6 でシグナルに接続したはずのスロットが呼ばれません。ローカル変数で作った受信オブジェクトや lambda、関数の参照は、どのように保持すればよいですか。

まず、接続先メソッドの所有者である受信オブジェクトが生きているか確認してください。connect() は接続を設定しますが、受信オブジェクトの寿命を必ず延ばすものではありません。

class Controller:
    def __init__(self, sender):
        self.receiver = Receiver()
        sender.fired.connect(self.receiver.receive)

sender = Sender()
controller = Controller(sender)
sender.fired.emit()

この例では controller が生きている間、self.receiver が受信オブジェクトを保持します。QObject の親子関係を使う場合は Receiver(parent) のように、必要な期間存続する親を指定できます。

ローカルの関数や lambda がスコープを抜けると必ず接続が切れる、という説明は正確ではありません。ただし、後で確実に切断する必要がある場合は、接続した関数を属性に保存してください。同じ式をもう一度書いても別の lambda になるため、保存した同じオブジェクトを disconnect() に渡します。

self._callback = lambda: print("受信しました")
sender.fired.connect(self._callback)

# 不要になったとき
sender.fired.disconnect(self._callback)
self._callback = None

親の破棄や deleteLater() で C++ オブジェクトが削除された場合、Python 側に参照が残っていても使用できません。送信元、受信オブジェクト、コールバックを誰がどこまで保持するかを明確にしてください。

参考: Signals and Slots、Object ownership - Shiboken

PySide6 で時間のかかる処理を別スレッドへ移したい場合、QThread を継承する方法と worker QObject を moveToThread() する方法のどちらを使えばよいですか。GUI の更新や GIL、終了処理で注意する点も教えてください。

シグナルで仕事を依頼し、ワーカースレッドのイベントループ上でスロットを実行したい場合は、処理を QObject のサブクラスに実装し、そのオブジェクトを moveToThread() する方法が分かりやすくなります。QThread オブジェクト自体は、run() が実行される新しいスレッドではなく、QThread を生成した元のスレッドに属するためです。QThread のサブクラスにスロットを追加しても、そのスロットが自動的に新しいスレッドで実行されるわけではありません。

次の例では、Worker を親なしで生成してから QThread へ移し、thread.started シグナルで処理を開始します。worker.progress と worker.finished は GUI スレッドにある Window のスロットへキュー経由で届くため、画面の更新は Window 側で行えます。

例:
import sys
import time

from PySide6.QtCore import QObject, QThread, Signal, Slot
from PySide6.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget


class Worker(QObject):
    progress = Signal(int)
    finished = Signal()

    @Slot()
    def run(self):
        thread = QThread.currentThread()
        try:
            for value in range(1, 6):
                if thread.isInterruptionRequested():
                    break
                time.sleep(1)  # 実際には時間のかかる処理を行う
                self.progress.emit(value)
        finally:
            self.finished.emit()


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.label = QLabel("待機中")
        self.start_button = QPushButton("開始")
        self.cancel_button = QPushButton("中止")

        layout = QVBoxLayout(self)
        layout.addWidget(self.label)
        layout.addWidget(self.start_button)
        layout.addWidget(self.cancel_button)

        self.thread = None
        self.worker = None
        self.start_button.clicked.connect(self.start_work)
        self.cancel_button.clicked.connect(self.cancel_work)

    @Slot()
    def start_work(self):
        if self.thread is not None:
            return

        self.thread = QThread(self)
        self.worker = Worker()  # moveToThread() のため親は付けない
        self.worker.moveToThread(self.thread)

        self.thread.started.connect(self.worker.run)
        self.worker.progress.connect(self.show_progress)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.thread.finished.connect(self.work_finished)

        self.start_button.setEnabled(False)
        self.label.setText("処理中")
        self.thread.start()

    @Slot(int)
    def show_progress(self, value):
        self.label.setText(f"進捗: {value}/5")

    @Slot()
    def cancel_work(self):
        if self.thread is not None:
            self.thread.requestInterruption()

    @Slot()
    def work_finished(self):
        self.label.setText("終了")
        self.start_button.setEnabled(True)
        self.worker = None
        self.thread = None


app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())

worker と thread は、処理中に Python の参照が失われないよう Window の属性に保持しています。moveToThread() する QObject には親を付けられません。子 QObject がある場合は子も一緒に移動しますが、単なる Python 属性に格納しただけの QObject は自動的に子にはならないため、親子関係も確認してください。

ワーカーから QLabel や QPushButton などの QWidget を直接操作してはいけません。結果や進捗をシグナルで GUI スレッドへ送り、画面は GUI 側のスロットで更新します。また、GUI スレッドで thread.wait() を呼んで長時間待つと、その間は画面が応答しなくなります。通常の完了処理には finished シグナルを使います。

終了時は、worker.finished から thread.quit() を呼び、worker.deleteLater() と thread.deleteLater() で各オブジェクトを適切なイベントループ上で破棄します。terminate() による強制終了は危険なので避けます。中止が必要なら、例のように requestInterruption() で要求し、ワーカー側が isInterruptionRequested() を定期的に確認して安全な位置で処理を終える協調的な設計にします。なお、長時間ブロックする API の呼び出し中は、その呼び出しが戻るまで中止要求を確認できません。

処理中にウィンドウやアプリケーションを終了できる場合は、その経路でもスレッドの終了を待つ必要があります。closeEvent() や aboutToQuit の処理で、まず requestInterruption() を呼び、必要に応じて quit() を要求してから、時間制限付きの wait() で終了を確認します。通常の画面操作中に wait() すると GUI が停止するため避けますが、アプリケーションを破棄する直前には、実行中の QThread を残さないことが重要です。時間内に終了しない場合はウィンドウを閉じず、処理が安全に停止できる設計を見直してください。

QThread のサブクラス化そのものが誤りというわけではありません。run() の中で完結する処理を一度実行し、ワーカースレッドのイベントループでスロットやタイマーを処理する必要がない場合には、run() を再実装する方法も選択肢です。ただし、QThread オブジェクトの所属スレッドと run() の実行スレッドが異なるため、両方から参照するデータの同期が必要です。

GIL の制約も別に考える必要があります。通常の GIL が有効な CPython では、worker を QThread へ移しても、純粋な Python の CPU バウンド処理が複数コアで自動的に並列実行されるわけではありません。I/O 待ちや GIL を解放するネイティブ処理ではスレッドが有効ですが、純粋な Python 計算の並列化には複数プロセスを検討します。それでも、重い処理を GUI スレッドから分離することには、画面の応答性を保つ意味があります。

参考資料

QThread - Qt for Python

QObject - Qt for Python

Threads and QObjects - Qt for Python

モデルビュー

QListView や QTableView、QTreeView などのビューでのドラグドロップはどのようにすればよいのでしょうか。

モデル/ビューでドラッグ&ドロップを有効にするには、ビューの設定に加えて、モデルが MIME データの作成と受け入れを実装する必要があります。

QAbstractItemModel のサブクラスでは、用途に応じて次の関数を再実装します。

  • flags() で Qt::ItemIsDragEnabled と Qt::ItemIsDropEnabled を返す
  • mimeTypes() で独自 MIME タイプを宣言する
  • mimeData() で選択項目を QMimeData に変換する
  • canDropMimeData() と dropMimeData() でドロップを検証し、モデルを更新する
  • supportedDropActions() で対応する Qt::DropActions を返す
class StringListModel : public QAbstractListModel
{
public:
    QStringList mimeTypes() const override
    {
        return {"application/x-example-string-list"};
    }

    Qt::DropActions supportedDropActions() const override
    {
        return Qt::CopyAction | Qt::MoveAction;
    }

    Qt::ItemFlags flags(const QModelIndex &index) const override
    {
        auto result = QAbstractListModel::flags(index);
        if (index.isValid())
            result |= Qt::ItemIsDragEnabled;
        result |= Qt::ItemIsDropEnabled;
        return result;
    }

    QMimeData *mimeData(const QModelIndexList &indexes) const override
    {
        auto *data = new QMimeData;
        QStringList values;
        for (const QModelIndex &index : indexes)
            values.append(index.data().toString());
        data->setData("application/x-example-string-list",
                      values.join('
').toUtf8());
        return data;
    }

    bool dropMimeData(const QMimeData *data, Qt::DropAction action,
                      int row, int, const QModelIndex &parent) override
    {
        if (action == Qt::IgnoreAction)
            return true;
        if (!data->hasFormat("application/x-example-string-list"))
            return false;

        const QStringList values = QString::fromUtf8(
            data->data("application/x-example-string-list"))
            .split('
', Qt::SkipEmptyParts);
        const int insertRow = row >= 0 ? row
            : (parent.isValid() ? parent.row() : rowCount());
        beginInsertRows({}, insertRow, insertRow + values.size() - 1);
        for (qsizetype i = 0; i < values.size(); ++i)
            m_items.insert(insertRow + i, values.at(i));
        endInsertRows();
        return true;
    }

private:
    QStringList m_items;
};

ビュー側では、例えば setDragDropMode(QAbstractItemView::DragDrop) と setDefaultDropAction(Qt::MoveAction) を設定します。実際に移動を行う場合は、コピー後の元データ削除や同一モデル内の行移動も含めて、モデルの整合性を保つように実装してください。

参考: Using drag and drop with item views

QFileSystemModel を QTreeView に設定して使っています。ツリービューが表示されたときにツリーが展開され、各列幅が表示内容に合わされた状態にするにはどのようにすればよいでしょうか。

ツリーを展開するには QTreeView::expandToDepth() を使い、各列幅を表示内容に合わせて広げるには QTreeView::resizeColumnToContents() を使います。 これらの関数はツリービューに設定するモデルにデータがロードされた後に呼び出す必要があります。QFileSystemModel のデータのロード完了を通知するシグナル QFileSystemModel::directoryLoaded を利用してこれらの関数を呼び出せば、ツリービューを展開して、項目を列幅に合わせて表示できます。


main.cpp:
#include <QApplication>
#include <QTreeView>
#include <QFileSystemModel>

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QTreeView tree;
    tree.resize(800, 600);

    QFileSystemModel model;

    QObject::connect(&model, &QFileSystemModel::directoryLoaded, [&]() {
        tree.expandToDepth(1);
        for (int column = 0; column < tree.model()->columnCount(); ++column) {
            tree.resizeColumnToContents(column);
        }
    });

    tree.setModel(&model);
    tree.show();

    model.setRootPath(QDir::homePath());

    return app.exec();
}

サンプルコード

treeviewdecoration.zip
QTreeView に QFileSystemModel を設定して使っています。空のディレクトリーで枝分かれの +/- 表示をしないようにするにはどのようにすればよいでしょうか。

hasChildren()を再実装できますが、QFileSystemModelは非同期で内容を読み込みます。未読込のディレクトリーと、読込済みで子項目がないディレクトリーを区別してください。未読込時のrowCount()==0だけで空と判断すると、展開できなくなることがあります。

以下はQt 6 / Qt Widgets用のmain.cpp全体です。directoryLoadedを受けたパスだけ、モデルのrowCount()で判定します。

#include <QApplication>
#include <QTreeView>
#include <QFileSystemModel>
#include <QDir>
#include <QSet>

class FileSystemModel : public QFileSystemModel
{
public:
    explicit FileSystemModel(QObject *parent = nullptr) : QFileSystemModel(parent)
    {
        connect(this, &QFileSystemModel::directoryLoaded, this,
                [this](const QString &path) {
            loaded.insert(path);
            const QModelIndex item = index(path);
            if (item.isValid())
                emit dataChanged(item, item);
        });
    }
    bool hasChildren(const QModelIndex &parent = QModelIndex()) const override
    {
        if (!parent.isValid())
            return QFileSystemModel::hasChildren(parent);
        if (parent.column() != 0 || !isDir(parent))
            return false;
        if (!loaded.contains(filePath(parent)))
            return QFileSystemModel::hasChildren(parent);
        return rowCount(parent) > 0;
    }
private:
    QSet<QString> loaded;
};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    FileSystemModel model;
    model.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
    QTreeView tree;
    tree.setModel(&model);
    tree.setRootIndex(model.setRootPath(QDir::homePath()));
    tree.resize(800, 500);
    tree.show();
    return app.exec();
}

確認方法と制約

空のフォルダーとファイル入りのフォルダーを用意し、それぞれ一度展開して読み込みを完了させます。空の方では展開記号が消え、内容がある方では残ることを確認します。未読込のフォルダーでは記号が一時的に表示されます。「空」はモデルに表示される子項目がないことを意味します。

hasChildren()内でQDirを使って同期的に全内容を走査する方法は、ネットワークドライブや大量のファイルでGUIを止めるおそれがあります。初回表示前から空を判定する必要がある場合は、対象範囲を限定して非同期に先読みし、結果をキャッシュする設計が必要です。

この例は起動中にrootPathやフィルターを切り替えない小さな例です。実アプリでは、モデルのリセット、ディレクトリーの名前変更、監視結果の反映に応じてキャッシュを無効化します。権限不足や読込エラーを空フォルダーと断定しないでください。

参考資料

従来の同期走査を使う参考サンプル: treeviewemptydirectory.zip(上の例とは実装が異なります)。

お問い合わせリンク(FAQ)