大一JAVA课设之JAVA开发桌面应用——开发自己的闲鱼交易市场,能聊天,能买卖商品哦!


本文内容过长,各位看官老爷们,酌量食用~~

戳这里,观看本系统的完整的演示视频哦~~

先让大家看一下效果!
登录注册:

在这里插入图片描述

聊天列表

在这里插入图片描述

QQ聊天:

在这里插入图片描述

商城首页:

在这里插入图片描述

个人信息

在这里插入图片描述

一、结论分析与体会

1.1.技术部分

1.1.1.swing

利用swing来开发GUI,不是一件容易的事情,尤其是要注重用户体验以及UI美观方面,更是显得有点弱势。
主要掌握的内容就是swing的基本组件(JFrame、Janel、JButton、JTextFiled等)、控件的布局管理器(GridLayout、BorderLayout等)、各种事件(ActionEvent、MouseEvent、KeyEvent等)以及它们的接口和适配器。
以及,为了做出自己需要的组件,学会了继承已有swing组件并重写paint方法。

1.1.2.多线程

接触了线程的概念、线程的生命周期、线程的创建方式(继承Thread和实现Runnable接口)。
了解了多线程的基本应用,尤其是在socket编程中的应用。
初步了解了synchronized、volatile关键字,以及它们在多线程中的应用。
初步了解了线程池的概念,使用了ExecutorService线程池工具。

1.1.3.数据库

了解了数据库的概念、应用。使用了MySql数据库,熟悉jdbc的连接
明白数据库中的基本概念(表、字段、数据类型等)。
了解jdbc(Statement,ResultSet是什么,并如何执行sql语句)。
熟悉基本的sql语句(增删改查)并进行实践。
初步了解了DAO模式进行数据库交互模块的封装。
初步了解了数据库连接池(Druid)的概念并初步简单的使用。

1.1.4.网络

了解网络通信的基本原理,了解TCP传输协议。
使用socket编程编写简单的网络聊天程序。
学会用socket传输对象流(需要实现Serializable接口)
知道对象实现序列化的注意点。

1.1.5.集合与泛型

了解了java中的各种集合,以及它们的组织构成。
主要使用了ArrayList、HashMap、HashSet等,
并了解在使用集合过程中的泛型,初步了解了泛型类、泛型接口、泛型方法等,
并使用泛型让自己代码的可读性以及整洁性得到提高。

1.1.6.接口与内部类

了解了接口和内部类支撑起java多态的机制。
了解内部类的几种类型(匿名的、静态的、局部的、成员的)。

1.2.内心感悟

这次java课设是我在没有系统地学习过java的基础上进行开发的(我是18级的降转的学生),一开始感觉比较吃力,因为不少java的语法点都还很模糊,面向对象的编程范式也是初步了解。

在初始开发阶段,主要熟悉了一些基本的知识如swing、socket、jdbc、
MySql等等,并且在真正的项目开发中锻炼了编码能力,更是为我之后在课堂上学习java语言打下基础,并在那时会更加地明白基础知识的重要性。

在项目开发的中间阶段,我遇到很多技术瓶颈,比如网络聊天实现的基本原理是什么,怎么才能做出像QQ聊天的效果,无论多线程、网络还是图形界面编程都对我产生了很大的挑战,好在通过查阅java宝典、学习优质的技术博客、并在和同学的探讨中一点点地克服了这些困难。

在项目开发的收尾阶段,已经实现了项目需求的基本功能,这时候我在反思我开发的项目,发现真的是“不堪入目”——太多太多隐藏的技术难点被貌似简单的需求掩盖住了。比如聊天,真的能做到很多人同时在线时也能平稳流畅的运行吗?
比如用户搜索商品,如何在海量的数据中以极快的速度反馈给用户,要求更高一点,怎么随着用户的个人喜好,智能化的推荐给用户?又比如,如果很多买家对同一件商品进行购买,在高并发环境下,我的系统能够安全地、顺畅地运行下去吗?更不要说,一旦涉及金钱的交易,我的系统能够抵御一定量的破坏攻击吗?
恐怕上述的问题目前我根本解决不了。这也恰恰提醒我一定要认真学习一些计算机方面的基础知识,基础不牢、地动山摇的苦头,我现在就已经尝到了。
总之,这是一次收获颇丰的课程设计,值得回过头来认真回味!

二、主要技术难点

2.0系统模块架构

  • 功能架构
    在这里插入图片描述

  • 基于C/S架构的程序

C: Client 、S: Server
C/S模式简而言之就是客户端连接到服务端,服务端提供一系列服务。具体地,客户端在界面上所显示的一切东西都由服务端提供,而服务器则需要担任中转站的角色从数据库存取信息,完成客户端请求完成的任务。
下图就是基于C/S模式的系统模块图。

在这里插入图片描述

