聊天项目(22) 实现气泡聊天对话框

气泡聊天框设计

我们期待实现如下绿色的气泡对话框

https://cdn.llfc.club/1718417551126.jpg

对于我们自己发出的信息,我们可以实现这样一个网格布局管理

https://cdn.llfc.club/1718423760358.jpg

NameLabel用来显示用户的名字,Bubble用来显示聊天信息,Spacer是个弹簧,保证将NameLabel,IconLabel,Bubble等挤压到右侧。

如果是别人发出的消息,我们设置这样一个网格布局

https://cdn.llfc.club/1718497364660.jpg

下面是实现布局的核心代码

  1. ChatItemBase::ChatItemBase(ChatRole role, QWidget *parent)
  2. : QWidget(parent)
  3. , m_role(role)
  4. {
  5. m_pNameLabel = new QLabel();
  6. m_pNameLabel->setObjectName("chat_user_name");
  7. QFont font("Microsoft YaHei");
  8. font.setPointSize(9);
  9. m_pNameLabel->setFont(font);
  10. m_pNameLabel->setFixedHeight(20);
  11. m_pIconLabel = new QLabel();
  12. m_pIconLabel->setScaledContents(true);
  13. m_pIconLabel->setFixedSize(42, 42);
  14. m_pBubble = new QWidget();
  15. QGridLayout *pGLayout = new QGridLayout();
  16. pGLayout->setVerticalSpacing(3);
  17. pGLayout->setHorizontalSpacing(3);
  18. pGLayout->setMargin(3);
  19. QSpacerItem*pSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
  20. if(m_role == ChatRole::Self)
  21. {
  22. m_pNameLabel->setContentsMargins(0,0,8,0);
  23. m_pNameLabel->setAlignment(Qt::AlignRight);
  24. pGLayout->addWidget(m_pNameLabel, 0,1, 1,1);
  25. pGLayout->addWidget(m_pIconLabel, 0, 2, 2,1, Qt::AlignTop);
  26. pGLayout->addItem(pSpacer, 1, 0, 1, 1);
  27. pGLayout->addWidget(m_pBubble, 1,1, 1,1);
  28. pGLayout->setColumnStretch(0, 2);
  29. pGLayout->setColumnStretch(1, 3);
  30. }else{
  31. m_pNameLabel->setContentsMargins(8,0,0,0);
  32. m_pNameLabel->setAlignment(Qt::AlignLeft);
  33. pGLayout->addWidget(m_pIconLabel, 0, 0, 2,1, Qt::AlignTop);
  34. pGLayout->addWidget(m_pNameLabel, 0,1, 1,1);
  35. pGLayout->addWidget(m_pBubble, 1,1, 1,1);
  36. pGLayout->addItem(pSpacer, 2, 2, 1, 1);
  37. pGLayout->setColumnStretch(1, 3);
  38. pGLayout->setColumnStretch(2, 2);
  39. }
  40. this->setLayout(pGLayout);
  41. }

设置用户名和头像

  1. void ChatItemBase::setUserName(const QString &name)
  2. {
  3. m_pNameLabel->setText(name);
  4. }
  5. void ChatItemBase::setUserIcon(const QPixmap &icon)
  6. {
  7. m_pIconLabel->setPixmap(icon);
  8. }

因为我们还要定制化实现气泡widget,所以要写个函数更新这个widget

  1. void ChatItemBase::setWidget(QWidget *w)
  2. {
  3. QGridLayout *pGLayout = (qobject_cast<QGridLayout *>)(this->layout());
  4. pGLayout->replaceWidget(m_pBubble, w);
  5. delete m_pBubble;
  6. m_pBubble = w;
  7. }

聊天气泡

我们的消息分为几种,文件,文本,图片等。所以先实现BubbleFrame作为基类

  1. class BubbleFrame : public QFrame
  2. {
  3. Q_OBJECT
  4. public:
  5. BubbleFrame(ChatRole role, QWidget *parent = nullptr);
  6. void setMargin(int margin);
  7. //inline int margin(){return margin;}
  8. void setWidget(QWidget *w);
  9. protected:
  10. void paintEvent(QPaintEvent *e);
  11. private:
  12. QHBoxLayout *m_pHLayout;
  13. ChatRole m_role;
  14. int m_margin;
  15. };

