Java-swing图形化——小小留言板

=====taoyyz小陶©版权所有=====

实验四 图形界面及应用系统的设计

要求:

  制作如图一个留言板的界面,并按要求加入所需控件,并能按要求进行窗口控件的布局。并按要求为按钮、文本框、窗口添加事件,使之实现提交显示留言,清屏,留言至顶和至尾。要求文本框能自动产生滚动条,界面美观。

注意:
1、通过两个文本文件存储表情和留言内容。
  1)Expression.dat:用于存储表情,如:微笑、大哭、流泪等;每个表情占一行。
  2)Msg.dat:用于存储留言信息,留言信息格式为: `[2019-10-04 12:35] 你微笑地说:今天下午去图书馆吗?` 其中:时间是提交留言的时间,每条留言一行。
2、窗口启动的时候(或点击“查看”按钮时),从 Msg.dat 文件中读出所有留言 记录,显示在文本框中;从 Expression.dat 文件中读出所有表情记录,显示在 表情下拉列表中。将 Msg.dat 中所有留言记录,倒序显示在文本域中。
3、点击提交按钮,将表单内容存入文件,同时刷新文本框的留言内容。
4、“清屏”代表清除留言框内容,“至顶”和“至尾”功能是当留言内容过多时, 将滚动条滚动到最上面或最下面,实现过程为移动文本区域里面的光标,使 其指向第一个位置和最后一个位置。(设置光标位置函数为:setCaretPosition(int 位置))
5、(选作)增加“表情维护”按钮,在新窗口修改表情文件的内容
6、(选作)增加“删除留言”按钮,在新窗口通过选择数字或全部删除相关的留言内容
7、时间安排:前 3 学时,做界面和基本的显示效果操作事件,后 3 学时将文件操作引入相关的事件中。

思路:

定义3个类,分别为留言板主界面类MessageBoard,表情维护类ExpressionManager,删除留言类DeleteMsg
重要方法:
  1.在多个方法之间产生交互的组件最好定义为全局成员变量
  2.显示窗口的2个必不可少的步骤:setSize()和setVisible(true)
  3.根据需要选择合适的Layout,最外层窗口选择BorderLayout最合适
  4.可以利用方法的可变参数列表来一次性初始化多个组件,很方便!
  5.新增、删除、修改留言之后要再次调用相应的load方法重新载入达到刷新效果
  6.其他类需要调用主类的方法时,可以把主类方法设置为静态static的
  7.JList类配合Collection集合类进行增删改
  8.把集合内容作为参数作为JList的数据,调用setListData()方法

代码:

MessageBoard类(含main方法):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
package com.experiment.exp4;
/*
小小留言板类,此类实现了一个简易的留言板,包括留言/展示、选择表情、清屏、滚动到顶部/底部、删除留言等功能
继承于JFrame类,为按钮增加了ActionListener监听器,利用ComponentListener监听窗口大小改变以实现标题居中
1.整体采用BorderLayout布局:
1.1 CENTER区域显示留言内容,并可以随时更新
1.2 EAST区域放置常用功能,例如清屏、至顶/尾、查看、表情维护、删除留言功能
1.3 SOUTH区域可以输入留言,并选择表情
2.在构造方法中指定窗口的标题、设置窗口大小、并初始化组件(加入窗口组件监听、限制窗口大小、设置窗口居中屏幕以及标题居中)
3.标题居中:通过FontMetrics类的stringWidth()方法得到当前字体所占用的宽度,来计算出需要在标题前补多少个空格
4.CENTER区域的JTextArea设置自动换行,并放入JScrollPane中,设置垂直滚动条
5.EAST区域的6个按钮放置在8行1列的GridLayout布局中,方便排列
6.SOUTH区域采用FlowLayout布局,并利用setPreferredSize设置面板大小
7.在actionPerformed()方法中分别对各个按钮的事件进行动作响应:
7.1 留言按钮除了会把输入框的内容追加到JTextArea中,还会调用saveMsg()方法保存到本地文本文件中
7.2 清屏按钮仅对JTextArea区域清空,不会影响本地文本文件中的内容
7.3 查看功能会调用loadMsg()再次载入本地文本文件中的留言内容,loadMsg()在WinInit()过程中会被初次调用
7.4 至顶:即调用setCaretPosition()并传入参数为0,把光标移至文本开头
7.5 至尾:即调用setCaretPosition()并传入参数为JTextArea实例的字符串长度,相当于把光标移至末尾
7.6 表情维护:调用manageExp()产生ExpressionManager对象,此对象会生成一个JFrame窗口,用于管理表情
7.7 删除留言:调用deleteMsg()产生DeleteMsg对象,此对象会生成一个JFrame窗口,用于删除留言
8.有个坑:双显示器场景下,窗口在副屏选中了JComboBox的某个内容,点击按钮(表情维护、删除留言)产生新JFrame对象时会导致那个按钮所处的面板下方花屏,猜测是面板没有刷新导致。仅在双显示器窗口位于副屏下重现。解决方法:调用repaint()方法重绘面板组件。
*/

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* @Author taoyyz(陶俊杰)
* @Date 2021/5/1 11:07
* @Version 1.0
*/
public class MessageBoard extends JFrame implements ComponentListener, ActionListener {
private static JTextArea textArea;
private static JComboBox<String> expressions;
private JTextField inputText;
private JButton submit;
private JButton clear;
private JButton toTop;
private JButton toTail;
private JButton viewAll;
private JButton expressionManager;
private JButton delete;
private final String title;
public static final String filePath = "C:\\Users\\SBTTG\\Desktop\\" +
"IDEA\\Demo\\src\\main\\java\\com\\experiment\\exp4";

public MessageBoard(String title) throws HeadlessException {
super(title);
this.title = title;
setSize(640, 480);
WinInit(title);
}

public static void main(String[] args) {
MessageBoard mb = new MessageBoard("小小留言板");
}

private void WinInit(String title) {
setLayout(new BorderLayout()); //设置整体JFrame布局为BorderLayout
setTitleCenter(title); //设置标题居中
addComponentListener(this); //加入组件监听器,主要是为了检测窗口大小变动
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //设置关闭按钮功能
setLocationRelativeTo(null); //设置窗口位置默认为屏幕中间
setMinimumSize(new Dimension(560, 420)); //设置窗口最小尺寸
//实例化组件
initTopPanel();
initRightPanel();
initBelowPanel();
initCenterPanel();
//下面的语句利用可变参数列表一次性设置按钮样式和监听器,我也太J8机智了
setButtonStyleAndListener(clear, toTop, toTail, viewAll, submit, expressionManager, delete);
setVisible(true); //设置可见性-->必须放在最后面,否则此语句后面的语句不生效,需要产生事件刷新屏幕才会生效
}

private void initCenterPanel() {
textArea = new JTextArea();
textArea.setText("留言内容:\n");
loadMsg();
textArea.setEditable(false);
textArea.setWrapStyleWord(true);
textArea.setLineWrap(true);
textArea.setFont(new Font("微软雅黑", Font.PLAIN, 14));
// textArea.append("呵呵哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈\n");
JScrollPane centerScrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
add(centerScrollPane, BorderLayout.CENTER);
}

private void initBelowPanel() {
JPanel below = new JPanel();
below.setLayout(new FlowLayout());
below.setPreferredSize(new Dimension(540, 40)); //BorderLayout下只能用这种方式修改面板大小
below.add(new JLabel("你"));
expressions = new JComboBox<>();
// expressions.addItem("微笑");
// expressions.addItem("开心");
loadExp();
below.add(expressions);
below.add(new JLabel("地说:"));
submit = new JButton("提交");
inputText = new JTextField(20);
below.add(inputText);
below.add(submit);
add(below, BorderLayout.SOUTH);
}

private void initTopPanel() {
JPanel top = new JPanel();
JLabel title = new JLabel("留言板");
title.setVerticalAlignment(SwingConstants.CENTER);
top.add(title);
add(top, BorderLayout.NORTH);
}

private void initRightPanel() {
JPanel right = new JPanel();
right.setLayout(new GridLayout(8, 1, 0, 25));
clear = new JButton("清屏");
toTop = new JButton("至顶");
toTail = new JButton("至尾");
viewAll = new JButton("查看");
expressionManager = new JButton("表情维护");
delete = new JButton("删除留言");
right.add(clear);
right.add(toTop);
right.add(toTail);
right.add(viewAll);
right.add(expressionManager);
right.add(delete);
add(right, BorderLayout.EAST);
}

private void setButtonStyleAndListener(JButton... buttons) {
for (JButton btn : buttons) {
btn.addActionListener(this); //加入按钮动作监听器
btn.setFocusPainted(false); //取消掉按钮点击后的虚线框
btn.setCursor(new Cursor(Cursor.HAND_CURSOR)); //设置光标悬停样式为小手
}
}

private void setTitleCenter(String title) {
setFont(new Font("System", Font.PLAIN, 14));
FontMetrics fm = this.getFontMetrics(this.getFont());
String spaces = String.format("%" + ((this.getWidth() - title.length()) / 2
/ fm.stringWidth(" ") - fm.stringWidth(title) / 4) + "s", "");
setTitle(spaces + title);
}

public static void loadMsg() {
textArea.setText("留言内容:");
try {
BufferedReader reader = new BufferedReader(new FileReader(filePath + "\\Msg.dat"));
String msg;
while ((msg = reader.readLine()) != null) {
textArea.insert("\n" + msg, "留言内容:".length());
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
}

public static void loadExp() {
expressions.removeAllItems();
try {
BufferedReader reader = new BufferedReader(new FileReader(filePath + "\\Expression.dat"));
String exp;
while ((exp = reader.readLine()) != null) {
expressions.addItem(exp);
}
reader.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}

private void saveMsg(String msg) {
try {
//注意这里的FileWriter构造方法应该调用带是否追加的,即第二个参数设定为true,否则默认覆盖
FileWriter fw = new FileWriter(MessageBoard.filePath + "\\Msg.dat", true);
fw.append(msg).append("\n");
fw.flush();
fw.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}

@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == submit) {
if (!(inputText.getText().equals(""))) {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd hh:mm");
String msg = "[" + sf.format(new Date()) + "]" + " 你"
+ expressions.getSelectedItem() + "地说:" + inputText.getText();
textArea.insert("\n" + msg, "留言内容:".length());
saveMsg(msg);
inputText.setText(null);
}
} else if (e.getSource() == clear) {
textArea.setText("留言内容:\n");
} else if (e.getSource() == toTop) {
textArea.setCaretPosition(0);
} else if (e.getSource() == toTail) {
System.out.println("文本长度:" + textArea.getText().length());
textArea.setCaretPosition(textArea.getText().length());
} else if (e.getSource() == viewAll) {
System.out.println("查看");
loadMsg();
} else if (e.getSource() == expressionManager) {
manageExp();
right.repaint(); //我擦?竟然有残影,利用repaint()刷新面板解决
} else if (e.getSource() == delete) {
deleteMsg();
right.repaint(); //同理
}
}

@Override
public void componentResized(ComponentEvent e) {
setTitleCenter(title);
}

@Override
public void componentMoved(ComponentEvent e) {

}

@Override
public void componentShown(ComponentEvent e) {

}

@Override
public void componentHidden(ComponentEvent e) {

}

private void manageExp() {
ExpressionManager em = new ExpressionManager("表情");
System.gc();
}

private void deleteMsg() {
DeleteMsg dm = new DeleteMsg("删除");
System.gc();
}
}

ExpressionManager类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.experiment.exp4;
/*
表情维护 继承于JFrame:
1.采用BorderLayout布局
1.1 CENTER部分放置一个可编辑的JTextArea(加入滚动条),用于操作修改表情文本
1.2 SOUTH部分放置2个按钮,分别用于确定和取消
2.首先在构造方法中设置窗口大小,调用初始化方法来初始化组件,设置可见性为可见
3.对于初始化方法,可以利用可变参数列表来适应参数个数,达到一次性设置按钮样式以及监听器
4.把CENTER区域的JTextArea放入JScrollPane中,设置产生垂直滚动条
5.在按钮动作监听事件中分别处理2个按钮的行为:
4.1 确认按钮:在按下此按钮之前,可以任意修改JTextArea区域的表情文本
当按下此按钮,调用saveExp()方法保存表情
要注意保存完毕后刷新面板,即调用loadExp()方法重新载入表情
4.2 取消按钮:直接调用dispose()方法关闭此Window,不影响其他JFrame
*/

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**
* @Author taoyyz(陶俊杰)
* @Date 2021/5/1 16:01
* @Version 1.0
*/
public class ExpressionManager extends JFrame implements ActionListener {
static JButton confirm;
static JButton cancel;
JTextArea textArea;
JPanel btnPanel;

public ExpressionManager(String title) throws HeadlessException {
super(title);
setSize(200, 250);
WinInit();
setVisible(true);
}

private void WinInit() {
setLayout(new BorderLayout());
setLocationRelativeTo(null);
setMinimumSize(new Dimension(230, 300));
initContent();
initBtnPanel();
setButtonAction(confirm, cancel);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}

private void setButtonAction(JButton... buttons) {
for (JButton button : buttons) {
button.addActionListener(this);
}
}

private void initContent() {
textArea = new JTextArea();
textArea.setFont(new Font("微软雅黑", Font.PLAIN, 16));
try {
BufferedReader reader = new BufferedReader(new FileReader(MessageBoard.filePath
+ "\\Expression.dat"));
String exp;
while ((exp = reader.readLine()) != null) {
textArea.append(exp + "\n");
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
JScrollPane centerScrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
add(centerScrollPane, BorderLayout.CENTER);
}

private void initBtnPanel() {
btnPanel = new JPanel();
btnPanel.setPreferredSize(new Dimension(180, 40));
confirm = new JButton("确认");
cancel = new JButton("取消");
btnPanel.add(confirm);
btnPanel.add(cancel);
add(btnPanel, BorderLayout.SOUTH);
}

@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == confirm) {
System.out.println("确定");
saveExp();
MessageBoard.loadExp(); //记得刷新主面板的表情列表
dispose();
} else if (e.getSource() == cancel) {
System.out.println("取消");
dispose();
}
}

private void saveExp() {
try {
FileWriter fw = new FileWriter(MessageBoard.filePath + "\\Expression.dat");
fw.write("");
fw.write(textArea.getText());
fw.flush();
fw.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}

DeleteMsg类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package com.experiment.exp4;
/*
删除留言 继承于JFrame:
1.采用BorderLayout布局:
1.1 CENTER部分放置一个JList,用于显示所有留言
1.2 SOUTH部分放置3个JButton,分别用于删除单项、删除全部、取消
2.首先在构造方法中设置窗口大小,调用初始化方法来初始化组件,设置可见性为可见
3.对于初始化方法,可以利用可变参数列表来适应参数个数,达到一次性设置按钮样式以及监听器
4.在按钮动作监听事件中分别处理3个按钮的行为:
4.1 取消按钮直接调用dispose()释放此Window,不影响其他JFrame,相当于关闭此窗口
4.2 删除按钮:如果选中了一个JList中的内容,那么调用delete()方法移除它
4.2.1 发现一个问题,直接调用JList实例的remove()方法并不会正确移除
但可以通过其他手段处理列表元素(例如通过集合中的remove和add等方法)
然后通过处理过后的集合作为参数,调用setListData(E[] listData)
也就是说,操作JList数据实际上是通过另一个Collection集合来进行增删改
把修改完成之后的Collection集合内容作为参数重新产生JList的数据
4.3 删除全部:同样的,也是通过Collection集合的clear()方法清空集合
然后把这个空集合作为参数,调用setListData()达到清空JList效果
*/

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

/**
* @Author taoyyz(陶俊杰)
* @Date 2021/5/1 17:16
* @Version 1.0
*/
public class DeleteMsg extends JFrame implements ActionListener {
JButton delete;
JButton deleteAll;
JButton cancel;
JPanel btnPanel;
JList<String> msgList;
ArrayList<String> messages;

public DeleteMsg(String title) throws HeadlessException {
super(title);
setSize(400, 300);
WinInit();
setVisible(true);
}

private void WinInit() {
setLocationRelativeTo(null);
setLayout(new BorderLayout());
msgList = new JList<>();
msgList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
loadList();
add(msgList, BorderLayout.CENTER);
btnPanel = new JPanel();
delete = new JButton("删除");
deleteAll = new JButton("删除全部");
cancel = new JButton("取消");
btnPanel.add(delete);
btnPanel.add(deleteAll);
btnPanel.add(cancel);
setButtonStyleAndListener(delete, deleteAll, cancel);
add(btnPanel, BorderLayout.SOUTH);
setMinimumSize(new Dimension(400, 300));
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}

private void setButtonStyleAndListener(JButton... buttons) {
for (JButton btn : buttons) {
btn.addActionListener(this); //加入按钮动作监听器
btn.setFocusPainted(false); //取消掉按钮点击后的虚线框
btn.setCursor(new Cursor(Cursor.HAND_CURSOR)); //设置光标悬停样式为小手
}
}

@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == cancel) {
System.out.println("取消");
dispose();
} else if (e.getSource() == delete) {
System.out.println("删除");
delete();
loadList();
} else if (e.getSource() == deleteAll) {
System.out.println("删除全部");
deleteAll();
loadList();
}
}

private void deleteAll() {
messages.clear();
updateMsgList();
}

private void delete() {
if (msgList.getSelectedIndex() != -1) {
messages.remove(msgList.getSelectedIndex());
updateMsgList();
}
}

private void updateMsgList() {
try {
FileWriter fw = new FileWriter(MessageBoard.filePath + "\\Msg.dat");
fw.write("");
for (String message : messages) {
fw.write(message + "\n");
}
fw.flush();
fw.close();
MessageBoard.loadMsg();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}

private void loadList() {
try {
BufferedReader reader = new BufferedReader(new FileReader(MessageBoard.filePath + "\\Msg.dat"));
messages = new ArrayList<>();
String msg;
while ((msg = reader.readLine()) != null) {
messages.add(msg);
}
String[] msgs = new String[messages.size()];
for (int i = 0; i < messages.size(); i++) {
msgs[i] = messages.get(i);
}
msgList.setListData(msgs);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}

运行截图:

main expression delete compare

心得:

  五一第一天,室友凌晨1点就在学习疯狂卷!睡到中午11点赶紧开始今天一天的学习,总的来说这次实验掌握了swing的很多方法,也踩了好几个坑,例如setVisible()位置不对,导致后初始化的组件显示不出来,这时候拖动窗口缩放才会显示,困扰了半天,原来只需要把setVisible()放在组件初始化语句后面就可以正常显示了。
  在JList的使用上也有很多坑,JList自带的remove()方法和removeAll()方法居然不能正常移除列表中的元素,而且还会报下标越界异常,真的🐔8坑。好在解决方法是:定义一个Collection集合作为JList的操作辅助,因为Collection集合刚好也带有remove()、clear()、add()这些方法,在Collection集合中操作内容之后,把每一个元素全部存入一个对象数组中,作为参数传入JList的setListData()方法重新设置JList内容就可以变相操作JList。(妈的为什么不能直接用JList的remove()等等方法啊,但是getSelectedIndex()又可以正常获取到点击的下标,害!)
  swing现在用的好像并不多,虽然是所谓的跨平台窗口库,但是没什么卵用。
  JavaWeb才是永远滴神,有浏览器就能用,为什么不学JavaWeb呢?
  耗时一天完成了这个swing小程序,终于脱离Console枯燥的文字了,图形化编程还是挺有成就感的🤩!