探究java swing 中repaint函数的作用


前言

我在编写java手写识别项目(一):初识KNN算法和GUI框架搭建的时候学到的新的函数——repaint函数,觉得这个函数在编写与绘图有关的项目中有着重要的作用,在这篇文章中,我将详细的分析这个函数,并探究这个函数的应用,希望对大家的java的学习有一定帮助。


一、repaint函数是什么?

1. 回到项目中的应用

在编写java手写识别项目(一):初识KNN算法和GUI框架搭建时,我接触到的用处就是清空画布在这里插入图片描述
每当执行完一个动作或Button后我都会调用一次repaint函数进行清空画布,这样再进行其他操作就比较方便。

那么repaint函数到底是什么?
我们先看看GPT4.0怎么说:

在Java Swing框架中,repaint()是一个方法,用于请求异步更新组件的显示。它标记一个组件为“需要重绘”,通知Swing的绘图系统在未来的某个时刻重新绘制该组件。这个过程涉及调用组件的paint()方法,通常是由事件调度线程(Event Dispatch Thread,EDT)处理。通过这种方式,repaint()实现了Swing组件视觉外观的动态更新。

😂🤔😓额…非常好,除了汉字之外我就看明白汉字了(啥也看不懂🥹)

2. 深入源码

既然专业的描述看不懂,那就需要深入分析,一个容易想到的方法就是直接查看它的源码。在 Windows/Linux 上,使用 Ctrl + BCtrl + Click。在 macOS 上,使用 Cmd + BCmd + Click,进入到Component类中。我们会看到以下代码:

    /**
     * Repaints this component.
     * <p>
     * If this component is a lightweight component, this method
     * causes a call to this component's {@code paint}
     * method as soon as possible.  Otherwise, this method causes
     * a call to this component's {@code update} method as soon
     * as possible.
     * <p>
     * <b>Note</b>: For more information on the paint mechanisms utilitized
     * by AWT and Swing, including information on how to write the most
     * efficient painting code, see
     * <a href="http://www.oracle.com/technetwork/java/painting-140037.html">Painting in AWT and Swing</a>.

     *
     * @see       #update(Graphics)
     * @since     1.0
     */
    public void repaint() {
        repaint(0, 0, 0, width, height);
    }

    /**
     * Repaints the component.  If this component is a lightweight
     * component, this results in a call to {@code paint}
     * within {@code tm} milliseconds.
     * <p>
     * <b>Note</b>: For more information on the paint mechanisms utilitized
     * by AWT and Swing, including information on how to write the most
     * efficient painting code, see
     * <a href="http://www.oracle.com/technetwork/java/painting-140037.html">Painting in AWT and Swing</a>.
     *
     * @param tm maximum time in milliseconds before update
     * @see #paint
     * @see #update(Graphics)
     * @since 1.0
     */
    public void repaint(long tm) {
        repaint(tm, 0, 0, width, height);
    }

    /**
     * Repaints the specified rectangle of this component.
     * <p>
     * If this component is a lightweight component, this method
     * causes a call to this component's {@code paint} method
     * as soon as possible.  Otherwise, this method causes a call to
     * this component's {@code update} method as soon as possible.
     * <p>
     * <b>Note</b>: For more information on the paint mechanisms utilitized
     * by AWT and Swing, including information on how to write the most
     * efficient painting code, see
     * <a href="http://www.oracle.com/technetwork/java/painting-140037.html">Painting in AWT and Swing</a>.
     *
     * @param     x   the <i>x</i> coordinate
     * @param     y   the <i>y</i> coordinate
     * @param     width   the width
     * @param     height  the height
     * @see       #update(Graphics)
     * @since     1.0
     */
    public void repaint(int x, int y, int width, int height) {
        repaint(0, x, y, width, height);
    }

    /**
     * Repaints the specified rectangle of this component within
     * {@code tm} milliseconds.
     * <p>
     * If this component is a lightweight component, this method causes
     * a call to this component's {@code paint} method.
     * Otherwise, this method causes a call to this component's
     * {@code update} method.
     * <p>
     * <b>Note</b>: For more information on the paint mechanisms utilitized
     * by AWT and Swing, including information on how to write the most
     * efficient painting code, see
     * <a href="http://www.oracle.com/technetwork/java/painting-140037.html">Painting in AWT and Swing</a>.
     *
     * @param     tm   maximum time in milliseconds before update
     * @param     x    the <i>x</i> coordinate
     * @param     y    the <i>y</i> coordinate
     * @param     width    the width
     * @param     height   the height
     * @see       #update(Graphics)
     * @since     1.0
     */
    public void repaint(long tm, int x, int y, int width, int height) {
        if (this.peer instanceof LightweightPeer) {
            // Needs to be translated to parent coordinates since
            // a parent native container provides the actual repaint
            // services.  Additionally, the request is restricted to
            // the bounds of the component.
            if (parent != null) {
                if (x < 0) {
                    width += x;
                    x = 0;
                }
                if (y < 0) {
                    height += y;
                    y = 0;
                }

                int pwidth = (width > this.width) ? this.width : width;
                int pheight = (height > this.height) ? this.height : height;

                if (pwidth <= 0 || pheight <= 0) {
                    return;
                }

                int px = this.x + x;
                int py = this.y + y;
                parent.repaint(tm, px, py, pwidth, pheight);
            }
        } else {
            if (isVisible() && (this.peer != null) &&
                (width > 0) && (height > 0)) {
                PaintEvent e = new PaintEvent(this, PaintEvent.UPDATE,
                                              new Rectangle(x, y, width, height));
                SunToolkit.postEvent(SunToolkit.targetToAppContext(this), e);
            }
        }
    }

