О ресурре... Главная
The Belarus Internet the Resource about Linux
 
   
Ж. Бланшетт, М. Саммерфильд, "Глава 3. Создание главного окна приложения" - 01/12/2004

Предыдущая глава | Содержание | Следующая глава

Глава 3. Создание главного окна приложения

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

Рисунок 3.1. Приложение -- электронная таблица.


Главное окно -- это своего рода каркас, на который "натягивается" весь пользовательский интерфейс приложения. Здесь мы рассмотрим пример создания главного окна электронной таблицы. Внешний вид нашего будущего приложения приводится на рисунке 3.1. В этом приложении мы будем использовать диалоги "Find", "Go-to-Cell" и "Sort", которые были созданы нами в Главе 2.

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


3.1. Создание класса-наследника от QMainWindow.

Главное окно любого приложения -- это класс-наследник QMainWindow. Большинство приемов, используемых при создания диалогов и о которых мы говорили в Главе 2, вполне применимы и при создании главного окна приложения.

Главное окно может быть создано в Qt Designer, но мы все будем делать "вручную", чтобы продемонстрировать процесс создания главного окна во всех деталях. Если вы предпочитаете визуальное проектирование -- прочитайте главу "Creating a Main Window Application" в справочном руководстве к Qt Designer.

Исходные тексты главного окна будут располагаться в двух файлах: mainwindow.cpp и mainwindow.cpp. Начнем с файла заголовка:

#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 
#include <qmainwindow.h> 
#include <qstringlist.h> 
class QAction; 
class QLabel; 
class FindDialog; 
class Spreadsheet; 

class MainWindow : public QMainWindow 
{ 
  Q_OBJECT 
public: 
  MainWindow(QWidget *parent = 0, const char *name = 0); 

protected: 
  void closeEvent(QCloseEvent *event); 
  void contextMenuEvent(QContextMenuEvent *event);      
      
Это определение класса MainWindow -- наследника QMainWindow. Оно содержит макрос Q_OBJECT, поскольку реализует свои собственные сигналы и слоты.

Функция closeEvent(), в классе QWidget, объявлена как виртуальная. Она автоматически вызывается перед завершением приложения. Мы перекрываем ее в MainWindow для того, чтобы иметь возможность спросить у пользователя -- желает ли он сохранить произведенные изменения, а также для того, чтобы сохранить на диск пользовательские настройки.

Аналогично, функция contextMenuEvent() вызывается, когда пользователь щелкает правой кнопкой мыши по виджету. Мы перекрываем ее в MainWindow для того, чтобы вывести контекстное меню.

private slots: 
  void newFile();
  void open(); 
  bool save(); 
  bool saveAs(); 
  void find(); 
  void goToCell(); 
  void sort(); 
  void about();     
      
Реализация действий некоторых пунктов меню, таких как File|New и Help|About, выполнена в виде приватных слотов MainWindow. Большинство слотов имеют тип void, но слоты save() и saveAs возвращают результат типа bool. Значение, возвращаемое слотом, игнорируется в случае вызова по сигналу, но когда слот вызывается как обычная функция, то мы получаем от него возвращаемое значение, которое можем использовать для своих нужд.
  void updateCellIndicators(); 
  void spreadsheetModified(); 
  void openRecentFile(int param); 

private: 
  void createActions(); 
  void createMenus(); 
  void createToolBars(); 
  void createStatusBar(); 
  void readSettings(); 
  void writeSettings(); 
  bool maybeSave(); 
  void loadFile(const QString &fileName); 
  void saveFile(const QString &fileName); 
  void setCurrentFile(const QString &fileName); 
  void updateRecentFileItems(); 
  QString strippedName(const QString &fullFileName);      
      
Дополнительные приватные функции, необходимые для обслуживания пользовательского интерфейса.
  Spreadsheet *spreadsheet; 
  FindDialog *findDialog; 
  QLabel *locationLabel; 
  QLabel *formulaLabel; 
  QLabel *modLabel; 
  QStringList recentFiles; 
  QString curFile; 
  QString fileFilters; 
  bool modified; 
  
  enum { MaxRecentFiles = 5 }; 
  int recentFileIds[MaxRecentFiles]; 
  
  QPopupMenu *fileMenu; 
  QPopupMenu *editMenu; 
  QPopupMenu *selectSubMenu; 
  QPopupMenu *toolsMenu; 
  QPopupMenu *optionsMenu; 
  QPopupMenu *helpMenu; 
  QToolBar *fileToolBar; 
  QToolBar *editToolBar;
  QAction *newAct; 
  QAction *openAct; 
  QAction *saveAct; 
  ... 
  QAction *aboutAct; 
  QAction *aboutQtAct; 
}; 
#endif      
      
Кроме функций, класс главного окна имеет ряд скрытых переменных. Все они будут описаны по мере необходимости.

Теперь перейдем к реализации:

#include <qaction.h> 
#include <qapplication.h> 
#include <qcombobox.h> 
#include <qfiledialog.h> 
#include <qlabel.h> 
#include <qlineedit.h> 
#include <qmenubar.h> 
#include <qmessagebox.h> 
#include <qpopupmenu.h> 
#include <qsettings.h> 
#include <qstatusbar.h> 

#include "cell.h" 
#include "finddialog.h" 
#include "gotocelldialog.h" 
#include "mainwindow.h" 
#include "sortdialog.h" 
#include "spreadsheet.h"      
      
