Темой обсуждения этой главы будут -- чтение и запись файлов, навигация по файловой системе и взаимодействие с внешними приложениями.
Qt предоставляет в ваше распоряжение два замечательных класса: QDataStream и QTextStream, которые значительно упрощают операции чтения-записи файлов. Они берут на себя хлопоты о порядке следования байт и кодировке текста, обеспечивая полную совместимость приложений на разных платформах.
Во многих приложениях необходимо реализовать возможность обхода файловой системы или предоставления сведений о файлах. Классы QDir и QFileInfo возьмут на себя эту "черную" и "неблагодарную" работу.
Иногда возникает необходимость запускать другие программы из нашего приложения. Класс QProcess сможет выполнить это в асинхронном режиме, не "замораживая" интерфейс с пользователем.
Чтение и запись данных произвольного формата, с помощью 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, процедура чтения и записи двоичных данных становится простой и надежной.
Пред. | В начало | След. |
Расширенные возможности буфера обмена. | На уровень выше | Чтение и запись текста. |