这就是源代码的特点,我第一次看,简单说就是一堆奇怪的注释和一些看不懂的代码。在C/C++中,源代码理解起来还是相当于难度的,但是在java IDEA中,理解成本降低了不少,对于初学者来说查看源代码对于理解一个类或函数的作用有很大的作用。

3. 详细分析

整体概括

进入源码后,我们会看到repaint函数的4的重载函数,它们分别是:

public void repaint() {
    repaint(0, 0, 0, width, height);
}
public void repaint(long tm) {
    repaint(tm, 0, 0, width, height);
}
public void repaint(int x, int y, int width, int height) {
    repaint(0, x, y, width, height);
}
public void repaint(long tm, int x, int y, int width, int height) {
    // ... (具体实现略)
}

这4个。我们不难看出,核心的代码就是第4个函数,其余3个都是对第4个进行调用,下面,我们将注意力集中到第4个函数上。对函数进行简单的处理:我们使用GPT为其添加中文的注释

public void repaint(long tm, int x, int y, int width, int height) {
    // 检查组件是否为轻量级组件(不是直接由本地系统窗口管理)
    if (this.peer instanceof LightweightPeer) {
        // 对于轻量级组件,重绘请求需要转发给父组件处理

        // 检查是否有父组件存在
        if (parent != null) {
            // 确保请求的重绘区域在组件的范围内
            if (x < 0) {
                width += x; // 调整宽度
                x = 0;      // 重置x坐标
            }
            if (y < 0) {
                height += y; // 调整高度
                y = 0;       // 重置y坐标
            }

            // 确保请求的重绘区域不大于组件自身的尺寸
            int pwidth = (width > this.width) ? this.width : width;
            int pheight = (height > this.height) ? this.height : height;

            // 如果计算后的重绘区域有效(即宽度和高度大于0)
            if (pwidth > 0 && pheight > 0) {
                // 将重绘区域的坐标转换为父组件的坐标系
                int px = this.x + x;
                int py = this.y + y;

                // 调用父组件的repaint方法,传递转换后的参数
                parent.repaint(tm, px, py, pwidth, pheight);
            }
        }
    } else {
        // 对于量级重组件(由本地系统窗口管理)

        // 检查组件是否可见并且有有效的peer(窗口句柄)
        if (isVisible() && (this.peer != null) && (width > 0) && (height > 0)) {
            // 创建一个绘图事件,指定需要更新的区域
            PaintEvent e = new PaintEvent(this, PaintEvent.UPDATE, new Rectangle(x, y, width, height));

            // 将绘图事件发布到Swing事件队列中,待后续处理
            SunToolkit.postEvent(SunToolkit.targetToAppContext(this), e);
        }
    }
}

这里我们看到了repaint函数对于轻量级和重量级组件分别进行了处理,为什么需要这样的区分:在 Swing 中,轻量级和重量级组件的绘制机制有显著差异。轻量级组件完全由 Java 自身处理绘制,而重量级组件的绘制则涉及到与本地操作系统的交互。这种区分对于优化绘制性能和处理组件间的交互(如重叠和布局)非常重要。
if (this.peer instanceof LightweightPeer)这一条语句,就是用于判断是否为轻量级组件,这个语句的具体含义发,我们这篇文章不做具体分析,相关文档我放到文章开头的资源绑定中了。

轻量级组件处理

这个过程有点像递归调用

