文章目录
完整源代码放在最后
1.画板构成及架构方式
1.1 构成
首先来看一下我所写的画板项目由哪些包构成:
- entry包是程序的入口(也就是main方法所在的地方)
它的内容十分简单,如下:
- 然后先说window包,顾名思义这个包里面定义了我们画板窗口的显示外观,我们可以称这一部分为“前端”。
画板的外观是这个样子的:
对于java的gui设计我不是很习惯,比如其中一些布局什么的,而且swing也不是类似于c++ qt框架这种可视化编程,所以设计出来的GUI也就没有那么好看。 - 有了画板的外观后,我们需要对画板上发生的事件进行监听。例如:在用户点击“直线”后,我们需要将画笔的模式调整到直线,在点击颜色“红色”后程序也需要将画笔颜色设置为红色等等。这些工作需要我们对鼠标进行监听,以实现某些特定的功能,而mouseListner包就是负责完成这样的工作。
- 最后一个要说的包是repaint包,我之所以给它命名为repaint是它在画板重绘的过程中起到了至关重要的作用(包括画板撤销这一功能中也起了很大作用)。它是我们画出来的图形的载体,例如,我们用画笔画出来一条直线,这时候我们就创建了一个repaint对象,它保存了这条直线的一些关键参数(起始位置,结束位置,线条粗细,线条颜色等信息)让我们可以在重绘的时候可以把这条直线重新画出来;画了一个圆,那么我们也需要保存下这个圆的关键参数。
1.2 架构方式
那么有了以上这些东西,我们如何把一个画板构建起来?我的思路是这样的:
- 把画板显示出来–>建立一个画板类,负责显示画板
- 给画板上的控件添加监听–>建立一个监听类,负责监听画板控件
- 重写画板的重绘函数以及实现撤销功能–>建立一个repaint类
- 最后,建立一个entry类,作为程序的入口
2.一个优化
2.1 窗口重绘机制
这里我们有必要提到提到java窗口的重绘机制。在我们将窗口最小化到任务栏后再点开,在我们将窗口最大化,在我们改变窗口大小,这些时候,窗口对象都会自动调用一个重绘函数paint(),它会重现绘画窗口的各部分控件,但是我们使用画笔画出来的所有图像都是不会被自动重绘的,所以这需要我们重写原有的重绘函数让窗口可以正常显示。
2.2 引发的问题
因为每次我们改变窗口大小,所有内容都要被重新绘制,当内容很少时还无伤大雅,但是当内容一多,程序运行就会非常缓慢(慢到你可以很清楚地看到看到你之前所画的所有内容从无到有被慢慢画出来,非常影响使用体验)。在撤销的时候也存在同样的问题,我们将所有画出来的东西都保存为一个个的对象,存到一个vector中,这样重绘的时候我们遍历vector按照其中保存下的关键参数重新绘制这些内容,在撤销时,我们将vector末尾的元素删除,再调用重绘函数,此时就可以达到重绘的目的。但同样的,只要内容一多我们调用重绘函数就会非常缓慢。
2.3 优化
这个优化也只能在一定程度上缓解这一问题,不能彻底解决,当内容一多依然会引发同样的问题。思路是在调用重绘函数重新绘制所有内容的时候使用多线程,将vector分为四部分同时进行绘制,这样能将重绘的时间缩短到原有时间的1/4,使用体验有所改善。
对此大家有什么好的解决方法可以在评论区一起交流
3.知识点梳理
那么要做到以上内容你需要掌握哪些知识点?
- 程序设计:java基础,Vector容器
- 窗口设计:swing库的基本操作
- 利用画笔画图:awt库的基本操作
- 简单了解窗口的一些机制
4.源代码
4.1 runtime environment
我的运行环境:eclipse+java jdk14.02
4.2 source code
源代码一共由4个源文件构成,一共696行代码。
ProgramEntry.java:9行代码
package entry;
import window.Outlook;
public class ProgramEntry {
public static void main(String[] args) {
Outlook windowObj = new Outlook();
}
}
Outlook.java:231行代码
package window;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.Vector;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.border.MatteBorder;
import mouseListener.MouseListener_;
import repaint.Repaint;
public class Outlook extends JFrame {
MouseListener_ ml;
public Graphics g;
public JTextField inputText;
public JTextField gSizeDisplay;
public JPanel jpDrawZone;
public Graphics2D g2d;
// 存储画下的图形
public Vector<Repaint> imgStore = new Vector<Repaint>();
public Outlook() {
// 设置窗体属性
this.setTitle("Drawing Board");
this.setSize(950, 610);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
JPanel jpFunc = new JPanel();
JPanel jpColor = new JPanel();
jpDrawZone = new JPanel();
JPanel jpUpZone = new JPanel();
ml = new MouseListener_(this); //传递窗体对象
this.addMouseListener(ml);
this.addMouseMotionListener(ml);
// 添加形状
String[] funcArray = {"straight line", "curve", "rectangle","circle","text", "image", "eraser"};
for(int i=0;i<funcArray.length;i++) {
JButton button = new JButton(funcArray[i]);
button.setPreferredSize(new Dimension(80,30));
jpFunc.add(button);
button.addActionListener(ml);
}
// 添加颜色
String[] colorSet = {"black", "red", "green", "blue"};
Color[] colorArray = {Color.black, Color.red,Color.green,Color.blue};
for(int i=0;i<colorArray.length;i++) {
JButton button = new JButton();
button.setBackground(colorArray[i]);
button.setPreferredSize(new Dimension(30,30));
button.setText(colorSet[i]);
jpColor.add(button);
button.addActionListener(ml);
}
// 添加 改变画笔大小
String[] gSizeChange = {"+", "-"};
for(int i=0;i<gSizeChange.length;i++) {
JButton button = new JButton(funcArray[i]);
button.setPreferredSize(new Dimension(50,30));
button.setText(gSizeChange[i]);
jpColor.add(button);
button.addActionListener(ml);
}
// 显示画笔大小
JLabel sizeText = new JLabel("size:");
sizeText.setPreferredSize(new Dimension(40,30));
jpColor.add(sizeText);
gSizeDisplay = new JTextField("1");
gSizeDisplay.setPreferredSize(new Dimension(30,30));
gSizeDisplay.setEditable(true);
jpColor.add(gSizeDisplay);
// 添加画布
JLabel drawZone = new JLabel();
jpDrawZone.add(drawZone);
jpDrawZone.setBackground(Color.white);
// 控制大小和布局
//jpFunc.setBounds(100, 10, 800, 40);
//jpColor.setBounds(100, 55, 800, 30);
jpUpZone.setLayout(new BorderLayout());
jpUpZone.add(jpFunc, BorderLayout.NORTH);
jpUpZone.add(jpColor, BorderLayout.CENTER);
//jpDrawZone.setBounds(10, 95, 900, 460);
jpDrawZone.setBorder(new MatteBorder(5,5,5,5,Color.black)); // 给画布设置边界
this.getContentPane().add(jpUpZone, BorderLayout.NORTH);
this.getContentPane().add(jpDrawZone, BorderLayout.CENTER);
// 添加文本输入框
inputText = new JTextField();
inputText.setEditable(true); // 可直接编辑画笔大小
this.getContentPane().add(inputText, BorderLayout.SOUTH);
// 设置菜单栏
JMenuBar jmb = new JMenuBar();
this.setJMenuBar(jmb);
JMenu MORE = new JMenu("more");
JMenu FONT = new JMenu("font");
jmb.add(MORE);
jmb.add(FONT);
JMenuItem SAVE = new JMenuItem("save");
JMenuItem UNDO = new JMenuItem("undo");
JMenuItem CLEAR = new JMenuItem("clear");
MORE.add(SAVE);
MORE.add(UNDO);
MORE.add(CLEAR);
SAVE.addActionListener(ml);
UNDO.addActionListener(ml);
CLEAR.addActionListener(ml);
JMenu fontName = new JMenu("font name");
JMenu fontStyle = new JMenu("font style");
FONT.add(fontName);
FONT.add(fontStyle);
// 子菜单
JMenuItem FONTNAME_1 = new JMenuItem("宋体");
FONTNAME_1.addActionListener(ml);
JMenuItem FONTNAME_2 = new JMenuItem("微软雅黑");
FONTNAME_2.addActionListener(ml);
JMenuItem FONTNAME_3 = new JMenuItem("黑体");
FONTNAME_3.addActionListener(ml);
fontName.add(FONTNAME_1);
fontName.add(FONTNAME_2);
fontName.add(FONTNAME_3);
JMenuItem FONTSTYLE_1 = new JMenuItem("plain");
FONTSTYLE_1.addActionListener(ml);
JMenuItem FONTSTYLE_2 = new JMenuItem("italic");
FONTSTYLE_2.addActionListener(ml);
JMenuItem FONTSTYLE_3 = new JMenuItem("bold");
FONTSTYLE_3.addActionListener(ml);
fontStyle.add(FONTSTYLE_1);
fontStyle.add(FONTSTYLE_2);
fontStyle.add(FONTSTYLE_3);
this.setVisible(true);
g = this.getGraphics(); //获取窗口画笔对象
ml.setG(g); //传递画笔
}
@Override
public void paint(Graphics g) {
super.paint(g);
g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);// 设置画笔抗锯齿
int gap = imgStore.size()/4-1;
if (gap!=-1) {
t1.setBorderAndRun(0, gap);
t1.setBorderAndRun(gap+1, 2*gap+1);
t1.setBorderAndRun(2*gap+2, 3*gap+2);
}
t1.setBorderAndRun(3*gap+3, imgStore.size()-1);
}
// 定义四个多线程对象
MultiThreadRepaint t1 = new MultiThreadRepaint(),
t2 = new MultiThreadRepaint(),
t3 = new MultiThreadRepaint(),
t4 = new MultiThreadRepaint();
// 本地内部类,多线程重绘
class MultiThreadRepaint extends Thread {
private int bp, ep;
public MultiThreadRepaint() {
super();
}
public void setBorderAndRun(int bp_, int ep_) {
bp = bp_;
ep = ep_;
run();
}
public void run() {
for (int i=bp; i<=ep; ++i) {
Repaint imgObj = imgStore.elementAt(i);
g2d.setStroke(new BasicStroke(imgObj.gSize));
g2d.setColor(imgObj.gColor);
switch(imgObj.pattern) {
case 0: // straight line
g2d.drawLine(imgObj.bx, imgObj.by, imgObj.ex, imgObj.ey);
break;
case 1:
g2d.drawRect(Math.min(imgObj.bx,imgObj.ex),Math.min(imgObj.by,imgObj.ey),
Math.abs(imgObj.bx-imgObj.ex),Math.abs(imgObj.by-imgObj.ey));
break;
case 2: //circle
g2d.drawOval(Math.min(imgObj.bx,imgObj.ex),Math.min(imgObj.by,imgObj.ey),
Math.abs(imgObj.bx-imgObj.ex),Math.abs(imgObj.by-imgObj.ey));
break;
case 3: //text
g2d.setFont(new Font(imgObj.fontName, imgObj.fontStyle, imgObj.gSize));
g2d.drawString(imgObj.text, imgObj.bx, imgObj.by);
break;
case 4:
g2d.drawImage(imgObj.imgFile, imgObj.bx, imgObj.by,
imgObj.ex-imgObj.bx, imgObj.ey-imgObj.by, null); // paste img
break;
}
}
}
}
}
MouseListener_.java:295行代码
package mouseListener;
import java.awt.AWTException;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileNameExtensionFilter;
import repaint.Repaint;
import window.Outlook;
public class MouseListener_ implements MouseListener, MouseMotionListener, ActionListener {
{
System.setProperty("black", "0x000000");
System.setProperty("blue", "0x0000ff");
System.setProperty("red", "0xff0000");
System.setProperty("green", "0x008000");
}
private Outlook frame; // 获取窗口的一些属性
private Graphics2D g; // 画笔
private Color currentColor=Color.black, previousColor = null; // 画笔颜色
private int bx,by,ex,ey; // 起始终止位置
private Integer gSize=1; // 画笔粗细
private Integer textSize = 20; // 文字大小
private String shape="curve"; // 要画的形状
private File imgFile;
private String fontName="宋体"; // 字体名称
private int fontStyle=0; // 字体风格
private int usrChoose; // 保存打开文件时的用户选择
private JFileChooser fc;
// steady graphic, old position
private int ox, oy;
// repaint function flag variable
private boolean havePrint = false;
private Image img; // 图片文件
public MouseListener_(Outlook frame) {
this.frame = frame;
}
public void setG(Graphics g) {
this.g = (Graphics2D) g;
this.g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);// 设置画笔开启抗锯齿
this.g.setStroke(new BasicStroke(gSize));
}
@Override
public void actionPerformed(ActionEvent e) {
String whichItem=e.getActionCommand();
boolean isText; //更改画笔大小时要使用
switch(whichItem) {
case "black":
case "red":
case "green":
case "blue":
g.setColor(currentColor=Color.getColor(whichItem));
break;
case "eraser":
if (currentColor!=Color.white)
previousColor = currentColor;
g.setColor(currentColor = Color.white);
shape="eraser";
break;
// 设置画笔颜色
case "straight line":
case "curve":
case "rectangle":
case "circle":
case "text":
frame.gSizeDisplay.setText((whichItem.equals("text")?textSize:gSize)+"");
shape = whichItem;
g.setColor(previousColor==null?currentColor:(currentColor=previousColor));
break;
case "+":
isText = shape.equals("text");
frame.gSizeDisplay.setText(isText?(textSize+=2).toString(): (gSize+=1).toString());
if (!isText)
this.g.setStroke(new BasicStroke(gSize.intValue()));
break;
case "-":
isText = shape.equals("text");
if (isText && textSize.intValue()<22
|| !isText && gSize.intValue()<2)
break;
frame.gSizeDisplay.setText(isText?(textSize-=2).toString():(gSize-=1).toString());
if (!isText)
this.g.setStroke(new BasicStroke(gSize.intValue()));
break;
case "image":
fc = new JFileChooser();
fc.setFileFilter(new FileNameExtensionFilter("image(*.jpg, *.png, *.jpeg)", "jpg", "png", "jpeg"));
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
usrChoose = fc.showOpenDialog(null);
if (usrChoose==JFileChooser.OPEN_DIALOG) {
imgFile = fc.getSelectedFile();
try {
img = ImageIO.read(imgFile);
} catch (IOException e1) {
}
shape = "image";
} else img = null;
break;
case "save":
fc = new JFileChooser();
fc.setFileFilter(new FileNameExtensionFilter("image(*.jpg, *.png, *.jpeg)", "jpg", "png", "jpeg"));
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
usrChoose = fc.showSaveDialog(null);
if (usrChoose==JFileChooser.APPROVE_OPTION) {
imgFile = fc.getSelectedFile();
try {
BufferedImage imgSave =new Robot().createScreenCapture(
new Rectangle(frame.getX()+10,
frame.getY()+135,
frame.jpDrawZone.getWidth()-5,
frame.jpDrawZone.getHeight()-5));
ImageIO.write(imgSave, "jpg", imgFile);
} catch (IOException err) {
} catch (AWTException err) {
}
}
break;
case "undo":
if (!frame.imgStore.isEmpty()) {
frame.imgStore.remove(frame.imgStore.size()-1);
frame.paint(frame.g); //重绘
}
break;
case "clear": //不可撤销的操作
frame.imgStore.clear();
frame.repaint();
break;
case "宋体":
case "黑体":
case "微软雅黑":
fontName=whichItem;
break;
case "plain":
fontStyle=0;
break;
case "bold":
fontStyle=1;
break;
case "italic":
fontStyle=2;
break;
default: break;
}
}
@Override
public void mouseDragged(MouseEvent e) {
Dimension size = frame.getContentPane().getSize();
int curX=e.getX(), curY=e.getY(), tp=3;
if (curX>10 && curY>140 && curX<size.width && curY<size.height+25) {
switch(shape) {
case "eraser":
case "curve":
if (bx>10 && by>140 && bx<size.width && by<size.height+25) {
g.drawLine(bx, by, curX, curY);
frame.imgStore.add(new Repaint(bx, by, curX, curY, gSize, currentColor, 0));
}
bx=curX; by=curY;
break;
case "straight line": //0
--tp;
case "rectangle": //1
--tp;
case "circle": //2
--tp;
case "image": //3
steadyDraw(tp, curX, curY);
break;
default: break;
}
} else {
bx = curX;
by = curY;
}
}
private void steadyDraw(int tp, int curX, int curY) {
boolean tmp;
if (havePrint==false) {
tmp = (tp==3 ? frame.imgStore.add(new Repaint(bx, by, curX, curY, img)):
frame.imgStore.add(new Repaint(bx, by, curX, curY, gSize, currentColor, tp)));
havePrint = true;
} else {
frame.imgStore.remove(frame.imgStore.size()-1); //移除原来的对象
// 添加新位置的对象
tmp = (tp==3 ? frame.imgStore.add(new Repaint(bx, by, curX, curY, img)):
frame.imgStore.add(new Repaint(bx, by, curX, curY, gSize, currentColor, tp)));
}
frame.repaint();
}
@Override
public void mouseMoved(MouseEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
bx = e.getX();
by = e.getY();
ox=bx; oy=by;
// 获取线条粗细
String _gSize = frame.gSizeDisplay.getText();
boolean isText = shape.equals("text");
try {
int tmpSize = Integer.parseInt(_gSize);
if (isText && tmpSize>=20)
textSize = tmpSize;
else if (!isText && tmpSize>=1)
gSize=tmpSize;
} catch(NumberFormatException err) {
} finally {
frame.gSizeDisplay.setText(isText?textSize.toString():gSize.toString());
}
if(!isText) g.setStroke(new BasicStroke(gSize));
}
@Override
public void mouseReleased(MouseEvent e) {
ex = e.getX();
ey = e.getY();
switch(shape) {
case "straight line":
case "rectangle":
case "circle":
case "image":
havePrint = false;
break;
case "text":
g.setFont(new Font(fontName, fontStyle, textSize));
g.drawString(frame.inputText.getText(), bx, by);
frame.imgStore.add(new Repaint(bx, by, textSize, currentColor, fontName, fontStyle,
frame.inputText.getText()));
break;
default:break;
}
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
Repaint.java:61行代码
package repaint;
import java.awt.Color;
import java.awt.Image;
public class Repaint {
//"straight line"0("curve"0), "rectangle"1,"circle"2,"text"3, "image"4, "eraser"0
public int pattern; // 0->...
public int gSize;
//"black", "red", "green", "blue"
public Color gColor;
//宋体、微软雅黑、黑体
public String fontName;
//plain, italic, bold
public int fontStyle;
// image file
public Image imgFile;
// text
public String text;
public int bx, by, ex, ey;
// straight line, curve
public Repaint(int bx, int by, int ex, int ey, int gSize, Color gColor, int pattern) {
this.bx=bx;
this.by=by;
this.ex=ex;
this.ey=ey;
this.gSize=gSize;
this.gColor=gColor;
this.pattern=pattern;
}
// text
public Repaint(int bx, int by, int gSize, Color gColor, String fontName, int fontStyle, String text) {
this.bx=bx;
this.by=by;
this.gSize=gSize;
this.gColor=gColor;
this.fontName=fontName;
this.fontStyle=fontStyle;
this.text=text;
this.pattern=3;
}
// image
public Repaint(int bx, int by, int ex, int ey, Image imgFile) {
this.bx=bx;
this.by=by;
this.ex=ex;
this.ey=ey;
this.imgFile=imgFile;
this.pattern=4;
}
}
正常运行无任何报错,但是仍有许多不足之处,望批评指正。