Java从零开始系列08:图形用户界面程序设计

学习目标

  • 显示窗体
  • 在组件中显示信息
  • 事件处理
  • 首选项API

一、显示窗体

Java中,顶层窗口称为窗体(frame)。抽象窗口工具包(Abstract Window Toolkit, AWT)库中有一个成为Frame的类,用于描述这个顶层窗口。这个类的Swing版本名为JFrame,它扩展了Frame类。JFrame是极少数几个不绘制在画布上的Swing组件之一,其修饰部件(按钮、标题栏、图标等)由用户的窗口系统绘制,而不是由Swing绘制。

(一)创建窗体

如下程序在屏幕中显示了一个空窗体:
Java从零开始系列08:图形用户界面程序设计_第1张图片

package com.my.simpleframe;

import java.awt.*;
import javax.swing.*;

/**
 * @author 
 * @date 2022/5/24
 * @Description
 * @FileName SimpleFrameTest
 * @History
 */
public class SimpleFrameTest {
    public static void main(String[] args) {
        EventQueue.invokeLater(() ->
        {
            var frame = new SimpleFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}

class SimpleFrame extends JFrame {
    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 200;

    public SimpleFrame() {
        setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
}

Swing类位于java.swing包中。包名javax表示这是一个java扩展包,而不是核心包。从Java1.2版本开始,每个Java实现中都包含这个类。

在默认情况下,窗体的大小为 0 × 0 像素,这种窗体没有什么实际意义。这里定义了一个子类SimpleFrame,他的构造器将窗体大小设置为 300 × 200 像素。这是SimpleFrameJFrame之间唯一的差别。

SimpleFrameTest类的main方法中,我们构造了一个SimpleFrame对象并使它可见。

在每个Swing程序中,有两个技术问题要强调:一是所有的Swing组件必须由事件分派线程(event dispatch thread)配置,这是控制线程,它将鼠标点击和按键等事件传递给用户接口组件。下面的代码段用来执行事件分派线程的语句:

EventQueue.invokeLater(() ->
{
	statements
});

二是定义用户关闭这个窗体时的响应动作。这里让程序简单地退出:

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

在包含多个窗体的程序中,不能在用户关闭其中一个窗体时就让程序退出,在默认情况下,用户关闭窗体时只是将窗体隐藏起来,而程序没有终止。

仅仅是构造窗体,窗体是不会自动显示的。窗体起初不可见,可以在窗体第一次显示之前向其添加组件。为了显示窗体,main方法需要调用窗体的setVisible方法。

初始化语句结束后,main方法退出,但此时并没有终止程序,终止的只是主线程,事件分派线程会保持程序处于激活状态,直到关闭窗体或调用System.exit方法终止程序。

标题栏和外框装饰(如重置窗口大小的拐角等)都是由操作系统绘制的,而不是Swing库。Swing库负责绘制窗体内的所有内容,这里只是用默认的背景色填充了窗体。

(二)窗体属性

JFrame类本身只包含若干个改变窗体外观的方法。利用继承的魔力,大多数处理窗体大小和位置的方法都来自JFrame的各个超类,其中最重要的有以下方法:

  • setLocation方法和setBounds方法用于设置窗体的位置
  • setIconImage方法用于告诉窗口系统在标题栏、任务切换窗口等位置显示那个图标
  • setTitle方法用于改变标题栏的文字
  • setResizable利用一个boolean值确定是否允许用户改变窗体大小

组件类的很多方法是以获取/设置方法对的形式出现的,例如,Frame类的以下方法:

public String getTitle()
public void setTitle(String title)

这样一对获取/设置方法被称为属性(property)。属性有一个名和一个类型。将get或set之后的第一个字母改为小写字母就可以得到相应的属性名。如,Frame类有一个名为title且类型为String的属性。

对于get/set 约定,有一个例外:对于类型为boolean的属性,获取方法以is开头,如,下面定义了resizable属性:

public boolean isResizable()
public void setResizable(boolean resizable)

要确定适当的窗体大小,首先要得出屏幕的大小。调用Toolkit类的静态方法getDefaultToolkit得到一个Toolkit对象。然后,再调用getScreenSize方法,以Dimension对象的形式返回屏幕的大小。Dimension对象同时用公共(!)实例变量widthheight保存屏幕的宽度和高度。然后可以使用屏幕大小的一个适当的百分数指定窗体的大小。如:

Toolkit kit = Toolkit.getDefaultToolkit();
Dimension screenSize = kit.getScreenSize();
int screenWidth = screenSize.width;
int screenHeight = screenSize.height;
setSize(screenWidth / 2, screenHeight / 2);

另外,还提供窗体图标:

Image img = new ImageIcon("icon.gif").getImage();
setIconImage(img);

(三)在组件中显示信息

在Java中,窗体实际上设计为组件的容器,如菜单栏或其他用户界面元素。在通常情况下,应该在添加到窗体的另一个组件上绘制信息。

JFrame由根窗格、层级窗格、玻璃窗格和内容窗格组成,前三者用来组织菜单栏和内容窗格以及实现观感,添加到窗体的所有组件都会自动添加到内容窗格(content pane)中:

Component c = ...;
frame,add(c);	// added to the content pane

这里我们计划将一个组件添加到窗体中,消息绘制在组件上。要在组件上绘制,需要定义一个扩展JComponent的类,并覆盖其中的paintComponent方法。

paintComponent方法有一个Graphics类型的参数,Graphics对象保存着用于绘制图像和文本的一组设置,例如,设置字体或当前的颜色。在Java中,所有绘制都必须通过Graphics对象完成,其中包含绘制图案、图像和文本的方法。

可以如下创建一个能够进行绘制的组件:

class MyComponent extends JComponent 
{
	public void paintComponent(Graphics g)
	{
		code for drawing
	}
}

无论何种原因,只要窗口需要重新绘制,事件处理器就会通知组件,从而引发执行所有组件的paintComponent方法。(无需人工调用)

对于屏幕显示来说,Graphics对象的度量单位是像素,坐标(0,0)指示所绘制组件的左上角。

Graphics类有很多绘制方法,显示文本是一种特殊的绘制,paintComponent方法如下:

class NotHelloWorldComponent extends JComponent {
    public static final int MESSAGE_X = 75;
    public static final int MESSAGE_Y = 100;

    @Override
    public void paintComponent(Graphics g) {
        g.drawString("Not a Hello, World program", MESSAGE_X, MESSAGE_Y);
    }
    ...
}

最后,组件要告诉用户窗体的大小。覆盖getPreferredSize方法,返回一个有首选宽度和高度的Dimension类对象。

class NotHelloWorldComponent extends JComponent {
    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 200;
	...
    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
}

在窗体填入一个或多个组件时,如果只想使用其首选大小,可以使用pack方法而非setSize方法。

class NotHelloWorldFrame extends JFrame {
    public NotHelloWorldFrame() {
        add(new NotHelloWorldComponent());
        pack();
    }
}

完整代码及效果图如下:
Java从零开始系列08:图形用户界面程序设计_第2张图片

package com.my.simpleframe;

import javax.swing.*;
import java.awt.*;

/**
 * @author 
 * @date 2022/5/25
 * @Description
 * @FileName NotHelloWorld
 * @History
 */
public class NotHelloWorld {
    public static void main(String[] args) {
        EventQueue.invokeLater(() ->
        {
            var frame = new NotHelloWorldFrame();
            frame.setTitle("NotHelloWorld");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}

class NotHelloWorldFrame extends JFrame {
    public NotHelloWorldFrame() {
        add(new NotHelloWorldComponent());
        pack();
    }
}

class NotHelloWorldComponent extends JComponent {
    public static final int MESSAGE_X = 75;
    public static final int MESSAGE_Y = 100;

    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 200;

    @Override
    public void paintComponent(Graphics g) {
        g.drawString("Not a Hello, World program", MESSAGE_X, MESSAGE_Y);
    }

    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
}

1.处理2D图形

Graphics类包含绘制直线、矩形和椭圆等方法,但其绘制图形的操作能力有限,可以使用Java2D库的图形类。

使用Java2D库绘制图形,需要获得Graphics2D类的一个对象。这个类是Graphics类的子类。自Java1.2开始,paintComponent等方法会自动地接收一个Graphics2D类对象。只需要使用一个类型转换强制转换:

public void paintComponent(Graphics g)
{
	Graohics2D g2 = (Graohics2D) g;
	...
}

Java 2D库采用面向对象的方式组织几何图形。它提供了直线、矩形和椭圆的类:Line2D、Rectangle2D、Ellipse2D。这些类都实现了Shape接口。Java 2D库支撑更加复杂的图形,如圆弧、二次曲线、三次曲线和通用路径。

要想绘制一个图形,要先创建一个实现了Shape接口的类的对象,然后调用Graphics2D类的draw方法。如:

Rectangle2D rect = ...;
g2.draw(rect);

Java 2D库针对像素采用的是浮点坐标,内部计算都采用单精度float。

Java在将double值转换为float值时必须进行强制类型转换,解决方法是给浮点常量添加一个后缀F:

float f = 1.2F;

对于getWidth方法返回的double类型,也需要提供强制类型转换:

float f = (float) r.getWidth();

2D库为每个图形类提供了两个两个版本:一个使用float类型的坐标,一个使用double类型的坐标。如Rectangle2D类,这一个抽象类有两个静态内部子类:Rectangle2D.Float、Rectangle2D.Double,当构造一个Rectangle2D.Float对象时,要为坐标提供float数,后者则需提供Double数。

var floatRect = new Rectangle2D.Float(10.0F, 25.0F, 22.5F, 20.0F);
var doubleRect = new Rectangle2D.Double(10.0, 25.0, 22.5, 20.0);

构造参数表示矩形的左上角位置、宽和高。

Rectangle2D方法的参数和返回值均使用double类型。该类的相关规则也适用于其他类。

Point2D类也有两个子集Point2D.Float和Point2D.Double。可以如下构造一个点对象:var p = new Point2D.Double(10,20)

Rectangle2DEllipse2D类都是由公共超类RectangularShape继承来的。椭圆虽不是矩形,但有一个外接矩形。

RectanglePoint类也被放置在图形类的继承层次中,分别扩展了Rectangle2DPoint2D类,用整型坐标存储矩形和点。

Rectangle2DEllipse2D对象都很容易构造。需要指定:

  • 左上角的x和y的坐标
  • 宽和高

对于椭圆,这些表示外接矩形。例如:

var e = new Ellipse2D.Double(150, 200, 100, 50);

会构造一个椭圆,它的外接矩形左上角位于(150,200)、宽为100、高为50.

构造椭圆时,通常知道椭圆的中心、宽和高,而不是外接矩形的四角顶点。setFrameFromCenter方法使用中心点,但还要给出四个顶点中的一个。因此,通常用以下方式构造椭圆:

var ellipse = new Ellipse2D.Double(centerX - width / 2, centerY - height / 2, width, height);

要想构造一条直线,需要提供起点和终点。可以用Point2D对象或一对数值表示:

var line = new Line2D.Double(start, end);
var line = new Line2D.Double(startX, startY, endX, endY);

以下程序绘制了一个矩形、这个矩形的内接椭圆、矩形的对角线以及矩形中心为圆点的圆。
Java从零开始系列08:图形用户界面程序设计_第3张图片

package com.my.simpleframe;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.time.Year;

/**
 * @author 
 * @date 2022/5/25
 * @Description
 * @FileName DrawTest
 * @History
 */
public class DrawTest {
    public static void main(String[] args) {
        EventQueue.invokeLater(() ->
        {
            var frame = new DrawFrame();
            frame.setTitle("DrawTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}

class DrawFrame extends JFrame {
    public DrawFrame() {
        add(new DrawComponent());
        pack();
    }
}

class DrawComponent extends JComponent {
    private static final int DEFAULT_WIDTH = 400;
    private static final int DEFAULT_HEIGHT = 400;

    @Override
    public void paintComponent(Graphics g) {
        var g2 = (Graphics2D) g;

        // draw a rectangle

        double leftX = 100;
        double topY = 100;
        double width = 200;
        double height = 150;

        var rect = new Rectangle2D.Double(leftX, topY, width, height);
        g2.draw(rect);

        // draw the enclosed ellipse

        var ellipse = new Ellipse2D.Double();
        ellipse.setFrame(rect);
        g2.draw(ellipse);

        // draw a diagonal line

        g2.draw(new Line2D.Double(leftX, topY, leftX + width, topY + height));

        // draw a circle with the same center

        double centerX = rect.getCenterX();
        double centerY = rect.getCenterY();
        double radius = 150;

        var circle = new Ellipse2D.Double();
        circle.setFrameFromCenter(centerX, centerY, centerX + radius, centerY + radius);
        g2.draw(circle);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
}

2.使用颜色

使用Graphics2D类的setPaint方法可以为图形上下文的所有后续的绘制操作选择颜色。如:

g2.setPaint(Color.RED);
g2.drawString("Warning!", 100, 100);

可以用一种颜色填充一个封闭图形(如,矩形或椭圆)的内部。只需要调用draw替换为调用fill:

Rectangle2D rect = ...;
g2.setPaint(Color.RED);
g2.fill(rect);

要想使用多种颜色绘制,就需要选择一个颜色、绘制图形、再选择另外一种颜色、再绘制图形。

Color类用于定义颜色。在java.awt.Color类中提供了13个预定义的常量,它们分别表示13种标准颜色:BLACK, BLUE, CYAN, DARK_GRAY, GREEN, LIGHT_GRAY, MAGENTA, ORANGE, PINK, RED, WHITE, YELLOW

也可以提供三色分量来创建Color对象,从而指定一个定制颜色。红、绿和蓝三种颜色取值为0~255之间的整数:

g2.setPaint(new Color(0, 128, 128));	// a dull blue-green
g2.drawString("Welcome!", 75, 125);

要设置背景颜色,需要使用Component类中的setBackground方法。Component类是JComponent类的祖先:

var component = new MyComponent();
component.setBackground(Color.PINK);

另外,还有一个setForeground方法,用来指定在组件上进行绘制时使用的默认颜色。

3.使用字体

可以通过字体名(font face name)指定一种字体。字体名由字体族名(font family name,如“Helvetica”)和一个可选的后缀(如“Bold”)组成。

获得某台特定计算机上可用的字体,可以调用GraphicsEnvironment类的getAvailableFontFamilyNames方法。下面程序将打印出系统上所有字体名:

public class ListFonts {
    public static void main(String[] args) {
        String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
        for (String fontName : fontNames) {
            System.out.println(fontName);
        }
    }
}

要想使用某种字体绘制字符,必须先创建一个Font类的对象,指定字体名、字体风格和字体大小。如:

var sansbold14 = new Font("SansSerif", Font.BOLD, 14);

第三个参数是以点数目计算字体的大小。每英寸包含72个点。

在Font构造器中,提供字体名的设置也可以给出逻辑字体名。可以把Font构造器的第二个参数设为以下值(常规、加粗、斜体或加粗斜体):
Font.PLAIN, Font.BOLD, Font.ITALIC, Font.BOLD + Font.ITALIC

常规字体的字体大小为1点。可以使用deriveFont方法来获得所需大小的字体:

Font f =  f1.deriveFont(14.0F);

下面使用系统中14点加粗的标准 sans serif字体显示字符串“Hello,World”:

var sansbold14 = new Font("SansSerif", Font.BOLD, 14);
g2.setFont(sanbold14);
var message = "Hello, World!";
g2.drawString(message, 75, 100);

接下来,将字符串居中,需要知道字符串占据的宽和高的像素数。这两个值取决于:

  • 使用的字体
  • 字符串
  • 绘制字体的设备

可以通过Graphics2D类中的getFontRenderContext方法。它将返回一个FontRenderContext类的对象,可以直接将其传递给Font类的getStringBounds方法:

FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = sansbold14.getStringBounds(message, context);

getStringBounds方法将返回包围字符串的矩形。

以字符串“ebkpg”为例说明一些基本的排版术语:基线(baseline)是一条虚构的线,例如,字母“e”所在的底线。上坡度(ascent)是从基线到坡顶(ascenter)的距离(“b” “k”或大写字母的上面部分)。下坡度(descent)是从基线到坡底(descenter)的距离(“p” “g”等字母的下面部分)。行间距(leading)是某一行的坡底与其下一行的坡顶之间的空隙。字体的高度是连续两个基线之间的距离,等于下坡度+行间距+上坡度。

getStringBounds方法返回的矩形宽度是字符串水平方向的宽度。矩形的高度是上坡度、下坡度和行间距的总和。这个矩形始于字符的基线,矩形顶部的y坐标为负值。可以用以下方法获得字符串的宽度、高度和上坡度:

double stringWidth = bounds.getWidth();
double stringHeight = bounds.getHeight();
double ascent = -bounds.getY();

如果需要知道下坡度或行间距,可以使用Font类的getLineMetrics方法。这个方法返回一个LineMetrics类的对象,获得下坡度和行间距的方法是:

LineMetrics metrics = f.getLineMetrics(message, context);
float descent = metrics.getDescent();
float leading = metrics.getLeading();

注:若需要在paintComponent方法外部计算布局大小,不能从Graphics2D对象得到字体绘制上下文。应该换作调用JComponent类的getFontMetrics方法,然后调用getFontRenderContext

FontRenderContext context = getFontMetrics(f).getFontRenderContext();

下面是程序的源代码以及效果图:
Java从零开始系列08:图形用户界面程序设计_第4张图片

package com.my.simpleframe;

import javax.swing.*;
import javax.xml.stream.FactoryConfigurationError;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;

/**
 * @author 
 * @date 2022/5/26
 * @Description
 * @FileName FontTest
 * @History
 */
public class FontTest {
    public static void main(String[] args) {
        EventQueue.invokeLater(() ->
        {
            var frame = new FontFrame();
            frame.setTitle("FontTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}

class FontFrame extends JFrame {
    public FontFrame() {
        add(new FontComponent());
        pack();
    }
}

class FontComponent extends JComponent {
    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 200;

    @Override
    public void paintComponent(Graphics g){
        var g2 = (Graphics2D) g;

        var message = "Hello, World!";

        var f = new Font("Serif", Font.BOLD, 36);
        g2.setFont(f);

        FontRenderContext context = g2.getFontRenderContext();
        Rectangle2D bounds = f.getStringBounds(message, context);

        double x = (getWidth() - bounds.getWidth()) / 2;
        double y = (getHeight() - bounds.getHeight()) / 2;

        double ascent = -bounds.getY();
        double baseY = y + ascent;

        g2.drawString(message, (int) x, (int) baseY);

        g2.setPaint(Color.LIGHT_GRAY);

        g2.draw(new Line2D.Double(x, baseY, x + bounds.getWidth(), baseY));

        var rect = new Rectangle2D.Double(x, y, bounds.getWidth(),bounds.getHeight());
        g2.draw(rect);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
}

4.显示图像

可以使用ImageIcon类从文件读取图像:

Image image = new ImageIcon(filename).getImage();

现在变量image包含了一个封装了图像数据的对象的引用。可以使用Graphics类的drawImage方法显示这个图像。

public void paintComponent(Graphics g)
{
	...
	g.drawImage(image, x, y, null);
}

可以再进一步,在一个窗口中平铺显示图像。如,采用paintComponent方法实现平铺的显示。首先在左上角显示图像的一个副本,然后使用copyArea调用将其复制到整个窗口:

for (int i = 0; i * imageWidth <= getWidth(); i++)
	for (int j = 0; j * imageHeight <= getHeight(); j++)
		if (i + j > 0)
			g.copyArea(0, 0, imageWidth, imageHeight, j * imageHeight);

二、事件处理

(一)基本事件处理概念

在Java AWT中,事件源(如按钮或滚条)有一些方法,允许你注册事件监听器,这些对象会对事件做出所需的响应。

通知一个事件监听器发生了某个事件时,这个事件的相关信息会封装在一个事件对象(event object)中。在Jav中,所有事件对象最终都派生于java.util.EventObject类。

不同的事件源可以产生不同类型的事件。如,按钮可以发送一个ActionEvent对象,而窗口会发送WindowEvent对象。

下面给出AWT事件处理机制的概要说明:

  • 事件监听器是一个实现了监听器的接口(listener interface)的类实例
  • 事件源对象能够注册监听器对象并向其发送事件对象
  • 当事件发生时,事件源将事件对象发送给所有注册的监听器
  • 监听器对象再使用事件对象中的信息决定如何对事件做出响应

下面是指定监听器的示例:

ActionListener listener = ...;
var button = new JButton("OK");
button.addActionListener(listener);

只要按钮产生了一个“动作事件”,listener对象就会得到通知。

要实现ActionListener接口,监听器类必须有一个名为actionPerformed的方法,该方法接收一个ActionEvent对象作为参数。

class MyListener implements ActionListener
{
	...
	public void actionPerformed(ActionEvent event)
	{
		// reaction to button click goes here
		...
	}
}

只要用户点击按钮,JButton对象就会创建一个ActionEvent对象,然后调用listener.actionPerformed(event),并传入这个事件对象。一个事件源(如按钮)可以有多个监听器,只要点击按钮,就会调用所有监听器的actionPerformed方法。

(二)实例:处理按钮点击事件

下面以一个实例进行说明:想要在一个面板中布置三个按钮,另外添加三个监视器对象作为这些按钮的动作监视器。

用户点击面板上任何一个按钮,相关的监听器对象都会接收一个Action Event对象,指示点击了某个按钮。在示例程序中,监听器对象会改变背景颜色。

首先要创建一个按钮,在构造器中指定一个标签字符串或一个图标。如:

var yellowButton = new JButton("Yellow");
var blueButton = new JButton(new ImageIcon("blue-ball.gif"));

调用add方法将按钮添加到面板中。

var yellowButton = new JButton("Yellow");
var blueButton = new JButton("Blue");
var redButton = new JButton("Red");

buttonPanel.add(yellowButton);
buttonPanel.add(blueButton);
buttonPanel.add(redButton);

接下来要添加监听这些按钮的代码,需要一个实现了ActionListener接口的类。这个类应该包含一个actionPerformed方法,方法签名为:

public void actionPerformed(ActionEvent event);

点击按钮时,我们希望将面板的背景颜色设置为指定颜色,将所需颜色存储在监听器内:

private class ColorAction implements ActionListener {
	private final Color backgroundColor;

	 public ColorAction(Color c) 
	 {
		backgroundColor = c;
	}

	@Override
	public void actionPerformed(ActionEvent event)
	{
		buttonPanel.setBackground(backgroundColor);
	}
}

然后为每种颜色构造一个对象,并将这些对象设置为按钮监听器:

// create button actions
var yellowAction = new ColorAction(Color.YELLOW);
var blueAction = new ColorAction(Color.BLUE);
var redAction = new ColorAction(Color.RED);

// associate actions with buttons
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);

由于ColorAction对象不能访问buttonPanel变量,可以采用:一是将面板存储在ColorAction对象中,并在ColorAction的构造器中设置它;更方便的方法是将ColorAction设计为ButtonFrame类的一个内部类,可以实现自动访问外部面板。

完整窗体类如下:

package com.my.button;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * @author 
 * @date 2022/5/28
 * @Description
 * @FileName ButtonFrame
 * @History
 */
public class ButtonFrame extends JFrame {
    private final JPanel buttonPanel;
    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 200;

    public ButtonFrame() {
        setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);

        // create buttons
        var yellowButton = new JButton("Yellow");
        var blueButton = new JButton("Blue");
        var redButton = new JButton("Red");

        buttonPanel = new JPanel();

        // add buttons to panel
        buttonPanel.add(yellowButton);
        buttonPanel.add(blueButton);
        buttonPanel.add(redButton);

        // add panel to frame
        add(buttonPanel);

        // create button actions
        var yellowAction = new ColorAction(Color.YELLOW);
        var blueAction = new ColorAction(Color.BLUE);
        var redAction = new ColorAction(Color.RED);

        // associate actions with buttons
        yellowButton.addActionListener(yellowAction);
        blueButton.addActionListener(blueAction);
        redButton.addActionListener(redAction);
    }

    private class ColorAction implements ActionListener {
        private final Color backgroundColor;

        public ColorAction(Color c) {
            backgroundColor = c;
        }

        @Override
        public void actionPerformed(ActionEvent event) {
            buttonPanel.setBackground(backgroundColor);
        }
    }
}

(三)简洁地指定监听器

一个监听器有多个实例的情况不多见,更常见的情况是:每个监听执行一个单独的动作。这种情况可以不建立单独的类,直接使用lambda表达式:

exitButton.addActionListener(event -> System.exit(0));

有多个关联动作时,如上一节的颜色按钮,可以实现一个辅助方法:

public void makeButton(String name, Color backgroundColor)
{
	var button = new JButton(name);
	buttonPanel.add(button);
	button.addActionListener(event ->
		buttonPanel.setBackground(backgroundColor));
}

lambda表达式指示参数变量backgroundColor

(四)适配器类

假如想监听用户何时想要关闭主窗体,可能希望弹出一个对话框,只有在用户确认后才退出程序。

当程序用户试图关闭一个窗口时,JFrame对象就是WindowEvent的事件源。如果希望捕获这个事件,就必须有一个合适的监听器对象,并将它添加到窗体的窗口监听器列表中。

WindowListener listener = ...;
frame.addWindowListener(listener);

窗口监听器必须是实现WindowListener接口的类的一个对象。WindowListener接口实际上包含7个方法。窗体将调用这些方法响应7个不同的窗口事件。下面是完整的WindowListener接口:

public interface WindowListener
{
	void windowOpened(WindowEvent e);
	void windowClosing(WindowEvent e);
	void windowClosed(WindowEvent e);
	void windowIconified(WindowEvent e);
	void windowDeiconified(WindowEvent e);
	void windowActivated(WindowEvent e);
	void windowDeactivated(WindowEvent e);
}

注:在Windows下,通常把图标化(iconified)称为最小化(minimized)。

也可以定义一个实现这个接口的类。

(五)动作

启动同一个命令可以有多种方式。用户可以通过菜单、按键或工具栏上的按钮选择特定的功能。在AWT事件模型中这非常容易实现:将所有事件连接同一个监听器。

Swing包提供了Action接口用来封装命令,并将它们关联到多个事件源。动作(action)是封装以下内容的一个对象:

  • 命令的说明(一个文本字符串和一个可选的图标)
  • 执行命令所需要的参数(如,实例中所请求的颜色)

Action接口包含以下方法:

  • void actionPerformed(ActionEvent event)
  • void setEnabled(boolean b)启动或禁用动作
  • boolean is Enabled()检查动作是否启用
  • void putValue(String key, Object value)存储动作对象中的任意名/值对
  • Object getValue(String key)获取动作对象中的任意名/值对
  • void addPropertyChangeListener(PropertyChangeListener listener)
  • void removePropertyChangeListener(PropertyChangeListener listener)

其中putValuegetValue方法有两个重要的预定义字符串Action.NAMEAction.SMALL_ICON,用于将动作的名字和图标存储到一个动作对象中。

下面是所有预定义的动作表名:

名称
NAME 动作名,显示在按钮和菜单项上
SMALL_ICON 存储小图标的地方,显示在按钮、菜单项或工具栏中
SHORT_DESCRIPTION 图标的一个简短说明,显示在工具提示中
LONG_DESCRIPTION 图标的详细说明;可能用在联机帮助中,没有Swing组件使用这个值
MNEMONIC_KEY 快捷键缩写,显示在菜单项中
ACCELERATOR_KEY 存储加速键的地方;没有Swing组件使用这个值
ACTION_COMMAND_KEY 原先在registerKeyboardAction方法中使用,现在已过时
DEFAULT 可能很有用的“全包型”属性,没有Swing组件使用这个值

Action的最后两个方法能够让其他对象在动作对象的属性发生变化时得到通知。

注:Action是一个接口,而不是一个类。

下面总结如何完成相同的动作来响应按钮、菜单项或按键:

  • 实现一个扩展AbstractAction类的类。可以使用同一个类表示多个相关的动作
  • 构造动作类的一个对象
  • 从动作对象构造一个按钮或菜单项。构造器将从动作对象读取标签文本和图标
  • 对于能够由按键触发的操作,必须额外多执行几步。首先找到窗口的顶层组件,例如,包含所有其他组件的面板
  • 然后,得到顶层组件的WHEN_ANCESTOR_OF_FOCUS_COMPONENT输入映射。为需要的按键创建一个KeyStroke对象。创建一个动作键对象,如描述动作的一个字符串。将(按键、动作键)对添加到输入映射中
  • 最后,得到顶层组件的动作映射。将(动作键,动作对象)对添加到映射中。

(六)鼠标事件

如果只希望用户能点击按钮或菜单,则不需要显式地处理鼠标事件,鼠标操作将由用户界面中的各种组件内部处理。不过如果需要使用鼠标画图,就需要捕获鼠标移动、点击和拖动事件。

用户点击鼠标按钮时,将会调用三个监听器方法:鼠标第一次被按下时调用mousePressed;鼠标被释放时调用mouseReleased;最后调用mouseClicked。以MouseEvent类对象作为参数,调用getXgetY方法可以获得鼠标被按下时鼠标指针所在地x和y坐标。要区分单击、双击和三击(!),需要使用getClickCount方法。

下面程序实现了当鼠标点击的像素在所有已绘制的小方块之外时,会增加一个新的小方块;在某个小方块中双击鼠标,就会将其擦除:

package com.my.mouse;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.awt.event.*;

/**
 * @author
 * @date 2022/6/22
 * @Description A component with mouse operations for adding and removing squares
 * @FileName MouseComponent
 * @History
 */
public class MouseComponent extends JComponent {
    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 200;

    private static final int SIDELENGTH = 10;
    private final ArrayList<Rectangle2D> squares;
    /** the square containing the mouse cursor */
    private Rectangle2D current;

    public MouseComponent(){
        squares = new ArrayList<>();
        current = null;

        addMouseListener(new MouseHandler());
        addMouseMotionListener(new MouseMotionHandler());
    }

    @Override
    public Dimension getPreferredSize(){
        return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }

    @Override
    public void paintComponent(Graphics g){
        var g2 = (Graphics2D) g;

        // draw all squares
        for (Rectangle2D r : squares){
            g2.draw(r);
        }
    }

    /**
     * Finds the first square containing a point.
     * @param p
     * @return
     */
    public Rectangle2D find(Point2D p) {
        for (Rectangle2D r : squares){
            if (r.contains(p)){
                return r;
            }
        }
        return null;
    }

    /**
     * Adds a square to the collection.
     * @param p the center of the square
     */
    public void add(Point2D p)
    {
        double x = p.getX();
        double y = p.getY();

        current = new Rectangle2D.Double(x - SIDELENGTH / 2, y - SIDELENGTH / 2,
                SIDELENGTH, SIDELENGTH);
        squares.add(current);
        repaint();
    }

    /**
     * Removes a square from the collection.
     * @param s the square to remove
     */
    public void remove(Rectangle2D s)
    {
        if (s == null) {
            return;
        }
        if (s == current) {
            current = null;
        }
        squares.remove(s);
        repaint();
    }

    private class MouseHandler extends MouseAdapter
    {
        @Override
        public void mousePressed(MouseEvent event)
        {
            // add a new square if the cursor isn't inside a square
            current = find(event.getPoint());
            if (current == null) {
                add(event.getPoint());
            }
        }

        @Override
        public void mouseClicked(MouseEvent event)
        {
            // remove the current square if double clicked
            current = find(event.getPoint());
            if (current != null && event.getClickCount() >= 2) {
                remove(current);
            }
        }
    }

    private class MouseMotionHandler implements MouseMotionListener
    {
        @Override
        public void mouseMoved(MouseEvent event)
        {
            // set the mouse cursor to cross hairs if it is inside a rectangle

            if (find(event.getPoint()) == null) {
                setCursor(Cursor.getDefaultCursor());
            }
            else {
                setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
            }
        }

        @Override
        public void mouseDragged(MouseEvent event)
        {
            if (current != null)
            {
                int x = event.getX();
                int y = event.getY();

                // drag the current rectangle to center it at (x, y)
                current.setFrame(x - SIDELENGTH / 2, y - SIDELENGTH / 2, SIDELENGTH, SIDELENGTH);
                repaint();
            }
        }
    }
}

—#### 参考资料:
狂神说Java
Java核心技术 卷I(第11版)


上一章:Java从零开始系列07:集合

你可能感兴趣的:(java,jvm,前端)