首页 > 编程语言 >C++全栈聊天项目(22) 气泡聊天对话框

C++全栈聊天项目(22) 气泡聊天对话框

时间:2024-06-16 09:31:07浏览次数:31  
标签:pTextEdit 22 对话框 bk WIDTH role 聊天 margin pGLayout

气泡聊天框设计

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

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

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

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

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

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

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

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

ChatItemBase::ChatItemBase(ChatRole role, QWidget *parent)
    : QWidget(parent)
    , m_role(role)
{
    m_pNameLabel    = new QLabel();
    m_pNameLabel->setObjectName("chat_user_name");
    QFont font("Microsoft YaHei");
    font.setPointSize(9);
    m_pNameLabel->setFont(font);
    m_pNameLabel->setFixedHeight(20);
    m_pIconLabel    = new QLabel();
    m_pIconLabel->setScaledContents(true);
    m_pIconLabel->setFixedSize(42, 42);
    m_pBubble       = new QWidget();
    QGridLayout *pGLayout = new QGridLayout();
    pGLayout->setVerticalSpacing(3);
    pGLayout->setHorizontalSpacing(3);
    pGLayout->setMargin(3);
    QSpacerItem*pSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
    if(m_role == ChatRole::Self)
    {
        m_pNameLabel->setContentsMargins(0,0,8,0);
        m_pNameLabel->setAlignment(Qt::AlignRight);
        pGLayout->addWidget(m_pNameLabel, 0,1, 1,1);
        pGLayout->addWidget(m_pIconLabel, 0, 2, 2,1, Qt::AlignTop);
        pGLayout->addItem(pSpacer, 1, 0, 1, 1);
        pGLayout->addWidget(m_pBubble, 1,1, 1,1);
        pGLayout->setColumnStretch(0, 2);
        pGLayout->setColumnStretch(1, 3);
    }else{
        m_pNameLabel->setContentsMargins(8,0,0,0);
        m_pNameLabel->setAlignment(Qt::AlignLeft);
        pGLayout->addWidget(m_pIconLabel, 0, 0, 2,1, Qt::AlignTop);
        pGLayout->addWidget(m_pNameLabel, 0,1, 1,1);
        pGLayout->addWidget(m_pBubble, 1,1, 1,1);
        pGLayout->addItem(pSpacer, 2, 2, 1, 1);
        pGLayout->setColumnStretch(1, 3);
        pGLayout->setColumnStretch(2, 2);
    }
    this->setLayout(pGLayout);
}

设置用户名和头像

void ChatItemBase::setUserName(const QString &name)
{
    m_pNameLabel->setText(name);
}

void ChatItemBase::setUserIcon(const QPixmap &icon)
{
    m_pIconLabel->setPixmap(icon);
}

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

void ChatItemBase::setWidget(QWidget *w)
{
   QGridLayout *pGLayout = (qobject_cast<QGridLayout *>)(this->layout());
   pGLayout->replaceWidget(m_pBubble, w);
   delete m_pBubble;
   m_pBubble = w;
}

聊天气泡

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

class BubbleFrame : public QFrame
{
    Q_OBJECT
public:
    BubbleFrame(ChatRole role, QWidget *parent = nullptr);
    void setMargin(int margin);
    //inline int margin(){return margin;}
    void setWidget(QWidget *w);
protected:
    void paintEvent(QPaintEvent *e);
private:
    QHBoxLayout *m_pHLayout;
    ChatRole m_role;
     int      m_margin;
};

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

const int WIDTH_SANJIAO  = 8;  //三角宽
BubbleFrame::BubbleFrame(ChatRole role, QWidget *parent)
    :QFrame(parent)
    ,m_role(role)
    ,m_margin(3)
{
    m_pHLayout = new QHBoxLayout();
    if(m_role == ChatRole::Self)
        m_pHLayout->setContentsMargins(m_margin, m_margin, WIDTH_SANJIAO + m_margin, m_margin);
    else
        m_pHLayout->setContentsMargins(WIDTH_SANJIAO + m_margin, m_margin, m_margin, m_margin);

    this->setLayout(m_pHLayout);
}

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

void BubbleFrame::setWidget(QWidget *w)
{
    if(m_pHLayout->count() > 0)
        return ;
    else{
        m_pHLayout->addWidget(w);
    }
}

接下来绘制气泡

