Qt 使用QListView实现简约美观的聊天窗口

news2025/4/4 8:49:52

今天和大家分享一个使用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();
}








本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1492998.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Keil MDK安装armcc V5编译器

不知道从什么时候开始&#xff0c;Keil MDK默认不支持V5的编译器了&#xff0c;里面默认只有V6的编译器&#xff0c;设置界面跟V5有很大的差异不太熟悉。最可怕的是&#xff0c;之前使用V5编译的工程&#xff0c;换成V6编译器后居然报错...虽然修改一下应该也可以正常编译&…

几种常用的查看Ros话题的命令

1、查看 rqt_tf_tree rosrun rqt_tf_tree rqt_tf_tree 2、查看ros node 关系 rqt_graph 3、列出所有话题 rostopic list 4、查看某一话题的类型 以 /scan 为例 rostopic type /scan 5、查看某个数据类型包含哪些数据 rosmsg show sensor_msgs/LaserScan sensor_…

Gitlab修改仓库权限为public、Internal、Private

Public&#xff08;公开&#xff09;&#xff1a;所有人都可以访问该仓库&#xff1b; Internal&#xff08;内部&#xff09;&#xff1a;同一个GitLab群组或实例内的所有用户都可以访问该仓库&#xff1b; Private&#xff08;私人&#xff09;&#xff1a;仅包括指定成员的用…

2024年最新《国际预警期刊》正式更新!

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、国际期刊预警名单的变化&#xff1f;二、课程案例展示&#xff08;篇幅有限仅展示部分&#xff09;1.【热图系列】2.【九象限图系列】3.【富集分析系列】4.【机…

自动化测试基础——Pytest框架之YAML详解以及Parametrize数据驱动

文章目录 一、YAML详解1.YAML作用2.YAML语法结构3.YAML数据类型3.1.对象3.2.数组3.3.标量 4.YAML的引用5.YAML类型转换 二、YAML的读写与清空1.YAML的读2.YAML的写3.YAML的清空 三、pytest的parametrize简单数据驱动四、pytest的parametrize结合yaml实现数据驱动五、解决pytest…

数据库期末速成100分训练,附练手数据库原件及教程

本文提供下面数据库代码的数据库原件&#xff0c;下载后可使用 教程如下&#xff1a; 1.打开sql sever 2.找到数据库 3.右键数据库点击“附加”&#xff0c;然后点击“添加” 4.导入数据库原件&#xff0c;点击确定 ps&#xff1a;如果没有sqlsever 或者页面编辑器&#x…

