Java实现仿QQ界面多人聊天客户端程序

看到此文,是否觉得体内洪荒之力爆发,饥渴难耐想吐槽、情不自禁想捐赠
本文为原创文章,尊重辛勤劳动,可以免费摘要、推荐或聚合,亦可完整转载,但完整转载需要标明原出处,违者必究。

支付宝微  信

记得几年前,在校期间写过一个聊天程序,也发布了一篇博客在csdn上。然而,近期有好多网友加我qq索要源码,可惜的是源码早已消失在网络中了。所以,借此闲暇时间重写一次Java多人聊天客户端程序,以供爱好者学习交流之用。如下是每日程序的进展日志。

项目源码:

客户端 -> github.com/genialx/ChatX

服务端 -> github.com/genialx/ChatXServer

前面的话

对于Java,笔者算是新手,没有用Java做过实际的项目。所以,在做这个项目的过程中,进行了大量的搜索工作,有很多问题都无法短时间内解决。固然,项目中的代码是很糟糕的,甚至垃圾代码。不过,有时间会进行深入的学习来优化项目甚至重构代码。

进展日志

2016.02.12

项目不是起于今日,目前已经完成了客户端的登陆界面,正在着手完善朋友列表的界面。

3A832476-A469-4A9C-B990-5C195D8A370C

登陆界面

