首页 > 解决方案 > 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;
}
}

感谢您的时间

标签: javaswingjframe

解决方案


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。我拥有的不是最少的代码实际上是这样的

这就是像interfaces 这样的东西真正派上用场的地方。您“字符串”生成类“可以”返回结果文本,或者您可以将引用传递给interface用于“显示”它的实现。这样,您SwingWorker就可以充当 the 的消费者String并通过该publish方法传递它。

有许多非常重要的概念需要理解。

  • 你想解耦你的代码。这使得在不影响其他部分的情况下更容易更改代码的某些部分
  • 您希望能够“编码到接口,而不是实现”。这与第一条评论密切相关。基本上,您希望尽可能“隐藏”实现细节 - 原因有很多,但它有助于保持代码精简,有助于使后续更易于理解,并阻止代码的一部分访问它真正拥有的另一部分没有责任这样做(字符串生成真的负责更新状态栏吗?恕我直言 - 不是真的)
  • 还有大量的设计模式可以让解决问题变得更容易。我已经提到了“生产/消费者”的概念,但这只是一个

“事件调度线程”


推荐阅读