【问题标题】:Swing application initialization and loading screen approachSwing 应用程序初始化和加载屏幕的方法
【发布时间】:2014-10-15 10:15:20
【问题描述】:

我制作了很多不同的 Swing 应用程序,它们的加载时间通常在几秒到几分钟之间变化,具体取决于应用程序 UI/数据大小。此外,在某些情况下,应用程序数据加载与 UI 加载混合在一起。

几秒钟的加载时间不是问题,但是当它超过 10 秒时 - 很明显应该显示某种加载屏幕,直到 UI/数据完全初始化。

您通常会做的 - 首先创建某种加载屏幕(例如,带有徽标的窗口和一些在应用程序加载时正在更新的标签)并从应用程序中的各种加载“点”更新它。

问题是 - 应用程序通常在排队到 EDT 中的单个调用中加载,并且很难将其分成对 EDT 的多次调用而不会使应用程序的代码复杂化。因此,由于应用程序加载是在对 EDT 排队的单个调用中执行的,因此您根本无法正确更新加载屏幕 - 由于 EDT 忙于加载应用程序,因此在初始化应用程序之前不会显示更新。

因此,为了在某些情况下实现加载屏幕,我将应用程序 UI 初始化移到了 EDT 之外,并将它们设计为在执行加载时不会执行任何 UI 更新内容。应用程序的框架显示和所有应用程序 UI 操作仍将在 EDT 中执行。这通常不太好,但经过大量测试和查看 Swing 代码后,我确信即使在大型应用程序上也不会导致任何问题。尽管如此,即使它不会引起任何问题,这通常也不是一件好事。

所以问题是:可以使用哪些方法来正确显示和更新应用程序加载屏幕,同时在 EDT 中保持应用程序初始化?

希望它不会太宽泛。

这是一个展示“坏”方法的“虚拟”应用程序:

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

public class DummyApplication extends JFrame
{
    private static JDialog loadingDialog;
    private static JLabel loadingProgress;

    public DummyApplication ()
    {
        super ( "Dummy application" );

        dummyProgressUpdate ( "Loading content...", 3000 );

        final JLabel label = new JLabel ( "Custom content" );
        label.setBorder ( BorderFactory.createEmptyBorder ( 100, 100, 100, 100 ) );
        getContentPane ().add ( label );

        dummyProgressUpdate ( "Loading settings...", 3000 );

        setDefaultCloseOperation ( WindowConstants.EXIT_ON_CLOSE );
        pack ();
        setLocationRelativeTo ( null );

        dummyProgressUpdate ( "Opening application...", 1000 );
    }

    private static void dummyProgressUpdate ( final String status, final int time )
    {
        SwingUtilities.invokeLater ( () -> loadingProgress.setText ( status ) );
        dummyLoadTime ( time );
    }

    private static void dummyLoadTime ( final long time )
    {
        try
        {
            Thread.sleep ( time );
        }
        catch ( final InterruptedException e )
        {
            e.printStackTrace ();
        }
    }

    public static void main ( final String[] args ) throws Exception
    {
        // Displaying loading screen from EDT first
        SwingUtilities.invokeAndWait ( () -> {
            loadingDialog = new JDialog ( ( Window ) null, "Loading screen" );
            loadingProgress = new JLabel ( "Initializing application...", JLabel.CENTER );
            loadingProgress.setBorder ( BorderFactory.createLineBorder ( Color.LIGHT_GRAY ) );
            loadingDialog.getContentPane ().setLayout ( new BorderLayout () );
            loadingDialog.getContentPane ().add ( loadingProgress );
            loadingDialog.setUndecorated ( true );
            loadingDialog.setAlwaysOnTop ( true );
            loadingDialog.setModal ( false );
            loadingDialog.setSize ( 400, 100 );
            loadingDialog.setLocationRelativeTo ( null );
            loadingDialog.setVisible ( true );
        } );

        // Initializing application outside of the EDT
        final DummyApplication applicationFrame = new DummyApplication ();

        // Displaying application from the EDT
        SwingUtilities.invokeLater ( () -> {
            loadingDialog.setVisible ( false );
            applicationFrame.setVisible ( true );
        } );
    }
}

前段时间我还发现在 JDK7 中实现了这个有趣的东西:
http://sellmic.com/blog/2012/02/29/hidden-java-7-features-secondaryloop/

SecondaryLoop 功能允许在 EDT 线程中阻止进一步的代码执行,而不会导致 UI 卡住。在 EDT 中打开模态 JDialog 时所做的基本相同。

所以有了这个功能,我发现可能是解决这个问题的更好方法:

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

public class DummyApplication extends JFrame
{
    private static JDialog loadingDialog;
    private static JLabel loadingProgress;

    public DummyApplication ()
    {
        super ( "Dummy application" );

        dummyProgressUpdate ( "Loading content...", 3000 );

        final JLabel label = new JLabel ( "Custom content" );
        label.setBorder ( BorderFactory.createEmptyBorder ( 100, 100, 100, 100 ) );
        getContentPane ().add ( label );

        dummyProgressUpdate ( "Loading settings...", 3000 );

        setDefaultCloseOperation ( WindowConstants.EXIT_ON_CLOSE );
        pack ();
        setLocationRelativeTo ( null );

        dummyProgressUpdate ( "Displaying application...", 1000 );
    }

    private static void dummyProgressUpdate ( final String status, final int time )
    {
        // Use SecondaryLoop to block execution and force loading screen update
        final SecondaryLoop loop = Toolkit.getDefaultToolkit ().getSystemEventQueue ().createSecondaryLoop ();
        SwingUtilities.invokeLater ( () -> {
            loadingProgress.setText ( status );
            loop.exit ();
        } );
        loop.enter ();

        // Perform dummy heavy operation
        dummyLoadTime ( time );
    }

    private static void dummyLoadTime ( final long time )
    {
        try
        {
            Thread.sleep ( time );
        }
        catch ( final InterruptedException e )
        {
            e.printStackTrace ();
        }
    }

    public static void main ( final String[] args ) throws Exception
    {
        // Displaying loading screen from EDT first
        SwingUtilities.invokeAndWait ( () -> {
            loadingDialog = new JDialog ( ( Window ) null, "Loading screen" );
            loadingProgress = new JLabel ( "Initializing application...", JLabel.CENTER );
            loadingProgress.setBorder ( BorderFactory.createLineBorder ( Color.LIGHT_GRAY ) );
            loadingDialog.getContentPane ().setLayout ( new BorderLayout () );
            loadingDialog.getContentPane ().add ( loadingProgress );
            loadingDialog.setUndecorated ( true );
            loadingDialog.setAlwaysOnTop ( true );
            loadingDialog.setModal ( false );
            loadingDialog.setSize ( 400, 100 );
            loadingDialog.setLocationRelativeTo ( null );
            loadingDialog.setVisible ( true );
        } );

        // Initializing and displaying application from the EDT
        SwingUtilities.invokeLater ( () -> {
            final DummyApplication applicationFrame = new DummyApplication ();
            loadingDialog.setVisible ( false );
            applicationFrame.setVisible ( true );
        } );
    }
}

如您所见 - 在dummyProgressUpdate 方法中,我为我的案例做了一些棘手的解决方法 - 基本上我正在阻止在 EDT 中执行的执行,并等待排队到 EDT 中的单独调用来更新加载屏幕。

这确实有效——尽管我不确定这是一件好事,也不确定它是否会在更大范围内引起任何副作用。而且在这种情况下,加载屏幕只会在进行强制更新时更新,这意味着如果加载屏幕(例如)运行了一些动画 - 它将无法正确显示,只会与文本一起更新。

【问题讨论】:

  • 我会考虑使用 SwingWorker 来完成大部分繁重的工作,它具有进度支持和 UI 同步功能。您还可以使用某种 ExecutorService 与 SwingWorker 混合作为代理与 UI 同步,最后,done 方法可用于关闭启动屏幕并启动主应用程序
  • 你不能只使用标准的闪屏吗? docs.oracle.com/javase/tutorial/uiswing/misc/splashscreen.html
  • @MadProgrammer 的重点是,如果您正在加载具有数千个单独 UI 元素的应用程序,则没有真正的“繁重工作” - 每个单独的元素几乎都会立即加载,但总共可能需要一些时间.加上这些组件之间的一些微操作和计算也需要一些时间,但是将这些操作中的每一个(沿着初始化代码传播)分离到一个单独的线程/工作者中简直是完全疯狂的。所以 SwingWorker 不会真正解决这个问题。
  • @a_horse_with_no_name 可能,但首先 - 这只是一个图像,以防我想将一些复杂的东西放在加载屏幕(或简单的基于 Swing 的屏幕)上,这是不可能的。此外,据我测试,SplashScreen 在 Unix 系统上加载后会导致应用程序内部出现各种问题,所以我不喜欢使用它。
  • +1 我认为我在某个地方看到了,在 JDK8 中编译的 SecondaryLoop 存在问题,而没有与 Java7 的向后兼容性,这个想法是正确的,但对于 Java7,我现在在某个地方看到了没有,不知道在哪里,除了我对 SecondaryLoop 和 (java.bean.)EventHandler 有问题,默认情况下 SecondaryLoop 的输出不需要 invokeLater,但是 ....

标签: java swing event-dispatch-thread


【解决方案1】:

如果您正在加载数据或必须连接到远程服务或其他冗长的应用程序,那么SwingWorker 是可用方法中更简单的一种,因为它通过其setProgress 方法和PropertyChangeListener 支持提供进度支持.

它通过 process/publish 方法提供 UI 同步,允许您安全地更新 UI,done 方法和/或 PropertyChangeListener 可用于确定工作人员何时完成

如果您所做的只是尝试加载 UI 元素,那么您应该考虑使用延迟加载方法。

因此,与其一次加载所有 UI 元素,尤其是当用户可能实际上并未查看 UI 的各个部分时,不如仅在绝对必要时才加载这些元素并节省时间。

当使用 CardLayoutJTabbedPane 之类的东西时,最好只在视图真正可见时才初始化 UI 和数据。

如果你真的想要,你也可以在视图不再可见时将其卸载,断开侦听器与数据源的连接,停止它以响应用户实际上看不到的数据源更改...

【讨论】:

  • 这可能是一个好方法——我会推迟一些 UI 部分的加载以稍后延迟加载它们,这将减少初始加载时间并且几乎永远不会冻结 UI。我猜这就是加载 IntelliJ IDEA 应用程序的方式,看看它是如何在慢速机器上初始化的。
  • @MikleGarin 这是一个很常见的概念,不需要它,不要加载它。我们经常这样做,但我们也为菜单这样做,因为它们可以根据用户正在执行/完成的操作来更改上下文……代码中的完全痛苦。 iOS 在幕后做了很多事情(我想 Android 会这样做),当你的资源有限时这是有道理的
  • @MikleGarin 在响应式 UI 和快速加载之间不断平衡的行为,有时它适用于快速加载,有时它适用于响应性,您需要对每个部分进行判断调用,相同的方法可能不是一直工作;)
  • @MadProgrammer 是的,整个案例非常模糊,可能需要不同的方法,具体取决于应用程序的具体情况,但仍然感谢有用的提示 - 我一定会使用这些提示来改进我的应用程序 :)
【解决方案2】:

here 所示,您可以使用SwingWorkerpublish() 中间结果在后台初始化任意大量数据,并在process() 中更新EDT 上的视图组件模型。使用PropertyChangeListener 来调整任何进度动画。禁用在加载完成之前不应使用的控件;当StateValueDONE 时启用它们。这样,

  • 结果将立即开始显示。
  • 将减少感知延迟。
  • GUI 将保持响应。

该方法最适用于使用flyweight pattern 最小化渲染的视图组件,例如JTableJListJTextAreaProfile 来寻找重构的最佳候选者。

【讨论】:

  • 这种方法对某些应用程序的问题在于,它们非常分散,甚至完全由运行时加载的单独插件组成,每个插件可能有自己的数据源和加载方式数据及其将数据发布到应用程序的自己的方式。虽然我猜这对于每个单独的插件和整个应用程序来说可能是一个好方法,但当请求特定部分时,整个应用程序将简单地延迟加载它的部分。
猜你喜欢
  • 2012-11-05
  • 2017-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-03
  • 2012-11-12
  • 1970-01-01
相关资源
最近更新 更多