В первой статье я рассказывал как мог о достоинствах фреймворка. Сегодня я попытаюсь рассказать о его темной стороне, плохо освещенной в документации.
Мы хотим изменять размер сцены и объектов в ней согласно размеру отображаемого окна. В доке сказано:«QGraphicsView takes ownership of the viewport widget». Ну что-ж, создадим простейшим проект и напишем следующее:
MainWindow.h:
MainWindow.cpp:
Ожидается, что меняя размера виджета, мы будем менять размер эллипса, но он по-прежнему будет виден целиком, т.к. изменится размер и отображаемой области. Компилируем, запускаем и убеждаемся, что как надо не работает: можно уменьшить первоначальный размер, но вот при превышении эллипс обрезается.
Тогда меняем
на
Компилируем, запускаем — все работает. Теперь попробуем установить фильтр в сцену. Компилируем, запускаем и ничего не видим, вообще.
Хотим отслеживать положение мыши, при ее перемещении по сцене. Для это модифицируем наш тестовый и класс, следующим образом:
В конструктор дописываем:
Ну и фильтр инсталлируем в graphicsView.
Компилируем, запускаем, убеждаемся что не работает. Инсталлируем фильтр в viewport и все снова работает.
Теперь немного модифицируем:
заменим на
И инсталлируем фильтр в сцену. И в отличии от прошлого раза мы обнаружим, что перемещение мыши отслеживается.
Как это было бы не странно с точки зрения новичка, но Graphics Framework действительно следует логике: отображение отдельно, а композиция отдельно. И потому событие об изменении размера не передается в сцену. И graphicsView действительно берет шефство над viewport. В этом можно убедится если в деле номер №1 заменить фильтр на:
Т.е. убрать фильтрацию события, то мы увидим ожидаемое поведение. Т.е. событие отфильтровалось и дальше не было передано в viewport. А вот адекватное поведение при уменьшении вызвано внутренней логикой Qt или спонтанными событиями, как их именуют в коде.
Вернемся к передачи событий в сцену. Этот процесс вообще не описан, а здесь есть важный момент: перед тем как отправится в сцену graphicsView преобразуют событие в соответствующий QGRaphisSceneEvent, но происходит это уже в специализированных методах, собственно поэтому в доке и рекомендует переопределять их, а не точку входа (event или viewportEvent).
Еще раз обращу внимания на последний момент: логика фреймворка такова, что все внешние события подготавливаются для работы со сценой. Т.е. если у вас есть ваш QEvent, который должен быть передан сцене, то лучший способ, не нарушающий логику Qt — это создать аналог кастомного события, отнаследованный от QGRaphicsSceneEvent, написать метод преобразование и посылки преобразованного в сцену, который будет вызываться в классе, отнаследованном от QGraphicsView, ну и посылать QEvent в этот-самый модифицированный QGraphicsView.
Осталось рассмотреть последний этап: доставка события к Item-ам. Здесь картина аналогичная с доставкой до сцены, а именно: после того как событие пришло определяется какую специализированную функцию надо задействовать; в этой специализированной функции определяются координаты события, с помощью методов itemAt() определяется каким Item-а доставлять событие, затем событие подготавливается к доставке в Item и только после этого отправляется в него.
Кстати, здесь тоже грабли лежат. В доке сказано, что перед изменением геометрических размеров нужно вызывать geometryChange, в противном случае наткнемся на проблему с отрисовкой. Но не только, проблемы возникнут и с доставкой событий.
Дело №1
Мы хотим изменять размер сцены и объектов в ней согласно размеру отображаемого окна. В доке сказано:«QGraphicsView takes ownership of the viewport widget». Ну что-ж, создадим простейшим проект и напишем следующее:
MainWindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsEllipseItem>
#include <QGraphicsView>
class MainWindow : public QWidget
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
bool eventFilter(QObject *, QEvent *);
private:
QGraphicsScene *m_scene;
QGraphicsEllipseItem *m_elipse;
QGraphicsView * graphicsView;
};
#endif // MAINWINDOW_H
MainWindow.cpp:
#include "MainWindow.h"
#include <QGridLayout>
#include <QEvent>
#include <QResizeEvent>
MainWindow::MainWindow(QWidget *parent) :
QWidget(parent)
{
setLayout(new QGridLayout());
graphicsView = new QGraphicsView();
layout()->addWidget(graphicsView);
graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scene = new QGraphicsScene();
m_elipse = new QGraphicsEllipseItem();
m_scene->addItem(m_elipse);
m_scene->setSceneRect(m_elipse->boundingRect());
graphicsView->setScene(m_scene);
graphicsView->installEventFilter(this);
}
MainWindow::~MainWindow()
{
}
bool MainWindow::eventFilter(QObject *, QEvent *event)
{
if(event->type() == QEvent::Resize )
{
QResizeEvent *res = reinterpret_cast<QResizeEvent*>(event);
m_elipse->setRect(0, 0, res->size().width(), res->size().height());
return true;
}
return false;
}
Ожидается, что меняя размера виджета, мы будем менять размер эллипса, но он по-прежнему будет виден целиком, т.к. изменится размер и отображаемой области. Компилируем, запускаем и убеждаемся, что как надо не работает: можно уменьшить первоначальный размер, но вот при превышении эллипс обрезается.
Тогда меняем
graphicsView->installEventFilter(this);
на
graphicsView->viewport()->installEventFilter(this);
Компилируем, запускаем — все работает. Теперь попробуем установить фильтр в сцену. Компилируем, запускаем и ничего не видим, вообще.
Дело №2
Хотим отслеживать положение мыши, при ее перемещении по сцене. Для это модифицируем наш тестовый и класс, следующим образом:
bool MainWindow::eventFilter(QObject *, QEvent *event)
{
if(event->type() ==QEvent::MouseMove)
{
qDebug()<<event;
return true;
}
return false;
}
В конструктор дописываем:
graphicsView->setMouseTracking(true);
Ну и фильтр инсталлируем в graphicsView.
Компилируем, запускаем, убеждаемся что не работает. Инсталлируем фильтр в viewport и все снова работает.
Теперь немного модифицируем:
if(event->type() ==QEvent::MouseMove)
заменим на
if(event->type() ==QEvent::GraphicsSceneMouseMove)
И инсталлируем фильтр в сцену. И в отличии от прошлого раза мы обнаружим, что перемещение мыши отслеживается.
Выводы следствия
Как это было бы не странно с точки зрения новичка, но Graphics Framework действительно следует логике: отображение отдельно, а композиция отдельно. И потому событие об изменении размера не передается в сцену. И graphicsView действительно берет шефство над viewport. В этом можно убедится если в деле номер №1 заменить фильтр на:
if(event->type() == QEvent::Resize )
{
QResizeEvent *res = reinterpret_cast<QResizeEvent*>(event);
m_elipse->setRect(0, 0, res->size().width(), res->size().height());
}
return false;
Т.е. убрать фильтрацию события, то мы увидим ожидаемое поведение. Т.е. событие отфильтровалось и дальше не было передано в viewport. А вот адекватное поведение при уменьшении вызвано внутренней логикой Qt или спонтанными событиями, как их именуют в коде.
Вернемся к передачи событий в сцену. Этот процесс вообще не описан, а здесь есть важный момент: перед тем как отправится в сцену graphicsView преобразуют событие в соответствующий QGRaphisSceneEvent, но происходит это уже в специализированных методах, собственно поэтому в доке и рекомендует переопределять их, а не точку входа (event или viewportEvent).
Еще раз обращу внимания на последний момент: логика фреймворка такова, что все внешние события подготавливаются для работы со сценой. Т.е. если у вас есть ваш QEvent, который должен быть передан сцене, то лучший способ, не нарушающий логику Qt — это создать аналог кастомного события, отнаследованный от QGRaphicsSceneEvent, написать метод преобразование и посылки преобразованного в сцену, который будет вызываться в классе, отнаследованном от QGraphicsView, ну и посылать QEvent в этот-самый модифицированный QGraphicsView.
Осталось рассмотреть последний этап: доставка события к Item-ам. Здесь картина аналогичная с доставкой до сцены, а именно: после того как событие пришло определяется какую специализированную функцию надо задействовать; в этой специализированной функции определяются координаты события, с помощью методов itemAt() определяется каким Item-а доставлять событие, затем событие подготавливается к доставке в Item и только после этого отправляется в него.
Кстати, здесь тоже грабли лежат. В доке сказано, что перед изменением геометрических размеров нужно вызывать geometryChange, в противном случае наткнемся на проблему с отрисовкой. Но не только, проблемы возникнут и с доставкой событий.