| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- #include "chatmessagedelegate.h"
- #include "chatmessage.h"
- #include "thememanager.h"
- #include <QAbstractTextDocumentLayout>
- #include <QFontMetrics>
- #include <QPainter>
- #include <QPainterPath>
- #include <QTextDocument>
- #include <QTextLayout>
- ChatMessageDelegate::ChatMessageDelegate(QObject *parent)
- : QStyledItemDelegate(parent)
- , m_viewportWidth(600)
- {
- test.setWindowTitle("test ChatMessageDelegate");
- //test.show();
- }
- int ChatMessageDelegate::hitTestText(const QPoint &pos, const QString &text) const
- {
- QFont fn;
- QFontMetrics fm(fn);
- if (text.isEmpty() || pos.x() < 0 || pos.y() < 0)
- return -1;
- int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
- - 2 * ChatConstants::BUBBLE_SPACING;
- // 使用QTextLayout计算文本布局
- QTextLayout textLayout(text);
- textLayout.setFont(QFont());
- QTextOption option;
- option.setWrapMode(QTextOption::WordWrap);
- option.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
- textLayout.setTextOption(option);
- // 计算文本布局
- textLayout.beginLayout();
- int y = 0;
- int lineIndex = 0;
- QVector<QTextLine> lines;
- while (true) {
- QTextLine line = textLayout.createLine();
- if (!line.isValid())
- break;
- line.setLineWidth(maxWidth);
- int lineHeight = line.height();
- line.setPosition(QPointF(0, y));
- lines.append(line);
- y += lineHeight;
- lineIndex++;
- }
- textLayout.endLayout();
- // 查找点击位置对应的行和字符
- for (int i = 0; i < lines.size(); ++i) {
- QTextLine line = lines[i];
- QRectF lineRect(0, line.y(), line.width(), line.height());
- if (pos.y() >= lineRect.top() && pos.y() <= lineRect.bottom()) {
- // 找到了行,现在找字符位置
- int xPos = qBound(0, pos.x(), int(line.width()));
- return line.xToCursor(xPos);
- }
- }
- // 如果点击在最后一行之后,返回文本末尾
- if (pos.y() > y && !lines.isEmpty())
- return text.length();
- return -1;
- }
- void ChatMessageDelegate::paint(QPainter *painter,
- const QStyleOptionViewItem &option,
- const QModelIndex &index) const
- {
- painter->setRenderHint(QPainter::Antialiasing);
- painter->setRenderHint(QPainter::TextAntialiasing);
- const ChatMessage message = index.data().value<ChatMessage>();
- // 系统消息特殊处理
- if (message.type == MessageType::System) {
- drawSystemMessage(painter, option, message);
- return;
- }
- // 计算名字高度
- QFont nameFont = option.font;
- nameFont.setPointSize(std::max(option.font.pointSize() - 2, 8));
- QFontMetrics nameFm(nameFont);
- int nameHeight = message.senderName.isEmpty() ? 0 : nameFm.height();
- int nameSpacing = message.senderName.isEmpty() ? 0 : 2;
- // 计算内容大小(文本或图片)
- QSize contentSize = calculateContentSize(message, option.fontMetrics);
- // 计算头像位置
- QRectF avatarRect;
- if (message.type == MessageType::Left) {
- avatarRect = QRectF(option.rect.left() + ChatConstants::BUBBLE_SPACING,
- option.rect.top() + nameHeight + nameSpacing,
- ChatConstants::AVATAR_SIZE,
- ChatConstants::AVATAR_SIZE);
- } else {
- avatarRect = QRectF(option.rect.right() - ChatConstants::AVATAR_SIZE
- - ChatConstants::BUBBLE_SPACING,
- option.rect.top() + nameHeight + nameSpacing,
- ChatConstants::AVATAR_SIZE,
- ChatConstants::AVATAR_SIZE);
- }
- // 计算气泡位置
- QRectF bubbleRect;
- if (message.type == MessageType::Left) {
- bubbleRect = QRectF(avatarRect.right() + ChatConstants::BUBBLE_SPACING,
- option.rect.top() + nameHeight + nameSpacing,
- contentSize.width() + 2 * ChatConstants::BUBBLE_PADDING,
- contentSize.height() + 2 * ChatConstants::BUBBLE_PADDING);
- } else {
- bubbleRect = QRectF(option.rect.right() - contentSize.width()
- - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
- - 2 * ChatConstants::BUBBLE_SPACING,
- option.rect.top() + nameHeight + nameSpacing,
- contentSize.width() + 2 * ChatConstants::BUBBLE_PADDING,
- contentSize.height() + 2 * ChatConstants::BUBBLE_PADDING);
- }
- // 绘制名字
- if (!message.senderName.isEmpty()) {
- painter->save();
- painter->setFont(nameFont);
- painter->setPen(QColor(120, 120, 120));
- int nameX, nameWidth;
- QRect nameRect;
- if (message.type == MessageType::Left) {
- // 左侧消息,名字左对齐,紧贴头像右侧
- nameX = avatarRect.right() + ChatConstants::BUBBLE_SPACING;
- nameWidth = bubbleRect.width();
- nameRect = QRect(nameX, option.rect.top(), nameWidth, nameHeight);
- painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, message.senderName);
- } else {
- // 右侧消息,名字右对齐,紧贴头像左侧
- nameWidth = nameFm.horizontalAdvance(message.senderName);
- // 名字宽度不超过气泡宽度
- nameWidth = std::min(nameWidth, static_cast<int>(bubbleRect.width()));
- nameX = bubbleRect.right() - nameWidth;
- nameRect = QRect(nameX, option.rect.top(), nameWidth, nameHeight);
- painter->drawText(nameRect, Qt::AlignRight | Qt::AlignVCenter, message.senderName);
- }
- painter->restore();
- }
- // 绘制气泡
- drawBubble(painter, bubbleRect, message.type == MessageType::Left);
- // 绘制头像
- drawAvatar(painter, avatarRect, message.avatar);
- // 绘制内容(文本、图片或混合内容)
- QRectF contentRect = bubbleRect.adjusted(ChatConstants::BUBBLE_PADDING,
- ChatConstants::BUBBLE_PADDING,
- -ChatConstants::BUBBLE_PADDING,
- -ChatConstants::BUBBLE_PADDING);
-
- // 检查是否是当前选中的消息
- bool isCurrentMessage = (index == m_currentMessageIndex);
-
- if (message.hasImage() && message.hasText()) {
- // 混合内容:图片在上,文字在下
- QSize imageSize = calculateImageSize(message.imagePath, contentRect.width(), contentRect.height() * 0.7);
- QRectF imageRect(contentRect.left(), contentRect.top(), imageSize.width(), imageSize.height());
- QRectF textRect(contentRect.left(), imageRect.bottom() + 4, contentRect.width(), contentRect.height() - imageSize.height() - 4);
-
- drawImage(painter, imageRect, message.imagePath);
- painter->setPen(ThemeManager::instance().color("colorText"));
- drawTextWithSelection(painter, textRect, message.text, option.fontMetrics, isCurrentMessage);
- } else if (message.hasImage()) {
- // 纯图片消息
- drawImage(painter, contentRect, message.imagePath);
- } else {
- // 纯文本消息
- painter->setPen(ThemeManager::instance().color("colorText"));
- drawTextWithSelection(painter, contentRect, message.text, option.fontMetrics, isCurrentMessage);
- }
- }
- QSize ChatMessageDelegate::sizeHint(const QStyleOptionViewItem &option,
- const QModelIndex &index) const
- {
- const ChatMessage message = index.data().value<ChatMessage>();
- QSize contentSize = calculateContentSize(message, option.fontMetrics);
- // 系统消息特殊处理
- if (message.type == MessageType::System) {
- int width = qMin(contentSize.width() + 2 * ChatConstants::BUBBLE_PADDING, viewportWidth() / 2);
- int height = contentSize.height() + 2 * ChatConstants::BUBBLE_PADDING
- + ChatConstants::TIMESTAMP_HEIGHT + 2 * ChatConstants::BUBBLE_SPACING;
- return QSize(width, height);
- }
- // 计算名字高度
- QFont nameFont = option.font;
- nameFont.setPointSize(std::max(option.font.pointSize() - 2, 8));
- QFontMetrics nameFm(nameFont);
- int nameHeight = message.senderName.isEmpty() ? 0 : nameFm.height();
- int nameSpacing = message.senderName.isEmpty() ? 0 : 2;
- int width = contentSize.width() + 2 * ChatConstants::BUBBLE_PADDING + ChatConstants::AVATAR_SIZE
- + 2 * ChatConstants::BUBBLE_SPACING;
- int height = qMax(contentSize.height() + 2 * ChatConstants::BUBBLE_PADDING,
- ChatConstants::AVATAR_SIZE)
- + ChatConstants::TIMESTAMP_HEIGHT
- + nameHeight + nameSpacing;
- return QSize(qMin(width, viewportWidth()), height + ChatConstants::BUBBLE_SPACING);
- }
- void ChatMessageDelegate::setViewportWidth(int width)
- {
- m_viewportWidth = width - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
- - 2 * ChatConstants::BUBBLE_SPACING;
- }
- void ChatMessageDelegate::drawBubble(QPainter *painter, const QRectF &rect, bool isLeft) const
- {
- QPainterPath path;
- path.addRoundedRect(rect, ChatConstants::BUBBLE_RADIUS, ChatConstants::BUBBLE_RADIUS);
- // 添加小三角
- const int triangleSize = 6;
- if (isLeft) {
- path.moveTo(rect.left(), rect.top() + ChatConstants::AVATAR_SIZE / 2 - triangleSize);
- path.lineTo(rect.left() - triangleSize, rect.top() + ChatConstants::AVATAR_SIZE / 2);
- path.lineTo(rect.left(), rect.top() + ChatConstants::AVATAR_SIZE / 2 + triangleSize);
- } else {
- path.moveTo(rect.right(), rect.top() + ChatConstants::AVATAR_SIZE / 2 - triangleSize);
- path.lineTo(rect.right() + triangleSize, rect.top() + ChatConstants::AVATAR_SIZE / 2);
- path.lineTo(rect.right(), rect.top() + ChatConstants::AVATAR_SIZE / 2 + triangleSize);
- }
- // 设置气泡颜色
- if (isLeft) {
- painter->fillPath(path, ThemeManager::instance().color("colorFillQuaternary"));
- } else {
- painter->fillPath(path, ThemeManager::instance().color("colorPrimaryBg"));
- }
- }
- void ChatMessageDelegate::drawAvatar(QPainter *painter,
- const QRectF &rect,
- const QString &avatarPath) const
- {
- // 检查缓存中是否已有头像
- if (!m_avatarCache.contains(avatarPath)) {
- QPixmap avatar(avatarPath);
- if (!avatar.isNull()) {
- avatar = avatar.scaled(ChatConstants::AVATAR_SIZE,
- ChatConstants::AVATAR_SIZE,
- Qt::KeepAspectRatio,
- Qt::SmoothTransformation);
- const_cast<ChatMessageDelegate *>(this)->m_avatarCache.insert(avatarPath, avatar);
- }
- }
- // 创建圆形裁剪区域
- QPainterPath path;
- path.addEllipse(rect);
- painter->setClipPath(path);
- if (m_avatarCache.contains(avatarPath)) {
- // 绘制缓存的头像
- painter->drawPixmap(rect.toRect(), m_avatarCache[avatarPath]);
- } else {
- // 绘制默认头像
- painter->setPen(Qt::NoPen);
- painter->setBrush(ThemeManager::instance().color("colorFill"));
- painter->drawEllipse(rect);
- }
- painter->setClipping(false);
- }
- void ChatMessageDelegate::drawSystemMessage(QPainter *painter,
- const QStyleOptionViewItem &option,
- const ChatMessage &message) const
- {
- // 为系统消息单独计算文本大小
- int maxWidth = viewportWidth() / (2.0 / 3.0);
- // 使用QTextDocument计算系统消息的实际大小
- QFont font = painter->font();
- int size = font.pointSize();
- font.setPointSize(std::max(size - 2, 8));
- QTextDocument doc;
- doc.setDefaultFont(font);
- doc.setDocumentMargin(0);
- doc.setTextWidth(maxWidth - 2 * ChatConstants::BUBBLE_PADDING);
- doc.setHtml(message.text.toHtmlEscaped().replace("\n", "<br>"));
- QSize textSize(doc.idealWidth(), doc.size().height());
- // 系统消息居中显示
- int bubbleWidth = qMin(textSize.width() + 2 * ChatConstants::BUBBLE_PADDING, maxWidth);
- // 计算气泡位置(居中)
- QRectF bubbleRect(option.rect.left() + (option.rect.width() - bubbleWidth) / 2,
- option.rect.top() + ChatConstants::BUBBLE_SPACING,
- bubbleWidth,
- textSize.height() + 2 * ChatConstants::BUBBLE_PADDING);
- // 绘制系统消息气泡(使用特殊样式)
- QPainterPath path;
- path.addRoundedRect(bubbleRect, ChatConstants::BUBBLE_RADIUS, ChatConstants::BUBBLE_RADIUS);
- // 使用半透明灰色背景
- painter->fillPath(path, QColor(200, 200, 200, 60));
- // 绘制文本
- QRectF textRect = bubbleRect.adjusted(ChatConstants::BUBBLE_PADDING,
- ChatConstants::BUBBLE_PADDING,
- -ChatConstants::BUBBLE_PADDING,
- -ChatConstants::BUBBLE_PADDING);
- // 使用特殊颜色
- painter->setPen(QColor(255, 0, 0));
- QTextCharFormat defaultFormat;
- defaultFormat.setForeground(ThemeManager::instance().color("colorText"));
- QTextCursor cursor(&doc);
- cursor.select(QTextCursor::Document);
- cursor.mergeCharFormat(defaultFormat);
- painter->save();
- painter->translate(textRect.topLeft());
- doc.drawContents(painter);
- painter->restore();
- // // // 绘制时间戳
- // QRectF timestampRect(bubbleRect.left(),
- // bubbleRect.bottom(),
- // bubbleRect.width(),
- // ChatConstants::TIMESTAMP_HEIGHT);
- // painter->setPen(ThemeManager::instance().color("colorTextSecondary"));
- // painter->setFont(QFont(option.font.family(), option.font.pointSize() - 2));
- // painter->drawText(timestampRect, Qt::AlignCenter, message.timestamp.toString("HH:mm"));
- }
- // 添加绘制带选择的文本方法
- void ChatMessageDelegate::drawTextWithSelection(QPainter *painter,
- const QRectF &rect,
- const QString &text,
- const QFontMetrics &fm,
- bool isCurrentMessage) const
- {
- // 如果不是当前消息或没有选择,直接绘制文本
- // 使用QTextDocument绘制多行文本
- QTextDocument doc;
- doc.setDefaultFont(painter->font());
- doc.setDocumentMargin(0);
- doc.setTextWidth(rect.width());
- doc.setHtml(text.toHtmlEscaped().replace("\n", "<br>"));
- // 设置默认文本格式(应用主题颜色)
- QTextCharFormat defaultFormat;
- defaultFormat.setForeground(ThemeManager::instance().color("colorText"));
- if (!isCurrentMessage || m_selectionStart < 0 || m_selectionEnd < 0
- || m_selectionStart == m_selectionEnd) {
- // 将普通文本转换为HTML,保留换行符
- QString htmlText = text.toHtmlEscaped().replace("\n", "<br>");
- doc.setHtml(htmlText);
- // 应用默认格式到整个文档
- QTextCursor cursor(&doc);
- cursor.select(QTextCursor::Document);
- cursor.mergeCharFormat(defaultFormat);
- painter->save();
- painter->translate(rect.topLeft());
- painter->setPen(ThemeManager::instance().color("colorText"));
- doc.drawContents(painter);
- painter->restore();
- return;
- }
- // 确保选择范围有效
- int start = qMin(m_selectionStart, m_selectionEnd);
- int end = qMax(m_selectionStart, m_selectionEnd);
- if (start >= text.length() || end <= 0) {
- QString htmlText = text.toHtmlEscaped().replace("\n", "<br>");
- doc.setHtml(htmlText);
- // 应用默认格式到整个文档
- QTextCursor cursor(&doc);
- cursor.select(QTextCursor::Document);
- cursor.mergeCharFormat(defaultFormat);
- painter->save();
- painter->translate(rect.topLeft());
- painter->setPen(ThemeManager::instance().color("colorText"));
- doc.drawContents(painter);
- painter->restore();
- return;
- }
- // 限制选择范围在文本长度内
- start = qMax(0, start);
- end = qMin(text.length(), end);
- // 将普通文本转换为HTML,保留换行符
- QString htmlText = text.toHtmlEscaped().replace("\n", "<br>");
- doc.setHtml(htmlText);
- // 首先应用默认格式到整个文档
- QTextCursor cursor(&doc);
- cursor.select(QTextCursor::Document);
- cursor.mergeCharFormat(defaultFormat);
- // 设置选择格式
- cursor.setPosition(start);
- cursor.setPosition(end, QTextCursor::KeepAnchor);
- QTextCharFormat selectionFormat;
- selectionFormat.setBackground(QColor(0, 120, 215, 128)); // 半透明蓝色背景
- selectionFormat.setForeground(Qt::white); // 白色文本
- cursor.mergeCharFormat(selectionFormat);
- // 绘制文本
- painter->save();
- painter->translate(rect.topLeft());
- doc.drawContents(painter);
- painter->restore();
- }
- QSize ChatMessageDelegate::calculateTextSize(const QFontMetrics &fm, const QString &text) const
- {
- int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE
- - 2 * ChatConstants::BUBBLE_SPACING;
- QRect textRect = fm.boundingRect(0,
- 0,
- maxWidth,
- INT_MAX,
- Qt::TextWordWrap | Qt::AlignLeft | Qt::AlignVCenter,
- text);
- return textRect.size();
- }
- int ChatMessageDelegate::getPositionFromPoint(const QPoint &pos,
- const QString &text,
- const QFontMetrics &fm)
- {
- if (text.isEmpty() || pos.x() < 0 || pos.y() < 0)
- return -1;
- int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING;
- // 使用QTextDocument处理多行文本
- QTextDocument doc;
- doc.setDefaultFont(QFont());
- doc.setDocumentMargin(0);
- doc.setTextWidth(maxWidth - 52); // AVATAR_SIZE + BUBBLE_PADDING
- doc.setHtml(text.toHtmlEscaped().replace("\n", "<br>"));
- test.setDocument(&doc);
- // 获取点击位置对应的文本位置
- QAbstractTextDocumentLayout *layout = doc.documentLayout();
- int position = layout->hitTest(pos, Qt::FuzzyHit);
- return position;
- }
- void ChatMessageDelegate::drawImage(QPainter *painter, const QRectF &rect, const QString &imagePath) const
- {
- if (imagePath.isEmpty())
- return;
- // 检查缓存中是否已有图片
- if (!m_imageCache.contains(imagePath)) {
- QPixmap image(imagePath);
- if (!image.isNull()) {
- // 计算合适的缩放尺寸
- QSize rectSize = rect.size().toSize();
- QSize targetSize = image.size().scaled(rectSize.width(), rectSize.height(), Qt::KeepAspectRatio);
- image = image.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
- const_cast<ChatMessageDelegate *>(this)->m_imageCache.insert(imagePath, image);
- }
- }
- if (m_imageCache.contains(imagePath)) {
- const QPixmap &image = m_imageCache[imagePath];
- // 居中绘制图片
- QRectF imageRect = rect;
- if (image.width() < rect.width()) {
- imageRect.setX(rect.x() + (rect.width() - image.width()) / 2);
- imageRect.setWidth(image.width());
- }
- if (image.height() < rect.height()) {
- imageRect.setY(rect.y() + (rect.height() - image.height()) / 2);
- imageRect.setHeight(image.height());
- }
-
- painter->setRenderHint(QPainter::SmoothPixmapTransform);
- painter->drawPixmap(imageRect.toRect(), image);
-
- // 绘制图片边框
- painter->setPen(QPen(QColor(200, 200, 200), 1));
- painter->setBrush(Qt::NoBrush);
- painter->drawRoundedRect(imageRect, 4, 4);
- } else {
- // 绘制占位符
- painter->setPen(Qt::NoPen);
- painter->setBrush(QColor(240, 240, 240));
- painter->drawRoundedRect(rect, 4, 4);
-
- painter->setPen(QColor(150, 150, 150));
- painter->drawText(rect, Qt::AlignCenter, "图片加载失败");
- }
- }
- QSize ChatMessageDelegate::calculateImageSize(const QString &imagePath, int maxWidth, int maxHeight) const
- {
- if (imagePath.isEmpty())
- return QSize(100, 100); // 默认尺寸
- QPixmap image(imagePath);
- if (image.isNull())
- return QSize(100, 100);
- QSize imageSize = image.size();
-
- // 限制最大尺寸
- if (imageSize.width() > maxWidth || imageSize.height() > maxHeight) {
- imageSize = imageSize.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio);
- }
-
- // 限制最小尺寸
- if (imageSize.width() < 50) imageSize.setWidth(50);
- if (imageSize.height() < 50) imageSize.setHeight(50);
-
- return imageSize;
- }
- QSize ChatMessageDelegate::calculateContentSize(const ChatMessage &message, const QFontMetrics &fm) const
- {
- if (message.hasImage() && message.hasText()) {
- // 混合内容:图片 + 文本
- int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE - 2 * ChatConstants::BUBBLE_SPACING;
-
- QSize imageSize = calculateImageSize(message.imagePath, maxWidth, 200);
- QSize textSize = calculateTextSize(fm, message.text);
-
- int width = qMax(imageSize.width(), textSize.width());
- int height = imageSize.height() + textSize.height() + 4; // 4px间距
-
- return QSize(width, height);
- } else if (message.hasImage()) {
- // 纯图片消息
- int maxWidth = viewportWidth() - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE - 2 * ChatConstants::BUBBLE_SPACING;
- return calculateImageSize(message.imagePath, maxWidth, 300);
- } else {
- // 纯文本消息
- return calculateTextSize(fm, message.text);
- }
- }
|