首先在工程中添加一个基于QGraphicsItem的C++类,然后在头文件的QGraphicsItem类名称上按Alt+Enter快捷键,在弹出的窗口中选择需要重载的函数,该快捷键只在QtCreator中生效,如果你使用的是Visual Studio + QT,是不可以的。对应的界面如下所示:
当然,我们也可以手动添加对应的重载函数。实现自定义Item有两个接口是必须实现的分别如下:
//返回图元的边界
QRectF boundingRect() const;
//添加图元的自定义绘制
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
这里我们定义的图元显示的是振幅为5的正弦波曲线,对应的函数是y=5sin(x)。对应的边界矩形如下所示:
QRectF SineItem::boundingRect() const
{
//左上角的坐标是(0,-5)矩形
//宽度m_maxX可以通过外部设置,默认是30
//高度就是正弦曲线的振幅的两倍10
return QRectF(0, -5, m_maxX, 10);
}
矩形对应的X的范围是(0,30),Y的范围是(-5,5)。
正弦曲线的绘制方法如下:
void SineItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
QPen pen;
//缩放的时候保持固定线宽
pen.setCosmetic(true);
//采样间隔
qreal dx = 0.5;
painter->setPen(pen);
painter->drawLine(-50, 0, 50, 0);
painter->drawLine(0, -50, 0, 50);
const int steps = qRound(m_maxX / dx);
QPointF previousPoint(0, sin(0));
for(int i = 1; i < steps; ++i) {
const float x = dx * i;
QPointF point(x, 5*sin(x));
painter->drawLine(previousPoint, point);
previousPoint = point;
}
}
这里的采样间隔也就是绘制的步长是是固定的0.5,固定的间隔会带来一个问题,如果间隔太短会带来性能损耗,如果间隔太大在视图缩放的时候,会出现绘制曲线不平滑的问题,如下图所示:
如果采样间隔可以根据缩放比例自动适配就好了。其实QGraphicsView是支持动态获取缩放比例的,所以我们可以根据缩放比例,动态调整采样间隔对应的设置如下:
//采样间隔
//获取缩放系数
const qreal detail = QStyleOptionGraphicsItem::levelOfDetailFromTransform(painter->worldTransform());
//计算精度
const qreal dx = 1 / detail;
通过动态设置采样精度我们就可以避免在缩放的时候出现曲线不平滑的问题了。
QGraphicsView中自定义的图元也可以通过QT框架的事件循环机制来处理对应的消息事件。我们通过各种事件的自定义处理完成各种各样的个性化操作。如果我们在事件处理完毕之后accpet()了事件,那么该事件就不会被后续对象处理,如果没有accept()事件那么它还会被他的父对象接收到,这样我们就可以在自定义图元中控制事件的生命周期了。
常见的事件列表如下:
事件名称 | 介绍 |
---|---|
QEvent::KeyPress QEvent::KeyRelease |
按键按下事件 按键释放事件 |
QEvent::MouseButtonPress QEvent::MouseButtonRelease QEvent::MouseButtonDblClick QEvent::Wheel QEvent::MouseMove |
鼠标点击按下事件 鼠标点击释放事件 鼠标双击事件 鼠标滚轮事件 鼠标移动事件 |
QEvent::Enter | 鼠标进入对象的事件 |
Event::Leave | 鼠标离开对象的事件 |
QEvent::Resize | 控件尺寸变化的事件 |
QEvent::Close | 窗口关闭事件 |
QEvent::ContextMenu | 打开上下文菜单的事件 |
QEvent::Paint | 图形绘制事件 |
QEvent::DragEnter QEvent::DragLeave QEvent::DragMove QEvent::Drop |
拖拽和释放事件 |
QEvent::TouchBegin QEvent::TouchUpdate QEvent::TouchEnd QEvent::TouchCancel |
触摸事件,在嵌入式触摸屏里面调用 |
QEvent::FocusIn QEvent::FocusOut |
获得键盘焦点事件 失去键盘焦点事件 |
QEvent::HoverEnter QEvent::HoverLeave QEvent::HoverMove |
悬浮进入事件 悬浮离开事件 悬浮移动事件 |
每个事件都有对应的事件处理函数,比如QMouseEvent对应的
QWidget::mousePressEvent(QMouseEvent *event);
QGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event);
由于QGraphicsItem对应的事件当中额外的携带了Scene中图元坐标信息等等,所以和QWidget的事件函数参数名称略有不同。
对于一些比较少见的事件,我们可以在对应的事件循环函数中通过事件的类型来进行处理。对应的事件循环接口如下:
bool QWidget::event(QEvent *event);
bool QGraphicsItem::sceneEvent(QEvent *event);
//通过event->type()获取事件的类型
QGraphicsItem的常见事件处理函数如下所示:
//事件过滤函数
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event);
//菜单事件
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event);
//拖拽进入事件
void dragEnterEvent(QGraphicsSceneDragDropEvent *event);
//拖拽离开事件
void dragLeaveEvent(QGraphicsSceneDragDropEvent *event);
//拖拽移动事件
void dragMoveEvent(QGraphicsSceneDragDropEvent *event);
//拖拽释放事件
void dropEvent(QGraphicsSceneDragDropEvent *event);
//得到焦点的事件
void focusInEvent(QFocusEvent *event);
//失去焦点的事件
void focusOutEvent(QFocusEvent *event);
//悬浮进入事件
void hoverEnterEvent(QGraphicsSceneHoverEvent *event);
//悬浮移动事件
void hoverMoveEvent(QGraphicsSceneHoverEvent *event);
//悬浮离开事件
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event);
//按键按下事件
void keyPressEvent(QKeyEvent *event);
//按键抬起事件
void keyReleaseEvent(QKeyEvent *event);
//鼠标按下事件
void mousePressEvent(QGraphicsSceneMouseEvent *event);
//鼠标移动事件
void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
//鼠标释放事件
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
//鼠标双击事件
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event);
//滚轮事件
void wheelEvent(QGraphicsSceneWheelEvent *event);
//输入法事件
void inputMethodEvent(QInputMethodEvent *event);
这里,我们实现鼠标点击事件,我们在鼠标点击的曲线位置添加一个椭圆的图元,用来标记点击位置,对应的实现如下所示:
void SineItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() & Qt::LeftButton)
{
float x = event->pos().x();
QPointF point(x, 5*sin(x));
//在对应的鼠标点击的位置添加一个椭圆图元
static const float r = 0.3;
QGraphicsEllipseItem *ellipse = new QGraphicsEllipseItem(-r, -r, 2 * r, 2 * r, this);
ellipse->setPen(Qt::NoPen);
ellipse->setBrush(QBrush(Qt::red));
ellipse->setPos(point);
event->accept();
}
else
{
event->ignore();
}
}
显示效果如下:
我们在自定义视图中通过实现WheelEvent()滚轮事件来实现对应的视图缩放效果。
void View::wheelEvent(QWheelEvent *event)
{
//先调用默认的滚轮事件处理,如果需要禁用的话,可以不调用
QGraphicsView::wheelEvent(event);
//已经被处理过的不再进行处理
if (event->isAccepted()) {
return;
}
//y>0放大
qreal factor = 1.1;
if (event->angleDelta().y() > 0)
{
scale(factor, factor);
}
//y<0缩小
else
{
scale(1 / factor, 1 / factor);
}
event->accept();
}
对应的效果显示如下:
我们可以通过setTransformationAnchor()设置缩放的中心,对应的属性值包括下面几个:
//NoAnchor的时候缩放中心是视图的左上角
NoAnchor
//缩放中心为视图的中心
AnchorViewCenter
//缩放中心是鼠标所在的位置
AnchorUnderMouse
为了方便自定义图元的添加,我们在自定义View中通过鼠标左右键来动态的增加和删除自定义图元,对应的实现如下:
void View::mousePressEvent(QMouseEvent *event)
{
QGraphicsView::mousePressEvent(event);
//事件已经被处理过了,便不再处理
//防止和图元事件冲突
if (event->isAccepted()) {
return;
}
switch (event->button())
{
//左键添加图元到鼠标位置
case Qt::LeftButton: {
SineItem *item = new SineItem();
item->setPos(mapToScene(event->pos()));
scene()->addItem(item);
event->accept();
break;
}
//右键删除鼠标位置的图元
case Qt::RightButton: {
QGraphicsItem *item = itemAt(event->pos());
if (item) {
delete item;
}
event->accept();
break;
}
default:
break;
}
}
为了防止添加图元的时候,View中的视图变动,我们可以指定SceneRect()或者设置好View与Scene的对齐方式,设置方法如下:
setAlignment(Qt::AlignTop | Qt::AlignLeft)
在绘制自定义图元的时候,有时候我们需要根据图元的状态来进行不同的绘制,这时候我们就需要对图元的状态进行判断和处理了。图元常见的状态列表如下所示:
状态 | 说明 |
---|---|
QStyle::State_Enabled | 表示图元被启用了。 |
QStyle::State_HasFocus | 表示图元获得了输入焦点,需要通过下面的设置启动该状态。 setFlag(QGraphicsItem::ItemIsFocusable,true); |
QStyle::State_MouseOver | 表示当前鼠标悬浮在图元的上方,通过下面的设置启动该状态。 setAcceptHoverEvents(true); |
QStyle::State_Selected | 表示该图元被选中,通过下面的设置启动该状态。 setFlag(QGraphicsItem::ItemIsSelectable,true); |
在图元自定义绘制的时候,我们可以根据不同的状态设置不同的绘制图形和样式,对应的绘制代码如下:
void SineItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
QPen pen;
//缩放的时候保持固定线宽
pen.setCosmetic(true);
//选中的时候线条变为红色并显示红色虚线边框
if (option->state.testFlag(QStyle::State_Selected))
{
pen.setColor(Qt::red);
pen.setStyle(Qt::DashLine);
painter->setPen(pen);
painter->drawRect(boundingRect());
}
//悬浮的时候显示虚线边框
else if(option->state.testFlag(QStyle::State_MouseOver))
{
pen.setColor(Qt::gray);
pen.setStyle(Qt::DashLine);
painter->setPen(pen);
painter->drawRect(boundingRect());
pen.setColor(Qt::black);
pen.setStyle(Qt::SolidLine);
painter->setPen(pen);
}
//采样间隔
//获取缩放系数
const qreal detail = QStyleOptionGraphicsItem::levelOfDetailFromTransform(painter->worldTransform());
//计算精度
const qreal dx = 1 / detail;
painter->setPen(pen);
const int steps = qRound(m_maxX / dx);
QPointF previousPoint(0, sin(0));
for(int i = 1; i < steps; ++i) {
const float x = dx * i;
QPointF point(x, 5*sin(x));
painter->drawLine(previousPoint, point);
previousPoint = point;
}
}
自定义图形在悬浮的时候显示灰色的虚线边框,在选中的时候显示红色虚线边框同时曲线也会被成红色虚线。对应的效果如下:
样例中我们自定义图样默认显示的范围是(0,30),我们可以根据需要动态的修改显示范围。对应的设置接口如下:
//显示范围
float SineItem::maxX()
{
return m_maxX;
}
void SineItem::setMaxX(float value)
{
if (m_maxX == value) {
return;
}
//通知Scene边界范围发生了变化,如果不调用的话
//缓存的边界范围不会发生变化
prepareGeometryChange();
m_maxX = value;
}
我们通过鼠标右键事件动态的调整显示范围,对应的配置如下
void SineItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() & Qt::LeftButton)
{
float x = event->pos().x();
QPointF point(x, 5*sin(x));
static const float r = 0.3;
QGraphicsEllipseItem *ellipse = new QGraphicsEllipseItem(-r, -r, 2 * r, 2 * r, this);
ellipse->setPen(Qt::NoPen);
ellipse->setBrush(QBrush(Qt::red));
ellipse->setPos(point);
event->accept();
}
//右键刷新显示范围
else if(event->button() & Qt::RightButton)
{
int value = m_maxX + 5;
setMaxX(value);
event->accept();
}
else
{
event->ignore();
}
}
显示效果:
这个设置和View中的右键点击删除图元的功能是冲突的,使用的时候根据自己的需要自行调整吧。
自定义图元的实现如下:
//sineitem.h
#ifndef SINEITEM_H
#define SINEITEM_H
#include
class SineItem : public QGraphicsItem
{
public:
SineItem();
//最大显示范围的设置默认为30
float maxX();
void setMaxX(float value);
public:
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
private:
//显示范围
float m_maxX;
};
#endif // SINEITEM_H
//sineitem.cpp
#include "sineitem.h"
#include
#include
#include
#include
#include
#include
SineItem::SineItem()
{
setFlag(QGraphicsItem::ItemIsSelectable,true);
setFlag(QGraphicsItem::ItemIsFocusable,true);
this->setAcceptHoverEvents(true);
m_maxX = 30;
}
//显示范围
float SineItem::maxX()
{
return m_maxX;
}
void SineItem::setMaxX(float value)
{
if (m_maxX == value) {
return;
}
prepareGeometryChange();
m_maxX = value;
}
QRectF SineItem::boundingRect() const
{
//左上角的坐标是(0,-5)矩形的宽度也就是显示范围
//高度就是正弦曲线的振幅的两倍
return QRectF(0, -5, m_maxX, 10);
}
void SineItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
QPen pen;
//缩放的时候保持固定线宽
pen.setCosmetic(true);
//选中的时候线条变为红色并显示红色虚线边框
if (option->state.testFlag(QStyle::State_Selected))
{
pen.setColor(Qt::red);
pen.setStyle(Qt::DashLine);
painter->setPen(pen);
painter->drawRect(boundingRect());
}
//悬浮的时候显示虚线边框
else if(option->state.testFlag(QStyle::State_MouseOver))
{
pen.setColor(Qt::gray);
pen.setStyle(Qt::DashLine);
painter->setPen(pen);
painter->drawRect(boundingRect());
pen.setColor(Qt::black);
pen.setStyle(Qt::SolidLine);
painter->setPen(pen);
}
//采样间隔
//获取缩放系数
const qreal detail = QStyleOptionGraphicsItem::levelOfDetailFromTransform(painter->worldTransform());
//计算精度
const qreal dx = 1 / detail;
painter->setPen(pen);
const int steps = qRound(m_maxX / dx);
QPointF previousPoint(0, sin(0));
for(int i = 1; i < steps; ++i) {
const float x = dx * i;
QPointF point(x, 5*sin(x));
painter->drawLine(previousPoint, point);
previousPoint = point;
}
}
void SineItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() & Qt::LeftButton)
{
float x = event->pos().x();
QPointF point(x, 5*sin(x));
static const float r = 0.3;
QGraphicsEllipseItem *ellipse = new QGraphicsEllipseItem(-r, -r, 2 * r, 2 * r, this);
ellipse->setPen(Qt::NoPen);
ellipse->setBrush(QBrush(Qt::red));
ellipse->setPos(point);
event->accept();
}
//右键刷新显示范围
else if(event->button() & Qt::RightButton)
{
// int value = m_maxX + 5;
// setMaxX(value);
event->ignore();
}
else
{
event->ignore();
}
}
自定义视图的实现如下:
//view.h
#ifndef VIEW_H
#define VIEW_H
#include
class View : public QGraphicsView
{
Q_OBJECT
public:
View(QWidget *parent = 0);
~View();
protected:
//滚轮事件和鼠标事件
void wheelEvent(QWheelEvent *event);
void mousePressEvent(QMouseEvent *event);
};
#endif // VIEW_H
//view.cpp
#include "view.h"
#include
#include
#include "sineitem.h"
View::View(QWidget *parent)
: QGraphicsView(parent)
{
setRenderHint(QPainter::Antialiasing);
setScene(new QGraphicsScene);
//默认添加一个图元
SineItem *item = new SineItem();
scene()->addItem(item);
scale(10, 10);
setAlignment(Qt::AlignTop | Qt::AlignLeft);
setTransformationAnchor(ViewportAnchor::AnchorUnderMouse);
}
View::~View()
{
delete scene();
}
void View::wheelEvent(QWheelEvent *event)
{
//先调用默认的滚轮事件处理,如果需要禁用的话,可以不调用
QGraphicsView::wheelEvent(event);
if (event->isAccepted()) {
return;
}
//y>0放大
qreal factor = 1.1;
if (event->angleDelta().y() > 0)
{
scale(factor, factor);
}
//y<0缩小
else
{
scale(1 / factor, 1 / factor);
}
event->accept();
}
void View::mousePressEvent(QMouseEvent *event)
{
QGraphicsView::mousePressEvent(event);
if (event->isAccepted()) {
return;
}
switch (event->button())
{
//左键添加图元
case Qt::LeftButton: {
SineItem *item = new SineItem();
item->setPos(mapToScene(event->pos()));
scene()->addItem(item);
event->accept();
break;
}
//右键删除图元
case Qt::RightButton: {
QGraphicsItem *item = itemAt(event->pos());
if (item) {
delete item;
}
event->accept();
break;
}
default:
break;
}
}