文章目录
- 一、结论分析与体会
- 1.1.技术部分
- 1.1.1.swing
- 1.1.2.多线程
- 1.1.3.数据库
- 1.1.4.网络
- 1.1.5.集合与泛型
- 1.1.6.接口与内部类
- 1.2.内心感悟
- 二、主要技术难点
- 2.0系统模块架构
- 2.1.服务端和客户端进行数据交互的形式
- 2.2.Socket编程实现聊天
- 2.3.多线程的应用以及用线程池管理线程
- 2.4.DAO模式的初步了解——数据库访问模块的封装
- 2.5服务端处理客户端请求的逻辑层次
- 2.6.多线程并发下使用数据库连接池的必要性
- 2.7使用.properties文件来配置数据库基本信息
- 2.8.初步学习maven项目的配置
- 2.9.个性化地创建美观、简洁、得体的swing组件
- 三、关键代码
- 3.1.一个“异常”:Socket 传输对象的时候程序一直阻塞,但是不报错,
- 3.2.重写swing组件的注意点
- 3.3.List的自定义排序
- 3.4播放音频文件
- 3.4.1播放MP3文件
- 3.4.2.播放WAV文件
- 3.5.实现窗口抖动
- 3.6.实现QQ聊天气泡
本文内容过长,各位看官老爷们,酌量食用~~
戳这里,观看本系统的完整的演示视频哦~~
先让大家看一下效果!
登录注册:
聊天列表
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;
}
}