Здесь подключаются заголовки всех классов Qt, которые используются в приложении, а также заголовок класса главного окна и ряд других заголовочных файлов, таких как finddialog.h, gotocelldialog.h и sortdialog.h, которые мы создали в предыдущей главе.
  MainWindow::MainWindow(QWidget *parent, const char *name) 
    : QMainWindow(parent, name) 
  { 
    spreadsheet = new Spreadsheet(this); 
    setCentralWidget(spreadsheet); 
    createActions(); 
    createMenus(); 
    createToolBars(); 
    createStatusBar(); 
    readSettings(); 
    setCaption(tr("Spreadsheet")); 
    setIcon(QPixmap::fromMimeSource("icon.png")); 
    findDialog = 0; 
    fileFilters = tr("Spreadsheet files (*.sp)"); 
    modified = false; 
  }      
      
Конструктор начинается с создания виджета Spreadsheet, который будет центральным виджетом главного окна. Центральный виджет занимает все пространство, находящееся между панелью инструментов (toolbar) и строкой состояния (statusbar). Класс Spreadsheet является потомком класса QTable и добавляет некоторые свойства, характерные для электронных таблиц. Среди них можно назвать поддержку формул, которая будет реализована в Главе 4.

Рисунок 3.2. Раскладка виджетов в главном окне.


Далее вызываются приватные функции createActions(), createMenus(), createToolBars() и createStatusBar(), которые создают остальную часть главного окна. Для восстановления пользовательских настроек вызывается функция readSettings().

В качестве иконки приложения устанавливается icon.png. Qt поддерживает различные форматы графических файлов, включая BMP, GIF [6] , JPEG, MNG, PNG, PNM, XBM и XPM. Вызов QWidget::setIcon() выводит иконку в верхний левый угол окна. К сожалению, отсутствует платформо-независимый способ помещения иконки на рабочий стол.

Программы с графическим интерфейсом обычно используют достаточно большое число изображений. Qt, в свою очередь, предоставляет значительное число методов работы с изображениями в приложениях. Наиболее употребимые из них:

В данном примере мы будем использовать механизм "коллекции изображений", поскольку работать с ним намного проще, чем загружать файлы в процессе исполнения. К тому же он может взаимодействовать со всеми, поддерживаемыми библиотекой, графическими форматами. Все изображения мы будем хранить в каталоге images. Чтобы создать файл на языке C++ (он будет создан утилитой uic), который будет хранить наши изображения, добавим следующие строки в файл .pro:

IMAGES = images/icon.png \ 
         images/new.png \ 
         images/open.png \ 
         ... 
         images/find.png \ 
         images/gotocell.png      
      
Изображения будут помещены в исполняемый файл приложения и могут быть получены вызовом QPixmap::fromMimeSource(). Преимущество такого способа организации хранения изображений заключается в том, что они никогда не потеряются, поскольку находятся внутри исполняемого файла.

Если главное окно создается в Qt Designer, то вы можете использовать визуальные средства, предоставляемые построителем, для вставки изображений в коллекцию.


3.2. Создание меню и панелей инструментов.

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

Qt значительно упрощает разработку меню и панелей инструментов за счет использования концепции "действия" (action). Action -- это элемент, который может быть добавлен в меню и/или на панель инструментов. Для создания меню и панели инструментов необходимо выполнить следующие шаги:

В нашем приложении все необходимое выполняет функция createActions():
void MainWindow::createActions() 
{ 
  newAct = new QAction(tr("&New"), tr("Ctrl+N"), this); 
  newAct->setIconSet(QPixmap::fromMimeSource("new.png")); 
  newAct->setStatusTip(tr("Create a new spreadsheet file")); 
  connect(newAct, SIGNAL(activated()), this, SLOT(newFile()));      
      
В данном случае создается новое "действие" с названием "New", горячей комбинацией клавиш Ctrl+N и с владельцем -- главным окном приложения. Затем к "действию" прицепляются иконка (new.png) и текст подсказки, который будет выводиться в строке состояния. В заключение -- сигнал activated() подключается к слоту главного окна newFile(), который будет описан в следующем разделе. Без этого соединения, при выборе пункта меню "File|New" или при нажатии на кнопку "New" (в панели инструментов), ничего происходить не будет.

Аналогичным образом создаются все остальные "действия" (action).

Рисунок 3.3. Меню приложения Spreadsheet.


Команда (action) "Show Grid", в меню "Options" реализуется несколько иначе:

  showGridAct = new QAction(tr("&Show Grid"), 0, this); 
  showGridAct->setToggleAction(true); 
  showGridAct->setOn(spreadsheet->showGrid()); 
  showGridAct->setStatusTip(tr("Show or hide the spreadsheet s " "grid")); 
  connect(showGridAct, SIGNAL(toggled(bool)), spreadsheet, SLOT(setShowGrid(bool)));      
      
Эта команда имеет два фиксированных состояния -- включено-выключено. В меню рядом с ней отображается галочка (когда включено), а на панели инструментов она выглядит как кнопка с фиксацией. Когда "действие" включено, то компонент Spreadsheet отображается в окне приложения. Мы инициализируем "действие" значением по-умолчанию -- включено. Таким образом выполняется начальная синхронизация "действия" с фактическим режимом отображения компонента Spreadsheet. Затем мы подключаем сигнал toggled(bool) к слоту setShowGrid(bool), компонента Spreadsheet. После этого "действие" (action) добавляется в меню или на панель инструментов. Теперь пользователь может "включать" и "выключать" таблицу.

Команды "Show Grid" и "Auto-recalculate" -- являются "действиями" с независимой фиксацией. Однако, QAction имеет наследника -- QActionGroup, с помощью которого можно создавать группы "действий" с зависимой фиксацией.

Рисунок 3.4. About Qt.


  aboutQtAct = new QAction(tr("About &Qt"), 0, this); 
  aboutQtAct->setStatusTip(tr("Show the Qt library's About box")); 
  connect(aboutQtAct, SIGNAL(activated()), qApp, SLOT(aboutQt())); 
}    
    

