使用QGraphicsView实现气泡聊天窗口+排雷

经过多方调查,用Qt实现气泡聊天窗口的方式有如下几个:

  • 使用QWebEngineView控件内嵌html+CSS
  • 使用QTextEdit内嵌html
  • 使用QGraphicsView实现
  • 使用QWidget自己绘制气泡样式实现

作为一名C++程序员,对CSS+html这套结构的不熟悉导致无法使用前两个方案,而第三个方案又不够高效,所以最终我选择了最后一个方案。
最终效果:

存在问题:无法选择文字及跨选(但理论上可以通过重写鼠标相关事件,达到模拟选择的效果)

左侧和右侧的消息分别是封装的两个Item,而这两个Item又从同一个基类继承而来。
气泡通过重写void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);函数,在里面根据文字的宽高计算气泡的位置并画上去,然后再把字写上去。
并且当窗口大小发生变化时,需要重新计算文字尺寸,进行绘制。

#pragma once

#include <QGraphicsRectItem>

//聊天元素所有item的基类
class ChatBaseItem : public QGraphicsRectItem
{
public:
    ChatBaseItem();
    virtual ~ChatBaseItem();

public:
    virtual int Resize(int width);      //传入值为viewport宽,返回值为item高
};
#include "chatbaseitem.h"

ChatBaseItem::ChatBaseItem()
    : QGraphicsRectItem()
{
}

ChatBaseItem::~ChatBaseItem()
{
}

int ChatBaseItem::Resize(int width)
{
    return 0;
}

左侧聊天气泡Item

#pragma once

#include "chatbaseitem.h"
#include <QDateTime>

class OtherMsgItem : public ChatBaseItem
{

public:
    OtherMsgItem(QPixmap icon, QString name, QString msg, QDateTime datetime = QDateTime());
    virtual ~OtherMsgItem();

    virtual int Resize(int width);  //返回整个item的高度

protected:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
    virtual QRectF boundingRect() const;

private:
    QGraphicsPixmapItem icon_item_;
    QGraphicsSimpleTextItem name_item_;
    QString text_;
    QSize text_size_;     //文字尺寸
    QDateTime datetime_;
};
#include <QPainter>
#include <QMargins>
#include <QTextOption>

#include "othermsgitem.h"

const int kMsgFontSize = 14;
const int kNameFontSize = 13;

const QPoint kNamePos = QPoint(64, 0);
const QPoint kIconPos = QPoint(20, 8);
const QPoint kBorderPos = QPoint(kNamePos.x(), kNamePos.y()+18);
const QMargins kMargins = QMargins(12,11,12,11);        //文字距边框的距离
const QPoint kTextPos = QPoint(kBorderPos.x()+ kMargins.left(), kBorderPos.y() + kMargins.top());
const int kMarginRight = 40;                           //边框距窗口右侧的距离

OtherMsgItem::OtherMsgItem(QPixmap icon, QString name, QString msg, QDateTime datetime /*= QDateTime()*/)
    : ChatBaseItem()
    , datetime_(datetime)
{
    icon_item_.setPixmap(icon);
    icon_item_.setPos(kIconPos);

    text_ = msg;

    QFont font("Microsoft YaHei");
    font.setPixelSize(kNameFontSize);

    name_item_.setText(name);
    name_item_.setPos(kNamePos);
    name_item_.setFont(font);
    name_item_.setBrush(QColor(153, 153, 153));

    icon_item_.setParentItem(this);

    name_item_.setParentItem(this);
}

OtherMsgItem::~OtherMsgItem()
{
}

int OtherMsgItem::Resize(int width)
{
    //每行最大可容纳文字的宽度
    int row_width = width - kTextPos.x() - kMarginRight-kMargins.right();

    //计算文字总共需要多宽
    QFont font("Microsoft YaHei");
    font.setPixelSize(kMsgFontSize);
    QFontMetrics font_matrics(font);
    int text_total_width = font_matrics.width(text_);
    int text_row_height = font_matrics.lineSpacing();

    if(row_width<text_total_width)
    {
        int row = text_total_width / row_width;
        ++row;

        int text_total_height = row* text_row_height;

        text_size_.setWidth(row_width);
        text_size_.setHeight(text_total_height);
    }
    else
    {
        text_size_.setWidth(text_total_width);
        text_size_.setHeight(text_row_height);
    }
    return text_size_.height()+kMargins.top()+kMargins.bottom()+kBorderPos.y();
}

void OtherMsgItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    QSize rnd(17,17);
    QRectF border(kBorderPos.x(), kBorderPos.y(), text_size_.width()+kMargins.left()+ kMargins.right(),text_size_.height() + kMargins.top() + kMargins.bottom());
    //气泡加边
    painter->setPen(QPen(QColor(229, 229, 229), 1, Qt::SolidLine));
    painter->drawRoundedRect(border.x(), border.y(), border.width(), border.height(), rnd.width(), rnd.height());

    //气泡
    painter->setBrush(QBrush(Qt::white));
    painter->setPen(Qt::NoPen);
    painter->drawRoundedRect(border.x()+1, border.y()+1, border.width()-2, border.height()-2, rnd.width(), rnd.height());

    //三角,用矩形实现
    QRect rect1(border.x()+1, border.y()+1, 20, 20);
    painter->setPen(Qt::NoPen);
    painter->setBrush(QBrush(Qt::white));
    painter->drawRect(rect1);

    //三角加边
    QPen pen;
    pen.setColor(QColor(229, 229, 229));
    painter->setPen(pen);
    painter->drawLine(border.x() , border.y() , border.x() +20, border.y() );
    painter->drawLine(border.x() , border.y() , border.x() , border.y() +20);

    QPen penText;
    penText.setColor(QColor(51, 51, 51));
    painter->setPen(penText);
    QTextOption option1(Qt::AlignLeft);
    option1.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
    QFont font("Microsoft YaHei");
    font.setPixelSize(kMsgFontSize);
    painter->setFont(font);

    QRectF text_rect(kTextPos.x(), kTextPos.y(), text_size_.width(), text_size_.height());
    painter->drawText(text_rect, text_, option1);
}

QRectF OtherMsgItem::boundingRect() const
{
    QRectF border(kBorderPos.x(), kBorderPos.y(), text_size_.width() + kMargins.left() + kMargins.right(), text_size_.height() + kMargins.top() + kMargins.bottom());
    return QRectF(0,0,border.width(),border.height());
}

右侧气泡和左侧气泡不同,计算位置时,左端点需要根据窗口宽度事实计算。

#pragma once

#include "chatbaseitem.h"
#include <QDateTime>

class SelfMsgItem : public ChatBaseItem
{
public:
    SelfMsgItem(QPixmap icon, QString msg, QDateTime datetime = QDateTime());
    virtual ~SelfMsgItem();

    virtual int Resize(int width);  //返回整个item的高度

protected:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
    virtual QRectF boundingRect() const;

private:
    QGraphicsPixmapItem icon_item_;
    QString text_;
    QSize text_size_;     //文字尺寸
    QDateTime datetime_;
    int port_width_;
};
#include <QPen>
#include <QPainter>
#include "selfmsgitem.h"

const int kMsgFontSize = 14;
const int kNameFontSize = 13;

const int kIconY = 0;
const int kBorderY = 10;
const int kIconWidth = 34;
const QMargins kIconMargins = QMargins(10,0,20,0);
const QMargins kMargins = QMargins(12, 11, 12, 11);        //文字距边框的距离
const int kMarginLeft = 40;                                //边框距窗口左侧的距离

SelfMsgItem::SelfMsgItem(QPixmap icon, QString msg, QDateTime datetime /*= QDateTime()*/)
    : ChatBaseItem()
    , datetime_(datetime)
    , text_(msg)
{
    icon_item_.setPixmap(icon);
    icon_item_.setY(kIconY);
    icon_item_.setParentItem(this);
}

SelfMsgItem::~SelfMsgItem()
{
}