基于SSM的农业信息管理系统的设计与实现(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的农业信息管理系统的设计与实现&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;…

JavaScript中的innerHTML、value属性,零基础开发web前端

CSS篇 让一个元素水平垂直居中&#xff0c;到底有多少种方案&#xff1f;浮动布局的优点&#xff0c;缺点&#xff1f;清除浮动的方式&#xff1f;使用display:inline-block会产生的问题&#xff1f;解决方法&#xff1f;布局题&#xff1a;div垂直居中&#xff0c;左右10px&a…

LeNet5实战——衣服分类

搭建模型训练代码&#xff08;数据处理、模型训练、性能指标&#xff09;——> 产生权重w ——>模型结构c、w测试 配置环境 Pycharm刚配置的环境找不到了-CSDN博客 model.py 导入库 import torch from torch import nn from torchsummary import summary 模型搭…

【HarmonyOS】Dev Eco Studio4.0安装教程

目录 下载安装开发工具配置 下载 Dev Eco Studio4.0下载连接→https://developer.huawei.com/consumer/cn/next/deveco-studio/ 安装 点击Next 选择安装目录。点击Next 勾选创建桌面快捷方式和环境变量&#xff0c;点击Next 点击Install&#xff0c;安装 等待安装 选…

android开发视频教程百度网盘,深入浅出Android

基于Linux的pc启动过程 我们都知道&#xff0c;所有的程序软件包括操作系统都是运行在内存中的&#xff0c;然而我们的操作系统一般是存放在硬盘上的&#xff0c;当我们按下开机键的时候&#xff0c;此时内存中什么程序也没有&#xff0c;因此需要借助某种方式&#xff0c;将操…

JavaWeb环境配置 IDE2022版

一、新建一个javaweb文件 文件名可以自己随意改 二、给建立的项目添加框架支持 勾选Web Application,点击确定 建立成功界面&#xff0c;会生成一个新的web文件夹 三、配置tomcat 1、两种打开配置文件方式&#xff1a; 第一种 第二种 2、打开后&#xff0c;点击号&#xf…

Redis面试问题纯享版

基础内容 1、简单介绍以下你了解的Redis 2、对比一下Redis和Memcache的异同&#xff1f; 3、为什么MySQL选用Redis作为缓存&#xff1f; 4、详细聊聊你对Redis各种数据类型的了解&#xff1f; 5、Redis中五种基本数据类型的底层数据结构是什么样的&#xff1f; Redis线程模型…

结合大象机器人六轴协作机械臂myCobot 280 ,解决特定的自动化任务和挑战!(上)

项目简介 本项目致力于探索和实现一种高度集成的机器人系统&#xff0c;旨在通过结合现代机器人操作系统&#xff08;ROS&#xff09;和先进的硬件组件&#xff0c;解决特定的自动化任务和挑战。一部分是基于Jetson Orin主板的LIMO PPRO SLAM雷达小车&#xff0c;它具备自主导航…

flutterpush消息,Android开发两年

如何进阶Android&#xff1f; 有些东西你不仅要懂&#xff0c;而且要能够很好地表达出来&#xff0c;能够让面试官认可你的理解&#xff0c;例如Handler机制&#xff0c;这个是面试必问之题。有些晦涩的点&#xff0c;或许它只活在面试当中&#xff0c;实际工作当中你压根不会…

LeetCode_24_中等_两两交换链表中的节点

文章目录 1. 题目2. 思路及代码实现&#xff08;Python&#xff09;2.1 递归2.2 迭代 1. 题目 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换…

微信小程序接入百度地图(微信小程序插件)使用文档

第一步配置域名 :在微信公众平台登录后配置服务域名称:https://apis.map.qq.com 第二步申请密钥 申请开发者密钥申请地址 第三步使用插件 选择添加插件 搜索腾讯位置服务地图选点 选择要授权的小程序 授权完毕会在这里显示插件信息 第四步查看使用文档 跳转至文…

[BUUCTF]-PWN:starctf_2019_babyshell解析(汇编\x00开头绕过+shellcode)

查看保护 查看ida 这里就是要输入shellcode&#xff0c;但是函数会有检测。 在shellcode前面构造一个以\x00机器码开头的汇编指令&#xff0c;这样就可以绕过函数检查了。 完整exp&#xff1a; from pwn import* context(log_leveldebug,archamd64) pprocess(./babyshell)she…

开源项目:图像分类算法在保险行业的创新应用与实践

一、引言 在当今数字化时代&#xff0c;保险行业正经历着前所未有的变革。传统保险公司面临着新兴科技的挑战&#xff0c;被迫重新思考其业务模式和营销策略。在这种背景下&#xff0c;我有幸参与了一个项目&#xff0c;该项目旨在通过整合多种销售渠道和技术手段&#xff0c;提…

电脑自动锁屏怎么设置?让你安心使用电脑

随着时代的进步&#xff0c;电脑已经成为我们日常生活中不可或缺的一部分。在使用电脑的过程中&#xff0c;为了保护隐私、节省能源或确保系统安全&#xff0c;许多用户都希望能够设置电脑自动锁屏。本文将详细介绍电脑自动锁屏怎么设置的三种方法&#xff0c;帮助用户轻松实现…