Пусть в приложении требуется функция увеличения произвольной области изображения, как под лупой. В качестве примера решения этой задачи скомпонуем простую Qt-программу.
В левой части отображается оригинал. Для него мы используем QGraphicsView и QGraphicsScene. Увеличенная копия выделенной части картинки выводится в правой верхней области окна с помощью QLabel.
Область увеличения изображения задается в виде прямоугольника, который можно перемещать при помощи мыши по принципу Drag&Drop. Размер области увеличения может быть изменен путем манипуляций с углами прямоугольника, соответствующего границам области, по аналогии с тем, к чему нас приучили всевозможные графические редакторы.
При любых изменениях области увеличения картинка в правом верхнем углу обновляется, чтобы отображать ровно то, что выделено на оригинальном изображении.
Реализация
В качестве нашей основной рабочей поверхности воспользуемся QGraphicsView и QGraphicsScene. Определим виджет главного окна для приложения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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; }; |
И добавим базовую реализацию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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:
1 |
ui->grView->setScene( &m_scene ) |
Чтобы изображение всегда вписывалось в Представление, переопределим функцию-обработчик события изменения размеров виджета resizeEvent():
1 2 3 4 5 6 7 8 9 10 |
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 ); } |
Также не забудем про заготовку слота, реагирующего на изменение выбранной области:
1 2 3 4 5 6 7 8 9 10 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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; }; |
Нам приходится контролировать каждый аспект поведения компонента вручную, но это дает полный контроль над ситуацией:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
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(). В ней мы контролируем все видоизменения компонента. Основная сложность здесь кроется в необходимости контроля границ. Мы учитываем, что область выделения компонента не должна выходить за границы Сцены (т.е. изображения), а также не должна становиться слишком маленькой. Для реализации всего этого нам достаточно простых наборов условий и знаний геометрии школьного уровня.
Осталось только подключить созданный компонент к нашей Сцене:
1 2 3 4 5 6 7 8 9 10 11 |
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, который позволяет выделять прямоугольную область с помощью мыши. Мы использовали этот компонент в качестве лупы, но идею можно обобщить и для решения более сложных задач.