之前JAVA老师布置的大作业,自选标题,然后我选的是实现一个聊天软件,使用JAVAFX来做界面(跟SWING差不多,但是可以用CSS来美化界面,而且拖入式布局比较方便),实现的功能有登入注册,找回密码,更改头像,发送接收消息,查看好友资料,修改好友备注,好友是否在线的提示,消息的提示,气泡的大小自动改变,标为已读未读,清除聊天记录,删除好友,添加好友,搜索好友,好友备注,个人资料的修改查看,设置,聊天助手的提示,右键菜单等功能。运行结果如下:
(项目已上传至github,客户端:https://github.com/sundial-dreams/WeChatClient,服务端:https://github.com/sundial-dreams/WeChatServer,数据库的话也就三个表,可以看着下图建,然后将客户端代码的数据库连接部分修改一下,指向你的数据库)
登入:
注册:
忘记密码:
主界面:
好友资料:
添加好友:
个人资料:
修改个人资料:
头像:
好了,现在讲讲我的构建思路,由于界面比较多,使用我采用MVC的架构模式,包括控制模块(Controller),数据模块(Model),界面模块(View)
然后控制模块将数据和界面整合,对于数据模块,包括数据库的连接,消息的保存,好友列表的保存,登入信息的保存,数据库部分,我的个人资料包括九个属性,分别是account(账号),name(姓名),password(密码),age(年龄),sex(性别),head(头像),address(地址),label(个性标签),phone(电话号),background(主题),数据库表(使用mysql数据库)如下:
好友的话,用I_account(我的账号),Y_account(你的账号),remark(备注)来表示,数据库表如下:
然后还用了个登入表,来表示用户已登入,不可重复登入,退出时在清除掉改用户,数据库表如下:
然后就可以专门写个类来连接,操控数据库了,
package Model;
import java.sql.*;
/**
*
* 数据库控制类
* 化简数据库的操作
*/
public class DatabaseModel {
private String url = "jdbc:mysql://localhost:3306/wechat?useUnicode=true&characterEncoding=utf-8";
private final static String driver = "com.mysql.jdbc.Driver";
private String userName = "root";
private String password = "";
private Connection connection;
private Statement statement;//静态查询
private PreparedStatement preparedStatement;//动态查询
public DatabaseModel() {
}
/*
链接数据库
*/
public void connect(){
try {
Class.forName(driver).newInstance();
connection = DriverManager.getConnection(url, userName, password);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
*
* 该方法用来执行Sql语句并返回结果集 适合需要返回结果集的查询语句 例如 execResult("select*from user where id = ? and name = ?","1","jack");
* 用问号占位 然后传入个String数组代表要问号的值 该方法返回个结果集 即 ResultSet
*
* @param Sql
* @param data
* @return
* @throws SQLException
*/
public ResultSet execResult(String Sql, String... data) throws SQLException {
preparedStatement = connection.prepareStatement(Sql);
for (int i = 1; i <= data.length; i++) {
preparedStatement.setString(i, data[i - 1]);
}
return preparedStatement.executeQuery();
}
/**
*
*
* 执行Sql语句 不返回任何东西 例如exec("update user set password = ? where account = ?","password","name");
* exec("delete from user where name = ? and account = ?","name","account");
* exec("insert into user values(?,?,?,?,?,?,?,?,?)",1,2,3,4,5,6,7,8,9);
* @param Sql
* @param data
* @throws SQLException
*/
public void exec(String Sql, String...data) throws SQLException {
preparedStatement = connection.prepareStatement(Sql);
for (int i = 1; i <= data.length; i++) {
preparedStatement.setString(i, data[i - 1]);
}
preparedStatement.executeUpdate();
}
/**
* 执行静态SQL语句 例如exec("delete from user");
* @param Sql
*/
public void exec(String Sql) {
try
{
preparedStatement = connection.prepareStatement(Sql);
preparedStatement.executeUpdate();
}catch (Exception e){
}
}
/**
* 该方法插入个数据 例如insert(表名,要插入的数据(String数组的形式))
*
* @param tableName
* @param data
* @throws SQLException
*/
public void insert(String tableName, String... data) throws SQLException {
String pre = "";
for (int i = 0; i < data.length; i++) {
if (i != data.length - 1)
pre += "?,";
else
pre += "?";
}
String Sql = "INSERT INTO " + tableName + " VALUES(" + pre + ")";
preparedStatement = connection.prepareStatement(Sql);
for (int i = 1; i <= data.length; i++) {
preparedStatement.setString(i, data[i - 1]);
}
preparedStatement.executeUpdate();
}
/**
* 该方法删除表数据 例如delete(表名,删除时的条件(例如"id = ? AND name = ?"),传入问号代表的值)
*
* @param tableName
* @param condition
* @param data
* @throws SQLException
*/
public void delete(String tableName, String condition, String... data) throws SQLException {
String Sql = "DELETE FROM " + tableName + " WHERE " + condition;
preparedStatement = connection.prepareStatement(Sql);
for (int i = 1; i <= data.length; i++) {
preparedStatement.setString(i, data[i - 1]);
}
preparedStatement.executeUpdate();
}
/**
* 跟上面那些一样
*
* @param tableName
* @param target
* @param condition
* @param data
* @throws SQLException
*/
public void update(String tableName, String target, String condition, String data[]) throws SQLException {
String Sql = "UPDATE " + tableName + " SET " + target + " WHERE " + condition;
preparedStatement = connection.prepareStatement(Sql);
for (int i = 1; i <= data.length; i++) {
preparedStatement.setString(i, data[i - 1]);
}
preparedStatement.executeUpdate();
}
/**
* @param Sql
* @return
* @throws SQLException
*/
public ResultSet select(String Sql) throws SQLException {
statement = connection.createStatement();
return statement.executeQuery(Sql);
}
/**
* @param Sql
* @param data
* @return
* @throws SQLException
*/
public ResultSet select(String Sql, String... data) throws SQLException {
preparedStatement = connection.prepareStatement(Sql);
for (int i = 1; i <= data.length; i++) {
preparedStatement.setString(i, data[i - 1]);
}
return preparedStatement.executeQuery();
}
/**
* 得到静态查询对象
* @return
*/
public Statement getStatement() {
return statement;
}
/**
* 得到动态查询对象
* @return
*/
public PreparedStatement getPreparedStatement() {
return preparedStatement;
}
/**
* 得到数据库链接对象
* @return
*/
public Connection getConnection() {
return connection;
}
/**
* 数据库重连
* @param Url
* @param UserName
* @param Password
* @throws ClassNotFoundException
* @throws SQLException
*/
public void reConnection(String Url, String UserName, String Password) throws ClassNotFoundException, SQLException {
Class.forName(driver);
connection = DriverManager.getConnection(Url, UserName, Password);
}
}
然后就是要保存登入人的个人资料了,我的个人资料部分,属性比较多,可以直接写9个私有属性,也可用个map映射来保存资料,修改的时候,只需要覆盖原来的键的内容即可,私有数据直接是个Map:
private Map usermap;//对应的属性和值
关于消息的保存如下:
public staticVector>msg=newVector<>();//保存消息,意思是和第几个好友的聊天消息是什么
public staticMap>MsgMap=newHashMap<>();//保存消息,列表中的某个好友,及和该好友的聊天消息
public staticVectoraccountList=newVector<>();//保存好友账号
public static Map msgTip = new HashMap<>();//保存消息提示,某个好友,和他的消息提示
对于气泡的大小变化,需要两个助手函数,来根据输入的文字来获得最适高度和宽度:
如下:
//获取最适高度和宽度
public class Tool {
public static double getWidth(String Msg){//获得宽度
int len = Msg.length();
double width=20;
for(int i=0;i=480)
{
height+=17.4;
width=20;
}
}
return height;
}
private static final boolean isChinese(char c) {//判断是否为中文字符,及中文标点,中文字符比英文的要大
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|| ub == Character.UnicodeBlock.GENERAL_PUNCTUATION
|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) {
return true;
}
return false;
}
}
关于界面模块,我使用的是javafx做界面,好处在于可以用CSS美化,而且它的拖入式布局比较方便,然后我的每个界面都是去除了操作系统的装饰,所以就自定义了最小化,退出,窗口拖拽等,然后每一个窗口都具有这些操作,所以可以定义个抽象类window,来定义这些方法:
public abstract class window extends Stage {
Parent root;
private double xOffset;
private double yOffset;
//设置图标方法
public void setIcon(){
getIcons().add(new Image(getClass().getResourceAsStream("/View/Fxml/CSS/Image/icon.png")));
}
/** * 窗口移动方法 */
public void move() {
root.setOnMousePressed(event -> {
xOffset = getX() - event.getScreenX();
yOffset = getY() - event.getScreenY();
getRoot().setCursor(Cursor.CLOSED_HAND);
});
root.setOnMouseDragged(event -> {
setX(event.getScreenX() + xOffset);
setY(event.getScreenY() + yOffset);
});
root.setOnMouseReleased(event -> {
root.setCursor(Cursor.DEFAULT);
});
}
/** * 抽象方法 窗口退出操作 */
abstract public void quit();
/** * 最小化 */
abstract public void minimiser();
/** * 获取root * * @return */
public Parent getRoot() {
return root;
}
/** * 选择界面元素 * * @param id * @return */
public Object $(String id) {
return (Object) root.lookup("#" + id);
}
}
之后再利用window类来派生出不同的界面类,如登入界面,主界面,修改资料界面等等,这样就可以获得不同的界面了,对于界面都可以使用javafx的secen build快速做出来,添加一些CSS样式,但做出来的是Fxml文件(类似于HTML),就得利用
Parentroot= FXMLLoader.load(getClass().getResource("Fxml/Dialog.fxml"));
的方式加载个文档对象,而root代表的就是整个界面文档,可以通过一些方法来获取界面文档中的特定元素,如(Button)root.lookup("#dialog")这样获取的就是文档中id叫dialog的按钮,所以可以使用这种方式来为界面中的不同元素设置事件,或获取内容等,可以对其进行封装,封装成一个方法,如:
public Object $(String id) {
return (Object) root.lookup("#" + id);
}
,对于每一个输入框都得用正则表达式匹配看看输入是否符合规范,比如账号,规定的账号只能是中文或数字或英文,并且在1-15位,所以对于的表达式为
"^[0-9,a-z,A-Z,\\u4e00-\\u9fa5]{1,15}$",
对于聊天的内容可以用个ListView来保存,聊天的内容也相当于是一个列表,然后根据不同的消息,添加不同的Pane,对于气泡,三角形是用一张图片做上去的,而内容框就是TextArea,设置为不可用,然后通过CSS改变颜色,和三角形一样的颜色
然后就是控制模块了,控制模块要做的事就是把数据和界面整合在一起,把每个界面类,和数据操作类都做为它的私有属性,然后用界面来展示数据,完成界面的交互操作,比如登入框的按钮点击,登入框隐藏,主界面显示等,每一个功能写一个方法,比如登入功能,就写一个public void dialog()//方法
然后就是接收消息的部分,得开个线程来监听别人发来的消息,利用socket来监听服务器发来的消息,例如Socket socket = new Socket("127.0.0.1",2347)//监听本机的2347端口,然后在这个端口上有个服务端,专门往这个端口发消息,消息可以用JSON格式来传,为了简单就直接传个String,然后把消息分为几种情况:
1.#### 姓名 #### 断开连接的消息,并把该用户的消息广播给所以在线用户,如果在线用户中有这个人,就把他的状态设置为离线
2.###@ user1 user2 添加的消息,user1把user2添加为好友.user2就要将user1添加到他的聊天列表中去
3.##@@ user1 user2 删除好友的消息,user1把user2删除,user2接收到这条消息,就要将user1在其好友列表中删除
4.user1 user2 msg 一般消息,user1 给user2发送消息,并把这个消息发给user2
5.#@@@ user1 #### user1上线的消息,广播给全体在线客户
6.@@@@ user1 #### user1下线的消息,广播给全体在线客户
通过这些类型的消息,客户端解析服务端过来的消息,根据不同的消息干不同的事。
对于服务端,使用Map
public void sendMsg(String from,String to,String Msg) throws IOException {
for(Map.Entry entry:map.entrySet()){
ChatSocket socket = entry.getValue();
if(entry.getKey().equals(to))//找到要发给谁
socket.out(from+" "+to+" "+Msg);//然后把消息发出去即可。
}
}
就可以了。
如果上面的没看太懂,可以参考我的工程,客户端:JavaFx WeChat聊天软件客户端
服务端:Java WeChat聊天软件服务端
(项目已上传到GitHub,如果觉得不错,欢迎点赞,客户端:https://github.com/sundial-dreams/WeChatClient,服务端:https://github.com/sundial-dreams/WeChatServer,数据库的话也就三个表,可以看着下图建,然后将客户端代码的数据库连接部分修改一下,指向你的数据)
本人现在上大二,学java没多久,如过有什么说得不对的地方,请多多包含。