File I/O & Qt Resources

QFile, QTextStream, QDataStream, QDir, QFileSystemWatcher, and the Qt Resource System — reading, writing, and packaging files.

6 min read
47539 chars

Overview

Qt provides a unified file I/O API that works on all platforms — Windows, Linux, embedded Linux — with the same code.

QIODevice (base)
  ├── QFile          ← files on disk
  ├── QBuffer        ← in-memory I/O
  ├── QTcpSocket     ← network I/O
  └── QSerialPort    ← serial port I/O

QTextStream   ← text layer over any QIODevice
QDataStream   ← binary layer over any QIODevice

QFile — Read a Text File

#include <QFile>
#include <QTextStream>

QFile file("/etc/hostname");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    qWarning() << "Cannot open:" << file.errorString();
    return;
}

QTextStream in(&file);
while (!in.atEnd()) {
    QString line = in.readLine();
    qDebug() << line;
}
file.close();

QFile — Write a Text File

QFile out("/tmp/sensor_log.txt");
if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) {
    qWarning() << "Write failed:" << out.errorString();
    return;
}

QTextStream stream(&out);
stream << "timestamp,temp,volt\n";
stream << "2026-03-15T10:00:00,36.5,3.30\n";
stream << "2026-03-15T10:00:01,36.7,3.31\n";
out.close();

Append Mode

QFile log("/var/log/device.log");
if (log.open(QIODevice::Append | QIODevice::Text)) {
    QTextStream s(&log);
    s << QDateTime::currentDateTime().toString(Qt::ISODate)
      << " INFO Device started\n";
}

QDataStream — Binary I/O

#include <QDataStream>

// Write binary
QFile binFile("config.bin");
binFile.open(QIODevice::WriteOnly);
QDataStream out(&binFile);
out.setVersion(QDataStream::Qt_6_0);
out << QString("device-001")
    << (float)36.5f
    << (quint32)115200
    << true;
binFile.close();

// Read binary
QFile inFile("config.bin");
inFile.open(QIODevice::ReadOnly);
QDataStream in(&inFile);
in.setVersion(QDataStream::Qt_6_0);

QString name; float temp; quint32 baud; bool active;
in >> name >> temp >> baud >> active;
qDebug() << name << temp << baud << active;

QDir — Directory Operations

#include <QDir>

QDir dir("/var/log/sensors");

// List files
QStringList logs = dir.entryList({"*.log"}, QDir::Files);
for (const QString &f : logs)
    qDebug() << f;

// Create directory
if (!dir.exists())
    dir.mkpath(".");  // creates all parent dirs

// File paths
QString path = dir.absoluteFilePath("latest.log");

// Iterate recursively
QDirIterator it("/var/log", QStringList("*.csv"),
                QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
    qDebug() << it.next();
}

QFileInfo — File Metadata

#include <QFileInfo>

QFileInfo info("/etc/qt/config.ini");
qDebug() << info.exists();          // true/false
qDebug() << info.size();            // bytes
qDebug() << info.suffix();          // "ini"
qDebug() << info.baseName();        // "config"
qDebug() << info.absolutePath();    // "/etc/qt"
qDebug() << info.isReadable();
qDebug() << info.lastModified();    // QDateTime

QFileSystemWatcher — Monitor Changes

#include <QFileSystemWatcher>

QFileSystemWatcher *watcher = new QFileSystemWatcher(this);
watcher->addPath("/etc/device/config.ini");
watcher->addPath("/var/log/sensors");

connect(watcher, &QFileSystemWatcher::fileChanged,
    [](const QString &path) {
        qDebug() << "File changed:" << path;
        // Re-read config
    });

connect(watcher, &QFileSystemWatcher::directoryChanged,
    [](const QString &path) {
        qDebug() << "Directory changed:" << path;
    });

Qt Resource System (.qrc)

The Qt Resource System embeds files inside the application binary — images, icons, configs, QML files — so they’re always available regardless of installation path.

1. Create resources.qrc