1]{]D5HEB~9$(IKA([V@DJS

朋友列表

遇到的一些不是问题的问题...

容器组件半透明的问题

起初进行了搜索引擎,大致分为两种。一种是利用重写父类的重画方法,个人尝试了网上的代码几次,不成功,也觉得Java应该能提供半透明的API,不至于还要重写于是就放弃了。

第二种是利用com.sun.awt.AWTUtilities类进行设置。

com.sun.awt.AWTUtilities.setWindowOpacity(this, 0.5f); 

但是,了解到AWT在Jre8中不再存在,同时网上描述说兼容不好,所以放弃了。

于是在网上找到了这个很简单的API,也刚好能够满足我的预期效果,如图“朋友列表”中的透明效果。

jPanel.setBackground(new Color(1, 1, 1, 0.95f));

如果是JScrollPane容器的话,需要如下设置。

JScrollPane.setBackground(new Color(1, 1, 1, 0.5f));
JScrollPane.getViewport().setBackground(new Color(1, 1, 1, 0.5f));

这是因为JScrollPane容器管理着视口、可选的垂直和水平滚动条以及可选的行和列标题视口。

JScrollPane容器

JScrollPane组成

 

JTextField容器输入文字内边距设置问题(未解决)

首先我在Eclipse的自动方法提示窗口里面找了所有set开头的方法,并在搜索引擎搜了几圈,最终也没能找到解决方案。感觉搜索引擎中基本上没有这个问题的提问,估计是我的关键词有问题吧。最后,通过改变JTextField的位置,以及JTextField所在容器的背景颜色来实现,输入框的内左边距的效果。

JScrollPane滚动时画布重画的问题(未解决)

如“朋友列表”界面,由于JScrollPane采用了半透明的机制,导致窗口滚动时半透明的效果失效,显示灰白色的底色。如下图:

1

JScrollPane滚动时半透明失效

而当触发重画方法时,JScrollPane界面又恢复半透明效果。所以,通过给JScrollPane加监听鼠标滚轮事件。

	class FriendListScrollPaneMouseWheelListener implements MouseWheelListener{
		public void mouseWheelMoved(MouseWheelEvent e) {
			List.this.updateG();
		}
	}

但是仍然无效,原因应该是JScrollPane滚动时先触发mouseWheelMoved事件,再触发界面滚动的效果事件。所以,由于监听mouseWheel事件过早导致重画效果被后面的滚动效果覆盖失效。不知道这个问题该如何解决...

JScrollPane无滚动条的问题

		/** scrollpane **/
		this.listScrollPane = new JScrollPane();
		this.listScrollPane.setBorder(null);
		this.listScrollPane.setBounds(0, 0, 280, 500);
		this.listScrollPane.setBackground(new Color(1, 1, 1, 0.5f));
		this.listScrollPane.getViewport().setBackground(new Color(1, 1, 1, 0.5f));
		
		/** friendlist jlabel **/
		int friendListCount  = 20;
		this.scrollPanePanel = new JPanel();
		this.scrollPanePanel.setSize(new Dimension(260, friendListCount * 50));
		this.scrollPanePanel.setOpaque(false);
		this.scrollPanePanel.setLayout(null);

如上述代码,是无法显示滚动条的,尽管scrollPanePanel的高度(20 * 50)是大于listScrollPane的高度(500),如下图:
no_scroll
但是若将上述代码的11行的setSize方法改成setPreferredSize方法,滚动条就显示了。这是为啥?如下解释是来自网络的文章

原因:
The short answer is: it's complicated.
The slightly longer answer is: use setSize() if your component's parent has no layout manager, and setPreferredSize() and its related setMinimumSize and setMaximumSize if it does.
setSize() probably won't do anything if the component's parent is using a layout manager; the places this will typically have an effect would be on top-level components (JFrames and JWindows) and things that are inside of scrolled panes. You also must call setSize if you've got components inside a parent without a layout manager.
As a general rule, setPreferredSize() should do the "right thing" if you've got a layout manager; most layout managers work by getting the preferred (as well as minimum and maximum) sizes of their components, and then using setSize() and setLocation() to position those components according to the layout's rules. So (as an example) a BorderLayout will try to make the bounds of its "north" region equal to the preferred size of its north component - they may end up larger or smaller than that, depending on the size of the frame, the size of the other components in the layout, and so on

上面大概意思说的就是:

1.setPreferredSize需要在使用布局管理器的时候使用,布局管理器会获取空间的preferredsize,因而可以生效。例如borderlayout在north中放入一个panel,panel的高度可以通过这样实现:panel.setPreferredSize(new Dimension(0, 100));这样就设置了一个高度为100的panel,宽度随窗口变化。

2.setSize,setLocation,setBounds方法需要在不使用布局管理器的时候使用,也就是setLayout(null)的时候可以使用这三个方法控制布局。

区分好这两个不同点之后,我相信你的布局会更随心所欲。

英文来源:http://stackoverflow.com/questions/1783793/java-difference-between-the-setpreferredsize-and-setsize-methods-in-compone

 

组件画布重画频繁的问题

如“朋友列表”图中的列表(由JPanel构成),每一个人都是一个JPanel构成的。这个JPanel监听mouse事件,每当触发相应事件就会进行底色的改变。点击底色为深黄,悬浮底色变浅黄,如图:mouse_yellow

事件代码如下:

		public void mouseEntered(MouseEvent e) {
			if(this.index == List.this.currentFriendListPanelIndex) {
				return;
			}
//			System.out.println("mouse entered the friendlist panel event.");
			JPanel jPanel = (JPanel)e.getSource();
			jPanel.setOpaque(true);
			jPanel.setBackground(new Color(249, 238, 194));
			List.this.updateG();
		}

		@Override
		public void mouseExited(MouseEvent e) {
			if(this.index == List.this.currentFriendListPanelIndex) {
				return;
			}
//			System.out.println("mouse exited the friendlist panel event.");
			JPanel jPanel = (JPanel)e.getSource();
			jPanel.setOpaque(false);
			List.this.updateG();
		}
		
		public void mouseClicked(MouseEvent e) {
			long now = new Date().getTime();
			if(List.this.lastClickFriendListPanelIndex == this.index) {
				if(now - List.this.lastClickFriendListPanelTimestamp < 500) {
					/** double clicked event on the friendlist jpanel to open the chat window. **/
					System.out.println("Open the chat window.");
					new Chat();
				}
			}
			/** remember the lastClick info. **/
			List.this.lastClickFriendListPanelTimestamp = now;
			List.this.lastClickFriendListPanelIndex 	= this.index;
			
			/** set the last current panel to normal status. **/
			if(List.this.currentFriendListPanelIndex != -1) {
				List.this.friendListPanel[List.this.currentFriendListPanelIndex].setOpaque(false);
			}
			/** set current panel to clicked status **/
			List.this.currentFriendListPanelIndex = this.index;
			List.this.friendListPanel[this.index].setOpaque(true);
			List.this.friendListPanel[this.index].setBackground(new Color(250, 233, 172));
//			System.out.println("mouse clicked the friendlist panel event.");
			List.this.updateG();
		}

可是当鼠标过快滑动,会频繁的触发事件,今儿频繁的调用List.this.updateG()方法进行界面的重画。这时根据电脑的性能,会出现不用程度的卡顿显示。那么,由于对于重画机制不是很熟悉,目前先走个捷径。通过限制重画的频率来避免这个问题。因为鼠标过快滑动是问题所在,那么久避免这个条件的产生即可。如代码:

	public void updateG() {
		if(this.lastUpdateGTimestamp == -1) {
			this.lastUpdateGTimestamp = new Date().getTime();
		}
		long now = new Date().getTime();
//		System.out.println("last udpateG timestamp:" + this.lastUpdateGTimestamp + " diff:" + (now - this.lastUpdateGTimestamp));
		if(now - this.lastUpdateGTimestamp > 50 || now - this.lastUpdateGTimestamp < 5) {
			this.lastUpdateGTimestamp = now;
			List.this.paint(List.this.getGraphics());
		}
	}

只有当两次触发间隔大于50ms时,才会触发重画(代码中的小于5ms的判断是排除鼠标同时触发两个事件的情况,比如鼠标移开某个JPanel进入另一个JPanel会依次触发existed和entered,这两个事件的触发时间间隔小于5ms)。

 

组件容器的位置问题

记得当时学习的时候接触到的布局常用的就是什么,FlowLayout、CardLayout、GridLayout和BorderLayout等。不过,并不清楚其中的细节,忘掉了很多。也是因为懒惰,大部分布局都采用了绝对布局。优点就是编码简单易阅读,缺点就是无法做出可响应的模式。后续有时间再了解下布局这一块。

2016.04.27

最近工作上有紧急项目,导致年后到现在都没能腾开空搞这个小项目。马上五一了,紧急项目也准备提测了,腾出时间来做做吧。

这几天完成了客户端与服务端通信的模块,随之建立了服务端项目:ChatXServer(项目地址如上文)。

客户端与服务端通信功能

这一块其实是这个项目的核心功能。简单聊下采用的实现方式和原理。

原理:阻塞IO+多线程

实现方式:

服务端监听本机某端口,每当接受一个新的socket后,将socket存入自定义的客户端线程池后,启动当前客户端线程的start方法。进而反复以上动作(红字)。

同时服务端也开启了一个定时任务进程(crontab),以轮询当前客户端线程池中每个客户端线程,检查是否还存在“心跳”,及时处理垃圾。

上文“心跳”的含义是代表当前客户端线程是否是处于连接活跃状态,若是代表有“心跳”,若无则代表无“心跳”。

客户端监听键盘和鼠标等相关事件,将用户输入的信息发送给服务端。服务端解析来自客户端的信息,进行消息处理、业务处理或消息转发等动作。

同时客户端也在监听来自服务端发来的“命令”,以进行相应的动作。

上文中客户端与服务端通信的数据是通过序列化后进行传输的。如代码(客户端向服务端写数据):

	/** for test **/
	Socket socket ;
	ObjectOutputStream o ;
	private void sendMsgTest() {
		try {
			if(this.socket == null) socket = new Socket("127.0.0.1", 1720);
			if(this.o == null) o = new ObjectOutputStream(socket.getOutputStream());
			TextMessage textMessage = new TextMessage();
			textMessage.set("uid", "233");
			textMessage.set("hello", "work");
			textMessage.set("clientKey", "123123123");
			MessageManager mM = new MessageManager(MessageManager.TYPE_CHAT_TEXT_MSG);
			mM.setTextMessage(textMessage);
			o.writeObject(mM);
		} catch (IOException e) {
			try {
				this.socket = new Socket("127.0.0.1", 1720);
				this.o = new ObjectOutputStream(socket.getOutputStream());
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			e.printStackTrace();
		}
	}

上述代码中的MessageManager和TextMessage类需要继承Serializable接口以实现序列化。

package com.ihuxu.chatxserver.common.model;

import java.io.Serializable;

public class MessageManager implements Manager, Serializable{

	private static final long serialVersionUID = 5090328872687319042L;
	
	private int type = MessageManager.TYPE_UNKNOWN;
	
	private TextMessage textMessage;
	// ...

2016.08.29

写到这里,我自己都笑了。一个小项目拖到现在。而且,不知道还要拖多久。

  • 服务端消息异步处理,包括聊天文本消息、登录事件消息
  • 服务端中客户端线程池的异常处理,保证掉线后重连
  • 客户端消息发送与回显(部分)

未完待续...

文章来源:胡旭个人博客 => 【原】Java实现仿QQ界面多人聊天客户端程序

转载请注明出处,违者必究!


这是一篇原创文章,如果您觉得有价值,可以通过捐赠来支持我的创作~
捐赠者会展示在博客的某个页面,钱将会用在有价值的地方,思考中...


分类: JAVA, 技术, 编程 | 标签: , , , , , , | 4个评论 | Permalink

4个评论

发表评论

电子邮件地址不会被公开。