| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- #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;
- }
- // 计算各个元素的位置
- 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(),
- ChatConstants::AVATAR_SIZE,
- ChatConstants::AVATAR_SIZE);
- } else {
- avatarRect = QRectF(option.rect.right() - ChatConstants::AVATAR_SIZE
- - ChatConstants::BUBBLE_SPACING,
- option.rect.top(),
- ChatConstants::AVATAR_SIZE,
- ChatConstants::AVATAR_SIZE);
- }
- // 计算气泡位置
- QRectF bubbleRect;
- if (message.type == MessageType::Left) {
- bubbleRect = QRectF(avatarRect.right() + ChatConstants::BUBBLE_SPACING,
- option.rect.top(),
- 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(),
- textSize.width() + 2 * ChatConstants::BUBBLE_PADDING,
- textSize.height() + 2 * ChatConstants::BUBBLE_PADDING);
- }
- // 绘制气泡
- 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);
- // // 绘制时间戳
- // 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"));
- }
- QSize ChatMessageDelegate::sizeHint(const QStyleOptionViewItem &option,
- const QModelIndex &index) const
- {
- const ChatMessage message = index.data().value<ChatMessage>();
- 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);
- }
- 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;
- 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;
- }
|