我要开发一个swing应用程序,我使用的是 MVC 模式的简化版本,其中控制器插入到视图和模型之间的两个方向:
复杂的应用程序可能会请求运行不同的线程:例如,如果必须在磁盘上写入文件,则可以启动后台线程,并且在写入文件结束时,此线程应发送通知以查看通过控制器。在这种情况下,可能会发生多个线程想要刷新视图,因此我认为应该以某种方式处理此问题。
我从this answer中获取灵感,以便编写以下SSCCE的方法appendText
和updateLastText
。
public class NewJFrame extends javax.swing.JFrame {
private volatile String lastText;
/**
* Creates new form NewJFrame
*/
public NewJFrame() {
initComponents();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
jScrollPane1 = new JScrollPane();
jTextArea1 = new JTextArea();
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jTextArea1.setEditable(false);
jTextArea1.setColumns(20);
jTextArea1.setRows(5);
jScrollPane1.setViewportView(jTextArea1);
getContentPane().add(jScrollPane1, BorderLayout.CENTER);
pack();
}// </editor-fold>
// Variables declaration - do not modify
private JScrollPane jScrollPane1;
private JTextArea jTextArea1;
// End of variables declaration
/**
*
* @param text
*/
public void appendText(final String text) {
updateLastText(text);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
jTextArea1.append(String.format("%s\n", lastText));
}
});
}
/**
*
* @param text
*/
private synchronized void updateLastText(String text) {
lastText = text;
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
final NewJFrame frame = new NewJFrame();
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
frame.setVisible(true);
}
});
Thread counter = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Logger.getLogger(NewJFrame.class.getName()).log(Level.SEVERE, null, ex);
}
frame.appendText((new Date()).toString());
}
}
};
counter.start();
}
}
上述解决方案似乎工作正常,我应该使用相同的技术来更新UI的其他swing组件吗?还是有更紧凑的解决方案?
答案 0 :(得分:3)
如果您正在运行多线程swing应用程序,那么您需要确保所有UI交互都在event dispatch thread上进行。 Swing类本身并不是线程安全的,它们只是安全的,因为它们仅限于一个线程(事件派发线程)。
答案 1 :(得分:2)
关于是否应该对所有Swing组件更新使用此方法的一般问题:是的,您应该这样做。
关于是否有更紧凑的解决方案的问题:我没有意识到这一点。一般情况下有事件处理的基础结构(例如Guava EventBus),但是没有一个能够从仔细考虑哪个线程什么(并且Swing组件上的所有操作都由EDT完成,特别是)。
这些调用只能稍微“更紧凑”,因为Runnable
通常可以在Java 8中写成lambda。
附注:您应该决定使用EventQueue
或SwingUtlities
。我更喜欢后者,但这些类的invokeLater
方法是等价的。只是交替使用它们可能会令人困惑。
另一方面注意:有时候检查意图行动是否已经在事件派遣线程上执行是值得的。例如,您可以考虑编写这样的一种或另一种方法:
public void appendText(final String text)
{
executeOnEventDispatchThread(() -> textField.setText(text));
}
private static void executeOnEventDispatchThread(Runnable runnable)
{
if (SwingUtilities.isEventDispatchThread())
{
runnable.run();
}
else
{
SwingUtilities.invokeLater(runnable);
}
}
在事件派发线程上调用appendText
时,它将直接在文本字段中设置文本。当从不同的线程调用它时,它将放置任务以更新事件队列上的文本。
编辑以回复评论
有时候从正确的线程进行某种修改是“显而易见的”。如果要在文本字段中附加文本,例如,从附加到按钮的actionPerformed
ActionListener
方法中添加文本,则此actionPerformed
方法将在事件派发线程 - 所以没有必要考虑线程问题。
但有时您将GUI直接或间接作为侦听器附加到某些数据模型。并且您不知道在此数据模型上发生哪些线程修改。例如:想象一个简单的数据模型
class Model {
void addModelListener(ModelListener listener) { ... }
String getValue() { ... }
void setValue(String newValue) {
....
// Notify all ModelListeners here...
}
}
现在,一些GUI组件附加到此,以便在某些标签中显示“值”:
model.addModelListener( event -> label.setText(model.getValue()) );
只要模型仅在Event Dispatch Thread 上修改,这就完全没问题了。例如:
someButton.addActionListener( event -> model.setValue("42") );
按下该按钮时,将在事件发送线程上执行ActionListener
。对setValue
的调用将依次通知事件调度线程中的所有ModelListener
。最后,ModelListener
将在事件调度线程上调用label.setText(...)
。一切都很好。
但是现在有人更改了逻辑:他获取了model
实例,并对不同的线程中的值进行了修改:
Thread t = new Thread(() -> model.setValue("123"));
t.start();
从来电者的角度来看,这非常好。他甚至可能不知道此应用程序中存在Swing GUI 。但setValue
调用将在新创建的线程上通知所有ModelListeners
,其中一个调用label.setText(...)
- 现在也在错误的线程上。
因此,应该清楚地记录线程约束,并且毫无疑问,可以将修改包含在GUI中,例如我在上面概述的executeOnEventDispatchThread
方法。