Swing:如何从AWT线程运行作业,但是在窗口布局之后?
我的完整GUI在AWT线程内运行,因为我使用SwingUtilities.invokeAndWait(...)
启动主窗口。
现在我有一个JDialog,它只是显示一个JLabel
,它表示某个作业正在进行中,并在作业完成后关闭该对话框。
问题是:标签不显示。 这项工作似乎是在JDialog
完全布局之前开始的。
当我只是打开对话框而不等待作业并关闭时, 会显示标签。
对话框在其ctor中执行的最后一项操作是setVisible(true)
。
诸如revalidate()
, repaint()
,……之类的东西也无济于事。
即使我为受监视的作业启动一个线程,并使用someThread.join()
等待它也没有帮助,因为当前线程(即AWT线程)被join
阻塞,我猜。
用JFrame
替换JDialog
也无济于事。
那么,这个概念一般是错的吗? 或者,在确保JDialog
(或JFrame
)完全布局后 ,我可以管理它以完成某项工作吗?
我想要实现的简化算法:
- 创建
JDialog
的子类 - 确保它及其内容完全布局
- 启动一个进程并等待它完成(线程与否,无关紧要)
- 关闭对话框
我设法编写了一个可重现的测试用例:
编辑现在解决了问题:这个用例确实显示了标签,但由于对话框的模态,它在“模拟过程”之后无法关闭。
import java.awt.*; import javax.swing.*; public class _DialogTest2 { public static void main(String[] args) throws Exception { SwingUtilities.invokeAndWait(new Runnable() { final JLabel jLabel = new JLabel("Please wait..."); @Override public void run() { JFrame myFrame = new JFrame("Main frame"); myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); myFrame.setSize(750, 500); myFrame.setLocationRelativeTo(null); myFrame.setVisible(true); JDialog d = new JDialog(myFrame, "I'm waiting"); d.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); d.add(jLabel); d.setSize(300, 200); d.setLocationRelativeTo(null); d.setVisible(true); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { Thread.sleep(3000); // simulate process jLabel.setText("Done"); } catch (InterruptedException ex) { } } }); d.setVisible(false); d.dispose(); myFrame.setVisible(false); myFrame.dispose(); } }); } }
尝试这个:
package javaapplication3; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.SwingUtilities; public class Main { public static void main(String[] args) throws Exception { SwingUtilities.invokeAndWait(new Runnable() { final JLabel jLabel = new JLabel("Please wait..."); @Override public void run() { JFrame myFrame = new JFrame("Main frame"); myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); myFrame.setSize(750, 500); myFrame.setLocationRelativeTo(null); myFrame.setVisible(true); JDialog d = new JDialog(myFrame, "I'm waiting"); d.add(jLabel); d.setSize(300, 200); d.setLocationRelativeTo(null); d.setVisible(true); new Thread(new Runnable() { @Override public void run() { public void run() { try { Thread.sleep(3000); // simulate process jLabel.setText("Done"); // HERE: should be done on EDT! } catch (InterruptedException ex) { } } }).start(); } }); } }
这个有效,但不正确。 我会解释发生了什么。
你的main()
方法从’main’线程开始。 所有与Swing相关的代码都应该在EDT线程上完成。 这就是你正在使用(正确) SwingUtilities.invokeAndWait(...)
。 到现在为止还挺好。
但是在EDT上应该没有长期运行的任务。 由于Swing是单线程的,因此任何长时间运行的进程都将阻止EDT。 因此,您的代码Thread.wait(...)
永远不应该在EDT上执行。 这是我的修改。 我将调用包装在另一个线程中。 所以这是Swing惯用的长期运行任务处理。 我使用Thread类来简洁,但我真的建议使用SwingWorker
线程。
而且非常重要:我在前面的例子中犯了一个错误。 看到“HERE”评论的行? 这是另一个Swing单线程规则违规。 线程内的代码在EDT 之外运行,所以它永远不应该触摸Swing。 因此,使用Swing单线程规则,此代码不正确。 冻结GUI并不安全。
怎么纠正这个? 简单。 你应该将你的调用包装在另一个线程中并将其放在EDT队列中。 所以正确的代码应如下所示:
SwingUtilities.invokeLater(new Runnable() { public void run() { jLabel.setText("Done"); } });
编辑 :这个问题涉及很多Swing相关问题。 无法一次解释所有这些……但这里还有一个片段,可以满足您的需求:
public static void main(String[] args) throws Exception { SwingUtilities.invokeAndWait(new Runnable() { final JFrame myFrame = new JFrame("Main frame"); final JLabel jLabel = new JLabel("Please wait..."); final JDialog d = new JDialog(myFrame, "I'm waiting"); @Override public void run() { myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); myFrame.setSize(750, 500); myFrame.setLocationRelativeTo(null); myFrame.setVisible(true); d.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); d.add(jLabel); d.setSize(300, 200); d.setLocationRelativeTo(null); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); // simulate process System.out.println("After"); SwingUtilities.invokeLater(new Runnable() { public void run() { d.setVisible(false); d.dispose(); myFrame.setVisible(false); myFrame.dispose(); } }); } catch (InterruptedException ex) { } } }).start(); d.setVisible(true); } }); }
总结一下:
- 所有与Swing相关的代码必须在EDT上运行
- 所有长时间运行的代码都不能在EDT上运行
- 代码可以使用
SwingUtilities.
在EDT上运行SwingUtilities.
…-
invokeAndWait()
– 顾名思义,调用是同步的, -
invokeLater()
– 在某个时候调用代码,但立即返回
-
- 如果你在EDT并想要在另一个线程上调用代码,那么你可以:
- 创建一个新的
Thread
(将Runnable传递给新的Thread或覆盖它的start()
方法)并启动它, - 创建一个新的
SwingWorker
线程,它有一些额外的东西。 - 可能使用任何其他线程机制(例如Executor线程)。
- 创建一个新的
典型的GUI场景包括:
- 创建GUI组件,
- 联系财产改变听众,
- 执行与用户操作相关的代码(即运行属性更改侦听器),
- 运行可能耗时的任务,
- 更新GUI状态,
1.,2.,3。和4.在EDT上运行。 4.不应该。 有很多方法可以编写正确的线程代码。 最麻烦的是使用早期版本的Java附带的Thread
类。 如果天真地做到这一点,就会浪费资源(一次运行的线程太多)。 更新GUI也很麻烦。 使用SwingWorker可以缓解这个问题。 它保证在启动,运行和更新GUI时表现正常(每个都有一个专用的方法,你可以覆盖并确保它在适当的线程上运行)。
如前面的答案中所述,你必须在EDT的一个不同的线程中运行冗长的工作。
最简单的恕我直言是使用SwingWorker并在完成后添加一个侦听器来处理对话框。 这是一个示例:
SwingWorkerCompletionWaiter.java
public class SwingWorkerCompletionWaiter implements PropertyChangeListener { private JDialog dialog; public SwingWorkerCompletionWaiter(JDialog dialog) { this.dialog = dialog; } @Override public void propertyChange(PropertyChangeEvent event) { if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.DONE == event.getNewValue()) { dialog.setVisible(false); dialog.dispose(); } } }
以下代码将执行一些冗长的任务:
SwingWorker worker = new SwingWorker() { @Override protected Void doInBackground() throws Exception { // do something long } }; JDialog dialog = new JDialog(); // initialize the dialog here worker.addPropertyChangeListener(new SwingWorkerCompletionWaiter(dialog)); worker.execute(); dialog.setVisible(true);
如果你想创建某种“请等待”对话框,可能更容易扩展JDialog并在你需要的任何地方使用扩展类。
首先,对于推测工作的事情感到抱歉,在发布代码之前,我并没有真正理解你要做的事情。
尽管如此,我并不熟悉这种创建对话框和窗口的方法,所以我建议你为JDialog创建一个单独的类。 只是一个简单的类,如下所示:
public class MyDialog extends JDialog { public MyDialog(JFrame owner, String title) { super(owner, title); d.setSize(300, 200); d.setLocationRelativeTo(null); d.setVisible(true); d.setDefaultCloseOperation(DISPOSE_ON_CLOSE ); Container c = getContentPane(); c.setLayout(new FlowLayout()); c.add(new JLabel("Please wait...")); } }
并以这种方式启动对话框:
new MyDialog(this, "Your title");
它可能看起来像狗屎,但它可能工作(我没有测试)。
希望这可以帮助。
您应该使用invokeLater而不是InvokeAndWait。
public class DialogTest { public static void main(String[] args) throws Exception { final JLabel comp = new JLabel("the job has started");; final JFrame myFrame = new JFrame("Main frame"); final JDialog d = new JDialog(myFrame, "I'm waiting"); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); myFrame.setSize(750, 500); myFrame.setLocationRelativeTo(null); myFrame.setVisible(true); d.setModalityType(JDialog.ModalityType.APPLICATION_MODAL); d.add(comp); d.setSize(300, 200); d.setLocationRelativeTo(null); d.setVisible(true); } }); try { Thread.sleep(3000); // d.setVisible(false); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { comp.setText("the job has finished"); d.setVisible(false); d.dispose(); myFrame.setVisible(false); myFrame.dispose(); } }); } catch (InterruptedException ex) { ex.printStackTrace(); } } }
您需要在后台线程或主应用程序线程中运行该作业。 应使用SwingUtilities.invokeLater();
在Event Dispatch Thread(EDT)上创建JDialog和JFrame SwingUtilities.invokeLater();
。 然后等到作业完成并关闭(EDT)中的对话框。 由于布局和作业位于两个独立的线程上,因此应该没有问题。 实际上,您的示例的以下修改有效:
public static void main(String[] args) throws Exception { final JFrame myFrame = new JFrame("Main frame"); final JDialog d = new JDialog(myFrame, "I'm waiting"); final Thread backgroundJob = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(5000); // simulate process } catch (InterruptedException ex) { Logger.getLogger(NewClass.class.getName()).log(Level.SEVERE, null, ex); } } }); backgroundJob.start(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); myFrame.setSize(750, 500); myFrame.setLocationRelativeTo(null); myFrame.setVisible(true); d.add(new JLabel("Please wait...")); d.setSize(300, 200); d.setLocationRelativeTo(null); d.setVisible(true); } }); backgroundJob.join(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { d.setVisible(false); } }); }