int SelfMsgItem::Resize(int width)
{
    port_width_ = width;
    //每行最大可容纳文字的宽度
    int row_width = width - kMarginLeft - kMargins.left() - kMargins.right() - kIconWidth - kIconMargins.left() - kIconMargins.right();

    //计算文字总共需要多宽
    QFont font("Microsoft YaHei");
    font.setPixelSize(kMsgFontSize);
    QFontMetrics font_matrics(font);
    int text_total_width = font_matrics.width(text_);
    int text_row_height = font_matrics.lineSpacing();

    if (row_width < text_total_width)
    {
        int row = text_total_width / row_width;

        int text_total_height = (row+1)* text_row_height;   //row从零开始,需要补加1

        text_size_.setWidth(row_width);
        text_size_.setHeight(text_total_height);
    }
    else
    {
        text_size_.setWidth(text_total_width);
        text_size_.setHeight(text_row_height);
    }
    return text_size_.height() + kMargins.top() + kMargins.bottom() + kBorderY;
}

void SelfMsgItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    QSize rnd(17, 17);
    //气泡聊天框左端点需要根据控件宽度计算
    QRectF border(port_width_- kIconMargins.left()-kIconMargins.right()-kIconWidth-text_size_.width()-kMargins.left()-kMargins.right()
        , kBorderY
        , text_size_.width() + kMargins.left() + kMargins.right()
        , text_size_.height() + kMargins.top() + kMargins.bottom());
    icon_item_.setX(border.x()+ border.width() + 10);

    //气泡
    painter->setBrush(QBrush(QColor(149,182,57)));
    painter->setPen(Qt::NoPen);
    painter->drawRoundedRect(border.x(), border.y(), border.width(), border.height(), rnd.width(), rnd.height());

    //三角,用矩形实现
    QRect rect1(border.x() + border.width() - 20, border.y(), 20, 20);
    painter->setPen(Qt::NoPen);
    painter->setBrush(QBrush(QColor(149, 182, 57)));
    painter->drawRect(rect1);

    QPen penText;
    penText.setColor(QColor(255, 255, 255));
    painter->setPen(penText);
    QTextOption option1(Qt::AlignLeft);
    option1.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
    QFont font("Microsoft YaHei");
    font.setPixelSize(kMsgFontSize);
    painter->setFont(font);

    QRectF text_rect(border.x()+kMargins.left(), border.y() + kMargins.top(), text_size_.width(), text_size_.height());
    painter->drawText(text_rect, text_, option1);
}

QRectF SelfMsgItem::boundingRect() const
{
    QRectF border(port_width_ - kIconMargins.left() - kIconMargins.right() - kIconWidth - text_size_.width() - kMargins.left() - kMargins.right()
        , kBorderY
        , text_size_.width() + kMargins.left() + kMargins.right()
        , text_size_.height() + kMargins.top() + kMargins.bottom());
    return QRectF(0, 0, border.width(), border.height());
}

接下是view调用

#pragma once

#include <QGraphicsView>
#include <QDateTime>
#include <QMap>

class ChatBaseItem;
class ChatView : public QGraphicsView
{
    Q_OBJECT

public:
    ChatView(QWidget *parent);
    ~ChatView();

    void Resize(int width);

    void ClearAll();
    void AppendSelfMessage(QPixmap icon, QString msg, QDateTime datetime);
    void AppendOtherMessage(QPixmap icon,QString name, QString msg, QDateTime datetime);
protected:
    virtual void mousePressEvent(QMouseEvent *e);

private:
    void CheckTime(QDateTime datetime);               //检查是否需要插入时间,传入值为当前消息时间
    void AppendTime(QDateTime datetime, QString time);
    QMap<QDateTime, ChatBaseItem*> items_;
};
#include <QDebug>
#include <QTextEdit>
#include <QScrollBar>
#include <QGraphicsScene>
#include "chatview.h"
#include "chatbaseitem.h"
#include "selfmsgitem.h"
#include "othermsgitem.h"
#include "chattimeitem.h"
#include "src/vapplication.h"

const int kMarkRole = Qt::UserRole;
const int kRoleOtherMsg = kMarkRole + 1;
const int kRoleSelfMsg = kRoleOtherMsg + 1;
const int kRoleTime = kRoleSelfMsg + 1;

