如何让 Java 正确地挥杆作画?

How To Get Java Swing To Paint Properly?

我有一个最小的应用程序来显示 Java Swing GUI 更新问题,似乎它没有正确绘制组件,错误如下所示:

这是我的程序:

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import java.util.concurrent.*;

public class Java_Test extends JPanel
{
  static JFrame frame=new JFrame("Java Test");
  static int W=800,H=260,Executor_Count=12;
  JPanel Progress_Panel=new JPanel(),Center_Panel;
  JButton Do_Test_Button=new JButton("Do Test");
  Get_Time Timer=new Get_Time();
  ThreadPoolExecutor executor;
  Progress_Bar Progress_bar;
  Timer Display_Timer=new Timer(1);

  public Java_Test()
  {
    setLayout(new BorderLayout());

    JPanel Top_Panel=new JPanel();
    Top_Panel.setPreferredSize(new Dimension(W-6,60));
    add("North",Top_Panel);

    Do_Test_Button.setFont(new Font("Times New Roman",0,16));
    Do_Test_Button.setBackground(new Color(118,198,250));
    Do_Test_Button.setForeground(new Color(0,28,218));
    Do_Test_Button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { Do_Test(); } });
    Top_Panel.add(Do_Test_Button);

    JPanel Center_Panel=new JPanel();
    Center_Panel.setPreferredSize(new Dimension(W-2,170));
    add("Center",Center_Panel);        

    Progress_Panel.setPreferredSize(new Dimension(W-2,160));
    Center_Panel.add(Progress_Panel);    

    JLabel Progress_Label=new JLabel("Progress");
    Progress_Label.setFont(new Font("Times New Roman",0,20));
    Progress_Label.setBackground(new Color(253,253,253));
    Progress_Label.setForeground(new Color(8,68,128));
    Progress_Label.setPreferredSize(new Dimension(W-20,53));
    Progress_Label.setHorizontalAlignment(SwingConstants.CENTER);
    Progress_Panel.add(Progress_Label);

    Progress_bar=new Progress_Bar(620,26);
    Progress_Panel.add(Progress_bar);

    JPanel Time_Used_Panel=new JPanel(new FlowLayout(FlowLayout.CENTER,6,26));
    Time_Used_Panel.setPreferredSize(new Dimension(W-20,60));
    Progress_Panel.add(Time_Used_Panel);

    JLabel Time_Used_Label=new JLabel("Time : ");
    Time_Used_Label.setFont(new Font("Times New Roman",0,14));
    Time_Used_Label.setForeground(new Color(0,0,238));
    Time_Used_Label.setHorizontalAlignment(SwingConstants.CENTER);
    Time_Used_Panel.add(Time_Used_Label);

    Display_Timer.setFont(new Font("Times New Roman",0,14));
    Display_Timer.setForeground(new Color(0,0,238));
    Display_Timer.setPreferredSize(new Dimension(50,17));
    Time_Used_Panel.add(Display_Timer);

    Clock clock=new Clock(0);
    clock.setFont(new Font("Monospaced",Font.PLAIN,16));
    clock.setBackground(new Color(0,110,220));
    clock.setForeground(new Color(250,250,250));
    clock.setOpaque(true);
    clock.setPreferredSize(new Dimension(288,30));
    clock.start();

    JPanel Bottom_Panel=new JPanel();
    Bottom_Panel.setPreferredSize(new Dimension(W-2,50));
    Bottom_Panel.add(clock);

    add("South",Bottom_Panel);
    setPreferredSize(new Dimension(W,H));
  }

  void Do_Test()
  {
    String Info="",Result;
    Out("Do_Test");
    try
    {
      Display_Timer.start();
      Timer.Start();
      Output_Time("[ 1 ]");
      Progress_bar.Set_Progress(1);

      int Task_Count=222;
      executor=new ThreadPoolExecutor(Executor_Count,Executor_Count*2,1,TimeUnit.SECONDS,new LinkedBlockingQueue());
      ArrayList<Future<String>> futures=new ArrayList<>(Task_Count);
      Test_Runner A_Runner;

      try
      {
        for (int i=0;i<Task_Count;i++)
        {
          A_Runner=new Test_Runner();
          futures.add(executor.submit(A_Runner));
        }
        executor.shutdown();
        while (!executor.isTerminated()) { executor.awaitTermination(100,TimeUnit.MILLISECONDS); }

        for (Future<String> future : futures)
        {
          Result=future.get();
          if (Result!=null) Info+=Result;
        }
      }
      catch (Exception e) { e.printStackTrace(); }
    }
    catch (Exception e) { e.printStackTrace(); }
    Output_Time("[ 2 ]");
    Progress_bar.Set_Progress(100);
    Out("Done");
    Display_Timer.stop();
  }

  String Output_Time(String Id)
  {
    Timer.End();
    String Time_Duration=Id+" : Time = "+Timer.Get_Duration_Hour_Minute_Second();
    Out(Time_Duration);
    return Time_Duration;
  }

  private static void Out(String message) { System.out.println(message); }

  static void Create_And_Show_GUI()
  {
    final Java_Test demo=new Java_Test();

    frame.add(demo);
    frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } });
    frame.pack();
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }

  public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { Create_And_Show_GUI(); } }); }
}

