将代码从 ActionListener 移动到 main()

Moving code from ActionListener to main()

问题:

我有以下 java.awt.Button 代码:

Button btn = new Button("abcde");
btn.addActionListener((ActionEvent e) ->
{
    String s = btn.getLabel().toUpperCase();    
    btn.setLabel(s);    
});

我需要将 btn.addActionListener 中的代码移动到 public static void main(String[] args),如下所示(伪代码):

Button btn = new Button("abcde");
btn.addActionListener((ActionEvent e) ->
{
    notify_main()_that_button_had_been_clicked();  
});

public static void main(String[] args)
{
    block_until_button_clicked();

    String s = UI.getButton().getLabel();
    s = s.toUpperCase();
    UI.getButton().setLabel(s);
}

相关信息:

我知道 GUI 开发有更好的解决方案,但我仅限于将 AWT 用于 UI。

我无权更改以上内容,由于法律限制,我也无法提供有关真实代码的详细信息。

为了解决上述问题,我提交了以下 MVCE。请根据它回答:

import java.awt.Frame;
import java.awt.Button;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class ResponsiveUI extends Frame
{
    public final Button btn = new Button("abcde");

    public ResponsiveUI()
    {
        add(btn);

        btn.addActionListener((ActionEvent e) ->
        {
            String s = btn.getLabel().toUpperCase();    
            btn.setLabel(s);    
        });
    }

    public static void main(String[] args)
    {
        ResponsiveUI rui = new ResponsiveUI();
        rui.addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent we)
            {
                System.exit(0);
            }
        });
        rui.setSize(250, 150);
        rui.setResizable(false);
        rui.setVisible(true);
    }
}

我为解决这个问题所做的努力:

我广泛使用了 Google,并且能够找到一些有用的链接。

  1. UI 将 运行 在单独的线程中,这将使它响应(不过我不知道如何正确 join() 它)。
  2. 对于信号机制,wait()notify() 似乎是正确的方法。
  3. 要设置按钮的文本,我可以使用 EventQueue.InvokeAndWait
  4. 要获取 Button 的文本,我不知道该怎么做,但我有一个丑陋的解决方法。

下面是修改后的 MVCE :

import java.awt.Frame;
import java.awt.Button;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;

public class ResponsiveUI extends Frame
{

    public final Object lock = new Object();  // POINT #2 : signaling mechanism
    public final Button btn = new Button("abcde");

    public ResponsiveUI()
    {
        add(btn);

        btn.addActionListener((ActionEvent e) ->
        {
            // POINT #2 : signal the main() thread that button is clicked
            synchronized (lock)
            {
                lock.notify();
            } 
        });
    }

    public static void main(String[] args)
    {
        ResponsiveUI rui = new ResponsiveUI();

        // POINT #1: put UI into separate thread, so we can keep it responsive
        // POINT #1: I still do not know how to properly join() (it works OK though)
        Runnable r = () ->
        {
            rui.addWindowListener(new WindowAdapter()
            {
                @Override
                public void windowClosing(WindowEvent we)
                {
                    System.exit(0);
                }
            });
            rui.setSize(250, 150);
            rui.setResizable(false);
            rui.setVisible(true);
        };

        EventQueue.invokeLater(r);

        try
        {
            synchronized (rui.lock)    // POINT #2  
            {                          // POINT #2 
                rui.lock.wait();       // POINT #2 : wait for button press

                final Button b = new Button();  // POINT #4 : EventQueue uses final local variables
                                                // store text into temp button (ugly but works)
                EventQueue.invokeAndWait(() ->  // POINT #4
                {
                    b.setLabel(rui.btn.getLabel());  
                });
                // we could do all kind of things, but for illustrative purpose just transform text into upper case
                EventQueue.invokeAndWait(() ->    // POINT #3 :
                {
                    rui.btn.setLabel(b.getLabel().toUpperCase());

                });
            }
        }
        catch (InterruptedException | InvocationTargetException ex)
        {
            System.out.println("Error : " + ex);
        }
    }
}

一般方法是您需要与 main 方法和 ActionListener 实现共享一些锁或信号量。一旦你有一个共享信号量,main 方法可以阻塞(等待)信号量并且 ActionListener 可以通知信号量。

在伪代码中:

Lock lock = new Lock();

Button btn = new Button("abcde");
btn.addActionListener((ActionEvent e) -> {
    lock.notify();
});

public static void main(String[] args) {

    lock.waitUntilNotified();

    String s = UI.getButton().getLabel();
    s = s.toUpperCase();
    UI.getButton().setLabel(s);
}

我已将确切的信号量实现留给您,因为您的应用程序的性质将决定哪种机制最有效。例如,也许你想使用一个简单的对象并在其上调用 waitnotify 方法,或者如果你需要对加载到队列中的元素执行处理,你可以使用阻塞队列(即,项目排队,按下按钮表示现在应该处理排队的项目)。

您可以在此处找到有关如何实施 waitnotify 方法的信息:

共享锁可能很困难,因为将锁作为方法参数传递不是 main 方法中的一个选项。一种简单的方法(在这种情况下不会是首选)是将锁创建为 static 变量:

public class Application {

    public static final Lock LOCK = new Lock();

    public static void main(String[] args) {

        LOCK.waitUntilNotified();

        String s = UI.getButton().getLabel();
        s = s.toUpperCase();
        UI.getButton().setLabel(s);
    }
}

