|
||
![]() |
||
Темой обсуждения этой главы будут -- чтение и запись файлов, навигация по файловой системе и взаимодействие с внешними приложениями.
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, процедура чтения и записи двоичных данных становится простой и надежной.
Для чтения и записи текстовых данных, Qt предоставляет класс QTextStream. Он может использоваться как для чтения/записи простого текста, так и для файлов с другими текстовыми форматами, такими как HTML, XML и файлов с исходными текстами программ. Он принимает на себя обязательства по преобразованию кодировки символов между Unicode и 8-ми битными кодировками, а так же по разному обрабатывает признак окончания строки, в соответствии с соглашениями, принятыми в различных операционных системах.
В качестве фундаментального типа данных, QTextStream использует QChar. В дополнение к символьным и строковым данным, QTextStream поддерживает базовые числовые типы языка C++, конвертируя их в/из строки.
С целью демонстрации возможностей QTextStream, продолжим рассмотрение реализации класса Gallery. Ниже приводится исходный текст функции saveText(), которая сохраняет список картин в простой текстовый файл:
bool Gallery::saveText(const QString &fileName) { QFile file(fileName); if (!file.open(IO_WriteOnly | IO_Translate)) { ioError(file, tr("Cannot open file %1 for writing")); return false; } QTextStream out(&file); out.setEncoding(QTextStream::UnicodeUTF8); list<Drawing>::const_iterator it = drawings.begin(); while (it != drawings.end()) { out << *it; ++it; } if (file.status() != IO_Ok) { ioError(file, tr("Error writing to file %1")); return false; } return true; }При открытии файла используется флаг IO_Translate, чтобы корректным образом перевести символ перевода строки в последовательность символов, которая соответствует используемой операционной системе ("/r/n" -- для Windows, "/r" -- для Mac OS X). Затем устанавливается кодировка символов UTF-8, совместимая с ASCII. (За дополнительной информацией об Unicode, см. Главу 15.) После этого, в цикле, в файл выводятся описания картин, с помощью перегруженного оператора "<<":
QTextStream &operator<<(QTextStream &out, const Drawing &drawing) { out << drawing.myTitle << ":" << drawing.myArtist << ":" << drawing.myYear << endl; return out; }При записи сведений о картине, в качестве разделителя полей, используется символ двоеточия. Каждая запись в файле завершается символом перевода строки. При этом мы исходим из предположения, что ни имя художника, ни название картины не содержат символов двоеточия или перевода строки.
Ниже показан пример содержимого файла, созданного функцией saveText():
The False Shepherds:Hans Bol:1576 Panoramic Landscape:Jan Brueghel the Younger:1619 Dune Landscape:Jan van Goyen:1630 River Delta:Jan van Goyen:1653Теперь перейдем к функции чтения файла:
bool Gallery::loadText(const QString &fileName) { QFile file(fileName); if (!file.open(IO_ReadOnly | IO_Translate)) { ioError(file, tr("Cannot open file %1 for reading")); return false; } drawings.clear(); QTextStream in(&file); in.setEncoding(QTextStream::UnicodeUTF8); while (!in.atEnd()) { Drawing drawing; in >> drawing; drawings.push_back(drawing); } if (file.status() != IO_Ok) { ioError(file, tr("Error reading from file %1")); return false; } return true; }Все самое интересное в этой функции, заключено внутри цикла while. Он выполняет чтение данных, с помощью оператора ">>", до тех пор, пока не будет достигнут конец файла.
Реализация оператора ">>" не так тривиальна, поскольку представление текстовых данных не так однозначно. Рассмотрим следующий пример:
out << "alpha" << "bravo";Если исходить из того, что out -- это экземпляр класса QTextStream, то в файл фактически будет записана одна строка "alphabravo". Мы не сможем прочитать данные, просто написав:
in >> str1 >> str2;Фактически, в переменную str1 будет записана строка "alphabravo", а в переменную str2 -- ничего.
Если записываемый текст состоит из отдельных слов, мы можем вставлять пробелы между ними и затем читать этот текст слово за словом. (Этот подход был реализован в функциях DiagramView::copy() и DiagramView::paste(), в Главе 8.) Но в данном случае этот вариант не подходит, поскольку имя художника и название картины могут состоять более чем из одного слова. Поэтому, за один раз читается целая строка и затем разбивается на элементы, с помощью функции QStringList::split() :
QTextStream &operator>>(QTextStream &in, Drawing &drawing) { QString str = in.readLine(); QStringList fields = QStringList::split(":", str); if (fields.size() == 3) { drawing.myTitle = fields[0]; drawing.myArtist = fields[1]; drawing.myYear = fields[2].toInt(); } return in; }Текстовые файлы могут читаться за один прием, с помощью QTextStream::read():
QString wholeFile = in.read();В переменной, конец каждой строки будет отмечен символом '\n', независимо от используемой операционной системы.
Считывание файла за раз может оказаться удобным решением, если данные должны пройти предварительную обработку, например:
wholeFile.replace("&", "&"); wholeFile.replace("<", "<"); wholeFile.replace(">", ">");Чтобы записать данные в файл за одно обращение, можно сначала разместить их в переменной, а затем вывести на диск:
QString Gallery::saveToString() { QString result; QTextOStream out(&result); list<Drawing>::const_iterator it = drawings.begin(); while (it != drawings.end()) { out << *it; ++it; } return result; }Связать поток со строковой переменной так же просто, как и связать поток с файлом.
void Gallery::readFromString(const QString &data) { QString string = data; drawings.clear(); QTextIStream in(&string); while (!in.atEnd()) { Drawing drawing; in >> drawing; drawings.push_back(drawing); } }Запись текстовых данных -- довольно простая операция, а вот чтение их может оказаться довольно сложной задачей. В случае использования сложных форматов может потребоваться написать полноценный синтаксический анализатор. Как правило, подобные анализаторы считывают текст символ за символом, с помощью оператора ">>" в переменную типа QChar или построчно, с помощью readLine() и затем анализируют полученную строку.
Класс QDir дает возможность навигации по файловой системе и получать информацию о файлах, независимо от типа операционной системы. Чтобы показать некоторые особенности класса QDir, напишем небольшое консольное приложение, которое подсчитывает суммарный объем всех файлов с изображениями в заданном каталоге и вложенных подкаталогах.
Основу приложения составляет функция imageSpace(), которая суммирует размеры файлов в заданном каталоге:
int imageSpace(const QString &path) { QDir dir(path); QStringList::Iterator it; int size = 0; QStringList files = dir.entryList("*.png *.jpg *.jpeg", QDir::Files); it = files.begin(); while (it != files.end()) { size += QFileInfo(path, *it).size(); ++it; } QStringList dirs = dir.entryList(QDir::Dirs); it = dirs.begin(); while (it != dirs.end()) { if (*it != "." && *it != "..") size += imageSpace(path + "/" + *it); ++it; } return size; }Начинается она с создания экземпляра класса QDir, с заданным полным именем каталога. Затем вызывается функция entryList(), которой передаются два аргумента. Первый из них -- это список шаблонов имен файлов, разделенных пробелами. В шаблонах допускается указывать символы подстановки '*' и '?'. В данном примере будут учитываться только файлы изображений, в форматах JPEG и PNG. Второй аргумент определяет тип элементов результирующего списка (обычные файлы, каталоги, устройства и пр.).
Затем, в цикле, осуществляется проход по списку файлов и суммируются их размеры. Класс QFileInfo позволяет получить доступ к таким характеристикам файла, как размер, права доступа, владелец и время (создания, последнего обращения, последнего изменения).
Вторым обращением к entryList() создается список вложенных подкаталогов. После чего, в цикле, выполняется проход по подкаталогам, с рекурсивным вызовом imageSpace() для каждого из них.
Полный путь к вложенным подкаталогам "собирается" из полного пути к текущему каталогу, символа слэша и имени подкаталога (*it). Класс QDir интерпретирует символ "/" как разделитель имен каталогов независимо от используемой операционной системы. Перед выводом полного пути перед пользователем, можно вызвать функцию QDir::convertSeparators(), которая преобразует символ "/" в корректное представление, в зависимости от используемой платформы.
Добавим в нашу программу функцию main():
int main(int argc, char *argv[]) { QString path = QDir::currentDirPath(); if (argc > 1) path = argv[1]; cerr << "Space used by images in " << endl << path.ascii() << endl << "and its subdirectories is " << (imageSpace(path) / 1024) << " KB" << endl; return 0; }В этом примере мы не создавали объект класса QApplication, потому что мы воспользовались только инструментальными классами, не имеющими отношения к графическому интерфейсу. Полный список таких классов вы найдете по адресу: http://doc.trolltech.com/3.2/tools.html.
Для начальной инициализации переменной path была использована функция QDir::currentDirPath(), которая возвращает полное имя текущего каталога. В качестве альтернативы можно было бы использовать функцию QDir::homeDirPath(), возвращающую полный путь к домашнему каталогу пользователя. Если путь к каталогу задается пользователем из командной строки, то он замещает значение по-умолчанию. В заключение вызывается функция imageSpace(), которая подсчитывает суммарный размер всех файлов с изображениями.
Класс QDir предоставляет ряд других функций, для работы с каталогами и файлами, среди них: rename(), exists(), mkdir() и rmdir().
Класс QProcess позволяет запускать и взаимодействовать с другими программами. Экземпляры класса работают асинхронно, выполняя всю работу в фоновом режиме, что не приводит к "замораживанию" пользовательского интерфейса. QProcess может известить приложение о завершении запущенной им программы или о наличии данных, полученных от нее, выдавая соответствующие сигналы.
В демонстрационных целях напишем небольшое приложение, которое предоставит пользователю интерфейс с внешней программой преобразования графических файлов. Для данного примера будет использоваться программа convert из пакета ImageMagick, которая доступна для большинства платформ.
Рисунок 10.1. Внешний вид приложения Image Converter.
QProcess *process; QString fileFilters;Утилита uic добавляет эти переменные в класс ConvertDialog.
void ConvertDialog::init() { process = 0; QStringList imageFormats = QImage::outputFormatList(); targetFormatComboBox->insertStringList(imageFormats); fileFilters = tr("Images") + " (*." + imageFormats.join(" *.").lower() + ")"; }Переменная fileFilters содержит текст описания и один, или более, шаблонов имен файлов (например, "Text files (*.txt)"). Функция QImage::outputFormatList() возвращает список форматов изображений, поддерживаемых Qt. Этот список тесно связан с опциями, выбранными при установке библиотки.
void ConvertDialog::browse() { QString initialName = sourceFileEdit->text(); if (initialName.isEmpty()) initialName = QDir::homeDirPath(); QString fileName = QFileDialog::getOpenFileName(initialName, fileFilters, this); fileName = QDir::convertSeparators(fileName); if (!fileName.isEmpty()) { sourceFileEdit->setText(fileName); convertButton->setEnabled(true); } }Кнопка Browse связана со слотом browse(). Если ранее пользователь уже выбирал файл, то путь поиска, для диалога выбора файла, назначается исходя из полного имени предыдущего файла, в противном случае, открывается домашний каталог.
void ConvertDialog::convert() { QString sourceFile = sourceFileEdit->text(); targetFile = QFileInfo(sourceFile).dirPath() + QDir::separator() + QFileInfo(sourceFile).baseName(); targetFile += "."; targetFile += targetFormatComboBox->currentText().lower(); convertButton->setEnabled(false); outputTextEdit->clear(); process = new QProcess(this); process->addArgument("convert"); if (enhanceCheckBox->isChecked()) process->addArgument("-enhance"); if (monochromeCheckBox->isChecked()) process->addArgument("-monochrome"); process->addArgument(sourceFile); process->addArgument(targetFile); connect(process, SIGNAL(readyReadStderr()), this, SLOT(updateOutputTextEdit())); connect(process, SIGNAL(processExited()), this, SLOT(processExited())); process->start(); }Кнопка Convert связана со слотом convert(). По сигналу от кнопки собирается имя целевого файла, из имени исходного файла и расширения, соответствующего заданному формату.
Затем создается экземпляр класса QProcess. После этого собирается список аргументов командной строки, с помощью функции addArgument(). Первым идет имя файла внешней программы. Далее следуют аргументы, которые будут ей передаваться.
После создания списка аргументов производится соединение сигнала readyReadStderr(), класса QProcess, со слотом updateOutputTextEdit() диалогового окна, чтобы выводить в QTextEdit сообщения от внешней программы, по мере их поступления. И затем соединяется сигнал processExited(), класса QProcess, со слотом processExited() диалогового окна.
void ConvertDialog::updateOutputTextEdit() { QByteArray data = process->readStderr(); QString text = outputTextEdit->text() + QString(data); outputTextEdit->setText(text); }Как только внешняя программа выдаст что нибудь на stderr, будет вызван слот updateOutputTextEdit(). Сообщение будет прочитано и записано в QTextEdit.
void ConvertDialog::processExited() { if (process->normalExit()) { outputTextEdit->append(tr("File %1 created") .arg(targetFile)); } else { outputTextEdit->append(tr("Conversion failed")); } delete process; process = 0; convertButton->setEnabled(true); }По завершении внешнего процесса перед пользователем выводится соответствующее сообщение, после чего процесс удаляется.
Создание графического интерфейса, для консольных приложений, подобным образом, может оказаться очень полезным, потому что позволяет использовать функциональность, заложенную в уже существующие программы и нам не нужно ломать голову над собственной реализацией.
|