-
设计良好的图形用户界面,界面中要求至少有默认大小的三角形、方框、圆形、椭圆、连接线等五种元素可供用户选择后,绘制到画布上;
-
允许用户添加文字描述;
-
单击可以选中图形,并允许对图形的拷贝复制;
-
多个图形可以组合,组合后的图形同样有拷贝复制的功能;
-
支持撤销上一步操作的功能。
-
右键点击图形可通过文本框对图形的尺寸进行调整;
-
支持个性化界面,包括设置界面字体大小、界面皮肤等;
-
支持图形(包括组合图形)的拖拽调整图形大小;
-
支持撤销多步的功能;
-
设计一种硬盘文件存储格式可以保存用户绘制的图形,并可以加载。
- 支持拖拽移动图形,包括组合图形的移动;
- 可以自定义设置画笔颜色;
- 支持导出图片。
本项目采用Java Swing
来实现GUI,Swing
是一个为Java设计的GUI工具包,属于Java基础类的一部分。是一个基于Java的跨平台MVC框架,并且使用了高度模块化的架构,可以通过接口的方式使用各种定制框架来扩展Swing
。
本项目主要采用了4种设计模式:
- 单例模式
- 原型模式
- 命令模式
- 观察者模式
项目的目录结构如下:
├─cn
│ └─edu
│ └─nju
│ │ Start.java
│ │
│ ├─Command
│ │ ChangeSizeCommand.java
│ │ Command.java
│ │ CreateCommand.java
│ │ DeleteCommand.java
│ │ MoveCommand.java
│ │ PasteCommand.java
│ │
│ ├─shape
│ │ AbstractShape.java
│ │ Ellipse.java
│ │ Line.java
│ │ RectAngle.java
│ │ Shape.java
│ │ Text.java
│ │ TriAngle.java
│ │
│ └─widget
│ MainWindow.java
│ MyColorPanel.java
│ MyDrawPanel.java
│ MyMenuBar.java
│ MyStatusBar.java
│ MyToolBar.java
│ Observer.java
│ Operation.java
│ Subject.java
│
├─icon
| .png
| .
| .
下面按照软件包的划分介绍我的设计思路。
首先定义图形的抽象类AbstractShape
,抽象方法有:
draw
绘制函数contains
判断点是否在图元内部update
更新图元位置坐标changeSize
改变图元大小
具体图形类有四种:Ellipse
椭圆(也就是圆)类,RectAngle
矩形类,TriAngle
三角形类,Line
直线类,Text
文本类。
Shape
类主要用于表示图元的位置信息
具体图元的绘制则使用java.awt.geom
包中2D图形的draw
方法,以椭圆为例:
public class Ellipse extends AbstractShape{
private Ellipse2D.Double ellipse; //geom包中的Ellipse2D
// ...
@Override
public void draw(Graphics2D g2d) {
// ...
g2d.draw(ellipse);
}
}
UML类图如下:
抽象图形类继承自
Serializable
是为了实现深克隆deepClone
,这是原型模式。
显然设计GUI用JFrame
来创建窗口,窗口上放各种JPanel
组件即可。本项目中类MainWindow
就继承自JFrame
,上面有组件:
- 画布
MyDrawPanel
- 工具栏
MyToolBar
- 取色盘
MyColorPanel
均继承自JPanel
类。
窗口的设置还有菜单栏MyMenuBar
,继承自JMenuBar
。
还有Operation
类,是为了实现撤销操作的命令执行者。
显然设计GUI用JFrame
来创建窗口,窗口上放各种JPanel
组件即可。本项目中类MainWindow
就继承自JFrame
,上面有组件:
- 画布
MyDrawPanel
- 工具栏
MyToolBar
- 取色盘
MyColorPanel
均继承自JPanel
类
大部分操作都是在使用鼠标在画布上进行的,因此需要重写鼠标事件。MyDrawPanel
内有大量为了交互而设置地字段和方法,这里不赘述了。关键的字段有:
ArrayList<AbstractShape> shapes
来存储画布上的所有图形。- 同时为了实现撤销操作,需要用
ArrayList<Command> undoCmds
来存储所有命令。
UML类图如下:
取色盘MyColorPanel
主要作用就是提取颜色,设置MainWindow
中的penColor
字段,UML类图如下:
工具栏MyToolBar
上面有一系列JButton
按钮,获取点击来设置MainWindow
中的currentMode
字段,UML类图如下:
可以看到他们都实现了接口
Observer
,也就是说他们都是观察者,这是为了实现更换皮肤和字体的功能,采取的观察者模式
MainWindow
需要设置菜单,菜单栏MyMenuBar
类,继承自JMenuBar
,里面包含:
- 菜单
MyMenu
类继承自JMenu
,其中又包含: - 菜单项
MyMenuItem
类继承自JMenuItem
。
在MyMenuBar
的构造函数里,add
添加菜单和菜单项。这里主要介绍为了实现自定义皮肤和界面字体的功能,在MyMenuBar
中包含字段:
skin
皮肤类型和fontSize
界面字体大小。private ArrayList<Observer> observers
观察者
将其作为被观察对象,实现接口Subject
,当skin
或者fontSize
发生变化时,就notifyObservers
通知注册observers
中的成员改变字体或者背景皮肤。
接口Subject
的抽象方法有:
void attach(Observer)
添加新的观察者void detach(Observer)
删除观察者void notifySkinObservers(String)
通知观察者换皮肤void notifyFontObservers(Font)
通知观察者换字体
实现了Observer
接口,也就是抽象方法updateSkin
改变皮肤和updateFont
改变字体,收到通知后做出相应改变。
整体菜单栏的UML类图如下:
为了实现多步撤销,采用命令模式,将命令的发出者和命令的执行者解耦,Operation
类就是命令执行者。
由于所有绘图操作都要在drawPanel
上操作,为了避免drawPanel
类过于复杂,单独创建了该类放在了Widget
包内,内部设有字段MyDrawPanel drawPanel
。
此类是实际执行各种操作的类,内部方法有:
Set<AbstractShape> paste(MouseEvent, Set<AbstractShape>)
通过鼠标进行粘贴操作void delete(Set<AbstractShape>)
删除图元void AbstractShape add(String, MouseEvent)
创建新图元void changeSize(int, int, Set<AbstractShape>)
改变图元大小void dragSize(int, int, Set<AbstractShape>)
拖拽改变图元大小
等等。
UML类图如下:
由于采用命令模式,经过分析本项目涉及的命令有5类:
PasteCommand
粘贴命令CreateCommand
创建命令ChangeSizeCommand
改变图元大小命令MoveCommand
移动图元命令DeleteCommand
删除图元命令
均继承自接口Command
,实现了抽象方法:
void execute(MouseEvent e)
需要鼠标信息的执行void execute()
直接执行void undo()
撤销
UML类图如下:
程序只有一个主窗口mainWindow
,那么这就是个单例,私有化MainWindow
的构造函数,使用双检锁实现:
public class MainWindow extends JFrame {
private static volatile MainWindow instance = null; // 单例
//
//...
//
// 双检锁 懒汉式获取单例的外部接口
public static MainWindow getInstance () {
if (instance == null) {
synchronized (MainWindow.class) {
if (instance == null) {
instance = new MainWindow();
drawPanel.mainWindow = instance;
}
}
}
return instance;
}
}
之前已经介绍过,AbstractShape
抽象图形类需要深拷贝,因此继承了Serializable
接口,深拷贝方法如下:
//使用序列化技术实现深拷贝
public AbstractShape deepClone() throws IOException, ClassNotFoundException, OptionalDataException {
//将对象写入流中
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(bao);
oss.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (AbstractShape) ois.readObject();
}
为了实现多步撤销的功能,在画板drawPanel
上进行操作时发出的是一个个命令,并且记录命令到列表ArrayList<Command> undoCmds
中,如下面代码所示:
public void mouseClicked(MouseEvent e) {
// ...
if (e.getButton() == MouseEvent.BUTTON1) { // 点击鼠标左键
String currentMode = MainWindow.getCurrentMode();
Color penColor = MainWindow.getPenColor();
if (currentMode.equals("null")) {
System.out.println("none chosen!");
} else if (currentMode.equals("copy")) { // 复制图元
PasteCommand cmd = new PasteCommand(operation, chosenShapes);
cmd.execute(e);
undoCmds.add(cmd);
System.out.println("undocmd size: " + undoCmds.size());
repaint();
} else { // 创建图元
CreateCommand cmd = new CreateCommand(operation, chosenShapes);
cmd.execute(currentMode, e);
undoCmds.add(cmd);
repaint();
}
// ...
}
// ...
}
创建的命令cmd
调用execute
方法,在命令内部有对Operation
类的引用,也就是命令的执行者。
在撤回按钮上添加点击事件,点击后取出undoCmds
中最后一条命令,执行其undo
操作,然后从命令列表中删去该命令,代码如下:
toolBar.toolButtons[8].addActionListener(e -> {
ArrayList<Command> undoCmds = drawPanel.getUndoCmds();
if(undoCmds.size() > 0) {
Command cmd = undoCmds.get(undoCmds.size() - 1);
cmd.undo();
undoCmds.remove(cmd);
System.out.println("undocmd size: " + drawPanel.getUndoCmds().size());
}
repaint();
});
皮肤的设置我的实现就是换背景颜色setBackGround(Color)
。MyMenuBar
菜单栏作为被观察对象,被其attach
注册的观察者有:
-
所有菜单
MyMenu
和菜单项MyMenuItem
(构造函数中注册 -
取色板
colorPanel
,状态栏statusBar
和工具栏toolBar
(由mainWindow
给myMenu
注册的
点击某些菜单项则通知观察者改变皮肤或者字体,如下代码所示:
// 改变皮肤为"spring"风格
Spring.addActionListener(e -> {
skin = "spring";
notifySkinObservers(skin);
});
// 改变界面字体为微软雅黑
YaHei.addActionListener(e -> {
Font font = new Font("微软雅黑", Font.PLAIN, getFontSize());
notifyFontObservers(font);
});
要以某种格式保存用户绘制的图,起始就是保存drawPanel
上的ArrayList<AbstractShape> shapes
列表,这里面包含了所有图形的全部信息。
因此只需要把这个列表写到文件里就好了,关键代码如下:
try {
//根据要保存的文件创建对象输出流
FileOutputStream fis = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fis);
//将容器里面所绘制的图形利用对象流全部写入选中的文件中
oos.writeObject(MainWindow.getDrawPanel().getShapes());
JOptionPane.showMessageDialog(null, "保存成功!");
oos.close();
} catch (Exception e1) {
e1.printStackTrace();
}
由于保存的文件就是AbstractShape
的列表,因此加载时只需要读取文件然后设置成当前画布的shapes
列表即可,关键代码如下:
try {
// ...
// 选中了相应的文件,则选中的文件创建对象输入流
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
// 将读出来的对象转换成父类对象的容器进行接收
MainWindow.getDrawPanel().setShapes((ArrayList<AbstractShape>) ois.readObject());
MainWindow.getDrawPanel().setChooseMore(false);
ois.close();
MainWindow.getDrawPanel().repaint();
} catch (Exception e1) {
e1.printStackTrace();
}
实现了基础功能和拓展功能的全部需求,并且额外实现了:
- 支持拖拽移动图形,包括组合图形的移动;
- 支持图元删除,包括组合图形的删除;
- 可以自定义设置画笔颜色;
- 支持导出图片。
双击Jar包Paint.jar
即可运行。
-
绘制图形时,首先在工具栏选中需要的图形然后单击鼠标即可绘制
注意创建图元时是点击,不要拖拽,想要调整大小创建后再调整
-
添加文字描述时点击"T"按钮,然后点击想要描述的位置,会弹出对话框,输入内容即可
-
组合图形时按下ctrl键即可选中多个图形
-
复制图形时首先选中一个或多个图形,然后点击复制按钮,再点击想要粘贴的位置即可
支持快捷键Ctrl+C和Ctrl+V操作,如果Ctrl+V粘贴的画默认粘贴位置在原图形的右下角,和ppt类似
演示如下:
- 可右击图元弹出对话框调整图元大小,也可以选中图元后拖拽右下角调整图元大小,均支持组合图形
演示如下:
- 支持拖拽移动(包括组合图形),撤销多步,操作后点击撤回按钮或者Ctrl+Z快捷键即可;支持自定义界面皮肤和界面字体,在菜单栏点击即可,演示如下:
-
支持保存可存储文件,点击菜单栏保存或者Ctrl+S快捷键;同样支持加载可存储文件,点击菜单栏打开或者Ctrl+O快捷键;支持导出图片,点击菜单栏导出图片或者Ctrl+I快捷键。
可存储文件格式后缀为
.my
演示如下:
- 支持图元(包括组合图元)删除,选中图元后点击删除按钮或者Ctrl+D快捷键;点击帮助按钮,会跳转到github页面,演示如下:
这是我第一次用Java写完整项目(大数据用的Hadoop基本是填框架
关于Swing的使用参考了很多资料,面向对象**是一致的,但Java写起来比C++方便点,比如不用垃圾回收,还有就是引用类型和C++区别很大,但是避免了区分指针和引用变量(🤭
Contact with me: