如何使用多线程连续更新图形用户界面(GUI)。

huangapple go评论57阅读模式
英文:

How to update GUI continuously with multithreading

问题

我已经制作了一个我完整程序的最小表示,以展示我需要帮助的地方,该程序由一个JFrame、主类和一个任务类组成。

任务类生成随机整数,用于在GUI上绘制字符串。我使得用户可以使用任意数量的线程来执行相同的任务。

我的目标是显示“解决方案”,目前还不是真正的解决方案。但是,随着每个线程生成整数值,我希望不断更新GUI,以便它可以继续显示解决方案,直到达到最终的“真正”解决方案。

我该如何做到这一点?

当我尝试通过GUI传递时,它只更新一次。我尝试过查阅Swing Worker,但我想使用自己的线程实现,我无法弄清楚如何正确实现它,或者在并发实现中它会如何工作。

import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Task extends Thread {

    private static Lock taskLock = new ReentrantLock();
    private Random rand = new Random();
    private static int y;
    static volatile ArrayList<Integer> finallist = new ArrayList<>();
    private GUI g;

    Task(GUI g) {
        this.g = g;
    }

    private void generatePoint() {
        taskLock.lock();
        y = rand.nextInt(1000);
        taskLock.unlock();
    }

    public void run() {
        generatePoint();
        int copy = y;
        finallist.add(copy);
        g.setData(copy);
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {

        GUI g = new GUI();
        g.setVisible(true);
        Scanner input = new Scanner(System.in);
        System.out.println("Please input the number of Threads you want to create: ");
        int n = input.nextInt();
        System.out.println("You selected " + n + " Threads");

        Thread[] threads = new Thread[n];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Task(g);
            threads[i].start();
        }

        for (int j = 0; j < n; j++) {
            threads[j].join();
        }

        for(Integer s: Task.finallist) {
            if(s > 30) {
                g.setData(s);       //in full program will find true solution
            }
        }
    }
}

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class GUI extends JFrame implements ActionListener {
    private boolean check;
    private int y;
    GUI()  {
        initialize();
    }

    private void initialize()  {
        check = false;
        y = 0;
        this.setLayout(new FlowLayout());
        this.setSize(1000,1000);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    @Override
    public void actionPerformed(ActionEvent e) {

    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        if(check) {
            g.drawString("here",500,y);
        }
    }

    void setData(int y) {
        check = true;
        this.y = y;
        repaint();
    }

}
英文:

I have made a minimal representation of my full program to show what I need help with, the program consists of a JFrame, the main class, and a task class.

The task class generates random integers that are used to paint strings onto the GUI. I made it so that the user can use any number of threads that each perform this same task.

My goal is to display the ending "solution" which for now is not a real solution. But, as each thread generates integer values, I want to continuously update the GUI so that it can keep showing solutions until it reaches the final "true" solution.

How can I do this?

When I try to pass the GUI through it only updates once. I tried looking into Swing Workers but I want to use my own thread implementation, and I cannot figure out how to implement it correctly, or how exactly it would work in a concurrent implementation.

import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Task extends Thread {
private static Lock taskLock = new ReentrantLock();
private Random rand = new Random();
private static int y;
static volatile ArrayList&lt;Integer&gt; finallist = new ArrayList&lt;&gt;();
private GUI g;
Task(GUI g) {
this.g = g;
}
private void generatePoint() {
taskLock.lock();
y = rand.nextInt(1000);
taskLock.unlock();
}
public void run() {
generatePoint();
int copy = y;
finallist.add(copy);
g.setData(copy);
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
GUI g = new GUI();
g.setVisible(true);
Scanner input = new Scanner(System.in);
System.out.println(&quot;Please input the number of Threads you want to create: &quot;);
int n = input.nextInt();
System.out.println(&quot;You selected &quot; + n + &quot; Threads&quot;);
Thread[] threads = new Thread[n];
for (int i = 0; i &lt; threads.length; i++) {
threads[i] = new Task(g);
threads[i].start();
}
for (int j = 0; j &lt; n; j++) {
threads[j].join();
}
for(Integer s: Task.finallist) {
if(s &gt; 30) {
g.setData(s);       //in full program will find true solution
}
}
}
}
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class GUI extends JFrame implements ActionListener {
private boolean check;
private int y;
GUI()  {
initialize();
}
private void initialize()  {
check = false;
y = 0;
this.setLayout(new FlowLayout());
this.setSize(1000,1000);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void paint(Graphics g) {
super.paint(g);
if(check) {
g.drawString(&quot;here&quot;,500,y);
}
}
void setData(int y) {
check = true;
this.y = y;
repaint();
}
}
</details>
# 答案1
**得分**: 3
重要的一点是,您需要记住的一个方面是 Swing 不是线程安全的,这意味着您不应该从事件分派线程之外的上下文中更新UI或更新UI所依赖的任何值。
处理这个问题有几种方法。其中一种是使用 `SwingUtilities.invokeLater`,但只有在调用次数较少或它们之间的时间相对较长(几秒钟)时才应使用。主要原因是很容易使事件分派线程负载过重,从而导致UI“停滞”。
另一种方法是使用 Swing 的 `Timer`,但这实际上只在您希望定期更新UI时(如动画)才有用。
还有一种方法是使用 `SwingWorker`。这是一个围绕 `Executor` 的“高级”包装器,允许您在后台线程上执行工作,然后将结果“发布”到事件分派线程,然后可以安全地进行UI处理。
但是为什么要这样做呢?`SwingWorker` 的一个功能是,如果您的线程在短时间内生成了大量数据,它会将许多结果压缩到一个 `List` 中,并减少回到UI的调用次数,从而有助于保持一定的性能水平。
然而,在您的情况下,这可能只是一个很好的练习,因为您正在创建一个单独的线程/worker来执行单一的任务,而不是让一个线程/worker 执行所有的任务。
在这种情况下,我可能会考虑使用 Swing 的 `Timer` 来探查数据模型,而不是让线程/worker 尝试直接更新UI,这样您就可以控制UI发生的更新次数。
对于所有这些方法都有利弊,您需要调查哪种方法最适合您的特定场景。
# `SwingWorker` 示例
```java
import ... // 省略导入部分
public class GUI {
// 省略其他部分...
public static class WorkerTask extends SwingWorker<Void, Integer> {
// 省略部分内容...
@Override
protected Void doInBackground() throws Exception {
// 稍作延迟,您现在应该能看到文本变化
Thread.sleep(rand.nextInt(1000));
publish(generatePoint());
return null;
}
@Override
protected void process(List<Integer> chunks) {
if (chunks.isEmpty()) {
return;
}
int last = chunks.get(chunks.size() - 1);
consumer.setData(last);
}
}
// 省略其他部分...
}

我建议您参考以下资源:

英文:

One of the important aspects you need to keep in mind is that Swing is not thread safe, this means you should not update the UI or update any value that the UI is reliant on from outside of the context Event Dispatching Thread.

There are a number of ways you deal with this. SwingUtilities.invokeLater, but you really should only use it if the number of times it would be called are small or the time between them is relatively larger (seconds). The main reason for this, is it's very easy to overload the EDT, which can cause the UI to "stall".

Another approach is to use Swing Timer, but this is really only useful if you want to update the UI on a regular based (like animation).

Another approach is to use a SwingWorker. This is a "fancy" wrapper around a Executor which allows you to perform work on a background thread and then publish the results to the EDT which can then safely be processed for the UI.

But why do this? Well, one of things that SwingWorker does is, if your Thread is generating a lot of data in a small amount of time, it will squeeze a number of results into a List and reduce the number of calls made back to the UI, helping maintain some level of performance.

However, in your case, it's probably just a really nice exercise, as you are creating a single thread/worker to do a single job and not getting a single worker/thread to do all the jobs.

In that case, I might consider using a Swing Timer to probe a model of the data instead of having the Thread/worker try and update the UI itself, this way you gain control over the number of updates which are occurring to the UI.

There are pros and cons to all these and you will need to investigate which one(s) work best for your particular scenario

SwingWorker example

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
public class GUI {
public static void main(String[] args) {
new GUI();
}
public GUI() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane(10));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public static interface Consumer {
void setData(int value);
}
public static class WorkerTask extends SwingWorker&lt;Void, Integer&gt; {
private static Lock taskLock = new ReentrantLock();
private Random rand = new Random();
private static int y;
// I&#39;d prefer this was a model object passed into the worker
static volatile ArrayList&lt;Integer&gt; finallist = new ArrayList&lt;&gt;();
private Consumer consumer;
public WorkerTask(Consumer consumer) {
this.consumer = consumer;
}
private int generatePoint() {
taskLock.lock();
int copy = 0;
try {
copy = rand.nextInt(200);
y = copy;
// Array list isn&#39;t thread safe ;)
finallist.add(copy);
} finally {
taskLock.unlock();
}
return copy;
}
@Override
protected Void doInBackground() throws Exception {
// And a random delay
// You should now be able to see the text change
Thread.sleep(rand.nextInt(1000));
publish(generatePoint());
return null;
}
@Override
protected void process(List&lt;Integer&gt; chunks) {
if (chunks.isEmpty()) {
return;
}
int last = chunks.get(chunks.size() - 1);
consumer.setData(last);
}
}
public class TestPane extends JPanel {
private boolean check;
private int y;
private CountDownLatch latch;
public TestPane(int count) {
latch = new CountDownLatch(count);
for (int i = 0; i &lt; count; i++) {
WorkerTask task = new WorkerTask(new Consumer() {
@Override
public void setData(int value) {
// This is on the EDT, so it&#39;s safe to update the UI with
TestPane.this.setData(value);
}
});
task.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
WorkerTask task = (WorkerTask) evt.getSource();
if (task.getState() == SwingWorker.StateValue.DONE) {
latch.countDown();
}
}
});
task.execute();
}
// This only matters if you want the result to be delivered
// on the EDT.  You could get away with using a Thread otherwise
SwingWorker waiter = new SwingWorker&lt;Void, List&lt;Integer&gt;&gt;() {
@Override
protected Void doInBackground() throws Exception {
System.out.println(&quot;Waiting&quot;);
try {
latch.await();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(&quot;All done here, thanks&quot;);
publish(WorkerTask.finallist);
return null;
}
@Override
protected void process(List&lt;List&lt;Integer&gt;&gt; chunks) {
if (chunks.isEmpty()) {
return;
}
List&lt;Integer&gt; values = chunks.get(chunks.size() - 1);
completed(values);
}
};
waiter.execute();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
if (check) {
g2d.drawString(&quot;here&quot;, 100, y);
}
g2d.dispose();
}
void setData(int y) {
check = true;
System.out.println(y);
this.y = y;
repaint();
}
protected void completed(List&lt;Integer&gt; values) {
// And executed on the EDT
System.out.println(&quot;All your value belong to us&quot;);
for (int value : WorkerTask.finallist) {
System.out.println(&quot;&gt;&gt; &quot; + value);
}
}
}
}

I recommend looking at:

答案2

得分: 1

在我解释解决方案之前,我想指出一下你的代码存在一些问题。

  1. 每个线程中只执行一个操作,并且这个操作似乎不是一个长时间运行的操作(run 方法仅被调用一次,如果你不知道的话)。我不确定你是否只是为了将其作为占位符而放置在代码中,还是它实际上是你的代码的一部分。但如果没有长时间运行的操作,将其放在单独的线程中是否值得?请考虑权衡。
  2. 没有必要等待线程完成,因为在你的代码中它们可能会几乎立即完成。

现在来解决方案,这份文档解释了 Swing 不是线程安全的,但有一种方法可以在 GUI 上进行更改。你应该使用这种方法进行任何更改:SwingUtilities.invokeLater(..)

public static void main(String[] args) throws InterruptedException {

    GUI g = new GUI();
    g.setVisible(true);
    Scanner input = new Scanner(System.in);
    System.out.println("请输入要创建的线程数:");
    int n = input.nextInt();
    System.out.println("您选择了 " + n + " 个线程");

    Thread[] threads = new Thread[n];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Task(g);
        threads[i].start();
    }

    for (int j = 0; j < n; j++) {
        threads[j].join();
    }

    for(Integer s: Task.finallist) {
        if(s > 30) {
            // 这里你向事件分发线程提交了一个 Runnable,在该线程中运行 Swing 应用程序
            SwingUtilities.invokeLater(() -> {
                 g.setData(s);       
            });
        }
    }
}

在你的 run 方法中也应该这样做:

public void run() {
    generatePoint();
    int copy = y;
    finallist.add(copy);
    SwingUtilities.invokeLater(() -> {
        g.setData(copy);       
    });
}
英文:

Before I explain the solution to your problem, I would like to point out some issues with your code.

  1. You only do one operation in each thread, and it does not seem to be a long-running one (The run method is only called once, in case you don't know). I'm not sure if you just put it for the sake of having it as a placeholder, or if it's actually part of your code. But if you don't have a long running operation, is it worth doing it in a separate thread? Think about the trade-offs.
  2. No need to join the threads, as they will probably finish almost immediately in your code.

Now to the solution, this documentation explains that Swing is not thread-safe, but there is a way to make changes on the GUI. You should use this method to make any changes: SwingUtilities.invokeLater(..).

public static void main(String[] args) throws InterruptedException {
GUI g = new GUI();
g.setVisible(true);
Scanner input = new Scanner(System.in);
System.out.println(&quot;Please input the number of Threads you want to create: &quot;);
int n = input.nextInt();
System.out.println(&quot;You selected &quot; + n + &quot; Threads&quot;);
Thread[] threads = new Thread[n];
for (int i = 0; i &lt; threads.length; i++) {
threads[i] = new Task(g);
threads[i].start();
}
for (int j = 0; j &lt; n; j++) {
threads[j].join();
}
for(Integer s: Task.finallist) {
if(s &gt; 30) {
// here you submit a Runnable to the event-dispatcher thread in which the Swing Application runs
SwingUtilities.invokeLater(() -&gt; {
g.setData(s);       
});
}
}
}

The same should also be done in your run method:

public void run() {
generatePoint();
int copy = y;
finallist.add(copy);
SwingUtilities.invokeLater(() -&gt; {
g.setData(copy);       
});
}

huangapple
  • 本文由 发表于 2020年10月4日 05:41:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/64189280.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定