void BubbleFrame::paintEvent(QPaintEvent *e)
{
    QPainter painter(this);
    painter.setPen(Qt::NoPen);

    if(m_role == ChatRole::Other)
    {
        //画气泡
        QColor bk_color(Qt::white);
        painter.setBrush(QBrush(bk_color));
        QRect bk_rect = QRect(WIDTH_SANJIAO, 0, this->width()-WIDTH_SANJIAO, this->height());
        painter.drawRoundedRect(bk_rect,5,5);
        //画小三角
        QPointF points[3] = {
            QPointF(bk_rect.x(), 12),
            QPointF(bk_rect.x(), 10+WIDTH_SANJIAO +2),
            QPointF(bk_rect.x()-WIDTH_SANJIAO, 10+WIDTH_SANJIAO-WIDTH_SANJIAO/2),
        };
        painter.drawPolygon(points, 3);
    }
    else
    {
        QColor bk_color(158,234,106);
        painter.setBrush(QBrush(bk_color));
        //画气泡
        QRect bk_rect = QRect(0, 0, this->width()-WIDTH_SANJIAO, this->height());
        painter.drawRoundedRect(bk_rect,5,5);
        //画三角
        QPointF points[3] = {
            QPointF(bk_rect.x()+bk_rect.width(), 12),
            QPointF(bk_rect.x()+bk_rect.width(), 12+WIDTH_SANJIAO +2),
            QPointF(bk_rect.x()+bk_rect.width()+WIDTH_SANJIAO, 10+WIDTH_SANJIAO-WIDTH_SANJIAO/2),
        };
        painter.drawPolygon(points, 3);

    }

    return QFrame::paintEvent(e);
}

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

对于文本消息的绘制

TextBubble::TextBubble(ChatRole role, const QString &text, QWidget *parent)
    :BubbleFrame(role, parent)
{
    m_pTextEdit = new QTextEdit();
    m_pTextEdit->setReadOnly(true);
    m_pTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_pTextEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_pTextEdit->installEventFilter(this);
    QFont font("Microsoft YaHei");
    font.setPointSize(12);
    m_pTextEdit->setFont(font);
    setPlainText(text);
    setWidget(m_pTextEdit);
    initStyleSheet();
}

setPlainText设置文本最大宽度

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

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

bool TextBubble::eventFilter(QObject *o, QEvent *e)
{
    if(m_pTextEdit == o && e->type() == QEvent::Paint)
    {
        adjustTextHeight(); //PaintEvent中设置
    }
    return BubbleFrame::eventFilter(o, e);
}

调整高度

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

设置样式表

void TextBubble::initStyleSheet()
{
    m_pTextEdit->setStyleSheet("QTextEdit{background:transparent;border:none}");
}

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

#define PIC_MAX_WIDTH 160
#define PIC_MAX_HEIGHT 90

PictureBubble::PictureBubble(const QPixmap &picture, ChatRole role, QWidget *parent)
    :BubbleFrame(role, parent)
{
    QLabel *lb = new QLabel();
    lb->setScaledContents(true);
    QPixmap pix = picture.scaled(QSize(PIC_MAX_WIDTH, PIC_MAX_HEIGHT), Qt::KeepAspectRatio);
    lb->setPixmap(pix);
    this->setWidget(lb);

    int left_margin = this->layout()->contentsMargins().left();
    int right_margin = this->layout()->contentsMargins().right();
    int v_margin = this->layout()->contentsMargins().bottom();
    setFixedSize(pix.width()+left_margin + right_margin, pix.height() + v_margin *2);
}

发送测试

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

void ChatPage::on_send_btn_clicked()
{
    auto pTextEdit = ui->chatEdit;
    ChatRole role = ChatRole::Self;
    QString userName = QStringLiteral("恋恋风辰");
    QString userIcon = ":/res/head_1.jpg";

    const QVector<MsgInfo>& msgList = pTextEdit->getMsgList();
    for(int i=0; i<msgList.size(); ++i)
    {
        QString type = msgList[i].msgFlag;
        ChatItemBase *pChatItem = new ChatItemBase(role);
        pChatItem->setUserName(userName);
        pChatItem->setUserIcon(QPixmap(userIcon));
        QWidget *pBubble = nullptr;
        if(type == "text")
        {
            pBubble = new TextBubble(role, msgList[i].content);
        }
        else if(type == "image")
        {
            pBubble = new PictureBubble(QPixmap(msgList[i].content) , role);
        }
        else if(type == "file")
        {

        }
        if(pBubble != nullptr)
        {
            pChatItem->setWidget(pBubble);
            ui->chat_data_list->appendChatItem(pChatItem);
        }
    }
}

效果展示

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

源码和视频

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

