背景
如图,这是GoF的《设计模式》一书第五章中Memento备忘录模式动机一节的例子。
应用介绍摘自Gang of Four的《设计模式》第五章行为型模式的备忘录模式:
例如,考虑一个图形编辑器,它支持图形对象之间的连线。用户可用一条直线连接两个矩形,者两个矩形仍能保持连接。在移动过程中,编辑器自带伸展这条直线以保持该连接。
完成效果
原书没有给出实现代码,所以我基于Java语言实现一个。
可拖拽任意一个矩形移动位置。可按Ctrl+Z撤销操作,回到图形上一次的位置。下面有完整代码和注释,读者可先尝试运行代码看看效果。
如何实现
定义一个Shape接口,其中Shape只有一个draw()方法。
分别定义Rect和Line类,实现Shape接口。
定义MyPanel继承Panel类,用于画板。保存2个矩形和一条直线,注册了鼠标监听器,实现拖动矩形的逻辑。
Stack< MyRectMemento >保存2个矩形的位置信息,undoShape()用于撤销操作和恢复图形上一次的位置。其中MyRectMemento作为备忘录,下面讲备忘录模式的时候会提到
定义MyFrame,继承Frame。注册键盘监听器,用于监听Ctrl+Z组合键。
执行撤销逻辑。
里面应用了一个模式——备忘录模式
定义MyRectMemento类作为MyRect类的备忘录,保存两个矩形的位置信息。
MyPanel作为Originator原发器和Caretaker管理者,原发器用于在鼠标点击事件的时候创建一个备忘录,记录当前矩形位置的状态。管理者利用内部变量Stack< MyRectMememto >保存好备忘录。当按Ctrl+Z时会触发在MyFrame类的键盘监听器,监听器会执行撤销逻辑。
完整代码【基于Java】
共分为6个类加上一个接口。
Client
public class Client {
public static void main(String[] args) {
MyFrame myFrame = new MyFrame();
}
}
Line
import java.awt.*;
public class Line implements Shape {
private int xBegin,yBegin,xEnd,yEnd;
private Color color;
private Graphics g;
public Line(int xBegin, int yBegin, int xEnd, int yEnd, Graphics g, Color color) {
this.xBegin = xBegin;
this.yBegin = yBegin;
this.xEnd = xEnd;
this.yEnd = yEnd;
this.g = g;
this.color = color;
}
@Override
public void draw() {
g.setColor(color);
g.drawLine(xBegin,yBegin,xEnd,yEnd);
}
public void setxBegin(int xBegin) {
this.xBegin = xBegin;
}
public void setyBegin(int yBegin) {
this.yBegin = yBegin;
}
public void setxEnd(int xEnd) {
this.xEnd = xEnd;
}
public void setyEnd(int yEnd) {
this.yEnd = yEnd;
}
}
MyFrame
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public class MyFrame extends JFrame {
MyPanel myPanel;
public MyFrame() {
this.myPanel = new MyPanel();
add(myPanel);//把画布装入边框
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);//设置窗口关闭方式
Dimension dim = this.getToolkit().getScreenSize();// 获取屏幕分辨率
setBounds(dim.width / 4, dim.height / 4, (dim.width / 2 + 40),dim.height / 2);// 窗体初始大小及定位
setTitle("GoF备忘录模式例子实现(可拖动图形,按Ctrl+Z可撤销操作)");
setVisible(true);//使组件可见
/*
因为在所有组件还没画出时,是不能获得Graphics的,只会返回Null,画图形操作放在setVisible(true)之后
另外,paint()画操作是基于JFrame的,因为我们要重新画整个界面,从而消除单个图形之前的痕迹。
读者可调用语句顺序尝试一下。
*/
myPanel.initShape();//获得JPanel的画笔,并初始化图形对象
addKeyListener();//绑定键盘监听器
}
@Override
public void paint(Graphics g) {
super.paint(g);
myPanel.paint(g);//通过JPanel的画笔g画出图形
}
/**
* 绑定键盘监听器:当点击Ctrl+Z时,执行撤销操作
*/
private void addKeyListener(){
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if ((e.isControlDown() == true)
&& (e.getKeyCode() == KeyEvent.VK_Z)) {
myPanel.undoShape();
}
}
});
}
}
MyPanel
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.Stack;
public class MyPanel extends JPanel {
private Rect rect1;
private Rect rect2;
private Line line;
private int xBefore,yBefore;
private Stack<MyRectMemento> rectList = new Stack<>();//保存2个矩形位置的历史记录
public MyPanel() {
Dimension dim = this.getToolkit().getScreenSize();// 获取屏幕分辨率
setPreferredSize(new Dimension((dim.width / 2 + 40),dim.height / 2));// 画布初始大小及定位
setBackground(Color.white);//设置背景色为白色
}
public void initShape(){
Graphics g = getGraphics();
this.rect1 = new Rect(100,100,100,100,g,Color.blue);
this.rect2 = new Rect(300,300,100,100,g,Color.green);
this.line = new Line(150,150,350,350,g,Color.red);
rectList.push(new MyRectMemento(rect1.getX(),rect1.getY(),rect2.getX(),rect2.getY()));//记录2个矩形的初始位置
addMouseListener();
addMouseMotionListener();
}
@Override
public void paint(Graphics g) {
super.paint(g);
rect1.draw();
rect2.draw();
line.draw();
}
/**
* 绑定鼠标监听器
* 自定义鼠标点击,释放事件
*/
private void addMouseListener(){
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (isInTheRect(e.getX(),e.getY(),rect1)){
rect1.setSelected(true);
xBefore = e.getX();
yBefore = e.getY();
}else if (isInTheRect(e.getX(),e.getY(),rect2)){
rect2.setSelected(true);
xBefore = e.getX();
yBefore = e.getY();
}
//赋值当前鼠标点击时2个矩形的位置
rectList.push(new MyRectMemento(rect1.getX(),rect1.getY(),rect2.getX(),rect2.getY()));
}
@Override
public void mouseReleased(MouseEvent e) {
rect1.setSelected(false);
rect2.setSelected(false);
}
});
}
private void addMouseMotionListener(){
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {
if (rect1.isSelected() && checkPoint(e.getX(),e.getY())){
rect1.setX(rect1.getX() + (e.getX() - xBefore));
rect1.setY(rect1.getY() + (e.getY() - yBefore));
xBefore = e.getX();
yBefore = e.getY();
line.setxBegin(rect1.getX() + 50);
line.setyBegin(rect1.getY() + 50);
}if (rect2.isSelected() && checkPoint(e.getX(),e.getY())){
rect2.setX(rect2.getX() + (e.getX() - xBefore));
rect2.setY(rect2.getY() + (e.getY() - yBefore));
xBefore = e.getX();
yBefore = e.getY();
line.setxEnd(rect2.getX() + 50);
line.setyEnd(rect2.getY() + 50);
}
//重画图形
MyFrame myFrame = (MyFrame) getParent().getParent().getParent().getParent();
myFrame.paint(getGraphics());
}
@Override
public void mouseMoved(MouseEvent e) {
}
});
}
/**
* //检测 点(mx,my) 是否在矩形1上
* @param mx 鼠标当前x坐标
* @param my 鼠标当前y坐标
* @param rect 当前矩形对象
* @return true or false
*/
private boolean isInTheRect(int mx, int my,Rect rect){
int x = rect.getX(),y = rect.getY(),
height = rect.getHeight(),width = rect.getWidth();
if (mx >= x && mx <= x + width && my >= y && my <= y + height){
return true;
}else {
return false;
}
}
/**
* 检查鼠标当前位置是否超出应用边界
* @param mx 鼠标当前x坐标
* @param my 鼠标当前y坐标
* @return true or false
*/
private boolean checkPoint(int mx, int my){
if (mx < 0 || my < 0 || mx > getWidth() || my > getHeight()) {
return false;
}else {
return true;
}
}
public Stack<MyRectMemento> getRectList() {
return rectList;
}
/**
* 当按Ctrl+Z时 恢复至图形上一次的位置
*/
public void undoShape(){
if (rectList.size() >= 1){
MyRectMemento memento = rectList.pop();
rect1.setX(memento.getX1());
rect1.setY(memento.getY1());
rect2.setX(memento.getX2());
rect2.setY(memento.getY2());
line.setxBegin(rect1.getX() + 50);
line.setyBegin(rect1.getY() + 50);
//重画图形
MyFrame myFrame = (MyFrame) getParent().getParent().getParent().getParent();
myFrame.paint(getGraphics());
}
}
}
MyRectMemento
/**
* 作为MyRect的备忘录,保存两个矩形的位置信息x1,y1,z2,y2
*/
public class MyRectMemento {
private final int x1;
private final int y1;
private final int x2;
private final int y2;
public MyRectMemento(int x1, int y1, int x2, int y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
public int getX1() {
return x1;
}
public int getY1() {
return y1;
}
public int getX2() {
return x2;
}
public int getY2() {
return y2;
}
}
Rect
import java.awt.*;
public class Rect implements Shape {
private int x,y,height,width;
private Graphics g;
private Color color;
private boolean isSelected = false;
public Rect(int x, int y, int height, int width, Graphics g, Color color) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.g = g;
this.color = color;
}
@Override
public void draw() {
g.setColor(color);
g.drawRect(x,y,width,height);
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
}
Shape
public interface Shape {
void draw();
}