天天看点

Java Swing交易市场(C/S模型)结构UI界面连接服务器项目链接

Java Swing交易市场

  • 结构
  • UI界面
    • 组件
      • 文本输入框
      • 按钮
      • 比较常用的组件
    • 动画和绘画
    • 界面构建
      • 简单界面布局
      • 复杂界面布局(GridBagLayout)
  • 连接服务器
    • servlet连接
    • socket
  • 项目链接

结构

C/S结构进行开发,将客户端UI等设计与后端服务器等分离开,分别进行开发

  • C/S结构
  • MVC结构
  • Socket和Servlet并用

UI界面

组件

文本输入框

交易市场比较常用的组件之一就是文本输入框,用于用户名的输入,密码的输入,或者商品搜索功能

/**
  * @Description 文字输入框
  * @author 广阔大世界
  * @Date 2020/3/31
  */
public class TextEdit extends JPanel {
    protected final Color errorColor = MaterialColors.COSMO_RED;
    protected final Color focusColor = MaterialColors.COSMO_BLUE;
    protected final Color normalColor = MaterialColors.COSMO_DARK_GRAY;
    protected int hintTextSize = 12;
    protected int column = 22;
    //绘制时的颜色,和计时器结合使用
    protected Color paintColor;
    //行尾图标
    protected ImageIcon endImage;
    protected boolean focused;
    //提示文字
    protected String hintText;
    //出错文字,不为空则改变输入框颜色
    protected String errorText;
    //文字输入框本体
    protected JTextField textField;
    //行尾图标的容器,可以增加监听
    protected JLabel endLabel;
    //动画计数
    protected int num = 0;
    //动画分割数量
    protected int max = 10;
    protected Timer timer;
    public TextEdit() {
        this("");
    }
    public TextEdit(String hint){
        super();
        this.hintText = hint;
        initial();
    }

    protected void initial(){
        if (this.textField == null){
            this.textField = new JTextField();
        }
        this.setLayout(new FlowLayout(FlowLayout.LEFT));
        this.add(textField);
        this.textField.setColumns(column);
        this.textField.setBackground(null);
        this.textField.setBorder(null);
        this.textField.setFont(new Font("宋体",Font.BOLD,(int)(hintTextSize * 1.5f)));
        this.setBorder(new EmptyBorder(hintTextSize + 1,0,hintTextSize,0));
        //设置监听,在获取焦点之后重绘颜色
        this.textField.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                TextEdit.this.focused = true;
                if (timer != null && timer.isRunning()){
                    timer.stop();
                }
                timer = new Timer(20,new GetFocusedListener());
                timer.start();
            }

