Qt和设计模式-画板的备忘录

  • 备忘录模式概述
    备忘录模式提供了一种状态恢复的实现机制,使得用户可以很方便的回到一个特定的历史状态,当前很多软件都提供了撤销(Undo)和恢复(反撤销)操作,其中就使用了备忘录模式。
  • 备忘录模式定义
    备忘录模式(Memento Pattern):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。
    本次需要实现一个画板绘画,支持撤销、反撤销动作。
    效果如下
    这里写图片描述
    图1 画板的撤销动作

    以下是实现部分:

  • 分析需求
    需要使用备忘录保存画板每一次绘画的所有坐标,以便可以保存。备忘录模式中的三个角色:Originator(原发器)、Memento(备忘录)、Caretaker(负责人)。在这里需要保存坐标状态的类就是原发器,也就是Widget画板。备忘录需要保存坐标列表作为一次绘画状态。负责人也就是管理者负责保存多个备忘录,以便恢复状态。

  • 具体实现
    需要重写QWidget的鼠标事件以便可以绘图

virtual void mousePressEvent(QMouseEvent *event);
virtual void mouseMoveEvent(QMouseEvent *event);
virtual void mouseReleaseEvent(QMouseEvent *event);
virtual void paintEvent(QPaintEvent *event);

绘图大致流程

Created with Raphaël 2.1.0 鼠标点击操作 鼠标左击? 记录鼠标移动坐标 松开左键绘制 绘制 yes no

所以为了需要将一次绘制状态保存,Caretaker类需要获取到每次的Memento,可以使用Qt的信号与槽机制,这里有至少两种方法可以采用:

  1. 通过信号与槽将Memento传出去
  2. 通过信号与槽,让Caretaker调用Widget(原发器)的save()接口(不能将Memento暴露给外界使用,只能Caretaker和Widget之间使用)。

恢复状态的实现同样的方法让Caretaker调用Widget的load()接口。

以下是具体实现步骤
Memento类 .h

//memento.cpp类实现比较简单 .cpp文件就不浪费篇幅了
//存储paint的画笔移动坐标(一次绘画的所有点集合)
class Memento
{
public:
    Memento(QList<QPoint> pointList);
    //一般情况下为了保证Memento只为某一个类备忘 C++中可以用友元实现
    //提供set 和 get接口破坏类封装(虽然友元也破坏了...)
    friend class Widget;

private:
    QList<QPoint> m_pointList;
};

MementoCaretaker类 .h or .cpp

//.h
class MementoCaretaker : public QObject
{
    Q_OBJECT
public:
    void push(Memento* m);  //保存状态
    Memento* undo();        //撤销保存(实际上已经保存在内存,可根据需求调整)
    Memento* restore();     //反撤销

private:
    QStack<Memento*> m_stack;         //保存所有状态
    static int       m_currentIndex;  //保存撤销和反撤销的位置
};

//.cpp
int MementoCaretaker::m_currentIndex = 0;

void MementoCaretaker::push(Memento *m)
{
    m_stack.push(m);
    m_currentIndex++;
}

Memento *MementoCaretaker::restore()
{
    if(m_currentIndex + 1 >= m_stack.size())
    {
        return nullptr;
    }
    m_currentIndex++;
    Memento* m = m_stack[m_currentIndex];
    return m;
}

Memento *MementoCaretaker::undo()
{
    if(m_currentIndex <= 0)
    {
        return nullptr;
    }
    m_currentIndex--;
    qDebug() << m_stack.size() << "  "<<m_currentIndex;
    Memento* m = m_stack[m_currentIndex];
    return m;
}

Widget类 .h or .cpp

//.h
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();

protected:
    virtual void mousePressEvent(QMouseEvent *event);
    virtual void mouseMoveEvent(QMouseEvent *event);
    virtual void mouseReleaseEvent(QMouseEvent *event);
    virtual void paintEvent(QPaintEvent *event);

private:
    bool          m_bPrint; //鼠标左击更为 true
    QList<QPoint> m_list;   //当前绘画状态


signals:
    void signal_memento();    //需要保存状态时 发出此信号

public slots:
    Memento* slot_save();       //外部接口
    void slot_load(Memento* m); //外部接口
};

