Учим Qt новым трюкам

Материал из b4wiki
Перейти к: навигация, поиск

Содержание

Введение

Предположим, что вы уже знаете, что такое Qt, что такое «виджет» — и даже что такое QWidget. Если нет, то начните, пожалуйста, с чего-нибудь вроде Бланшет, Саммерфилд Qt 4. Программирование GUI на C++.

Примечание. Чуть более старую версию этой книги можно скачать на http://www.qtrac.eu/C++-GUI-Programming-with-Qt-4-1st-ed.zip, причём совершенно официально.

Предположим, вы уже ведёте разработку пользовательских интерфейсов с использованием QtCreator, знаете, как создавать формы с использованием стандартных виджетов Qt, как компоновать виджеты на форме, и — надеюсь — знаете даже, как использовать QPainter при обработке QWidget::paintEvent.

И — вот сюрприз — стандартных виджетов вам уже не хватает.

Что ж, вам повезло: Qt предоставляет кучу способов научить GUI делать то, что вам нужно; и — что интересно — делаются такие вещи довольно-таки просто. Работали б на какой-нибудь другой системе — дела бы обстояли существенно хуже :)

Ну вот и начнём. От простого к сложному. С картинками, примерами и всем прочим.

К вопросу о примерах. Их можно скачать здесь: http://www.metrotek.spb.ru/files/sources/qt-widgets-article.zip

Фильтры событий

Для начала поговорим о том, как изменить поведение уже созданного виджета. Рога к нему пририсовать, или ещё чего.

Фильтры событий на уровне виджета

Допустим, есть такая простая задача. Есть окно (MainWindow, наследник QMainWindow), и на этом окне есть QGroupBox.

Widget-event-filter-before.png

И на этом QGroupBox надо нарисовать горизонтальную черту. Или ещё чего-нибудь.

Widget-event-filter-after.png

Коли бы эту черту рисовать на MainWindow — переопределили бы paintEvent и всё. Но здесь-то пририсовывать надо к дочернему виджету… Что?! Кто сказал «наследовать QGroupBox»? Наследовать будем в следующей главе, а тут всё проще :)

Оказывается, любой QObject (в т.ч. и QWidget) имеет метод QObject::eventFilter. Этот «фильтр» может перехватывать события (QEvent), поступающие другому QObject'у, и — в зависимости от задачи — пропускать эти события, блокировать их, либо добавлять к обработке события что-нибудь своё.

Таким образом, мы можем установить наш класс MainWindow как eventFilter для того QGroupBox — и рисовать на нём что угодно.

Устанавливаем фильтр событий:

  MainWindow::MainWindow( QWidget *parent ) :
      QMainWindow( parent ),
      ui( new Ui::MainWindow )
  {
    ui->setupUi( this );
    ...
    ui->groupBox->installEventFilter( this );
  }

И обрабатываем событие:

  bool MainWindow::eventFilter( QObject *obj, QEvent *evt )
  {
    if( evt->type() == QEvent::Paint )
    {
      QPainter p( ui->groupBox );
      p.drawLine( 0,
                  ui->groupBox->rect().center().y(),
                  ui->groupBox->width(),
                  ui->groupBox->rect().center().y() );
    }
    return QMainWindow::eventFilter( obj, evt );
  }
См. widget-event-filter в папке с примерами.

Такой способ подходит для того, чтобы визуально сгруппировать или разделить несколько виджетов, или нарисовать стрелку от одного виджета к другому; в общем, для визуального украшательства. Если eventFilter начинает обрабатывать несколько разных событий (например, QWidget::mousePressEvent и QWidget::mouseReleaseEvent) — то уже стоит подумать про наследование от QWidget.

Также упомяну, что один eventFilter можно установить в несколько объектов, и проверять, какой QObject поступил на вход метода.

Event-filter-decorations.png

Этот способ очень пригодился при создании показанного на рисунке окна. Для обеспечения работы менеджера компоновки (см. Layout Management) большая часть виджетов расположена на дочерних QFrame'ах, которые к тому же непрозрачные (autoFillBackground = true). И, когда встал вопрос, как рисовать «декорации» (на рисунке отмечены красными стрелками) — писать двух наследников QFrame'а или ставить в главное окно eventFilter, — фильтр, конечно, оказался проще :)

