image-20221026154454258

GUI程序开发

注意:开始学习之前请确保你完成了《Java SE》篇视频教程。

前面我们已经完成了JavaSE部分的全部内容学习,只不过我们在初学阶段一直都是开发的控制台程序,也就是最原始的命令窗口形式的程序,而Java也可以开发桌面图形化程序,所以我们接着来学习一下Java的图形化界面的开发。

AWT组件介绍

在Java正式推出的时候,它还包含一个用于基本GUI程序设计的类库,名字叫 Abstract Window Toolkit,简称AWT,抽象窗口工具包,我们可以直接使用Java为我们提供的工具包来进行桌面应用程序的开发。只不过这套工具包依附于操作系统提供的UI,具体样式会根据不同操作系统提供的界面元素进行展示。

实际上我们现代操作系统都是图形化界面,应用程序都是以一个窗口的形式展示出来的,我们可以直接使用鼠标点击窗口内的元素来使用应用程序,相比传统的命令行形式,可方便太多了,比如在Windows和MacOS这两种操作系统下:

image-20221026164200924

可以看到,不同的操作系统的窗口样式稍微有一些不一样,但是大致的使用方式是差不多的,我们接着来看一下如何使用Java编写简单的桌面图形化程序。

基本框架

既然我们要编写一个桌面程序,那么肯定是需要窗口来展示我们程序的内容的,所以说,我们可以使用AWT为我们提供的组件来创建窗口:

public static void main(String[] args) {
    Frame frame = new Frame();   //Frame是窗体,我们只需要创建这样一个对象就可以了,这样就会直接创建一个新的窗口
    frame.setSize(500, 300);   //可以使用setSize方法设定窗体大小
    frame.setVisible(true);    //默认情况下窗体是不可见的,我们如果要展示出来,还需要设置窗体可见性
}

可以看到,桌面的左上角已经展示出我们的窗口了:

image-20221026165600076

在不同的操作系统下,窗口的样式会不同。

我们可以通过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个像素的矩形,只要是在这个范围内的窗口,都可以显示到屏幕上:

image-20221026170449235

那么问题就来了,如果现在我们希望将这个窗口居中,就需要手动调整位置,但我们是要去适配各种分辨率的显示器才可以,不然到其他分辨率下,就无法居中了,我们可以动态获取分辨率来进行位置计算:

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开发图形界面好像也不是那么难?

得益于Java已经为我们封装好了各种方法,所以说要实现什么功能直接调用对应的方法即可,比如我们想要个性化光标,我们可以使用setCursor方法来实现,JDK已经为我们提供了一些预设的光标样式:

image-20221027151713661

设定光标样式后,当我们的鼠标移动到这个窗口内部时,就会变成我们设定好的光标样式了。

有关其他方法,这里暂时不进行介绍。

监听器

我们可以为窗口添加一系列的监听器,监听器会监听窗口中发生的一些事件,比如我们点击关闭窗口、移动鼠标、鼠标点击等,当发生对应的事件时,就会通知到对应的监听器进行处理,从而我们能够在发生对应事件时进行对应处理。

image-20221027161611050

比如我们现在希望点击关闭按钮关闭当前的窗口,但是我们发现默认情况下实际上是关不掉的,因为我们并没有对关闭事件进行处理,默认情况下对于这种点击时没有设定任何动作的,万一我们点了之后并不是要关闭窗口呢。要实现关闭窗口,我们可以使用addXXXListener来添加对应的事件监听器,比如窗口相关的操作那么就是WindowListener:

image-20221027155830335

这里我们可以给一个接口实现,或是使用对应的适配器(适配器模式是设计模式中的一种写法,因为接口中要实现的方法太多,但是实际上我们并不需要实现那么多,只需要实现对应的即可,所以说就可以使用适配器)我们只需要重写对应的方法,当发生对应事件时就会自动调用我们已经实现好的方法:

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是监听鼠标点击事件(必须要用真的鼠标点击,不知道为啥,笔记本的触摸板不行,可能是MacOS的BUG吧)
        System.out.println("鼠标点击:"+e.getX()+","+e.getY());
    }
});

这样,当我们点击窗口中的某个位置时,就可以获取对应的坐标并打印出来:

image-20221027164500070

注意这里的坐标并不是按照我们在数学中学习的平面直角坐标系来的,它的X轴是从左往右,但是Y轴是从上往下,原点也不是整个屏幕开始,而是我们的窗口左上角。所以说当我们点击右下角时,就会得到一个接近于窗口大小的坐标了。

我们也可以获取鼠标是使用哪个键点击的,我们的鼠标一般情况下有三个按键:

  • 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());    //获取滚动数量
    }
});

MacOS下的鼠标滚动是平滑滚动,会触发很多次,不像Windows下是一格一格的僵硬滚动。

通过使用这些监听器,我们就可以更好的控制我们的GUI程序了。

常用组件

前面我们介绍了监听器,我们接着来看看常用的一些组件,那么什么是组件呢?

组件实际上是AWT为我们预设好的一些可以直接使用的界面元素,比如按钮、文本框、标签等等,我们可以使用这些已经帮我们写好的组件来快速拼凑出一个好看且功能强大的程序:

image-20221027170224462

在开始学习组件之前,我们先将布局设定为null(因为默认情况下会采用BorderLayout作为布局)有关布局我们会在下一部分中进行介绍,这节课我们先介绍没有布局的情况下如何使用这些组件。

frame.setLayout(null);

首先我们来介绍一下最简单的组件,标签组件相当于一个普通的文本内容,我们可以将自己的标签添加到窗口中:

Label label = new Label("我是标签");   //添加标签只需要创建一个Label对象即可
label.setLocation(20, 50);   //注意,必须设定标签的位置和大小,否则无法展示出来
label.setSize(100, 20);
frame.add(label);    //使用add方法添加组件到窗口中

注意,组件的位置是以整个窗口的左上角为原点开始的(整个窗口指的是包括标题栏在内)所以说我们如果想要设置组件的位置,我们还得注意加上标题栏的高度,否则会被标题栏遮挡:

image-20221027175842110

我们可以自由修改文本的字体和大小:

//直接构造并传入一个Font对象即可
label.setFont(new Font("SimSong", Font.BOLD, 15));   //Font构造方法需要字体名称、字体样式(加粗、斜体)、字体大小

注意必须是操作系统已经安装的字体才支持展示,如果各位小伙伴不知道操作系统有哪些字体,可以使用:

GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()

来获取所有的系统字体:

image-20221027181909908

这里我们直接使用前面的family即可,比如我们要使用宋体,那么就输入其名称:

label.setFont(new Font("Songti SC", Font.BOLD, 15));

image-20221027182010485

可以看到字体已经成功修改了,当然,为了方便,如果我们的窗口中有很多的标签都想统一使用某一个字体,我们可以直接对窗口设定字体,那么只要是添加到窗口中的组件都会默认使用这个字体,除非单独指定组件字体。

要修改字体的颜色也很简单,我们可以使用:

label.setBackground(Color.BLACK);    //setBackground依然是背景颜色,注意背景填充就是我们之前设定的大小
label.setForeground(Color.WHITE);    //setForeground是设定字体颜色

image-20221027183745934

我们接着来认识一下下一个组件,这个组件的名字叫做按钮,实际上按钮也是我们经常会使用的一个组件:

Button button = new Button("点击充值");   //Button是按钮组件
button.setBounds(20, 50, 100, 50);
frame.add(button);

这样就可以添加一个按钮到我们的窗口中了:

image-20221027182903783

只不过,既然是按钮,那么肯定要添加一些点击动作才可以,比如点击按钮之后打印充值成功:

button.addActionListener(e -> System.out.println("充值成功"));  //addActionListener就是按钮点击监听器

是不是感觉还是很简单?当然,如果要修改按钮的字体或是颜色,依然使用之前的方式即可。

只不过光有按钮似乎太单调了一点,我们接着来认识下一个组件:

TextField field = new TextField();   //TextField是文本框
field.setBounds(20, 50, 100, 25);
frame.add(field);

image-20221027184138604

我们经常要在一些软件上登录,那么就要输入我们的用户名和密码,所以说文本框的作用还是非常明显的,我们也可以通过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);

我们来试试看吧:

image-20221027184618359

image-20221027184627653

是不是感觉有内味了?当然,可能会有小伙伴觉得如果我们输入密码的话,不应该将展示的文字隐藏起来吗?我们可以这样:

field.setEchoChar('*');   //setEchoChar设定展示字符,无论我们输入的是什么,最终展示出来的都是我们指定的字符

image-20221027184814288

当然,我们在获取输入的文本时还是输入的文本本身,不会变成展示的文本,只是一个视觉效果而已。这样,我们就可以将密码框做出来了。各位小伙伴可以尝试做一个登录界面。

但是肯定有小伙伴疑问,不是还有一个记住密码的勾选框吗?安排:

Checkbox checkbox = new Checkbox("记住密码");
checkbox.setBounds(20, 50, 100, 30);   //这个大小并不是勾选框的大小,具体的勾选框大小要根据操作系统决定,跟Label一样,是展示的空间大小
frame.add(checkbox);

最终展示出来的效果就是:

image-20221027185748324

效果还是挺不错的,我们也可以设定一个多选框:

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);

image-20221027190207441

我们可以使用getSelectedCheckbox方法来获取已经被选中的勾选框:

System.out.println(group.getSelectedCheckbox());

常用组件就暂时介绍到这里。

布局和面板

前面我们介绍了各种各样的组件,现在我们就可以利用这些组件来拼凑一个好看的程序了。

只不过,如果不使用布局,那么我们只能手动设置组件的位置以及大小,这就使得我们的程序在尺寸的设计上很有限,因为一旦窗口的大小发生变化,我们的组件依然是会放置在原本的位置上,要保证我们的设计不被破坏就只能固定窗口大小,但是很多应用都是支持放大和缩小的,并且在不同的大小下组件会自己调整位置:

image-20221028135701447

可以看到窗口的大小可以自由移动并且组件的位置会根据窗口大小自己进行调整。

这正是因为使用了布局实现的,布局可以根据自己的一些性质,对容器(这里可以是我们的窗口)内部的组件自动进行调整,包括组件的位置、组件的大小等,Java为我们提供了各种各样的布局管理器,我们来看看吧。

默认情况下,我们的窗口采用的是边界布局(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);

注意,约束只有在当前容器为对应布局时才可以使用。这里我们采用的是边界布局,边界布局可以将组件设定到五个区域:

image-20221028140816161

可以看到,分别在东、南、西、北、中心位置都可以添加组件,组件的大小会被自动调整,并且随着我们的窗口大小变化,组件的大小也会跟着自动调整,是不是感觉挺方便的?边界布局的性质:

  • BorderLayout布局的容器某个位置的某个组件会直接充满整个区域。
  • 如果在某个位置重复添加组件,只有最后一个添加的组件可见。
  • 缺少某个位置的组件时,其他位置的组件会延伸到该位置。

该博客转载自B站UP青空的霞光