ChatView::ChatView(QWidget *parent)
    : QGraphicsView(parent)
{
    setScene(new QGraphicsScene());
    this->setAlignment(Qt::AlignLeft | Qt::AlignTop);

    setStyleSheet("background: rgb(245,245,245) ;border:0px");
    this->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    this->verticalScrollBar()->setStyleSheet(theStyleSheet["scrollbar"]);
}

ChatView::~ChatView()
{
    ClearAll();
}

void ChatView::Resize(int width)
{
    int height = 20;
    for (ChatBaseItem* item : items_)
    {
        item->setPos(0, height);
        height = height + item->Resize(width);
        height += 20;
    }

    this->scene()->setSceneRect(QRectF(0, 0, width, height));
}

void ChatView::ClearAll()
{
    for (auto it = items_.begin(); it != items_.end();)
    {
        ChatBaseItem* item = it.value();
        it = items_.erase(it);
        delete item;
    }
}

void ChatView::AppendSelfMessage(QPixmap icon, QString msg, QDateTime datetime)
{
    ChatBaseItem* item = new SelfMsgItem(icon, msg, datetime);
    item->setData(kMarkRole,kRoleSelfMsg);
    CheckTime(datetime);
    scene()->addItem(item);
    items_.insert(datetime, item);

    Resize(this->viewport()->width());
    update();

    //滚动到底部
    QScrollBar *vScrollBar = verticalScrollBar();
    vScrollBar->setValue(vScrollBar->maximum());
}

void ChatView::AppendOtherMessage(QPixmap icon, QString name, QString msg, QDateTime datetime)
{
    ChatBaseItem* item = new OtherMsgItem(icon, name, msg, datetime);
    item->setData(kMarkRole, kRoleOtherMsg);
    CheckTime(datetime);
    scene()->addItem(item);
    items_.insert(datetime, item);

    Resize(this->viewport()->width());
    update();
    QScrollBar *vScrollBar = verticalScrollBar();
    vScrollBar->setValue(vScrollBar->maximum());
}

void ChatView::mousePressEvent(QMouseEvent *e)
{
    //截获鼠标点击事件
}

void ChatView::CheckTime(QDateTime datetime)
{
    if (items_.size() == 0|| datetime.secsTo(items_.lastKey())>60 * 5/*5分钟*/)
    {
        //第一条消息前插入时间
        QDateTime dt = datetime.addMSecs(-1);
        AppendTime(dt,dt.toString("hh:mm:ss"));
    }
}

void ChatView::AppendTime(QDateTime datetime, QString time)
{
    ChatBaseItem* item = new ChatTimeItem(time);
    item->setData(kMarkRole, kRoleTime);
    scene()->addItem(item);
    items_.insert(datetime, item);

    Resize(this->viewport()->width());
    update();
}

在子类化Item时,一定要注意重写virtual QRectF boundingRect() const;方法,返回实际item的尺寸,让scene知道,并且要加入const,不然当消息左上角超出窗口范围时,会出现无法触发paint的问题。

原文地址:https://www.cnblogs.com/AlainGao/p/10912354.html

时间: 2024-08-30 13:48:27

使用QGraphicsView实现气泡聊天窗口+排雷的相关文章

如何弹出一个窗口气泡(使用定时器向上移动)

原文链接:http://blog.csdn.net/tangaowen/article/details/5108980 如何弹出一个窗口气泡 最近在工作中遇到这样一个需求,就是需要将一个窗口从右下角任务栏下面缓缓的上升到任务栏的上面,现在有很多的软件都有这样的气泡,比如:搜狗输入法的词条更新窗口,还比如CSDN的广告窗口等等. 1.首先 将要弹出的窗口移动到任务栏(当前屏幕)以下 2.然后,获得任务栏(本质是个窗口)的高度,这样就可以知道窗口最终的位置了 3.然后,计算获得窗口最终停止的位置:计

纯css 画气泡