Также в один объект может быть установлено несколько разных фильтров; в этом случае они выполняются по очереди, и, если один фильтр вернул true («заблокировать обработку события») — то больше никто это событие не получит.

Фильтры событий на уровне приложения

Рассмотрим следующий пример. В программе есть куча окон и куча виджетов на них; при наведении мышки на любой виджет в любом окне краткое описание виджета надо вывести в специально отведённый QLabel. Использование стандартных всплывающих подсказок и виджета QWhatsThis не допускается, т.к. они не вписываются в дизайн приложения.

Application-event-filter.png

Простейшее решение — в конструкторе MainWindow установить тот же eventFilter — но не в какой-то виджет, а прямо в объект qApp:

  MainWindow::MainWindow( QWidget *parent ) :
      QMainWindow( parent ),
      ui( new Ui::MainWindow )
  {
      ui->setupUi( this );
      ...
      qApp->installEventFilter( this );
  }

Через такой фильтр будут проходить все события, поступающие всем виджетам во всех окнах приложения. И в фильтре можно запросто написать

  bool MainWindow::eventFilter( QObject *obj, QEvent *evt )
  {
    if( evt->type() == QEvent::MouseMove )
    {
      QMouseEvent *m = static_cast< QMouseEvent * >( evt );
      QWidget *w = QApplication::widgetAt( m->globalPos() );
      if( w )
        ui->labelHint->setText( w->whatsThis() );
    }
    return QMainWindow::eventFilter( obj, evt );
  }
См. application-event-filter в папке с примерами.

Теперь при наведении мышки на любой виджет его строка QWidget::whatsThis будет выведена в ui->labelHint.

Обратите внимание, что мы не используем указатель на объект, которому предназначено сообщение (obj). Дело в том, что, если объект «не заинтересовался» событием мышки, событие поступает к его «родителю» (QWidget::parentWidget), и так далее; т.е. одно перемещение мышки порождает несколько вызовов eventFilter с разными параметрами obj. А метод QApplication::widgetAt при всех этих вызовах всегда возвращает самый «верхний» виджет.

Таким образом, установка фильтра событий в qApp позволяет «ловить» определённые события на всех виджетах приложения — и делать то, что считаем нужным. Можно даже «ловить» сообщения, поступающие только виджетам определённого типа; например,

   QPushButton *p = qobject_cast< QPushButton * >( obj );
   if( p )
   {
      // чё-то делаем с этой кнопкой
      ...
   }

И — так же, как и с виджетами — в один qApp могут установить свои фильтры несколько QObject'ов.

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

Переопределение виджетов

Перейдём к более сложным задачам.

Составные виджеты

Рассмотрим, например, QSpinBox.

Spin-small.png

Что, мелко? Увеличим.

Spin-bigger.png

Ага. А теперь предположим, что программа должна работать на устройстве с сенсорным экраном, к которому к тому же не прилагается стилус. И как, пальцем по таким кнопкам попадёшь? Попадёшь, но хотелось бы сделать их покрупнее.

Позже (в главе Переопределение QStyle) поговорим, как можно увеличить кнопки QSpinBox, вообще не трогая виджет. А пока рассмотрим более простое решение.

Примечание. Создание составных виджетов, во-первых, очень подробно описано у тех же Бланшет и Саммерфилд, а во-вторых, мало чем отличается от создания «обычных» окон. Так что я здесь кратенько.

Разработка составного виджета

Берём QtCreator, создаём новый класс формы, наследуем от QWidget. На форму — две QPushButton, и QLabel между ними; и загоняем всё это в QHBoxLayout.

Spin-custom.png

В конструкторе формы не забываем писать

  setLayout( ui->horizontalLayout );

чтобы при изменении размеров формы horizontalLayout растягивался вместе с формой (и все три дочерних виджета вместе с ним).

Теперь в заголовочном файле создаём три Q_PROPERTYmin, max и value, для каждой заводим геттер и сеттер (методы min, setMin, max и так далее).