Для вызова диалога "AboutQt" используется слот aboutQt() глобальной переменной qApp -- экземпляр класса QApplication.

После создания всех "действий" (action) мы можем разместить их в меню:

void MainWindow::createMenus() 
{ 
  fileMenu = new QPopupMenu(this); 
  newAct->addTo(fileMenu); 
  openAct->addTo(fileMenu); 
  saveAct->addTo(fileMenu); 
  saveAsAct->addTo(fileMenu); 
  fileMenu->insertSeparator(); 
  exitAct->addTo(fileMenu); 
  
  for (int i = 0; i < MaxRecentFiles; ++i) 
    recentFileIds[i] = -1;      
      
В Qt все меню являются экземплярами QPopupMenu. Мы создали меню "File" и затем добавили в него пункты "New", "Open", "Save", "Save As" и "Exit". Перед пунктом "Exit" добавлен разделитель, чтобы визуально отделить его от остальных. Цикл for инициализирует recentFileIds -- массив файлов, открывавшихся недавно. Мы еще вернемся к этому массиву, когда приступим к рассмотрению реализации слотов меню "File" в следующем разделе.
  editMenu = new QPopupMenu(this); 
  cutAct->addTo(editMenu); 
  copyAct->addTo(editMenu); 
  pasteAct->addTo(editMenu); 
  deleteAct->addTo(editMenu); 
  
  selectSubMenu = new QPopupMenu(this); 
  selectRowAct->addTo(selectSubMenu); 
  selectColumnAct->addTo(selectSubMenu); 
  selectAllAct->addTo(selectSubMenu); 
  editMenu->insertItem(tr("&Select"), selectSubMenu); 
  
  editMenu->insertSeparator(); 
  findAct->addTo(editMenu); 
  goToCellAct->addTo(editMenu);      
      
Меню "Edit" включает в себя подменю, которое так же является экземпляром класса QPopupMenu. Мы просто создаем подменю и вставляем его в то место меню "Edit", где оно должно находиться.
  toolsMenu = new QPopupMenu(this); 
  recalculateAct->addTo(toolsMenu); 
  sortAct->addTo(toolsMenu); 
  
  optionsMenu = new QPopupMenu(this); 
  showGridAct->addTo(optionsMenu);      
  autoRecalcAct->addTo(optionsMenu); 
  
  helpMenu = new QPopupMenu(this); 
  aboutAct->addTo(helpMenu); 
  aboutQtAct->addTo(helpMenu); 
  
  menuBar()->insertItem(tr("&File"), fileMenu); 
  menuBar()->insertItem(tr("&Edit"), editMenu); 
  menuBar()->insertItem(tr("&Tools"), toolsMenu); 
  menuBar()->insertItem(tr("&Options"), optionsMenu); 
  menuBar()->insertSeparator(); 
  menuBar()->insertItem(tr("&Help"), helpMenu); 
}  
      
Меню "Tools", "Options" и "Help" создаются аналогичным образом и в конце, все созданные меню вставляются в полосу меню, в верхней части главного окна приложения. Функция QMainWindow::menuBar() возвращает указатель на экземпляр класса QMenuBar, который создается автоматически, при первом вызове menuBar(). Мы добавили разделитель между меню "Options" и "Help". В случае отображения в стиле Motif и ему подобных, меню "Help" смещается в крайнее правое положение, в других стилях отображения разделитель игнорируется.

Рисунок 3.5. Полоса меню в стиле Motif и Windows.


Создание панелей инструментов происходит очень похожим образом:

void MainWindow::createToolBars() 
{ 
  fileToolBar = new QToolBar(tr("File"), this); 
  newAct->addTo(fileToolBar); 
  openAct->addTo(fileToolBar); 
  saveAct->addTo(fileToolBar); 
  
  editToolBar = new QToolBar(tr("Edit"), this); 
  cutAct->addTo(editToolBar); 
  copyAct->addTo(editToolBar); 
  pasteAct->addTo(editToolBar); 
  editToolBar->addSeparator(); 
  findAct->addTo(editToolBar); 
  goToCellAct->addTo(editToolBar); 
}      
      
Мы создали две панели инструментов -- "File" и "Edit". Как и меню, панели инструментов могут включать в себя разделители.

Рисунок 3.6. Панели инструментов приложения Spreadsheet.

Теперь, когда мы закончили создание главного меню и панелей инструментов, добавим контекстное меню.

void MainWindow::contextMenuEvent(QContextMenuEvent *event) 
{ 
  QPopupMenu contextMenu(this); 
  cutAct->addTo(&contextMenu); 
  copyAct->addTo(&contextMenu); 
  pasteAct->addTo(&contextMenu); 
  contextMenu.exec(event->globalPos()); 
}      
      
Когда пользователь щелкает правой кнопкой мыши, то виджету посылается событие (event) "контекстное меню". Перекрывая метод QWidget::contextMenuEvent(), мы можем перехватить это событие и показать контекстное меню в позиции курсора мыши.

Рисунок 3.5. Контекстное меню приложения Spreadsheet.


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

