Глава 10. Ввод/вывод.

Темой обсуждения этой главы будут -- чтение и запись файлов, навигация по файловой системе и взаимодействие с внешними приложениями.

Qt предоставляет в ваше распоряжение два замечательных класса: QDataStream и QTextStream, которые значительно упрощают операции чтения-записи файлов. Они берут на себя хлопоты о порядке следования байт и кодировке текста, обеспечивая полную совместимость приложений на разных платформах.

Во многих приложениях необходимо реализовать возможность обхода файловой системы или предоставления сведений о файлах. Классы QDir и QFileInfo возьмут на себя эту "черную" и "неблагодарную" работу.

Иногда возникает необходимость запускать другие программы из нашего приложения. Класс QProcess сможет выполнить это в асинхронном режиме, не "замораживая" интерфейс с пользователем.

10.1. Чтение и запись двоичных данных.

Чтение и запись данных произвольного формата, с помощью QDataStream -- это самый простой способ организовать сохранение и загрузку данных в Qt-приложении. Он поддерживает огромное количество типов данных Qt, включая QByteArray, QFont, QImage, QMap<K, T>, QPixmap, QString, QValueList<T> и QVariant. Перечень типов данных, поддерживаемых QDataStream вы найдете по адресу http://doc.trolltech.com/3.2/datastreamformat.html .

Чтобы продемонстрировать основные приемы работы с двоичными данными, мы напишем два класса: Drawing и Gallery. Первый будет хранить основные сведения о картине (имя художника, название и год создания), второй -- список картин.

Начнем с класса Gallery.

class Gallery : public QObject { public: bool loadBinary(const QString &fileName); bool saveBinary(const QString &fileName); ... private: enum { MagicNumber = 0x98c58f26 }; void writeToStream(QDataStream &out); void readFromStream(QDataStream &in); void error(const QFile &file, const QString &message); void ioError(const QFile &file, const QString &message); QByteArray getData(); void setData(const QByteArray &data); QString toString(); std::list<Drawing> drawings; }; Он содержит публичные функции, которые сохраняют и загружают данные. Данные -- это список картин. Каждый элемент списка -- это объект класса Drawing. Приватные функции мы будем рассматривать по мере необходимости.

Ниже приводится исходный текст функции, сохраняющей список картин в двоичном виде:

bool Gallery::saveBinary(const QString &fileName) { QFile file(fileName); if (!file.open(IO_WriteOnly)) { ioError(file, tr("Cannot open file %1 for writing")); return false; } QDataStream out(&file); out.setVersion(5); out << (Q_UINT32)MagicNumber; writeToStream(out); if (file.status() != IO_Ok) { ioError(file, tr("Error writing to file %1")); return false; } return true; } Сначала мы открываем файл. Затем устанавливаем версию QDataStream. Номер версии определяет способ сохранения различных типов данных. Базовые типы языка C++ всегда сохраняются в неизменном виде.

Далее в файл выводится сигнатура (число), которая идентифицирует файлы галереи. Чтобы обеспечить совместимость с другими платформами, мы приводим MagicNumber к типу Q_UINT32.

Список картин выводится в файл приватной функцией writeToStream(). Нет необходимости явно закрывать файл -- это будет сделано автоматически, когда объект QFile выйдет из области видимости по завершении функции.

После вызова writeToStream() проверяется статус устройства QFile. Если возникла ошибка -- вызывается ioError(), которая выводит окно с сообщением и вызывающей программе возвращается значение false.