Примечание. Объявление публичных методов виджета как Q_PROPERTY понадобится в дальнейшем, при разработке плагина для QtCreator. Методы, объявленные таким образом, становятся доступны редактору форм. Имеет смысл всегда объявлять подобные методы как Q_PROPERTY, даже если пока вы не собираетесь делать из виджета плагин.

При изменениях value обновляем текст на QLabel. setValue желательно объявить в разделе public slots, в будущем пригодится. Да, и при изменениях значения (в том же setValue) неплохо бы генерировать какой-нибудь сигнал, например, valueChanged( int )… ладно, приведу код метода полностью, он в этом классе самый сложный :)

  void CustomSpin::setValue( int Value )
  {
    if( Value < min() )
      Value = min();
    if( Value > max() )
      Value = max();
    if( Value != _value )
    {
      _value = Value;
      ui->label->setNum( _value );
      emit valueChanged( _value );
    }
  } 
См. compound-widget в папке с примерами.

И, наконец, создаём для каждой из кнопок по слоту (на сигнал QAbstractButton::clicked), и в этих слотах уменьшаем/увеличиваем значение с использованием того же setValue. Ну и ставим кнопкам QAbstractButton::autoRepeat = true, чтобы при удерживании кнопки значения продолжали меняться.

И… и всё. Составной виджет готов.

Хорошо. И как его теперь использовать?

Программное создание виджета

Можно создать программно, прямо в конструкторе. Например, так:

  #include "customspin.h"
  ...
  MainWindow::MainWindow(QWidget *parent) 
    : QMainWindow( parent ),
      ui( new Ui::MainWindow )
  {
    ui->setupUi( this );
    ...
    spin = new CustomSpin( this );
    connect( spin, SIGNAL( valueChanged( int ) ), 
             this, SLOT( spinValueChanged( int ) ) );
    spin->setGeometry( 10, 10, 300, 50 );
    spin->setValue( 12 );
  }
  ...
  void MainWindow::spinValueChanged( int i )
  {
    // do something
  }

«Продвижение» виджета

Ещё один способ использования составных виджетов — «продвижение» виджета (“Widget Promotion”). В редакторе форм ставим на форму, например, QWidget, жмём на нём правую кнопку, выбираем «Преобразовать в…» (“Promote to…»), и указываем имя класса, до которого выбранный виджет надо «продвинуть».

Другой вопрос, что это мало чем поможет. Да, uic возьмёт на себя создание виджета и установку размера, — и виджет можно даже включить в какой-нибудь layout manager; но не более. Все свойства (Q_PROPERTY), определённые в классе виджета, придётся устанавливать программно, и соединения сигналов со слотами тоже.

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

А почему бы, спрашивается, сразу не в плагин?

Плагин с виджетами

Действительно, в QtCreator'е предусмотрена возможность загрузки плагинов (динамических библиотек с виджетами). Разработка плагинов описана в статье “Creating Custom Widgets for Qt Designer”. Если вкратце, то, помимо самого виджета, надо реализовать класс плагина — наследника от QDesignerCustomWidgetInterface, который будет сообщать QtCreator'у название виджета, имя заголовочного файла (который будет включаться в файлы .ui при добавлении виджета на форму) и прочую информацию о виджете. Ну, и создавать сам виджет, когда QtCreator попросит.

Класс плагина

Объявление класса плагина начинается с чего-то вроде

class CustomSpinPlugin : public QObject,
                         public QDesignerCustomWidgetInterface
{
    Q_OBJECT
    Q_INTERFACES( QDesignerCustomWidgetInterface )
    ...

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

 QString CustomSpinPlugin::domXml() const
 {
    return "<ui language=\"c++\">\n"
           " <widget class=\"CustomSpin\" name=\"customSpin\" />\n"
           "</ui>\n";
 }

а при необходимости — ещё и значения свойств виджета. Если в этом коде «наврать», QtCreator не сможет создать виджет.

Помимо этого, в реализации метода QDesignerCustomWidgetInterface::icon желательно вернуть иконку, которую QtDesigner будет отображать в списке виджетов. PNG, 22x22, с прозрачностью. Например,

 QIcon CustomSpinPlugin::icon() const
 {
    return QIcon( ":/main/customspin.png" );
 }

Надеюсь, что иконка customspin.png уже добавлена в файл ресурсов проекта (а сам файл ресурсов и префикс “main” в нём уже созданы) :)

