C++ и Qt: оптимизация интерфейса

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

Введение

Создание интерфейса в самом деле занятие нудное, поскольку громоздкое. Для добавления одной кнопки и реакции на нее приходится, если пользоваться лишь стандартными средствами тулкита, писать много кода. Взамен нас агитируют пользоваться разными «визуальными редакторами», которые не генерируют код, а создают промежуточные файлы, подгружаемые в процессе работы программы. Практика показывает, что если для простейшего интерфейса такой подход годен, то для сложного — уже нет. И приходится либо изобретать пути упрощения этой задачи, либо копировать и вставлять, копировать и вставлять…

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

Далее речь пойдет о связке C++ и Qt. Никакого Qt Quick и никаких форм, создаваемых в «дизайнере». Только код на C++ и только стандартные виджеты.

Первые шаги

В концепцию построения интерфейса в Qt входит понятие макета — layout. Макет задает способ упорядочения виджетов (по вертикали или горизонтали) и их последовательность. Он может содержать в себе другие макеты и виджеты — так и составляется интерфейс.

Как знают Qt-программисты, для помещения виджетов по вертикали используется класс QVBoxLayout, а по горизонтали — QHBoxLayout. Ради помещения в интерфейс, например, выпадающего списка с подписью слева от него, надо создать QHBoxLayout, поместить в него QLabel, QComboBox, а сам QHBoxLayout — в какой-то макет-родитель. Эти вот «кирпичики» занимают очень много строчек кода, а ведь они фактически повторяются.

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

Наиболее часто группировать в макет приходится те виджеты, у которых нет своей надписи (а вот у QCheckBox, например, она есть) — это строки ввода, списки и тому подобное. Для них и целесообразно написать функции построения «блоков». Каждая такая функция должна возвращать не макет, не метку, а основной виджет, потому что к нему нам может понадобиться подключать разные обработчики событий. Поэтому в функцию мы передаем в качестве параметра макет-родитель, чтобы новый созданный макет был туда помещен.

Напишем функцию для создания экземпляра QLineEdit (виджет строки ввода) и помещения её в макет. Вот как выглядит код функции:

QLineEdit* new_line_edit (QBoxLayout *layout, const QString &label, const QString &def_value)
 
{
 
QHBoxLayout *lt_h = new QHBoxLayout;
 
QLabel *l = new QLabel (label);
 
QLineEdit *r = new QLineEdit; r->setText (def_value);
 
lt_h->addWidget (l); lt_h ->addWidget (r);
 
layout->addLayout (lt_h);
 
return r;
 
}

Здесь:

> QBoxLayout *layout — макет-родитель;

> QString &label — метка, надпись слева от строки ввода;

> QString &def_value — значение по умолчанию, помещаемое в строку ввода.

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

QVBoxLayout *layout = new QVBoxLayout;
 
QHBoxLayout *lt_h1 = new QHBoxLayout;
 
QLabel *l1 = new QLabel ("test02");
 
QLineEdit *r1 = new QLineEdit; r1->setText ("test01");
 
lt_h1->addWidget (l1); lt_h1 ->addWidget (r1);
 
layout->addLayout (lt_h1);
 
QHBoxLayout *lt_h2 = new QHBoxLayout;
 
QLabel *l2 = new QLabel ("test02");
 
QLineEdit *r2 = new QLineEdit; r1->setText ("test02");
 
lt_h2->addWidget (l2); lt_h2 ->addWidget (r2);
 
layout->addLayout (lt_h2);
 
Конечно, можно было и покороче, но ненамного. Теперь код с тем же действием и с применением описанной выше функции:
 
QVBoxLayout *layout = new QVBoxLayout;
 
QLineEdit *r1 = new_line_edit (layout, "test01", "test01");
 
QLineEdit *r2 = new_line_edit (layout, "test02", "test02");

Конечно, можно было и покороче, но ненамного. Теперь код с тем же действием и с применением описанной выше функции:

QVBoxLayout *layout = new QVBoxLayout;

QLineEdit *r1 = new_line_edit (layout, «test01», «test01»);

QLineEdit *r2 = new_line_edit (layout, «test02», «test02»);
Разница очевидна. Чуть «многословнее» будет функция создания QSpinBox — виджета ввода числовых значений. В ней мы кроме прочих параметров передаем наименьшее и наибольшее значения, значение по умолчанию и шаг прироста (сколько добавлять или отнимать от числа в поле ввода при нажатии на стрелки виджета). Код будет таким:

QSpinBox* new_spin_box (QBoxLayout *layout, const QString &label, int min, int max, J int value, int step)
 
{
 
QHBoxLayout *lt_h = new QHBoxLayout;
 
QLabel *l = new QLabel (label);
 
QSpinBox *r = new QSpinBox;
 
r->setSingleStep (step);
 
r->setRange (min, max);
r->setValue (value);
 
lt_h->addWidget (l); lt_h ->addWidget (r);
 
layout->addLayout (lt_h);
 
return r;
 
}

Наконец, создание комбобокса. Сначала код функции, а затем — пример и немного комментариев:

QComboBox* new_combobox (QBoxLayout *layout, const QString &label, J const QStringList &items, const QString &def_value)
 