void Gallery::ioError(const QFile &file, const QString &message) { error(file, message + ": " + file.errorString()); } Функция ioError() вызывает более универсальную функцию error(): void Gallery::error(const QFile &file, const QString &message) { QMessageBox::warning(0, tr("Gallery"), message.arg(file.name())); } Теперь рассмотрим функцию writeToStream(): void Gallery::writeToStream(QDataStream &out) { list<Drawing>::const_iterator it = drawings.begin(); while (it != drawings.end()) { out << *it; ++it; } } Она последовательно проходит по списку картин и сохраняет их одну за другой в поток, который был передан в качестве аргумента. Если бы мы, вместо list<Drawing> использовали определение QValueList<Drawing>, мы могли бы обойтись без цикла, просто записав: out << drawings; Когда QValueList<T> помещается в поток, то каждый элемент списка записывается посредством его собственного оператора "<<". QDataStream &operator<<(QDataStream &out, const Drawing &drawing) { out << drawing.myTitle << drawing.myArtist << drawing.myYear; return out; } Вывод объекта Drawing осуществляется простой записью трех его переменных-членов: myTitle, myArtist и myYear. Перегруженный оператор operator<<() должен быть объявлен как "дружественный" (friend). В заключение функция возвращает поток. Это общепринятая в языке C++ идиома программирования, которая позволяет объединять операторы "<<" в цепочки, например: out << drawing1 << drawing2 << drawing3; Ниже приводится определение класса Drawing: class Drawing { friend QDataStream &operator<<(QDataStream &, const Drawing &); friend QDataStream &operator>>(QDataStream &, Drawing &); public: Drawing() { myYear = 0; } Drawing(const QString &title, const QString &artist, int year) { myTitle = title; myArtist = artist; myYear = year; } QString title() const { return myTitle; } void setTitle(const QString &title) { myTitle = title; } QString artist() const { return myArtist; } void setArtist(const QString &artist) { myArtist = artist; } int year() const { return myYear; } void setYear(int year) { myYear = year; } private: QString myTitle; QString myArtist; int myYear; }; Рассмотрим функцию, которая читает файл со списком картин: bool Gallery::loadBinary(const QString &fileName) { QFile file(fileName); if (!file.open(IO_ReadOnly)) { ioError(file, tr("Cannot open file %1 for reading")); return false; } QDataStream in(&file); in.setVersion(5); Q_UINT32 magic; in >> magic; if (magic != MagicNumber) { error(file, tr("File %1 is not a Gallery file")); return false; } readFromStream(in); if (file.status() != IO_Ok) { ioError(file, tr("Error reading from file %1")); return false; } return true; } Файл открывается на чтение и создается объект QDataStream, который будет читать данные из файла. Мы установили версию 5 для QDataStream, поскольку в этой версии была произведена запись в файл. Использование фиксированного номера версии -- 5, гарантирует, что приложение всегда сможет читать и записывать данные, если оно собрано с Qt 3.2 или более поздней.

Работа с файлом начинается со считывания сигнатуры (числа) MagicNumber. Это дает нам уверенность, что мы работаем с файлом, содержащим список картин, а не что-то иное. Затем список считывается функцией readFromStream().

void Gallery::readFromStream(QDataStream &in) { drawings.clear(); while (!in.atEnd()) { Drawing drawing; in >> drawing; drawings.push_back(drawing); } } Функция начинается с очистки ранее находившихся в списке данных. Затем в цикле производится считывание всех описаний картин, одного за другим. Если бы мы, вместо list<Drawing> использовали определение QValueList<Drawing>, мы могли бы обойтись без цикла, просто записав: in >> drawings; Когда QValueList<T> получает данные из потока, то каждый элемент списка читается посредством его собственного оператора ">>". QDataStream &operator>>(QDataStream &in, Drawing &drawing) { in >> drawing.myTitle >> drawing.myArtist >> drawing.myYear; return in; } Реализация оператора ">>" является зеркальным отражением оператора "<<". При использовании QDataStream у нас не возникает необходимости производить синтаксический анализ в любом его проявлении.

При желании, читать и записывать любые двоичные данные в необработанном виде, можно с помощью функций readRawBytes() и writeRawBytes().

Чтение и запись данных базовых типов (таких как Q_UINT16 или float), может производиться как операторами "<<" и ">>", так и с помощью функций readRawBytes() и writeRawBytes(). По-умолчанию, порядок следования байт, используемый QDataStream -- "big-endian". Для того, чтобы изменить его на "little-endian" (храктерный для платформы Intel), необходимо указывать его явно:

stream.setByteOrder(QDataStream::LittleEndian); В случае чтения/записи базовых типов языка C++, указывать версию, через вызов setVersion(), необязательно.

