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().