2.1.服务端和客户端进行数据交互的形式

基于Java是一种纯面向对象的语言,在服务端、客户端传输数据时,采用了对象流(ObjectStream),所有交换的数据全部封装为对象的形式。
定义了一个类(TransferObject),用来承担这个任务。

代码:

public class TransferObject implements Serializable
{
    private static final long serialVersionUID = 1L;
    private String Code;
    private Object data;

    public TransferObject(String netCode, Object data) {
        this.Code = netCode;
        this.data = data;
    }

    public String getCode() {
        return Code;
    }

    public Object getData() {
        return data;
    }
}

Code是一个用于区分不同信息的编码类。成员变量都是一些公共的、静态的字符串常量。
代码:

public class Code
{
    public final static String LOGIN = "AAAA";
    public final static String REGISTER = "AAAB";
    public final static String GET_USER = "AAAD";
    public final static String DOWNLOAD_MESSAGE = "AAAE";
    public final static String GET_FRIENDS = "AAAF";
    public final static String MESSAGE = "AAAG";
    public final static String CLEAR_MESSAGE_BY_FROM_TO_ID = "AAAH";
    public final static String ALTER_STATE_BY_ID = "AAAI";

	//…………………………………………………………
}

服务器端实现多线程。

2.2.Socket编程实现聊天

为了实现多个用户同时进行一对一的聊天,服务器端必须用多线程。同样,客户端为了在进行其他的任务时,同时接受和发送消息,也必须使用多线程。

服务器端的多线程:

代码:

public class ClientHandler implements Runnable
{
    private String userID;
    private ObjectInputStream ois = null;
    private ObjectOutputStream oos = null;
    private Handler handler;
    private Socket socket;

