Qt学习之路(32): 一个简易画板的实现(Graphics View)

这一次将介绍如何使用Graphics View来实现前面所说的画板。前面说了不少有关Graphics View的好话,可是没有具体的实例很难说究竟好在哪里。如今咱们就把前面的内容使用Graphics View从新实现一下,你们能够对比一下看有什么区别。
 
同前面类似的内容就再也不叙述了,咱们从上次代码的基础上进行修改,以便符合咱们的须要。首先来看MainWindow的代码:
 
mainwindow.cpp
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
{
        QToolBar *bar = this->addToolBar( "Tools");
        QActionGroup *group = new QActionGroup(bar);

        QAction *drawLineAction = new QAction( "Line", bar);
        drawLineAction->setIcon(QIcon( ":/line.png"));
        drawLineAction->setToolTip(tr( "Draw a line."));
        drawLineAction->setStatusTip(tr( "Draw a line."));
        drawLineAction->setCheckable( true);
        drawLineAction->setChecked( true);
        group->addAction(drawLineAction);

        bar->addAction(drawLineAction);
        QAction *drawRectAction = new QAction( "Rectangle", bar);
        drawRectAction->setIcon(QIcon( ":/rect.png"));
        drawRectAction->setToolTip(tr( "Draw a rectangle."));
        drawRectAction->setStatusTip(tr( "Draw a rectangle."));
        drawRectAction->setCheckable( true);
        group->addAction(drawRectAction);
        bar->addAction(drawRectAction);

        QLabel *statusMsg = new QLabel;
        statusBar()->addWidget(statusMsg);

        PaintWidget *paintWidget = new PaintWidget( this);
        QGraphicsView *view = new QGraphicsView(paintWidget, this);
        setCentralWidget(view);

        connect(drawLineAction, SIGNAL(triggered()),
                         this, SLOT(drawLineActionTriggered()));
        connect(drawRectAction, SIGNAL(triggered()),
                         this, SLOT(drawRectActionTriggered()));
        connect( this, SIGNAL(changeCurrentShape(Shape::Code)),
                        paintWidget, SLOT(setCurrentShape(Shape::Code)));
}

void MainWindow::drawLineActionTriggered()
{
        emit changeCurrentShape(Shape::Line);
}

void MainWindow::drawRectActionTriggered()
{
        emit changeCurrentShape(Shape::Rect);
}
 
因为mainwindow.h的代码与前文相同,这里就再也不贴出。而cpp文件里面只有少数几行与前文不一样。因为咱们使用Graphics View,因此,咱们必须把item添加到QGprahicsScene里面。这里,咱们建立了scene的对象,而scene对象须要经过view进行观察,所以,咱们须要再使用一个QGraphcisView对象,而且把这个view添加到MainWindow里面。
 
咱们把PaintWidget当作一个scene,所以PaintWidget如今是继承QGraphicsScene,而不是前面的QWidget。
 
paintwidget.h
#ifndef PAINTWIDGET_H
#define PAINTWIDGET_H

#include <QtGui>
#include <QDebug>

#include "shape.h"
#include "line.h"
#include "rect.h"

class PaintWidget : public QGraphicsScene
{
        Q_OBJECT

public:
        PaintWidget(QWidget *parent = 0);

public slots:
         void setCurrentShape(Shape::Code s)
        {
                 if(s != currShapeCode) {
                        currShapeCode = s;
                }
        }

protected:
         void mousePressEvent(QGraphicsSceneMouseEvent * event);
         void mouseMoveEvent(QGraphicsSceneMouseEvent * event);
         void mouseReleaseEvent(QGraphicsSceneMouseEvent * event);

private:
        Shape::Code currShapeCode;
        Shape *currItem;
         bool perm;
};

#endif // PAINTWIDGET_H
 
paintwidget.cpp
#include "paintwidget.h"

PaintWidget::PaintWidget(QWidget *parent)
        : QGraphicsScene(parent), currShapeCode(Shape::Line), currItem(NULL), perm( false)
{

}

void PaintWidget::mousePressEvent(QGraphicsSceneMouseEvent * event)
{
         switch(currShapeCode)
        {
         case Shape::Line:
                {
                        Line *line = new Line;
                        currItem = line;
                        addItem(line);
                         break;
                }
         case Shape::Rect:
                {
                        Rect *rect = new Rect;
                        currItem = rect;
                        addItem(rect);
                         break;
                }
        }
         if(currItem) {
                currItem->startDraw( event);
                perm = false;
        }
        QGraphicsScene::mousePressEvent( event);
}

void PaintWidget::mouseMoveEvent(QGraphicsSceneMouseEvent * event)
{
         if(currItem && !perm) {
                currItem->drawing( event);
        }
        QGraphicsScene::mouseMoveEvent( event);
}

void PaintWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent * event)
{
        perm = true;
        QGraphicsScene::mouseReleaseEvent( event);
}
 