И, наконец, не забываем добавить в файл реализации плагина макрос Q_EXPORT_PLUGIN2:

 Q_EXPORT_PLUGIN2( widget-plugin, CustomSpinPlugin )
Примечание. Макрос Q_EXPORT_PLUGIN2 может быть включён в программу только один раз. Если один плагин экспортирует несколько виджетов, то надо реализовать QDesignerCustomWidgetCollectionInterface — и экспортировать его с помощью Q_EXPORT_PLUGIN2.

Сборка и установка плагина

Итак, класс плагина реализован, сам виджет тоже. Рассмотрим файл .PRO.

  ...
  TEMPLATE = lib
  CONFIG += designer plugin
  DESTDIR = $$(QT_PLUGIN_PATH)/designer
  QMAKE_POST_LINK = cp customspin.h $(DESTDIR)
  ...
См. widget-plugin в папке с примерами.

Проект является библиотекой, плагином, и предназначен для QtDesigner/QtCreator. Бинарный файл библиотеки будет установлен в $$(QT_PLUGIN_PATH)/designer… так, а это куда? Оператор $$( ) означает подстановку переменной окружения (см. qmake Manual), а переменная окружения QT_PLUGIN_PATH говорит QtCreator'у, где ему при запуске искать плагины. Т.е. создаём директорию, и создаём соответствующую переменную окружения; именно туда при сборке будет положена библиотека плагина — и оттуда её заберёт QtCreator. И, наконец, последняя строчка говорит, куда после сборки плагина класть заголовочный файл виджета (customspin.h). Для определённости я положил его в ту же директорию с плагинами.

Теперь соберём библиотеку, перезапустим QtCreator, откроем редактор форм и удостоверимся, что новый виджет появился в списке. А с этим иногда возникают сложности.

Если виджета в списке нет, то во-первых, ещё раз проверим наличие переменной окружения QT_PLUGIN_PATH — и наличие файла плагина в QT_PLUGIN_PATH/designer/. Далее, удостоверимся, что плагин собран в конфигурации release, в противном случае QtCreator может отказаться его грузить. И, наконец, последний случай — это если Qt SDK и QtCreator имеют разные версии. Если вдруг Qt SDK более поздней версии, чем QtCreator (а достаточно более поздней даты сборки!) — плагин не будет загружен. И тут либо править вручную кэш плагинов (см. Deploying Plugins), либо установить QtCreator поновее.

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

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

  ...
  INCLUDEPATH += $$(QT_PLUGIN_PATH)/designer
  LIBS += -L$$(QT_PLUGIN_PATH)/designer \
      -lwidget-plugin
  ...
См. widget-plugin-usage в папке с примерами.

и потом, перед запуском программы, добавляем в переменную окружения LD_LIBRARY_PATH путь до директории с плагинами (тот самый “$$(QT_PLUGIN_PATH)/designer”). Или — как вариант — копировать бинарник библиотеки в директорию с использующей её программой, ещё одной командой

  QMAKE_POST_LINK = cp $(DESTDIR)$(TARGET) куда-то там

в файле .PRO плагина.

И всё. Виджет добавляется на форму так же, как любой из стандартных виджетов Qt, все свойства (Q_PROPERTY), описанные при разработке виджета, доступны в редакторе, и соединения «сигнал-слот» устанавливаются обычным образом.

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

Наследование от QWidget

Составной виджет, разработанный в предыдущей главе, имеет несколько недостатков. Во-первых, он состоит из трёх виджетов, из них два (две кнопки) могут получить клавиатурный фокус; а по-хорошему фокус-то должен быть один, на всём виджете. Во-вторых, не работают изменения значения с клавиатуры при нажатии стрелки вверх и стрелки вниз. И, в третьих, куда-то подевались свойства стандартного QSpinBox, такие, как prefix, wrapping, singleStep.

И вот тут мы подходим к прямому наследованию от QWidget (или одного из его потомков) — и самостоятельной реализации поведения виджета.

И — для начала — вопрос: а от кого наследовать?