//.cpp
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , m_bPrint(false)
{
    setWindowFlags(Qt::FramelessWindowHint);
}
Widget::~Widget()
{

}

Memento *Widget::slot_save()
{
    update();
    Memento *m = new Memento(m_list);
    //松开鼠标左键  证明需要保存 发出信号 外部调用该函数  然后完成绘制
    return m;
}

void Widget::slot_load(Memento* m)
{
    if(m == nullptr)
    {
        return;
    }
    m_list.clear();
    m_list = m->m_pointList;
    qDebug() <<"load";
    update();
}

void Widget::mousePressEvent(QMouseEvent *event)
{
    if(event->buttons() == Qt::LeftButton)
    {
        m_bPrint = true;
        //m_list.clear();
        //若想让线不是连接在一起  则需要导入整个Memento的列表
    }
}

void Widget::mouseMoveEvent(QMouseEvent *event)
{
    if(m_bPrint)
    {
        m_list.append(event->pos());
    }
}

void Widget::mouseReleaseEvent(QMouseEvent *event)
{
    if(m_bPrint)
    {
        m_bPrint = false;
        emit signal_memento();
        qDebug() << "updata";
    }
}

void Widget::paintEvent(QPaintEvent *event)
{
    if(!m_list.isEmpty())
    {
        QPainter p(this);
        p.setRenderHint(QPainter::Antialiasing);    //抗锯齿化
        p.setPen(QColor(250,24,0));

        qDebug() <<"line";
        for(int i = 0; i < m_list.size() - 1; i++)
        {
            p.drawLine(m_list.at(i), m_list.at(i+1));
        }
    }
    QWidget::paintEvent(event);
}

最后定义了一个Helper类测试使用
Helper类.h or .cpp

//.h
class Helper : public QWidget
{
public:
    Helper();

    void initLayout();  //初始化界面
    void init();        //初始化数据

private:
    Widget*           m_pPaintWidget;
    QPushButton*      m_pUndoButton;       //撤销
    QPushButton*      m_pRestoreButton;    //撤销恢复

    MementoCaretaker* m_pCaretaker;
};

//.cpp
Helper::Helper()
{
    initLayout();
    init();
}

void Helper::initLayout()
{
    m_pPaintWidget = new Widget;
    m_pPaintWidget->show();
    m_pUndoButton = new QPushButton("撤销", this);     //撤销
    m_pRestoreButton = new QPushButton("反撤销", this);    //撤销恢复

    QGridLayout *l = new QGridLayout;

    l->addWidget(m_pUndoButton);
    l->addWidget(m_pRestoreButton);
    setLayout(l);

    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
}

void Helper::init()
{
    m_pCaretaker = new MementoCaretaker;

    //保存状态
    connect(m_pPaintWidget, &Widget::signal_memento, [=]()
    {
        m_pCaretaker->push(m_pPaintWidget->slot_save());
    });

    //撤销
    connect(m_pUndoButton, &QPushButton::clicked, [=]()
    {
        m_pPaintWidget->slot_load(m_pCaretaker->undo());
    });

    //反撤销
    connect(m_pRestoreButton, &QPushButton::clicked, [=]()
    {
        m_pPaintWidget->slot_load(m_pCaretaker->restore());
    });
}

最终实现效果如图1所示。

  • 谈谈备忘录模式
    备忘录模式的封装:备忘录是一个比较特殊的对象,对备忘录的创建、控制等这些使用方法应该只有原发器拥有,而管理类只负责管理,其他类不能访问到备忘录,所以需要对备忘录封装。理想的情况是只允许生成该备忘录的那个原发器访问备忘录的内部状态。所以C++语言中可以使用friend,让原发器类和备忘录类成为友元类。在Java语言中可以将原发器类和备忘录类放在一个包中,让它们之间满足默认的包内可见性,也可以将备忘录类作为原发器类的内部类,只有原发器才可以访问备忘录中的数据,其他对象都无法使用备忘录中的数据。
  • 备忘录模式的优缺点
    提供了一种状态恢复的能力,外界无法访问到原发器和备忘录的具体数据(不用提供get/set方法给外界)实现了数据的封装。
    但是需要保存的状态多了,内存开销很大
  • 扩展
    支持撤销动作的大多数都可使用备忘录方法,或者使用命令模式实现。

新手一枚,有什么不对的,轻喷。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值