【Qt 实现一个画板,基于QWidget,可以绘制直线和矩形】
- 简介
- 效果展示
- 源码
- mainwindow.h
- mainwindow.cpp
- painterwidget.h
- painterwidget.cpp
- shape.h (管理)
- line.h
- line.cpp
- rect.h
- rect.cpp
- 结
🙉🙉更多内容 点击:Qt 专栏 🙉🙉
简介
这里我会只做出一个简单的画板程序,大体上就是能够画直线和矩形吧。这样,我计划分成两种实现,一是使用普通的 QWidget 作为画板,第二则是使用 Graphcis View Framework 来实现。因为前面有朋友说不大明白 Graphics View 的相关内容,所以计划如此。
好了,现在先来看看我们的主体框架。我们的框架还是使用 Qt Creator 创建一个 Gui Application工程。
简单的 main()函数就不再赘述。
效果展示
喜欢的伙伴可以自己扩展
图片由以下软件制作
源码
mainwindow.h
Shape::Code newshape 枚举变量 画直线 画矩形
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QActionGroup>
#include <QToolBar>
#include <QStatusBar>
#include <QLabel>
#include "shape.h"
#include "painterwidget.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
void sig_changeCurrentShape(Shape::Code newshape);
private slots:
void drawLine_ActionTriggered();
void drawRect_ActionTriggered();
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QToolBar *bar = this->addToolBar("Tools"); //工具栏提示信息
QActionGroup *group = new QActionGroup(bar); //创建动作组,工具栏是他爸爸
QAction *act_line = new QAction("Line",bar)//创建动作组,工具栏是他爸爸
act_line->setIcon(QIcon(":/png/line.png")); //设置图标
act_line->setToolTip(tr("Draw a Line"));//设置工具提示信息
act_line->setStatusTip(tr("Draw a Line"));//状态栏提示信息
act_line->setCheckable(true);
act_line->setChecked(true);
group->addAction(act_line); //添加进组
bar->addAction(act_line);
QAction *act_rect = new QAction("Rect",bar);
act_rect->setIcon(QIcon(":/png/rect.png"));
act_rect->setToolTip(tr("Draw a Rect"));
act_rect->setStatusTip(tr("Draw a Rect"));
act_rect->setCheckable(true);
group->addAction(act_rect);
bar->addAction(act_rect);
QLabel *status = new QLabel;
statusBar()->addWidget(status); //状态栏添加标签
//创建绘画现象
PainterWidget *painterwidget = new PainterWidget(this);
this->setCentralWidget(painterwidget); //添加绘画容器
connect(act_line,&QAction::triggered,this,&MainWindow::drawLine_ActionTriggered);
connect(act_rect,&QAction::triggered,this,&MainWindow::drawRect_ActionTriggered);
connect(this,&MainWindow::sig_changeCurrentShape,painterwidget,&PainterWidget::SLOT_setCurrentShape);
}
MainWindow::~MainWindow()
{
}
//发射 画直线
void MainWindow::drawLine_ActionTriggered()
{
emit this->sig_changeCurrentShape(Shape::Line);
}
//发射 画矩形
void MainWindow::drawRect_ActionTriggered()
{
emit this->sig_changeCurrentShape(Shape::Rect);
}
我们在 MainWindow 类里面声明了一个信号,changeCurrentShape(Shape::Code),用于按钮按下后通知画图板。注意,QActio 的triggered()信号是没有参数的,因此,我们需要在 QAction 的槽函数中重新 emit 我们自己定义的信号。构造函数里面创建了两个 QAction,一个是 drawLineAction,一个是 drawRectAction,分别用于绘制直线和矩形。MainWindow 的中心组件是 PainWidget,也就是我们的画图板。下面来看看PaintWidget 类:
painterwidget.h
#ifndef PAINTERWIDGET_H
#define PAINTERWIDGET_H
#include <QWidget>
#include "QMouseEvent"
#include "QDebug"
#include "line.h"
#include "shape.h"
#include "rect.h"
class PainterWidget : public QWidget
{
Q_OBJECT
public:
explicit PainterWidget(QWidget *parent = nullptr);
public slots:
void SLOT_setCurrentShape(Shape::Code enumVal) //区分形状
{
if (enumVal != this->curenum__value)
this->curenum__value = enumVal;
}
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
signals:
private:
Shape::Code curenum__value;
Shape *shape; //形状指针
bool perm;
QList<Shape*>shapeList; //保存形状
};
#endif // PAINTERWIDGET_H
painterwidget.cpp
设置大小策略:可以拖住放大大小。
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); //设置策略
#include "paintwidget.h"
PaintWidget::PaintWidget(QWidget *parent)
: QWidget(parent), currShapeCode(Shape::Line), shape(NULL), perm(false)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); //设置策略
}
void PaintWidget::paintEvent(QPaintEvent *event)
{
QPainter painter(this); //在本Widget上绘图
painter.setBrush(Qt::white); //画刷白色
painter.drawRect(0, 0, size().width(), size().height()); //画最外面的大矩形
foreach(Shape * shape, shapeList) {
shape->paint(painter);
}
if(shape) {
shape->paint(painter);
}
}
void PaintWidget::mousePressEvent(QMouseEvent *event)
{
switch(currShapeCode) //选中的是直线还是矩形
{
case Shape::Line:
{
shape = new Line; //父类指针指向 子类对象
break;
}
case Shape::Rect:
{
shape = new Rect;
break;
}
}
if(shape != NULL) {
perm = false;
shapeList<<shape; //保存形状
shape->setStart(event->pos());
shape->setEnd(event->pos());
}
}
void PaintWidget::mouseMoveEvent(QMouseEvent *event)
{
if(shape && !perm) {
shape->setEnd(event->pos()); //鼠标最后的坐标点
update(); //更新绘图 paintEvent(QPaintEvent *event)
}
}
void PaintWidget::mouseReleaseEvent(QMouseEvent *event)
{
perm = true; //代表画完
}
PaintWidget 类定义了一个 slot,用于接收改变后的新的 ShapeCode。最主要的是,PaintWidget重定义了三个关于鼠标的事件:mousePressEvent,mouseMoveEvent和mouseReleaseEvent。
我们来想象一下如何绘制一个图形:图形的绘制与鼠标操作息息相关。以画直线为例,首先我们需要按下鼠标,确定直线的第一个点,所以在 mousePressEvent 里面,我们让 shape 保存下 start 点。然后在鼠标按下的状态下移动鼠标,此时,直线就会发生变化,实际上是直线的终止点在随着鼠标移动,所以在 mouseMoveEvent 中我们让 shape 保存下 end 点,然后调用 update()函数,这个函数会自动调用 paintEvent()函数,显示出我们绘制的内容。最后,当鼠标松开时,图形绘制完毕,我们将一个标志位置为 true,此时说明这个图形绘制完毕。
为了保存我们曾经画下的图形,我们使用了一个 List。每次按下鼠标时,都会把图形存入这个 List。可以看到,我们在 paintEvent()函数中使用了 foreach 遍历了这个 List,绘制出历史图形。foreach 是 Qt 提供的一个宏,用于遍历集合中的元素。
最后我们来看看 Shape 类。
shape.h (管理)
#ifndef SHAPE_H
#define SHAPE_H
#include "QPainter"
#include"QPoint"
class Shape
{
public:
Shape();
enum Code{Line,Rect}; //枚举
void setStart(QPoint point_localstart)
{
this->point_start = point_localstart;
}
void setEnd(QPoint point_localend)
{
this->point_end = point_localend;
}
QPoint get_point_start()
{
return this->point_start;
}
QPoint get_point_end()
{
return this->point_end;
}
void virtual paint(QPainter &painter) = 0; //写一个纯虚函数,让子类去实现自己功能
protected:
QPoint point_start;
QPoint point_end;
};
#endif // SHAPE_H
Shape 类最重要的就是保存了 start 和 end 两个点。为什么只要这两个点呢?因为我们要绘制的是直线和矩形。对于直线来说,有了两个点就可以确定这条直线,对于矩形来说,有了两个点作为左上角的点和右下角的点也可以确定这个矩形,因此我们只要保存两个点,就足够保存这两种图形的位置和大小的信息。paint()函数是 Shape 类的一个纯虚函数,子类都必须实现这个函数。我们现在有两个子类:Line和 Rect,分别定义如下:
line.h
#ifndef LINE_H
#define LINE_H
#include "shape.h"
class Line : public Shape
{
public:
Line();
void paint(QPainter &painter) override; //重写父类虚函数
};
#endif // LINE_H
line.cpp
#include "line.h"
Line::Line()
{
}
void Line::paint(QPainter &painter)
{
painter.drawLine(point_start,point_end);//画直线
}
rect.h
#ifndef RECT_H
#define RECT_H
#include "shape.h"
class Rect : public Shape
{
public:
Rect();
void paint(QPainter &painter) override;
};
#endif // RECT_H
rect.cpp
#include "rect.h"
Rect::Rect()
{
}
void Rect::paint(QPainter &painter) //画矩形 起始点为左上角 结束点为右下角
{
painter.drawRect(point_start.x(),point_start.y(),point_end.x()-point_start.x(),point_end.y()-point_start.y());//画矩形
}
结
使用 paint()函数,根据两个点的数据,Line 和 Rect 都可以绘制出它们自身来。此时就可以看出,我们之所以要建立一个 Shape 作为父类,因为这两个类有几乎完全相似的数据对象,并且从语义上来说,Line、Rect 与 Shape 也完全是一个 is-a 的关系。如果你想要添加颜色等的信息,完全可以在Shape 类进行记录。这也就是类层次结构的好处。