            @Override
            public void focusLost(FocusEvent e) {
                TextEdit.this.focused = false;
                if (timer != null && timer.isRunning()){
                    timer.stop();
                }
                timer = new Timer(10,new LoseFocusListener());
                timer.start();
            }
        });
        //绘制行尾图标
        endLabel = new JLabel(endImage);
        this.add(endLabel);
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        Graphics2D g2=(Graphics2D) g;
        int h = this.getHeight();
        int w = this.textField.getWidth();
        //设置绘图质量
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
        //绘制下划线
        if (errorText != null && errorText.length() > 0){
            paintColor = errorColor;
        } else if (num == max){
            paintColor = focusColor;
        } else if (num == 0){
            paintColor = normalColor;
        }
        g2.setColor(paintColor);
        g2.setStroke(new BasicStroke(1.5f));
        g2.drawLine(0,h- hintTextSize - 1,w + 36,h - hintTextSize - 1);
        //下划线绘制结束
        Paint textPaint;
        //绘制错误信息(不为空时)
        if (errorText != null && errorText.length() > 0){
            textPaint = new GradientPaint(0,0,errorColor,0,0,errorColor);
            g2.setPaint(textPaint);
            Font font = new Font("微软雅黑",Font.PLAIN,hintTextSize - 1);
            g2.setFont(font);
            g2.setStroke(new BasicStroke(1));
            g2.drawString(errorText,0,h - 2);
        }
        //绘制提示
        if (hintText != null && hintText.length() > 0){
            if (focused){
                textPaint = new GradientPaint(0,0,focusColor,0,0,focusColor);
            } else if (textField.getText() != null && textField.getText().length() > 0){
                textPaint = new GradientPaint(0,0,MaterialColors.GRAY_700,0,0,MaterialColors.GRAY_700);
            } else {
                textPaint = new GradientPaint(0,0,normalColor,0,0,normalColor);
            } if (errorText != null && errorText.length() > 0){
                textPaint = new GradientPaint(0,0,errorColor,0,0,errorColor);
            }
            Font font = new Font("微软雅黑",Font.PLAIN,hintTextSize);
            g2.setPaint(textPaint);
            g2.setFont(font);
            g2.setStroke(new BasicStroke(1));
            g2.drawString(hintText,0,hintTextSize);
        }

        g2.dispose();
    }

    //设置初始文本
    public void setText(String text){
        this.textField.setText(text);
        repaint();
    }

    public String getText(){
        return this.textField.getText();
    }

    public String getHintText() {
        return hintText;
    }

    public void setHintText(String hintText) {
        this.hintText = hintText;
        repaint();
    }

    public String getErrorText() {
        return errorText;
    }

    /**
     * @Description 设置错误信息,设为空或者""则不显示
     * @param errorText 出错时的文本
     * @return void
     * @author 广阔大世界
     * @Date 2020/3/31
     */
    public void setErrorText(String errorText) {
        this.errorText = errorText;
        repaint();
    }

    public int getColumn() {
        return column;
    }

    public void setColumn(int column) {
        this.column = column;
        this.textField.setColumns(column);
        repaint();
    }

    public ImageIcon getEndImage() {
        return endImage;
    }

    public void setEndImage(ImageIcon endImage) {
        this.endImage = endImage;
        this.endLabel.setIcon(this.endImage);
        repaint();
    }

    public JLabel getEndLabel() {
        return endLabel;
    }
        //获取焦点后的行为
    protected class GetFocusedListener implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e) {
            //绘制随时间变化的颜色
            double individualR = ( 1.0 * focusColor.getRed() - normalColor.getRed()) / max;
            double individualG = ( 1.0 * focusColor.getGreen() - normalColor.getGreen()) / max;
            double individualB = ( 1.0 * focusColor.getBlue() - normalColor.getBlue()) / max;
            num++;
            if (num <= max){
                paintColor = new Color(normalColor.getRed() + (int)individualR * num,
                        normalColor.getGreen() + (int)individualG * num,
                        normalColor.getBlue() + (int)individualB * num
                        );
            } else {
                timer.stop();
            }
            repaint();
        }
    }

    //失去焦点后的行为
    protected class LoseFocusListener implements ActionListener{

        @Override
        public void actionPerformed(ActionEvent e) {
            //绘制随时间变化的颜色
            double individualR = ( 1.0 * focusColor.getRed() - normalColor.getRed()) / max;
            double individualG = ( 1.0 * focusColor.getGreen() - normalColor.getGreen()) / max;
            double individualB = ( 1.0 * focusColor.getBlue() - normalColor.getBlue()) / max;
            num--;
            if (num >= 0){
                paintColor = new Color(normalColor.getRed() + (int)individualR * num,
                        normalColor.getGreen() + (int)individualG * num,
                        normalColor.getBlue() + (int)individualB * num
                );
            } else {
                timer.stop();
            }
            repaint();
        }
    }
}
           

这里的文本输入框实现了悬浮标题,以及错误提示,在获取到焦点之后和失去焦点之后会有一段动画效果,界面效果还可以

按钮

按钮也是很常用的组件,计划是做了两个button一个是有阴影的,另一个是扁平的,这里只贴出了扁平的按钮

public class FlatButton extends JButton implements MouseListener{
    protected String text;
    protected Icon icon;
    protected Color normalColor;
    protected Color hoverColor;
    protected Color pressedColor;
    protected Color normalTextColor;
    protected Color hoverTextColor;
    protected Color pressedTextColor;
    protected boolean selected;
    private boolean hover = false;
    public FlatButton(String text, Icon icon) {
        this(text,icon, MaterialColors.GRAY_800,MaterialColors.GRAY_700,MaterialColors.GRAY_600,MaterialColors.WHITE,MaterialColors.WHITE,MaterialColors.BLUE_400);
    }

    public FlatButton(String text, Icon icon, Color normalColor, Color hoverColor, Color pressedColor, Color normalTextColor, Color hoverTextColor, Color pressedTextColor) {
        super(text,icon);
        this.text = text;
        this.icon = icon;
        this.normalColor = normalColor;
        this.hoverColor = hoverColor;
        this.pressedColor = pressedColor;
        this.normalTextColor = normalTextColor;
        this.hoverTextColor = hoverTextColor;
        this.pressedTextColor = pressedTextColor;
        init(text,icon);
    }

    @Override
    protected void init(String text, Icon icon) {
        super.init(text, icon);
        this.setBorderPainted(false);
        this.setFocusPainted(false);
        this.setBackground(normalColor);
        this.setForeground(normalTextColor);
        this.addMouseListener(this);
    }

