Пусть в приложении требуется функция увеличения произвольной области изображения, как под лупой. В качестве примера решения этой задачи скомпонуем простую 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;
};
И добавим базовую реализацию:
Hellcase CS:GO - cases & get exclusive CS2 skins, gloves, knives, agents. Best drop in cases.
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
Спасибо большое за такой пример!
Возник вопрос: почему в Вашем случае не нужны преобразования координат элемента к координатам сцены?