    public ClientHandler(Socket socket,Handler handler){
        this.socket = socket;
        this.handler = handler;
        try {
            // 先输入流、后输出流
            ois = new ObjectInputStream(socket.getInputStream());
            oos = new ObjectOutputStream(socket.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            TransferObject transObj = null;
            String code = "";
            while(( transObj = (TransferObject)ois.readObject())!=null ) {
                code = transObj.getCode();
                switch (code)
                {
                    case Code.MESSAGE:
                        handler.processMessage(transObj);
                        break;

                    case Code.LOGIN:
                        handler.tryToLogin(transObj,this.oos,this);
                        break;

                    case Code.REGISTER:
                        handler.tryToRegister(transObj,this.oos);
                        break;

                    case Code.GET_USER:
                        handler.getUserByID(transObj,this.oos);
                        break;

 				// ………………………………………………………………………………
                }
            }
        }catch(IOException | ClassNotFoundException | SQLException e){
            e.printStackTrace();
            if(handler.getUserService().checkOnline(this.userID)) {
                handler.getUserService().alterState(this.userID, 0);
            }
        }finally {
            //出错后,将这个客户端对应的输出流移除
            if(handler.getUserService().checkOnline(this.userID)) {
                handler.getUserService().alterState(this.userID, 0);
            }
            Main.serverPool.getOutStreamMap().remove(oos);
            if(socket!=null) {
                try{
                    socket.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

    public void setUserID(String userID) {
        this.userID = userID;
    }
}

客户端的多线程:
代码:

public class ClientToServer implements Runnable
{
    private Socket s ;
    private ObjectInputStream ois=null;
    private ObjectOutputStream oos=null;

    public ClientToServer()
    {
        try {
            s = new Socket("127.0.0.1",8001);
            // 先输出流、后输入流
            oos = new ObjectOutputStream(s.getOutputStream());
            ois = new ObjectInputStream(s.getInputStream());
        } catch (IOException e) {
            System.out.println("初始化失败");
            JOptionPane.showMessageDialog(null,"连接服务器失败");
        }
    }

    @Override
    public void run() {
       try {
           TransferObject transObj = null;
           String code = "";
           while((transObj=(TransferObject)ois.readObject())!=null)
           {
               code = transObj.getCode();
               switch (code)
               {
                   case Code.MESSAGE:
                        Handler.processMessage(transObj);
                        break;

                   case Code.LOGIN:
                       Handler.answerToLogin(transObj);
                       break;

                   case Code.REGISTER:
                       Handler.answerRegister((transObj));
                       break;

                   case Code.GET_USER:
                       Handler.answerGetUserByID(transObj);
                       break;

				//…………………………………………………………………………
               }
           }

       }catch(IOException | ClassNotFoundException | InterruptedException e){
           e.printStackTrace();
       }
    }
    public synchronized void send(TransferObject t){
        try {
            oos.writeObject(t);
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端如何解决消息的转发功能呢?
比如1号客户想用发送一条消息给2号客户,服务端该如何解决这个转发的任务呢?我将所有在线的用户所对应的对象输出流全都添加到HashMap<String,ObjectOutputStream>。然后可以根据ID值去找对应的输出流。

2.3.多线程的应用以及用线程池管理线程

多线程可以更高效地利用CPU资源,于是在IO密集的地方使用了多线程。

  • 在客户端从本地读取大量的图片的时候,为了不阻塞下面要进行的任务,这时新开一个线程去执行这样的任务。
  • 又比如,在要进行一个较为复杂的界面的绘制时,也可以用多线程的思想开一个线程去绘制这个界面。

但是,线程也不是开得越多越好的,尤其是在频繁的创建线程和销毁线程时。
在服务器端,由于很多个用户可能频繁的上线,下线,那么线程就会被反复的创建和销毁,这不仅消耗很多的时间,而且在线程开的很多的情况下会对服务器造成很大的压力。
根据享元模式的思想,借助JDK自带的ExecutorService线程池来帮助我们来管理线程。
首先,this.threadPool = Executors.newCachedThreadPool();
其中CachedThreadPool:可缓冲线城池,核心线程数0,最大线程数为最大整数值,没有线程数限制,每来一个任务立即提交线程执行,如果有空闲线程使用空闲线程,没有空闲线程直接新建一个线程,当线程空闲时间超过60s被回收。

代码:

public class ServerPool {
    private static ServerSocket serverSocket;
    //所有客户端输出流的集合
    private static Map<String, ObjectOutputStream> outStreamMap;
    //商品拍卖的群聊
    private static Map<String, Set<String>> groupChat;
    // 线程池
    private static ExecutorService threadPool;

    public ServerPool(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
        this.outStreamMap = new HashMap<>();
        this.groupChat = new HashMap<>();
        this.threadPool = Executors.newCachedThreadPool();
    }
    public void service()
    {
        while(true)
        {
            try {
                Socket socket = serverSocket.accept();
                this.threadPool.execute(new ClientHandler(socket,new Handler()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static Map<String, ObjectOutputStream> getOutStreamMap() {
        return outStreamMap;
    }

    public static Map<String, Set<String>> getGroupChat() {
        return groupChat;
    }

    public static synchronized void addOutStream(String ID, ObjectOutputStream oos) {
        outStreamMap.put(ID,oos);
    }

    public static synchronized void removeOutStream(String ID){
        outStreamMap.remove(ID);
    }


    public static void send(String ID, TransferObject t)
    {
        ObjectOutputStream oos = outStreamMap.get(ID);
        try {
            oos.writeObject(t);
            oos.flush();
        } catch (IOException e) {
            outStreamMap.remove(ID);
            e.printStackTrace();
        }
    }

    public static void send(ObjectOutputStream oos,TransferObject t)
    {
        try {
            oos.writeObject(t);
            oos.flush();
        } catch (IOException e) {
            outStreamMap.remove(oos);
            e.printStackTrace();
        }
    }

}

2.4.DAO模式的初步了解——数据库访问模块的封装

DAO(Database Access Object 数据库访问对象)
为了降低耦合性,提出了DAO封装数据库操作的设计模式。
它可以实现业务逻辑与数据库访问相分离。相对来说,数据库是比较稳定的,其中DAO组件依赖于数据库系统,提供数据库访问的接口。
隔离了不同的数据库实现。

DAO大致由四部分组成:

domain 存放一些实体类
utils 存放创建连接、关闭Connection等常用工具
dao 存放对数据库进行增删改查的接口
daoImpl dao的实现类

下图是这次课设的dao模式构成。
在这里插入图片描述

举一个例子来说明:
实体类User:

public class User extends BaseUser
{
    private static final long serialVersionUID = 1L;
    private String pass;

    public User(String ID, String nickname, String campus, String phone, Image head, String pass) throws IOException {
        super(ID, nickname, campus, phone, head);
        this.pass = pass;
    }
    public String getPass() {
        return pass;
    }
}

dao接口UserDao

public interface UserDao {

    //获取一个用户的完整信息
    public User getUser(String ID);
    //查询一个用户的在线状态
    public boolean checkOnline(String ID);
    //新增一个用户
    public boolean insertUser(User user,String headIamgeURL);
    //用户上线或下线时更改此用户的状态
    public boolean alterState(String ID,int state);
}

dao实现类UserDaoImpl

public class UserDaoImpl implements UserDao
{
    @Override
    public User getUser(String ID) {
        User user = null;
        String pass = "", nickname = "", campus = "", phone = "",headURL = null;
        Image head = null;

        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs =null;
        try {
            connection = DruidFactory.getConnection();
            pstmt =  connection.prepareStatement(SQL.GET_USER_BY_ID);
            pstmt.setString(1,ID);
            rs = pstmt.executeQuery();
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            while(rs.next()) {
                pass = rs.getString("pass");
                nickname = rs.getString("nickname");
                campus = rs.getString("campus");
                phone = rs.getString("phone");
                headURL = rs.getString("headURL");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        {
            if (headURL != null && headURL.length() > 0) {
                try {
                    head = ImageIO.read(new File(headURL));
                } catch (IOException e) {
                    //            e.printStackTrace();
                    return null;
                }
            } else {
                return null;
            }
            try {
                user = new User(ID, nickname, campus, phone, head, pass);
            } catch (IOException e) {
//            e.printStackTrace();
                return null;
            }
        }
        DruidFactory.closeAll(connection,pstmt,rs);
        return user;
    }

    @Override
    public boolean checkOnline(String ID)
    {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            connection = DruidFactory.getConnection();
            pstmt = connection.prepareStatement(SQL.CHECK_ON_LINE);
            pstmt.setString(1, ID);
            rs = pstmt.executeQuery();
            if (rs.next() && rs.getInt("online") == 1) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }finally {
            DruidFactory.closeAll(connection,pstmt,rs);
        }
        return false;
    }

    @Override
    public boolean insertUser(User user,String URL)
    {

        Connection connection = null;
        try {
            connection = DruidFactory.getConnection();
            PreparedStatement pstmt = connection.prepareStatement(SQL.INSERT_USER);

            pstmt.setString(1, user.getID());
            pstmt.setString(2, user.getPass());
            pstmt.setInt(3, 0);
            pstmt.setString(4, user.getNickname());
            pstmt.setString(5, user.getCampus());
            pstmt.setString(6, user.getPhone());
            pstmt.setString(7,URL);

            pstmt.execute();

            DruidFactory.closeAll(connection,pstmt);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean alterState(String ID, int state)
    {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            connection = DruidFactory.getConnection();
            pstmt = connection.prepareStatement(SQL.ALTER_STATE_BY_ID);

            pstmt.setInt(1, state);
            pstmt.setString(2,ID);
            pstmt.executeUpdate();

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }finally {
            DruidFactory.closeAll(connection,pstmt,rs);
        }
        return true;
    }
}

最后看一下工具类

public class DruidFactory {
    private static DruidDataSource dataSource = null;

    public DruidFactory() {
        Properties properties = new Properties();
        InputStream in = DruidFactory.class.getClassLoader().getResourceAsStream("druid.properties");
        try {
            properties.load(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            dataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("数据库初始化成功");
    }

    public static Connection getConnection() throws Exception {
        return dataSource.getConnection();
    }

    public static boolean closeAll(Connection connection, PreparedStatement pstmt) {
        if(pstmt!=null){
            try {
                pstmt.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        if(connection!=null){
            try {
                //这里并不会真的关闭connection,只是返还给数据库连接池进行管理
                connection.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        return true;
    }

    public static boolean closeAll(Connection connection, PreparedStatement pstmt, ResultSet rs){
        if(rs!=null){
            try {
                rs.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
                return true;
            }
        }
        closeAll(connection,pstmt);
        return true;
    }
}

2.5服务端处理客户端请求的逻辑层次

最初,在处理客户端的各种请求时,使用dao里面的接口足以完成任务,但是之后发现很多事物对数据库的操作并不具有简单的原子性
举个例子:一个用户想要查询另一个用户的信息(类似于QQ里的加好友),这里客户端可能传来的是用户的ID,有可能是他的昵称,这时候到数据库里查询的时候,实际上要根据这两种不同的信息进行分别查询。
也就是说,在dao层里面,会有两个接口,分别处理这两个任务,但是在处理客户端的请求时,对外其实只是表现出一种功能,就是搜索用户。当然,还可以有更加复杂的任务,需要多个对数据库简单的操作组合而成。
本着面向对象的设计原则,不要让一个类做过多的事情,我将这些对外表现的服务功能再次封装在一个service包里。

于是,服务器端处理层次如下图。

在这里插入图片描述
于是,ClientHandler不断接受来自不同客户端的请求,根据传输过来的对象的编码,通过switch语句的甄别,调用类Handler的一系列响应方法,而Handler处理事务的方法则是基于service包里的封装过的方法,最终调用dao的接口查询数据并返回,最后Handler再去将数据发送给对应的客户端。

处理事务的核心类Handler:

在这里插入图片描述

与之相对应的客户端的处理机制:
sendRequest包里存放的是向服务器发送各种请求服务的指令,然后由线程类ClientToServer的run方法一直监听来自服务端的处理结果,然后交给view包里面的各个界面去呈现。

在这里插入图片描述

view里面按照不同界面所属的逻辑层次进行了划分。
在这里插入图片描述

2.6.多线程并发下使用数据库连接池的必要性

数据库连接池的思想,其实与线程池的思想是如出一辙的,都是基于享元模式的一种设计思想。数据库连接池里,初始化若干的连接,而后如果需要使用连接,如果有空闲的connection,就直接使用;如果没有才新创建一个connection。
这样做的优点就是避免反复的创建、销毁连接,消耗大量时间。

但是,这不由得疑问,为什么不能只使用一个connection,然后就用这一个connection去创建PrepareStatement。

点击这里,详见这篇博客

总结:在多线程的环境中,在不对connection做线程安全处理的情况下,使用单个connection会引起事务的混乱。
与使用线程一样的问题,数据库的connection也不是开的越多越好,对机器和数据库都会造成很大的压力。

解决方案就是使用数据库连接池。
在本次Java课程设计中,我使用了性能较优越的Druid数据库连接池来管理和数据库的连接。这个时候我再次发觉了使用了DAO封装对数据库的增删改查操作的优越性,这降低业务逻辑和数据库访问的耦合性,也就是说外部调用dao的接口时,无需管和底层的数据库是什么。将来,如国使用其他类型的数据库(本次使用的MySQL),只在数据库连接那里发生变化,调用dao接口的地方无需修改,正常调用即可。

需要使用connection时,从Druid获取,然后关闭时实际上返还给数据库连接池管理。

public class DruidFactory {
    private static DruidDataSource dataSource = null;

    public DruidFactory() {
        Properties properties = new Properties();
        InputStream in = DruidFactory.class.getClassLoader().getResourceAsStream("druid.properties");
        try {
            properties.load(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            dataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("数据库初始化成功");
    }

    public static Connection getConnection() throws Exception {
        return dataSource.getConnection();
    }

    public static boolean closeAll(Connection connection, PreparedStatement pstmt) {
        if(pstmt!=null){
            try {
                pstmt.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        if(connection!=null){
            try {
                //这里并不会真的关闭connection,只是返还给数据库连接池进行管理
                connection.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        return true;
    }

    public static boolean closeAll(Connection connection, PreparedStatement pstmt, ResultSet rs){
        if(rs!=null){
            try {
                rs.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
                return true;
            }
        }
        closeAll(connection,pstmt);
        return true;
    }

}

2.7使用.properties文件来配置数据库基本信息

在建立和数据库的连接时,必然要有一些基本信息需要配置,如驱动名,数据库名,用户名,密码,当然还有配置数据库连接池的信息——初始化连接数,最大连接数,最大间隔时长等等。
当然可以选择去正常地在代码区去配置。但在这里使用了软编码的方式,即在外部文件中写下配置信息,然后加载这个文件进行配置,而不是直接在代码区。
这样的好处就是以后要更改基本信息,不需要修改源代码,只需修改外部的文件即可。
.properties是一个基于HashTable结构的文件,存储内容就是一些键值对。

driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/userInfo?characterEncoding=utf-8&useSSL=false&useUnicode=true
username=root
password=12345678
filters=stat
initialSize=2
maxActive=300
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
validationQuery=SELECT 1
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
poolPreparedStatements=false
maxPoolPreparedStatementPerConnectionSize=200

2.8.初步学习maven项目的配置

点这里,详见这篇博客。

2.9.个性化地创建美观、简洁、得体的swing组件

想要自己做出一些比较美观的效果图,主要就是要重写paintComponen方法和paint方法。
因为java swing中所有的组件都是画出来的,所以在自己制作一些组件的时候,也要熟悉一些基本操作,比如画出一个圆角矩形、一个圆,设置字体格式、大小,
设置背景色、前景色等等。同时为了让我们制作的组件具有一些动态效果,还要注意鼠标事件、键盘事件的运用。
下面举一些例子来加以说明。

  • 简洁的、美观的文本框:

在这里插入图片描述

制作这个搜索框
主要就是一个圆角矩形的绘制:

public class RoundRecTextField extends RoundRecBlankPanel
{

    private Color colorOfBackground = new Color(0,0,0,40) ;
    private Color colorOfText = new Color(0,0,0,80);

    private String text;
    private int width,height,arcw,arch;
    private JTextField jTextField;

    public RoundRecTextField(int width, int height, String text)  {
        super(width, height, 10, 10);
        this.width = width;
        this.height = height;
        this.text = text;
        init();
    }

    public RoundRecTextField(int width, int height, String text, Color colorOfBackground,Color colorOfText)  {
        super(colorOfBackground,width, height, 10, 10);
        this.colorOfBackground = colorOfBackground;
        this.colorOfText = colorOfText;
        this.width = width;
        this.height = height;
        this.text = text;
        init();
    }

    public void init()
    {

        int newH = (int)(0.70*height);
        int border = (int)(0.15*height);

        this.text = text;
        this.setLayout(null);

        jTextField = new JTextField(text,20);
        jTextField.setForeground(colorOfText);
        jTextField.setBackground(colorOfBackground);
        jTextField.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                //字体置为黑色
                jTextField.setForeground(null);
                jTextField.setText("");
                ((JTextField)e.getSource()).removeFocusListener(this);
            }
            @Override
            public void focusLost(FocusEvent e) {

            }
        });

        jTextField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                super.keyPressed(e);
                if(e.getKeyCode() == KeyEvent.VK_ENTER){
                    handler();
                }
            }
        });

        jTextField.setBorder(null);
        jTextField.setBounds(10,0,width-20,height);
        this.add(jTextField);
    }

    public void handler()
    {

    }

    public String getContent()
    {
        if(jTextField.getText().equals(text))
        {
            return "";
        }
        return jTextField.getText();
    }

    public void clear()
    {
        jTextField.setText("");
    }


//测试用
    public static void main(String[] args)  {
        JFrame jf = new JFrame();
        jf.setSize(425,750);
        jf.setLayout(null);

        jf.setLocationRelativeTo(null);
        jf.setResizable(false);
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        RoundRecTextField roundRecTextFileld = new RoundRecTextField(400,30,"你好啊");
        roundRecTextFileld.setBounds(10,10,400,30);

        RoundRecTextField roundRecTextFileld1 = new RoundRecTextField(400,30,"你好啊");
        roundRecTextFileld1.setBounds(10,50,400,30);

        jf.add(roundRecTextFileld);
        jf.add(roundRecTextFileld1);

        jf.setVisible(true);
    }
}

为了增加用户的使用好感,还可以借助FocusListener使得初始时文本框显示提示文字,然后用户点击文本框准备输入文字时,文字自动消失。

jTextField.addFocusListener(focusListener = new FocusListener() {
    @Override
    public void focusGained(FocusEvent e) {
        jTextField.setForeground(null);
        jTextField.setText("");
    }
    @Override
    public void focusLost(FocusEvent e) {
        jTextField.setForeground(firstColor);
        jTextField.setText(text);
    }
});
  • 圆角矩形

在这里插入图片描述
代码:

public class MyRoundButton extends JButton {

    private static final long serialVersionUID = 1L;

    private String nameOfButton = null;
    private Color colorOfButton = new Color(252, 237, 0);
    private Color colorOfString = Color.black ;
    private int x, y ;
    private int arcw=15,arch=15;
    private int style = 1;
    //按下按钮时字体的默认颜色

    //判断是否按下
    private boolean hover;
    private float clickTran = 0.6F, exitTran = 1F;
    //修改按下后透明度


    public MyRoundButton(String name) {
        this.nameOfButton = name;
        Init();
    }
    public MyRoundButton(String name,int arcw,int arch) {
        this.nameOfButton = name;
        this.arcw = arcw;
        this.arch = arch;
        Init();
    }

    public MyRoundButton(String name, Color colorOfButton) {
        this.nameOfButton = name;
        this.colorOfButton = colorOfButton;
        Init();
    }

    public MyRoundButton(String name, Color colorOfButton,Color colorOfString) {
        this.nameOfButton = name;
        this.colorOfButton = colorOfButton;
        this.colorOfString = colorOfString;
        Init();
    }
    public MyRoundButton(String name, Color colorOfButton,Color colorOfString,int arc) {
        this.nameOfButton = name;
        this.colorOfButton = colorOfButton;
        this.colorOfString = colorOfString;
        this.arcw = this.arch = arc;
        Init();
    }

    public void Init() {
        setBorderPainted(false);
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {  //鼠标移动到上面时
                hover = true;
                repaint();
            }
            @Override
            public void mouseExited(MouseEvent e) {  //鼠标移开时
                hover = false;
                repaint();
            }
        });
    }

    @Override
    protected void paintComponent(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();
        int h = getHeight(), w = getWidth();
        x = (int)(0.25*w);
        y = (int)(0.65*h);

        float tran = clickTran;
        if (!hover) {
            tran = exitTran;
        }
        //抗锯齿
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                tran));

        g2d.setColor(colorOfButton);
        g2d.fillRoundRect(0, 0, w - 1, h - 1, arcw, arch);
        g2d.setColor(new Color(0,0,0,50));
        g2d.drawRoundRect(0, 0, w - 1, h - 1, arcw, arch);
        g2d.setColor(colorOfString);
        g2d.setFont(new Font(null,style,(int)(0.45*h)));
        g2d.drawString(nameOfButton, x,y);
        g2d.dispose();
        super.paintComponent(g);
    }
}

三、关键代码

3.1.一个“异常”:Socket 传输对象的时候程序一直阻塞,但是不报错,

socket.getInputStream() 和 socket.getOutputStream() 是阻塞性函数,所以要严格按照顺序来构造。

// 服务端 :  先输入流、后输出流
ois = new ObjectInputStream(socket.getInputStream());
oos = new ObjectOutputStream(socket.getOutputStream());
// 先输出流、后输入流
oos = new ObjectOutputStream(s.getOutputStream());
ois = new ObjectInputStream(s.getInputStream());

3.2.重写swing组件的注意点

重写组件的时候,必须在重写方法的最后面添加super.paintComponent()方法,否则画出的图形无法显示完整。

protected void paintComponent(Graphics g) {
	//……………………
super.paintComponent(g);
}

而如果是重写paint方法,则应该在最开始将Graphics类的对象传给paint()。

public void paint(Graphics g){
	super.paint(g);
//……………………
}

3.3.List的自定义排序

在开发过程中有时需要对一个List中的元素进行排序。
对于java的集合,想要实现排序功能,有两种做法,T实现Comparable接口,但这样并不好,因为可能下一次的排序方式就发生了变化。比如一开始用户想要按照价格升序排序,之后又想按照商品的热度来排序等等。
解决方法,把实现Comparator<T>接口的类作为参数传给sort函数即可。
比如商品按照价格排序:

List<Product> productList = new ArrayList<>();
productList.sort(new CompareProductByHigherPrice());
public class CompareProductByHigherPrice implements Comparator<Product> {
    @Override
    public int compare(Product o1, Product o2) {
        return (int)(o1.getPrice()-o2.getPrice());
    }
}

3.4播放音频文件

3.4.1播放MP3文件

public class PlayMusic {
    public static Player player;

    public static void playMP3() {
        Thread thread = new Thread(() -> {
            try {
                try {
                    player = new Player(new BufferedInputStream(new FileInputStream(new File("src/main/resources/mp3/dingdong.mp3"))));
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
                player.play();
            } catch (JavaLayerException e) {
                e.printStackTrace();
            }
        });
        thread.start();
    }
}

3.4.2.播放WAV文件

    public static void playWAV(){
        Thread thread = new Thread(() -> {
            AudioInputStream as;
            try {
                as = AudioSystem.getAudioInputStream(new File("src/main/resources/mp3/folder.wav"));//音频文件目录
                AudioFormat format = as.getFormat();
                SourceDataLine sdl = null;
                DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
                sdl = (SourceDataLine) AudioSystem.getLine(info);
                sdl.open(format);
                sdl.start();
                int nBytesRead = 0;
                byte[] abData = new byte[512];
                while (nBytesRead != -1) {
                    nBytesRead = as.read(abData, 0, abData.length);
                    if (nBytesRead >= 0)
                        sdl.write(abData, 0, nBytesRead);
                }
                //关闭SourceDataLine
                sdl.drain();
                sdl.close();
            }catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
                e.printStackTrace();
            }
        });
        thread.start();
    }

3.5.实现窗口抖动

实现窗口抖动的本质其实就是让窗口的坐标发生变化。
于是,我模仿了简谐振动的运动规律,让窗体周期性的“震动”起来。
注意,循环执行过程中,要休息13毫秒的原因是不让窗体运动的太快,以致效果不明显。

//实现窗口抖动
public void tremble(){
    double[] T = new double[]{1,-1,-1,1};
    final int A = 30;
    int x = this.getX();
    int y = this.getY();
    for(int i = 0;i<8;i++)
    {
        int nx = x + (int)(A*T[i%4]);
        this.setLocation(nx,y);
        try {
            Thread.sleep(13);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    this.setLocation(x,y);

3.6.实现QQ聊天气泡

这是“我”发的消息对应的气泡,对方发的,类似。
首先,气泡也是画出来的,继承JComponent,重写paintComponent(Graphics g) 方法,具体的步骤是:
首先画出发消息的人的头像 :
再画出消息箭头 g.fillPolygon(xPoints, yPoints, nPoints):
然后在根据消息的宽度,以及高度,画出消息矩形框:g.fillRoundRect(x, y, width, height, arcWidth, arcHeight);
最后画出文字:g.drawString(str, x, y);

public class MessagePanelMe extends JPanel
{

//    private static int[] xLPoints = {65,65,53};
//    private static int[] yLPoints = {30,37,30};

    private static int[] xRPoints = {715,715,727};
    private static int[] yRPoints = {30,37,30};

    //大的面板的宽度
    private int width,height;
    private static Color grey = new Color(244,245,249);
    private static Color rightColor = new Color(211,245,255,150);

//    private Message message;
    private Message.MessageType messageType;
    private String text;
    private BufferedImage imageContent;
    private int xOfBubble,yOfBubble;
    //字符串的宽度
    private int bestLength,bestHeight;
    private ArrayList<String> stringArrayList;

    private Image head;

    FontMetrics fm = FontDesignMetrics.getMetrics(MyFont.getFontPlain(13));

    public MessagePanelMe(Message message, User friend) throws IOException {
        messageType = message.getMessageType();
        switch (messageType)
        {
            case PURE_STRING:
                this.text = message.getMsgStr();
                ProcessString processString = new ProcessString(text);
                stringArrayList = processString.getStringList();
                bestLength = getWidth(text)<=300?getWidth(text):300;
                bestHeight = stringArrayList.size()*16;
                this.width = 700;
                this.height = bestHeight+45;
                break;


            case PHOTOS:
                this.imageContent = message.getImages()[0];
                int width = imageContent.getWidth();
                int height = imageContent.getHeight();
                double rate = height*1.0/width;
                bestLength = width<=300?width:300;
                bestHeight = width<=300?height:(int)(300*rate);
                this.width = 700;
                this.height = bestHeight+45;
                break;
        }

        this.setLayout(null);
        this.setBackground(grey);
        this.setPreferredSize(new Dimension(width,height));
        this.head = friend.getHead();
    }

    private int getWidth(String string)
    {

        int width = fm.stringWidth(string);
        return width;
    }

    public void paint(Graphics g)
    {
        super.paint(g);
        //画头像及边框
        {
            Graphics2D graphics = (Graphics2D) g.create();
            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            BufferedImage src = (BufferedImage) head;
            BufferedImage newImg = null;
            switch (src.getType()) {
                case 5:
                    newImg = new BufferedImage(40, 40, 5);
                    break;
                case 6:
                    newImg = new BufferedImage(40, 40, 6);
                    break;
                default:
                    break;
            }
            // 根据图片尺寸压缩比得到新图的尺寸
            newImg.getGraphics().drawImage(
                    src.getScaledInstance(40, 40, Image.SCALE_SMOOTH), 0, 0,
                    null);
            graphics.drawImage(newImg, 730, 10, 40, 40, null);
            //画边框
            graphics.drawImage(MyImages.headBorderImage.getScaledInstance(40, 40, Image.SCALE_SMOOTH), 730, 10, 40,
                    40, null);

        }


        //画气泡
        Graphics2D g2d = (Graphics2D)g.create();
        {
            g2d.setColor(rightColor);
            g2d.fillPolygon(xRPoints, yRPoints, 3);
            xOfBubble = 695 - bestLength;
            yOfBubble = 15;
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.fillRoundRect(xOfBubble, yOfBubble, bestLength + 20, bestHeight + 24, 20, 20);
        }

        //画具体的消息
        switch (messageType) {

            case PURE_STRING:
                g2d.setColor(Color.black);
                 for (int i = 0; i < stringArrayList.size(); i++)
                 {
                     g2d.drawString(stringArrayList.get(i), xOfBubble + 10, 40 + i * 16);
                 }
                 break;

            case PHOTOS:
                xOfBubble = 695 - bestLength;
                yOfBubble = 15;
                g2d.drawImage(imageContent.getScaledInstance(bestLength, bestHeight, Image.SCALE_SMOOTH), xOfBubble+10,
                        yOfBubble+12, bestLength, bestHeight, null);
                break;


        }
    }
}
  private class ProcessString {
        private final int WIDTH = 300;
        private String text;
        public ArrayList<String> arrayList;
        public ProcessString(String text) {
            this.text = text;
        }

        public ArrayList<String> getStringList() {
            arrayList = new ArrayList<>();
            int width = getWidth(text);
            int length = text.length();

            if(width<WIDTH) {
                arrayList.add(text);
                return arrayList;
            }
            else {
                int beginIndex=0,endIndex=0;
                outer:while(beginIndex<length) {
                    while(getWidth(text.substring(beginIndex,endIndex))<WIDTH) {
                        endIndex++;
                        if(endIndex==length+1) {
                            arrayList.add(text.substring(beginIndex));
                            break outer;
                        }
                    }
                    endIndex--;
                    arrayList.add(text.substring(beginIndex,endIndex));
                    beginIndex = endIndex;
                }
            }
            return  arrayList;
        }
    }

其中最难以处理的是文字的排版。
下面是根据界面动态选择的最佳的文字排版方式。

  private class ProcessString {
        private final int WIDTH = 300;
        private String text;
        public ArrayList<String> arrayList;
        public ProcessString(String text) {
            this.text = text;
        }

        public ArrayList<String> getStringList() {
            arrayList = new ArrayList<>();
            int width = getWidth(text);
            int length = text.length();

            if(width<WIDTH) {
                arrayList.add(text);
                return arrayList;
            }
            else {
                int beginIndex=0,endIndex=0;
                outer:while(beginIndex<length) {
                    while(getWidth(text.substring(beginIndex,endIndex))<WIDTH) {
                        endIndex++;
                        if(endIndex==length+1) {
                            arrayList.add(text.substring(beginIndex));
                            break outer;
                        }
                    }
                    endIndex--;
                    arrayList.add(text.substring(beginIndex,endIndex));
                    beginIndex = endIndex;
                }
            }
            return  arrayList;
        }
    }
展开阅读全文
©️2020 CSDN 皮肤主题: 游动-白 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值