一、概述
在 LocalChatRoom 局域网聊天室项目中,我负责客户端界面与交互层的开发。这一层是用户直接面对的前端,承担着登录引导、消息展示、交互操作和状态反馈等全部 UI 职责。
我负责的三个核心文件分别是:
| 文件 | 职责 |
| LoginDialog.java | 登录对话框:服务器地址/端口/昵称输入与校验 |
| ChatFrame.java | 主窗口框架:标签页管理、用户列表、私聊路由、状态栏 |
| ChatPanel.java | 聊天面板组件:消息渲染、输入发送、表情选择器 |
这三个文件合计约 753 行代码,构成了客户端的完整 UI 层。它们通过调用组长提供的 ChatClient.send(Message) 发送消息,并通过实现 ChatFrame.handleMessage(Message) 接收并渲染消息,以 Message 类为唯一耦合接口实现了前后端分离。
二、LoginDialog —— 登录对话框
LoginDialog 是用户启动客户端后看到的第一个窗口,负责收集连接参数并通过校验后才放行。
2.1 核心设计
- 模态对话框:继承 JDialog,构造时传入 modal = true,阻塞父窗口直到用户完成或取消登录。
- 布局结构:BorderLayout 整体布局,NORTH 为标题,CENTER 为表单面板(GridLayout(3, 2)),SOUTH 为操作按钮。
- 默认焦点:窗口打开时通过 WindowListener.windowOpened 自动将焦点定位到昵称输入框,减少用户鼠标操作。
2.2 局域网 IP 自动检测
private String detectLocalIP() { Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces(); while (nets.hasMoreElements()) { NetworkInterface net = nets.nextElement(); if (net.isLoopback() || !net.isUp()) continue; Enumeration<InetAddress> addrs = net.getInetAddresses(); while (addrs.hasMoreElements()) { InetAddress addr = addrs.nextElement(); String ip = addr.getHostAddress(); // 过滤 IPv6、回环地址、APIPA 自动配置地址 if (ip.contains(":") || ip.startsWith("127.") || ip.startsWith("169.254.")) continue; return ip; } } return "localhost"; // 兜底 }遍历本机所有网络接口,跳过回环接口、已禁用的接口以及 IPv6 和 APIPA 地址(169.254.x.x),返回第一个有效的局域网 IPv4 地址。检测失败时回退到 "localhost",兼顾单机测试场景。
2.3 昵称校验
- 空值校验:昵称为空时弹出警告弹窗并重新聚焦输入框。
- 逗号校验:昵称不能包含逗号,因为服务端使用逗号分隔在线用户列表,昵称含逗号会破坏协议解析。
2.4 对外接口
提供四个 Getter 方法供 ChatClient 读取用户输入:isConfirmed()、getHost()、getPort()、getNickname()。
三、ChatFrame —— 主窗口框架
ChatFrame 是整个客户端的中枢,承担消息路由、标签管理、用户列表和状态反馈等核心职责。
3.1 整体布局
JSplitPane 左右分割,左侧占比约 78%(setResizeWeight(1.0)),窗口缩放时优先保证聊天区域的空间。右侧用户列表固定首选宽度 185px。
3.2 标题栏渐变背景
重写 JPanel.paintComponent(),使用 GradientPaint 从左到右(皇家蓝 → 矢车菊蓝)渐变填充,配合白色字体。
3.3 JTabbedPane 多标签管理
| 标签类型 | 标题 | 可关闭 | 创建方式 |
| 大厅(群聊) | 🏠 大厅 | 否 | 构造时创建,始终存在 |
| 私聊标签 | 💬<对方昵称> | 是 | 双击用户列表或收到私聊消息时自动创建 |
私聊标签关闭按钮:通过 buildClosableTabHeader() 为每个私聊标签构建自定义标签头组件,关闭按钮鼠标悬停时变红。重复打开保护:openPrivateChat(target) 先检查 privatePanels Map 是否已存在该用户的私聊面板,存在则直接切换到该标签而非重复创建。
3.4 用户列表与双击私聊
使用 DefaultListModel<String> + JList,自定义 UserCellRenderer:每个用户项显示为 ● <昵称> (我),自己的昵称加粗且设为蓝色。双击列表项触发 openPrivateChat(target),含防呆设计:不能和自己私聊。
3.5 未读角标逻辑
- 大厅未读:新群聊消息到达但大厅不是当前活动标签时,标题显示 🏠 大厅 ●,切换回大厅后自动清除。
- 私聊未读:通过 ChatPanel 的未读计数回调机制,在标签标题中显示数字角标如 💬 Alice [3],且标题文字变红。
3.6 断线状态切换
setConnected(boolean) 统一管理:状态栏从绿色 ● 已连接 切换为红色 ● 已断开;所有面板输入框全部禁用。
3.7 消息路由(handleMessage)
switch (msg.getType()) { case TEXT: → hallPanel.receiveMessage(msg); // 群聊 → 大厅 case PRIVATE: → handlePrivateMessage(msg); // 私聊 → 路由到对应面板 case JOIN: → hallPanel.appendStatus(...); // 上线通知 case LEAVE: → hallPanel.appendStatus(...); // 下线通知 case USER_LIST: → updateUserList(msg.getContent()); // 刷新在线列表 case SYSTEM: → hallPanel.appendSystem(...); // 系统消息 }这是消息从网络层进入 UI 层的唯一入口,由 ChatClient 的后台接收线程在 EDT 中回调。
四、ChatPanel —— 聊天面板
ChatPanel 是群聊大厅和私聊共用的核心 UI 组件,通过 chatTarget 字段区分模式:null 为群聊,非 null 为私聊。私聊模式下顶部显示浅蓝色提示条 "🔒 与 XXX 的私聊 — 消息仅你们可见"。
4.1 JTextPane 富文本渲染
选择 JTextPane + StyledDocument,实现每条消息的颜色和样式独立控制:
| 消息角色 | 发送者颜色 | 内容颜色 | 说明 |
| 自己发送 | 蓝色(30,100,220) | 蓝色 | 加粗发送 |
| 他人发送 | 红色(200,60,60) | 黑色 | —— |
| 系统消息 | —— | 灰色 | 斜体,前缀 [系统] |
| 状态消息 | 绿色 | —— | 斜体,前缀 ► |
| 时间戳 | 灰色 | —— | 小字号(11px) |
每次追加消息动态创建新的 Style 对象并通过 doc.insertString() 插入文档末尾,最后执行 scrollDown() 将插入符滚动到最新消息处。
4.2 输入与发送
- 回车发送:inputField.addActionListener(e -> doSend())。
- 发送按钮:根据 chatTarget 判断创建 TEXT(群聊)或 PRIVATE(私聊)消息,调用 client.send(msg) 发出,随后清空输入框并重新聚焦。
- 发送按钮悬停效果:鼠标进入时变亮,离开时恢复。
4.3 表情选择器
非模态 JDialog,内含30 个 Emoji,以 GridLayout(3, 10) 网格排列。每个按钮使用 Segoe UI Emoji 字体渲染,点击后将对应字符追加到输入框末尾并自动关闭选择器。
4.4 未读计数回调机制
private int unreadCount = 0; private Runnable onUnreadChange; // 回调接口 public void receiveMessage(Message msg) { appendChat(msg); if (chatTarget != null && onUnreadChange != null) { unreadCount++; onUnreadChange.run(); // 通知 ChatFrame 刷新标签标题 } }通过 Runnable 回调通知父组件而非直接持有引用,降低了耦合度。
五、技术难点与亮点
5.1 Swing EDT 线程安全
Swing 是单线程模型,所有 UI 操作必须在 EDT 中执行。消息从网络线程到达后,通过 SwingUtilities.invokeLater() 投递到 EDT 才能安全更新 UI,本项目通过 ChatClient 在后台接收线程中包装 invokeLater 调用 handleMessage()。
5.2 JTextPane 样式控制
相比 JTextArea 只能设置全局字体和颜色,JTextPane + StyledDocument 允许在同一文档内对不同段落的文字设置独立的颜色、字号、粗体和斜体。每追加一条消息需动态创建 Style 对象并管理插入符位置。
5.3 标签页动态管理
JTabbedPane 默认不提供标签关闭按钮,需自定义标签头组件。同时维护 privatePanels HashMap 跟踪已创建的私聊面板,关闭标签时既要移除 Swing 组件也要清除 Map 引用,防止内存泄漏。
5.4 未读角标刷新
需在两个维度正确更新:消息到达时通过回调通知 ChatFrame;切换标签时通过 ChangeListener 清零。大厅标签和私聊标签使用两套不同的角标策略。
5.5 局域网 IP 自动检测
正确过滤回环地址、IPv6 和 APIPA 地址,让大多数用户无需手动输入服务器 IP。
5.6 渐变色标题栏与自定义渲染
GradientPaint 渐变背景 + UserCellRenderer 自定义用户列表外观,让纯 Java Swing 应用摆脱原生控件的廉价感。
六、总结
本次 LocalChatRoom 项目的客户端 UI 层开发让我对 Java Swing 有了系统性的实践理解。最大收获在于:组件化思维——将 ChatPanel 设计为群聊和私聊共用的通用组件,通过一个 chatTarget 字段区分行为模式,避免了两套代码的重复维护;以及回调解耦——ChatPanel 通过 Runnable 回调通知父组件而非直接持有引用,让组件边界更加清晰。
本项目为两人协作完成,我负责的界面与交互层代码共 753 行,完整实现了登录、群聊、私聊、表情、未读提醒等全部 UI 功能。