3.3. Реализация меню "File".

В этом разделе мы рассмотрим реализацию всех слотов меню "File".

void MainWindow::newFile() { if (maybeSave()) { spreadsheet->clear(); setCurrentFile(""); } } Слот newFile() вызывается, когда пользователь выбирает пункт меню "File|New" или щелкает по кнопке "New" на панели инструментов. Функция maybeSave() спрашивает пользователя: "Do you want to save your changes?" ("Желаете ли сохранить изменения?"), если файл был изменен. Она возвращает true, если пользователь ответил "Yes" или "No" (в случае ответа "Yes" -- файл сохраняется), и false -- если пользователь нажал на кнопку "Cancel" ("Отмена"). Приватная функция setCurrentFile() обновляет заголовок окна программы, показывая, что редактируется неозаглавленный документ.

Рисунок 3.8. Запрос: "Do you want to save your changes?"


bool MainWindow::maybeSave() { if (modified) { int ret = QMessageBox::warning(this, tr("Spreadsheet"), tr("The document has been modified.\n" "Do you want to save your changes?"), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No, QMessageBox::Cancel | QMessageBox::Escape); if (ret == QMessageBox::Yes) return save(); else if (ret == QMessageBox::Cancel) return false; } return true; } Функция maybeSave() выводит перед пользователем диалоговое окно с запросом (см. рис. 3.8). Диалог имеет три кнопки -- три варианта ответа: "Yes", "No" и "Cancel". Модификатор QMessageBox::Default назначает кнопку "Yes" -- кнопкой по-умолчанию. Модификатор QMessageBox::Escape связывает кнопку "No" с клавишей Esc.

Вызов QMessageBox::warning() может показаться на первый взгляд немного не понятным. Синтаксис этого метода:

QMessageBox::warning(parent, caption, messageText, button0, button1, ...); Класс QMessageBox имеет еще ряд аналогичных методов: information(), question() и critical(), Все они отображают диалоговое окно с различными иконками.

Information


Question


Warning


Critical


Рисунок 3.9. Иконки диалога запроса. void MainWindow::open() { if (maybeSave()) { QString fileName = QFileDialog::getOpenFileName(".", fileFilters, this); if (!fileName.isEmpty()) loadFile(fileName); } } Слот open() соответствует пункту меню "File|Open". Аналогично слоту newFile() -- сначала вызывается функция maybeSave(), чтобы сохранить имеющиеся изменения. Затем, с помощью функции QFileDialog::getOpenFileName(), у пользователя запрашивается имя открываемого файла. Она выводит перед пользователем диалоговое окно, которое предлагает выбрать требуемый файл и возвращает программе его имя или пустую строку, если пользователь отменил операцию открытия файла.

Функции getOpenFileName() передаются три аргумента. Первый аргумент -- это каталог, где может находиться файл, в нашем случае -- это текущий каталог. Второй аргумент -- fileFilters, задет фильтр имен файлов. Фильтр состоит из двух частей -- текста описания и шаблона. В конструкторе MainWindow фильтр был инициализирован так:

fileFilters = tr("Spreadsheet files (*.sp)"); Если бы наша программа дополнительно поддерживала файлы форматов CSV и Lotus 1-2-3, то фильтр имен файлов мог бы быть инициализирован следующим образом: fileFilters = tr("Spreadsheet files (*.sp)\n" "Comma-separated values files (*.csv)\n" "Lotus 1-2-3 files (*.wk?)"); И наконец третий аргумент указывает, что окно диалога является подчиненным, по отношению к главному окну приложения.

Для диалоговых окон, отношение "владелец-подчиненный", носит иной смысл, чем для виджетов. Диалоговое окно всегда отображает поверх других окон, но если оно имеет владельца, то центрируется относительно него. Этот же диалог вызывается по нажатии на кнопку "Open", на панели инструментов.

void MainWindow::loadFile(const QString &fileName) { if (spreadsheet->readFile(fileName)) { setCurrentFile(fileName); statusBar()->message(tr("File loaded"), 2000); } else { statusBar()->message(tr("Loading canceled"), 2000); } } Функция loadFile() вызывается из open() для загрузки файла. Мы вынесли операцию загрузки файла в отдельную функцию, потому что она потребуется нам при реализации слота, открывающего недавно использовавшиеся файлы.