{
 
QHBoxLayout *lt_h = new QHBoxLayout;
 
QLabel *l = new QLabel (label);
 
QComboBox *r = new QComboBox;
 
r->addItems (items);
 
r->setCurrentIndex (r->findText (def_value));
 
lt_h->addWidget (l); lt_h->addWidget (r);
 
layout->addLayout (lt_h);
 
return r;
 
}

В параметрах к этой функции мы, кроме макета-родителя и метки, передаем список элементов (QStringList &items) и значение по умолчанию (QString &def_value). Список элементов создается очень просто:
QStringList items;

items << "one" << "two" << "three" << "four"; Последующее создание комбобокса будет выглядеть так: QComboBox *cmb_test = new_combobox (layout, "test", items, "three"); Внутри функции мы переключаем текущий элемент, чтобы он соответствовал значению по умолчанию, следующим образом: r->setCurrentIndex (r->findText (def_value));

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

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

Объявление функции в классе окна будет таким:

QAction* add_to_menu (QMenu *menu,
 
const QString &caption,
 
const char *method,
 
const QString &shortkt = QString(),
 
const QString &iconpath = QString());

Здесь:

> QMenu *menu — родительское меню, к которому «цепляется» создаваемый нами пункт;

> const QString &caption — надпись на пункте меню;

> const char *method — слот-обработчик, вызываемый при выборе этого пункта меню.

И еще два параметра, которые имеют значения по умолчанию:

> const QString &shortcut — сочетание клавиш;

> const QString &iconpath — путь к иконке, обычно в файле-ресурсе.

Теперь посмотрим на код функции:

QAction* такой-то класс::add_to_menu (QMenu *menu,
 
const QString &caption, const char *method, const QString &shortkt, const QString &iconpath)
 
{
 
QAction *act = new QAction (caption, this);
if (! shortkt.isEmpty())
 
act->setShortcut (shortcut);
 
if (! iconpath.isEmpty())
 
act->setIcon (Qlcon (iconpath));
 
connect (act, SIGNAL(triggered()), this, method); menu->addAction (act); return act;
 
}

И вот пример использования этой функции:

add_to_menu (fileMenu, tr («Open»), SLOT(open()), «Ctrl+O», «:/icons/file-open.png»);

Здесь мы добавляем пункт Open к меню fileMenu, с горячими клавишами + и иконкой, находящейся в ресурсе по пути «:/icons/file-open.png». Обратите внимание на передачу слота в качестве параметра: SLOT(open()). То есть имя и параметровые скобки заключаем еще в макрос SLOT(). Напомню, что два последних параметра (сочетание клавиш и иконку) можно не указывать, т.к. им назначены нулевые значения по умолчанию.

И вариант динамического создания меню, где функция не является методом класса. Класс, слоты которого служат обработчиками этого меню, будет передан в функцию через параметр handler. Приведенная ниже функция предназначена для создания меню из нескольких элементов, надписи на которых передаются в функцию через список QStringList &list:

void create_menu_from_list (QObject *handler,
 
QMenu *menu,
 
const QStringList &list, const char *method)
 
{
 
menu->setTearOffEnabled (true); foreach (QString s, list)
 
{
 
QAction *act = new QAction (s, menu->parentWidget()); handler->connect (act, SIGNAL(triggered()), handler, method); menu->addAction (act);
 
}
 
}

Возникает вопрос о слоте-обработчике. Очевидно, что он, будучи вызванным при выборе пункта меню, должен как-то получить надпись на конкретном элементе меню — том элементе, для которого произошел вызов слота. Иначе говоря, нам надо получить QString надписи на пункте меню. Беда в том, что сигнал triggered(), который посылается при выборе пункта меню, не имеет параметров. Как же получить надпись?

Внутри слота мы можем вызывать функцию sender(), которая вернет нам (при приведении типа) экземпляр QAction, связанный с вызванным пунктом меню. В примере ниже мы получаем надпись на выбранном пункте меню и выводим её в консоль:

void такой-то класс::такой-то слот()
 
{
 
QAction *Act = qobject_cast<QAction *>(sender());
 
qDebug() << Act->text();
 
}

Напоследок — окно настроек. В каждой мало-мальски сложной программе есть такое окно, и порой в исходнике можно встретить громоздкий код этого окна. Громоздкость проявляется не только в повторяющихся конструкциях — выше я показал, как это решается при помощи функций, конструирующих «блоки» интерфейса. Бывает, что к каждой кнопке-переключателю, каждому полю ввода привязывается слот. Нажал пользователь переключатель — вызывается слот, там считывается состояние кнопки-переключателя и записывается в файл настроек. Это напрасный расход сил и лишние строки кода.

Удобнее сделать так: «опрос» состояния всех элементов управления, где не требуется мгновенная реакция, вынести в отдельную функцию, которая будет вызываться, например, при нажатии на кнопку ОК окна настроек. Слоты же оставить только для тех элементов, которые сразу должны что-то изменить: например, выбрали стиль оформления, и он применяется, не дожидаясь, пока пользователь нажмет на ОК.

Заключение

Пожалуй, вот основные способы оптимизировать интерфейс, не прибегая к иным средствам его построения, кроме традиционного кода на С++ и «зашитых» (hardcoded) элементов управления. Не так уж всё уныло — надо лишь приложить немного усилий, и это воздастся сторицей. eof|
Петр Семилетов [email protected]


http://blog.wel.org.ua

работаю админом, прогером сеошнегом :)

Leave a Comment

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Загрузка...
Menu Title