【发布时间】: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 方法可用于关闭启动屏幕并启动主应用程序
-
@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