<!DOCTYPE RCC>
<RCC version="1.0">
  <qresource prefix="/icons">
    <file>connect.svg</file>
    <file>disconnect.svg</file>
  </qresource>
  <qresource prefix="/config">
    <file alias="defaults.ini">config/defaults.ini</file>
  </qresource>
</RCC>

2. Add to CMakeLists.txt

qt_add_resources(MyApp "resources"
    PREFIX "/"
    FILES
        icons/connect.svg
        icons/disconnect.svg
        config/defaults.ini
)

3. Use with : prefix

// Load embedded icon
QIcon icon(":/icons/connect.svg");
btn->setIcon(icon);

// Read embedded config file
QFile cfg(":/config/defaults.ini");
cfg.open(QIODevice::ReadOnly | QIODevice::Text);
QTextStream in(&cfg);
qDebug() << in.readAll();

Visual: File I/O Paths

┌───────────────────────────────────────────────────────┐
│                    Qt File I/O                        │
│                                                       │
│  QFile(":/icons/icon.svg")   ← embedded in binary    │
│  QFile("config.ini")         ← relative to CWD       │
│  QFile("/etc/device/config") ← absolute path         │
│  QFile("C:/config.ini")      ← Windows absolute      │
│                                                       │
│  QStandardPaths::writableLocation(...)                │
│    AppDataLocation  → /home/user/.config/AppName/     │
│    TempLocation     → /tmp/                           │
│    HomeLocation     → /home/user/                     │
└───────────────────────────────────────────────────────┘

QStandardPaths — Platform Paths

#include <QStandardPaths>

// Write config to platform-correct location
QString configDir = QStandardPaths::writableLocation(
    QStandardPaths::AppConfigLocation);
QDir().mkpath(configDir);

QFile cfg(configDir + "/settings.ini");
cfg.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream s(&cfg);
s << "[uart]\nbaud=115200\n";

Lab 1 — CSV Sensor Logger

class CsvLogger : public QObject {
    Q_OBJECT
    QFile       m_file;
    QTextStream m_stream;
    qint64      m_rowCount = 0;

public:
    CsvLogger(const QString &path, QObject *p = nullptr)
        : QObject(p), m_file(path)
    {
        if (!m_file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            qWarning() << "Cannot open log:" << m_file.errorString();
            return;
        }
        m_stream.setDevice(&m_file);
        m_stream << "row,timestamp,temperature,voltage\n";
    }

    ~CsvLogger() {
        if (m_file.isOpen()) m_file.close();
    }

public slots:
    void log(float temp, float volt) {
        ++m_rowCount;
        m_stream << m_rowCount << ","
                 << QDateTime::currentDateTime().toString(Qt::ISODate) << ","
                 << QString::number(temp, 'f', 2) << ","
                 << QString::number(volt, 'f', 3) << "\n";
        m_stream.flush();
    }

    qint64 rowCount() const { return m_rowCount; }
};

// Usage
CsvLogger logger("/var/log/sensors/data.csv");
connect(&sensor, &Sensor::dataReady, &logger, &CsvLogger::log);

Lab 2 — Binary Config File

struct DeviceConfig {
    QString deviceId;
    quint32 baudRate;
    float   sampleRate;
    bool    autoConnect;
};

void saveConfig(const QString &path, const DeviceConfig &cfg) {
    QFile f(path);
    if (!f.open(QIODevice::WriteOnly)) return;
    QDataStream ds(&f);
    ds.setVersion(QDataStream::Qt_6_0);
    ds << cfg.deviceId << cfg.baudRate << cfg.sampleRate << cfg.autoConnect;
}

DeviceConfig loadConfig(const QString &path) {
    DeviceConfig cfg{"uart-0", 115200, 10.0f, true};
    QFile f(path);
    if (!f.open(QIODevice::ReadOnly)) return cfg;
    QDataStream ds(&f);
    ds.setVersion(QDataStream::Qt_6_0);
    ds >> cfg.deviceId >> cfg.baudRate >> cfg.sampleRate >> cfg.autoConnect;
    return cfg;
}

