赶上了实习的末班车,现在在做QML开发,第一天的学习成果,一个计时器.逻辑挺简单的,纯QML实现,代码在仓库QT-Timer
学习使用c++的listmodel
学习使用了如何用c++的listmodel来存储数据.
新建一个TImeListModel类继承自QAbstractListModel
class TimeListModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit TimeListModel(QObject *parent = nullptr);
创建一个结构体存储数据对,在用一个列表存储所有的数据,这个datalist就是listview
private:
// 定义一个结构体来存储列表项的数据,包括 idnumber 和 timeStr
struct Data{
QString m_idnumber; // 存储编号
QString m_timeStr; // 存储时间字符串
};
// 使用 QList 来存储所有 Data 结构体的数据
QList<Data> m_datalist;
然后重写三个函数,这三个函数是必须重写的,直接复制就行,都不用改.在定义一个枚举以便 QML 通过这些角色从模型中获取数据。具体来说,NumberRole
和 TimerRole
代表不同的数据属性,能够让 ListView
或其他基于模型的视图组件根据这些角色来访问对应的字段。
// 返回模型中数据的总数,用于 ListView 获取到数据的数量
// 这个函数是 QAbstractListModel 的纯虚函数,必须实现
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
// 返回模型的角色名称,用于在 QML 中根据角色访问数据
// 例如,NumberRole 对应的别名可以被 ListView 使用
virtual QHash<int, QByteArray> roleNames() const override;
// 获取指定行和角色的数据,这个函数会在 ListView 渲染数据时被调用
// 数据是从 QList 中的 Data 结构体获取的
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
// 枚举角色,用于指定 QML 中访问数据的键值
// NumberRole 和 TimerRole 允许 QML 中根据不同的键来访问数据
enum DataRoles{
NumberRole = Qt::UserRole + 1, // 用户自定义的角色从 Qt::UserRole 开始
TimerRole, // 时间角色
};
重写这三个函数的实现,根据你的数据直接抄就行,数据的个数与结构体和枚举对应
rowCount
获取列表里元素的个数
roleNames
设置角色与对应的名称,这样可以在qml使用model.m_idnumber来访问列表元素的idnumber
data
用于获取索引和角色的数据
// 返回列表项的数量,用于 ListView 知道有多少项要显示
int TimeListModel::rowCount(const QModelIndex &parent) const
{
// 返回数据列表的大小
return m_datalist.size();
}
// 定义数据角色的名称映射,用于在 QML 中使用这些角色名称来访问数据
QHash<int, QByteArray> TimeListModel::roleNames() const
{
// 设置角色与对应的名称,这样可以在 QML 中通过 "m_idnumber" 和 "m_timeStr" 获取对应的数据
QHash<int,QByteArray> roles;
roles[NumberRole] = "m_idnumber"; // 角色 NumberRole 对应 m_idnumber
roles[TimerRole] = "m_timeStr"; // 角色 TimerRole 对应 m_timeStr
return roles;
}
// 获取指定索引行和角色的数据,用于 ListView 显示数据
QVariant TimeListModel::data(const QModelIndex &index, int role) const
{
int row = index.row(); // 获取当前索引的行号
// 检查索引是否有效,避免访问越界
if(row < 0 || row >= m_datalist.count()) {
return QVariant(); // 返回无效数据
}
// 获取当前行对应的数据
const Data &data = m_datalist[row];
// 根据传入的角色返回不同的数据
switch(role) {
case NumberRole:
return data.m_idnumber; // 返回编号数据
case TimerRole:
return data.m_timeStr; // 返回时间字符串数据
default:
return QVariant(); // 返回空值
}
}
除了三个必须重写的函数,一般我们会添加一个增加数据 append
和删除数据的函数,我这边直接清空就行使用 clear
.将这两个函数暴露给QML,使用 Q_INVOKABLE
添加上这个后,这两个函数可以在qml中调用
// Q_INVOKABLE 使得这些方法可以从 QML 中被调用
// append 函数用于向模型中添加数据,接收 idnumber 和 timeStr 两个字符串
Q_INVOKABLE void append(const QString &idnumber , const QString &timeStr);
// 清空模型中的所有数据
Q_INVOKABLE void clear();
实现这两个函数,添加数据时需要发送信号,添加时发送,添加完成发送,这样listView会在界面上实时刷新,如果想插入到末尾,发送信号位置是开始位置m_datalist.lastIndexOf(),结束位置也是
// 向数据列表中添加一项新数据
void TimeListModel::append(const QString &idnumber, const QString &timeStr)
{
// 通知视图模型即将插入一行新数据,索引 0 表示新数据会插入到列表的最前面
emit beginInsertRows(QModelIndex(), 0, 0);
// 使用 prepend 将新的 Data 结构体添加到列表的开头
m_datalist.prepend({idnumber, timeStr});
// 通知视图模型插入操作完成,ListView 会根据此信号刷新显示
emit endInsertRows();
}
// 清除所有数据
void TimeListModel::clear()
{
int row = m_datalist.count(); // 获取当前数据的行数
// 只有当数据列表不为空时才执行清除操作
if(row > 0) {
// 通知视图模型即将移除所有行,范围从 0 到最后一行
emit beginRemoveRows(QModelIndex(), 0, m_datalist.size() - 1);
// 清空数据列表
m_datalist.clear();
// 通知视图模型移除操作完成
emit endRemoveRows();
} else {
return; // 如果没有数据,直接返回
}
}
随后在main.cpp中注册这个listmodel,引入头文件,实例化model,这样在qml中可以使用m_TimeListModel
这个对象.第二种方法需要在qml中实例化对象
//把写好的list模型注册到qml中
TimeListModel listmodel;
engine.rootContext()->setContextProperty("m_TimeListModel",&listmodel);
//第二种方法
qmlRegisterType<TimeListModel>("com.timeListModel",1,0,"TimeListModel");
//在qml中import导入
import com.timeListModel 1.0
TimeListModel{
id:m_TimeListModel
}
现在可以把qml中ListView中原来的model替换为m_TimeListModel
,在原来的逻辑中使用append和clear
ListView{
id:list1
anchors.fill:parent
anchors.margins: 20 //让元素离listview有边界
clip: true
model:m_TimeListModel
delegate: recordlist
spacing: 5
}
m_TimeListModel.append(index.toString(),totaltime);
TimeListModel.clear()
多线程优化
在使用的过程中发现自己的计时器时间会慢,并且一直点击记录的话时间1s可以走10s,排查发现是在计时器的间隔取得太小了,取了1太过于消耗资源,改成10的话能解决这个问题.同时也想尝试使用线程来解决.
新建TimerThread 类继承自QObject,只有这样才能使用线程.我们需要发送时间和运行的状态,因此使用信号和QML文件通信
#ifndef TIMERTHREAD_H
#define TIMERTHREAD_H
#include <QObject>
#include <QTimer>
#include <QThread>
#include <cmath>
/******************************************************************************
*
* @file timerthread.h
* @brief 把计时放入线程
*
* @author 纯真丁一郎
* @date 2024/09/18
* @Blog https://www.relxdingyilang.cn/
* @history
*****************************************************************************/
class TimerThread : public QObject
{
Q_OBJECT
public:
explicit TimerThread(QObject *parent = nullptr);
//判断运行状态
bool isRunning = false;
QString caculateTime(int totaltime);
signals:
void timeUpdated(QString totaltimestr); //发送时间给主界面
void sig_isRunning(bool isRunning);//发送状态
public slots:
void start();
void stop();
void pause();
void onTimeout();
private:
int m_totaltime; //总时间
QTimer *timer;
};
#endif // TIMERTHREAD_H
cpp里实现计时的功能启动计时器,计算时间格式.使用定时器的timeout信号,让我们的时间增加
#include "timerthread.h"
TimerThread::TimerThread(QObject *parent)
: QObject{parent}
{
m_totaltime = 0;
timer = new QTimer(this);
connect(timer,&QTimer::timeout,this,&TimerThread::onTimeout);
}
void TimerThread::start(){
timer->start(1);
isRunning = true;
emit sig_isRunning(isRunning);
}
void TimerThread::pause(){
timer->stop();
isRunning = false;
emit sig_isRunning(isRunning);
}
void TimerThread::stop(){
timer->stop();
isRunning = false;
m_totaltime = 0;
emit sig_isRunning(isRunning);
}
void TimerThread::onTimeout(){
//计时
m_totaltime += 1;
emit timeUpdated(caculateTime(m_totaltime));
}
QString TimerThread::caculateTime(int totaltime){
//格式化字符串
int millisecond =totaltime % 1000;
millisecond = std::floor(millisecond/10);
int second = int(std::floor(totaltime /1000) )% 60;
int minute = int(std::floor(totaltime/1000 /60)) % 60;
QString result = (minute<10 ? "0":"") + QString::number(minute)+":"+
(second<10 ? "0":"") + QString::number(second) + ":"+
(millisecond<10 ? "0":"")+QString::number(millisecond);
return result;
}
在main.cpp里实现多线程,实例化timerThread类,在实例化一个工作线程,把我们自己的类放入工作线程,启动工作线程即可.
同时我们需要qmlRegisterType
来注册我们的类,这样才能让QML文件知道要与这个文件通信
//注册计时线程,并将计时线程移动到工作线程
TimerThread timerThread;
QThread workerThread;
timerThread.moveToThread(&workerThread);
//启动工作线程
workerThread.start();
QQmlApplicationEngine engine;
qmlRegisterType<TimerThread>("com.timerthread",1,0,"TimerThread");
main.qml的修改,首先使用import倒入我们的timerThread类,这样我们就可以在qml中实例化,可以加上idimport com.timerthread 1.0
定义两个变量,接受我们信号发送的参数.发送的参数的作用域只在Connection里,所以需要外部变量来接收,方便我们的使用,
property bool isrunning: false
property string totaltime: ""
TimerThread{
id:timerThread
}
Connections{
target:timerThread
// 使用传递过来的 totaltime 参数,信号传递出来的参数在connect内部可以直接使用,在外部不行
onTimeUpdated:{
timerDisplay.text = totaltimestr
totaltime = totaltimestr
//console.log(totaltime)
}
onSig_isRunning:{
isrunning = isRunning
console.log(isRunning)
}
}
后边就将原来的一些变量替换为新接收的变量就行.
使用多线程的方式,定时器间隔取1也能精确计时
实际上 创建的timerThread并没有被使用,在qml中又实例化了另一个TimerThread对象,这两是不同的实例,放入线程的并没有被使用
点击访问博客查看更多内容 |
---|