今天和大家分享一个使用QListView来展现聊天窗口的历史记录的例子, 因为聊天记录可能会有很多, 所以使用试图-模型的方式更加合理
这是最终效果:
ChatHistoryModel继承自QAbstractListModel ,
ChatHistoryViewDelegate继承自QStyledItemDelegate,
这个例子最关键的就是在QStyledItemDelegate的sizeHint函数中对每一条消息所需的高度进行计算,其他都很简单
一共五个文件,包含一个UI文件,可以直接编译运行
//Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "ChatView.h"
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
ChatHistoryModel * mModel;
ChatHistoryViewDelegate * mDelegate;
public slots:
void onAppendClicked();
private:
void drawIcon();
};
#endif // WIDGET_H
//Widget.cpp
#include "Widget.h"
#include "ui_Widget.h"
#include <QApplication>
#include <QPixmap>
#include <QPainter>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
setWindowTitle(" ");
drawIcon();
mModel = new ChatHistoryModel;
ui->listView->setModel(mModel);
mDelegate = new ChatHistoryViewDelegate(ui->listView);
ui->listView->setItemDelegate(mDelegate);
connect(ui->lineEdit,&QLineEdit::returnPressed,this,&Widget::onAppendClicked);
resize(600,400);
}
void Widget::onAppendClicked(){
auto msg = ui->lineEdit->text().trimmed();
if(ui->radioButtonRecv->isChecked()) msg = "r" + msg;
else msg = "s" + msg;
ui->lineEdit->clear();
mModel->append(msg);
ui->listView->scrollToBottom();
}
void Widget::drawIcon(){
static const int LEN = 40;
QPixmap pix(LEN,LEN);
pix.fill(QColor("transparent"));
QPainter painter(&pix);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(QBrush("lime"));
QPainterPath pp;
pp.addEllipse(QPointF(0,0),LEN/2,LEN/2);
pp.addEllipse(QPointF(0,0),LEN/2-6,LEN/2-6);
painter.translate(LEN/2,LEN/2);
painter.drawPath(pp);
setWindowIcon(QIcon(pix));
}
Widget::~Widget()
{
delete ui;
}
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>668</width>
<height>486</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListView" name="listView">
<property name="styleSheet">
<string notr="true">border:none;</string>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="styleSheet">
<string notr="true">border:none;</string>
</property>
<property name="title">
<string/>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>20</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QRadioButton" name="radioButtonRecv">
<property name="text">
<string>接收</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButtonSend">
<property name="text">
<string>发送</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>32</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">border:none;background:transparent</string>
</property>
<property name="text">
<string>我觉得你好多了</string>
</property>
<property name="placeholderText">
<string>输入信息内容</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>
//ChatView.h
#ifndef CHATVIEW_H
#define CHATVIEW_H
#include <QDebug>
#include <QAbstractListModel>
#include <QStyledItemDelegate>
#include <QListView>
class ChatHistoryModel:public QAbstractListModel
{
Q_OBJECT
public:
ChatHistoryModel();
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override ;
Qt::ItemFlags flags(const QModelIndex &index) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override ;
void append(const QString str);
private:
QStringList mMsgList;
};
class ChatHistoryViewDelegate:public QStyledItemDelegate{
Q_OBJECT
public:
explicit ChatHistoryViewDelegate(QListView* parent);
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void setFont(const QFont& font);
void setTextLeftGap(int gap);
private:
QFont mFont;
QListView * mListView;
int mTextGap;//文本左右边距,右边距和左边距是一样的
};
#endif // CHATVIEW_H
//ChatView.cpp
#include "ChatView.h"
#include <QMouseEvent>
#include <QListView>
#include <QEvent>
#include <QLineEdit>
#include <QPainter>
ChatHistoryModel::ChatHistoryModel(){
mMsgList<< "raaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" <<
"s你在说什么?" <<
"ra" <<
"s你没事吧?" <<
"r有事" <<
"sDude,快去看医生吧"<<
"r正在看"<< "s医生怎么说?" <<
"r啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊";
}
void ChatHistoryModel::append(const QString str){
if(str.length() < 2) return;
beginInsertRows(QModelIndex(),rowCount(),rowCount());
mMsgList.push_back(str);
endInsertRows();
}
Qt::ItemFlags ChatHistoryModel::flags(const QModelIndex &index) const{
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QVariant ChatHistoryModel::data(const QModelIndex &index, int role) const {
if(!index.isValid() || index.row() <0 || index.row() >= mMsgList.size()){
return QVariant();
}
if(role == Qt::DisplayRole){
const auto& str = mMsgList[index.row()];
if(str.length() > 1) return str.mid(1);
else return "invalid message";
}
else if(role == Qt::UserRole){
const auto& str = mMsgList[index.row()];
if(str.length() > 0) return str[0];
return 's';
}
return QVariant();
}
int ChatHistoryModel::rowCount(const QModelIndex &parent ) const {
Q_UNUSED(parent)
return mMsgList.size();
}
ChatHistoryViewDelegate::ChatHistoryViewDelegate(QListView* parent):mListView(parent),mTextGap(16){
Q_ASSERT(parent!=nullptr);
mFont = QFont("Microsoft YaHei",12,2);
}
void ChatHistoryViewDelegate::setFont(const QFont& font){
if(mFont != font){
mFont = font;
mListView->update();
}
}
void ChatHistoryViewDelegate::setTextLeftGap(int gap){
if(mTextGap/2 != gap){
mTextGap = gap*2;
mListView->update();
}
}
QSize ChatHistoryViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
QString str = index.data(Qt::DisplayRole).toString();
if(str.length() <= 0) return QSize(0,0);
QFontMetrics fm(mFont);
//在这里,我要给的宽度一定是可以绘画的总宽度,他要尽可能大,这样在paint中才有更多空间来进行间距调整,左右对齐的操作
qreal w = mListView->width();
if(w <= 0) w = 200; //这个分支只有刚创建实例的时候才会发生,而且很快会被覆盖掉
const qreal txth = fm.height();
const qreal vGap = 16;//上下两头的间距,这并不是精确的间距,因为在paint函数中,还要扣除一点点来显示不同行之间的间距
qreal txtw = fm.horizontalAdvance(str);
int times = txtw / (w*0.8-mTextGap) + 1;//总宽度的0.8是一条消息的最大长度,减去边距才是每行有效长度
qreal h = txth * times + vGap;
return QSize(w,h);
}
void ChatHistoryViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
const QString str = index.data().toString();
if(str.length() <= 0) return;
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->setFont(mFont);
QFontMetrics fm(mFont);
qreal txtw = fm.horizontalAdvance(str);//总的文本有效长度
qreal maxw = mListView->width() * 0.8 - mTextGap;//最大文本宽度
if(txtw > maxw) txtw = maxw; //如果有效长度比这个最大长度大,说明换行了
QRect rct = option.rect;
if(index.data(Qt::UserRole) == 's'){
//发送的消息右对齐
rct.setLeft(rct.width() - txtw-mTextGap);
painter->setBrush(QBrush("lightblue"));
}
else{
//接收的消息左对齐
rct.setRight(txtw+mTextGap);
painter->setBrush(QBrush("lightgreen"));
}
rct = rct.adjusted(0,2,0,0);//下面扣除2像素来分隔不同的行
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(rct,8,8);
rct = rct.adjusted(mTextGap/2,0,-mTextGap/2,0);//左右扣除2像素来表示水平文本边距,
painter->setBrush(Qt::NoBrush);
painter->setPen("black");
painter->drawText(rct,Qt::AlignVCenter | Qt::AlignLeft | Qt::TextWordWrap | Qt::TextWrapAnywhere,str);
painter->restore();
}