使用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();
  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();
  //计算文字总共需要多宽
  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->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);
  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();
}
#pragma once

#include "chatbaseitem.h"
#include <QGraphicsRectItem>
class ChatTimeItem : public ChatBaseItem
{
public:
  ChatTimeItem(QString time);
  virtual ~ChatTimeItem();
  virtual int Resize(int width);
protected:
  void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
private:
  QGraphicsSimpleTextItem text_item_;
  QString text_;
  int port_width_;
};
#include <QFont>
#include <QPen>

#include "chattimeitem.h"
#include "color.h"
ChatTimeItem::ChatTimeItem(QString time)
  : ChatBaseItem()
{
  text_item_.setText(time);
  item_tool::SetFontColor(&text_item_, 13, false, colorspace::GetTextLightColor());
  text_item_.setParentItem(this);
}
ChatTimeItem::~ChatTimeItem()
int ChatTimeItem::Resize(int width)
  port_width_ = width;
  return text_item_.boundingRect().height();
void ChatTimeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
  int width = text_item_.boundingRect().width();
  text_item_.setX((port_width_ - width) / 2);

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

关于使用QGraphicsView实现气泡聊天窗口+排雷的文章就介绍至此,更多相关QGraphicsView气泡聊天窗口内容请搜索编程宝库以前的文章,希望以后支持编程宝库

 线索二叉树的意义对于一个有n个节点的二叉树,每个节点有指向左右孩子的指针域。其中会出现n+ 1个空指针域,这些空间不储存任何事物,浪费着内存的资源。对于一些需要频繁进行二叉树遍历操作的场合,二叉树的非递 ...