大一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;
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章