    public Color getNormalColor() {
        return normalColor;
    }

    public void setNormalColor(Color normalColor) {
        this.normalColor = normalColor;
        this.setBackground(normalColor);
        repaint();
    }

    public Color getHoverColor() {
        return hoverColor;
    }

    public void setHoverColor(Color hoverColor) {
        this.hoverColor = hoverColor;
        repaint();
    }

    public Color getPressedColor() {
        return pressedColor;
    }

    public void setPressedColor(Color pressedColor) {
        this.pressedColor = pressedColor;
        repaint();
    }

    public Color getNormalTextColor() {
        return normalTextColor;
    }

    public void setNormalTextColor(Color normalTextColor) {
        this.normalTextColor = normalTextColor;
        repaint();
    }

    public Color getHoverTextColor() {
        return hoverTextColor;
    }

    public void setHoverTextColor(Color hoverTextColor) {
        this.hoverTextColor = hoverTextColor;
        repaint();
    }

    public Color getPressedTextColor() {
        return pressedTextColor;
    }

    public void setPressedTextColor(Color pressedTextColor) {
        this.pressedTextColor = pressedTextColor;
        repaint();
    }

    public Boolean getSelected() {
        return selected;
    }

    @Override
    public void setSelected(boolean b) {
        super.setSelected(b);
        this.selected = b;
        if (b){
            this.setBackground(pressedColor);
        } else {
            this.setBackground(normalColor);
        }
        this.setForeground(normalTextColor);
        repaint();
    }

    @Override
    public void mouseClicked(MouseEvent e) {

    }

    @Override
    public void mousePressed(MouseEvent e) {
        this.setForeground(this.pressedTextColor);
        this.setBackground(this.pressedColor);
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        if (hover){
            this.setBackground(this.hoverColor);
            this.setForeground(this.hoverTextColor);
        } else {
            this.setForeground(this.normalTextColor);
            this.setBackground(this.normalColor);
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        hover = true;
        if (!selected){
            this.setBackground(this.hoverColor);
            this.setForeground(this.hoverTextColor);
        }
    }

    @Override
    public void mouseExited(MouseEvent e) {
        hover = false;
        if (!selected){
            this.setBackground(this.normalColor);
            this.setForeground(this.normalTextColor);
        }
    }
}
           

相当于一个比较基础的按钮,实现了鼠标悬浮,点击变色等,可以继承做出更好的效果

比较常用的组件

还有一些例如展示商品的card都可以提前做出来,等待调用

/**
*这是一个非常简单的分割线组件
*/
public class Divider extends JLabel {
    private Color color;
    private int thickness;
    public Divider(Color color, int thickness){
        super();
        this.color = color;
        this.thickness = thickness;
//        this.setBorder(BorderFactory.createLineBorder(color, thickness));
        this.setBorder(new EmptyBorder(thickness,0,0,0));

    }

    public Divider(){
        this(MaterialColors.GRAY_700,1);
    }

    public Divider(int thickness){
        this(MaterialColors.GRAY_700, thickness);
    }

    public Divider(Color color){
        this(color,1);
    }