От кого наследовать?

Хочется, конечно, писать поменьше кода, и наследовать от более «продвинутого» виджета; в нашем случае от QSpinBox. Но вот именно такое наследование в Qt и проводит к самым большим проблемам.

Что нам пришлось бы делать при наследовании от QSpinBox? Для начала — переписать отрисовку виджета, т.е. переопределить paintEvent. И тут бы мы выяснили, что QSpinBox создаёт поверх себя дочерний QLineEdit, который ещё надо куда-нибудь «спрятать», чтоб тот не рисовал каретку и выделение текста. Потом — найдя способ «спрятать» QLineEdit — мы начали бы обрабатывать события мышки, и обнаружили бы, что «стандартный» QSpinBox не сам определяет, какая из кнопок нажата, а пользуется для этого каким-то там QStyle… то есть и этот код пришлось бы выкинуть нафиг. В конечном итоге, мало чего осталось бы от «стандартного» QSpinBox'а, большая часть кода была бы реализована заново.

И хорошо ещё, если получится «реализовать заново»! Если посмотреть исходный код виджетов Qt (в директории qt-install-directory/qt/src/gui/widgets/), то окажется, что у большинства виджетов значительная часть функций реализована на так называемых «приватных» классах (например, QAbstractSpinBoxPrivate), к которым «наследник» не имеет никакого доступа! И далеко не всегда удаётся просто так взять и «отменить» стандартное поведение виджета, прежде чем написать своё…

Вывод: в большинстве случаев наследуем от QWidget. Если очень надо наследовать от более высокоуровневого класса — то надо перед этим очень серьёзно подумать. И приготовиться долго и с удовольствием копаться в исходниках Qt, которые отнюдь не так хороши, как Qt'шная документация.

Примечание. Некоторые виджеты Qt всё-таки позволяют наследовать; например, в том же QSpinBox можно переопределить пару методов — см. Subclassing QSpinBox. Но если под вашу конкретную задачу в выбранном вами виджете не предусмотрен никакой виртуальный метод — унаследуйте лучше от QWidget, и напишите заново.

А, кстати, так ли много придётся писать, если наследовать от QWidget? А вот тут есть один секрет.

Использование QStyle для рисования виджетов

А секрет заключается в следующем. Ни один виджет в Qt не рисует сам себя. Есть класс QStyle, который отвечает за рисование подавляющего большинства виджетов (за исключением QLabel и QTextEdit, которые рисуются с помощью QTextDocument). А значит, отрисовка виджета — а это самый потенциально сложный код в его реализации! — значительно упрощается.

Рассмотрим пример.

  ...
  #include <QStylePainter>
  #include <QStyleOptionButton>
  ...
  void MainWindow::paintEvent( QPaintEvent * )
  {
    QStylePainter p( this );
    QStyleOptionButton b;
    b.initFrom( this );
    b.state = QStyle::State_Raised | QStyle::State_Enabled;
    b.rect = QRect( 10, 10, 200, 60 );
    b.icon = QIcon( ":/main/voice.png" );
    b.iconSize = QSize( 32, 32 );
    b.text = tr( "Йа кнопко x_x" );
    p.drawControl( QStyle::CE_PushButton, b );
  }
См. qstyle-drawing в папке с примерами.

И что? И всё:

Qstyle-drawing.png

Т.е. никакой кнопки на форме нет, но кнопка рисуется. И, более того, если запустить программу с ключом, например, -style plastique, то

Qstyle-drawing-plastique.png

получаем эту самую кнопку с градиентами и закруглёнными углами.

