На этом шаге рассмотрим реализацию меню File.
Определим слоты и закрытые функции, необходимые для обеспечения работы меню File и для управления списком недавно используемых файлов.
Слот newFile() вызывается при выборе пользователем пункта меню Файл/Новый или при нажатии кнопки на панели инструментов.
void MainWindow::newFile() { /*Закрытая функция okToContinue() отображает всплывающее диалоговое окно, показанное на рис. 1 ("Сохранить изменения?"), если изменения до этого не были сохранены. Она возвращает значение true, если пользователь отвечает Yes или No (сохраняя документ при ответе Yes), и она возвращает значение false, если пользователь отвечает Cancel*/ if (okToContinue()) { /*Функция Spreadsheet::clear() очищает все ячейки и формулы электронной таблицы */ spreadsheet->clear(); /*Закрытая функция set CurrentFile(), кроме установки закрытой переменной curFile и обновления списка недавно используемых файлов, изменяет заголовок окна, отражая тот факт, что редактируемый документ не имеет заголовка*/ setCurrentFile(""); } }
Рис.1. Диалоговое окно "Сохранить изменения?"
bool MainWindow::okToContinue() { /*В okToContinue() мы проверяем свойство windowModified. Если оно имеет значение true, мы выводим на экран сообщение, показанное на рис. 1*/ if (isWindowModified()) { /*Окно сообщения содержит кнопки Yes, No и Cancel. QMessageBox предлагает много стандартных кнопок и автоматически пытается сделать одну кнопку выбранной по умолчанию (активируемой при нажатии пользователем клавиши Enter), а другую - кнопкой отмены (активируемой при нажатии пользователем клавиши Esc). Также существует возможность выбирать те кнопки, которые будут использоваться по умолчанию или будут кнопкой отмены. Кроме того, можно настраивать текст на кнопке. Вызов функции warning()на первый взгляд может показаться слишком сложным, но он имеет очень простой формат: QMessageBox::warning(родительский объект, заголовок, сообщение, кнопки);*/ int r = QMessageBox::warning(this, tr("Электронная таблица"), tr("Документ был изменен.\n" "Сохранить изменения?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); if (r == QMessageBox::Yes) { return save(); } else if (r == QMessageBox::Cancel) { return false; } } return true; }
Слот open() соответствует пункту меню Файл/Открыть.
void MainWindow::open() { /*слот сначала вызывает okToContinue() для обработки несохраненных изменений*/ if (okToContinue()) { /*вызывает удобную статическую функцию QFileDialog::getOpenFileName() для получения от пользователя нового имени файла. Эта функция выводит на экран диалоговое окно для выбора пользователем файла и возвращает имя файла - или пустую строку при нажатии пользователем клавиши Cancel*/ QString fileName = QFileDialog::getOpenFileName(this, tr("Открыть"), ".", tr("Файл электронной таблицы (*.sp)")); if (!fileName.isEmpty()) loadFile(fileName); } }
В первом аргументе функции QFileDialog::getOpenFileName() задается родительский виджет. Взаимодействие родительских и дочерних объектов для диалоговых окон и для других виджетов будет различно. Диалоговое окно всегда является самостоятельным окном, однако если у него имеется родитель, то оно размещается по умолчанию в верхней части родительского объекта. Кроме того, дочернее диалоговое окно использует панель задач родительского объекта. Во втором аргументе задается название диалогового окна. В третьем аргументе задается каталог начала просмотра файлов; в нашем случае это будет текущий каталог. Четвертый аргумент определяет фильтры файлов. Фильтр файла состоит из описательной части и образца поиска. Если допустить поддержку не только родного формата файлов приложения Электронная таблица, а также формата файлов с запятой в качестве разделителя и файлов Lotus 1-2-3, нам пришлось бы инициализировать переменные следующим образом:
tr("Spreadsheet files (*.sp)\n" "Comma-separated values files (*.csv)\n" "Lotus 1-2-3 files (*.wk1 *.wks)")
Закрытая функция loadFile() вызвана в open() для загрузки файла. Мы делаем эту функцию независимой, поскольку нам потребуется выполнить те же действия для загрузки файлов, которые открывались недавно:
bool MainWindow::loadFile(const QString &fileName) { /*Мы используем функцию Spreadsheet::readFile() для чтения файла с диска*/ if (!spreadsheet->readFile(fileName)) { statusBar()->showMessage(tr("Загрузка отменена"), 2000); return false; } /*Если загрузка завершилась успешно, мы вызываем функцию setCurrentFile() для обновления заголовка окна, в противном случае функция Spreadsheet::readFile() уведомит пользователя о возникшей проблеме, выдав соответствующее сообщение. В целом полезно предусматривать выдачу сообщений об ошибках в компонентах низкого уровня, поскольку они могут обеспечить получение точной информации о причинах ошибки. В обоих случаях мы будем выдавать сообщение в строке состояния в течение двух секунд (2000 миллисекунд) для того, чтобы пользователь знал о выполняемых приложением действиях*/ setCurrentFile(fileName); statusBar()->showMessage(tr("Файл загружен"), 2000); return true; }
Слот save() соответствует пункту меню Файл/Сохранить.
bool MainWindow::save() { if (curFile.isEmpty()) { return saveAs(); } else { return saveFile(curFile); } }
Если файл уже имеет имя, потому что уже открывался до этого или уже сохранялся, слот save() вызывает saveFile(), задавая это имя, в противном случае он просто вызывает saveAs():
bool MainWindow::saveFile(const QString &fileName) { if (!spreadsheet->writeFile(fileName)) { statusBar()->showMessage(tr("Сохранение отменено"), 2000); return false; } setCurrentFile(fileName); statusBar()->showMessage(tr("Файл сохранен"), 2000); return true; }
Слот saveAs() соответствует пункту меню Файл/Сохранить как.
bool MainWindow::saveAs()
{
/*Мы вызываем QFileDialog:: getSaveFileName() для получения имени
файла от пользователя. Если пользователь нажимает кнопку Cancel, мы возвращаем
значение false, которое передается дальше вплоть до вызвавшей функции (save()
или okToContinue()). Если файл с данным именем уже существует, функция
getSaveFileName() попросит пользователя подтвердить его перезапись. Такое поведение
можно предотвратить, передавая функции getSaveFileName() дополнительный аргумент
QFileDialog::DontConfirmOverwrite*/
QString fileName = QFileDialog::getSaveFileName(this,
tr("Сохранить как"), ".",
tr("Файл электронной таблицы (*.sp)"));
if (fileName.isEmpty())
return false;
return saveFile(fileName);
}
Когда пользователь выбирает пункт меню Файл/Выход или щелкает по кнопке Закрыть заголовка окна, вызывается слот QWidget::close(). В результате будет сгенерировано событие виджета "close". Переопределяя функцию QWidget::closeEvent(), мы можем перехватывать команды по закрытию главного окна и принимать решения относительно возможности его фактического закрытия.
void MainWindow::closeEvent(QCloseEvent *event) { if (okToContinue()) { writeSettings(); event->accept(); } else { event->ignore(); } }
Если изменения не сохранены и пользователь нажимает кнопку Cancel, мы "игнорируем" это событие, и оно никак не повлияет на окно. В обычном случае мы реагируем на это событие, и в результате Qt закроет окно. Мы вызываем также закрытую функцию writeSettings() для сохранения текущих настроек приложения.
Когда закрывается последнее окно, приложение завершает работу. При необходимости мы можем отменить такой режим работы, устанавливая свойство quitOnLastWindowClosed класса QApplication на значение false, и в результате приложение продолжит выполняться до тех пор, пока мы не вызовем функцию QApplication::quit().
В функции setCurrentFile() мы задаем значение закрытой переменной curFile, в которой содержится имя редактируемого файла.
void MainWindow::setCurrentFile(const QString &fileName) { curFile = fileName; setWindowModified(false); /*Перед тем как отобразить имя файла в заголовке, мы убираем путь к файлу с помощью функции strippedName(), чтобы имя файла выглядело более привлекательно*/ QString shownName = tr("Безымянный"); if (!curFile.isEmpty()) { shownName = strippedName(curFile); /*Если задано имя файла, мы обновляем recentFiles - список имен файлов, которые открывались в приложении недавно. Мы вызываем функцию removeAll() для удаления всех файлов с этим именем из списка, чтобы избежать дублирования*/ recentFiles.removeAll(curFile); /*Вызываем функцию prepend() для помещения имени данного файла в начало списка*/ recentFiles.prepend(curFile); /*После обновления списка имен файлов мы вызываем функцию updateRecentFileActions() для обновления пунктов меню File*/ updateRecentFileActions(); } /*Каждый QWidget имеет свойство windowModified, которое должно быть установлено на значение true, если документ окна содержит несохраненные изменения, и на значение false в противном случае. В системе Mac OS X несохраненные документы отмечаются точкой на кнопке закрытия, расположенной в заголовке окна, в других системах такие документы отмечаются звездочкой в конце имени файла. Все это обеспечивается в Qt автоматически, если мы своевременно обновляем свойство windowModified и помещаем маркер "[*]" в заголовок окна по мере необходимости. В функцию setWindowTitle() мы передали следующий текст:*/ setWindowTitle(tr("%1[*] - %2").arg(shownName) .arg(tr("Электронная таблица"))); /*Функция QString::arg() заменяет своим аргументом параметр "%n" с наименьшим номером и возвращает параметр "%n" с аргументом и полученную строку. В нашем случае arg() имеет два параметра "%n". При первом вызове функция аrg() заменяет параметр "%1"; второй вызов заменяет "%2". Если файл имеет имя "budget.sp" и файл перевода не загружен, мы получим строку "budget.sp[*] - Электронная таблица". Применение arg() облегчает перевод сообщения на другие языки*/ } QString MainWindow::strippedName(const QString &fullFileName) { return QFileInfo(fullFileName).fileName(); } void MainWindow::updateRecentFileActions() { /*Сначала мы удаляем все файлы, которые больше не существуют, используя итератор в стиле Java. Некоторые файлы могли использоваться в предыдущем сеансе, но с этого момента их уже не будет*/ QMutableStringListIterator i(recentFiles); while (i.hasNext()) { if (!QFile::exists(i.next())) i.remove(); } /*Проходим по списку файла, на этот раз пользуясь индексацией массива. Для каждого файла мы создаем строку из амперсанда, номера файла (j + 1), пробела и имени файла (без пути).*/ for (int j = 0; j < MaxRecentFiles; ++j) { if (j < recentFiles.count()) { QString text = tr("&%1 %2") .arg(j + 1) .arg(strippedName(recentFiles[j])); recentFileActions[j]->setText(text); /*С каждым пунктом меню recentFileActions может быть связан элемент данных "data" типа QVariant. Тип QVariant может хранить многие типы C++ и Qt. Здесь в элементе меню "data" мы храним полное имя файла, чтобы можно было позже его легко найти*/ recentFileActions[j]->setData(recentFiles[j]); /*Делаем этот пункт меню видимым*/ recentFileActions[j]->setVisible(true); } else { /*Если пунктов меню (массив recentFileActions) больше, чем недавно открытых файлов (массив recentFiles), мы просто не отображаем дополнительные пункты*/ recentFileActions[j]->setVisible(false); } } /*Если существует по крайней мере один недавно используемый файл, мы делаем разделитель видимым*/ separatorAction->setVisible(!recentFiles.isEmpty()); }
На рис. 2 показано соответствие между массивом recentFileActions и получающимся меню.
Рис.2. Меню File со списком файлов, которые открывались недавно
При выборе пользователем какого-нибудь недавно используемого файла вызывается слот openRecentFile().
void MainWindow::openRecentFile()
{
/*Функция okToContinue() используется в том случае, когда имеются несохраненные
изменения, и если пользователь не отменил сохранение изменений, мы определяем,
какой конкретный пункт меню вызвал слот, используя функцию QObject::sender().
Функция qobject_cast<T>() выполняет динамическое приведение типов на основе
метаинформации, сгенерированной moc-компилятором метаобъектов Qt. Она возвращает
указатель на запрошенный подкласс QObject или 0, если нельзя объект привести
к данному типу. В отличие от функции dynamic_cast<T>() стандартного C++ функция
Qt qobject_cast<T>() работает правильно за пределами динамической библиотеки.
В нашем примере мы используем qobject_cast<T>() для приведения указателя
QObject в указатель QAction. Если приведение удачно (а оно должно быть удачным),
мы вызываем функцию loadFile(), задавая полное имя файла, которое мы извлекаем
из элемента данных пункта меню. Поскольку мы знаем, что слот вызывался объектом
QAction, в данном случае программа все же правильно сработала бы при использовании
функции static_cast<T>() или при традиционном приведении С-типов.*/
if (okToContinue()) {
QAction *action = qobject_cast<QAction *>(sender());
if (action)
loadFile(action->data().toString());
}
}
На следующем шаге рассмотрим применение ранее спроектированных диалоговых окон.