    @Override
    public void paint(Graphics g) {
        Graphics2D graphics2D = (Graphics2D) g.create();
        graphics2D.setColor(color);
        graphics2D.fillRect(0,0,getWidth(),thickness);
        graphics2D.dispose();
    }
}
           

动画和绘画

动画可以根据Timer定时器来制作,具体可以参见上面的输入框里的写法,动画效果只要入门就比较简单,主要是坐标的计算问题

绘画也是坐标的计算

//这是一个继承了FlatButton的组件重写的绘制方法
    @Override
    public void paint(Graphics g) {

        super.paint(g);
        if (selected){
            Graphics2D graphics2D = (Graphics2D) g.create();
            graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            GradientPaint paint = new GradientPaint(0, 0, pressedTextColor,0,0,pressedTextColor);
            graphics2D.setPaint(paint);
            graphics2D.fillRect(0,0,5,getHeight());
            GradientPaint paint1 = new GradientPaint(0, 0, normalColor,0,0,normalColor);
            graphics2D.setPaint(paint1);
            graphics2D.drawRoundRect(2,getHeight() / 4,1,1,1,1);
            graphics2D.drawRoundRect(2,getHeight() / 2,1,1,1,1);
            graphics2D.drawRoundRect(2,getHeight() * 3 / 4,1,1,1,1);
            graphics2D.dispose();
        }
    }
           
Java Swing交易市场(C/S模型)结构UI界面连接服务器项目链接
Java Swing交易市场(C/S模型)结构UI界面连接服务器项目链接

上面的代码可以实现这样的按钮效果

界面构建

简单界面布局

布局 简介 优点 缺点 坑点
FlowLayout 行布局在一行填满之后换到下一行 简单易用 不够灵活,只能成行排列,不能以其他方式排列布局,选定方向后所有组件依次排列,不能更改某一个的位置。 在嵌套JScrollPanel时不会自动换行排列
BorderLayout 边界布局,可以按照五个方向排列组件 可以在五个方向排列组件,已经能应付绝大多数简单的布局 只有五个方向,不足以应付所有的布局,其中的组件默认是充满父组件,尽量的大,尤其是Center组件的填充,容易使布局变丑 如果在add时候不指定BorderLayout左右的组件都会添加到中间,后添加的组件会覆盖之前添加的
GridLayout 表格布局,实现多行多列的排列 在大小相等的多行排列中变现出色 不能调整每一行每一列的宽度,必须等宽,行高也登高,而且会为了填充父布局,拉伸子组件 在组件数量较少的时候会填充整个父组件导致子组件很大很大
CardLayout 在一个Panel里同时放多个Panel 便于切换整体布局,比如导航栏对应的部分 如果存放的几个JPanel的大小不一致可能会有问题 储存JPanel的时候如果给它一个名字,点用的时候不要记错

复杂界面布局(GridBagLayout)

可以说在我写项目的时候几乎所有的难以用其他布局完成的布局GridBagLayout都可以轻松完成,但是这个GridBagLayout也比较繁琐,用法简单但是代码比较多

//需要有配套的Constrant
		GridBagLayout gridBagLayout = new GridBagLayout();
        GridBagConstraints gridBagConstraints;
        JPanel contentPanel = new JPanel(gridBagLayout);
        gridBagConstraints = new GridBagConstraints();
        //第几行
        gridBagConstraints.gridx = 1;
        //第几列
        gridBagConstraints.gridy = 1;
        //在这一个各自中的位置
        gridBagConstraints.anchor = GridBagConstraints.EAST;
        //占据父容器的比重
        gridBagConstraints.weightx = 0.7;
        //占据子容器的比重
        gridBagConstraints.weighty = 0.7;
        //填充方向,水平,竖直或者所有
        gridBagConstraints.fill = GridBagConstraints.BOTH;
        //还有gridWidth是占据几行,gridHeight是几列
        //然后inset是边距,不推荐使用,但是效果也还可以
        contentPanel.add(new JLabel("test"), gridBagConstraints);
           
Java Swing交易市场(C/S模型)结构UI界面连接服务器项目链接

这是利用GridBagLayout实现的一个界面

每一个组件需要一个GridBagConstraints来约束他的表现,可以约束的大概有以下几点

  • 组件占据的比重(weightx/weighty)
  • 组件位于的表格(gridx/gridy)
  • 组建的空白(insert)
  • 组件占据几行几列,不完全规则的网格布局(gridheight/gridwidth)
  • 组件的对齐方式,支持9个方位,基本完全足够(anchor)

有几点需要注意

  • insert不建议使用,会导致父组件背景色显示出现问题
  • anchor的固定对齐是相对于某个网格的对齐,不是针对整个页面
  • anchor如果确实想要利用GridBagLayout将组件放置在比如右下角,在只有一个组件的情况下需要规定weight和weighty都是1.0或者更大
  • 网格布局的最终依旧保证每一行等高,每一列等宽

连接服务器

Java swing是线程不安全的,所有的更新UI都要在UI线程中完成,或者利用SwingUtil.invokeLater()进行更新。

在连接网络的时候,网络的延时比较大,需要通过新开线程的方式进行连接。在UI线程中连接网络,会造成UI线程的卡顿,在网络条件很差的情况下,会卡死。

要善于利用线程池,防止同时连接数量过大,服务器过载(毕竟是自家小水管)

servlet连接

如果后端用servlet或者是其他方式传递数据,返回Json格式,或者是个人规定好的格式的数据。记住一下几点:

  • 无论后端给了多好(垃圾)的接口一定要二次封装,便于以后功能的填充,或者是在明知道后端应该祭天的情况下给自己留一条活路
  • 二次封装的方法不应是异步方法,或者处理Future数据也可以
  • 一开始的时候要打印出返回数据,便于调试
//某一个封装样例,ResponseEntity是根据后端返回的数据写的封装类
//ConnectControl是连接网络的方法处理后端返回的数据
public class CommentControl {
    CommentControl(){}
    public ResponseEntity addComment(String token, int lastCommentId, int goodId, String message){
        ConnectControl connectControl = new ConnectControl();
        Map<String, String> map = new HashMap<>();
        map.put("lastCommentId", lastCommentId + "");
        map.put("goodId", goodId + "");
        map.put("message", message);
        connectControl.doPost(BaseValue.load + "addComment", token, map);
        return ResponseEntity.fit(connectControl);
    }