class Progress_Bar extends JPanel implements Runnable
{
  int W,H,Last_Progress=-99,Progress,counter=0,Unit_Size=20;
  static JProgressBar b=new JProgressBar();
  boolean Started_B=false,Do_Step_B=false;
  Thread Progress_Bar_Runner_Thread;

  public Progress_Bar(int W,int H)
  {
    setPreferredSize(new Dimension(W,H));
    b.setPreferredSize(new Dimension(W-20,H-8));
    b.setStringPainted(true);
    add(b);
    start();
  }

  public void Set_Progress(int Progress)
  {
    if (Progress==1 || (this.Progress<Progress && Progress<=100))
    {
      this.Progress=Progress;
      b.setValue(Progress);
      b.paintImmediately(0,0,b.getWidth(),b.getHeight());
      Out("        Progress = "+Progress+" %");
    }
    if (Progress==1) Started_B=true;
    else if (Progress>=100)
    {
      Started_B=false;
      Do_Step_B=false;
    }
  }

  public void run()
  {
    try
    {
      while (Progress<=100)
      {
        if ((Progress==0 || Progress==50 || Progress==100 || Do_Step_B) && Last_Progress!=Progress)
        {
          b.setValue(Progress);
//          revalidate();
          b.paintImmediately(0,0,b.getWidth(),b.getHeight());
          Last_Progress=Progress;
        }
        Thread.sleep(200);                                                      // Delay the thread
        Do_Step_B=(Started_B && (counter++ % Unit_Size ==0));
        if (Progress<100 && Do_Step_B) Progress++;
      }
    }
    catch (Exception e) { e.printStackTrace(); }
  }

  public void start()
  {
    if (Progress_Bar_Runner_Thread==null)
    {
      Progress_Bar_Runner_Thread=new Thread(this);
      Progress_Bar_Runner_Thread.setPriority(Thread.NORM_PRIORITY);
      Progress_Bar_Runner_Thread.start();
    }
  }

  public void stop() { if (Progress_Bar_Runner_Thread!=null) Progress_Bar_Runner_Thread=null; }

  private static void Out(String message) { System.out.println(message); }
}

class Test_Runner implements Callable<String>
{
  int Start_Index=0,End_Index=6999;
  StringBuilder StrBdr=new StringBuilder();

  public Test_Runner() { }

  public String call() throws InterruptedException
  {
    try { for (int i=Start_Index;i<End_Index;i++) StrBdr.append("Test_Runner + Test_Runner + Test_Runner + Test_Runner + Test_Runner + Test_Runner"); }
    catch (Exception e) {}
    return StrBdr.toString();
  }
}

class Timer extends JLabel implements Runnable
{
  public static final long serialVersionUID=26362862L;
  private Thread Timer_Thread;
  String Time_Text="";
  int updateInterval=1000,Format=0;
  Get_Time Timer=new Get_Time();

  public Timer()
  {
    setFont(new Font("Monospaced",Font.PLAIN,16));
    setVerticalAlignment(SwingConstants.CENTER);
    setHorizontalAlignment(SwingConstants.CENTER);
  }

  public Timer(int Format) { this.Format=Format; }

  public void start()
  {
    Timer.Start();
    if (Timer_Thread==null)
    {
      Timer_Thread=new Thread(this);
      Timer_Thread.setPriority(Thread.NORM_PRIORITY);
      Timer_Thread.start();
    }
  }

  public void run()
  {
    Thread myThread=Thread.currentThread();
    while (Timer_Thread==myThread)
    {
      switch (Format)
      {
        case 1 : Time_Text=Timer.Get_Duration_Hour_Minute_Second();break;
      }
      setText(Time_Text);
      paintImmediately(0,0,getWidth(),getHeight());
      revalidate();
      try { Thread.sleep(updateInterval); }
      catch (InterruptedException e) { } 
    }
  }

  public void stop() { if (Timer_Thread != null) Timer_Thread=null; }
}

class Clock extends JLabel implements Runnable
{
  public static final long serialVersionUID=26362862L;
  private Thread clockThread;
  String Time_Text="";
  int updateInterval=1000,Format=0;
  Get_Time Timer=new Get_Time();

  public Clock() { start(); }

  public Clock(int Format)
  {
    this.Format=Format;
    start();
  }

  public void start()
  {
    setVerticalAlignment(SwingConstants.CENTER);
    setHorizontalAlignment(SwingConstants.CENTER);
    if (clockThread==null)
    {
      clockThread=new Thread(this);
//      clockThread.setPriority(Thread.NORM_PRIORITY);
      clockThread.setPriority(Thread.MIN_PRIORITY);
      clockThread.start();
    }
  }

  public void run()
  {
    Thread myThread=Thread.currentThread();

    while (clockThread==myThread)
    {
      switch (Format)
      {
        case 0 : Time_Text=" "+new java.util.Date().toString().substring(0,19)+" ";break;
      }
      setText(Time_Text);
      paintImmediately(0,0,getWidth(),getHeight());
      revalidate();

      try { Thread.sleep(updateInterval); }
      catch (InterruptedException e) { } 
    }
  }

  public void stop() { if (clockThread != null) clockThread=null; }
}

class Get_Time
{
  private long start,end;
  public int Hours,Minutes,Seconds,Total_Seconds;
  String ST_Hours,ST_Minutes,ST_Seconds;

  public Get_Time() { Reset(); }

  public void Start() { start=System.currentTimeMillis(); }

  public void End()
  {
    int half_second;

    end=System.currentTimeMillis();

    Total_Seconds=(int)(end-start)/1000;
    half_second=(int)(end-start)%1000;

    if (half_second>499) Total_Seconds++;

    Hours=Total_Seconds/3600;
    Minutes=(Total_Seconds%3600)/60;
    Seconds=(Total_Seconds%3600)%60;

    ST_Hours=new String((Hours>9)?""+Hours:"0"+Hours);
    ST_Minutes=new String((Minutes>9)?""+Minutes:"0"+Minutes);
    ST_Seconds=new String((Seconds>9)?""+Seconds:"0"+Seconds);
  }

  public String Get_Duration_Hour_Minute_Second()
  {
    End();
    return ST_Hours+":"+ST_Minutes+":"+ST_Seconds;
  }

  public void Reset() { start=0;end=0; }
}

我的电脑有 12 核 3.33 GH Intel Corei7 x 980 CPU,24 GB 内存,运行 Windows 7,Java 版本 = 1.8 .0_212

问题是如何解决?

Swing线程和绘画问题列举:

以下所有代码都是从 Swing 事件分派线程 (EDT) 调用的,到目前为止一切顺利....

