作者:billy
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处
前面的章节介绍了 Graphics View 绘图架构,终于到实战了,真的是千呼万唤始出来!这一章节就用 Graphics View 绘图架构来做一个绘图工具,实现一些基础图形的绘制,废话不多说先来看一下结果演示:
我们知道简单的几何图形,只要确定两个点就可以完成绘制。比如要画一个圆,一个点可以定为圆心,由另一个点则可以计算出到圆心的距离,即圆的为半径,确定了圆心和半径那么这个圆就完成了。
同样的道理,椭圆、正方形、矩形、圆端矩形,也可以由一个中心点和一个可拖动来改变图形的形状和大小的点(这里我们管这个点叫边缘点)来确定,边缘点的坐标可以直接用来确定图形的宽度和高度。
而在饼、和弦中,边缘点不仅仅是用来决定图形的宽度和高度,还需要用来确定角度(与X轴正方向的夹角)。
最麻烦的是多边形,因为每点击一下就需要绘制一个点并且完成连线,所以需要把每个点的坐标从场景传递到图形,然后在图形中完成绘制。
点依附于图形之上,所以构造函数中需要传入一个图形,代表这个点在这个图形上,并且需要初始化点的坐标和类型;
class BPointItem : public QObject, public QAbstractGraphicsShapeItem
{
Q_OBJECT
public:
enum PointType {
Center = 0, // 中心点
Edge, // 边缘点(可拖动改变图形的形状、大小)
Special // 特殊功能点
};
BPointItem(QAbstractGraphicsShapeItem* parent, QPointF p, PointType type);
QPointF getPoint() { return m_point; }
void setPoint(QPointF p) { m_point = p; }
protected:
virtual QRectF boundingRect() const override;
virtual void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
private:
QPointF m_point;
PointType m_type;
};
根据类型不同,绘制的形状也不同。中心点是一个圆,并且光标是 OpenHandCursor,而其他类型的点则是一个正方形,光标是 PointingHandCursor;
当我们移动点的时候,如果是中心点则会整个图形一起移动,而移动边缘点的时候会根据边缘点的坐标来动态改变图形的形状,这里代码比较多就不贴了。
BPointItem::BPointItem(QAbstractGraphicsShapeItem* parent, QPointF p, PointType type)
: QAbstractGraphicsShapeItem(parent)
, m_point(p)
, m_type(type)
{
this->setPos(m_point);
this->setFlags(QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemIsMovable |
QGraphicsItem::ItemIsFocusable);
switch (type) {
case Center:
this->setCursor(Qt::OpenHandCursor);
break;
case Edge:
this->setCursor(Qt::PointingHandCursor);
break;
case Special:
this->setCursor(Qt::PointingHandCursor);
break;
default: break;
}
}
QRectF BPointItem::boundingRect() const
{
return QRectF(-4, -4, 8, 8);
}
void BPointItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option);
Q_UNUSED(widget);
painter->setPen(this->pen());
painter->setBrush(this->brush());
this->setPos(m_point);
switch (m_type) {
case Center:
painter->drawEllipse(-4, -4, 8, 8);
break;
case Edge:
painter->drawRect(QRectF(-4, -4, 8, 8));
break;
case Special:
painter->drawRect(QRectF(-4, -4, 8, 8));
break;
default: break;
}
}
void BPointItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if ( event->buttons() == Qt::LeftButton ) {
qreal dx = event->scenePos().x() - event->lastScenePos().x();
qreal dy = event->scenePos().y() - event->lastScenePos().y();
BGraphicsItem* item = static_cast(this->parentItem());
BGraphicsItem::ItemType itemType = item->getType();
switch (m_type) {
case Center: {
item->moveBy(dx, dy);
this->scene()->update();
} break;
case Edge: {
略...
} break;
case Special: {
略...
} break;
default: break;
}
}
}
定义了一个图元基类,每一个图元都至少需要一个中心点、一个边缘点和图元类型。重写了焦点事件,当获得焦点时会改变颜色;
class BGraphicsItem : public QObject, public QAbstractGraphicsShapeItem
{
Q_OBJECT
public:
enum ItemType {
Circle = 0, // 圆
Ellipse, // 椭圆
Concentric_Circle, // 同心圆
Pie, // 饼
Chord, // 和弦
Rectangle, // 矩形
Square, // 正方形
Polygon, // 多边形
Round_End_Rectangle,// 圆端矩形
Rounded_Rectangle // 圆角矩形
};
QPointF getCenter() { return m_center; }
void setCenter(QPointF p) { m_center = p; }
QPointF getEdge() { return m_edge; }
void setEdge(QPointF p) { m_edge = p; }
ItemType getType() { return m_type; }
protected:
BGraphicsItem(QPointF center, QPointF edge, ItemType type);
virtual void focusInEvent(QFocusEvent *event) override;
virtual void focusOutEvent(QFocusEvent *event) override;
protected:
QPointF m_center;
QPointF m_edge;
ItemType m_type;
BPointItemList m_pointList;
QPen m_pen_isSelected;
QPen m_pen_noSelected;
};
BGraphicsItem::BGraphicsItem(QPointF center, QPointF edge, ItemType type)
: m_center(center), m_edge(edge), m_type(type)
{
m_pen_noSelected.setColor(QColor(0, 160, 230));
m_pen_noSelected.setWidth(2);
m_pen_isSelected.setColor(QColor(255, 0, 255));
m_pen_isSelected.setWidth(2);
this->setPen(m_pen_noSelected);
this->setFlags(QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemIsMovable |
QGraphicsItem::ItemIsFocusable);
}
void BGraphicsItem::focusInEvent(QFocusEvent *event)
{
Q_UNUSED(event);
this->setPen(m_pen_isSelected);
}
void BGraphicsItem::focusOutEvent(QFocusEvent *event)
{
Q_UNUSED(event);
this->setPen(m_pen_noSelected);
}
以矩形为例来介绍一下图元,构造函数参数为中心点坐标和矩形的宽、高、类型。初始化中心点和边缘点之后即可绘制出矩形,重写 paint 函数把矩形的宽和高绑定到 m_edge 的坐标,矩形的大小就由边缘点的坐标决定,当我们拖到边缘点时只需更新 m_edge 的坐标,矩形的形状就能动态改变;
class BRectangle : public BGraphicsItem
{
public:
BRectangle(qreal x, qreal y, qreal width, qreal height, ItemType type);
protected:
virtual QRectF boundingRect() const override;
virtual void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
};
BRectangle::BRectangle(qreal x, qreal y, qreal width, qreal height, ItemType type)
: BGraphicsItem(QPointF(x,y), QPointF(width/2,height/2), type)
{
BPointItem *point = new BPointItem(this, m_edge, BPointItem::Edge);
point->setParentItem(this);
m_pointList.append(point);
m_pointList.append(new BPointItem(this, m_center, BPointItem::Center));
m_pointList.setRandColor();
}
QRectF BRectangle::boundingRect() const
{
return QRectF(m_center.x() - m_edge.x(), m_center.y() - m_edge.y(), m_edge.x() * 2, m_edge.y() * 2);
}
void BRectangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option);
Q_UNUSED(widget);
painter->setPen(this->pen());
painter->setBrush(this->brush());
QRectF ret(m_center.x() - m_edge.x(), m_center.y() - m_edge.y(), m_edge.x() * 2, m_edge.y() * 2);
painter->drawRect(ret);
}
当我们绘制多边形时,需要每点击一下绘制点和连线。此时我们的鼠标点击事件是在场景中的,而我们需要把场景中的坐标传递到图元中,所以我们需要自定义场景来重写场景中的鼠标事件;
class BQGraphicsScene : public QGraphicsScene
{
Q_OBJECT
public:
BQGraphicsScene(QObject *parent = nullptr);
void startCreate();
protected:
virtual void mousePressEvent(QGraphicsSceneMouseEvent *event);
signals:
void updatePoint(QPointF p, QList list, bool isCenter);
void createFinished();
protected:
QList m_list;
bool is_creating_BPolygon;
};
BQGraphicsScene::BQGraphicsScene(QObject *parent) : QGraphicsScene(parent)
{
is_creating_BPolygon = false;
}
void BQGraphicsScene::startCreate()
{
is_creating_BPolygon = true;
m_list.clear();
}
void BQGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (is_creating_BPolygon) {
QPointF p(event->scenePos().x(), event->scenePos().y());
switch ( event->buttons() )
{
case Qt::LeftButton: {
m_list.push_back(p);
emit updatePoint(p, m_list, false);
} break;
case Qt::RightButton: {
if (m_list.size() >= 3) {
emit updatePoint(p, m_list, true);
emit createFinished();
is_creating_BPolygon = false;
m_list.clear();
}
} break;
default: break;
}
} else {
QGraphicsScene::mousePressEvent(event);
}
}
void MainWindow::on_polygonBtn_clicked()
{
m_scene.startCreate();
setBtnEnabled(false);
BPolygon *m_polygon = new BPolygon(BGraphicsItem::ItemType::Polygon);
m_scene.addItem(m_polygon);
connect(&m_scene, SIGNAL(updatePoint(QPointF, QList, bool)), m_polygon, SLOT(pushPoint(QPointF, QList, bool)));
connect(&m_scene, &BQGraphicsScene::createFinished, [=](){
setBtnEnabled(true);
});
}
右键跳出弹窗可以重写 contextMenuEvent 函数来完成,弹窗中需要哪些控件可以自定义实现;
void BEllipse::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
if ( !this->isSelected() )
return;
QMenu* menu = new QMenu();
menu->setStyleSheet("QMenu { background-color:rgb(89,87,87); border: 5px solid rgb(235,110,36); }");
QSpinBox* width_spinBox = new QSpinBox(menu);
width_spinBox->setStyleSheet("QSpinBox{ width:120px; height:30px;}");
width_spinBox->setRange(0, 1000);
width_spinBox->setPrefix("w: ");
width_spinBox->setSuffix(" mm");
width_spinBox->setSingleStep(1);
width_spinBox->setValue(2 * abs(m_edge.x()));
connect(width_spinBox, static_cast(&QSpinBox::valueChanged), [=](int v){
m_edge.setX(v/2);
m_pointList.at(0)->setPoint(m_edge);
this->hide();
this->update();
this->show();
});
QSpinBox* height__spinBox = new QSpinBox(menu);
height__spinBox->setStyleSheet("QSpinBox{ width:120px; height:30px;}");
height__spinBox->setRange(0, 1000);
height__spinBox->setPrefix("h: ");
height__spinBox->setSuffix(" mm");
height__spinBox->setSingleStep(1);
height__spinBox->setValue(2 * abs(m_edge.y()));
connect(height__spinBox, static_cast(&QSpinBox::valueChanged), [=](int v){
m_edge.setY(v/2);
m_pointList.at(0)->setPoint(m_edge);
this->hide();
this->update();
this->show();
});
QWidgetAction* width_widgetAction = new QWidgetAction(menu);
width_widgetAction->setDefaultWidget(width_spinBox);
menu->addAction(width_widgetAction);
QWidgetAction* height_widgetAction = new QWidgetAction(menu);
height_widgetAction->setDefaultWidget(height__spinBox);
menu->addAction(height_widgetAction);
menu->exec(QCursor::pos());
delete menu;
QGraphicsItem::contextMenuEvent(event);
}