Пусть в приложении требуется функция увеличения произвольной области изображения, как под лупой. В качестве примера решения этой задачи скомпонуем простую Qt-программу:
В левой части отображается оригинал. Для него мы используем QGraphicsView и QGraphicsScene. Увеличенная копия выделенной части картинки выводится в правой верхней области окна с помощью QLabel.
Область увеличения изображения задается в виде прямоугольника, который можно перемещать при помощи мыши по принципу Drag&Drop. Размер области увеличения может быть изменен путем манипуляций с углами прямоугольника, соответствующего границам области, по аналогии с тем, к чему нас приучили всевозможные графические редакторы.
При любых изменениях области увеличения картинка в правом верхнем углу обновляется, чтобы отображать ровно то, что выделено на оригинальном изображении.
В качестве нашей основной рабочей поверхности воспользуемся QGraphicsView и QGraphicsScene. Определим виджет главного окна для приложения:
class CustomGraphicsItemDemo : public QWidget {
Q_OBJECT
public:
explicit CustomGraphicsItemDemo( QWidget* parent = 0 );
~CustomGraphicsItemDemo();
protected:
void resizeEvent( QResizeEvent* );
private slots:
void onSelectedAreaChanged( const QRectF& area );
private:
Ui::CustomGraphicsItemDemo* ui;
QGraphicsScene m_scene;
QPixmap m_pix;
};
И добавим базовую реализацию:
Каждый игрок в PUBG стремится к победе, но только лучшие знают, как её достичь.
CustomGraphicsItemDemo::CustomGraphicsItemDemo( QWidget* parent ) :
QWidget( parent ),
ui( new Ui::CustomGraphicsItemDemo ) {
ui->setupUi( this );
ui->grView->setScene( &m_scene );
m_pix.load( ":/images/image.jpeg" );
m_scene.setSceneRect( 0, 0, m_pix.width(), m_pix.height() );
m_scene.addPixmap( m_pix );
resizeEvent( NULL );
}
CustomGraphicsItemDemo::~CustomGraphicsItemDemo() {
delete ui;
}
void CustomGraphicsItemDemo::resizeEvent( QResizeEvent* ) {
static const int GRAPHICS_VIEW_MARGIN = 20;
double scale = std::min(
( ui->grView->width() - GRAPHICS_VIEW_MARGIN ) / m_scene.width(),
( ui->grView->height() - GRAPHICS_VIEW_MARGIN ) / m_scene.height()
);
ui->grView->resetMatrix();
ui->grView->scale( scale, scale );
}
void CustomGraphicsItemDemo::onSelectedAreaChanged( const QRectF& area ) {
static const int SCALED_VIEW_MARGIN = 20;
ui->lbScaledView->setPixmap(
m_pix.copy( area.toRect() ).scaled(
ui->lbScaledView->size().width() - SCALED_VIEW_MARGIN,
ui->lbScaledView->size().height() - SCALED_VIEW_MARGIN,
Qt::KeepAspectRatio
)
);
}
Чтобы не усложнять пример, я поместил картинку в ресурсы приложения, и загружаю ее в QPixmap оттуда. Размер Сцены задается исходя из размеров изображения, поскольку именно изображение наш основной объект экспериментов. Сама картинка добавляется на Сцену с помощью setPixmap().
Важно: Не забудьте связать Сцену QGraphicsScene с Представлением QGraphicsView:
ui->grView->setScene( &m_scene )
Чтобы изображение всегда вписывалось в Представление, переопределим функцию-обработчик события изменения размеров виджета resizeEvent():
void CustomGraphicsItemDemo::resizeEvent( QResizeEvent* ) {
static const int GRAPHICS_VIEW_MARGIN = 20;
double scale = std::min(
( ui->grView->width() - GRAPHICS_VIEW_MARGIN ) / m_scene.width(),
( ui->grView->height() - GRAPHICS_VIEW_MARGIN ) / m_scene.height()
);
ui->grView->resetMatrix();
ui->grView->scale( scale, scale );
}
Также не забудем про заготовку слота, реагирующего на изменение выбранной области:
void CustomGraphicsItemDemo::onSelectedAreaChanged( const QRectF& area ) {
static const int SCALED_VIEW_MARGIN = 20;
ui->lbScaledView->setPixmap(
m_pix.copy( area.toRect() ).scaled(
ui->lbScaledView->size().width() - SCALED_VIEW_MARGIN,
ui->lbScaledView->size().height() - SCALED_VIEW_MARGIN,
Qt::KeepAspectRatio
)
);
}
В качестве компонента для выделения области на Сцене мы могли бы взять за основу QGraphicsRectItem с установленным флагом ItemIsMovable. Но это решение недостаточно гибкое. Придется идти другим путем, и реализовать собственный компонент, унаследовав его от QGraphicsItem:
class AreaSelector : public QObject, public QGraphicsItem {
Q_OBJECT
static const int RESIZE_ZONE_SIZE;
static const int MIN_AREA_SIZE;
public:
explicit AreaSelector( const QRectF& initialRect );
~AreaSelector();
QRectF boundingRect() const;
QRectF getSelectedArea() const {
return m_innerRect;
}
signals:
void selectedAreaChanged( const QRectF& area );
protected:
void paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0 );
void hoverEnterEvent( QGraphicsSceneHoverEvent* event );
void hoverLeaveEvent( QGraphicsSceneHoverEvent* event );
void mousePressEvent( QGraphicsSceneMouseEvent* event );
void mouseMoveEvent( QGraphicsSceneMouseEvent* event );
void mouseReleaseEvent( QGraphicsSceneMouseEvent* event );
private:
enum SelectorZone {
NONE,
MOVE_ZONE,
RESIZE_TOP_LEFT_ZONE,
RESIZE_TOP_RIGHT_ZONE,
RESIZE_BOTTOM_LEFT_ZONE,
RESIZE_BOTTOM_RIGHT_ZONE,
};
QHash< SelectorZone, QRectF > generateZones() const;
private:
QRectF m_innerRect;
bool m_hovered;
SelectorZone m_activeZone;
QPointF m_centerOffset;
};
Нам приходится контролировать каждый аспект поведения компонента вручную, но это дает полный контроль над ситуацией:
const int AreaSelector::RESIZE_ZONE_SIZE = 20;
const int AreaSelector::MIN_AREA_SIZE = 100;
AreaSelector::AreaSelector( const QRectF& initialRect ) :
m_innerRect( initialRect ), m_hovered( false ), m_activeZone( AreaSelector::NONE ) {
setAcceptHoverEvents( true );
}
AreaSelector::~AreaSelector() { }
QRectF AreaSelector::boundingRect() const {
return QRectF(
m_innerRect.left() - RESIZE_ZONE_SIZE / 2.0,
m_innerRect.top() - RESIZE_ZONE_SIZE / 2.0,
m_innerRect.width() + RESIZE_ZONE_SIZE,
m_innerRect.height() + RESIZE_ZONE_SIZE
);
}
void AreaSelector::paint( QPainter* painter, const QStyleOptionGraphicsItem*, QWidget* ) {
painter->drawRect( m_innerRect );
if( m_hovered ) {
QHash< SelectorZone, QRectF > zones = generateZones();
foreach( const SelectorZone& key, zones.keys() ) {
painter->fillRect( zones[ key ], Qt::white );
painter->drawRect( zones[ key ] );
}
}
}
void AreaSelector::hoverEnterEvent( QGraphicsSceneHoverEvent* ) {
m_hovered = true;
update();
}
void AreaSelector::hoverLeaveEvent( QGraphicsSceneHoverEvent* ) {
m_hovered = false;
update();
}
void AreaSelector::mousePressEvent( QGraphicsSceneMouseEvent* event ) {
switch( event->button() ) {
case Qt::LeftButton: {
QHash< SelectorZone, QRectF > zones = generateZones();
foreach( const SelectorZone& key, zones.keys() ) {
if( zones[ key ].contains( event->pos() ) ) {
m_activeZone = key;
break;
}
}
if(
m_activeZone == AreaSelector::NONE &&
m_innerRect.contains( event->pos() )
) {
m_activeZone = MOVE_ZONE;
m_centerOffset = event->pos() - m_innerRect.center();
}
break;
}
default:
// Nothing to do here
break;
}
}
void AreaSelector::mouseMoveEvent( QGraphicsSceneMouseEvent* event ) {
prepareGeometryChange();
QRectF sRect;
if( scene() ) {
sRect = scene()->sceneRect();
}
switch( m_activeZone ) {
case MOVE_ZONE:
m_innerRect.moveCenter( event->pos() - m_centerOffset );
if( m_innerRect.left() < sRect.left() ) {
m_innerRect.moveLeft( sRect.left() );
}
if( m_innerRect.top() < sRect.top() ) {
m_innerRect.moveTop( sRect.top() );
}
if( sRect.right() < m_innerRect.right() ) {
m_innerRect.moveRight( sRect.right() );
}
if( sRect.bottom() < m_innerRect.bottom() ) {
m_innerRect.moveBottom( sRect.bottom() );
}
break;
case RESIZE_TOP_LEFT_ZONE:
m_innerRect.setTopLeft( event->pos() );
break;
case RESIZE_TOP_RIGHT_ZONE:
m_innerRect.setTopRight( event->pos() );
break;
case RESIZE_BOTTOM_LEFT_ZONE:
m_innerRect.setBottomLeft( event->pos() );
break;
case RESIZE_BOTTOM_RIGHT_ZONE:
m_innerRect.setBottomRight( event->pos() );
break;
default:
// Nothing to do here
break;
}
if( m_activeZone != AreaSelector::NONE ) {
if( m_innerRect.left() < sRect.left() ) {
m_innerRect.setLeft( sRect.left() );
}
if( m_innerRect.top() < sRect.top() ) {
m_innerRect.setTop( sRect.top() );
}
if( sRect.right() < m_innerRect.right() ) {
m_innerRect.setRight( sRect.right() );
}
if( sRect.bottom() < m_innerRect.bottom() ) {
m_innerRect.setBottom( sRect.bottom() );
}
if( m_innerRect.width() < MIN_AREA_SIZE ) {
if( m_activeZone == RESIZE_BOTTOM_LEFT_ZONE || m_activeZone == RESIZE_TOP_LEFT_ZONE ) {
m_innerRect.setLeft( m_innerRect.right() - MIN_AREA_SIZE );
} else {
m_innerRect.setRight( m_innerRect.left() + MIN_AREA_SIZE );
}
}
if( m_innerRect.height() < MIN_AREA_SIZE ) {
if( m_activeZone == RESIZE_TOP_LEFT_ZONE || m_activeZone == RESIZE_TOP_RIGHT_ZONE ) {
m_innerRect.setTop( m_innerRect.bottom() - MIN_AREA_SIZE );
} else {
m_innerRect.setBottom( m_innerRect.top() + MIN_AREA_SIZE );
}
}
}
emit selectedAreaChanged( m_innerRect );
}
void AreaSelector::mouseReleaseEvent( QGraphicsSceneMouseEvent* ) {
m_activeZone = AreaSelector::NONE;
}
QHash< AreaSelector::SelectorZone, QRectF > AreaSelector::generateZones() const {
QRectF zoneRect( 0, 0, RESIZE_ZONE_SIZE, RESIZE_ZONE_SIZE );
QHash< SelectorZone, QPointF > zoneCenters = QHash< SelectorZone, QPointF >();
zoneCenters[ RESIZE_TOP_LEFT_ZONE ] = m_innerRect.topLeft();
zoneCenters[ RESIZE_TOP_RIGHT_ZONE ] = m_innerRect.topRight();
zoneCenters[ RESIZE_BOTTOM_LEFT_ZONE ] = m_innerRect.bottomLeft();
zoneCenters[ RESIZE_BOTTOM_RIGHT_ZONE ] = m_innerRect.bottomRight();
QHash< SelectorZone, QRectF > zones;
foreach( const SelectorZone& key, zoneCenters.keys() ) {
zoneRect.moveCenter( zoneCenters[ key ] );
zones[ key ] = zoneRect;
}
return zones;
}
Кода получилось довольно много, но его суть определяется в трех пунктах:
замена шлагбаума. видеозвонок на дверь Уфа.
boundingRect(). Не забудьте учесть, что помимо самой области выделения мы берем небольшой запас на маленькие квадраты, указывающие на зоны растяжения, которые появляются по углам прямоугольника при наведении курсора мыши;paint(). В ней мы рисуем текущую зону выделения и зоны растяжения (маленькие квадраты в углах). Последние отображаются только тогда, когда курсор мыши попадает в boundingRect();hoverEnterEvent(), hoverLeaveEvent(), mousePressEvent(), mouseMoveEvent() и mouseReleaseEvent(). Об этом пункте поговорим поподробнее.Обратите внимание, что для работы hoverEnterEvent(), hoverLeaveEvent() требуется явно включить поддержку с помощью setAcceptHoverEvents( true ). В обработчиках этих событий мы определяем, что курсор мыши входит или выходит из области нашего компонента: меняем значение m_hovered, а затем обеспечиваем перерисовку (неявный вызов paint()) с помощью update().
В обработчике mousePressEvent() мы фиксируем щелчок левой кнопкой мыши. Если щелчок произошел, то мы определяем в какую зону он пришелся (в одну из зон растяжения по углам или в любое другое место, что соответствует зоне перемещения). Если щелчок произошел в зоне компонента, предназначенной для перемещения, то мы дополнительно сохраняем смещение от центра компонента до позиции курсора в момент щелчка.
В mouseReleaseEvent() мы сбрасываем информацию об активной зоне, в которую пришелся щелчок левой кнопкой мыши, поскольку перемещать или менять размер компонента больше не требуется.
Вся "магия" заключена в функции mouseMoveEvent(). В ней мы контролируем все видоизменения компонента. Основная сложность здесь кроется в необходимости контроля границ. Мы учитываем, что область выделения компонента не должна выходить за границы Сцены (т.е. изображения), а также не должна становиться слишком маленькой. Для реализации всего этого нам достаточно простых наборов условий и знаний геометрии школьного уровня.
Осталось только подключить созданный компонент к нашей Сцене:
CustomGraphicsItemDemo::CustomGraphicsItemDemo( QWidget* parent ) :
// …
QRectF initialRect( 0, 0, m_pix.width() * 0.2f, m_pix.height() * 0.2f );
initialRect.moveCenter( m_scene.sceneRect().center() );
AreaSelector* areaSelector = new AreaSelector( initialRect );
connect( areaSelector, SIGNAL( selectedAreaChanged( QRectF ) ), SLOT( onSelectedAreaChanged( QRectF ) ) );
m_scene.addItem( areaSelector );
// …
}
Мы реализовали собственный компонент для Сцены QGraphicsScene, который позволяет выделять прямоугольную область с помощью мыши. Мы использовали этот компонент в качестве лупы, но идею можно обобщить и для решения более сложных задач.
Скачать пример создания пользовательского компонента для QGraphicsScene
Anonymous:
Спасибо большое за такой пример!
Здравствуйте. Спасибо за комментарий =)
Anonymous:
Возник вопрос: почему в Вашем случае не нужны преобразования координат элемента к координатам сцены?
Это связано с тем, что элемент AreaSelector уже работает в координатах сцены. Все преобразования происходит относительно m_innerRect
Спасибо! До конца не понятно, почему AreaSelector в координатах сцены. В руководстве Qt положение события QGraphicsSceneMouseEvent* event->pos() в системе координат элемента
Anonymous:
Спасибо! До конца не понятно, почему AreaSelector в координатах сцены. В руководстве Qt положение события QGraphicsSceneMouseEvent* event->pos() в системе координат элемента
Более точно можно сказать, что QGraphicsSceneMouseEvent::pos() возвращает позицию элемента в родительской системе координат. В нашем случае явного родителя у экземпляра AreaSelector нет, поэтому родителем неявно считается сцена, что и приводит к совпадению систем координат элемента и сцены
ОЧЕНЬ ХОРОШИЙ ПРИМЕР!
спасибо огромное, пример супер и статья отличная!
Anonymous
Спасибо большое за такой пример!
Возник вопрос: почему в Вашем случае не нужны преобразования координат элемента к координатам сцены?