Отлавливать событие мы будем в MainWindow потому, что здесь реализуются все наши "действия" (actions). Однако, это событие можно поймать и в виджете Spreadsheet. Когда пользователь щелкнет правой кнопкой мыши по виджету, то этот виджет первым получит событие. Если виджет перекроет реализацию функции contextMenuEvent() и обработает событие, то дальше передаваться оно не будет. В противном случае оно будет передано дальше -- владельцу виджета (MainWindow). Более подробно события будут рассматриваться в Главе 7.

Функция-обработчик события вызова контекстного меню отличается от всего, что мы до сих пор видели, поскольку она создает экземпляр QPopupMenu, размещая его на стеке. Хотя, в принципе, можно было бы создать/удалить этот виджет и с помощью операторов new/delete:

  QPopupMenu *contextMenu = new QPopupMenu(this); 
  cutAct->addTo(contextMenu); 
  copyAct->addTo(contextMenu); 
  pasteAct->addTo(contextMenu); 
  contextMenu->exec(event->globalPos()); 
  delete contextMenu;      
      
Еще один примечательный аспект -- это функция exec(). Она выводит меню на экран, в заданную позицию, и ожидает, пока пользователь не сделает свой выбор, после чего управление возвращается в точку вызова. С этого момента экземпляр QPopupMenu нам больше не нужен, поэтому он удаляется. В случае размещения на стеке -- он будет уничтожен автоматически, по завершении работы функции.

Часть интерфейса, касающуюся меню и панелей инструментов, можно считать завершенной. В следующем разделе мы рассмотрим реализацию слотов меню "File".


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) и соединении их с пятью различными слотами.


3.4. Настройка строки состояния.

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

Обычный режим.


Подсказка


Сообщение


Рисунок 3.13. Строка состояния.

Создается и настраивается строка состояния в функции createStatusBar(), которая вызывается из конструктора MainWindow:

void MainWindow::createStatusBar() 
{ 
  locationLabel = new QLabel(" W999 ", this); 
  locationLabel->setAlignment(AlignHCenter); 
  locationLabel->setMinimumSize(locationLabel->sizeHint()); 
  
  formulaLabel = new QLabel(this); 
  
  modLabel = new QLabel(tr(" MOD "), this); 
  modLabel->setAlignment(AlignHCenter); 
  modLabel->setMinimumSize(modLabel->sizeHint());      
  modLabel->clear(); 
  
  statusBar()->addWidget(locationLabel); 
  statusBar()->addWidget(formulaLabel, 1); 
  statusBar()->addWidget(modLabel); 
  
  connect(spreadsheet, SIGNAL(currentChanged(int, int)), 
          this, SLOT(updateCellIndicators())); 
  connect(spreadsheet, SIGNAL(modified()), 
          this, SLOT(spreadsheetModified())); 
  updateCellIndicators(); 
}
      
Она возвращает указатель на созданный ею компонент - строку состояния. (Компонент строки состояния создается автоматически, при первом вызове функции statusBar().) Индикаторы -- это простые метки (QLabel), текст в которых изменяется по мере необходимости. Во время создания, меткам передается указатель на "владельца" (this), хотя в этом нет особой необходимости, поскольку QStatusBar::addWidget() "переподчиняет" их автоматически, назначая в качестве "владельца" сам компонент строки состояния.

Из рисунка 3.13 видно, что метки имеют различные размеры. Адрес ячейки и индикатор MOD -- самые короткие, а метка, отображающая действующую в ячейке формулу, самая длинная. Кроме того, при изменении размеров окна, все дополнительное пространство должно выделяться именно ей. Такое поведение достигается за счет указания фактора "stretch" (значение 1 в вызове QStatusBar::addWidget()). Для других двух меток этот фактор по-умолчанию принимается равным нулю, что означает фиксированный размер меток.

Когда QStatusBar размещает виджеты, он сначала выделяет место для "нерастягиваемых" компонентов (выделяя им "идеальный" размер, определяемый вызовом QWidget::sizeHint()), а затем все остальное пространство отдается "растягиваемым" виджетам. "Идеальный" размер, в свою очередь, зависит от содержимого виджетов и меняется при изменении содержимого. Задавая начальные значения меток ("W999" и "MOD"), мы тем самым определяем их минимально возможный размер.

В конце функции мы выполняем соединения между сигналами Spreadsheet к двум слотам MainWindow: updateCellIndicators() и spreadsheetModified().

void MainWindow::updateCellIndicators() 
{ 
  locationLabel->setText(spreadsheet->currentLocation()); 
  formulaLabel->setText(" " + spreadsheet->currentFormula()); 
}
      
Слот updateCellIndicator() обновляет метки, которые отображают адрес текущей ячейки и действующую в ней формулу. Он вызывается всякий раз, когда пользователь переходит из одной ячейки в другую. Кроме того, он вызывается как обычная функция, в конце createStatusBar(), для инициализации меток. Это совершенно необходимо, так как Spreadsheet не выдает сигнал currentChanged() во время инициализации.
void MainWindow::spreadsheetModified() 
{ 
  modLabel->setText(tr("MOD")); 
  modified = true; 
  updateCellIndicators(); 
}
      
Слот spreadsheetModified() обновляет все три индикатора и устанавливает признак modified в true. (Эта переменная используется для определения несохраненных изменений).


3.5. Использование диалогов.

Здесь мы расскажем о принципах работы с диалогами в Qt -- о том как они создаются, инициализируются, запускаются и как от них получить выбор, сделанный пользователем. Здесь мы будем использовать диалоги "Find", "Go-to-Cell" и "Sort", созданные нами в Главе 2. Кроме того, мы создадим простенький диалог "About" ("О программе").

Начнем с диалога "Find". Так как мы хотим, чтобы пользователь имел возможность переключаться между главным окном приложения и окном диалога, необходимо, чтобы диалоговое окно было НЕМОДАЛЬНЫМ. Немодальным называется такое окно, которое работает независимо от остальных окон приложения.

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

void MainWindow::find() 
{ 
  if (!findDialog) { 
    findDialog = new FindDialog(this); 
    connect(findDialog, SIGNAL(findNext(const QString &, bool)), 
            spreadsheet, SLOT(findNext(const QString &, bool))); 
    connect(findDialog, SIGNAL(findPrev(const QString &, bool)), 
            spreadsheet, SLOT(findPrev(const QString &, bool))); 
  } 
  
  findDialog->show(); 
  findDialog->raise(); 
  findDialog->setActiveWindow(); 
}
      
Диалог "Find" предназначен для выполнения поиска некоторого значения в таблице. Слот find() вызывается, когда пользователь выбирает пункт меню "Edit|Find", и предназначен для вывода диалогового окна на экран. С этого момента возможны три сценария дальнейшего развития событий: Если диалог не был создан ранее, то он создается и устанавливаются соединения между сигналами findNext() и findPrev() диалога, и соответствующим слотами Spreadsheet. Мы могли бы создать диалог и в конструкторе MainWindow, но не делаем этого по соображениям уменьшения времени, необходимого на запуск приложения.

Далее вызываются show(), raise() и setActiveWindow(), которые выводят окно диалога на экран, поверх других окон приложения, и активизируют его. Метод show() делает окно диалога видимым, но оно может уже присутствовать на экране -- в этом случае функция show() ничего не делает. Так как нам необходимо вывести диалог поверх других окон и активизировать его, мы должны вызвать raise() и setActiveWindow(). В качестве альтернативы можно предложить следующий код:

  if (findDialog->isHidden()) { 
    findDialog->show(); 
  } else { 
    findDialog->raise(); 
    findDialog->setActiveWindow(); 
  }     
      
но он более медлительный.

Перейдем к диалогу "Go-to-Cell". В этом случае нет необходимости переключаться между окном приложения и окном диалога. Отсюда следует, что окно диалога "Go-to-Cell" должно быть МОДАЛЬНЫМ. Модальным называется такое окно, которое блокирует возможность взаимодействия пользователя с другими окнами приложения до тех пор, пока не будет закрыто модальное окно. Все диалоги нашего приложения, за исключением "Find", будут модальными.

Немодальные диалоги вызываются при помощи функции show() (если перед этим не вызывалась функция setModal(), которая делает окно модальным). Модальные диалоги вызываются функцией exec(). Как правило, для модальных диалогов не требуется устанавливать соединения между сигналами и слотами.

void MainWindow::goToCell() 
{ 
  GoToCellDialog dialog(this); 
  if (dialog.exec()) { 
    QString str = dialog.lineEdit->text(); 
    spreadsheet->setCurrentCell(str.mid(1).toInt() - 1, 
                                str[0].upper().unicode() -  'A' ); 
  } 
}
      
Функция QDialog::exec() возвращает true, если результат диалога принимается пользователем, и false -- в противном случае. (Помните? В главе 2 мы соединяли сигнал кнопки OK со слотом accept(), а сигнал от кнопки Cancel со слотом reject().) Если пользователь нажмет кнопку OK, то мы выполним переход к заданной ячейке, если Cancel -- exec() вернет false и мы не будем ничего предпринимать.

Функция QTable::setCurrentCell() принимает два аргумента: номер строки и номер колонки. В нашем приложении, адрес A1, например, соответствует ячейке (0, 0) в таблице, а адрес B27 -- ячейке (26, 1). Чтобы получить номер строки, из QString, возвращаемой QLabel::text(), извлекается ее часть, с помощью QString::mid() и затем преобразуется в целое число с помощью QString::toInt(). После этого, из полученного числа вычитается единица (поскольку нумерация строк в QTable начинается с 0). Чтобы получить номер колонки, из кода символа колонки мы просто вычитаем код символа "A".

В отличие от диалога "Find", экземпляр диалога "Go-to-Cell" создается на стеке. Это общепринятая практика для модальных диалогов, вызываемых из разного рода меню, поскольку они становятся не нужны после их использования.

А теперь перейдем к диалогу сортировки. Этот диалог так же является модальным и позволяет отсортировать выделенный дипазон ячеек по заданным колонкам. На рисунке 3.14 показан пример сортировки по колонкам B (первичный ключ) и A (вторичный ключ) в порядке возрастания.

(а) До сортировки


(б) После сортировки


Рисунок 3.14. Сортировка выбранного диапазона ячеек.
void MainWindow::sort() 
{ 
  SortDialog dialog(this); 
  QTableSelection sel = spreadsheet->selection(); 
  dialog.setColumnRange( A  + sel.leftCol(),  A  + sel.rightCol()); 
  
  if (dialog.exec()) { 
    SpreadsheetCompare compare; 
    compare.keys[0] = 
          dialog.primaryColumnCombo->currentItem(); 
    compare.keys[1] = 
          dialog.secondaryColumnCombo->currentItem() - 1; 
    compare.keys[2] = 
          dialog.tertiaryColumnCombo->currentItem() - 1; 
    compare.ascending[0] = 
          (dialog.primaryOrderCombo->currentItem() == 0); 
    compare.ascending[1] = 
          (dialog.secondaryOrderCombo->currentItem() == 0); 
    compare.ascending[2] = 
          (dialog.tertiaryOrderCombo->currentItem() == 0); 
    spreadsheet->sort(compare); 
  } 
}
      