BubbleFrame基类构造函数创建一个布局,要根据是自己发送的消息还是别人发送的,做margin分布

  1. const int WIDTH_SANJIAO = 8; //三角宽
  2. BubbleFrame::BubbleFrame(ChatRole role, QWidget *parent)
  3. :QFrame(parent)
  4. ,m_role(role)
  5. ,m_margin(3)
  6. {
  7. m_pHLayout = new QHBoxLayout();
  8. if(m_role == ChatRole::Self)
  9. m_pHLayout->setContentsMargins(m_margin, m_margin, WIDTH_SANJIAO + m_margin, m_margin);
  10. else
  11. m_pHLayout->setContentsMargins(WIDTH_SANJIAO + m_margin, m_margin, m_margin, m_margin);
  12. this->setLayout(m_pHLayout);
  13. }

将气泡框内设置文本内容,或者图片内容,所以实现了下面的函数

  1. void BubbleFrame::setWidget(QWidget *w)
  2. {
  3. if(m_pHLayout->count() > 0)
  4. return ;
  5. else{
  6. m_pHLayout->addWidget(w);
  7. }
  8. }

接下来绘制气泡

  1. void BubbleFrame::paintEvent(QPaintEvent *e)
  2. {
  3. QPainter painter(this);
  4. painter.setPen(Qt::NoPen);
  5. if(m_role == ChatRole::Other)
  6. {
  7. //画气泡
  8. QColor bk_color(Qt::white);
  9. painter.setBrush(QBrush(bk_color));
  10. QRect bk_rect = QRect(WIDTH_SANJIAO, 0, this->width()-WIDTH_SANJIAO, this->height());
  11. painter.drawRoundedRect(bk_rect,5,5);
  12. //画小三角
  13. QPointF points[3] = {
  14. QPointF(bk_rect.x(), 12),
  15. QPointF(bk_rect.x(), 10+WIDTH_SANJIAO +2),
  16. QPointF(bk_rect.x()-WIDTH_SANJIAO, 10+WIDTH_SANJIAO-WIDTH_SANJIAO/2),
  17. };
  18. painter.drawPolygon(points, 3);
  19. }
  20. else
  21. {
  22. QColor bk_color(158,234,106);
  23. painter.setBrush(QBrush(bk_color));
  24. //画气泡
  25. QRect bk_rect = QRect(0, 0, this->width()-WIDTH_SANJIAO, this->height());
  26. painter.drawRoundedRect(bk_rect,5,5);
  27. //画三角
  28. QPointF points[3] = {
  29. QPointF(bk_rect.x()+bk_rect.width(), 12),
  30. QPointF(bk_rect.x()+bk_rect.width(), 12+WIDTH_SANJIAO +2),
  31. QPointF(bk_rect.x()+bk_rect.width()+WIDTH_SANJIAO, 10+WIDTH_SANJIAO-WIDTH_SANJIAO/2),
  32. };
  33. painter.drawPolygon(points, 3);
  34. }
  35. return QFrame::paintEvent(e);
  36. }

绘制的过程很简单,先创建QPainter,然后设置NoPen,表示不绘制轮廓线,接下来用设置指定颜色的画刷绘制图形,我们先绘制矩形再绘制三角形。

对于文本消息的绘制

  1. TextBubble::TextBubble(ChatRole role, const QString &text, QWidget *parent)
  2. :BubbleFrame(role, parent)
  3. {
  4. m_pTextEdit = new QTextEdit();
  5. m_pTextEdit->setReadOnly(true);
  6. m_pTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  7. m_pTextEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  8. m_pTextEdit->installEventFilter(this);
  9. QFont font("Microsoft YaHei");
  10. font.setPointSize(12);
  11. m_pTextEdit->setFont(font);
  12. setPlainText(text);
  13. setWidget(m_pTextEdit);
  14. initStyleSheet();
  15. }

