在视频会议或即时通讯软件中,一个美观且功能完善的聊天界面是必不可少的。本文将介绍如何使用Qt实现一个高度自定义的聊天消息控件ChatMessage,支持文本气泡、头像显示、多类型消息和自动换行等特性。
一、ChatMessage控件概述
ChatMessage是一个继承自QWidget的自定义控件,专门用于聊天场景中的消息展示。它可以显示四种类型的消息:
- 用户自己发送的消息(右对齐,蓝色气泡)
- 他人发送的消息(左对齐,白色气泡)
- 系统通知消息(居中,灰色文本)
- 时间戳消息(居中,浅灰色时间分隔符)
二、核心功能与实现原理
2.1 控件初始化
控件的构造函数负责初始化基本样式和资源:
ChatMessage::ChatMessage(QWidget *parent) : QWidget(parent)
{
// 设置默认字体
QFont te_font = this->font();
te_font.setFamily("MicrosoftYaHei");
te_font.setPointSize(12);
this->setFont(te_font);
// 加载头像资源
m_leftPixmap = QPixmap(":/myImage/1.jpg");
m_rightPixmap = QPixmap(":/myImage/1.jpg");
// 初始化"发送中"动画
m_loadingMovie = new QMovie(this);
m_loadingMovie->setFileName(":/myImage/3.gif");
m_loading = new QLabel(this);
m_loading->setMovie(m_loadingMovie);
m_loading->setScaledContents(true);
m_loading->resize(40, 40);
m_loading->setAttribute(Qt::WA_TranslucentBackground, true);
}
2.2 消息内容设置
setText方法是控件的核心接口,用于设置消息的各种属性:
void ChatMessage::setText(QString text, QString time, QSize allSize,
QString ip, User_Type userType)
{
m_msg = text;
m_userType = userType;
m_time = time;
m_curTime = QDateTime::fromSecsSinceEpoch(time.toInt()).toString("ddd hh:mm");
m_allSize = allSize;
m_ip = ip;
// 如果是自己发送的消息且未发送成功,显示加载动画
if(userType == User_Me) {
if(!m_isSending) {
m_loading->move(m_kuangRightRect.x() - m_loading->width() - 10,
m_kuangRightRect.y() + m_kuangRightRect.height()/2 -
m_loading->height()/2);
m_loading->show();
m_loadingMovie->start();
}
} else {
m_loading->hide();
}
this->update(); // 触发重绘
}
2.3 自动换行处理
getRealString方法负责计算文本的实际显示尺寸并处理自动换行:
QSize ChatMessage::getRealString(QString src)
{
QFontMetricsF fm(this->font());
m_lineHeight = fm.lineSpacing();
int nCount = src.count("\n");
int nMaxWidth = 0;
if(nCount == 0) {
nMaxWidth = fm.horizontalAdvance(src);
QString value = src;
if(nMaxWidth > m_textWidth) {
nMaxWidth = m_textWidth;
int size = m_textWidth / fm.horizontalAdvance(" ");
int num = fm.horizontalAdvance(value) / m_textWidth;
nCount += num;
QString temp = "";
for(int i = 0; i < num; i++) {
temp += value.mid(i * size, (i + 1) * size) + "\n";
}
src.replace(value, temp);
}
} else {
// 处理已包含换行符的文本
for(int i = 0; i < (nCount + 1); i++) {
QString value = src.split("\n").at(i);
nMaxWidth = fm.horizontalAdvance(value) > nMaxWidth ?
fm.horizontalAdvance(value) : nMaxWidth;
if(nMaxWidth > m_textWidth) {
// 自动换行逻辑
nMaxWidth = m_textWidth;
int size = m_textWidth / fm.horizontalAdvance(" ");
int num = fm.horizontalAdvance(value) / m_textWidth;
nCount += num;
QString temp = "";
for(int i = 0; i < num; i++) {
temp += value.mid(i * size, (i + 1) * size) + "\n";
}
src.replace(value, temp);
}
}
}
return QSize(nMaxWidth + m_spaceWid,
(nCount + 1) * m_lineHeight + 2 * m_lineHeight);
}
2.4 消息绘制
paintEvent方法是控件的绘制核心,根据消息类型绘制不同的UI样式:
void ChatMessage::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing |
QPainter::SmoothPixmapTransform);
painter.setPen(Qt::NoPen);
painter.setBrush(QBrush(Qt::gray));
if(m_userType == User_Type::User_She) {
// 绘制他人消息:左对齐,白色气泡
painter.drawPixmap(m_iconLeftRect, m_leftPixmap);
// 绘制气泡边框
QColor col_KuangB(234, 234, 234);
painter.setBrush(QBrush(col_KuangB));
painter.drawRoundedRect(m_kuangLeftRect.x() - 1,
m_kuangLeftRect.y() - 1 + 10,
m_kuangLeftRect.width() + 2,
m_kuangLeftRect.height() + 2, 4, 4);
// 绘制气泡
QColor col_Kuang(255, 255, 255);
painter.setBrush(QBrush(col_Kuang));
painter.drawRoundedRect(m_kuangLeftRect, 4, 4);
// 绘制气泡三角
QPointF points[3] = {
QPointF(m_sanjiaoLeftRect.x(), 40),
QPointF(m_sanjiaoLeftRect.x() + m_sanjiaoLeftRect.width(), 35),
QPointF(m_sanjiaoLeftRect.x() + m_sanjiaoLeftRect.width(), 45)
};
QPen pen;
pen.setColor(col_Kuang);
painter.setPen(pen);
painter.drawPolygon(points, 3);
// 绘制发送者IP
QPen penIp;
penIp.setColor(Qt::darkGray);
painter.setPen(penIp);
QFont f = this->font();
f.setPointSize(10);
painter.setFont(f);
painter.drawText(m_ipLeftRect, m_ip,
Qt::AlignHCenter | Qt::AlignVCenter);
// 绘制消息内容
QPen penText;
penText.setColor(QColor(51, 51, 51));
painter.setPen(penText);
QTextOption option(Qt::AlignLeft | Qt::AlignVCenter);
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
painter.setFont(this->font());
painter.drawText(m_textLeftRect, m_msg, option);
} else if(m_userType == User_Type::User_Me) {
// 绘制自己消息:右对齐,蓝色气泡
// 实现逻辑类似上面,位置和颜色不同
} else if(m_userType == User_Type::User_Time) {
// 绘制时间戳消息
QPen penText;
penText.setColor(QColor(153, 153, 153));
painter.setPen(penText);
QTextOption option(Qt::AlignCenter);
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
QFont te_font = this->font();
te_font.setFamily("MicrosoftYaHei");
te_font.setPointSize(10);
painter.setFont(te_font);
painter.drawText(this->rect(), m_curTime, option);
}
}
三、使用示例
3.1 基本使用方法
// 创建消息列表容器
QListWidget* msgList = new QListWidget(this);
msgList->setGeometry(10, 10, 500, 400);
// 创建自己发送的消息
ChatMessage* myMsg = new ChatMessage(msgList);
myMsg->setText("你好,这是我发送的消息",
QString::number(QDateTime::currentSecsSinceEpoch()),
QSize(msgList->width(), 0),
"192.168.1.100",
ChatMessage::User_Me);
// 创建他人发送的消息
ChatMessage* otherMsg = new ChatMessage(msgList);
otherMsg->setText("收到,这是我的回复",
QString::number(QDateTime::currentSecsSinceEpoch()),
QSize(msgList->width(), 0),
"192.168.1.101",
ChatMessage::User_She);
// 添加到消息列表
QListWidgetItem* item1 = new QListWidgetItem(msgList);
item1->setSizeHint(myMsg->fontRect(myMsg->text()));
msgList->setItemWidget(item1, myMsg);
QListWidgetItem* item2 = new QListWidgetItem(msgList);
item2->setSizeHint(otherMsg->fontRect(otherMsg->text()));
msgList->setItemWidget(item2, otherMsg);
// 消息发送成功后更新状态
myMsg->setTextSuccess();
3.2 高级功能:@提及和文件选择
在实际聊天应用中,经常需要支持特殊功能如@提及他人或文件选择。可以通过扩展ChatMessage类来实现:
// 扩展ChatMessage类,增加特殊内容处理
void ChatMessage::handleSpecialContent(QString text)
{
// 处理@提及
QRegularExpression atPattern("@([^\\s@]+)");
QRegularExpressionMatchIterator it = atPattern.globalMatch(text);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
QString username = match.captured(1);
// 高亮显示@提及,并添加点击事件
}
// 处理文件选择
if (text.startsWith("/file")) {
// 解析文件信息,显示文件图标和下载选项
}
}