其他一些class:

public class SomeOtherClass {

    public void doSomething() {
        Button btn = new Button("abcde");
        btn.addActionListener((ActionEvent e) -> {
            Application.LOCK.notify();
        });
    }
}

切记 Lock 只是伪代码,应替换为您决定使用的锁的任何实现。

我使用了 CountDownLatch 来简化所有锁定系统。

import java.awt.Frame;
import java.awt.Button;
import java.awt.EventQueue;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.CountDownLatch;

public class ResponsiveUI extends Frame
{
    private static final CountDownLatch LOCK = new CountDownLatch(1);

    public final Button btn = new Button("abcde");

    public ResponsiveUI()
    {
        add(btn);

        btn.addActionListener(ae -> LOCK.countDown());
    }

    public static void main(String[] args)
    {
        ResponsiveUI rui = new ResponsiveUI();
        rui.addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent we)
            {
                System.exit(0);
            }
        });
        rui.setSize(250, 150);
        rui.setResizable(false);
        rui.setVisible(true);
        try {
            LOCK.await();
            String s = rui.btn.getLabel().toUpperCase();
            EventQueue.invokeAndWait(() ->
            {
                rui.btn.setLabel(s);
            });
        }
        catch (InvocationTargetException | InterruptedException ex)
        {
            System.out.println("Error : " + ex);
        }
    }
}

变化:

public final Button btn = new Button("abcde");

至:

public static final Button btn = new Button("abcde");

并移动:

btn.addActionListener((ActionEvent evt) -> {
    String s = btn.getLabel().toUpperCase();
    btn.setLabel(s);
});

在主要方法中。

为什么首先要弄乱线程? AWT/Swing 应用程序中的线程有一些 "conventions" 而你似乎离他们很远。

为什么不使用 getter 来访问 main 层中的按钮和动作侦听器?

第一个解决方案。干净利落,大家都看得懂(组件实例中没有public static final修饰符)":

public class ResponsiveUI extends Frame {

    private Button button;

    public ResponsiveUI() {
        super("UI");
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(1);
            }
        });

        button = new Button("something");
        add(button);
    }

    public Button getButton() {
        return button;
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            ResponsiveUI ui = new ResponsiveUI();

            Button button = ui.getButton();
            button.addActionListener(e -> {
                button.setLabel(button.getLabel().toUpperCase());
            });

            ui.setSize(250, 150);
            ui.setResizable(false);
            ui.setVisible(true);
        });
    }
}

第二种解决方案,使用某种依赖注入并避免按钮使用 getter:

public class ResponsiveUI extends Frame {

    private Button button;

    public ResponsiveUI(ActionListener buttonListener) {
        super("UI");
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(1);
            }
        });

        button = new Button("something");
        button.addActionListener(buttonListener);
        add(button);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            ActionListener changeLabelToUppercaseListener = e -> {
                Object source = e.getSource();
                if (source instanceof Button) {
                    Button b = (Button) source;
                    b.setLabel(b.getLabel().toUpperCase());
                }
            };

            ResponsiveUI ui = new ResponsiveUI(changeLabelToUppercaseListener);

            ui.setSize(250, 150);
            ui.setResizable(false);
            ui.setVisible(true);
        });
    }
}

据我了解您的问题,您希望在单击 [AWT] 按钮时通知主线程,并在收到该通知后更改该按钮标签的文本。

我从您在问题中发布的修改后的 minimal-reproducible-example 代码开始。

这是我的代码,后面有注释。

import java.awt.Button;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class ResponsiveUI extends Frame {
    private static String  btnTxt;
    public final Object lock = new Object();
    public final Button btn = new Button("abcde");

    public ResponsiveUI() {
        add(btn);
        btn.addActionListener((ActionEvent e) -> {
            synchronized (lock) {
                btnTxt = e.getActionCommand();
                lock.notifyAll();
            } 
        });
    }

    public static void main(String[] args) {
        ResponsiveUI rui = new ResponsiveUI();

        Runnable r = () -> {
            rui.addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosing(WindowEvent we) {
                    System.exit(0);
                }
            });
            rui.setSize(250, 150);
            rui.setResizable(false);
            rui.setVisible(true);
        };
        EventQueue.invokeLater(r);
        synchronized (rui.lock) {
            try {
                rui.lock.wait();
                String newBtnTxt = btnTxt.toUpperCase();
                EventQueue.invokeLater(() -> rui.btn.setLabel(newBtnTxt));
            }
            catch (InterruptedException x) {
                x.printStackTrace();
            }
        }
    }
}

为了让GUI线程通知主线程,我同意你的看法wait()notifyAll()是使用的机制。我使用 notifyAll() 而不是 notify() 来确保通知 所有 等待线程。

我使用共享变量 btnTxt 在 GUI 线程和主线程之间传输按钮标签的文本。

默认情况下,ActionEvent 操作命令 是 [AWT] 按钮标签的文本。因此在我的 ActionListener 实现中使用方法 getActionCommand()

对 GUI 的所有更改都必须在 GUI 线程上执行,因此在主线程中为按钮标签创建新文本后,我使用 class 的方法 invokeLater() java.awt.EventQueue 实际更改按钮的标签文本。

您应该能够按原样复制上面的代码,编译它并运行它。