Алгоритм функции sort(): Объект compare хранит первичный, вторичный и третичный ключи сортировки, а так же порядок сортировки по каждому из ключей. (Определение класса SpreadsheetCompare мы опишем в следующей главе.) Этот объект используется функцией Spreadsheet::sort() для сравнения двух строк. Массив keys хранит номера колонок-ключей. Например, если выбран диапазон ячеек с C2 по E5, то колонка C имеет номер 0. Массив ascending хранит порядок сортировки для каждого из ключей. Функция QComboBox::currentItem() возвращает индекс текущего выбранного элемента списка, начиная с 0. Для вторичного и третичного ключей, из индекса вычитается 1, чтобы учесть элемент "None".

Реализация sort() очень чувствительна к дизайну диалога "Sort", точнее -- эта "чувствительность" связана с выпадающими списками и элементами списков "None". Если вы измените диалог, то скорее всего вам придется изменить и код функции. Пока этот диалог вызывается из одного места в программе -- обслуживание его не так трудоемко. Но как только вы попытаетесь вызывать диалог из разных точек в программе, то обслуживание всех изменений, вносимых в него, может превратиться в кошмарный сон.

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

void MainWindow::sort() 
{ 
  SortDialog dialog(this); 
  QTableSelection sel = spreadsheet->selection(); 
  dialog.setColumnRange( 'A' + sel.leftCol(), 'A' + sel.rightCol()); 
  if (dialog.exec()) 
    spreadsheet->performSort(dialog.comparisonObject()); 
}
      
Такой подход применяется к слабосвязанным компонентам и практически всегда оправдан в тех случаях, когда один и тот же диалог вызывается более чем из одного места в программе.

Более радикальный подход -- передать диалогу указатель на Spreadsheet и позволить ему напрямую работать с таблицей. Это несколько снижает универсальность диалога, так как он теперь будет "привязан" к определенному типу виджета, но значительно упрощает код за счет отказа от функции SortDialog::setColumnRange(). В этом случае, код функции MainWindow::sort() приобретает такой вид:

void MainWindow::sort() 
{ 
  SortDialog dialog(this); 
  dialog.setSpreadsheet(spreadsheet);      
  
  dialog.exec(); 
}  
      
Этот подход является полной противоположностью. Теперь уже не программа должна "знать" архитектуру и алгоритм работы диалога, а диалог должен "знать" об архитектуре вызывающей программы. Такой подход может оказаться оправданным, когда диалогу необходимо предоставить возможность оперативного изменения данных. Но и в этом случае код программы крайне чувствителен к реализации диалога.

Некоторые разработчики выбирают один из описанных вариантов и применяют только его. В этом есть свои преимущества, поскольку все диалоги имеют одинаковое строение, хотя при этом могут оказаться не использованными преимущества других подходов. В любом случае, решение о том, какой из подходов должен использоваться, необходимо принимать в каждом конкретном приложении, на основе требований, предъявляемых к диалогам.

В завершение этого раздела мы создадим простенький диалог -- окно, содержащее сведения о программе и разработчике. Такой диалог можно создать самому, аналогично рассмотренным ранее диалогам "Find" или "Sort", но Qt предоставляет более простое решение.

void MainWindow::about() 
{ 
  QMessageBox::about(this, tr("About Spreadsheet"), 
          tr("<h2>Spreadsheet 1.0</h2>" 
          "<p>Copyright &copy; 2003 Software Inc." 
          "<p>Spreadsheet is a small application that " 
          "demonstrates <b>QAction</b>, <b>QMainWindow</b>, " 
          "<b>QMenuBar</b>, <b>QStatusBar</b>, " 
          "<b>QToolBar</b>, and many other Qt classes.")); 
}
      
Вызывается диалог функцией QMessageBox::about(). Очень похоже на функцию QMessageBox::warning(), за одним маленьким исключением: вместо стандартной иконки "warning", используется иконка приложения.

Рисунок 3.15. Диалог с информацией о программе.


До сих пор мы использовали ряд, очень удобных в обращении, статических функций-членов из классов QMessageBox и QFileDialog. Эти функции "на лету" создают диалоги, инициализируют их и вызывают функцию exec(). Но можно, хотя это и менее удобно, самому создать QMessageBox или QFileDialog, подобно любому другому виджету, и явно вызвать exec() или даже show().


3.6. Сохранение пользовательских настроек приложения.

В конструкторе MainWindow, для загрузки пользовательских настроек, мы вызывали функцию readSettings(). Аналогично, для их сохранения, в обработчике closeEvent(), вызывалась функция writeSettings(). Пришло время рассмотреть реализацию обоих функций, которые являются методами класса MainWindow.

Для своего приложения, в качестве хранилища настроек, мы выбрали класс QSettings. Экземпляр этого класса может быть создан и использован в любой момент, по мере необходимости.

void MainWindow::writeSettings() 
{ 
  QSettings settings; 
  settings.setPath("software-inc.com", "Spreadsheet"); 
  settings.beginGroup("/Spreadsheet"); 
  settings.writeEntry("/geometry/x", x()); 
  settings.writeEntry("/geometry/y", y()); 
  settings.writeEntry("/geometry/width", width()); 
  settings.writeEntry("/geometry/height", height()); 
  settings.writeEntry("/recentFiles", recentFiles); 
  settings.writeEntry("/showGrid", showGridAct->isOn()); 
  settings.writeEntry("/autoRecalc", showGridAct->isOn()); 
  settings.endGroup(); 
}
      
