суббота, 6 октября 2012 г.

Как сделать "живую" кнопку с картинкой в Qt

... или QWidget с использованием CSS и обработка его событий HoverEnter и HoverLeave в обработчике eventFilter()

Задача о "живой" кнопке с картинкой в Qt

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

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

  1. Простейшее использование CSS для виджета Qt.
  2. Простейшее размещение и использование файла с картинкой в ресурсе Qt.
  3. Установка фильтра событий на объект класса QLabel и реализация фильтра событий.
  4. Обработка событий QEvent::HoverEnter и QEvent::HoverLeave.

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

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

    QWidget m_pwgtTitle;
    QString m_sStyleOnExpanded;

    ...

    m_sStyleOnExpanded = "* "
            " { "
            " border-width: 1px; "
            " border-style: solid; "
            " border-top-left-radius: 10px; "
            " border-top-right-radius: 10px; "
            " border-color: white; "
            " padding: 5px; "
            " background: qlineargradient(x1:0.5, y1:0, x2:0.5, y2:1, "
            " stop:0 #33CCCC, stop: 0.4 #33FFFF, stop:1 #339999) "
            " }";

    m_pwgtTitle = new QWidget(this);
    m_pwgtTitle->setStyleSheet(m_sStyleOnExpanded);

Из примера видно, что для код CSS надо записать в строковую переменную и передать ее значением в метод setStyleSheet() того виджета, к которому необходимо применить таблицу стилей.

Чтобы просто разместить на виджете картинку средствами CSS нужна значительно более простая запись стиля. Ниже показаны две строки стиля, которые будут использованы для нашего демо-виджета в обычном состоянии и в состоянии on-hover (под курсором мыши).

    m_sCallNormalStyle = "* { background-image: 
        url(:/images/images/green-phone-button.png); }";
    m_sCallOnHoverStyle = "* { background-image: 
        url(:/images/images/green-phone-button-on-hover.png); }";

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

Создание и использование ресурсов в Qt

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

Я создаю файл ресурсов средствами QtCreator. Так же, с помощью QtCreator я управляю этим файлом. Тех, кто хочет сделать это иначе можно обрадовать. Файл ресурсов это обычный текстовый файл с расширением *.qrc внутри которого содержится XML-описание ресурсов. Для того, чтобы система сборки Qt использовала обработку и линковку файла ресурсов, его надо включить в раздел RESOURCES проектного файла. Так, если вы имеете файл ресурсов с названием resourses.qrc, то он должен быть описан в файле проекта следующим образом.

RESOURCES += \
    resources.qrc

С помощью меню QtCreator, добавьте в проект файл ресурса, откройте его щелчком в панели файлов проекта, создайте там нужный префикс (раздел) и добавьте под этот префикс те файлы, которые вы хотите разместить в ресурсах. Файлы должны лежать в какой-нибудь поддиректории проекта, иначе они будут скопированы в корень проекта. Для размещения картинок я создаю поддиректорию images и файлы из этой директории размещаю под префиксом /images.

Для нашей демонстрации я добавлю в файл ресурсов два файла из директории images и мой результирующий XML-файл с описанием ресурса будет выглядеть следующим образом.

<RCC>
    <qresource prefix="/images">
        <file>images/green-phone-button.png</file>
        <file>images/green-phone-button-on-hover.png</file>
    </qresource>
</RCC>

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

Если объект не имеет в своем описании сигнала о некотором нужном нам событии, то это не повод для уныния. Прежде всего, можно унаследовать данный класс с целью переопределения виртуального callback-метода с нужным событием. Однако, кроме этого, Qt предоставляет более удобную возможность - установку в данный виджет фильтра события и в реализации этого фильтра поймать и обработать нужное нам событие.

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

  1. Для формы F реализовать виртуальный метод virtual bool eventFilter(QObject *, QEvent *), который изначально было описан для класса QObject.
  2. Передать объект формы F как объект с реализацией фильтра событий в объект виджета W. Это делается с помощью метода installEventFilter(). Теперь объект виджета W будет вызывать метод фильтра объекта F при возникновении в объекте W разных событий.
  3. В реализации eventFilter() необходимо отловить требуемое событие и обработать его.

