Java Swing
Java Swing 目前已经不怎么使用了。但是因为课程需要的原因,我们还是要来看一下其使用方法。本文章部分内容来源于柏码网站,戳我跳转。
FlatLaf
虽然现在桌面应用不怎么使用 Java Swing 开发,旦不代表 Java Swing 开发不出好看的界面。例如我们经常使用的 IDEA 就是 Swing 开发的,而 JetBrains 的开发人员也对 Swing 外观做了一系列封装,最终以 FlatLaf 的形式呈现到我们面前。
FlatLaf 为 Swing 的各种组件提供了现代化 UI,并且其样式风格和 JetBrains 系列 IDE 相似,帮助我们开发现代审美的桌面应用。
使用之前需要引入依赖:
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>3.4.1</version>
</dependency>
<!-- IDEA 主题 -->
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf-intellij-themes</artifactId>
<version>3.5.2</version>
</dependency>
在实际开发中,只需要指定使用对应的 UI 主题即可:
public class ChatApplication {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(new FlatIntelliJLaf());
} catch (Exception ex) {
ex.printStackTrace();
}
SwingUtilities.invokeLater(() -> {
// 页面代码
LoginFrame loginFrame = new LoginFrame();
loginFrame.setVisible(true);
});
}
}
当然,也可以对组件样式进行改动(详情见官方文档)。
AWT
在 Java 正式推出的时候,它还包含一个用于基本 GUI 程序设计的类库,名字叫 Abstract Window Toolkit,简称AWT,抽象窗口工具包,我们可以直接使用 Java 为我们提供的工具包来进行桌面应用程序的开发。只不过这套工具包依附于操作系统提供的 UI,具体样式会根据不同操作系统提供的界面元素进行展示。
基本框架
我们使用 Frame 来创建应用窗口:
public static void main(String[] args) {
Frame frame = new Frame("Awt Demo");
frame.setSize(300, 300);
frame.setVisible(true); // 显示窗口
}
我们可以通过 Frame 的各种方法来设置窗口的各项属性:
public static void main(String[] args) {
Frame frame = new Frame();
frame.setTitle("我是标题"); // 设置窗口标题
frame.setSize(500, 300); // 设置窗口大小
frame.setBackground(Color.BLACK); // 设置窗口背景颜色
frame.setResizable(false); // 设置窗口大小是否固定
frame.setAlwaysOnTop(true); // 设置窗口是否始终展示在最前面
frame.setVisible(true); // 注意,只有将可见性变为true时才会展示出这个窗口,否则窗口是隐藏的
}
实际上当我们创建一个窗口之后,会在其他线程中进行处理,包括窗口的绘制、窗口事件的监听等,所以说我们的主线程不会卡住。
实际上我们的程序打开都是默认居中显示的,所以说我们可以调整一下窗口的位置:
frame.setLocation(100, 200); // setLocation可以调整窗口位置
注意,这里的窗口位置以及窗口大小都是以像素为单位。整个屏幕有多少个像素,是根据电脑的显示器屏幕分辨率来决定的,比如我们的电脑显示器屏幕分辨率为 1920 x 1080,那么我们显示器就可以显示长为 1920个 像素,宽 1080 个像素的矩形,只要是在这个范围内的窗口,都可以显示到屏幕上。
如果现在我们希望将这个窗口居中,就需要手动调整位置,但我们是要去适配各种分辨率的显示器才可以,不然到其他分辨率下,就无法居中了,我们可以动态获取分辨率来进行位置计算:
public static void main(String[] args) {
Frame frame = new Frame("我是标题");
frame.setSize(500, 300);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); // 获取到屏幕尺寸
int x = (int) ((screenSize.getWidth() - frame.getWidth()) / 2); // 居中位置就是:屏幕尺寸/2 - 窗口尺寸/2
int y = (int) ((screenSize.getHeight() - frame.getHeight()) / 2);
frame.setLocation(x, y); // 位置设置好了之后再展示出来
frame.setVisible(true);
}
这样我们的窗口打开之后默认就是居中放置的了。
得益于 Java 已经为我们封装好了各种方法,所以说要实现什么功能直接调用对应的方法即可,比如我们想要个性化光标,我们可以使用 setCursor
方法来实现,JDK 已经为我们提供了一些预设的光标样式:
public static void main(String[] args) {
Frame frame = new Frame("Awt Demo");
frame.setSize(500, 300);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int x = (int) ((screenSize.getWidth() - frame.getWidth()) / 2);
int y = (int) ((screenSize.getHeight() - frame.getHeight()) / 2);
frame.setLocation(x, y);
// 设置光标样式
frame.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
frame.setVisible(true);
}
监听器
我们可以为窗口添加一系列的监听器,监听器会监听窗口中发生的一些事件,比如我们点击关闭窗口、移动鼠标、鼠标点击等,当发生对应的事件时,就会通知到对应的监听器进行处理,从而我们能够在发生对应事件时进行对应处理。
这里我们可以给一个接口实现,或是使用对应的适配器(适配器模式是设计模式中的一种写法,因为接口中要实现的方法太多,但是实际上我们并不需要实现那么多,只需要实现对应的即可,所以说就可以使用适配器)我们只需要重写对应的方法,当发生对应事件时就会自动调用我们已经实现好的方法:
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) { // windowClosing方法对应的就是窗口关闭事件
frame.dispose(); // 当我们点击X号关闭窗口时,就会自动执行此方法了
// 使用dispose方法来关闭当前窗口
}
@Override
public void windowClosed(WindowEvent e) { // 对应窗口已关闭事件
System.out.println("窗口已关闭!"); // 当窗口成功关闭后,会执行这里重写的内容
System.exit(0); // 窗口关闭后退出当前Java程序
}
});
我们可以来看看效果,现在我们点击 X 号关闭窗口就可以成功执行了,并且窗口关闭后我们的 Java 程序就结束了。当然,监听器可以添加多个,并不是只能有一个。
这里总结一下窗口常用的事件:
public interface WindowListener extends EventListener {
public void windowOpened(WindowEvent e); // 当窗口的可见性首次变成true时会被调用
public void windowClosing(WindowEvent e); // 当以后企图关闭窗口(也就是点击X号)时被调用
public void windowClosed(WindowEvent e); // 窗口被我们成功关闭之后被调用
public void windowIconified(WindowEvent e); // 窗口最小化时被调用
public void windowDeiconified(WindowEvent e); // 窗口从最小化状态变成普通状态时调用
public void windowActivated(WindowEvent e); // 当窗口变成活跃状态时被调用
public void windowDeactivated(WindowEvent e); // 当窗口变成不活跃时被调用
}
除了监听窗口相关的动作之外,我们也可以监听鼠标、键盘等操作的事件,比如键盘事件:
frame.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) { // 监听键盘输入事件,当我们在窗口中敲击键盘输入时会触发
System.out.print(e.getKeyChar()); // 可以通过KeyEvent对象来获取当前事件输入的对应字符
}
});
键盘事件甚至可以细致到键盘按键的几种状态:
public interface KeyListener extends EventListener {
public void keyTyped(KeyEvent e); // 当一个按键按下之后触发(感觉跟下面这个没啥区别)
public void keyPressed(KeyEvent e); // 当一个按键按下后触发(按下之后如果不松开会连续触发此事件)
public void keyReleased(KeyEvent e); // 当一个按键按下然后松开后触发
}
我们也可以监听鼠标相关的事件,比如当鼠标点击我们界面上某一个位置时,我们就可以获取一下:
frame.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) { // mouseClicked是监听鼠标点击事件
System.out.println("鼠标点击:"+e.getX()+","+e.getY());
}
});
这样,当我们点击窗口中的某个位置时,就可以获取对应的坐标并打印出来。
当然,我们也可以获取鼠标是使用哪个键点击的,我们的鼠标一般情况下有三个按键:
- BUTTON1:鼠标左键,也是我们用的最多的键。
- BUTTON2:鼠标中键,一般是鼠标滚轮,也是是可以点击的。
- BUTTON3:鼠标右键,右键一般就是辅助点按,展开各种选项等。
如果是游戏鼠标,也许能监听到一些其他的按键,这里我们就不测试了,我们来尝试监听一下:
frame.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("鼠标点击:"+e.getButton());
}
});
鼠标滚动事件也可以进行监听:
frame.addMouseWheelListener(new MouseAdapter() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
System.out.println(e.getScrollAmount()); //获取滚动数量
}
});
常用组件
组件实际上是 AWT 为我们预设好的一些可以直接使用的界面元素,比如按钮、文本框、标签等等,我们可以使用这些已经帮我们写好的组件来快速拼凑出一个好看且功能强大的程序,在开始学习组件之前,我们先将布局设定为 null
(因为默认情况下会采用 BorderLayout 作为布局)有关布局我们会在下一部分中进行介绍,这节课我们先介绍没有布局的情况下如何使用这些组件。
frame.setLayout(null);
标签组件
首先我们来介绍一下最简单的组件,标签组件相当于一个普通的文本内容,我们可以将自己的标签添加到窗口中:
Label label = new Label("我是标签"); // 添加标签只需要创建一个Label对象即可
label.setLocation(20, 50); // 注意,必须设定标签的位置和大小,否则无法展示出来
label.setSize(100, 20);
// label.setBounds(20, 50, 100, 20); // 也可以直接调用setBounds一步到位
frame.add(label); // 使用add方法添加组件到窗口中
注意,组件的位置是以整个窗口的左上角为原点开始的(整个窗口指的是包括标题栏在内)所以说我们如果想要设置组件的位置,我们还得注意加上标题栏的高度,否则会被标题栏遮挡。
我们可以自由修改文本的字体和大小(注意必须是操作系统已经安装的字体才支持展示):
// 直接构造并传入一个Font对象即可
label.setFont(new Font("SimSong", Font.BOLD, 15)); // Font构造方法需要字体名称、字体样式(加粗、斜体)、字体大小
可以使用来获取所有的系统字体,在使用的时候把 family
这一项填入到上述 Font 类的第一个参数即可:
GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()
当然,为了方便,如果我们的窗口中有很多的标签都想统一使用某一个字体,我们可以直接对窗口设定字体,那么只要是添加到窗口中的组件都会默认使用这个字体,除非单独指定组件字体。
要修改字体的颜色也很简单,我们可以使用:
label.setBackground(Color.BLACK); // setBackground依然是背景颜色,注意背景填充就是我们之前设定的大小
label.setForeground(Color.WHITE); // setForeground是设定字体颜色
按钮组件
按钮也是我们经常会使用的一个组件:
Button button = new Button("点击充值"); // Button是按钮组件
button.setBounds(20, 50, 100, 50);
frame.add(button);
这样就可以添加一个按钮到我们的窗口中了。
当然,对按钮添加一个监听器监听动作也是可以的:
button.addActionListener(e -> System.out.println("充值成功")); // addActionListener就是按钮点击监听器
当然,如果要修改按钮的字体或是颜色,依然使用之前的方式即可。
文本框组件
我们经常要在一些软件上登录,那么就要输入我们的用户名和密码,所以说文本框的作用还是非常明显的,我们也可以通过 AWT 组件来实现这些功能:
TextField field = new TextField();
field.setBounds(20, 50, 200, 25);
frame.add(field);
Button button = new Button("点击登录");
button.setBounds(20, 80, 100, 50);
// 点击按钮直接获取文本框中的文本内容,只需要调用getText方法即可
button.addActionListener(e -> System.out.println("输入的用户名是:"+field.getText()));
frame.add(button);
调用指定方法,可以把展示出来的字符替换掉,用于制作密码框:
field.setEchoChar('*'); // setEchoChar设定展示字符,无论我们输入的是什么,最终展示出来的都是我们指定的字符
文本域组件
TextArea textArea = new TextArea();
textArea.setBounds(20, 30, 100 ,20);
textArea.setEditable(true); // 是否可编辑
frame.add(textArea);
复选框组件
fCheckbox checkbox = new Checkbox("记住密码");
checkbox.setBounds(20, 50, 100, 30); // 这个大小并不是勾选框的大小,具体的勾选框大小要根据操作系统决定,跟Label一样,是展示的空间大小
frame.add(checkbox);
button.addActionListener(e -> System.out.println(checkbox.getState()));
单选框组件
CheckboxGroup group = new CheckboxGroup(); // 创建勾选框组
Checkbox c1 = new Checkbox("选我");
c1.setBounds(20, 50, 100, 30);
frame.add(c1);
Checkbox c2 = new Checkbox("你干嘛");
c2.setBounds(20, 80, 100, 30);
frame.add(c2);
c1.setCheckboxGroup(group); // 多个勾选框都可以添加到勾选框组中
c2.setCheckboxGroup(group);
布局
前面我们介绍了各种各样的组件,现在我们就可以利用这些组件来拼凑一个好看的程序了。
只不过,如果不使用布局,那么我们只能手动设置组件的位置以及大小,这就使得我们的程序在尺寸的设计上很有限,因为一旦窗口的大小发生变化,我们的组件依然是会放置在原本的位置上,要保证我们的设计不被破坏就只能固定窗口大小,但是很多应用都是支持放大和缩小的,并且在不同的大小下组件会自己调整位置。
为了让我们自己编写的程序中的组件也具有上述的自动调节位置的功能,我们可以使用布局来实现。
边界布局
默认情况下,我们的窗口采用的是边界布局(BorderLayout)这种布局方式支持将组件放置到五个区域:
frame.setLayout(new BorderLayout()); // 使用边界布局
frame.add(new Button("1号按钮"), BorderLayout.WEST); // 在添加组件时,可以在后面加入约束
frame.add(new Button("2号按钮"), BorderLayout.EAST);
frame.add(new Button("3号按钮"), BorderLayout.SOUTH);
frame.add(new Button("4号按钮"), BorderLayout.NORTH);
frame.add(new Button("5号按钮"), BorderLayout.CENTER);
边界布局的性质:
- BorderLayout 布局的容器某个位置的某个组件会直接充满整个区域。
- 如果在某个位置重复添加组件,只有最后一个添加的组件可见。
- 缺少某个位置的组件时,其他位置的组件会延伸到该位置。
我们还可以调整组件之间的间距:
BorderLayout borderLayout = new BorderLayout();
borderLayout.setHgap(50); // Hgap是横向间距
borderLayout.setVgap(50); // Vgap是纵向间距
调整之后,边距就非常明显了。
流式布局
FlowLayout 流式布局,流式布局实际上就是按顺序排列的一种布局:
frame.setLayout(new FlowLayout()); // 采用流式布局
frame.add(new Button("1号按钮"));
frame.add(new Button("2号按钮"));
frame.add(new Button("3号按钮"));
采用流式布局后,按钮会根据内容大小,自动调整为对应的大小,并且他们之间是有间距的。
我们也可以在设定流式布局时指定对齐模式,对齐方式会直接决定组件的排列方式:
frame.setLayout(new FlowLayout(FlowLayout.RIGHT)); // 指定为右对齐
我们同样可以使用 Hgap 和 Vgap 来调整组件之间的间距:
FlowLayout flowLayout = new FlowLayout();
flowLayout.setHgap(50);
flowLayout.setVgap(0);
卡片布局
卡片布局,CardLayout 对象将卡片作为一个容器中的每个组件。
我们可以添加多个组件:
CardLayout layout = new CardLayout();
frame.setLayout(layout);
frame.add(new Label("我是1号"));
frame.add(new Label("我是2号"));
frame.setVisible(true);
while (true) {
Thread.sleep(3000);
layout.next(frame); //我们需要使用CardLayout对象来进行切换
}
这里我们每三秒钟切换一次卡片,可以看到我们添加的标签每三秒就会变化一次,实际上我们可以利用卡片布局来做一个类似跑马灯的效果。
网格布局
GridLayout 以矩形网格的形式对组件进行管理:
frame.setLayout(new GridLayout());
for (int i = 0; i < 10; i++) {
frame.add(new Button(i + "号按钮"));
}
这种布局就很好理解了,默认情况下会生成一行按格子划分的相等区域。我们也可以手动指定行数和列数:
GridLayout gridLayout = new GridLayout();
gridLayout.setRows(2);
frame.setLayout(gridLayout);
for (int i = 0; i < 10; i++) {
frame.add(new Button(i + "号按钮"));
}
网格包布局
最后一种布局是 GridBagLayout,是最灵活的布局管理器,它同样是按照网格进行划分,但是一个组件可以同时占据多个网格。这种情况其实也是经常会出现的,比如计算器上的按钮虽然看起来也是按照网格排列的,但是有些按钮同时占据了横向或是纵向的两个网格,这种情况使用 GridBagLayout 布局就可以很好的处理。
虽然这个布局很强大,但是用起来也是很麻烦的,所以说这里就不做讲解了。
面板
基础面板
虽然认识了这么多的布局,但是我们发现,很多应用程序并不只是由单一的布局组成的,而是多种布局相互嵌套的结果,比如我们的IDEA界面,就不仅仅是一个布局完成的(这里只是举个例子)而是多种布局在嵌套使用:
但是只有我们的窗口才能设置布局,总不可能让多个窗口拼接在一起吧?实际上除了窗口可以作为容器之外,我们也可以使用其他的容器,这时,我们就需要用到面板。
类面板是最简单的容器类,它跟窗口一样,可以提供一个空间,同样可以随意添加组件到面板中,只不过面板本身也是一个组件,所以说面板是可以放到其他容器中的容器。
既然面板本身也是容器,所以说也可以单独设置面板内部的布局,比如现在我们想要分两个区域,上半部分区域是流式布局,下半部分区域采用网格布局,那么我们就可以先将窗口采用网格布局,并在上下各添加一个面板:
GridLayout layout = new GridLayout(); // 先设置整个窗口的布局
layout.setRows(2); // 设置行数为2,一会就会分成两行了
frame.setLayout(layout);
Panel top = new Panel(); // 接着我们创建一下上半部分的面板和下半部分的面板
top.setBackground(Color.PINK); // 添加一个背景颜色方便区分
frame.add(top);
Panel bottom = new Panel();
bottom.setBackground(Color.ORANGE);
frame.add(bottom);
这样,我们的两个面板就按照网格布局,被分成了上下两部分。
接着我们就可以分别在上半部分的面板和下半部分的面板中进行单独配置了:
Panel top = new Panel();
top.setBackground(Color.PINK);
top.setLayout(new FlowLayout()); // 面板默认会采用FlowLayout,所以说这里指不指定都一样
for (int i = 0; i < 5; i++) { // 面板就像窗口一样,可以设定布局和添加组件
top.add(new Button("流式" + i));
}
frame.add(top);
Panel bottom = new Panel();
bottom.setBackground(Color.ORANGE);
bottom.setLayout(new GridLayout()); // 下半部分我们采用网格布局
for (int i = 0; i < 5; i++) {
bottom.add(new Button("网格" + i));
}
frame.add(bottom);
这里我们将上半部分面板设定为流式布局,下半部分面板设定为网格布局。利用面板,我们就可以实现各种布局的自由组合,当然,面板在后面还会有更多的用处。
滚动面板
有些时候,我们的窗口大小可能并不能完全显示内部的内容,比如出现了一张很大的图片。
此时就会出现滚动条来让我们进行拖拽,这样就可以向下滑动查看没有完全展示出来的内容了。而我们之前开发的程序都没办法做到这样的滚动,超出部分会直接无法显示。
AWT 也为我们提供了滚动面板组件,滚动面板也是一个容器,但是我们无法修改它的布局,它只能容纳单个组件,比如展示一个图片、或者是列表等,我们也可以将其与 Panel 配合使用,比如:
ScrollPane scrollPane = new ScrollPane(); // 创建滚动面板
frame.add(scrollPane);
GridLayout layout = new GridLayout(); // 创建滚动面板内部的要展示的面板
layout.setRows(20);
Panel panel = new Panel();
panel.setLayout(layout);
for (int i = 0; i < 20; i++) {
panel.add(new Button("我是按钮" + i)); // 为面板添加按钮
}
scrollPane.add(panel);
可以看到,无法显示的部分会自动变成滚动面板,我们滑动就可以展示了。
这里需要特别提一下,我们看到这里的按钮大小采用的是自动生成的大小,但是如果我们希望按钮的大小按照我们喜欢的来怎么办呢?我们知道,使用布局之后,组件的大小实际上是自动决定的,只有未使用布局的情况下才能自由更改组件大小,那么我们怎么才能干预呢?我们可以为组件设定一个建议的大小:
for (int i = 0; i < 20; i++) {
Button button = new Button("我是按钮" + i);
button.setPreferredSize(new Dimension(100, 50)); // 设置首选大小
panel.add(button);
}
当布局管理器在自动调整内部组件大小时,如果不是必须要按照布局大小来展示或者是高度或宽度不确定,那么就会采用我们建议的大小展示,比如这里只能确定宽度,而高度是不确定的,那么就可以使用我们建议的大小来展示。
其他组件
列表
实际上滚动面板的最佳搭档就是 List 列表(注意这里的列表不是我们集合类里面学习的列表,而是展示出来的列表组件):
List list = new List(); // 注意是awt包下的List,别导错了
list.add("小糍粑");
list.add("锅巴洋芋");
list.add("手抓饼");
list.add("凉面");
list.setMultipleMode(true); // 是否开启多选模式
列表会将元素依次展示出来,我们可以选择列表中的某一项:
list.addItemListener(System.out::println);
列表可以添加监听器,当我们选择某个物品时,就会自动触发。
菜单栏
菜单栏由菜单栏和菜单项组成:
MenuBar bar = new MenuBar(); // 创建菜单栏
Menu menu = new Menu("我是1号菜单");
menu.add(new MenuItem("测试1"));
menu.add(new MenuItem("测试2"));
bar.add(menu);
frame.setMenuBar(bar); // 为窗口设定刚刚定义好的菜单栏
我们着重来看一下 MenuItem,这是我们菜单的每一个选项,我们可以为其添加监听器来监听用户是否点击:
MenuItem item = new MenuItem("测试1");
item.addActionListener(e -> System.out.println("一号选项被点击了!"));
menu.add(item);
我们还可以为菜单中的选项设定快捷键:
MenuItem item = new MenuItem("测试1");
item.setShortcut(new MenuShortcut('A')); // MenuShortcut就是指定快捷键组合,默认情况下是Ctrl+指定按键
// item.setShortcut(new MenuShortcut('A', true)); // 第二个参数指定为true表示需要Ctrl+Shift+指定按键
当然,除了这种普通的菜单选项之外,还有可以勾选的:
menu.add(new CheckboxMenuItem("测试2"));
CheckboxMenuItem 是可以勾选的选项,它能够对状态进行记录,我们点击选项之后会变成勾选状态。
弹出菜单
弹出一个浮在窗口之上的,并且可以进行选择的菜单,这个就是弹出菜单。
比如我们想要实现右键窗口任意位置都弹出菜单:
PopupMenu menu = new PopupMenu(); // 创建弹出菜单
menu.add(new MenuItem("选项1")); // 每一个选项依然是使用MenuItem
menu.add(new MenuItem("选项2"));
frame.add(menu); // 注意,弹出菜单也要作为组件加入到窗口中(但是默认情况下不显示)
frame.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON3) { // 监听鼠标右键
menu.show(frame, e.getX(), e.getY()); // 要展示弹出菜单,我们只需要调用show方法即可
// 注意,第一个参数必须是弹出菜单所加入的窗口或是窗口中的任意一个组件
// 后面的坐标就是相对于这个窗口或是组件的原点(左上角)这个位置进行弹出
// 我们这里写的就是相对于当前窗口的左上角,鼠标点击位置的x、y位置弹出窗口
}
}
});
对话框
有些时候,我们点击关闭按钮之后,窗口并不会直接关闭,而是会弹出一个对话框询问我们是否要退出。我们也可以使用 AWT 为我们提供的对话框,比如我们现在希望在关闭窗口时询问我们是否真的要关闭:
Dialog dialog = new Dialog(frame, "我是对话框", true);
// 第一个参数是父窗口或是父对话框(没错,对话框也可以由对话框唤起)
// 最后一个参数是当对话框展示时,是否让父窗口(对话框)无法点击
dialog.setSize(200, 80);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
dialog.setVisible(true);
}
});
只不过就单单是这样的一个对话框太过单调了,我们可以为其添加一些按钮之类的东西:
Dialog dialog = new Dialog(frame, "我是对话框", true);
dialog.setResizable(false);
dialog.add(new Label("确定是否要退出程序?"), BorderLayout.NORTH); // 对话框默认采用的是边界布局
dialog.add(new Button("取消"), BorderLayout.WEST);
dialog.add(new Button("不退出"), BorderLayout.EAST);
dialog.setSize(200, 80);
这样我们退出时,就有对应的提示了。
有些时候我们在使用应用程序的时候,可能需要我们去选择电脑上的一些文件,这个时候我们就可以使用文件对话框:
FileDialog dialog = new FileDialog(frame, "请选择一个文件", FileDialog.LOAD); // 选择文件对话框类型,可以是加载文件或是保存文件
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
dialog.setVisible(true);
System.out.println("选择的文件为:"+dialog.getDirectory() + dialog.getFile());
}
});
自定义组件
除了使用官方提供的这些组件之外,我们也可以自己创建一些组件来使用,比如官方没有提供图片组件,我们可以自己编写一个图片组件用于在窗口中展示我们的图片。
要自己编写一个组件,需要完成下面的步骤:
- 必须继承自 Component 类,表示这是一个 AWT 组件。
- 需要自己实现
paint
方法,这个方法就是组件的绘制方法,最终绘制出来的结果就是展示出来的结果了。
我们这里要实现的时绘制一个图片,那么我们就可以像这样编写了:
public class ImageView extends Component {
private final Image image;
public ImageView(String filename) throws IOException {
File file = new File(filename);
image = ImageIO.read(file); // 我们可以使用ImageIO类来快速将图片文件读取为Image对象
}
@Override
public void paint(Graphics g) {
// 绘制图片需要提供Image对象
g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
}
}
当然,现在我们讲了如何加载图片,顺便把设定自定义的程序图标介绍一下吧:
Image image = ImageIO.read(new File("test.png"));
frame.setIconImage(image);
注意,在 MacOS 下这样写没用,得用专用的增强包:
Image image = ImageIO.read(new File("test.png"));
Application.getApplication().setDockIconImage(image);
这样,我们的程序就会显示为我们自己定义的图标了。
窗口修饰和自定义形状
实际上我的窗口在默认情况下都是处于修饰状态,那么什么是修饰状态呢?窗口修饰实际上就是我们窗口外面添加的边框。
有些时候,可能我们并不需要系统为我们提供的窗口边框,我们希望能够自己编写窗口的边框,包括各种按钮等,此时我们就可以将窗口设定为非修饰状态:
public static void main(String[] args) throws IOException {
Frame frame = new Frame("我是窗口");
frame.setUndecorated(true); // 将窗口设定为非修饰状态
frame.setSize(200, 200);
frame.setVisible(true);
}
非修饰状态下,就只有一个窗口本身了。
并且这个窗口是无法完成拖拽操作的,要实现拖拽还得我们自己编写(太原始了)可以看到,在默认情况下窗口的形状是一个方形的,我们可以将其调整为其他形状:
public static void main(String[] args) throws IOException {
Frame frame = new Frame("我是窗口");
frame.setUndecorated(true);
frame.setSize(200, 200);
// 注意,只有窗口在非修饰状态下才能设定形状
// 这里我们使用圆角矩形,形状最好跟窗口大小一样
frame.setShape(new RoundRectangle2D.Double(0, 0, 200, 200, 20, 20));
frame.setVisible(true);
}
我们也可以自行为窗口添加标题栏,同样只需要重写一下 paint
方法自行绘制就可以了:
Frame frame = new Frame("我是窗口") { // 使用匿名内部类(或者自己写个子类也行)
@Override
public void paint(Graphics g) {
g.setColor(Color.LIGHT_GRAY);
g.fillRect(0, 0, getWidth(), 28); // 先绘制标题栏
g.setColor(Color.BLACK);
g.drawString(getTitle(), getWidth() / 2, 20); // 绘制标题名称
super.paint(g); // 原本的绘制别覆盖了,该怎么做还要怎么做
}
};
只不过这个窗口还不能拖动,我们来实现一下按住标题栏就可以拖动:
frame.addMouseMotionListener(new MouseMotionAdapter() { // 只需要写一个监听器就可以搞定了
int oldX, oldY;
public void mouseDragged(MouseEvent e) {
if (e.getY() <= 28) { // 鼠标拖动时如果是标题栏,就将窗口位置修改
frame.setLocation(e.getXOnScreen() - oldX, e.getYOnScreen() - oldY);
}
}
public void mouseMoved(MouseEvent e) { // 记录上一次的鼠标位置
oldX = e.getX();
oldY = e.getY();
}
});
至此,有关 AWT 相关的内容,我们就讲解到这里。只不过很遗憾,Java 官方并没有再对 AWT 相关内容进行维护,因为 AWT 采用的是取不同操作系统交集策略,因为有些功能只有部分操作系统才有,这就导致很多功能都被砍掉,维护起来也很困难。下节课开始,我们会继续介绍 Swing 相关组件。
Swing
Swing 是在 AWT 的基础上构建的一套新的图形界面系统,它提供了 AWT 所能够提供的所有功能,并且用纯粹的 Java 代码对 AWT 的功能进行了大幅度的扩充。例如说并不是所有的操作系统都提供了对树形控件的支持, Swing 利用了AWT 中所提供的基本作图方法对树形控件进行模拟。由于 Swing 控件是用100% 的 Java 代码来实现的,因此在一个平台上设计的树形控件可以在其他平台上使用。由于在 Swing 中没有使用本地方法来实现图形功能,我们通常把 Swing 控件称为轻量级控件。
其实简单来说,Swing 就是 AWT 的扩展,或者说是强化版,很多东西还是沿用的 AWT 中的。
组件层次
- 顶层容器:JFrame、JApplet、JDialog 和 JWindow。
- 中间容器:JPanel、JScrollPane、JSplitPane、JToolBar 等。
- 特殊容器:在用户界面上具有特殊作用的中间容器,如 JIntemalFrame、JRootPane、JLayeredPane 和 JDestopPane 等。
- 基本组件:实现人机交互的组件,如 JButton、JComboBox、JList、JMenu、JSlider 等。
- 不可编辑信息的显示组件:向用户显示不可编辑信息的组件,如 JLabel、JProgressBar 和 JToolTip 等。
- 可编辑信息的显示组件:向用户显示能被编辑的格式化信息的组件,如 JTable、JTextArea 和 JTextField 等。
- 特殊对话框组件:可以直接产生特殊对话框的组件,如 JColorChooser 和 JFileChooser 等。
快速上手
我们来看看如何使用 Swing 编写桌面程序,首先还是最重要的窗口(JFrame 内部实际上单独维护了一个面板来存放组件,很多操作都被重定向给了内部的面板):
public static void main(String[] args) {
JFrame frame = new JFrame("我是窗口"); // Swing中的窗口叫做JFrame,对应的就是AWT中的Frame
// 它实际上就是Frame的子类,所以说我们之前怎么用的,现在怎么用就行了
frame.setSize(500, 300);
frame.setVisible(true);
// 关闭时自动停止主线程
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
其他的组件基本上都是直接在 AWT 的组件基础上进行强化,命名方面就是在前面加一个字母 J。其余使用方法基本相同。
新增组件
工具提示
工具提示就是当我们鼠标移动到某个组件上时,会给出一个漂浮提示,告诉我们这个组件是干嘛用的:
JButton button = new JButton("我是按钮");
button.setBounds(50, 50, 100, 30);
button.setToolTipText("这个按钮是用来点击的!");
setToolTipText
方法是 JComponent
就带有的,因此任何组件都可以设置这样的工具提示。
进度条组件
很多时候我们都会用到进度条来展示某些任务的完成进度:
JProgressBar bar = new JProgressBar();
bar.setMaximum(100); //设定进度条的最大值
bar.setValue(50); //设定进度值
bar.setBounds(20, 50, 100, 10);
我们可以利用进度条来写一个很简单的案例,比如文件的拷贝:
JProgressBar bar = new JProgressBar(); // 进度条显示文件拷贝进度
bar.setMaximum(1000);
bar.setBounds(20, 50, 300, 10);
JButton button = new JButton("点击开始"); // 点击按钮开始拷贝文件
button.setBounds(20, 100, 100, 30);
button.addActionListener(e -> new Thread(() -> {
// 注意,不能直接在这个线程里面处理,因为这个线程是负责图形界面的,得单独创建一个线程处理,否则图形界面会卡死
File file = new File("in");
try(FileInputStream in = new FileInputStream(file);
FileOutputStream out = new FileOutputStream("out")) {
long size = file.length(), current = 0;
int len;
byte[] bytes = new byte[1024];
while ((len = in.read(bytes)) > 0) {
current += len;
bar.setValue((int) (bar.getMaximum() * (double)current / size)); // 每次拷贝都更新进度条
bar.repaint(); // 因为并不是每次更新值都会使得组件重新绘制,如果视觉上比较卡,可以每次拷贝都重新绘制组件
out.write(bytes, 0, len);
}
} catch (IOException exception) {
exception.printStackTrace();
}
}).start());
这样,我们在拷贝文件的时候,就有一个进度条实时显示当前的进度了。
文件树组件
我们的文件实际上在硬盘上就是以树形存储的,而Swing也为我们提供了能够显示树形关系的组件:
JTree tree = new JTree();
tree.setBounds(0, 0, 200, 200);
这样,我们就可以用它来做一个文件资源管理器了:
private static JTree getFileTree(String path) {
File file = new File(path);
DefaultMutableTreeNode root = new DefaultMutableTreeNode(file.getName());
// 递归方法,遍历所有文件和文件夹
addFilesToTree(root, file);
return new JTree(root);
}
// 递归添加文件夹和文件到树
private static void addFilesToTree(DefaultMutableTreeNode parentNode, File file) {
// 获取文件夹下的所有文件和子文件夹
File[] files = Optional.ofNullable(file.listFiles()).orElse(new File[0]);
for (File f : files) {
DefaultMutableTreeNode node = new DefaultMutableTreeNode(f.getName());
parentNode.add(node);
// 如果是文件夹,则递归处理
if (f.isDirectory()) {
addFilesToTree(node, f);
}
}
}
开关组件
看下一个组件,开关按钮:
JToggleButton button = new JToggleButton("我是切换按钮"); // 开关按钮有两个状态,一个是开一个是关
button.setBounds(20, 50, 100, 30);
工具条组件
Swing 提供了 JToolBar 来创建工具条,并且可以往 JToolBar 中添加多个工具按钮:
JToolBar toolBar = new JToolBar("工具条", SwingConstants.HORIZONTAL); // 横向摆放
toolBar.setFloatable(true); // 让工具条可以拖动
// 以Action的形式添加选项
Action pre = new AbstractAction("工具", new ImageIcon("路径")) {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("工具被点击");
}
};
JButton button = new JButton(pre);
toolBar.add(button);
toolBar.addSeparator(); // 添加分割线
表格组件
// 存储标题
Object[] titles = { "姓名", "年龄", "性别" };
// 创建二维数组,存储数据
Object[][] data = {
{ "小明", 18, "男" },
{ "小红", 19, "女" },
};
JTable table = new JTable(data, titles);
frame.add(new JScrollPane(table)); // 放到JScrollPane中,支持滚动
当然,可以通过监听器等获取选中的行和列索引:
JButton button = new JButton("获取数据");
button.addActionListener(e -> {
int selectedColumn = table.getSelectedColumn();
int selectedRow = table.getSelectedRow();
System.out.println("当前选中: " + selectedColumn + ", " + selectedRow);
});
颜色选择器
JColorChooser chooser = new JColorChooser();
chooser.setBounds(0, 0, 600, 300);
文件选择器
JFileChooser chooser = new JFileChooser();
chooser.setBounds(0, 0, frame.getWidth(), frame.getHeight());
标签页
标签页顾名思义,就是为了在一个窗口中展示多个面板,但是面板是可以自由切换的,在顶部会有一个小小的标签,我们点击之后就可以切换到对应的面板了:
JTabbedPane pane = new JTabbedPane();
pane.setBounds(0, 0, 500, 300);
pane.addTab("一号", new JPanel(){{setBackground(Color.ORANGE);}});
pane.addTab("二号", new JPanel(){{setBackground(Color.PINK);}});
JTabbedPane 跟我们之前认识的 Panel 很像,相当于也是将我们的组件装进了内部,但是它可以同时装很多个,并且支持自由切换,所以说是很高级的。
分割线
分割线用于分割两侧面板,并且支持拖拽:
JSplitPane pane = new JSplitPane();
pane.setOrientation(JSplitPane.HORIZONTAL_SPLIT); // 设定为横向分割
// 横向分割之后,我们需要指定左右两边的组件
pane.setLeftComponent(new JPanel(){{setBackground(Color.ORANGE);}});
pane.setRightComponent(new JPanel(){{setBackground(Color.PINK);}});
选项窗口
前面我们介绍过对话框,但是 AWT 提供的对话框太过原始,很多功能都需要我们自行实现,而Swing为我们提供了一套已经实现好的预设选项对话框,我们只需要直接使用即可。
JFrame frame = new JFrame("我是窗口");
frame.setSize(500, 300);
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); // 先将默认关闭行为设定为什么都不做
frame.addWindowListener(new WindowAdapter() { // 我们自己来实现窗口关闭行为
@Override
public void windowClosing(WindowEvent e) { // 这里我们可以直接展示一个预设好的确认对话框
int value = JOptionPane.showConfirmDialog(frame, "你真的要退出吗?");
if (value == JOptionPane.OK_OPTION) // 返回值就是用户的选择结果,也是预置好的,这里判断如果是OK那么就退出
System.exit(0);
}
});
我们之前要实现这样的一个功能,非常麻烦,但是现在就很简单了。
官方已经给我们预设好了一个对话框,我们直接用就可以了。当然,还有各种类型的,我们可以自己定义窗口的标题、图标等:
JOptionPane.showConfirmDialog(frame, "你真的要退出吗?", "退出程序", JOptionPane.YES_NO_OPTION);
除了这种简单的对话框,Swing 还为我们提供了一些其他类型的对话框,比如单纯的消息提示框:
JOptionPane.showMessageDialog(frame, "我是简单的提示消息!");
还有用户输入文本的输入对话框:
JFrame frame = new JFrame("我是窗口");
frame.setSize(500, 300);
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
String str = JOptionPane.showInputDialog("毕业后的你,将何去何从呢?");
System.out.println(str);
}
});
文件查看器开发
结合上述的文件树组件、文本框组件、分割线以及面板组件,我们可以开发一个简易版的文件查看器:
public class FileLooker {
public static void main(String[] args) {
// 皮肤
try {
UIManager.setLookAndFeel(new FlatDarkFlatIJTheme());
// 使用 IntelliJ 的主题图标
UIManager.put("Tree.icons", new Icon[] {
UIManager.getIcon("FileView.directoryIcon"),
UIManager.getIcon("FileView.fileIcon")
});
} catch (UnsupportedLookAndFeelException e) {
throw new RuntimeException(e);
}
// 窗口
JFrame frame = new JFrame("文件查看器");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1400, 880);
// 窗口居中
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int x = (int) ((screenSize.getWidth() - frame.getWidth()) / 2);
int y = (int) ((screenSize.getHeight() - frame.getHeight()) / 2);
frame.setLocation(x, y);
// 分割面板
JSplitPane splitPane = new JSplitPane();
splitPane.setOrientation(JSplitPane.HORIZONTAL_SPLIT); // 横向分割
splitPane.setDividerLocation(245);
// 指定左右两边组件
JTextArea area = new JTextArea();
area.setEditable(false);
area.setFont(new Font("Monospaced", Font.PLAIN, 15));
splitPane.setLeftComponent(new JScrollPane(getFileTree("C:\\Users\\40200\\Desktop\\temp", area)));
splitPane.setRightComponent(new JScrollPane(area));
// 文件树
frame.add(splitPane);
frame.setVisible(true);
}
// 根据文件路径获取文件树
private static JTree getFileTree(String path, JTextArea area) {
File file = new File(path);
DefaultMutableTreeNode root = new DefaultMutableTreeNode(file.getName());
// 递归方法,遍历所有文件和文件夹
addFilesToTree(root, file);
JTree jTree = new JTree(root);
jTree.addTreeSelectionListener(e -> {
// 拼接路径
Object[] paths = e.getPath().getPath(); // 当前选中文件在path下的子路径
StringBuilder finalPath = new StringBuilder(path);
for (int i = 1; i < paths.length; i++) {
finalPath.append("\\").append(paths[i]);
}
String finalPathString = finalPath.toString();
if (finalPathString.contains(".")) {
area.setText("");
try (FileReader reader = new FileReader(finalPathString)) {
char[] chars = new char[128];
int len;
while ((len = reader.read(chars)) > 0) {
area.setText(area.getText() + new String(chars, 0, len));
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
jTree.setCellRenderer(new FileTreeCellRenderer());
jTree.setFont(new Font("Monospaced", Font.PLAIN, 15));
return jTree;
}
// 递归添加文件夹和文件到树
private static void addFilesToTree(DefaultMutableTreeNode parentNode, File file) {
// 获取文件夹下的所有文件和子文件夹
File[] files = Optional.ofNullable(file.listFiles()).orElse(new File[0]);
for (File f : files) {
DefaultMutableTreeNode node = new DefaultMutableTreeNode(f.getName());
parentNode.add(node);
// 如果是文件夹,则递归处理
if (f.isDirectory()) {
addFilesToTree(node, f);
}
}
}
}
FileTreeCellRenderer 用于渲染文件树 Icon(需要自己重写):
public class FileTreeCellRenderer extends DefaultTreeCellRenderer {
private final Icon directoryIcon = UIManager.getIcon("FileView.directoryIcon");
private final Icon fileIcon = UIManager.getIcon("FileView.fileIcon");
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
File nodeFile = new File(value.toString());
Icon icon = !nodeFile.toString().contains(".") ? directoryIcon : fileIcon;
setIcon(icon);
return this;
}
}