Никто не мешает таким же способом нарисовать QRadioButton, QScrollBar — да что в голову взбредёт. Только правильным образом заполняем поля структуры QStyleOption, да правильным образом указываем тип виджета (QStyle::ComplexControl, QStyle::ControlElement или QStyle::PrimitiveElement при обращении к QStylePainter::drawComplexControl, QStylePainter::drawControl или QStylePainter::drawPrimitive.

Кстати, обратите внимание на вызов

    ...
    QStylePainter p( this );
    QStyleOptionButton b;
    b.initFrom( this );
    ...

в приведённом выше примере. Именно в этом месте в QStyleOption и QStylePainter копируются палитра (QPalette) и шрифт (QFont) виджета; и никто не мешает их потом поменять. Только не забудьте про поле QStyleOption::fontMetrics, оно должно соответствовать выбранному размеру шрифта.

Итак, с рисованием более или менее понятно. И что дальше?

Что переопределяем

Рассмотрим основные события, которые придётся переопределять при разработке виджета.

QWidget::paintEvent — про это сказано достаточно. Уж конечно, нестандартный виджет должен что-нибудь рисовать на экране.

QWidget::resizeEvent — если виджет должен каким-то образом реагировать на изменение размера. Обычной практикой является в resizeEvent расчитать координаты активных областей (если у виджета их несколько) — и использовать эти координаты при рисовании и обработке событий мышки.

QWidget::mousePressEvent
QWidget::mouseMoveEvent
QWidget::mouseReleaseEvent — обрабатываем, если виджет должен реагировать на события мышки.

QWidget::keyPressEvent — если нужно реагировать на события клавиатуры.

И — чаще всего этого бывает достаточно. Реализацию самодельного QSpinBox'а не привожу, она — после всего сказанного — не должна бы вызывать затруднений :)

А на вопрос «что потом делать с этим виджетом» ответ был дан выше. Либо создавать программно (если не влом), либо использовать «продвижение» (если виджет используется только в одном приложении), либо оформлять как плагин.

Всё? Нет, есть ещё один способ поменять поведение виджета. Не трогая сам виджет.

Переопределение QStyle

Как я уже упоминал, большинство виджетов рисуются с помощью класса QStyle. Ну так может быть, переопределить этот QStyle — и пусть все виджеты в приложении работают по-другому?

Чем занимается QStyle

Для начала поймём, что делает QStyle. А он не только рисует, он отвечает ещё за расположение активных областей виджета на экране. Например, тот же QSpinBox использует три метода QStyle:

QStyle::subControlRect — возвращает области, которые занимают обе кнопки (SC_SpinBoxUp и SC_SpinBoxDown) и область редактирования (SC_SpinBoxEditField).

QStyle::hitTestComplexControl — получает координаты точки, и определяет, какая из областей находится под этой точкой. Границы активных областей получает от subControlRect.

QStyle::drawComplexControl — рисует сам спин бокс, т.е. обе кнопки и прямоугольник вокруг области редактирования. Сама область редактирования рисуется отдельно (для этого QSpinBox, как уже упоминалось, создаёт QLineEdit).

Таким образом, если бы мы хотели просто разместить кнопки со стрелками по разные стороны от области редактирования (и при этом не убирать саму область редактирования, т.е. не переделывать сам виджет заново), мы могли бы унаследовать QStyle, и переопределить три упомянутых метода. В методе subControlRect расчитали бы положения кнопок, а в drawComplexControl нарисовали бы эти кнопки, как описано в предыдущей главе

Cтоп. А вот какой из QStyle мы собрались наследовать?!

Наследование или инкапсуляция?

Если QStyle наследовать, то надо знать, какой именно стиль будет использоваться в приложении. Решили, например, что программа всегда будет использовать QWindowsStyle, унаследовали его, потом в файле main.cpp написали

   QApplication a(argc, argv);
   a.setStyle( new MyWindowsStyle() );

и вперёд. Но запустить программу с ключом

   my-application -style motif

уже не получится. Точнее, получится, но использован всё равно будет MyWindowsStyle. То есть, унаследовав определённый QStyle, мы теряем возможность сменить стиль оформления программы штатными средствами Qt. Пример такого наследования не привожу, он есть в статье Styles Example.

Альтернативный способ — инкапсулировать стиль. А именно, после создания QApplicationперед созданием первого окна) сделать копию QApplication::style и запомнить её в своей реализации класса. «Делать копию» необходимо, поскольку после установки стиля приложения с помощью QApplication::setStyle «старый» стиль будет уничтожен.

То есть, конструктор инкапсулированного стиля может выглядеть следующим образом:

  StyleOverride::StyleOverride( QStyle *oldStyle )
      : QStyle()
  {
    savedStyle = QStyleFactory::create( oldStyle->objectName() );
    setObjectName( oldStyle->objectName() );
  }