Приведем простейший пример обработки клавиатурных событий в объекте строки ввода QLineEdit.

// Описание класса формы со строкой ввода
class MyForm: public QWidget
{
public:
  MyForm();
...

protected:
  virtual bool eventFilter(QObject *, QEvent *)

private:
  QLineEdit *m_pleIn;
...
};

...

// Реализация конструктора
MyForm::MyForm()
{
  // Создаем объект строки ввода
  m_pleIn = new QLineEdit(this);

  // Передаем объект формы делегатом для 
  // получения событий от объекта строки ввода
  m_pleIn->installEventFilter(this);
}

bool MyForm::eventFilter(QObject *obj, QEvent *event)
{
    if (obj != m_pleIn) return false;

    if (event->type() == QEvent::KeyPress) {
        // Обрабатываем событие нажатия на клавишу
        QKeyEvent *ke = static_cast(event);
        int key = ke->key();

        if (key == Qt::Key_Escape) {
            // Обрабатываем клавишу Escape
            ... 
            return true;
        }

        if (key == Qt::Key_Up) {
            // Обрабатываем клавишу стрелка вверх
            ... 
            return true;
        }

        if (key == Qt::Key_Down) {
            // Обрабатываем клавишу стрелка вниз
            ... 
            return true;
        }
    }
    return false;
}

Обработка событий QEvent::HoverEnter и QEvent::HoverLeave

Данные события являются событиями мыши, которые возникают при наведении мыши на виджет (QEvent::HoverEnter) и при уходе мыши с поверхности виджета (QEvent::HoverLeave).

Особенностью обработки событий QEvent::HoverEnter и QEvent::HoverLeave в Qt является то, что для их обработки необходимо, чтобы виджет, для которого мы хотим обработать данные события, имел установленным атрибут Qt:WA_HOVER. Только после установки этого атрибута можно рассчитывать на возможность обработки этих событий в методе фильтра eventFilter().

Для нашей будущей демо-кнопки установка этого атрибута будет выполнена так.

  m_pwgtCall->setAttribute(Qt::WA_Hover);

Решение задачи о "живой" кнопке

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

// заголовочный файл класса SipAgentPanel
class SipAgentPanel : public QWidget
{
    Q_OBJECT
public:
  SipAgentPanel(QWidget *parent = 0);

protected:
  bool eventFilter(QObject *, QEvent *);

private:
  QWidget *m_pwgtCall;

  QString m_sCallNormalStyle;
  QString m_sCallOnHoverStyle;
};

// файл реализации класса SipAgentPanel
SipAgentPanel::SipAgentPanel(QWidget *parent)
    : QWidget(parent)
{
    m_sCallNormalStyle = "* { background-image: 
        url(:/images/images/green-phone-button.png); }";
    m_sCallOnHoverStyle = "* { background-image: 
        url(:/images/images/green-phone-button-on-hover.png); }";

    m_pwgtCall = new QWidget(wgt);
   
    // Установим жестко размер виджета по размеру картинок 72x66
    m_pwgtCall->setFixedSize(72,66);

    m_pwgtCall->setStyleSheet(m_sCallNormalStyle);
    m_pwgtCall->setAttribute(Qt::WA_Hover);
    m_pwgtCall->installEventFilter(this);
}

bool SipAgentPanel::eventFilter(QObject * obj, QEvent * event)
{
    if (obj == m_pwgtCall) {
        QEvent::Type type = event->type();
        if  (type == QEvent::HoverLeave) {

            m_pwgtCall->setCursor(Qt::ArrowCursor);
            m_pwgtCall->setStyleSheet(m_sCallNormalStyle);

        } else if (type == QEvent::HoverEnter) {

            m_pwgtCall->setCursor(Qt::PointingHandCursor);
            m_pwgtCall->setStyleSheet(m_sCallOnHoverStyle);

        } else if (type == QEvent::MouseButtonPress) {

            QMouseEvent *mev = static_cast(event);
            if (mev) {
                if (mev->button() == Qt::LeftButton) {
                    // обработка события щелчка по объекту
                    ...
                }
            }

        }
    }

    return QWidget::eventFilter(obj, event);
}

Для повторения этого проекта не забудьте разместить в файле ресурса соответствующие картинки.

Комментариев нет: