Search code examples
javamultithreadinguser-interfaceelementinvokelater

java updating UI components from another thread


I found many answers about my question, but I still don't understand why my application does not throw any exceptions. I created a new java form application in NetBeans 8. My form is created and displayed in main method like this:

public static void main(String args[])
    {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try
        {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels())
            {
                if ("Nimbus".equals(info.getName()))
                {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        }
        catch (ClassNotFoundException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        catch (InstantiationException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        catch (IllegalAccessException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        catch (javax.swing.UnsupportedLookAndFeelException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                new MainForm().setVisible(true);     
            }
        });
    }

So, this new Runnable creates new MainForm and sets it visible.

Then, in my code I start new threads which updates some jButtons and jTextFields. Code below:

private void updateUI() {
        updateUIThread = new Thread(() ->
        { 
            while (true) {
                try {
                    jtfIP.setEnabled(!Start && !autoRec);
                    jtfPort.setEnabled(!Start && !autoRec);
                    jtfSlaveID.setEnabled(!Start && !autoRec);
                    jtfTimeout.setEnabled(!Start && !autoRec);
                    jtfReqInterval.setEnabled(!Start && !autoRec);
                    jCheckBox1.setEnabled(!Start && !autoRec);
                    jCBReconnect.setEnabled(!Start && !autoRec);

                    if (db != null) {
                        if (!db.getIsOpen()) {
                            jPBD.setBackground(Color.RED);
                            jPBD.setForeground(Color.WHITE);
                            jPBD.setText("ER");
                        } else {
                            jPBD.setBackground(Color.GREEN);
                            jPBD.setForeground(Color.BLACK);
                            jPBD.setText("OK ");
                        }
                    } else {
                        jPBD.setBackground(Color.RED);
                        jPBD.setForeground(Color.WHITE);
                        jPBD.setText(" ER ");
                    }


                    if (autoRec){
                        jbtnConnect.setText("Auto");
                        if (Start && Connected) {
                            jbtnConnect.setForeground(Color.BLACK);
                            jbtnConnect.setBackground(Color.GREEN);
                        } else {       
                            jbtnConnect.setForeground(Color.WHITE);
                            jbtnConnect.setBackground(Color.RED);
                        }
                    } else {
                        if (Start) {
                            jbtnConnect.setText("Disconnect");
                            jbtnConnect.setForeground(Color.BLACK);
                            jbtnConnect.setBackground(Color.GREEN);

                        } else {
                            jbtnConnect.setText("Connect");
                            jbtnConnect.setForeground(Color.WHITE);
                            jbtnConnect.setBackground(Color.RED);
                        }
                    }

                    jtfErroriCitire.setText(String.valueOf(totalErrors));

                    try
                    {
                        Thread.sleep(300);
                        jPanel4.repaint(1);
                    }
                    catch (InterruptedException ex)
                    {
                        Logger.getLogger(MainForm.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                catch (Exception ex) {
                    Logger.getLogger(MainForm.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
        updateUIThread.start();
    }

And there are other threads started like this above and where I get different values which are updated in the above thread.

My question is why my code does not throw any exception regarding UI elements which are updated from another thread? I did NOT use SwingUtilities.invokeLater(new Runnable() { //code here }); And my code executes perfectly...

Thank you!


Solution

  • Swing is not thread safe and is single threaded. You should never update UI components from outside the Event Dispatching Thread, equally, you should never run long running processes or blocking code within the EDT, as this will prevent it from processing new events within the event queue, causing your app to look like it's hung...because it has...

    Take a look at Concurrency in Swing for more details.

    After scratching my head for a while, I realised, the simple solution would be to just use javax.swing.Timer

    You want to repeat the update at a regular interval (300 milliseconds) and update the UI, perfect, the Swing Timer is capable of scheduling updates at regular intervals and executes it call back within the context of the EDT!

    It also has the ability to consolidate repeated calls. This means, if there is already a "timer" action in the event queue, the timer will not generate a new one, preventing from flooding the EDT and cause possible performance issues...

    javax.swing.Timer timer = new Timer(300, new ActionListener() {
        public void actionPerformed(ActionEvent evt) {    
            jtfIP.setEnabled(!Start && !autoRec);
            jtfPort.setEnabled(!Start && !autoRec);
            jtfSlaveID.setEnabled(!Start && !autoRec);
            jtfTimeout.setEnabled(!Start && !autoRec);
            jtfReqInterval.setEnabled(!Start && !autoRec);
            jCheckBox1.setEnabled(!Start && !autoRec);
            jCBReconnect.setEnabled(!Start && !autoRec);
    
            if (db != null) {
                if (!db.getIsOpen()) {
                    jPBD.setBackground(Color.RED);
                    jPBD.setForeground(Color.WHITE);
                    jPBD.setText("ER");
                } else {
                    jPBD.setBackground(Color.GREEN);
                    jPBD.setForeground(Color.BLACK);
                    jPBD.setText("OK ");
                }
            } else {
                jPBD.setBackground(Color.RED);
                jPBD.setForeground(Color.WHITE);
                jPBD.setText(" ER ");
            }
    
    
            if (autoRec){
                jbtnConnect.setText("Auto");
                if (Start && Connected) {
                    jbtnConnect.setForeground(Color.BLACK);
                    jbtnConnect.setBackground(Color.GREEN);
                } else {       
                    jbtnConnect.setForeground(Color.WHITE);
                    jbtnConnect.setBackground(Color.RED);
                }
            } else {
                if (Start) {
                    jbtnConnect.setText("Disconnect");
                    jbtnConnect.setForeground(Color.BLACK);
                    jbtnConnect.setBackground(Color.GREEN);
    
                } else {
                    jbtnConnect.setText("Connect");
                    jbtnConnect.setForeground(Color.WHITE);
                    jbtnConnect.setBackground(Color.RED);
                }
            }
    
            jtfErroriCitire.setText(String.valueOf(totalErrors));
        }
    });
    timer.start();
    

    See How to use Swing Timers for more details