setPlainText设置文本最大宽度

  1. void TextBubble::setPlainText(const QString &text)
  2. {
  3. m_pTextEdit->setPlainText(text);
  4. //m_pTextEdit->setHtml(text);
  5. //找到段落中最大宽度
  6. qreal doc_margin = m_pTextEdit->document()->documentMargin();
  7. int margin_left = this->layout()->contentsMargins().left();
  8. int margin_right = this->layout()->contentsMargins().right();
  9. QFontMetricsF fm(m_pTextEdit->font());
  10. QTextDocument *doc = m_pTextEdit->document();
  11. int max_width = 0;
  12. //遍历每一段找到 最宽的那一段
  13. for (QTextBlock it = doc->begin(); it != doc->end(); it = it.next()) //字体总长
  14. {
  15. int txtW = int(fm.width(it.text()));
  16. max_width = max_width < txtW ? txtW : max_width; //找到最长的那段
  17. }
  18. //设置这个气泡的最大宽度 只需要设置一次
  19. setMaximumWidth(max_width + doc_margin * 2 + (margin_left + margin_right)); //设置最大宽度
  20. }

我们拉伸的时候要调整气泡的高度,这里重写事件过滤器

  1. bool TextBubble::eventFilter(QObject *o, QEvent *e)
  2. {
  3. if(m_pTextEdit == o && e->type() == QEvent::Paint)
  4. {
  5. adjustTextHeight(); //PaintEvent中设置
  6. }
  7. return BubbleFrame::eventFilter(o, e);
  8. }

调整高度

  1. void TextBubble::adjustTextHeight()
  2. {
  3. qreal doc_margin = m_pTextEdit->document()->documentMargin(); //字体到边框的距离默认为4
  4. QTextDocument *doc = m_pTextEdit->document();
  5. qreal text_height = 0;
  6. //把每一段的高度相加=文本高
  7. for (QTextBlock it = doc->begin(); it != doc->end(); it = it.next())
  8. {
  9. QTextLayout *pLayout = it.layout();
  10. QRectF text_rect = pLayout->boundingRect(); //这段的rect
  11. text_height += text_rect.height();
  12. }
  13. int vMargin = this->layout()->contentsMargins().top();
  14. //设置这个气泡需要的高度 文本高+文本边距+TextEdit边框到气泡边框的距离
  15. setFixedHeight(text_height + doc_margin *2 + vMargin*2 );
  16. }

设置样式表

  1. void TextBubble::initStyleSheet()
  2. {
  3. m_pTextEdit->setStyleSheet("QTextEdit{background:transparent;border:none}");
  4. }

对于图像的旗袍对话框类似,只是计算图像的宽高即可

  1. #define PIC_MAX_WIDTH 160
  2. #define PIC_MAX_HEIGHT 90
  3. PictureBubble::PictureBubble(const QPixmap &picture, ChatRole role, QWidget *parent)
  4. :BubbleFrame(role, parent)
  5. {
  6. QLabel *lb = new QLabel();
  7. lb->setScaledContents(true);
  8. QPixmap pix = picture.scaled(QSize(PIC_MAX_WIDTH, PIC_MAX_HEIGHT), Qt::KeepAspectRatio);
  9. lb->setPixmap(pix);
  10. this->setWidget(lb);
  11. int left_margin = this->layout()->contentsMargins().left();
  12. int right_margin = this->layout()->contentsMargins().right();
  13. int v_margin = this->layout()->contentsMargins().bottom();
  14. setFixedSize(pix.width()+left_margin + right_margin, pix.height() + v_margin *2);
  15. }

发送测试

