使用QGraphicsView實(shí)現(xiàn)氣泡聊天窗口+排雷功能
經(jīng)過多方調(diào)查,用Qt實(shí)現(xiàn)氣泡聊天窗口的方式有如下幾個(gè):
- 使用QWebEngineView控件內(nèi)嵌html+CSS
- 使用QTextEdit內(nèi)嵌html
- 使用QGraphicsView實(shí)現(xiàn)
- 使用QWidget自己繪制氣泡樣式實(shí)現(xiàn)
作為一名C++程序員,對CSS+html這套結(jié)構(gòu)的不熟悉導(dǎo)致無法使用前兩個(gè)方案,而第三個(gè)方案又不夠高效,所以最終我選擇了最后一個(gè)方案。
最終效果:

存在問題:無法選擇文字及跨選(但理論上可以通過重寫鼠標(biāo)相關(guān)事件,達(dá)到模擬選擇的效果)
左側(cè)和右側(cè)的消息分別是封裝的兩個(gè)Item,而這兩個(gè)Item又從同一個(gè)基類繼承而來。
氣泡通過重寫void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);函數(shù),在里面根據(jù)文字的寬高計(jì)算氣泡的位置并畫上去,然后再把字寫上去。
并且當(dāng)窗口大小發(fā)生變化時(shí),需要重新計(jì)算文字尺寸,進(jìn)行繪制。
#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;左側(cè)聊天氣泡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); //返回整個(gè)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; //邊框距窗口右側(cè)的距離
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();
//計(jì)算文字總共需要多寬
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());
//三角,用矩形實(shí)現(xiàn)
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());右側(cè)氣泡和左側(cè)氣泡不同,計(jì)算位置時(shí),左端點(diǎn)需要根據(jù)窗口寬度事實(shí)計(jì)算。
#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); //返回整個(gè)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; //邊框距窗口左側(cè)的距離
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();
//計(jì)算文字總共需要多寬
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從零開始,需要補(bǔ)加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);
//氣泡聊天框左端點(diǎn)需要根據(jù)控件寬度計(jì)算
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());
//三角,用矩形實(shí)現(xiàn)
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調(diào)用
#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); //檢查是否需要插入時(shí)間,傳入值為當(dāng)前消息時(shí)間
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();
//滾動(dòng)到底部
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)
{
//截獲鼠標(biāo)點(diǎn)擊事件
}
void ChatView::CheckTime(QDateTime datetime)
{
if (items_.size() == 0|| datetime.secsTo(items_.lastKey())>60 * 5/*5分鐘*/)
{
//第一條消息前插入時(shí)間
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時(shí),一定要注意重寫virtual QRectF boundingRect() const;方法,返回實(shí)際item的尺寸,讓scene知道,并且要加入const,不然當(dāng)消息左上角超出窗口范圍時(shí),會(huì)出現(xiàn)無法觸發(fā)paint的問題。
到此這篇關(guān)于使用QGraphicsView實(shí)現(xiàn)氣泡聊天窗口+排雷的文章就介紹到這了,更多相關(guān)QGraphicsView氣泡聊天窗口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MATLAB中subplot函數(shù)的語法與使用實(shí)例
subplot()是將多個(gè)圖畫到一個(gè)平面上的工具,下面這篇文章主要給大家介紹了關(guān)于MATLAB中subplot函數(shù)的語法與使用的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08
解析OpenSSL1.1.1?centos7安裝編譯aes的c++調(diào)用
這篇文章主要介紹了OpenSSL1.1.1?centos7安裝編譯aes的c++調(diào)用,實(shí)現(xiàn)方法也很簡單,主要是在該文檔內(nèi)加入openssl的lib路徑,感興趣的朋友跟隨小編一起看看吧2022-03-03
C語言 while for do while循環(huán)體詳解用法
在不少實(shí)際問題中有許多具有規(guī)律性的重復(fù)操作,因此在程序中就需要重復(fù)執(zhí)行某些語句。一組被重復(fù)執(zhí)行的語句稱之為循環(huán)體,能否繼續(xù)重復(fù),決定循環(huán)的終止條件2021-10-10
C++編程中new運(yùn)算符的使用學(xué)習(xí)教程
這篇文章主要介紹了C++編程中new運(yùn)算符的使用學(xué)習(xí)教程,是C++入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2016-01-01
Qt基于QRencode實(shí)現(xiàn)生成二維碼
QRencode是一個(gè)開源的庫,專門用于生成二維碼(QR?Code),這篇文章主要為大家詳細(xì)介紹了Qt如何使用QRencode實(shí)現(xiàn)生成二維碼功能,需要的可以參考下2025-02-02
c++網(wǎng)絡(luò)編程下Linux的epoll技術(shù)和Windows下的IOCP模型
c++ 網(wǎng)絡(luò)編程LINUX-epoll/windows-IOCP下socket opoll函數(shù)用法 優(yōu)于select方法的epoll 以及windows下IOCP 解決多進(jìn)程服務(wù)端創(chuàng)建進(jìn)程資源浪費(fèi)問題,感興趣的小伙伴一起來學(xué)習(xí)吧2021-08-08
C++ 標(biāo)準(zhǔn)庫中的 <algorithm> 頭文件算法操作總結(jié)
C++ 標(biāo)準(zhǔn)庫中的 <algorithm> 頭文件提供了大量有用的算法,主要用于操作容器(如 vector, list, array 等),這些算法通常通過迭代器來操作容器元素,本文給大家介紹C++ 標(biāo)準(zhǔn)庫中的 <algorithm> 頭文件算法總結(jié),感興趣的朋友一起看看吧2025-04-04
c++基礎(chǔ)算法動(dòng)態(tài)DP解決CoinChange問題
這篇文章主要為大家介紹了c++基礎(chǔ)算法如何利用動(dòng)態(tài)DP來解決Coin Change的問題示例過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10

