#include "chatmessagedelegate.h" #include "chatmessage.h" #include "thememanager.h" #include #include #include #include #include #include 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 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(); // 系统消息特殊处理 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(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(); 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(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", "
")); 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", "
")); // 设置默认文本格式(应用主题颜色) 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", "
"); 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", "
"); 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", "
"); 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", "
")); 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(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); } }