文章目錄
本文內容過長,各位看官老爺們,酌量食用~~
先讓大家看一下效果!
登錄註冊:
聊天列表
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;
}
}