void Do_Test() {
    String Info = "", Result;
    Out("Do_Test");
    try {
        Display_Timer.start();
        Timer.Start();
        Output_Time("[ 1 ]");
        Progress_bar.Set_Progress(1);   // OK to call this on the event thread

        int Task_Count = 222;
        executor = new ThreadPoolExecutor(Executor_Count, Executor_Count * 2, 1,
                TimeUnit.SECONDS, new LinkedBlockingQueue());
        ArrayList<Future<String>> futures = new ArrayList<>(Task_Count);
        Test_Runner A_Runner;

        try {
            for (int i = 0; i < Task_Count; i++) {
                A_Runner = new Test_Runner();
                futures.add(executor.submit(A_Runner));
            }
            executor.shutdown();

好的,这里我们开始看到问题,因为您在 EDT 上调用可能很长的 while 循环,可能会阻止它:

while (!executor.isTerminated()) {
    executor.awaitTermination(100, TimeUnit.MILLISECONDS);
}

此处相同,因为对 Future#get() 的调用是阻塞调用:

for (Future<String> future : futures) {
    Result = future.get();
    if (Result != null)
        Info += Result;
}

好的,现在我们开始看到一些真正不寻常的 Swing 代码,您可以在其中进行 Swing 调用,包括设置 JProgressBar 的值 关闭 EDT,然后尝试强制 EDT 执行一些操作通过 paintImmediately(....) 绘画。在 EDT 之外调用可能会导致间歇性和不可预测的错误,并可能导致绘画混乱,并且 GUI 有时会冻结:

class Progress_Bar extends JPanel implements Runnable {
    // .....

    public void Set_Progress(int Progress) {
        if (Progress == 1 || (this.Progress < Progress && Progress <= 100)) {
            this.Progress = Progress;    // ****** being called off the EDT
            b.paintImmediately(0, 0, b.getWidth(), b.getHeight());  // ***** Don't do this
            Out("        Progress = " + Progress + " %");
        }
        // .....
    }

    public void run() {
        try {
            while (Progress <= 100) {
                if ((Progress == 0 || Progress == 50 || Progress == 100 || Do_Step_B)
                        && Last_Progress != Progress) {

                    // ***** called off the EDT -- don't do this
                    b.setValue(Progress);
                    // revalidate();
                    b.paintImmediately(0, 0, b.getWidth(), b.getHeight()); // **** and don't do this
                    Last_Progress = Progress;
                }
                Thread.sleep(200); // Delay the thread
                Do_Step_B = (Started_B && (counter++ % Unit_Size == 0));
                if (Progress < 100 && Do_Step_B)
                    Progress++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // .......
}

类似奇怪的goings-ons在这个class,定时器class,你设置一个JLabel的文本关闭的EDT:

class Timer extends JLabel implements Runnable { // ....

public void run() {
    Thread myThread = Thread.currentThread();
    while (Timer_Thread == myThread) {
        switch (Format) {
        case 1:
            Time_Text = Timer.Get_Duration_Hour_Minute_Second();
            break;
        }

        // again this is dangerous code **********
        setText(Time_Text);
        paintImmediately(0, 0, getWidth(), getHeight());
        // ....

时钟相同class...


解决方案:

要在 Swing GUI 中重复调用代码,请使用 javax.swing.Timer 或 "Swing Timer"。有关使用它的示例,请参阅我对您的 MCVE 的实现,但在上面的线程代码中使用了 Swing 计时器。

另一个代码,即调用 long-running 任务的代码,应该在 SwingWorker 中完成。此辅助线程通常以两种方式之一(或两种方式)与 Swing GUI 通信——使用 publish/process 方法对,或(如以下示例所示)使用附加到辅助线程的 PropertyChangeListener。 Worker 有几个 "bound" 属性,通知侦听器更改的字段,包括 progress 属性,可以保存从 0 到 100 的值,以及 SwingWorker.StateValue,或 "state" 属性:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.swing.*;

@SuppressWarnings("serial")
public class JavaTest2 extends JPanel {
    private static final int W = 800;
    private static final int H = 260;
    private Action doTestAction = new DoTestAction("Do Test");
    private JProgressBar progressBar = new JProgressBar(0, 100);
    private MyClockPanel clockPanel = new MyClockPanel();
    private MyTimerPanel timerPanel = new MyTimerPanel();

    public JavaTest2() {
        JPanel topPanel = new JPanel();
        topPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 35, 5));
        topPanel.add(new JButton(doTestAction));

        progressBar.setStringPainted(true);
        JPanel progressPanel = new JPanel(new GridBagLayout());
        progressPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weightx = 1.0;
        progressPanel.add(progressBar, gbc);

        JLabel progressLabel = new JLabel("Progress", SwingConstants.CENTER);
        progressLabel.setFont(new Font("Times New Roman", 0, 20));
        progressLabel.setForeground(new Color(8, 68, 128));

        JPanel centralPanel = new JPanel(new BorderLayout(5, 5));
        centralPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        centralPanel.add(progressLabel, BorderLayout.PAGE_START);
        centralPanel.add(progressPanel);

        JPanel clockWrapper = new JPanel();
        clockWrapper.add(clockPanel);
        JPanel bottomPanel = new JPanel();
        bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.PAGE_AXIS));
        bottomPanel.add(timerPanel, BorderLayout.PAGE_START);
        bottomPanel.add(clockWrapper, BorderLayout.PAGE_END);

        setLayout(new BorderLayout());
        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        add(topPanel, BorderLayout.PAGE_START);
        add(bottomPanel, BorderLayout.PAGE_END);
        add(centralPanel);
    }

    @Override
    public Dimension getPreferredSize() {
        Dimension superSize = super.getPreferredSize();
        if (isPreferredSizeSet()) {
            return superSize;
        } else {
            int w = Math.max(superSize.width, W);
            int h = Math.max(superSize.height, H);
            return new Dimension(w, h);
        }
    }

    private class DoTestAction extends AbstractAction {
        private MyWorker myWorker = null;

        public DoTestAction(String name) {
            super(name);
            int mnemonic = (int) name.charAt(0);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (myWorker != null && myWorker.getState() == SwingWorker.StateValue.STARTED) {
                return; // still running
            }
            timerPanel.start();
            progressBar.setValue(0);
            myWorker = new MyWorker();
            myWorker.addPropertyChangeListener(new WorkerListener());
            myWorker.execute();
            setEnabled(false);
        }
    }

    class WorkerListener implements PropertyChangeListener {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            // if the worker is changing its progress bound property:
            if (evt.getPropertyName().equals("progress")) {
                int progress = (int) evt.getNewValue();
                // just for safety's sake, limit progress to 100 and no more
                progress = Math.min(progress, 100);
                progressBar.setValue(progress);
            } else if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
                // else if worker is done
                try {
                    // get the result to at least trap errors
                    String result = ((MyWorker) evt.getSource()).get();
                    // can display result in the GUI
                    timerPanel.stop();
                } catch (Exception e) {
                    // worker's exception is available to the GUI if desired here
                    e.printStackTrace();
                }
                progressBar.setValue(100);
                doTestAction.setEnabled(true);
            }
        }
    }

    private static class MyWorker extends SwingWorker<String, Void> {
        private static final int EXECUTOR_COUNT = 12;
        private static final int TASK_COUNT = 222;

        @Override
        protected String doInBackground() throws Exception {
            ExecutorService executor = new ThreadPoolExecutor(EXECUTOR_COUNT, EXECUTOR_COUNT * 2, 1,
                    TimeUnit.SECONDS, new LinkedBlockingQueue<>());
            List<Future<String>> futures = new ArrayList<>();
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < TASK_COUNT; i++) {
                Callable<String> aRunner = new ARunner();
                futures.add(executor.submit(aRunner));
            }
            executor.shutdown();
            int index = 0;
            for (Future<String> future : futures) {
                String result = future.get();
                sb.append(result);
                sb.append(" ");
                index++;
                int progress = (100 * index) / TASK_COUNT;
                progress = Math.min(progress, 100);
                setProgress(progress);
            }

            return sb.toString();
        }
    }

    private static class ARunner implements Callable<String> {
        private static final long SLEEP_TIME = 800;

        @Override
        public String call() throws Exception {
            TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
            return "Foo";
        }
    }

    private static void createAndShowGui() {
        JavaTest2 mainPanel = new JavaTest2();

        JFrame frame = new JFrame("Java Test 2");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}
@SuppressWarnings("serial")
class MyClockPanel extends JPanel {
    private static final Color FG = new Color(250, 250, 250);
    private static final Color BG = new Color(0, 110, 220);
    private static final int TIMER_DELAY = 200;
    private static final Font FONT = new Font("Monospaced", Font.PLAIN, 16);
    private JLabel clockLabel = new JLabel("", SwingConstants.CENTER);
    private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E MMM dd yyyy kk:mm:ss");

    public MyClockPanel() {
        setBackground(BG);
        clockLabel.setForeground(FG);
        clockLabel.setFont(FONT);
        displayDateTime();
        setLayout(new BorderLayout());
        setBorder(BorderFactory.createEmptyBorder(5, 50, 5, 50));
        add(clockLabel);
        new javax.swing.Timer(TIMER_DELAY, e -> {
            displayDateTime();
        }).start();
    }

    private void displayDateTime() {
        LocalDateTime dateTime = LocalDateTime.now();
        String text = dateTime.format(formatter);
        clockLabel.setText(text);
    }
}
@SuppressWarnings("serial")
class MyTimerPanel extends JPanel {
    private static final Color FG = new Color(0, 0, 238);
    private static final Font FONT = new Font("Times New Roman", 0, 14);
    private static final int TIMER_DELAY = 40;
    private static final String FORMAT_TXT = "Elapsed Time: %02d:%02d:%02d";
    private JLabel timerLabel = new JLabel("", SwingConstants.CENTER);
    private LocalDateTime startTime = null;
    private Timer timer = null;

    public MyTimerPanel() {
        timerLabel.setForeground(FG);
        timerLabel.setFont(FONT);
        setLayout(new BorderLayout());
        setBorder(BorderFactory.createEmptyBorder(5, 50, 5, 50));
        timerLabel.setText(String.format(FORMAT_TXT, 0, 0, 0));
        add(timerLabel);
    }

    public void start() {
        stop();
        startTime = LocalDateTime.now();
        timer = new Timer(TIMER_DELAY, e -> incrementTime());
        timer.start();
    }

    public void stop() {
        if (timer != null && timer.isRunning()) {
            timer.stop();
        }
    }

    private void incrementTime() {
        LocalDateTime currentTime = LocalDateTime.now();
        long hours = ChronoUnit.HOURS.between(startTime, currentTime);
        long minutes = ChronoUnit.MINUTES.between(startTime, currentTime) % 60;
        long seconds = ChronoUnit.SECONDS.between(startTime, currentTime) % 60;
        String text = String.format(FORMAT_TXT, hours, minutes, seconds);
        timerLabel.setText(text);
    }
}