#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 textSize = calculateTextSize(option.fontMetrics, message.text); // 计算头像位置 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, textSize.width() + 2 * ChatConstants::BUBBLE_PADDING, textSize.height() + 2 * ChatConstants::BUBBLE_PADDING); } else { bubbleRect = QRectF(option.rect.right() - textSize.width() - 2 * ChatConstants::BUBBLE_PADDING - ChatConstants::AVATAR_SIZE - 2 * ChatConstants::BUBBLE_SPACING, option.rect.top() + nameHeight + nameSpacing, textSize.width() + 2 * ChatConstants::BUBBLE_PADDING, textSize.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 textRect = bubbleRect.adjusted(ChatConstants::BUBBLE_PADDING, ChatConstants::BUBBLE_PADDING, -ChatConstants::BUBBLE_PADDING, -ChatConstants::BUBBLE_PADDING); painter->setPen(ThemeManager::instance().color("colorText")); // 检查是否是当前选中的消息 bool isCurrentMessage = (index == m_currentMessageIndex); drawTextWithSelection(painter, textRect, message.text, option.fontMetrics, isCurrentMessage); } QSize ChatMessageDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { const ChatMessage message = index.data().value(); QSize textSize = calculateTextSize(option.fontMetrics, message.text); // 系统消息特殊处理 if (message.type == MessageType::System) { int width = qMin(textSize.width() + 2 * ChatConstants::BUBBLE_PADDING, viewportWidth() / 2); int height = textSize.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 = textSize.width() + 2 * ChatConstants::BUBBLE_PADDING + ChatConstants::AVATAR_SIZE + 2 * ChatConstants::BUBBLE_SPACING; int height = qMax(textSize.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; }