if (parent != null) {
	// 确保请求的重绘区域在组件的范围内
	if (x < 0) {
		width += x; // 调整宽度
		x = 0;      // 重置x坐标
	}
	if (y < 0) {
		height += y; // 调整高度
		y = 0;       // 重置y坐标
	}
			
	// 确保请求的重绘区域不大于组件自身的尺寸
  	int pwidth = (width > this.width) ? this.width : width;
	int pheight = (height > this.height) ? this.height : height;

	if (pwidth <= 0 || pheight <= 0) {
		return;
	}
	
	// 将重绘区域的坐标转换为父组件的坐标系
	int px = this.x + x;
	int py = this.y + y;
			
	// 调用父组件的repaint方法,传递转换后的参数
	parent.repaint(tm, px, py, pwidth, pheight);
}

当这个轻量级组件的父组件不为空时,这个轻量级组件的重绘请求在组件树中向上传递,因为对于轻量级组件,重绘操作通常需要父组件来协助处理,轻量级组件本身不拥有与底层平台的直接交互能力。这个过程中不断调整坐标让它适应父组件,直至找到一个合适的处理者。
最终,重绘请求会传递到一个可以直接处理绘制请求的组件。在 Swing 中,这通常是一个能够与底层窗口系统交互的重量级组件,或者是顶级容器(如 JFrameJDialog)。这个组件或容器负责管理其所有子组件的绘制。
接下来就进入到了else下面的语句:

重量级组件处理

else {
    // 检查组件是否可见、是否有对应的peer对象(即与本地窗口系统的连接),
    // 以及请求重绘的区域宽度和高度是否有效(大于0)
    if (isVisible() && (this.peer != null) && (width > 0) && (height > 0)) {
        // 创建一个绘制事件(PaintEvent),指定需要更新的区域
        PaintEvent e = new PaintEvent(this, PaintEvent.UPDATE, new Rectangle(x, y, width, height));

        // 将这个绘制事件发布到事件队列中,等待事件调度线程(Event Dispatch Thread, EDT)处理
        SunToolkit.postEvent(SunToolkit.targetToAppContext(this), e);
    }
}

对这些底层组件进行处理,这是repaint函数的核心所在。它通过创建一个绘制事件并将其传递给事件队列中等待EDT处理。这部分需要与底层建立链接,下面时详细的执行过程:

检查组件状态
  1. 可见性与存在性检查

    • isVisible(): 确保组件当前是可见的。如果组件不可见,那么没有必要进行重绘操作。
    • this.peer != null: 确保组件具有有效的“peer”,即它已经与底层窗口系统建立了连接。对于重量级组件,peer 是其在本地窗口系统中的表示。
  2. 重绘区域的有效性检查

    • width > 0 && height > 0: 确保请求重绘的区域具有有效的尺寸。如果宽度或高度为零或负数,则没有实际的绘制工作需要执行。