源码链接

https://gitee.com/secondtonone1/llfcchat

标签:pTextEdit,22,对话框,bk,WIDTH,role,聊天,margin,pGLayout
From: https://blog.csdn.net/secondtonone1/article/details/139712803

相关文章

  • 代码随想录 算法训练营 day10 leetcode232 用栈实现队列 Leetcode225 用队列实现栈 Le
    Leetcode232用栈实现队列题目链接讲解用两个栈实现队列每次需要出队列或者查看队头元素时,将输入栈的所有元素放到输出栈classMyQueue{Stack<Integer>stackIn;Stack<Integer>stackOut;publicMyQueue(){stackIn=newStack<>();//负责进......
  • [lnsyoj284/luoguP2286/HNOI2004]宠物收养场
    题意原题链接每个宠物和领养人有一个对应的特点值\(a\),当领养人过多时,若添加一个特点值为\(a_p\)的宠物,则查询收养店中特点值最接近\(a_p\),为\(a_q\)的领养人(\(<a_p\)的值优先),将答案累加\(|a_q-a_p|\),然后删除该领养人,否则在收养店中添加一个领养人。反之亦然。求最终的答案\(......
  • openEuler22.03(LTS-SP3)安装kubernetesV1.29
    一、环境配置主机配置角色系统版本IPmaster012核4GmasteropenEuler22.03(LTS-SP3)192.168.0.111master022核4GmasteropenEuler22.03(LTS-SP3)192.168.0.112master032核4GmasteropenEuler22.03(LTS-SP3)192.168.0.113worker012核4GworkeropenEu......
  • 5月22日
    今天上午上了python课和工程数学,python完成了实验二Python面向对象基础训练【实验编号】【实验专责】刘立嘉;【实验目的】使学生掌握Python下类与对象的基本应用;使学生掌握Python下继承与多态的基本应用;使学生掌握Python接口的基本应用;使学生掌握Python异常......
  • ABC 322 E Product Development
    题意公司要升级一个产品的K种属性,每种的初始值为0。有N种升级计划,第i种花费c[i]的代价给编号为j=1,2,...,K的属性分别增加a[i][j],求把所有属性提升到大于等于P的最小代价题解显然多维费用背包,定义dp[t][i][j][k][s][r]为前t个物品,让这几种属性为i,j,k,s,r的时候的最小费用。在......
  • VS2022最新版Bug
    自从我昨天更新了VS2022,还有下载了VS2022预览版本后,点击文件资源管理器,Handycontrol所在的目录,才到src所在的目录,还没有点进去查看sln所在的目录,文件资源管理器黑屏直接退出,重启电脑也没好,其他的目录,或者其他我下载的源码都能查看,本来sln后缀应该和VS2022图标一样是紫色的,但是那个......
  • 算法训练(leetcode)第九天 | 232. 用栈实现队列、225. 用队列实现栈、20. 有效的括号、1
    刷题记录232.用栈实现队列225.用队列实现栈20.有效的括号1047.删除字符串中的所有相邻重复项232.用栈实现队列leetcode题目地址考察栈与队列之间的特性。栈:后进先出(先进后出)——FILO。队列:先进先出——FIFO。所以使用两个栈模拟队列,分别为in和out。当入队新......
  • 阿里云 Ubuntu 22.04.4 LTS 安装postfix+dovecot 搭建邮件服务器
    一安装1安装postfixsudoapt-getinstallpostfix#如果没有弹出配置界面,运行dpkg-reconfigurepostfix     #sudovim/etc/postfix/main.cfsmtpd_banner=$myhostnameESMTP$mail_name(Ubuntu)biff=noappend_dot_mydomain=noreadme_directory=......
  • 洛谷P8807 [蓝桥杯 2022 国 C] 取模
    题目:解读(思路与分析):题目总结:对于给定的整数n和范围m,要找到两个不同的x和y,它们除以n后的余数相等。思路:对于每组给出的n,m询问,可以通过遍历范围从1到m的所有可能的j,并计算n对j取模的余数。使用一个集合来存储已经出现过的余数,如果当前余数已经存在于集......
  • 中国城市建设统计年鉴(1978-2022年)
    数据年份:1978-2022数据格式:excel、pdf数据内容:以2022年为例,《中国城市建设统计年鉴—2022》根据各省、自治区和直辖市建设行政主管部门上报的2022年及历年城市建设统计数据编辑。共分13个部分,包括城市市政公用设施水平、城市人口和建设用地、城市维护建设财政性资金收支、城......