// Round-trip
DeviceConfig cfg{"uart-0", 115200, 100.0f, true};
saveConfig("/etc/device/config.bin", cfg);
auto loaded = loadConfig("/etc/device/config.bin");
qDebug() << loaded.deviceId << loaded.baudRate;

Interview Questions

Q1: What is the difference between QTextStream and QDataStream?

QTextStream reads/writes human-readable text (UTF-8/locale encoded). QDataStream reads/writes binary data in a platform-independent format — useful for config files, network protocols, and firmware images.

Q2: How do Qt resources (: prefix) differ from regular files?

Resources are compiled into the application binary. They’re always available regardless of the working directory or installation path. They’re read-only and can’t be modified at runtime. Ideal for icons, default configs, and QML files.

Q3: What does QFile::flush() vs QFile::close() do?

flush() writes buffered data to the OS without closing the file — useful for partial writes in long-running loggers. close() flushes and releases the file handle. Always close files when done.

Q4: How do you find platform-correct write locations on any OS?

Use QStandardPaths::writableLocation() with types like AppConfigLocation, AppDataLocation, or TempLocation. This returns the correct platform path automatically on Windows, Linux, macOS, and embedded.

Q5: What is the risk of not setting QDataStream::setVersion()?

The stream format may vary between Qt versions. If writer and reader use different Qt versions and no version is set, reading may fail silently. Always call setVersion(QDataStream::Qt_6_0) (or appropriate version) on both sides.


Application: Firmware Update Logger

class FirmwareUpdateLogger : public QObject {
    Q_OBJECT

    struct UpdateRecord {
        QDateTime timestamp;
        QString   firmwarePath;
        quint64   size;
        bool      success;
        QString   errorMsg;
    };

    QList<UpdateRecord> m_history;
    QString             m_historyPath;

public:
    FirmwareUpdateLogger(const QString &path, QObject *p = nullptr)
        : QObject(p), m_historyPath(path) { load(); }

    void recordUpdate(const QString &fwPath, quint64 size,
                      bool success, const QString &err = {})
    {
        m_history.append({
            QDateTime::currentDateTime(), fwPath, size, success, err
        });
        save();
        emit recordAdded(m_history.last().success);
    }

    void printHistory() const {
        qDebug() << "=== Firmware Update History ===";
        for (const auto &r : m_history) {
            qDebug() << r.timestamp.toString(Qt::ISODate)
                     << (r.success ? "OK" : "FAIL")
                     << r.firmwarePath
                     << r.size << "bytes"
                     << (r.errorMsg.isEmpty() ? "" : "(" + r.errorMsg + ")");
        }
    }

signals:
    void recordAdded(bool success);

private:
    void save() {
        QFile f(m_historyPath);
        if (!f.open(QIODevice::WriteOnly)) return;
        QDataStream ds(&f);
        ds.setVersion(QDataStream::Qt_6_0);
        ds << (qint32)m_history.size();
        for (const auto &r : m_history)
            ds << r.timestamp << r.firmwarePath << r.size << r.success << r.errorMsg;
    }

    void load() {
        QFile f(m_historyPath);
        if (!f.open(QIODevice::ReadOnly)) return;
        QDataStream ds(&f);
        ds.setVersion(QDataStream::Qt_6_0);
        qint32 n; ds >> n;
        for (int i = 0; i < n; ++i) {
            UpdateRecord r;
            ds >> r.timestamp >> r.firmwarePath >> r.size >> r.success >> r.errorMsg;
            m_history.append(r);
        }
    }
};

References

ResourceLink
Qt Docs: QFilehttps://doc.qt.io/qt-6/qfile.html
Qt Docs: QTextStreamhttps://doc.qt.io/qt-6/qtextstream.html
Qt Docs: QDataStreamhttps://doc.qt.io/qt-6/qdatastream.html
Qt Docs: Resource Systemhttps://doc.qt.io/qt-6/resources.html
Qt Docs: QStandardPathshttps://doc.qt.io/qt-6/qstandardpaths.html

Next tutorial → Custom Widgets & QPainter