咱们把继承自QWidget改为继承自QGraphicsScene,一样也会有鼠标事件,只不过在这里咱们把鼠标事件所有转发给具体的item进行处理。这个咱们会在下面的代码中看到。另一点是,每个鼠标处理函数都包含了调用其父类函数的语句。
 
shape.h
#ifndef SHAPE_H
#define SHAPE_H

#include <QtGui>

class Shape
{
public:

         enum Code {
                Line,
                Rect
        };

        Shape();

         virtual void startDraw(QGraphicsSceneMouseEvent * event) = 0;
         virtual void drawing(QGraphicsSceneMouseEvent * event) = 0;
};

#endif // SHAPE_H
 
shape.cpp
#include "shape.h"

Shape::Shape()
{
}
 
Shape类也有了变化:还记得咱们曾经说过,Qt内置了不少item,所以咱们没必要所有重写这个item。因此,咱们要使用Qt提供的类,就不须要在咱们的类里面添加新的数据成员了。这样,咱们就有了不带有额外的数据成员的Shape。那么,为何还要提供Shape呢?由于咱们在scene的鼠标事件中须要修改这些数据成员,若是没有这个父类,咱们就须要按照Code写一个长长的switch来判断是那一个图形,这样是很麻烦的。因此咱们依然建立了一个公共的父类,只要调用这个父类的draw函数便可。
 
line.h
#ifndef LINE_H
#define LINE_H

#include <QGraphicsLineItem>
#include "shape.h"

class Line : public Shape, public QGraphicsLineItem
{
public:
        Line();

         void startDraw(QGraphicsSceneMouseEvent * event);
         void drawing(QGraphicsSceneMouseEvent * event);
};

#endif // LINE_H
 
line.cpp
#include "line.h"

Line::Line()
{
}

void Line::startDraw(QGraphicsSceneMouseEvent * event)
{
        setLine(QLineF( event->scenePos(), event->scenePos()));
}

void Line::drawing(QGraphicsSceneMouseEvent * event)
{
        QLineF newLine(line().p1(), event->scenePos());
        setLine(newLine);
}
 
Line类已经和前面有了变化,咱们不只仅继承了Shape,并且继承了QGraphicsLineItem类。这里咱们使用了C++的多继承机制。这个机制是很危险的,很容易发生错误,可是这里咱们的Shape并无继承其余的类,只要函数没有重名,通常而言是没有问题的。若是不但愿出现不推荐的多继承(无论怎么说,多继承虽然危险,但它是符合面向对象理论的),那就就想办法使用组合机制。咱们之因此使用多继承,目的是让Line类同时具备Shape和QGraphicsLineItem的性质,从而既能够直接添加到QGraphicsScene中,又能够调用startDraw()等函数。
 
一样的还有Rect这个类:
 
rect.h
#ifndef RECT_H
#define RECT_H

#include <QGraphicsRectItem>
#include "shape.h"

class Rect : public Shape, public QGraphicsRectItem
{
public:
        Rect();

         void startDraw(QGraphicsSceneMouseEvent * event);
         void drawing(QGraphicsSceneMouseEvent * event);
};

#endif // RECT_H
 
rect.cpp
#include "rect.h"

Rect::Rect()
{
}

void Rect::startDraw(QGraphicsSceneMouseEvent * event)
{
        setRect(QRectF( event->scenePos(), QSizeF(0, 0)));
}

void Rect::drawing(QGraphicsSceneMouseEvent * event)
{
        QRectF r(rect().topLeft(),
                         QSizeF( event->scenePos().x() - rect().topLeft().x(), event->scenePos().y() - rect().topLeft().y()));
        setRect(r);
}
 
Line和Rect类的逻辑都比较清楚,和前面的基本相似。所不一样的是,Qt并无使用咱们前面定义的两个Qpoint对象记录数据,而是在QGraphicsLineItem中使用QLineF,在QGraphicsRectItem中使用QRectF记录数据。这显然比咱们的两个点的数据记录高级得多。其实,咱们也彻底可使用这样的数据结构去重定义前面那些Line之类。
 
这样,咱们的程序就修改完毕了。运行一下你会发现,几乎和前面的实现没有区别。这里说“几乎”,是在第一个点画下的时候,scene会移动一段距离。这是由于scene是自动居中的,因为咱们把Line的第一个点设置为(0, 0),所以当咱们把鼠标移动后会有一个偏移。
 
看到这里或许并无显示出Graphics View的优点。不过,建议在Line或者Rect的构造函数里面加上下面的语句,
 
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsItem::ItemIsSelectable, true);
 
此时,你的Line和Rect就已经支持选中和拖放了!值得试一试哦!不过,须要注意的是,咱们重写了scene的鼠标控制函数,因此这里的拖动会很粗糙,甚至说是不正确,你须要动动脑筋从新设计咱们的类啦!