接下来在发送处实现文本和图片消息的展示,点击发送按钮根据不同的类型创建不同的气泡消息

  1. void ChatPage::on_send_btn_clicked()
  2. {
  3. auto pTextEdit = ui->chatEdit;
  4. ChatRole role = ChatRole::Self;
  5. QString userName = QStringLiteral("恋恋风辰");
  6. QString userIcon = ":/res/head_1.jpg";
  7. const QVector<MsgInfo>& msgList = pTextEdit->getMsgList();
  8. for(int i=0; i<msgList.size(); ++i)
  9. {
  10. QString type = msgList[i].msgFlag;
  11. ChatItemBase *pChatItem = new ChatItemBase(role);
  12. pChatItem->setUserName(userName);
  13. pChatItem->setUserIcon(QPixmap(userIcon));
  14. QWidget *pBubble = nullptr;
  15. if(type == "text")
  16. {
  17. pBubble = new TextBubble(role, msgList[i].content);
  18. }
  19. else if(type == "image")
  20. {
  21. pBubble = new PictureBubble(QPixmap(msgList[i].content) , role);
  22. }
  23. else if(type == "file")
  24. {
  25. }
  26. if(pBubble != nullptr)
  27. {
  28. pChatItem->setWidget(pBubble);
  29. ui->chat_data_list->appendChatItem(pChatItem);
  30. }
  31. }
  32. }

效果展示

https://cdn.llfc.club/1718499438435.jpg

源码和视频

https://www.bilibili.com/video/BV1Mz4218783/?vd_source=8be9e83424c2ed2c9b2a3ed1d01385e9

源码链接

https://gitee.com/secondtonone1/llfcchat

热门评论

热门文章

  1. windows环境搭建和vscode配置

    喜欢(587) 浏览(1849)
  2. slice介绍和使用

    喜欢(521) 浏览(1926)
  3. 解密定时器的实现细节

    喜欢(566) 浏览(2261)
  4. C++ 类的继承封装和多态

    喜欢(588) 浏览(3175)
  5. Linux环境搭建和编码

    喜欢(594) 浏览(6833)

最新评论

  1. 线程基础 mzx2023:新手好奇问一下,这是什么原因呢?
  2. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。
  3. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  4. asio多线程模式IOThreadPool secondtonone1:这么优秀吗
  5. 互斥与死锁 Vstronzw://仅提供一份参考代码给同样初学者的我们[脱单doge] /* 定义了如下栈, 对于多线程访问时判断栈是否为空, 此后两个线程同时出栈,可能会造成崩溃 */ #include <iostream> #include <string> #include <thread> #include <vector> #include <mutex> #include <stack> #include <exception> using namespace std; struct empty_stack : public std::exception { public: const char* what()const throw() //函数后面必须跟throw(),括号里面不能有任务参数,表示不抛出任务异常 //因为这个已经是一个异常处理信息了,不能再抛异常。 { return "empty_stack"; } }; //struct empty_stack : std::exception //{ // const char* what() const throw(); //}; template<typename T> class threadsafe_stack { private: std::stack<T> data; mutable std::mutex m; public: threadsafe_stack() {} threadsafe_stack(const threadsafe_stack& other) { std::lock_guard<std::mutex> lock(other.m); //①在构造函数的函数体(constructor body)内进行复制操作 data = other.data; } threadsafe_stack& operator=(const threadsafe_stack&) = delete; void push(T new_value) { std::lock_guard<std::mutex> lock(m); data.push(std::move(new_value)); } T pop() { std::lock_guard<std::mutex> lock(m); if (data.empty()) throw empty_stack(); auto element = data.top(); data.pop(); return element; } bool empty() const { std::lock_guard<std::mutex> lock(m); return data.empty(); } }; void test_threadsafe_stack() { threadsafe_stack<int> safe_stack; safe_stack.push(1); std::thread t1([&safe_stack]() { if (!safe_stack.empty()) { std::this_thread::sleep_for(std::chrono::seconds(1)); try { safe_stack.pop(); } catch (empty_stack &e) { cout << e.what() << endl; } } }); std::thread t2([&safe_stack]() { if (!safe_stack.empty()) { std::this_thread::sleep_for(std::chrono::seconds(1)); try { safe_stack.pop(); } catch (empty_stack &e) { cout << e.what() << endl; } } }); t1.join(); t2.join(); } int main() { test_threadsafe_stack(); return 0; } /* empty_stack */

个人公众号

个人微信