欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java Swing学习笔记(2)

程序员文章站 2024-02-16 09:22:40
...

Java Swing学习笔记(2)

本文主要记录了使用IntelliJ IDEA Community Edition + Java Swing编写UDP接收工具的过程中遇到的问题。

程序运行效果如下:

监听端口文本框只能输入数字:

Java Swing学习笔记(2)

开始监听后,输入文本框变灰色,退出时弹出提示框,确认退出后,关闭端口。

Java Swing学习笔记(2)

接收到数据后进行显示:

Java Swing学习笔记(2)

在完成本程序的过程中,遇到的问题有:

  1. 一个Thread结束运行后,再次运行会出现异常IllegalThreadStateException
  2. 端口文本框需要只能输入数字
  3. 点击开始后,检查端口号是否符合要求(0~65535)。不符合要求或打开出错则弹出对话框进行提醒。
  4. 在监听时,点击关闭窗口按钮,能够弹出对话框进行提示。
  5. 监听过程中对耗时操作更新UI操作的处理

1. 两次启动同一个线程带来的异常IllegalThreadStateException

在对同一个Thread对象执行两次start()方法时,会出现该异常。

例如下面的代码:

Thread thr = new Thread(new Runnable() {
    @Override
    public void run() {
        ...
    }
});

thr.start();
...
thr.start();  //同一个Thread对象两次start会出现异常IllegalThreadStateException

1.1 问题分析

查看Thread类的start()方法的源码,可以看到如下代码:

if (threadStatus != 0)
    throw new IllegalThreadStateException();

这里的0是一个代表线程状态的常量,查看Thread类的Enum类State:

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called {@code Object.wait()}
     * on an object is waiting for another thread to call
     * {@code Object.notify()} or {@code Object.notifyAll()} on
     * that object. A thread that has called {@code Thread.join()}
     * is waiting for a specified thread to terminate.
     */
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;
}

可以看到,只有线程状态为NEW的线程,才允许使用start()方法。

否则会抛出异常。

1.2 解决方法

每次执行start方法之前,才new一个Thread对象。

例如,在类里定义了一个Thread对象mRecThr,并且该线程需要多次启动,则应该如下定义:

public class UDP {
    ...
    private Thread mRecThr;
    ...
    public void startReceive() {
        mRecFlag = true;
        mRecThr = new Thread(new Runnable() {

            public void run() {
                ...
            }
        });
        mRecThr.start();  //WARNING:同一个线程启动两次,会出现异常IllegalThreadStateException
    }
    ...
}

即保证每次start时,threadStatus都是0。

2. 让文本框JTextField只能输入数字

实现这个效果的方法有很多,这里使用重写PlainDocument类的insertString()方法的方式来实现。

首先,先自定义一个继承PlainDocument类的子类NumberTextField并重写insertString()方法:

class NumberTextField extends PlainDocument {
    public NumberTextField() {
        super();
    }

    public void insertString(int offset, String str, AttributeSet attr)
            throws javax.swing.text.BadLocationException {
        if (str == null) {
            return;
        }

        char[] s = str.toCharArray();
        int length = 0;
        // 过滤非数字
        for (int i = 0; i < s.length; i++) {
            if ((s[i] >= '0') && (s[i] <= '9')) {
                s[length++] = s[i];
            }
            // 插入内容
            super.insertString(offset, new String(s, 0, length), attr);
        }
    }
}

再使用要设置的JTextField对象的setDocument方法应用修改:

textField1.setDocument(new NumberTextField());   //使用该方法让文本框只能接受数字

3. 弹出提示对话框

使用JOptionPaneshowMessageDialog方法来实现这个效果。

打开JOptionPane.java,我们可以看到该方法存在着多个重载。

以下面的方法为例,说明各参数的含义:

public static void showMessageDialog(Component parentComponent,
    Object message, String title, int messageType)
    throws HeadlessException {
    ...
}

Component parentComponent:指定弹出对话框的位置,若传入一个Frame对象,则弹出对话框的位置会在该Frame的中心。若置为null,则弹出对话框在屏幕中心。

Object message:要显示的信息。

String title:对话框标题。

int messageType:对话框的提示图标,在JOptionPane类中有如下常量定义:

//
// Message types. Used by the UI to determine what icon to display,
// and possibly what behavior to give based on the type.
//
/** Used for error messages. */
public static final int  ERROR_MESSAGE = 0;
/** Used for information messages. */
public static final int  INFORMATION_MESSAGE = 1;
/** Used for warning messages. */
public static final int  WARNING_MESSAGE = 2;
/** Used for questions. */
public static final int  QUESTION_MESSAGE = 3;
/** No icon is used. */
public static final int   PLAIN_MESSAGE = -1;

综上所述,下列代码的执行结果如图:

JOptionPane.showMessageDialog(null, "端口号错误,请输入0~65535之间的端口号。", "Error", JOptionPane.ERROR_MESSAGE);

Java Swing学习笔记(2)

4. 点击“关闭窗口”按钮时的操作和确认对话框

要自定义点击“关闭窗口”按钮时的操作,首先需要取消关闭按钮的功能,然后自己完成点击关闭窗口按钮的事件。

关键代码如下:

frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);  //自定义关闭窗口动作
frame.addWindowListener(new WindowAdapter() {
    @Override
    public void windowClosing(WindowEvent e) {
        if(udpService.isRunning()) {
            int flag = 
                JOptionPane.showConfirmDialog(null,"正在监听,是否退出?",
                                              "退出",JOptionPane.YES_NO_OPTION, 
                                              JOptionPane.INFORMATION_MESSAGE);
            if(flag == JOptionPane.NO_OPTION) {
                return;
            } else {
                udpService.stopUDPListen();
                System.exit(0);
            }
        }
        System.exit(0);
    }
});

4.1 自定义点击“关闭窗口”按钮时的操作

如上述代码所示。

首先设置关闭窗口动作为DO_NOTHING_ON_CLOSE(即点击时系统不执行任何操作)

之后在该窗口上添加自定义的WindowAdapter对象,重写windowClosing方法。

在该方法中添加自己需要的逻辑即可。

若要关闭程序,请使用System.exit(0)

4.2 显示确认对话框

JOptionPane.showConfirmDialog(null,"正在监听,是否退出?",
                                              "退出",JOptionPane.YES_NO_OPTION, 
                                              JOptionPane.INFORMATION_MESSAGE);

使用如上代码即可显示确认对话框。

其中的JOptionPane.YES_NO_OPTION常量用于控制确认对话框上显示的按钮。

其他常量如下:

//
// Option types
//

/**
 * Type meaning Look and Feel should not supply any options -- only
 * use the options from the <code>JOptionPane</code>.
 */
public static final int         DEFAULT_OPTION = -1;
/** Type used for <code>showConfirmDialog</code>. */
public static final int         YES_NO_OPTION = 0;
/** Type used for <code>showConfirmDialog</code>. */
public static final int         YES_NO_CANCEL_OPTION = 1;
/** Type used for <code>showConfirmDialog</code>. */
public static final int         OK_CANCEL_OPTION = 2;

可通过监听返回值获得用户按下的按钮是什么,可能的返回值如下所示:

//
// Return values.
//
/** Return value from class method if YES is chosen. */
public static final int         YES_OPTION = 0;
/** Return value from class method if NO is chosen. */
public static final int         NO_OPTION = 1;
/** Return value from class method if CANCEL is chosen. */
public static final int         CANCEL_OPTION = 2;
/** Return value form class method if OK is chosen. */
public static final int         OK_OPTION = 0;
/** Return value from class method if user closes window without selecting
 * anything, more than likely this should be treated as either a
 * <code>CANCEL_OPTION</code> or <code>NO_OPTION</code>. */
public static final int         CLOSED_OPTION = -1;

5. 耗时操作与更新UI

耗时操作请使用子线程完成。

更新UI的操作请用Runnable包装,并使用invokeLater执行。

下面是一个用子线程从阻塞队列中获取UDP传来的数据并刷新UI的例子。

thrGetData = new Thread(new Runnable() {
    @Override
    public void run() {
        while (MainClass.udpService.isRunning()) {
            DatagramPacket dp = MainClass.udpService.getDataFromQueue(); 
            //耗时操作,从阻塞队列中获取数据,队列为空时线程阻塞

            if (dp == null)
                return;
            byte[] recv = new byte[dp.getLength()];
            System.arraycopy(dp.getData(), 0, recv, 0, dp.getLength());
            final String data = new String(recv);
            javax.swing.SwingUtilities.invokeLater(new Runnable() {
                //使用invokeLater来更新UI
                public void run() {
                    recData.setText(data);
                }
            });
        }
    }
});
thrGetData.start();