创建并发布绘制事件
  1. 创建绘制事件(PaintEvent

    • new PaintEvent(this, PaintEvent.UPDATE, new Rectangle(x, y, width, height)): 创建一个新的 PaintEvent 对象,表示一个绘制请求。这个事件包含了组件自身的引用和需要更新的区域。
    • PaintEvent.UPDATE: 这是事件的类型,表示这是一个更新事件。在 Swing 中,UPDATE 类型的事件通常会触发组件的 update 方法,该方法再调用 paint 方法来执行实际的绘制操作。
  2. 发布事件到事件队列

    • SunToolkit.postEvent(SunToolkit.targetToAppContext(this), e): 这一步骤将创建的 PaintEvent 发布到 Swing 的事件队列中。SunToolkit.targetToAppContext(this) 确定了事件应该被发布到哪个应用程序上下文中。
    • 事件队列由事件调度线程(Event Dispatch Thread, EDT)处理。EDT 负责处理所有的 GUI 事件,包括绘制事件。这样做确保了 GUI 操作的线程安全和一致性。
事件处理和绘制执行
  1. 事件调度线程处理事件

    • PaintEvent 到达事件队列后,EDT 将提取并处理这个事件。处理通常涉及调用组件的 update 方法,后者通常再调用 paint 方法来进行实际的绘制。
    • 绘制操作会根据 PaintEvent 指定的区域更新组件的视觉表示。
  2. 完成重绘流程

    • 一旦 paint 方法执行完成,组件的相关部分就被更新了。如果有必要,操作系统会将这些更改反映到屏幕上。
    • 这样,重量级组件的更新请求就在 Swing 的架构中得到了正确的处理。

二、一个简短的总结

repaint实现重绘操作最终还是由paint函数实现的,哈哈哈哈哈说了一堆repaint就是用paint画😂
那么问题来了,为什么repaint函数回经过如此复杂的过程来连接底层,连接操作系统,而不是直接通过paint函数覆盖实现重绘?
通过查阅资料,总结出一下5点:
涉及到Java Swing 的设计哲学和多线程环境中的绘制机制。

1. 线程安全和事件调度

  • 事件调度线程(EDT):Swing 是一个基于事件驱动的框架,所有的 GUI 操作,包括绘制,都必须在事件调度线程(EDT)上执行。这是为了确保线程安全,因为 GUI 组件不是线程安全的。如果直接在非EDT线程上调用 paint 方法,可能会导致线程冲突和界面更新的不一致性。

2. 优化绘制过程

  • 避免不必要的绘制repaint 方法允许 Swing 延迟绘制操作,合并多个绘制请求,从而减少不必要的屏幕刷新和重新绘制。这种优化对于提高应用程序的性能和响应速度非常重要。

3. 管理绘制的复杂性

  • 复杂的组件层次结构:Swing 应用通常包含复杂的组件层次结构。repaint 方法通过委托给父组件或 Swing 的绘制系统,可以更有效地管理这种复杂性,例如处理重叠的组件或部分重绘。

4. 提供灵活的绘制机制

  • 自定义绘制的控制repaint 方法为开发者提供了控制何时以及如何更新组件的灵活性。而直接调用 paint 方法则缺乏这样的控制,可能导致无效的绘制和性能问题。

5. 遵守 MVC 设计模式

  • 分离表示和逻辑:Swing 遵循模型-视图-控制器(MVC)设计模式。在这种模式中,repaint 方法属于控制器的一部分,负责处理绘制逻辑,而 paint 方法属于视图,负责表示。这种分离确保了代码的模块化和可维护性。

三、一个应用案例

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

// 定义 SimpleRepaintExample 类,继承自 JFrame,用于创建主窗口
public class SimpleRepaintExample extends JFrame {

    // 定义 ColorPanel 实例变量
    private final ColorPanel colorPanel;

    // 构造函数
    public SimpleRepaintExample() {
        // 设置窗口标题
        setTitle("Simple Repaint Example");
        // 设置默认关闭操作,关闭窗口时退出应用程序
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // 创建 ColorPanel 实例,并添加到窗口的中央
        colorPanel = new ColorPanel();
        add(colorPanel, BorderLayout.CENTER);

        // 创建按钮,用于触发颜色变化和重绘
        JButton changeColorButton = new JButton("Change Color");
        changeColorButton.addActionListener(e -> {
            colorPanel.changeColor(); // 改变颜色
            colorPanel.repaint();     // 请求重绘 ColorPanel
        });
        add(changeColorButton, BorderLayout.SOUTH); // 将按钮添加到窗口的南部

        // 设置窗口的大小
        setSize(300, 200);
        // 使窗口可见
        setVisible(true);
    }

    // 定义 ColorPanel 类,继承自 JPanel
    private static class ColorPanel extends JPanel {
        // 初始颜色设置为红色
        private Color color = Color.RED;

        // 用于改变颜色的方法
        public void changeColor() {
            // 随机生成新的颜色
            Random rand = new Random();
            float r = rand.nextFloat();
            float g = rand.nextFloat();
            float b = rand.nextFloat();
            color = new Color(r, g, b); // 设置新颜色
        }

        // 重写 paintComponent 方法,用于绘制组件
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);  // 调用父类的 paintComponent 方法
            // 使用当前颜色填充矩形
            g.setColor(color);
            g.fillRect(20, 20, 100, 100);
        }
    }

    // 主函数
    public static void main(String[] args) {
        // 使用 SwingUtilities.invokeLater 来确保 GUI 更新在事件调度线程中执行
        SwingUtilities.invokeLater(SimpleRepaintExample::new);
        // 这里使用了 Java 8 的方法引用(SimpleRepaintExample::new)作为 Runnable 的实现
        // 目的是创建 SimpleRepaintExample 类的实例,同时确保这个操作在正确的线程中执行
        // 在 Swing 中,所有的 UI 更新操作都应该在事件调度线程(EDT)中进行,以避免线程安全问题
    }
}

一个以变色的小方块😊
在这里插入图片描述

总结

在本篇博客中,我们深入探讨了 Java Swing 中 repaint 方法的工作原理及其在组件绘制中的关键作用。通过分析 repaint 方法的源码,我们理解了 Swing 如何处理轻量级和重量级组件的绘制请求,并确保了在多线程环境中的线程安全和高效绘制。我们还探讨了 repaintpaint 方法之间的区别以及 Swing 的事件驱动架构。此外,通过具体的代码示例,我们展示了如何在实际应用中使用这些知识。这些理解对于开发高效、稳定的 Swing 应用至关重要。

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

钻石程序的金锄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值