之前JAVA老师布置的大作业,自选标题,然后我选的是实现一个聊天软件,使用JAVAFX来做界面(跟SWING差不多,但是可以用CSS来美化界面,而且拖入式布局比较方便),实现的功能有登入注册,找回密码,更改头像,发送接收消息,查看好友资料,修改好友备注,好友是否在线的提示,消息的提示,气泡的大小自动改变,标为已读未读,清除聊天记录,删除好友,添加好友,搜索好友,好友备注,个人资料的修改查看,设置,聊天助手的提示,右键菜单等功能。运行结果如下:
登入:
注册:
忘记密码:
主界面:
好友资料:
添加好友:
个人资料:
修改个人资料:
头像:
好了,现在讲讲我的构建思路,由于界面比较多,使用我采用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<String,String> usermap;//对应的属性和值关于消息的保存如下:
public staticVector<Vector<String>>msg=newVector<>();//保存消息,意思是和第几个好友的聊天消息是什么
public staticMap<String,
Vector<String>>MsgMap=newHashMap<>();//保存消息,列表中的某个好友,及和该好友的聊天消息
public staticVector<String>accountList=newVector<>();//保存好友账号
public static Map<String,Integer> msgTip = new HashMap<>();//保存消息提示,某个好友,和他的消息提示
对于气泡的大小变化,需要两个助手函数,来根据输入的文字来获得最适高度和宽度:
如下:
//获取最适高度和宽度
public class Tool { public static double getWidth(String Msg){//获得宽度 int len = Msg.length(); double width=20; for(int i=0;i<len;i++) { if(isChinese(Msg.charAt(i))){ width+=17;//一个中文字符占17个大小 } else { width+=9;//其他占9个大小 } } //29中 17=15px/64英 8=7px if(width<=480)//气泡最宽设置为480 { return width; } else { return 480; } } public static double getHight(String Msg){//获得高度 int len = Msg.length(); double width = 20; double height = 40; for(int i=0;i<len;i++){ if(isChinese(Msg.charAt(i))){ width+=17; } else { width+=9; } if(width>=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上线的消息,广播给全体在线客户
[email protected]@@@ user1 #### user1下线的消息,广播给全体在线客户
通过这些类型的消息,客户端解析服务端过来的消息,根据不同的消息干不同的事。
对于服务端,使用Map<String,Socket>的方式保存客户端的Socket,键是账号,账号是唯一的,然后也得开个线程来处理消息,每个线程处理一个客户端Socket的方式,然后对应的发消息,user1 user2 Msg的消息就可以发送了,比如:
public void sendMsg(String from,String to,String Msg) throws IOException { for(Map.Entry<String ,ChatSocket> entry:map.entrySet()){ ChatSocket socket = entry.getValue(); if(entry.getKey().equals(to))//找到要发给谁 socket.out(from+" "+to+" "+Msg);//然后把消息发出去即可。
} }就可以了。
如果上面的没看太懂,可以参考我的工程,客户端:JavaFx WeChat聊天软件客户端
本人现在上大二,学java没多久,如过有什么说得不对的地方,请多多包含。