    public ResponseEntity getCommentByGoodId(int goodId){
        ConnectControl connectControl = new ConnectControl();
        Map<String, String> map = new HashMap<>();
        map.put("goodId", goodId + "");
        connectControl.doPost(BaseValue.load + "getCommentByGoodId", null, map);
        return ResponseEntity.fit(connectControl);
    }
}
           

socket

如果要做网络请求,尤其是类似于C/S模型的聊天socket是必不可少的。实现方法一般是while(true)嵌套socket进行连接

  1. 可以在while(true)增加Thread.sleep(100)来缓冲,防止请求过于频繁
  2. 可以用到上面讲的java.awt的Timer定时器来实现定时的效果
public class ChatControl {
    private final static int RETRY_TIME = 5;
    private static int registerCount = 0;
    private static int sendCount = 0;
    private static int receiveCount = 0;
    private static int userId;
    private static Executor fixedThreadPool = Executors.newFixedThreadPool(3);
    private static Socket socket;
    private static DataInputStream inputStream;
    private static DataOutputStream outputStream;
    private static final List<String> messages = new ArrayList<>();
    private static TrayIcon trayIcon;
    static {
        try {
            socket = new Socket(BaseValue.host,BaseValue.chatPort);
            inputStream = new DataInputStream(socket.getInputStream());
            outputStream = new DataOutputStream(socket.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("socket启动失败");
        }
    }

    ChatControl(){
    //这一部分还挺有意思的,是调用系统消息提示,增加托盘图标
        SystemTray tray = SystemTray.getSystemTray();
        trayIcon = new TrayIcon(Icons.shop.getImage(), "闲置物品交易平台");
        //Let the system resize the image if needed
        trayIcon.setImageAutoSize(true);
        //添加托盘图标
        trayIcon.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                JFrame f = FrameBase.frameBaseList.get(0);
                f.setVisible(true);
                f.setExtendedState(JFrame.NORMAL);
            }
        });
        try {
            tray.add(trayIcon);
        } catch (AWTException e) {
            e.printStackTrace();
        }
        receive()
    }

    public void send(String message) {
        ResponseEntity responseEntity = new ResponseEntity(0, "",message);
        String json = JSONObject.toJSONString(responseEntity);
        try {
            outputStream.writeUTF(json);
            outputStream.flush();
        } catch (IOException e) {
            if (sendCount == 0){
                e.printStackTrace();
            }
            if (sendCount <= RETRY_TIME){
                System.out.println("sending" + sendCount);
                sendCount++;
                send(message);
            } else {
                System.out.println("尝试发送失败");
                sendCount = 0;
            }
        }
    }

    public void receive() {
        fixedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                while (true){
                    try{
                        String result = inputStream.readUTF();
                        JSONObject object = JSONObject.parseObject(result);
                        if (object.getInteger("code") == 0){
                            Stringmessage = JSONObject.parseObject(object.getString("data"), String.class);
                            trayIcon.displayMessage("新消息", message.getText(), TrayIcon.MessageType.NONE);
                            Thread.sleep(200);
                            messages.add(message);
                        }
                    } catch (IOException | InterruptedException e){
                        if (receiveCount == 0){
                            e.printStackTrace();
                        }
                        if (receiveCount <= RETRY_TIME){
                            System.out.println("retry" + receiveCount);
                            receiveCount++;
                            if (receiveCount > RETRY_TIME){
                                System.out.println("socket连接丢失");
                                receiveCount = 0;
                                return;
                            }
                            receive();
                        }
                    }
                }
            }
        });
    }

	//外部只需调用getMessages就能获取消息
    public List<Message> getMessages(){
        if (messages.size() == 0){
            return new ArrayList<>();
        } else {
            List<String> messageList =  new ArrayList<>(messages);
            messages.clear();
            return messageList;
        }
    }

    public void closeSocket() {
        try {
            socket.close();
            outputStream.close();
            inputStream.close();
        } catch (IOException e){
            e.printStackTrace();
        }

    }

}

           

上面的代码外部调用只需要实例化之后调用getMesages方法就可以获取信息,不用持有外部引用,会降低内存泄漏的风险

外部只要再用Timer定时查询数据更新ui就可以了

项目链接

Swing C/S交易市场源码