Если необходимо записать/прочитать файл, что называется "за один присест", то можно воспользоваться методами класса QFile -- writeBlock() и readAll(), например:

file.writeBlock(getData()); Данные, записанные таким образом, находятся в файле в виде простой последовательности байт. Однако, в этом случае, вся ответственность за структурирование и идентификацию данных при считывании, полностью ложится на плечи разработчика. За создание списка QByteArray и заполнение его данными, в классе Gallery отвечает приватная функция getData(). Чтение блока данных из файла выглядит не менее просто, чем запись: setData(file.readAll()); За извлечение данных из QByteArray, в классе Gallery отвечает приватная функция setData().

Сохранение всех данных, в виде QByteArray, может потребовать значительного объема памяти, но такой способ имеет свои преимущества. Например, мы можем сжать данные, с помощью qCompress(), при записи в файл:

file.writeBlock(qCompress(getData())); И разархивировать при считывании: setData(qUncompress(file.readAll())); Ниже приводится один из возможных вариантов реализации функций getData() и setData(): QByteArray Gallery::getData() { QByteArray data; QDataStream out(data, IO_WriteOnly); writeToStream(out); return data; } Здесь создается поток QDataStream, которому в качестве устройства вывода, вместо QFile, назначается QByteArray. После этого массив заполняется двоичными данными, вызовом writeToStream().

Аналогичным образом, функция setData() обращается к readFromStream(), для чтения ранее записанных данных:

void Gallery::setData(const QByteArray &data) { QDataStream in(data, IO_ReadOnly); readFromStream(in); } В примерах выше, мы сохраняли и считывали данные, жестко задавая номер версии для QDataStream. Такой подход достаточно прост и надежен, но он имеет один маленький недостаток: мы не сможем работать с файлами, записанными с новыми версиями. Например, если в последующих версиях Qt, в класс QFont будут добавлены новые элементы, то мы лишимся возможности сохранять и загружать компоненты этого типа, используя более старую версию QDataStream.

Как одно из возможных решений этой проблемы -- записывать в файл номер версии:

QDataStream out(&file); out << (Q_UINT32)MagicNumber; out << (Q_UINT16)out.version(); writeToStream(out); Этот код будет выполнять запись данных, с использованием самой последней версии QDataStream.

При чтении таких файлов, сначала будет считываться сигнатура файла и номер версии QDataStream:

QDataStream in(&file); Q_UINT32 magic; Q_UINT16 streamVersion; in >> magic >> streamVersion; if (magic != MagicNumber) { error(file, tr("File %1 is not a Gallery file")); return false; } else if ((int)streamVersion > in.version()) { error(file, tr("File %1 is from a more recent version of the " "application")); return false; } in.setVersion(streamVersion); readFromStream(in); Чтение данных будет возможно в том случае, если номер версии будет меньше или равен версии, используемой приложением. В противном случае чтение завершится сообщением об ошибке.

Вместо версии QDataStream можно использовать версию приложения. Например, допустим, что некий формат файла соответствует версии 1.3 приложения. Тогда мы могли бы записать следующий код:

QDataStream out(&file); out.setVersion(5); out << (Q_UINT32)MagicNumber; out << (Q_UINT16)0x0103; writeToStream(out); При чтении такого файла можно определять версию QDataStream, основываясь на версии приложения: QDataStream in(&file); Q_UINT32 magic; Q_UINT16 appVersion; in >> magic >> appVersion; if (magic != MagicNumber) { error(file, tr("File %1 is not a Gallery file")); return false; } else if (appVersion > 0x0103) { error(file, tr("File %1 is from a more recent version of the " "application")); return false; } if (appVersion <= 0x0102) { in.setVersion(4); } else { in.setVersion(5); } readFromStream(in); Этот код говорит, что для чтения данных из файла, созданного приложением с версией 1.2 или более ранней, должна использоваться 4-я версия QDataStream, для чтения данных из файла, созданного приложением с версией 1.3 -- 5-я версия QDataStream.

Как только мы получаем в руки механизм определения версии QDataStream, процедура чтения и записи двоичных данных становится простой и надежной.