Программирование на языке C++ в среде Qt Creator

Собственные классы в Qt. Создание элементов графического интерфейса

Показывать лекцию целиком

14.1 Класс QObject

QObject является базовым классом для почти всех классов Qt. Исключением являются только классы, которые должны быть достаточно "лёгкими" (экземпляры которых должны занимать как можно меньше памяти) и классы, объекты которых должны копироваться (QObject не поддерживает копирования), а также контейнерные классы. Все виджеты Qt наследуют QObject (класс QWidget является потомком QObject). QObject реализует все базовые особенности, которыми обладают классы Qt:

  • мощный механизм взаимодействия между объектами с помощью сигнально-слотовых соединений;
  • иерархические взаимосвязи между объектами, позволяющие объединять их в объектные деревья;
  • управление памятью;
  • "умные" указатели, позволяющие отслеживать уничтожение объекта;
  • поддержка свойств;
  • таймеры;
  • обработка событий и фильтры событий;
  • метаинформация о типе объекта, его свойства и т. п.
  • Каждый объект типа QObject обладает метаданными, которые хранятся внутри специального метаобъекта. Этот метаобъект создаётся для каждого потомка QObject и сохраняет различную информацию об объекте (так называемые метаданные). Среди доступных для программиста метаданных есть:

  • имя класса (метод const chat *QObject::className());
  • наследование (метод bool QObject::inherits(const char *className));
  • информация о свойствах;
  • информация о сигналах и слотах;
  • общая информация о классе (QОbject::classInfo).
  • Метаданные собираются во время компиляции (предварительной обработки проекта с помощью qmake) метаобъектным компилятором moc, который анализирует содержание заголовочных файлов программы. Эти метаданные позволяют получить информацию о любом потомке QObject. Эта информация может быть полезна как для отладки программы, так и для создания различных механизмов взаимодействия между объектами в программе.

    При разработке с использованием Qt часто возникает необходимость наследовать класс QObject непосредственно или его потомка. Объекты, которые наследуют QObject:

  • имеют имя (QObject :: objectName ()), которое используется в Qt для реализации различных возможностей (стили, QML и т.д.);
  • могут занимать место в иерархии других объектов QObject;
  • могут иметь сигнально-слотовые соединения с другими объектами QObject.
  • Рассмотрим создание собственного класса на примере создания виждета, который можно будет многократно использовать в разных программах. Разработаем поле ввода с пиктограммой, которая может реагировать на действия пользователя. Такая пиктограмма:

  • может быть статически изображена в поле ввода (например, для обозначения обязательных полей в форме ввода данных пользователем);
  • обозначать пустое поле или поле с введёнными данными (например, для обозначения некорректно введённых данных);
  • выполнять заранее заданное действие после нажатия (например, для открытия диалога выбора файла для поля ввода пути к файлу).
  • Создадим новый проект и добавим к нему новый класс, который будет наследовать от QLineEdit (создание новых классов с использованием мастера описано в разделе 13.1). Назовём новый класс IconizedLineEdit. После создания нового класса откроем файл описания (IconizedLineEdit.h).

    Для того, чтобы корректно наследовать класс от QObject необходимо выполнить несколько условий:

  • во-первых, QObject (или потомок QObject, от которого наследуют) должен стоять первым в списке классов, от которых наследуют;
  • во-вторых, перед объявлением интерфейса класса сразу после фигурной скобки в частной секции размещают макрос Q_OBJECT.
  • Созданный класс выполняет эти условия (наследует от QLineEdit, который наследует от QWidget, а тот в свою очередь от QObject и содержит макрос Q_OBJECT в частной секции класса).

    #ifndef QRCLEARABLELINEEDIT_H
    #define QRCLEARABLELINEEDIT_H
    #include <QLineEdit>
    class QLabel;
    class IconizedLineEdit : public QLineEdit
    {
    	Q_OBJECT
    public :
    	//Режимы отображения пиктограммы, которые определяют её поведение
    enum IconVisibilItyMode {
    	//Всегда отображать пиктограмму
    	IconAlwaysVisible =0,
    	//Отображать пиктограмму после наведения курсора на поле ввода
    	IconVisibleOnHover,
    	//Отображать пиктограмму при присутствии текста
    	IconVisibleOnTextPresence,
    	//Отображать пиктограмму при отсутствии текста
    	IconVisibleOnEmptyText,
    	//Всегда прятать пиктограмму
    	IconA lwaysH idden
    	};
    	explicit IconizedLineEdit ( QWidget * parent = 0 );
    	bool isIconVisible ( ) const;
    	void setIconPixmap ( const QPixmap pPixmap );
    	void setIconTooltip ( const QString pToolTip );
    private :
    	void updateIconPositionAndsize ( );
    private :
    	QLabel *mIconLabel; //Указатель на метку, которая отображает пиктограмму
    };
    #end if // QRCLEARABLELINEEDIT_H

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

    #include " IconizedLineEdit .h "
    #include <QSty le>
    #include <QLabel>
    //Конструктор класса
    IconizedLineEdit::IconizedLineEdit ( QWidget * parent ) : QLineEdit ( parent )
    {
    	mIconLabel = new QLabel ( this ); //Создаём метку для того, чтобы показать пиктограмму
    }
    //Возвращает true, если пиктограмма видима
    bool IconizedLineEdit::isIconVisible ( ) const
    {
    	return mIconLabel-> isVisible ( );
    }
    //Устанавливает пиктограмму
    void IconizedLineEdit::setIconPixmap ( const QPixmap pPixmap )
    {
    	//Устанавливаем пиктограмму для метки
    	mIconLabel->setP ixmap ( pPixmap );
    	//Обновляем позицию и размеры
    	updateIconPositionAndsize ( );
    }
    //Устанавливаем подсказку для пиктограммы
    void IconizedLineEdit::setIconTooltip ( const QString pToolTip )
    {
    	//Подсказка будет видимой после наведения курсора на метку с пиктограммой
    	mIconLabel->setToolTip ( pToolTip );
    }
    void IconizedLineEdit::updateIconPositionAndsize ( )
    {
    	//Обновить размер пиктограммы
    	mIconLabel->setScaledContents ( true );
    	mIconLabel->setFixedsize ( height ( ), height ( ) );
    	//Обновить размещение пиктограммы
    	QSizelsize = mIconLabel->size ( );
    	mIconLabel->move ( rect ( ).right ( ) - lsize.width ( ), ( rect ( ).bottom ( ) + 1 -
    	lsize .height ( ) ) / 2 );
    	//Изменить отступы текста внутри поля ввода в зависимости от видимости
    	if ( mIconLabel-> isVisible ( ) )
    	{
    		//Добавить отступ справа чтобы текст не накладывался на пиктограмму
    		QMarginslMargins = textMargins ( );
    		lMargins.setright ( mIconLabel->size ( ).width ( ) + 1 );
    		setTextMargins ( lMargins );
    	}
    	else
    	{
    		//Убрать отступы
    		setTextMargins ( QMargins ( 0, 0, 0, 0 ) );
    	}
    }

    Чтобы показать значок, мы используем метку, изображения для которой можно передать с помощью метода setPixmap(). Этот метод принимает экземпляр QPixmap класса для работы с растровыми изображениями.

    Метод updateIconPositionAndSize() обновляет размер и размещение для метки. Для того, чтобы растянуть\сжать изображения мы передаём true методу метки setScaledContents(). Это позволяет игнорировать размеры изображения и изменить размер для значков. Далее мы устанавливаем фиксированный размер для метки, таким образом, чтобы её пропорции подходили для размещения в поле. Затем размещаем метку в правом конце текстового поля.

    Для того, чтобы установить тот или иной режим отображения значка запрограммируем метод setIconVisibility(). Добавим объявление метода в файл описания, а также добавим описание перечисления IconVisibilityMode, содержащее режимы отображения пиктограммы.

    public :
    	//Режимы отображения пиктограммы, которые определяют её поведение
    	enum IconVisibilItyMode
    	{
    		//Всегда отображать пиктограмму
    		IconAlwaysVisible =0,
    		//Отображать пиктограмму после наведения на поле ввода
    		IconVisibleOnHover,
    		//Отображать пиктограмму при присутствии текста
    		IconVisibleOnTextPresence,
    		//Отображать пиктограмму при отсутствии текста
    		IconVisibleOnEmptyText,
    		//Всегда прятать пиктограмму
    		IconA lwaysH idden
    	};
    void setIconVisibilIt y ( IconVisibilItyMode pIconVisibilItyMode );
    ... .
    private slots :
    	void slotTextChanged ( const QString pText );
    private :
    	void updateIconPositionAndsize ( );
    	void setIconVisible ( bool pisVisible );
    private :
    	IconVisibilItyMode mIconVisibilItyMode; //Режим отображения

    Добавим к файлу iconizedlineedit.cpp реализацию для этих методов.

    //Устанавливает режим отображения для пиктограммы
    void IconizedLineEdit::setIconVisibilIty ( IconVisibilItyMode
    	pIconVisibilItyMode )
    {
    	//Сохранение режима
    	mIconVisibilItyMode = pIconVisibilItyMode;
    	//Выполняем изменения соответсвенно к установленому значению
    	switch ( pIconVisibilItyMode )
    	{
    	case IconAlwaysVisible :
    		setIconVisible ( true );
    		break;
    	case IconVisibleOnEmptyText :
    	case IconVisibleOnTextPresence :
    		slotTextChanged ( Text( ) );
    		break;
    	default :
    		setIconVisible ( false );
    		break;
    	}
    }
    //Реализует реакцию на изменение текста в поле для режимов IconVisibleOnEmptyText
    //и IconVisibleOnNotEmptyText
    void IconizedLineEdit::slotTextChanged ( const QString pText )
    {
    	if ( IconVisibleOnEmptyText == mIconVisibilItyMode )
    	{
    		setIconVisible ( pText.isEmpty ( ) );
    	}
    	else if ( IconVisibleOnTextPresence == mIconVisibilItyMode )
    	{
    		setIconVisible ( ! pText.isEmpty ( ) );
    	}
    }
    //Сделать пиктограмму видимой или спрятать
    void IconizedLineEdit::setIconVisible ( bool pisVisible )
    {
    	//Показать/скрыть метку с пиктограммой
    	mIconLabel->setVisible ( pisVisible );
    }

    Для того, чтобы слот slotTextChanged(QString) работал, добавим в конструктор сигнально-слотовое соединение. Также добавим начальную инициализацию для поля mIconVisibilityMode.

    //Конструктор класса
    IconizedLineEdit::IconizedLineEdit ( QWidget * parent ) : QLineEdit ( parent ),
    	mIconVisibilItyMode ( IconAlwaysVisible ) //Инициализация
    {
    	mIconLabel = new QLabel ( this ); //Создаём метку для того, чтобы показать пиктограмму
    	//Обработка изменения текста в поле
    	connect ( this, SIGNAL( textChanged ( QString ) ), this,SLOT( slotTextChanged (
    		QString ) ), Qt::UniqueConnection );
    }

    Чтобы использовать наш виджет в программе, добавим файл описания класса и создадим несколько экземпляров, которые разместим на форме. Ответственным за создание интерфейса окна будет отдельный метод createUi().

    В файле описания класса главного окна напишем:

    #include <QWidget>
    class IconizedLineEdit;
    class MainWindow : public QWidget
    {
    	Q_OBJECT
    public :
    	explicit MainWindow ( QWidget * parent = 0 );
    private :
    	void createUi ( );
    private :
    	IconizedLineEdit * IconizedLineEdit;
    	IconizedLineEdit * IconizedLineEdit_ 2;
    	IconizedLineEdit * IconizedLineEdit_ 3;
    	IconizedLineEdit * IconizedLineEdit_ 4;
    	IconizedLineEdit * IconizedLineEdit_ 5;
    };

    В файле реализации главного окна разместим код:

    #include " mainwindow .h "
    #include " IconizedLineEdit .h "
    #include <QVBoxLayout>
    MainWindow::MainWindow ( QWidget * parent ) :
    QWidget ( parent )
    {
    	createUi ( );
    }
    void MainWindow::createUi ( )
    {
    QVBoxLayout * lMa inLayout = new QVBoxLayout;
    set Layout ( lMa inLayout );
    IconizedLineEdit = new IconizedLineEdit;
    IconizedLineEdit ->setPlaceholderText( " Click to open File " );
    IconizedLineEdit ->setIconPixmap (QPixmap ( " Folder.png " ) );
    IconizedLineEdit ->setIconVisibilIt y ( IconizedLineEdit::IconAlwaysVisible );
    lMa inLayout->addWidget ( IconizedLineEdit );
    IconizedLineEdit_2 = new IconizedLineEdit;
    iconizedLineEdit_2 ->setPlaceholderText( " Enter IP address " );
    iconizedLineEdit_2 ->setIconPixmap (QPixmap ( " Checkmark.png " ) );
    iconizedLineEdit_2 ->setIconVisibilIt y ( IconizedLineEdit::IconAlwaysVisible );
    lMa inLayout->addWidget ( IconizedLineEdit_2 );
    IconizedLineEdit_3 = new IconizedLineEdit;
    iconizedLineEdit_3 ->setPlaceholderText( " " );
    iconizedLineEdit_3 ->setIconPixmap (QPixmap ( " Questions.png " ) );
    iconizedLineEdit_3 ->setIconVisibilIt y ( IconizedLineEdit : :
    	IconVisibleOnTextPresence );
    lMa inLayout->addWidget ( IconizedLineEdit_3 );
    IconizedLineEdit_4 = new IconizedLineEdit;
    iconizedLineEdit_4 ->setPlaceholderText( " Cannot be empty..." );
    iconizedLineEdit_4 ->setIconPixmap (QPixmap ( " Warning.png " ) );
    iconizedLineEdit_4 ->setIconVisibilIt y ( IconizedLineEdit : :
    	IconVisibleOnEmptyText );
    lMa inLayout->addWidget ( IconizedLineEdit_4 );
    IconizedLineEdit_5 = new IconizedLineEdit;
    iconizedLineEdit_5 ->setPlaceholderText( " Clearable " );
    iconizedLineEdit_5 ->setIconPixmap (QPixmap ( " X.png " ) );
    iconizedLineEdit_5 ->setIconVisibilIt y ( IconizedLineEdit : :
    	IconVisibleOnTextPresence );
    lMa inLayout->addWidget ( IconizedLineEdit_5 );
    }

    Пути к файлам изображений для значков мы передаём в конструктор QPixmap. Изображения должны находиться в текущей папке (папке, в которой расположен файл проекта).

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

    14.2 Управление памятью. Иерархии объектов

    Как мы уже знаем, каждый объект может иметь "родительские" и "дочерние" объекты. Таким образом объекты организуют в иерархические структуры напоминающие дерево. Каждый из объектов может содержать дочерние объекты, а также иметь родительский объект. Отношение между объектами можно задать либо воспользовавшись параметром конструктора (при создании), или же с помощью метода QObject::setParent(). Обычно пользуются первым способом, то есть задают родительский объект при конструировании.

    //Задаём родительский виджет с помощью конструктора
    QLabel * lLabel = new QLabel ( lSomeWidget );
    	//Задаём родительский виджет с помощью метода QObject::setparent ( )
    	QPushButton * lPushButton = new QPushButton;
    	lPushButton->setParent ( lSomeWidget );

    Если виджет содержит компоновщики, то и компоновщики и все размещённые в нём виджеты автоматически получат "родителя" (ведь каждый визуальный элемент наследует от QWidget, а тот в свою очередь наследует от QObject). Объекты также могут менять "родителя", для этого достаточно вызвать метод QObject::setParent (QObject *) с указателем на другой родительский объект. Объектную иерархию можно увидеть наглядно, воспользовавшись методом dumpObjectTree() класса QObject. Он выводит в стандартный поток вывода тип и имя всех дочерних объектов. Зададим имена для объектов lLabel и lPushButton:

    lLabel ->setobjectName ( " childLabel " );
    lPushButton->setobjectName ( " child PushButton " );

    Если теперь вызвать метод dumpObjectTree() для объекта lSomeWidget, то получим вывод:

    SomeWidget : :
    	QLabel::childLabel
    	QPushButton::childPushButton

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

    Управление памятью важно, поскольку неуправляемое выделение памяти приводит к утечке памяти в программе — ситуации, когда программа не освобождает память. Если такое неуправляемое выделение памяти повторяется периодически, а сама программа выполняется относительно долго, то со временем программа будет занимать всё больше места в оперативной памяти. Наконец, таким образом можно исчерпать всю доступную память, что приведёт к аварийному завершению программы и исчерпанию ресурсов операционной системы.

    Например, при использовании ключевого слова new память выделяется в динамически распределяемой памяти (так называемой "куче" — heap). Динамическая память должна быть освобождена от созданных объектов с помощью ключевого слова delete для того, чтобы избежать утечки памяти (memory leak). Объекты, созданные в динамически распределяемой памяти могут существовать столько, сколько необходимо. Динамическая память не будет освобождена автоматически, поэтому программист должен самостоятельно следить за высвобождением выделенной памяти. Рассмотрим следующий пример:

    #include <QApplication>
    #include <QWidget>
    #include <QLabel>
    #include <QPushButton>
    int main ( int arg c, char * argv [ ] )
    {
    	QApplication lApplication ( arg c, argv );
    	//(1) Память в динамически-распределяемой памяти (heap)
    	QWidget _lSomeWidget = new QWidget ( 0 ); //Окно
    	//Задаём родительский виджет с помощью конструктора
    	QLabel * lLabel = new QLabel ( lSomeWidget );
    	//Задаём родительский виджет с помощью метода QObject::setparent ( )
    	QPushButton _ lPushButton = new QPushButton;
    	lPushButton->setParent ( lSomeWidget );
    	lLabel ->setText ( " Label " );
    	lLabel ->move ( 10, 10 );
    	lPushButton->setText ( " Button " );
    	lPushButton->move ( 50, 10 );
    	//(1) Память в динамически-распределяемой памяти (heap)
    	lSomeWidget->resize ( 150, 50 );
    	lSomeWidget->show ( );
    return lApplication.exec ( );
    }

    Хотя с первого взгляда программа может показаться корректной (она компилируется и выполняется), в ней есть ошибка, связанная с некорректным управлением памятью. Для того, чтобы проанализировать использование памяти в программе, воспользуемся программой Valgrind. Это свободно-распространяемая программа для Linux с открытым кодом. Среда Qt Creator поддерживает работу с этой программой для анализа использования памяти и оптимизации программ на Qt.

    Перейдите в режим анализа Analyse (Анализ) воспользовавшись кнопкой на панели переключения режимов работы слева или воспользуйтесь комбинацией клавиш Ctrl+6. Снизу под окном редактирования появится дополнительная панель. В выпадающем списке выберите Valgrind Memory Analyzer (Анализатор памяти Valgrind) и запустите процесс анализа (нажмите на кнопку с пиктограммой треугольника на панели или выберите в главном меню Analyze->Valgrind Memory Analyzer (Анализ->Анализатор памяти Valgrind). Начнётся анализ работы программы и программа запустится. Затем закройте окно программы. После завершения работы на панели появится сообщение об утечке памяти (см. рис. 14.1).

    Сообщение указывает на строку:

    QWidget _lSomeWidget = new QWidget ( 0 ); //Окно
    (рис 14.1) Анализ памяти с помощью Valgrind.

    Это происходит из-за того, что для lSomeWidget была выделена динамическая память, но не была корректно высвобождена с помощью оператора delete. Для того, чтобы корректно освободить память, модифицируем последние строки программы:

    int exitCode = lApplication.exec ( );
    //(1) Память в динамически-распределяемой памяти (heap)
    delete lSomeWidget;
    return exitCode;

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

    Можно выделить память для родительского объекта в стеке. Память для объектов, созданных в стеке, освобождается автоматически как только объект выходит за пределы области видимости. Поэтому объекты, созданные в стеке, удаляются как только они выходят из области видимости. Для того, чтобы освободить память автоматически, достаточно только родительский объект создать в стеке. Как только родительский объект удаляется, будут автоматически удалены и все дочерние объекты. Мы можем модифицировать последний пример таким образом, чтобы память для родительского объекта была выделена в стеке. Для этого закомментируйте все части программы обозначенные (1) и измените текст программы:

    //(2) Память в стеке
    SomeWidget lSomeWidget;
    //Задаём родительский виджет с помощью конструктора
    QLabel * lLabel = new QLabel( lSomeWidget );
    //Задаём родительский виджет с помощью метода QObject::setparent ( )
    QPushButton * lPushButton = new QPushButton;
    lPushButton->setParent ( lSomeWidget );
    .....
    //(2) Память в стеке
    lSomeWidget.resize ( 1 5 0, 5 0 );
    lSomeWidget.show ( );

    При описании собственных классов необходимо учитывать следующие рекомендации:

  • задавать для конструкторов класса параметр, который принимает указатель на родительский объект и имеет значение по умолчанию 0;
  • создавать дополнительный конструктор, который принимает только параметр с указателем на родительский объект;
  • параметр с указателем на "родителя" желательно должен быть первым параметром среди параметров со значением по умолчанию (если параметров со значением по умолчанию несколько);
  • используйте ключевое слово this как указатель на родительский объект при создании объектов внутри своего класса. Например:
    #include <QWidget>
    class CustomWidget : public QWidget
    {
    	Q_OBJECT
    public :
    	explicit CustomWidget ( QWidget * parent = 0 );
    };
    //Конструктор
    CustomWidget::CustomWidget ( QWidget * parent ) : QWidget ( parent )
    {
    	//Задаём родительский виджет — this то есть экземпляр класса CustomWidget
    	QPushButton * lPushButton = new QPushButton ( this );
    	lPushButton->setGeometry ( 50, 50, 200, 30 );
    }
  • 14.3 События (Events). Обработка событий (Event handling)

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

  • события, возникшие спонтанно и поступающие от оконной системы, такие как нажатие клавиш, движения мышкой, манипуляции с окном программы (spontaneous events);
  • события, отправленные изнутри программы, созданные объектами в программе и направленные в цикл обработки событий (posted events) или напрямую к другому объекту (sent events).
  • Спонтанные события и большинство внутренних событий поступают в цикл обработки событий (event loop). Этот цикл последовательно рассматривает каждое из событий и отправляет на обработку конкретному объекту. Каждый объект в свою очередь имеет возможность обрабатывать событие, которое поступает к нему.

    Каждое событие в Qt существует в виде объекта, унаследованного от QEvent. Когда событие поступает на обработку, то для объекта-обработчика выполняется виртуальный метод QObject::event(). Этот метод обычно вызывает другой метод-обработчик события, который для большинства событий является отдельным. Это необходимо для удобства и гибкости, поскольку таким образом есть возможность запрограммировать обработку конкретного класса событий в отдельном методе. Почти все основные типы событий реализованы в виде отдельного класса. Например, события для движений и нажатий клавиш мышки имеют класс QMouseEvent, события для нажатий клавиш клавиатуры имеют класс QKeyEvent и т. п. В табл. 14.1 приведены события, которые часто необходимо обрабатывать при разработке программ с графическим интерфейсом.

    Часто употребляемые классы событий и их обработчики
    Название класса события Описание Обработчик события
    QMouseEvent Событие для движений мышкой и нажатия клавиш мышки. Посылается виджетам. Выполняется только при нажатии клавиши мышки. Для того, чтобы виджет постоянно отслеживал этот тип событий, необходимо передать true в метод виджета setMouseTracking(). клавиша мышки нажата
    QWidget::mousePressEvent()
    клавишу мышки отпустили
    QWidget::mouseReleaseEvent()
    клавишу мышки нажали два раза
    QWidget::mouseDoubleClickEvent()
    курсор мышки изменил позицию
    QWidget::mouseMoveEvent()
    QKeyEvent Событие для нажатия клавиш клавиатуры клавиша нажата
    QWidget::keyPressEvent()
    клавишу отпустили
    QWidget::keyReleaseEvent()
    QResizeEvent Сообщает об изменении размера виджета (изменение уже произошло)
    QWidget::resizeEvent()
    QPaintEvent Сообщает о необходимости перерисовки виджета
    QWidget::paintEvent()
    QTimerEvent Посылается через заданные интервалы времени объектом, который запустил один или несколько таймеров с помощью метода QObject::startTimer(). Каждый таймер имеет уникальный идентификатор, который возвращает метод QObject::startTimer() при создании нового таймера. Таймер можно остановить с помощью вызова метода QObject::killTimer().
    QObject::timerEvent()
    QCloseEvent Посылается окну, которое пользователь пытается закрыть. Позволяет контролировать закрыто окно после обработки события или нет.
    QWidget::closeEvent()

    Таким образом, для того, чтобы выполнить обработку события, необходимо переопределить нужный обработчик события, выполнить необходимые для обработки действия и, если необходимо, сохранить стандартное поведение для обработчика — вызвать обработчика события из родительского класса. Теперь вернёмся к одному из предыдущих примеров — собственному виджету для поля ввода с пиктограммой. После изменения размера окна (и поля ввода, которое было размещено в компоновке), нам необходимо обновить позицию значка для того, чтобы она всегда находилась в правом конце поля ввода. Для этого мы можем переопределить обработчик события QResizeEvent для класса IconizedLineEdit:

    //Событие изменения размера виджета
    void IconizedLineEdit::resizeEvent ( QRes izeEvent *pEvent )
    {
    	//Если изменение размера состоялось, обновить позицию и размер пиктограммы
    	updateIconPositionAndsize ( );
    	QWidget::resizeEvent ( pEvent );
    }

    Для обновления позиции пиктограммы достаточно ещё раз вызвать метод IconizedLineEdit::updateIconPositionAndSize(). После изменения размера поля ввода позиция пиктограммы будет автоматически обновлена.

    14.4 Фильтры событий (Event filters). Распространение (всплытие) событий (Event propagation)

    Когда цикл обработки посылает событие объекту-адресату, он ожидает, что объект соответствующим образом обработает событие или сообщит о том, что он не будет выполнять обработку. Во втором случае некоторые события могут быть распространены (propagated), то есть переданы на обработку к родительскому объекту, если дочерний объект проигнорировал событие. Примером событий, для которых работает распространение — это события мышки и нажатий клавиш клавиатуры. Они будут "всплывать" по объектной иерархии от дочернего объекта к родительскому, пока один из этих объектов не обработает событие или все не проигнорируют его.

    Для управления этим процессом используют методы QEvent::accept() (для обозначения события как обработанного) и QEvent::ignore() (для игнорирования события). Их используют только в виртуальных методах-обработчиках. Обычно QEvent::accept() явно не вызывают, поскольку большинство распространяемых событий обозначаются как обработанные по умолчанию.

    Несмотря на гибкую систему контроля обработки событий, иногда возникает потребность выполнить обработку вместо объекта-адресата. Также иногда возникает необходимость заблокировать (отфильтровать) некоторые события, которые поступают на обработку к объекту. Этого можно достичь с помощью фильтров событий (event filters).

    Фильтры событий являются обычными объектами, унаследованными от QObject. Но перед тем, как событие поступит к адресату, оно поступает к фильтру событий. Он может передать его дальше на обработку адресату, отсеять или выполнить необходимые действия в ответ. Для обработки событий, фильтру необходимо переопределить виртуальный метод QObject::eventFilter(). Этот метод возвращает логическое значение, которое означает, будет ли событие отфильтровано (значение true) или поступит на дальнейшую обработку (значение false). Фильтр событий для объекта можно задать с помощью метода QObject::installEventFilter(), который принимает указатель на QObject, выступающий в роли фильтра.

    Наш предыдущий пример содержит поле ввода с пиктограммой. Для того чтобы реализовать реакцию на нажатие клавиши мыши на пиктограмму, но не создавать своего класса, унаследованного от класса метки с пиктограммой, мы добавим фильтр событий, который будет посылать сигнал, если для пиктограммы поступит событие типа QEvent::MouseButtonPress. Добавим объявление класса

    QEvent: #include <QEvent>

    Установим фильтр событий для метки: mIconLabel->installEventFilter(this);

    И добавим фильтр событий для метки в файле IconizedLineEdit.h:

    class IconizedLineEdit : public QLineEdit
    {
    	Q_OBJECT
    public :
    .....
    	void setIconClickable ( bool pIsIconClickable );
    .....
    signals :
    	void iconPressed ( );
    protected :
    .....
    	bool eventFilter ( QObject *pobject, QEvent *pEvent );
    .....
    private :
    .....
    	bool mIsIconCklickable;
    };

    В файле IconizedLineEdit.cpp:

    bool IconizedLineEdit::eventFilter ( QObject *pobject, QEvent *pEvent )
    {
    	if ( mIsIconCklickable )
    	{
    		if ( ( pobj e ct==mIconLabel )  ( pEvent->type ( )== QEvent::MouseButtonPress ) )
    		{
    			emit iconPressed ( );
    			return true;
    		}
    	}
    return false;
    }
    //Установить режим реакции на нажатие мышкой на пиктограмму
    void IconizedLineEdit::setIconClickable ( bool pIsIconClickable )
    {
    	mIsIconCklickable = pIsIconClickable;
    	//Устанавливаем вид курсора при наведении на метку с пиктограммой
    	if ( mIsIconCklickable )
    	{
    		mIconLabel->setCursor (Qt::PointingHandCursor );
    	}
    	else
    	{
    		mIconLabel->setCursor (Qt::ArrowCursor );
    }
    }

    Наш класс для поля ввода с пиктограммой одновременно выступает фильтром событий для метки.

    Добавим реакцию на нажатие к двум полям в mainwindow.cpp. Первое поле будет открывать диалог выбора файла и записывать путь к файлу. Последнее поле будет стирать своё содержимое в ответ на нажатие пиктограммы:

    #include <QFileDialog>
    .....
    void MainWindow::createUi ( )
    {
    .....
    IconizedLineEdit ->setIconClickable ( true );
    connect ( IconizedLineEdit, SIGNAL( iconPressed ( ) ), this, SLOT( slotChooseFile ( ) ),
    	Qt::UniqueConnection );
    .....
    iconizedLineEdit_5 ->setIconClickable ( true );
    connect ( iconizedLineEdit_5, SIGNAL( iconPressed ( ) ),
    iconizedLineEdit_5, SLOT( clear ( ) ), Qt::UniqueConnection );
    .....
    }
    void MainWindow::slotChooseFile ( )
    {
    QString lFileName = QFileDialog::getOpenFileName ( this, " open File " );
    IconizedLineEdit ->setText ( lFileName );
    }

    Объявления слота slotChooseFile() добавим к описанию класса главного окна:

    class MainWindow : public QWidget
    {
    	Q_OBJECT
    ..... .
    private slots :
    	void slotChooseFile ( );
    ..... .
    };

    После этого наш пример окончательно готов (см. рис. 14.2).

    (рис 14.2) Пример: поле ввода с пиктограммой.

    Просуммируем наши знания об обработке событий в Qt:

  • события дают возможность проводить обработку различных ситуаций, возникающих во время работы программы;
  • есть два источника поступления событий. Спонтанные события поступают от оконной системы в ответ на действия пользователя (движения мышкой, нажатия клавиш клавиатуры, действия с окном программы и т. д.). События также возникают внутри самой программы и поступают от объектов, которые участвуют в работе программы;
  • события направляются на обработку объектам, участвующим в работе программы. За сбор и перенаправление событий к обработчикам ответственным является цикл обработки событий в программе;
  • каждое событие существует в виде объекта типа QEvent. Для большинства событий существуют специализированные классы этих событий, унаследованные от QEvent, которые содержат информацию о событии и данные необходимые для её корректной обработки;
  • для участия в обработке событий объект должен быть унаследован от класса QObject. События, которые поступают к объекту на обработку, автоматически передаются к виртуальному методу QObject::event();
  • для удобства, для большинства событий существуют отдельные виртуальные методы обработки, которые вызываются виртуальным методом QObject::event();
  • объекты имеют возможность быть фильтрами событий для других объектов в приложении. Для того, чтобы установить объект в качестве фильтра событий, пользуются методом installEventFilter(). Объект-фильтр должен переопределить виртуальный метод QObject::eventFilter(), который возвращает логическое значение, было событие отфильтровано (то есть, оно не будет направляться на обработку) или нет.
  • А вот общий алгоритм для программной обработки событий:

  • в случае, когда нет возможности унаследовать класс объекта и переопределить методы обработки, а также в случае, когда необходимо фильтровать, блокировать или предварительно обработать событие перед тем, как она будет отправлена на обработку к объекту — воспользуйтесь фильтром событий
  • переопределите виртуальный метод QObject::eventFilter() для объекта, который будет выступать в роли фильтра событий;
  • в методе eventFilter() проверьте объект к которому на обработку будет направляться событие;
  • дальше, определите тип события (QEvent::Type) и класс события, проверьте является ли полученное событие событием нужного вам типа;
  • приведите объект события до нужного вам класса событий;
  • выполните предварительную обработку события, если необходимо. Для того, чтобы отфильтровать событие (после этого оно не будет передаваться объекту на обработку) верните из метода логическое значение true. Для того, чтобы отправить событие на обработку объекту- адресату верните логическое значение false.
  • для некоторых типов событий, которые не имеют отдельного метода- обработчика, обработку можно запрограммировать, переопределив метод QObject::event()
  • переопределите виртуальный метод для объекта QObject::event(), который должен выполнять обработку;
  • определите тип события (QEvent::Type) и класс события, проверьте является ли полученное событие событием нужного вам типа;
  • приведите объект события до нужного вам класса событий;
  • выполните обработку или вызовите метод, который её выполнит, верните значение true, которое означает, что событие распознано и обработано, или false в случае, если объект не будет выполнять обработку. В этом случае событие может быть переслано на обработку родительскому объекту.
  • вызовите метод event() для базового класса, чтобы провести обработку всех остальных видов событий. Верните из метода значение, которое вернёт метод event() базового класса после выполнения.
  • если объект имеет виртуальный метод-обработчик события, используйте его
  • переопределите виртуальный метод для объекта QObject::event(), который должен выполнять обработку;
  • выполните обработку события. Вызовите метод accept() для события, если во время обработки убедились, что событие должно быть проработано данным объектом. В противном случае, вызовите ignore() для объекта события (в этом случае событие может быть направлено к родительскому объекту на обработку)
  • .

    14.5 Создание собственного элемента интерфейса. Создание свойств

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

    Определение свойства обычно состоит из макроса Q_PROPERTY, который описывает свойство и содержит описания:

  • метода для установки начального значения;
  • метода для получения значения;
  • метода для установки значения;
  • сигнала об изменении значения свойства;
  • дополнительных настроек.
  • Метод для установки значения принимает одно значение (того же типа, что и свойство) и возвращает void. Метод для получения значения возвращает значение свойства.

    Обычно метод для получения значения называют так же как и свойство, а метод для установки значения ещё и имеет префикс "set". Метод для получения значения, который возвращает тип bool также обычно имеет префикс "is", например:

    QStirng Text( ) const;
    void setText ( const QString Text);
    bool isVisible ( ) const;
    void setVisible ( bool isVisible );

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

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

    Для начала, создадим оконный проект и добавим к нему класс LedIndicator, унаследованный от QWidget:

    class LedIndicator : public QWidget
    {
    	Q_OBJECT
    	Q_PROPERTY( QString TextREAD TextWRITE setText )
    	Q_PROPERTY( bool turnedOn READ isTurnedOn WRITE setTurnedOn NOTIFY
    	state toggled )
    public :
    	explicit LedIndicator ( QWidget * parent = 0 );
    	QString Text( ) const;
    	bool isTurnedOn ( ) const;
    signals :
    	void state toggled ( bool );
    public slots :
    	void setText ( const QString );
    	void setTurnedOn ( bool );
    private :
    	QString mText;
    	bool mIsTurnedOn;
    };

    Мы определили два свойства: text типа QString для надписи, а также turnedOn типа bool для состояния индикатора. Специальные слова READ и WRITE в описании свойства обозначают названия методов для установления и изменения значения свойства. В описании второго свойства использовано слово NOTIFY для обозначения названия сигнала, который будет сообщать об изменении свойства. Добавим реализацию для класса:

    #include <QPainter>
    #include <QPaintEvent>
    //Радиус индикатора
    const int cLedRadius = 7;
    //Отступ между индикатором и надписью
    const int cLedSpacing = 5;
    LedIndicator::LedIndicator ( QWidget * parent ) : QWidget ( parent ),
    mIsTurnedOn ( false ) //Инициализируем начальным значением!
    {
    }
    //Метод получения значения — свойство text
    QString LedIndicator::Text( ) const
    {
    	return mText;
    }
    //Метод получения значения — свойство turnedOn
    bool LedIndicator::isTurnedOn ( ) const
    {
    	return mIsTurnedOn;
    }
    //Метод установки значения — свойство text
    void LedIndicator::setText ( const QString pText )
    {
    	mText = pText;
    }
    //Метод установки значения — свойство turnedOn
    void LedIndicator::setTurnedOn ( bool pIsTurnedOn )
    {
    	//Проверка уже установленного значения
    	if ( isTurnedOn ( ) == pIsTurnedOn )
    	{
    		return;
    	}
    	mIsTurnedOn = pIsTurnedOn;
    	//Выпускаем сигнал про изменение
    	em it state toggled ( mIsTurnedOn );
    	//Вызываем метод QWidget::update(), который добавляет в очередь событий QPaintEvent
    	//для того чтобы перерисовать наш виджет в соответствии с установленным isTurnedOn()
    	update ( );
    }

    Обратите внимание: в большинстве случаев при создании собственных слотов, которые получают и устанавливают значения, следует сравнивать переданное значение с текущим и запрещать выполнение тела слота, если переданное и текущее значения равны. Особенно за этим необходимо следить, если слот выдаёт сигнал об изменении значения. Об этом необходимо помнить, чтобы избежать бесконечной рекурсии и аварийного завершения программы в случае перекрёстного сигнально-слотового соединения между такими слотами. Именно такую проверку мы выполнили в методе setTurnedOn() перед отправкой сигнала об изменении и перерисовкой.

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

    14.6 Рисование элементов. Класс QPainter

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

    QPainter обладает богатым набором методов для рисования различных геометрических примитивов, надписей и частей изображений. Для рисования он использует объекты класса QPaintDevice, реализующие область для вывода графической информации (на экран, область памяти, на принтер и т. д.). Класс QWidget как раз наследует от классов QObject и QPaintDevice, что позволяет использовать QPainter для рисования интерфейса.

    Оконная система и Qt следят за изменениями размеров, позиции, видимости окон и отдельных виджетов в программе и направляют специальные события, которые сообщают каждый виджет о необходимости обновления вида. В виртуальном обработчике paintEvent() виджеты имеют возможность использовать QPainter для собственного отражения. В нашем примере мы определим обработчик события QPaintEvent.

    #include <QWidget>
    class LedIndicator : public QWidget
    {
    	Q_OBJECT
    	Q_PROPERTY( QString TextREAD TextWRITE setText )
    	Q_PROPERTY( bool turnedOn READ isTurnedOn WRITE setTurnedOn NOTIFY
    		state toggled )
    public :
    .....
    	QSizeminimumSizeHint ( ) const;
    ... .
    protected :
    	void paintEvent ( QPaintEvent *);
    .....
    };

    И добавим реализацию для него:

    void LedIndicator::paintEvent ( QPaintEvent *pEvent )
    {
    //Создаём объект QPainter и указываем QPaintDevice текущий виджет
    QPainter lPainter ( this );
    //Используем сглаживание при рисовании для лучшего вида
    lPainter.setRenderHint ( QPainter::Antialiasing );
    //Центр окружности индикатора QPoint — класс для описания точки
    QPoint lLedCenter ( cLedRadius + 1, height ( ) / 2 );
    //Фигура, которую мы будем рисовать QPainterPath — класс для описания фигуры
    //состоящей из нескольких примитивов
    QPainterPath lPath;
    //Добавляем окружность
    lPath.addEllipse ( lLedCenter, cLedRadius, cLedRadius );
    lPainter.save ( ); //Сохраняем настройки после всех изменений мы восстановим их для
    //рисования подписи
    //Создаём радиальный (окружностями) градиент указываем центр для градиента и радиус
    QRadialGradient lGradient ( lLedCenter, cLedRadius );
    	if ( mIsTurnedOn ) //Устанавливаем цвет границы и градиент
    	{ //для включённого и выключенного состояний
    		//Задаём объект QPen — настройки рисования контуров
    		//Используем константу для задания цвета контура в конструкторе QPen
    		lPainter.setP en (QPen(Qt::darkGreen ) );
    		//Задаём цвет в разных точках (0 — центр, 1 — край) цвет будет равномерно изменяться
    		//Для задания цвета пользуемся текстовым шестнадцатеричным RGB обозначением
    		//— неявное преобразование в QColor
    		lGradient.setcolorA t ( 0.2, " #70FF70 " );
    		lGradient.setcolorA t ( 1, " #00CC00 " );
    	}
    	else
    	{
    		//Здесь задаём чёрный цвет. Конструктор QColor, значение красной,
    		//зелёной и синей (0-255) компонент цвета
    		lPainter.setPen (QPen( QColor ( 0, 0, 0 ) ) );
    		lGradient.setcolorAt ( 0.2, Qt::gray );
    		lGradient.setcolorAt ( 1, Qt::darkGray );
    	}
    //Заполняем фигуру индикатора градиентом
    lPainter.fillPath ( lPath, QBrush ( lGradient ) );
    //Рисуем границу индикатора
    lPainter.drawPath ( lPath );
    //Восстанавливаем настройки перед последним сохранением
    lPainter.restore ( );
    //Устанавливаем шрифт для рисования текста используем QWidget::font(),
    //чтобы иметь возможность стилизовать надпись
    lPainter.setFont ( font ( ) );
    //Квадрат, в котором будет рисоваться текст. QRect — класс для обозначения прямоуольной области
    QRect lTextrect ( cLedRadius*2+ cLedSpacing, 0, width ( )- ( cLedRadius *2 +
    	cLedSpacing ), height ( ) );
    //Рисуем текст в заданном прямоугольнике, выравнивание по левому краю и вертикально по
    	центру
    lPainter.drawText ( lTextRect, Qt::AlignVCenter | Qt::Alignleft, mText );
    }
    //Переопределяем виртуальный метод minimumSizeHint() для передачи корректных минимальных
    	размеров
    QS ize LedIndicator::minimumSizeHint ( ) const
    {
    	return QS ize ( cLedRadius * 2 //Диаметр индикатора
    	+ fontMetrics ( ).width (mText ) //Ширина текста mText
    	+ cLedSpacing, //Отступ
    	cLedRadius * 2 );
    }

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

    #include <QHBoxLayout>
    #include <QCheckBox>
    #include " LedIndicator .h "
    MainWindow::MainWindow ( QWidget * parent ) : QWidget ( parent )
    {
    //Главное компонование
    QHBoxLayout * lLayout = new QHBoxLayout;
    set Layout ( lLayout );
    //Создаём наш индикатор и добавляем его к компоновщику
    LedIndicator * lLedIndicator = new LedIndicator;
    l LedIndicator ->setText ( " LED Indicator " );
    lLayout->addWidget ( lLedIndicator );
    //Создаём и добавляем флажок
    QCheckBox *lCheckBox = new QCheckBox ( " Led ON " );
    lLayout->addWidget ( lCheckBox );
    //Соединяем флажок и индикатор
    connect ( lCheckBox, SIGNAL( toggled ( bool ) ),
    l LedIndicator, SLOT( setTurnedOn ( bool ) ), Qt::UniqueConnection );
    }

    Окончательно, окно нашей программы имеет вид (см. рис. 14.3).

    (рис 14.3) Пример: виджет-индикатор.

    14.7 Задачи для самостоятельного решения

  • Создайте окно для ввода имени пользователя и пароля. Используйте для этого класс IconizedLineEdit.
  • Добавьте возможность проверки правильности введённых данных с помощью класса QValidator.
  • Создайте программу, которая открывает изображение в окне. Для ввода пути к изображению добавьте к окну поле ввода. Для поля ввода пути к изображению используйте класс IconizedLineEdit. При нажатии на пиктограмму должен открываться диалог выбора файла.
  • Добавьте к классу LedIndicator поддержку третьего промежуточного состояния. Для включения поддержки этого состояния, добавьте метод setTristate(). Для установки и чтения состояния индикатора добавьте методы setState.
  • Вернуться к учебному плану