А установка этого стиля в функции main

  int main(int argc, char *argv[])
  {
      QApplication a(argc, argv);
      QApplication::setStyle( new StyleOverride( QApplication::style() ) );
      ...

После чего надо взять все pure virtual методы класса QStyle — и реализовать их через savedStyle:

  class StyleOverride : public QStyle
  {
     ...
     virtual void polish(QPalette &p)
       { savedStyle->polish(p); }
     virtual QRect itemTextRect(const QFontMetrics &fm, const QRect &r,
                                int flags, bool enabled,
                                const QString &text) const
       { return savedStyle->itemTextRect(fm, r, flags, enabled, text); }
     ...

После этого определяемся с виджетами, которые надо переделать, выясняем, какие методы QStyle они используют — и реализуем соответствующие методы в нашем StyleOverride.

Здесь две проблемы.

Во-первых, как узнать, какие методы класса QStyle нужны определённому виджету? Их ведь там три группы — одна для «примитивов», одна для «простых» виджетов (типа QPushButton) и ещё одна для «сложных» (к которым, например, относятся QSpinBox и QComboBox). В этом может помочь статья Implementing Styles and Style Aware Widgets, а могут помочь и исходники Qt (см. в директории qt-install-directory/qt/src/gui/widgets/). И — да, согласен, эта работа не из простых.

И во-вторых, помним, что инкапсуляция — это не наследование. Например, QSpinBox использует subControlRect и hitTestComplexControl. Коли б мы наследовали QStyle, переписали бы только метод subControlRect, а hitTestComplexControl его бы сам вызвал. В случае инкапсуляции переписывать придётся оба.

Таким образом, выбор между наследованием и инкапсуляцией — за вами. Инкапсуляция более сложна и громоздка — но зато мы не теряем возможность менять стиль оформления программы.

И — напоследок — как выглядит программа с переопределённым стилем:

Style-override.png

То есть, сразу все QSpinBox'ы в программе меняют свой внешний вид — а их поведение остаётся без изменений.

Код довольно-таки объёмный; его можно найти в примерах, в папке style-override.

Заключение

Итак, подведём итоги. Мы рассмотрели несколько способов поменять внешний вид и поведение виджетов Qt.

Фильтры событий на уровне виджета. Быстрый способ поменять внешний вид дочернего виджета; похож на обработку QWidget::paintEvent — но позволяет рисовать на любом виджете формы без переопределения самого виджета.

Фильтры событий на уровне приложения. «Ловят» все события для всей программы, что позволяет поменять поведение или внешний вид сразу всех виджетов. Отслеживать QWidget::mouseMoveEvent, например.

Составные виджеты. Можно оформить некий набор виджетов как «составной виджет» — а потом либо создавать «составной» виджет программно, либо использовать «продвижение». А при необходимости использовать один виджет в нескольких программах (а также для доступа к свойствам и сигналам виджета из редактора форм) — оформить такой виджет как плагин.

Разработка виджета «с нуля». Самый мощный из способов; применяется в случаях, когда компоновки из «стандартных» виджетов уже недостаточно. Позволяет создать то-чаго-на-свете-не-было, насколько фантазии хватит. При этом, скорее всего, всё поведение виджета придётся реализовывать самостоятельно: большинство «продвинутых» виджетов Qt плохо приспособлены к наследованию, и наследовать, по-видимому, придётся базовый QWidget (либо долго и тщательно копаться в исходниках). Упрощает задачу тот факт, что рисование виджетов берёт на себя класс QStyle.

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

Мы рассмотрели далеко не все (!) способы менять внешний вид и поведение виджетов. В частности, за кадром остались Qt Style Sheets, позволяющие описывать внешний вид виджетов набором строк в стиле CSS. Также не были рассмотрены модели и представления — а ведь там есть свои методы поменять, например, внешний вид одной из ячеек в таблице… Так что есть что поизучать на досуге!

Желаю удачи!

--Антон Черниговский, НТЦ «Метротек», 11:25, 16 февраля 2010 (UTC)

Личные инструменты
Пространства имён

Варианты
Действия
разработки
технологии
разное
Инструменты