java - JAVA - 从动作侦听器调用时 JFrame 看起来不正确
问题描述
当按下按钮时,我的一个框架看起来不像它应该有的那样有问题。
框架看起来好像渲染不正确,其中的标签文本被缩短了,但是当我将同一行代码移到动作侦听器之外时,它可以正常工作。
我有一个主菜单,有两个按钮,目前只有生成菜单有效,它看起来像这样:
https://i.imgur.com/k1Ne5v9.png
动作监听器的代码:
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
mF.dispose();
MenuGenerator.generateTheMenu();
}
});
结果看起来不对:https ://i.imgur.com/n86y4CD.png 框架也没有响应,点击 X 没有,但它应该关闭框架和应用程序。
但是将代码更改为:
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
//mF.dispose();
}
});
MenuGenerator.generateTheMenu();
产生正确的外观:https ://i.imgur.com/TFbkmAO.png
“主菜单”的代码
public static void openMainMenu() {
Font menuFont = new Font("Courier",Font.BOLD,16);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
mF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mF.setSize(465,230);
mF.setLocation(dim.width/2-mF.getSize().width/2, dim.height/2-mF.getSize().height/2);
mF.getContentPane().setBackground(Color.WHITE);
Color blueSteel = new Color(70,107,176);
JPanel p = new JPanel();
p.setSize(600,50);
p.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
p.setLocation((mF.getWidth() - p.getWidth()) /2, 20);
p.setBackground(blueSteel);
JLabel l = new JLabel("Welcome to the menu GENERATORRRR");
l.setFont(menuFont);
l.setForeground(Color.WHITE);
p.add(l, gbc);
JButton runMenuButt = new JButton("Generate Menu");
runMenuButt.setLocation(20 , 90);
JButton manageRecipButt = new JButton("Manage Recipients");
manageRecipButt.setLocation(240 , 90);
menuUtilities.formatButton(runMenuButt);
menuUtilities.formatButton(manageRecipButt);
mF.setResizable(false);
mF.setLayout(null);
mF.add(runMenuButt);
mF.add(manageRecipButt);
mF.add(p);
mF.setVisible(true);
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
//mF.dispose();
}
});
MenuGenerator.generateTheMenu();
manageRecipButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, "Not supported yet", "Function not yet available",JOptionPane.INFORMATION_MESSAGE);
}
});
//System.out.println(mF.getContentPane().getSize());
}
和状态栏:
public class StatusBar {
private static JLabel statusLabel= new JLabel("Starting");
private static JFrame statusFrame = new JFrame("Generation Status");
public static void createStatusBar() {
Font menuFont = new Font(Font.MONOSPACED,Font.BOLD,20);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
statusFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
statusFrame.setSize(700,100);
JPanel p = new JPanel();
p.setPreferredSize(new Dimension(100,100));
statusLabel.setFont(menuFont);
p.add(statusLabel);
statusFrame.add(p,BorderLayout.CENTER);
statusFrame.setLocation(dim.width/2-statusFrame.getSize().width/2, dim.height/2-statusFrame.getSize().height/2);
statusFrame.setVisible(true);
}
public static void setStatusBar(String statusText) {
statusLabel.setText(statusText);
statusLabel.paintImmediately(statusLabel.getVisibleRect());
statusLabel.revalidate();
}
public static void closeStatusBar(){
statusFrame.dispose();
}
}
我用这一行创建栏:StatusBar.createStatusBar();
为什么在 MenuGenerator.generateTheMenu(); 时状态栏无法正确呈现 从动作监听器调用?
以下是为任何想要测试它的人重现此行为的最小代码:它还使用已发布的 StatusBar 类。
public class MinimalClass {
private static JFrame mF = new JFrame("Main Menu");
public static void main(String[] args) {
openMainMenu();
}
public static void openMainMenu() {
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
mF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mF.setSize(465,230);
mF.setLocation(dim.width/2-mF.getSize().width/2, dim.height/2-mF.getSize().height/2);
mF.getContentPane().setBackground(Color.WHITE);
JButton runMenuButt = new JButton("Generate Menu");
runMenuButt.setLocation(20 , 90);
runMenuButt.setSize(200 , 85);
mF.setResizable(false);
mF.setLayout(null);
mF.add(runMenuButt);
mF.setVisible(true);
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
mF.dispose();
generateTheMenu();
}
});
}
public static void generateTheMenu() {
System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider");
String rawMenuOutput = "";
try {
rawMenuOutput= getMenuInJavaNow();
} catch (Exception e){
System.out.println("Something went terribly wrong");
}
System.out.println(rawMenuOutput);
}
public static String getMenuInJavaNow() throws IOException {
String rawMenuOutput = "Restaurant Menu" ;
rawMenuOutput = rawMenuOutput + "Test line";
String []menuOtpArr = new String [3];
try {
StatusBar.createStatusBar();
TimeUnit.SECONDS.sleep(2);
StatusBar.setStatusBar("Test1");
TimeUnit.SECONDS.sleep(2);
menuOtpArr[0]="Test line";
StatusBar.setStatusBar("Test2");
TimeUnit.SECONDS.sleep(2);
menuOtpArr[1]="Test line";
StatusBar.setStatusBar("Test3");
TimeUnit.SECONDS.sleep(2);
menuOtpArr[2]="Test line";
StatusBar.setStatusBar("Test4");
TimeUnit.SECONDS.sleep(2);
StatusBar.closeStatusBar();
} catch (Exception e) {
}
for (int i=0;i < menuOtpArr.length;i++) {
rawMenuOutput = rawMenuOutput + "\n\n" +menuOtpArr[i];
}
return rawMenuOutput;
}
}
感谢您的时间
解决方案
statusLabel.paintImmediately(statusLabel.getVisibleRect());
似乎掩盖了一个更大的问题。
问题是,Swing 是单线程的(不是线程安全的)。这意味着当您TimeUnit.SECONDS.sleep(2);
从内部调用时getMenuInJavaNow
,由 调用,由generateTheMenu
调用ActionListener
,它是在事件调度线程的上下文中调用的。
这使 EDT 进入睡眠状态,这意味着它没有(正确地)处理布局或绘制请求
首先阅读Swing 中的并发以了解更多详细信息
现在,你有一个更大的问题,如何解决它。对于这个问题的答案,我们需要比目前可用的更多的上下文。
似乎正在返回一些值,getMenuInJavaNow
我不确定到底是什么。
“A”解决方案是使用 a SwingWorker
(有关更多详细信息,请参阅Worker Threads 和 SwingWorker)。它提供了在后台执行长时间运行任务的能力,还提供了将更新同步回 UI 的方法,例如...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
public class MinimalClass {
private static JFrame mF = new JFrame("Main Menu");
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
openMainMenu();
}
});
}
public static void openMainMenu() {
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
mF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mF.setLocationRelativeTo(null);
mF.getContentPane().setBackground(Color.WHITE);
JButton runMenuButt = new JButton("Generate Menu");
runMenuButt.setMargin(new Insets(25, 25, 25, 25));
JPanel buttons = new JPanel(new GridLayout(0, 2));
buttons.add(runMenuButt);
mF.add(buttons);
mF.pack();
mF.setVisible(true);
runMenuButt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Generate Menu pressed");
mF.dispose();
generateTheMenu();
}
});
}
public static void generateTheMenu() {
System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider");
StatusBar.createStatusBar();
SwingWorker<String, String> worker = new SwingWorker<String, String>() {
@Override
protected String doInBackground() throws Exception {
String rawMenuOutput = "Restaurant Menu";
rawMenuOutput = rawMenuOutput + "Test line";
String[] menuOtpArr = new String[3];
try {
TimeUnit.SECONDS.sleep(2);
publish("1234567890123456789012345678901234567890");
TimeUnit.SECONDS.sleep(2);
publish("This is a test");
TimeUnit.SECONDS.sleep(2);
publish("More testing");
TimeUnit.SECONDS.sleep(2);
publish("Still testing");
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
}
for (int i = 0; i < menuOtpArr.length; i++) {
rawMenuOutput = rawMenuOutput + "\n\n" + menuOtpArr[i];
}
return rawMenuOutput;
}
@Override
protected void done() {
StatusBar.closeStatusBar();
}
@Override
protected void process(List<String> chunks) {
StatusBar.setStatusBar(chunks.get(chunks.size() - 1));
}
};
worker.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (worker.getState() == SwingWorker.StateValue.DONE) {
try {
String result = worker.get();
System.out.println(result);
StatusBar.closeStatusBar();
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
}
}
});
worker.execute();
}
public static class StatusBar {
private static JLabel statusLabel = new JLabel("Starting");
private static JFrame statusFrame = new JFrame("Generation Status");
public static void createStatusBar() {
Font menuFont = new Font(Font.MONOSPACED, Font.BOLD, 20);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
statusFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
statusFrame.setSize(700, 100);
JPanel p = new JPanel();
p.setBackground(Color.RED);
// p.setPreferredSize(new Dimension(100, 100));
statusLabel.setFont(menuFont);
p.add(statusLabel);
statusFrame.add(p, BorderLayout.CENTER);
statusFrame.setLocationRelativeTo(null);
statusFrame.setVisible(true);
}
public static void setStatusBar(String statusText) {
statusLabel.setText(statusText);
}
public static void closeStatusBar() {
statusFrame.dispose();
}
}
}
观察...
static
不是你的朋友,尤其是在这种情况下。你真的,真的,真的需要学会在没有它的情况下生活。setLayout(null)
对你没有任何好处,尤其是从长远来看。花点时间在容器中布局组件并开始正确使用布局管理器,它们可能看起来“复杂”,但它们会让你免于大量脱发- 尽可能避免使用
setPreferred/Minimum/MaximumSize
,您正在剥夺组件提供有用渲染提示的能力,这些提示可能会跨平台和渲染管道发生变化
只是一个快速跟进的问题, done 和 addPropertyListener 有什么区别?有没有?两者都用不是多余的吗?
这里的示例非常基本,对我来说,我习惯于done
处理SwingWorker
“知道”需要做什么,但是它不知道要对结果做什么。
我用PropertyChangeListener
来处理这个问题 - 重点 - 这是一个例子。
而且我还注意到,我不需要实际发布,因为调用 StatusBar.setStatusBar(""); 也可以。有必要使用发布吗?
总之是。Swing 不是线程安全的,StatusBar.setStatusBar("")
直接调用会导致一些奇怪和意想不到的结果。 publish
将调用推送到事件调度线程中,从而可以安全地从内部更新 UI。
我有用于生成我想在另一个类中设置为状态栏标题的字符串的代码,而不是在 generateTheMenu 中,因此我更方便的是简单地调用 .setStatusBar。我拥有的不是最少的代码实际上是这样的
这就是像interface
s 这样的东西真正派上用场的地方。您“字符串”生成类“可以”返回结果文本,或者您可以将引用传递给interface
用于“显示”它的实现。这样,您SwingWorker
就可以充当 the 的消费者String
并通过该publish
方法传递它。
有许多非常重要的概念需要理解。
- 你想解耦你的代码。这使得在不影响其他部分的情况下更容易更改代码的某些部分
- 您希望能够“编码到接口,而不是实现”。这与第一条评论密切相关。基本上,您希望尽可能“隐藏”实现细节 - 原因有很多,但它有助于保持代码精简,有助于使后续更易于理解,并阻止代码的一部分访问它真正拥有的另一部分没有责任这样做(字符串生成真的负责更新状态栏吗?恕我直言 - 不是真的)
- 还有大量的设计模式可以让解决问题变得更容易。我已经提到了“生产/消费者”的概念,但这只是一个
“事件调度线程”
推荐阅读
- java - Hibernate从数据库中删除记录?
- html - 如何覆盖内联 CSS 背景色不透明度,而不是颜色?
- php - 生成 XML 签名
- sql-server - 无法使用 SQL Server 连接到 Azure VM
- c - 在非结构或联合的情况下请求成员“nama”
- flowtype - 获取带流的函数组合的输出类型
- vba - 由于升级到 Access 2013 OutputTo 命令(到 PDF)不起作用
- regex - 正则表达式不会用边界条件替换完全匹配
- reactjs - componentWillRecieveProps 与 getDerivedStateFromProps
- sql-server-2012 - 在多个数据库中搜索特定列名的表