Функция writeSettings() сохраняет геометрию главного окна (положение на экране и размеры), список недавно использовавшихся файлов и состояние флагов Show Grid и Auto-recalculate.

Место, куда QSettings сохраняет настройки, зависит от используемой платформы. В Windows сохранение производится в системный реестр, в Unix -- в текстовый файл, в Mac OS X используется Carbon API. Методу setPath() передаются названия организации и программного продукта. Эти сведения используются для преобразование в платформо-зависимое представление места сохранения настроек.

Настройки хранятся в виде пары: ключ-значение. Ключ очень похож на строку пути в файловой системе и всегда должен начинаться с названия приложения. Например, /Spreadsheet/geometry/x или /Spreadsheet/showGrid. (Функция beginGroup() "запоминает" префикс ключа - название приложения, которое будет автоматически подставляться в начало ключа, что позволяет нам сэкономить на своих усилиях.) Значение, той или иной настройки, может быть одного из пяти типов: int, bool, double, QString или QStringList.

void MainWindow::readSettings() 
{ 
  QSettings settings; 
  settings.setPath("software-inc.com", "Spreadsheet"); 
  settings.beginGroup("/Spreadsheet"); 
  int x = settings.readNumEntry("/geometry/x", 200); 
  int y = settings.readNumEntry("/geometry/y", 200);      
  int w = settings.readNumEntry("/geometry/width", 400); 
  int h = settings.readNumEntry("/geometry/height", 400); 
  move(x, y); 
  resize(w, h); 
  
  recentFiles = settings.readListEntry("/recentFiles"); 
  updateRecentFileItems(); 
  
  showGridAct->setOn( 
          settings.readBoolEntry("/showGrid", true)); 
  autoRecalcAct->setOn( 
          settings.readBoolEntry("/autoRecalc", true)); 
          
  settings.endGroup(); 
}
      
Функция readSettings() загружает настройки, предварительно сохраненные вызовом writeSettings(). Второй аргумент в "read"-функциях -- это значение по-умолчанию, возвращаемое в том случае, если запрошенный ключ отсутствует. Значения по-умолчанию используются на самом первом запуске приложения, когда настройки еще не были сохранены.

На этом мы заканчиваем рассмотрение реализации главного окна приложения. В следующих разделах мы покажем, как изменить наше приложение, чтобы оно могло работать одновременно с несколькими документами, и как добавить к приложению показ заставки во время загрузки.


3.7. Работа с несколькими документами одновременно.

Мы готовы приступить к созданию функции main():

#include <qapplication.h> 
#include "mainwindow.h" 

int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv); 
  MainWindow mainWin; 
  app.setMainWidget(&mainWin); 
  mainWin.show(); 
  return app.exec(); 
}      
      
Эта функция немного отличается от того, что мы видели до сих пор: экземпляр MainWindow был создан на стеке, без использования оператора new. Благодаря этому, объект класса MainWindow будет уничтожен автоматически, по завершении работы функции.

В данном случае, программа Spreadsheet представляет из себя единственное главное окно, и может работать только с одним документом в каждый конкретный момент времени. Если нам потребуется работать с несколькими документами одновременно, мы должны будем запустить несколько экземпляров программы.

Но для пользователя было бы гораздо удобнее, если бы программа могла запускать несколько главных окон с различными документами, как, например, это делают некоторые web-браузеры.

Попробуем внести дополнительные изменения в нашу программу, чтобы она могла одновременно работать с несколькими документами. Для этого, прежде всего, необходимо немного изменить меню File:

  • Пункт File|New создает новое главное окно с пустым документом, вместо того, чтобы создавать новый документ в этом же окне.

  • Пункт File|Close закрывает текущее главное окно.

  • Пункт File|Exit закрывает все окна приложения.

Рисунок 3.16. Новое меню File.




В своем первоначальном варианте, меню File не имело пункта Close, поскольку смысл операции закрытия окна был равносилен завершению приложения (пункт Exit).

Так выглядит новый вариант функции main():

#include <qapplication.h> 
#include "mainwindow.h" 

int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv); 
  MainWindow *mainWin = new MainWindow; 
  mainWin->show(); 
  QObject::connect(&app, SIGNAL(lastWindowClosed()), 
                   &app, SLOT(quit())); 
  return app.exec(); 
}      
      
Здесь мы связали сигнал lastWindowClosed() со слотом quit(), который завершает приложение.

В данном варианте, теперь имеет смысл создавать экземпляр MainWindow оператором new, поскольку затем, при закрытии окна, он будет удаляться оператором delete. Эта необходимость не возникает в случае приложения, которое работает с единственным документом.

Ниже приводится измененный вариант слота MainWindow::newFile():

void MainWindow::newFile() 
{ 
  MainWindow *mainWin = new MainWindow; 
  mainWin->show(); 
}      
      
Здесь просто создается новый экземпляр MainWindow. Может показаться странным, что мы нигде не запоминаем указатель на вновь созданный объект, но здесь нет никакой ошибки -- Qt хранит указатели на все окна сама, без нашего участия.

Ниже приводится код, создающий "действия" (actions) Close и Exit:

  closeAct = new QAction(tr("&Close"), tr("Ctrl+W"), this); 
  connect(closeAct, SIGNAL(activated()), this, SLOT(close())); 
  
  exitAct = new QAction(tr("E&xit"), tr("Ctrl+Q"), this); 
  connect(exitAct, SIGNAL(activated()), qApp, SLOT(closeAllWindows()));      
      
