记得几年前,在校期间写过一个聊天程序,也发布了一篇博客在 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);
no_scroll

如上述代码,是无法显示滚动条的,尽管 scrollPanePanel 的高度(20 * 50)是大于 listScrollPane 的高度(500),如下图:

但是若将上述代码的 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 需要在使用布局管理器的时候使用,布局管理器会获取空间的 preferred size,因而可以生效。例如 border layout 在 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

组件画布重画频繁的问题

mouse_yellow

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

事件代码如下:

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

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

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

未完待续…

Share:

5 comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.