我们知道运用css可以绘画出各式各样的形状:三角形,圆形,正方形,椭圆形,平行四边形等等,而通过他们之间进行两两组合可以变换出各种意想不到的效果图,气泡框就是其中一个.最简单的气泡框就是一个矩形框+一个三角框. 我们先来看看用css如何画三角形:  css绘三角形之 border方法 先画一个100*100的矩形,给他四边加上不同颜色的边框.代码如下: .demo{ width:100px; height:100px; border:50px solid ;border-color:red pi

WinForms 实现气泡提示窗口(转载)

[实例说明] 气泡提示因为他的美观又好被大多数用户所接收,用户所喜爱的就是程序员要实现的. 本实例实现了任务栏气泡提示,运行本实例,效果图如下所示: 单击提示.气泡提示就会显示,单击“关闭”气泡又会消失掉. [关键技术] 本实例实现时用到了NotifyIcon控件的ShowBallonTip方法. NotifyIcon控件表示在通知区域中创建图标的控件,其ShowBalloonTip方法用于在任务栏中持续显示具有指定标题.问题和图标的气球提示指定的时间,该方法的语法格式如下: 1 /** 2 *

气泡的图像增强

对于这样的图片,如果只是基于普通的阈值处理.或者是梯度增强,都会因为背景比较复杂,从而结果不是很理想.很久之前,我的考虑就是要基于图像的本质特征. 什么是图像的本质特征?比如这里我关心的是圆的区域,它的特点就是梯度从四周向中心有强烈的抖动变化.那么如何将这个变化表现出来?最为直观的就是计算图像的梯度场. 这个结果出来以后,就可以比较明显.这里梯度场的计算,是对sobel在x方向和y方向的结果分别作平均(就是一个小区域求平均值)后再求和.思路比较简洁明了,但是结果却是能够看出,感兴趣的区域都凸显出

模仿QQ气泡聊天

尝试了几种方案,想模仿QQ的气泡聊天,总是不尽如意.网上倒是大把的Android和Html的例子,Delphi的没找着,只能自己试着折腾. 1. 用WebBrowser加载本地html,屡次折腾,失败. 遇到的问题是(1)CSS3效果显示不出来(2)不熟悉JS,没整明白如何加载记录.刷新数据. 2. VCLForm中加载FMXForm, 效果倒是出来了,跟下图相差不大,但是结果还是失败. 遇到的问题是(1)使用的FireMonkey控件有几个报错没搞定(2)引用了FMX相关的单元后,编译出来的e

纯CSS气泡框实现方法探究

气泡框(或者提示框)是网页中一种很常见的元素,大多用来展示提示信息,如下图所示: 拆分来看,形如这种气泡框无外乎就是一个矩形框+一个指示方向的三角形小箭头,要制作出这样的气泡框,如果解决了三角形小箭头就容易了.一种方法就是制作这样一个三角形箭头的图片,然后定位在矩形框上.但这种解决办法在后期更改气泡框会很不方便,可能每修改一次气泡框都要重新制作一个三角形小图标.如果我们能够直接用HTML和CSS代码实现这样一个三角形小箭头一切都迎刃而解了. 首先我们来看一下border这个属性,当我们把一个di

动画气泡指示当前滑动值--第三方开源--DiscreteSeekbar

DiscreteSeekbar在github上的项目主页是:https://github.com/AnderWeb/discreteSeekBar DiscreteSeekbar可以自定制的属性很多,可以在其github的项目主页上查看.DiscreteSeekbar可以像Android 原生的Seekbar一样使用. 使用方法: 写布局activity_main.xml: 1 <LinearLayout xmlns:android="http://schemas.android.com/

android 仿QQ气泡聊天界面

1.现在的QQ,微信等一些APP的聊天界面都是气泡聊天界面,左边是接收到的消息,右边是发送的消息, 这个效果其实就是一个ListView在加载它的Item的时候,分别用了不同的布局xml文件. 2.效果图(其中的聊天信息框是采用了.9.png的图片): 3.对中间聊天的的listView进行说明: 左边Item的xml文件效果如下: 右边Item的xml文件效果如下: 4.在加载ListView当中重写getView()方法,通过判断消息传入类型,来使item加载哪一个xml文件: 自定义ada

纯CSS实现气泡框

效果图如下: 源码: <!DOCTYPE html public "-//w3c//dtd html 4.01 transitional//en" "http://www.w3c.org/tr/html4/loose.dtd"> <html> <head> <meta charset="utf-8"> <title></title> <style type="