Слот closeAllWindows() закрывает все окна приложения, кроме тех, которые отвергнут событие close. Это в точности соответствует нашим требованиям. Нам нет нужды беспокоиться о несохраненных изменениях, поскольку сохранение выполняется в обработчике MainWindow::closeEvent(), при закрытии окна.

Теперь наше приложение в состоянии работать с несколькими окнами. К сожалению, на данный момент у нас в программе кроется трудноуловимая ошибка. Если пользователь будет создавать и закрывать окна приложения, то может наступить момент, когда вся доступная память в машине будет исчерпана! Это происходит потому, что мы создаем новые окна, выбирая пункт меню File|New, но нигде не удаляем их из памяти. Когда пользователь закрывает очередное окно, то объект класса MainWindow не удаляется из памяти, а просто делается невидимым.

Решение этой проблемы заключается в добавлении флага WDestructiveClose в конструктор:

MainWindow::MainWindow(QWidget *parent, const char *name) 
    : QMainWindow(parent, name, WDestructiveClose) 
{ 
  ...
}
      
Он вынуждает Qt удалять объект окна при его закрытии. Этот флаг один из множества, которые могут быть переданы в конструктор наследника от QWidget, но другие флаги используются довольно редко.

Однако, утечка памяти -- не единственная проблема, с которой мы можем столкнуться. Весь наш первоначальный дизайн предполагал работу с единственным главным окном. Теперь, каждое из окон приложения может иметь свой список недавно использовавшихся файлов и свои дополнительные настройки. Совершенно очевидно, что список недавно использовавшихся файлов должен быть глобальным для всего приложения. Сделать это можно довольно легко, достаточно просто объявить переменную recentFiles статической. Но, теперь везде, где необходимо вызвать updateRecentFileItems() для обновления меню File, мы должны вызвать эту функцию для всех главных окон. Ниже приводится код, который делает это:

  QWidgetList *list = QApplication::topLevelWidgets(); 
  QWidgetListIt it(*list); 
  QWidget *widget; 
  while ((widget = it.current())) { 
    if (widget->inherits("MainWindow")) 
      ((MainWindow *)widget)->updateRecentFileItems(); 
      ++it; 
  } 
  delete list;      
      
Здесь выполняется перебор всех виджетов верхнего уровня и вызывается функция updateRecentFileItems() во всех экземплярах MainWindow. Аналогичный подход может быть использован для синхронизации флагов Show Grid и Auto-recalculate, а так же для предотвращения загрузки одного и того же документа дважды. Тип QWidgetList определен как QPtrList<QWidget>, который будет обсуждаться в Главе 11 (Классы-контейнеры).

Рисунок 3.17. SDI и MDI.

Когда приложение открывает каждый следующий документ в новом окне, то говорят, что приложение относится к классу SDI-приложений (от англ. single document interface -- однодокументный интерфейс). Популярная альтернатива SDI -- MDI (от англ. multiple document interface -- многодокументный интерфейс), в этом случае приложение имеет одно главное окно, которое может включать в себя несколько дочерних окон с открытыми документами и разделяющими между собой пространство главного окна. С помощью Qt можно создавать как SDI, так и MDI приложения. На рисунке 3.17 показаны оба варианта оформления приложения Spreadsheet. Более подробно MDI будет описан в Главе 6.


3.8. Экран-заставка.

Многие приложения показывают заставку во время запуска. Одни разработчики "показывают" заставку, чтобы снизить психологическое давление на пользователя, ожидающего пока приложение загрузится, другие -- в рекламных целях. Добавление экрана-заставки к приложению -- довольно простая задача.

В Qt реализация экрана-заставки выполнена в виде класса QSplashScreen, который выводит на экран окно с изображением до того, как приложение будет полностью загружено. Имеется возможность отображать ход загрузки на заставке. Код, который выводит заставку на экран, как правило размещается в функции main(), перед вызовом QApplication::exec().

Ниже показан пример функции main(), использующей QSplashScreen для вывода заставки на время загрузки дополнительных модулей и установки соединения по сети.

int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv);  
  QSplashScreen *splash = 
          new QSplashScreen(QPixmap::fromMimeSource("splash.png")); 
  splash->show(); 
  
  splash->message(QObject::tr("Setting up the main window..."), 
                  Qt::AlignRight | Qt::AlignTop, Qt::white); 
  MainWindow mainWin; 
  app.setMainWidget(&mainWin); 
  
  splash->message(QObject::tr("Loading modules..."),
                  Qt::AlignRight | Qt::AlignTop, Qt::white); 
  loadModules(); 
  
  splash->message(QObject::tr("Establishing connections..."), 
                  Qt::AlignRight | Qt::AlignTop, Qt::white); 
  establishConnections(); 
  
  mainWin.show(); 
  splash->finish(&mainWin); 
  delete splash; 
  
  return app.exec(); 
}
      

Рисунок 3.18. Виджет QSplashScreen.


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

Предыдущая глава | Содержание | Следующая глава


Ж. Бланшетт, М. Саммерфильд, "Глава 3. Создание главного окна приложения" - 01/12/2004

г.ВИТЕБСК
РЕСПУБЛИКА БЕЛАРУСЬ

 
  О ресурре... Главная
[ ГЛАВНАЯ ] [ СОДЕРЖАНИЕ ]

E-mail : БИРЛ "Ombrello".

Hosted by uCoz