Непосредственное чтение файла с диска выполняется в функции Spreadsheet::readFile(). Если чтение прошло без ошибок, то вызывается setCurrentFile(), чтобы обновить заголовок окна. В противном случае readFile() выведет окно с сообщением об ошибке. Обычно, считается хорошей практикой давать возможность низкоуровневым компонентам выводить свои сообщения, поскольку в этом случае диагностика ошибок может быть выполнена более точно.

В обоих случаях, в строку состояния выводится сообщение, которое демонстрируется 2000 миллисекунд (2 секунды).

bool MainWindow::save() { if (curFile.isEmpty()) { return saveAs(); } else { saveFile(curFile); return true; } } void MainWindow::saveFile(const QString &fileName) { if (spreadsheet->writeFile(fileName)) { setCurrentFile(fileName); statusBar()->message(tr("File saved"), 2000); } else { statusBar()->message(tr("Saving canceled"), 2000); } } Слот save() соответствует пункту меню "File|Save". Если файлу ранее уже было назначено имя, то он сохраняется вызовом saveFile(), иначе вызывается saveAs(). bool MainWindow::saveAs() { QString fileName = QFileDialog::getSaveFileName(".", fileFilters, this); if (fileName.isEmpty()) return false; if (QFile::exists(fileName)) { int ret = QMessageBox::warning(this, tr("Spreadsheet"), tr("File %1 already exists.\n" "Do you want to overwrite it?") .arg(QDir::convertSeparators(fileName)), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape); if (ret == QMessageBox::No) return true; } if (!fileName.isEmpty()) saveFile(fileName); return true; } Слот saveAs() соответствует пункту меню "File|Save As". Он запрашивает у пользователя имя сохраняемого файла, вызовом QFileDialog::getSaveFileName(). Если пользователь нажмет кнопку "Cancel", то возвращается значение false, которое затем передается выше, функцией maybeSave(). Иначе возвращается имя файла, которое может быть как новым именем, так и именем существующего файла. В последнем случае перед пользователем демонстрируется предупреждение:

Рисунок 3.10. Запрос: "Do you want to overwrite it?"


Диалогу передается текст:

tr("File %1 already exists\n" "Do you want to override it?") .arg(QDir::convertSeparators(fileName)) где функция QString::arg() выполняет подстановку спецификатора "%1" своим аргументом. Например, если предположить, что имя файла A:\tab04.sp, то вышеприведенный код будет полностью эквивалентен следующему: "File A:\\tab04.sp already exists.\n" "Do you want to override it?" есстественно, если исходить из предположения, что приложение не было переведено на какой либо другой язык. Функция QDir::convertSeparators() выполняет преобразование платформо-зависимых разделителей элементов пути в файловой системе ("/" -- для Unix и Mac OS X, "\" -- для Windows) в символ прямого слэша. void MainWindow::closeEvent(QCloseEvent *event) { if (maybeSave()) { writeSettings(); event->accept(); } else { event->ignore(); } } Когда пользователь выбирает пункт меню "File|Exit" или закрывает приложение нажатием на кнопку "X" в заголовке окна, то вызывается слот QWidget::close(). Он передает приложению событие "close". Перекрыв функцию QWidget::closeEvent(), мы можем предотвратить закрытие окна и решить -- что делать дальше.

Если в приложении имеются несохраненные данные и пользователь отменил операцию сохранения файла, то мы просто "игнорируем" событие и продолжаем работу. В противном случае -- мы подтверждаем закрытие приложения и приложение завершает свою работу.

void MainWindow::setCurrentFile(const QString &fileName) { curFile = fileName; modLabel->clear(); modified = false; if (curFile.isEmpty()) { setCaption(tr("Spreadsheet")); } else { setCaption(tr("%1 - %2").arg(strippedName(curFile)) .arg(tr("Spreadsheet"))); recentFiles.remove(curFile); recentFiles.push_front(curFile); updateRecentFileItems(); } } QString MainWindow::strippedName(const QString &fullFileName) { return QFileInfo(fullFileName).fileName(); } В функции setCurrentFile() мы записываем имя файла в приватную переменную-член curFile, сбрасываем признак "изменен" и обновляем заголовок окна. Обратите внимание: теперь мы использовали два спецификатора, вида "%n". Подстановкой первого ("%1") занимается первый вызов arg(), второго ("%2") -- второй вызов. Такую форму записи можно несколько упростить: setCaption(strippedName(curFile) + tr(" - Spreadsheet")); но использование arg() дает большую гибкость переводчикам. Чтобы не загромождать заголовок окна длинной строкой, мы удалили из нее путь к файлу с помощью функции strippedName().

Затем обновляется список файлов recentFiles, использовавшихся недавно. Для начала вызывается remove(), которая удаляет имя файла из списка, а затем push_front() добавляет имя файла в начало. Вызов remove() необходим для предотвращения появления дублирующихся записей. После обновления списка вызывается updateRecentFileItems(), которая выполняет обновление меню "File".

Переменная recentFiles имеет тип QStringList (список строк QString). В Главе 11 мы подробнее остановимся на классах-контейнерах, таких как QStringList.

На этом реализация меню "File" практически завершена. Но остается еще один момент. Необходимо выполнить реализацию слота открывающего файлы из списка недавно использовавшихся файлов.

Рисунок 3.11. Меню "File" со списком недавно использовавшихся файлов.


void MainWindow::updateRecentFileItems() { while ((int)recentFiles.size() > MaxRecentFiles) recentFiles.pop_back(); for (int i = 0; i < (int)recentFiles.size(); ++i) { QString text = tr("&%1 %2") .arg(i + 1) .arg(strippedName(recentFiles[i])); if (recentFileIds[i] == -1) { if (i == 0) fileMenu->insertSeparator(fileMenu->count() - 2); recentFileIds[i] = fileMenu->insertItem(text, this, SLOT(openRecentFile(int)), 0, -1, fileMenu->count() - 2); fileMenu->setItemParameter(recentFileIds[i], i); } else { fileMenu->changeItem(recentFileIds[i], text); } } } Функция updateRecentFileItems() вызывается для обновления элементов меню, соответствующих недавно открывавшимся файлам. Для начала удаляются все "лишние" элементы, начиная с конца списка (длина списка не может превышать числа MaxRecentFiles. которое определено в mainwindow.h и равно числу 5)

Затем в меню добавляется новый элемент или используется существующий. В самый первый раз в меню добавляется разделитель, отделяющий список файлов от остальных пунктов. Чуть ниже мы объясним назначение функции setItemParameter().

На первый взгляд кажется странным, что в функции updateRecentFileItems() мы создаем элементы меню, но никогда не удаляем их! Однако тут нет ничего странного. Мы можем смело утверждать, что в течение сессии список файлов уменьшаться не будет.

Функция QPopupMenu::insertItem() имеет следующий синтаксис:

fileMenu->insertItem(text, receiver, slot, accelerator, id, index); где text -- это текст, который будет отображаться, в данном случае мы используем имя файла без пути к нему. Можно было бы использовать полное имя файла, но это сделает панель меню слишком широкой. Если у вас возникнет необходимость сохранять в меню полный путь к файлу вместе с его именем, то рекомендуем оформлять список файлов в виде подменю.

Аргументы receiver и slot определяют функцию-обработчик, которая будет вызываться при выборе этого пункта меню. В нашем примере мы указали слот openRecentFile(int) главного окна.

В аргументах accelerator и id мы передаем значения по-умолчанию. Это означает, что данный пункт меню не имеет комбинации "горячих" клавиш, а идентификатор (id) генерируется автоматически. Мы сохраняем полученный id в массиве recentFileIds, что позднее позволит нам обращаться к пункту меню по его идентификатору.

Аргумент index -- это порядковый номер записи в меню. Значение fileMenu->count()-2, означает, что пункт меню вставляется выше разделителя, отделяющего пункт "Exit".

void MainWindow::openRecentFile(int param) { if (maybeSave()) loadFile(recentFiles[param]); } Слот openRecentFile() открывает файл, соответствующий выбранному пункту меню. В качестве аргумента param передается число, записанное нами вызовом setItemParameter(). Мы выбрали числа такими, что теперь можем использовать их как индексы в списке recentFiles.

Рисунок 3.12. Соответствие между пунктами меню и полными именами файлов.


Таким образом мы решаем проблему сопоставления пунктов меню полным именам файлов. Менее элегантный способ заключается в создании пяти "действий" (action) и соединении их с пятью различными слотами.