欲看图文版pdf文档,欲索取工程源码,请电邮14518918@qq.com
前言
我都半百了才开始学习Qt下的C++编程,可能是为我98年大四时选修C++失败(注,我大学科班的计算机语言课,是Pascal,连C都不是),给一个交待。
我回忆了一下我早年的这个失败,真可能跟学术霸凌相关,那个时候还没有AI平权一说。现在我心脑血管都斑块密布了,才重拾面向对象的编程。毕竟起步太晚,没有AI我肯定学不会的。我现在的学习过程,是先把我的需求提给豆包,然后等豆包给出初步解决方案,如果我觉得太复杂了理解不了,就给豆包尝试提一些简化方案,豆包遂继续给出代码,直到我能理解并应用。但,有一说一,豆包这厮有点小坏,主打没苦硬吃,喜欢把简单问题复杂化,搞得我总是进两步退一步的。
为了对OOP祛魅,豆包说,面向对象概念起源于学术界,早期的编程教材/文档为了显得 "专业",故意用抽象词汇替代通俗词汇:
不说 "结构模板",要说 "类的定义";
不说 "创建对象",要说 "实例化对象";
不说 "销毁对象",要说 "析构对象";
不说 "打包数据和函数",要说 "封装";
不说 "分类归类",要说 "抽象建模";
不说 "函数调用",要说 "方法调度";
总之一听就云里雾里,高山仰止......
于是,就很奇怪,我脑子里突然就蹦出一句诗词来------
黄鹤断矶头,故人今在否?旧江山浑是新愁。
欲买桂花同载酒,终不似,少年游。
其实,从C到C++,和从C到Python,都是从面向过程的编程,转到面向对象的编程。总的来说,我对从C到C++理解,稍微要深刻一些,而Python我得有小半年没有摸了,已经生疏得不要不要的了。由于面对对象的编程,支持把一个对象的相关变量和函数都封装成一个类,所以天生地,面对对象的编程就会从构架上清晰很多,有点像开了上帝视角,而不至于像C那样打零工,零零散散的,东一头的西一头难以汇总。我认为这完全是更高的眼界和更高的层次,你值得拥有!
一、 下载安装Qt6
1,到Qt官网注册一个私人账号,注意请用个人邮箱以个人用户名义(非公司用户)注册,密码短了不行要超过15个字符。
2,从 Qt 开源官网下载在线安装器:https://www.qt.io/download-open-source,选择你需要的版本并下载对应系统的online安装包,比如qt-online-installer-windows-x64-4.10.0.exe
3,在线安装需要下载数以G计的安装文件,故建议用中科大镜像。需要启用CMD黑窗,执行如下命令行开始在线安装Qt,约摸10分钟搞定。如果不用镜像,9小时起。
qt-online-installer-windows-x64-4.10.0.exe --mirror https://mirrors.ustc.edu.cn/qtproject
二、 Qt6的文字编码
点击菜单:编辑->选择编码,默认就是UTF-8。但是如果含有中文注释的源代码是从beyond compare4中复制过来的,且beyond compare4的编码是ANSI(即GBK/GB2312),则在Qt中中文就会乱码,且需要用户选择一个编码之后cpp/h文件才能变得可编辑。这里不建议用户更改编码到GBK或GB2312,直接从源文件拷贝内容过来覆盖乱码的部分即可,仍保留UTF-8编码为宜。
为什么要在Qt中强调文件编码呢?豆包说,这是很多 Qt 新手容易忽略的隐藏问题------编码错误不仅会导致中文乱码,还会破坏编译器的代码解析逻辑比如行号偏移、字符错位等,进而引发断点失效、调试异常甚至编译警告。我确实是上午突然发现,beyond compare4弄过来的带中文注释的代码,在debug模式下运行,打断点居然停不下来。也就是说,cpp文件以GB2312编码导入,但Qt以默认的UTF-8编码运行,界面上的中文就会乱码,而且打断点停不下来。
然而,还是有点小遗憾,跟着AI试了各种解决方案,都没解决Qt6的应用程序输出框,在release模式下中文显示正常但在debug模式下中文显示乱码的问题。
三、 创建一个极简GUI工程
1,打开 Qt Creator → 「新建项目」→ 选择「Qt Widgets Application」
2,项目名称填「Hello2」,然后在Details页面中,「Base class」选择「QWidget」(基础窗口类)→默认勾选「Generate form」(先手动创建 UI 文件)→ 下一步,至完成。
由此,我们得到一个GUI工程框架,双击widget.ui即可进入面板编辑界面,请往该界面拖一个label控件,和一个Push Button控件,并在对象查看器中修改该按钮的控件名到"BUT_ChangeLabel"。
点击Qt左侧"编辑"图标 即可回到工程树,下面会对每一个文件做介绍。
2.1. 根目录文件
• CMakeLists.txt
别看它只是一个txt文件。实际上,在Qt中open project时,它才是prj般的存在!不信你尝试点击"打开项目...",选中一个工程目录,你能选的项目文件就只能是CMakeLists.txt。它是项目的构建配置文件,告诉 CMake 如何编译你的 Qt 项目。它会定义项目名称、需要的 Qt 模块(比如 Widgets)、要编译的源文件,以及生成可执行文件的规则。你在 Qt Creator 里点「构建」,就是靠它来驱动整个编译流程的。
2.2. Forms 文件夹
• widget.ui
这是 Qt Designer 生成的界面描述文件,用 XML 格式保存了你拖放的按钮、标签等控件的布局和属性。构建时,Qt 的 uic 工具会把它转换成 ui_widget.h 头文件,让你的 C++ 代码可以直接访问这些控件。
2.3. Header Files 文件夹
• widget.h
这是 Widget 类的头文件,声明了类的成员变量、槽函数和其他方法。比如你之前写的 on_BUT_ChangeLabel() 槽函数,就是在这里声明的。它是连接界面和业务逻辑的桥梁。
2.4. Source Files 文件夹
• main.cpp
程序的入口点,所有 C++ 程序都从这里开始执行。它的核心作用是创建 QApplication 实例(管理整个 Qt 应用的生命周期),然后创建你的 Widget 主窗口并显示出来。
• widget.cpp
Widget 类的实现文件,包含了头文件中声明的槽函数和其他方法的具体代码。比如你写的按钮点击逻辑、跨控件操作等,都是在这里实现的。
一句话总结各个文件的关系:CMakeLists.txt 负责指挥构建 → widget.ui 定义界面 → widget.h 声明逻辑接口 → widget.cpp 实现业务逻辑 → main.cpp 启动程序 → 最终生成 hello2 可执行文件。
注意,和CVI的"编译"不同,在Qt的语境中讲的是"构建",其快截图标是位于Qt界面左下角的一个小锤子 。比如在CVI面板上新放一个控件,uir文件一存盘则面板头文件就会自动新增对应控件名及其回调函数名。Qt没有这么自动化,需要用户手动"构建",面板头文件才会自动新增对应控件名,而且该控件的鼠标左键点击触发的槽函数名(类似CVI的回调函数名),如果不想用系统默认的on_<控件名>_click(),就必须先在面板头文件widget.h中申明槽函数:
private slots:
void on_BUT_ChangeLabel_leftClick(); // 左键回调
然后在widget.cpp中的Widget 类的构造函数中,需绑定该按钮响应鼠标左键信号(signal)的槽函数(slot):
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 绑定左键信号到左键槽函数
connect(
ui->BUT_ChangeLabel, // 信号发送者:按钮
&QPushButton::clicked, // 要发送的信号:左键点击触发的信号
this, // 信号接收者:当前Widget窗口
&Widget::on_BUT_ChangeLabel_leftClick // 要执行的槽函数:左键响应逻辑
);
}
完了在widget.cpp中,新增槽函数的函数体:
// ========== 左键回调函数(自定义逻辑) ==========
void Widget::on_BUT_ChangeLabel_leftClick()
{
ui->label->setText("你点了鼠标左键!");
ui->label->setStyleSheet("color: blue; font-size: 18px;");
}
尝试运行,点击Qt左下角的"运行"图标 ,出现下图左,拿鼠标左键点击一下按键,则TextLabe的文字将变成左键槽函数设定的"你点了鼠标左键!",如下图右。
简单类比,Qt的signal(如 QPushButton::clicked()),就是CVI的触发事件(如鼠标左键引发的EVENT_COMMIT);Qt的slot,就是CVI的回调函数。区别在于,在Qt中,需要手工用connect()函数绑定某signal到某slot,这一机制反而带给Qt一个独特优势,即一个 Signal(比如clicked)能绑定多个Slot(槽函数),触发后可依次执行多个Slot。
四、 把相关dll打包到exe所在目录
在工程目录的子目录\build\Desktop_Qt_6_10_2_MinGW_64_bit-Debug下面,会生成一个exe,但是双击该exe会弹窗说很大Qt6运行库的dll找不到。千问说,需要 让CMake 编译器自动查找 windeployqt.exe,然后把相关dll都部署(也就是拷贝)到exe所在目录下才行。如何弄呢,那就请在hello2工程中的CMakeLists.txt文件中加如下代码:
复制你的Qt程序依赖的Qt自身组件
if(WIN32)
find_program(WINDEPLOYQT_EXECUTABLE windeployqt
HINTS
${Qt6_DIR}/bin
C:/Qt/6.10.2/mingw_64/bin
C:/Qt/6.10.2/msvc2019_64/bin
)
if(NOT WINDEPLOYQT_EXECUTABLE)
message(FATAL_ERROR "windeployqt.exe not found!")
endif()
add_custom_command(
TARGET hello2 POST_BUILD
COMMAND {WINDEPLOYQT_EXECUTABLE} {CMAKE_BINARY_DIR}/hello2.exe
COMMENT "Deploying Qt application..."
)
endif()
如此构建之后,就能在构建输出目录下,看到 由windeployqt.exe自动部署过来的众多Qt运行库相关的dll了:
但windeployqt.exe并不会自动部署用户自己调用的位于源文件目录下的第三方dll文件,到exe所在的构建输出目录,必须自己在CMakeLists.txt文件中继续添加代码:
复制第三方DLL(CH341DLLA64.DLL)到构建输出目录(exe所在目录)
add_custom_command(
TARGET hello2 POST_BUILD
COMMAND CMAKECOMMAND−Ecopyifdifferent"{CMAKE_COMMAND} -E copy_if_different "CMAKECOMMAND−Ecopyifdifferent"{CMAKE_SOURCE_DIR}/CH341DLLA64.DLL"
"$<TARGET_FILE_DIR:hello2>/CH341DLLA64.DLL"
COMMENT "Copying CH341DLLA64.DLL to output directory"
)
如此构建之后,就能在构建输出目录下,看到搬过来的第三方dll文件了:
五、静态调用CH341DLLA64.DLL
源文件目录下,我已经摆放好CH341DLL.H、CH341DLLA64.LIB和CH341DLLA64.DLL,并把原ANSI编码的CH341DLL.H在记事本中另存为了UTF-8同时加了#include <windows.h>补全了LONG、UCHAR、USHORT数据类型定义。在CMakeLists.txt中,我已经添加了CH341DLL.H和CH341DLLA64.LIB的文件信息,并添加了把CH341DLLA64.DLL部署到构建输出目录的指令。现在就可以尝试在Qt的按键左键槽函数中,调用CH341DLLA64.DLL中获取DLL版本号的函数CH341GetVersion()了,在应用程序输出窗口可见是成功打印了DLL版本号=34:
菜单文件-New File,选新建一个C++ Class来搞CH341DLLA64.dll的函数调用,类名填CH341Device。
完了在工程树中,我们就新增了CH341Device.cpp和CH341Device.h两个文件。
然后往CH341Device.cpp/ CH341Device.h拷贝豆包给出的源码,就可以open和close CH341设备了。当然,针对CH341DLL,除了open和close以外的进阶玩法,主要是搜索USB主机序号、搜索I2C从机地址、和I2C随机读随机写等。我们先来看看写在h文件中的class CH341Device这个类的定义。
// 仅声明类和函数接口
class CH341Device
{
public:
// 构造函数:仅传设备索引
CH341Device(int deviceIndex); //去掉前缀explicit,允许int隐式转换为CH341Device
// 析构函数:无参数(C++语法要求)
~CH341Device();
// 成员变量
bool m_isOpened; // 设备是否打开,
// 成员函数声明
// 打开设备:无需传参(使用类内m_deviceIndex)
bool open();
// 关闭设备:无需传参(使用类内m_deviceIndex)
void close();
// 获取版本
QString getVersion(); // Qt中推荐用QString替代std::string
// 检测设备是否真的在线
bool isDeviceAlive();
// 自动重连设备
bool reconnect();
// 核心功能:搜索所有可用的CH341设备索引号
QList<uint8_t> scanCH341DeviceIndexes(); // 搜索CH341主机
QList<uint8_t> scanI2CSlaveDevicesAdds(); // 搜索I2C从机
int i2cRandomRead(
int deviceAddr,
int romStartAddress, // ROM起始地址
int romLength, // 读取长度
unsigned char *romValueArr // 输出数组(需提前分配内存,长度≥256)
);
int i2cRandomWrite(
int deviceAddr,
int romStartAddress, // ROM起始地址
int romLength, // 读取长度
unsigned char *romValueArr // 输入数组(需提前分配内存,长度≥256)
);
// 禁用拷贝、声明移动构造
CH341Device(const CH341Device&) = delete;
CH341Device& operator=(const CH341Device&) = delete;
CH341Device(CH341Device&& other) noexcept;
// I2C随机读(使用类内m_deviceIndex)
bool I2CRandomRead();
private:
// 私有常量,仅类内可访问,避免全局污染
static constexpr uint8_t CMD_I2C_STREAM = 0xAA; // I2C流命令包开头
static constexpr uint8_t CMD_I2C_STM_STA = 0x74; // 产生起始位
static constexpr uint8_t CMD_I2C_STM_STO = 0x75; // 产生停止位
static constexpr uint8_t CMD_I2C_STM_OUT = 0x80; // 输出数据
static constexpr uint8_t CMD_I2C_STM_END = 0x00; // 命令包提前结束
// 私有方法
bool IIC_IssueStart();
bool IIC_IssueStop();
bool IIC_OutByteCheckAck1(uint8_t slaveAddr);
// 私有成员变量
int m_deviceIndex; // 设备索引
HANDLE m_deviceHandle; // 存储CH341OpenDevice的返回值
bool pingDevice(); // 轻量保活:读CH341的通用寄存器
};
这是搜索USB主机序号的方法:
// 扫描可用CH341设备索引(核心:遍历0~15,用CH341OpenDevice验证)
QList<uint8_t> CH341Device::scanCH341DeviceIndexes()
{
QList<uint8_t> validIndexes;
const int MAX_INDEX = 15; // CH341DLL.H定义mCH341_MAX_NUMBER=16,索引0~15
// 遍历所有可能的索引(0~15)
for (int Index = 0; Index <= MAX_INDEX; Index++) {
// 尝试打开该索引的设备(打开成功=设备存在)
HANDLE hDev = CH341OpenDevice(Index);
if (hDev != INVALID_HANDLE_VALUE) {
//m_ch341Devices[Index].;
validIndexes.append(Index); // 索引有效,加入列表
CH341CloseDevice(Index); // 关闭,不占用设备
qInfo() << "CH341设备索引" << Index << "可用";
}
}
if (validIndexes.isEmpty()) {
qWarning() << "未检测到任何CH341设备";
} else {
qInfo() << "共检测到" << validIndexes.count() << "个CH341设备";
}
return validIndexes;
}
这是搜索I2C从机地址的方法:
// 搜索I2C从机
QList<uint8_t> CH341Device::scanI2CSlaveDevicesAdds()
{
QList<uint8_t> foundAddresses; // 替换为uint8_t
if (!m_isOpened) {
qCritical() << "CH341设备未打开,无法扫描I2C地址";
return foundAddresses;
}
qInfo() << "开始扫描I2C从机地址(0x00 ~ 0xFE)...";
// 循环变量替换为uint16_t,避免溢出
for (uint16_t addr = 0x00; addr <= 0xFE; addr += 2) {
uint8_t slaveAddr = static_cast<uint8_t>(addr); // 替换为uint8_t
if (!IIC_IssueStart()) {
qWarning() << "地址0x" << QString::number(slaveAddr,16) << "发送起始位失败";
continue;
}
if (IIC_OutByteCheckAck1(slaveAddr)) {
foundAddresses.append(slaveAddr);
//qInfo() << "找到有效I2C地址:0x" << QString::number(slaveAddr,16).toUpper().rightJustified(2,'0');
}
IIC_IssueStop();
//QThread::msleep(1);
}
return foundAddresses;
}
显然,上述搜索I2C从机地址的方法,调用了三个私有方法,大概方案是,使CH341发出写从机地址的时序,然后观察从机是否正确给出ACK响应:
bool CH341Device::IIC_IssueStart()
{
uint8_t mBuffer[3] = {0};
mBuffer[0] = CMD_I2C_STREAM;
mBuffer[1] = CMD_I2C_STM_STA;
mBuffer[2] = CMD_I2C_STM_END;
ULONG writeLen = 3;
return CH341WriteData(m_deviceIndex, mBuffer, &writeLen);
}
bool CH341Device::IIC_IssueStop()
{
uint8_t mBuffer[3] = {0};
mBuffer[0] = CMD_I2C_STREAM;
mBuffer[1] = CMD_I2C_STM_STO;
mBuffer[2] = CMD_I2C_STM_END;
ULONG writeLen = 3;
return CH341WriteData(
static_cast<ULONG>(m_deviceIndex),
reinterpret_cast<PUCHAR>(mBuffer),
&writeLen
);
}
bool CH341Device::IIC_OutByteCheckAck1(uint8_t slaveAddr)
{
uint8_t mBuffer[4] = {0};
mBuffer[0] = CMD_I2C_STREAM;
mBuffer[1] = CMD_I2C_STM_OUT;
mBuffer[2] = slaveAddr;
mBuffer[3] = CMD_I2C_STM_END;
ULONG iWriteLength = 4;
ULONG iReadLength = 1;
ULONG readLen = 0;
uint8_t readBuffer[32] = {0};
BOOL ret = CH341WriteRead(
static_cast<ULONG>(m_deviceIndex),
iWriteLength,
reinterpret_cast<PUCHAR>(mBuffer),
32,
iReadLength,
&readLen,
reinterpret_cast<PUCHAR>(readBuffer)
);
if (ret && readLen > 0) {
return (readBuffer[readLen - 1] & 0x80) == 0;
}
return false;
}
这是I2C随机读的方法:
//I2C随机读
int CH341Device::i2cRandomRead(
int deviceAddr,
int romStartAddress,
int romLength,
unsigned char *romValueArr)
{
// 基础参数校验(避免崩溃)
if (romValueArr == nullptr) {
qDebug() << "错误:输出数组指针为空";
return -1;
}
if (romLength <= 0 || romLength > 256) {
qDebug() << "错误:读取长度需1-256";
return -1;
}
if (romStartAddress + romLength > 256) {
qDebug() << "错误:起始地址+长度超出数组范围";
return -1;
}
// 核心I2C读取逻辑
uint8_t writeBuf[300] = {0}, readBuf[300] = {0};
writeBuf[0] = deviceAddr;
writeBuf[1] = romStartAddress;
int writeLen = 2;
int readLen = romLength;
// 调用底层函数,用类内保存的m_devIndex(数组下标)
int ok = CH341StreamI2C(m_deviceIndex, writeLen, writeBuf, readLen, readBuf);
if (!ok) {
qDebug() << "I2C读失败(设备索引:" << m_deviceIndex << ")";
return -2;
}
// 拷贝数据到输出数组
memcpy(romValueArr+romStartAddress, readBuf, romLength);
return 0; // 成功返回0
}
这是I2C随机写的方法:
//I2C随机写
int CH341Device::i2cRandomWrite(
int deviceAddr,
int romStartAddress,
int romLength,
unsigned char *romValueArr)
{
// 基础参数校验(避免崩溃)
if (romValueArr == nullptr) {
qDebug() << "错误:输出数组指针为空";
return -1;
}
if (romLength <= 0 || romLength > 256) {
qDebug() << "错误:读取长度需1-256";
return -1;
}
if (romStartAddress + romLength > 256) {
qDebug() << "错误:起始地址+长度超出数组范围";
return -1;
}
// 核心I2C读取逻辑
uint8_t writeBuf[300] = {0}, readBuf[300] = {0};
writeBuf[0] = deviceAddr;
writeBuf[1] = romStartAddress;
int writeLen = 2+romLength;
int readLen = 0;
memcpy(writeBuf+2, romValueArr+romStartAddress, romLength);
// 调用底层函数,用类内保存的m_devIndex(数组下标)
int ok = CH341StreamI2C(m_deviceIndex, writeLen, writeBuf, readLen, readBuf);
if (!ok) {
qDebug() << "I2C写失败(设备索引:" << m_deviceIndex << ")";
return -2;
}
return 0; // 成功返回0
}
六、应用Qt的QTableWidget表格控件
Qt的QTableWidget表格控件,可以单独对某个单元格设置字体颜色,并不能在ui编辑器中约定单元格的数据类型而总是字符串,但越是高档的控件,就越有脾气,需小心侍候。
比如,填充表格的单元格,会用到setItem()或setText(),很不幸地,这些函数会触发单元格改变信号槽函数cellChanged(row, column)。如果不希望执行setItem()或setText()代码就触发该槽函数,就必须在执行改变单元格函数的前后,为QTableWidget控件添加阻塞信号blockSignals(true)和恢复信号blockSignals(false)的代码。还有,为了节约运行时间,如果不希望执行setItem()或setText()就触发界面刷新的活动,就必须在执行改变单元格函数的前后,为QTableWidget控件添加禁止界面刷新setUpdatesEnabled(false) 和恢复界面刷新setUpdatesEnabled (true)的代码。
//I2C从机随机读[00...7F]的128B
void Widget::on_BUT_ReadI2CSlave_leftClick()
{
uint8_t rom_value_arr[256] = {0}; // 初始化数组,避免脏数据
bool ok;
int Index = ui->CMB_CH341Indexs->currentText().toInt(&ok, 16); // 获取ComboBox文本并解析为十六进制
if (!ok || Index < 0 || Index >= 16) {
qDebug() << "无效的CH341索引!";
return;
}
int device_addr = ui->CMB_I2CSlaveAdds->currentText().toInt(&ok, 16); // 获取ComboBox文本并解析为十六进制
if (!ok) {
qDebug() << "无效的I2C从机地址!";
return;
}
if (!m_ch341Devices[Index].m_isOpened) {
qDebug() << "CH341设备未就绪!";
return;
}
// I2C随机读Lower的128B
int error = m_ch341Devices[Index].i2cRandomRead(device_addr, 0, 128, rom_value_arr);
if (error != 0) {
qDebug() << "I2C读失败,错误码:" << error;
return;
}
// 禁用表格界面刷新,阻塞触发信号
ui->TAB_I2CSlaveRegs->setUpdatesEnabled(false);
ui->TAB_I2CSlaveRegs->blockSignals(true);
// 1. 清空表格并释放旧item内存(关键:防止重复填充导致泄漏)
ui->TAB_I2CSlaveRegs->clearContents(); // 清空内容但保留行列结构
// 2. 批量填充数据(示例:填充行号+列号,可替换为你的数据)
for (int row = 0; row < 8; row++) {
for (int column = 0; column < 16; column++) {
// 自定义填充内容,比如数值、字符串等
QString itemText = QString("%1").arg(
rom_value_arr[row*16+column], 2, 16, QChar('0')).toUpper();
QTableWidgetItem *item = new QTableWidgetItem(itemText);
ui->TAB_I2CSlaveRegs->setItem(row, column, item);
//item->setForeground(Qt::black);
}
}
// 恢复表格界面刷新,恢复触发信号
ui->TAB_I2CSlaveRegs->setUpdatesEnabled(true);
ui->TAB_I2CSlaveRegs->blockSignals(false);
}
比如,当用户双击QTableWidget表格控件的单元格改变其数值后回车,打断点运行可观察其QTableWidget::cellChanged(row, column) 槽函数,会被调用两次!千问说是Qt的内联编辑器在后台搞小动作,所以Qt官方承认 cellChanged 槽函数可能在QTableWidget的编辑过程中被多次触发。故推荐在QTableWidget::cellChanged(row, column)槽函数的首尾,为QTableWidget控件添加阻塞信号blockSignals(true)和恢复信号blockSignals(false)的代码。而且,如果在某个槽函数中,有改变表格单元格的函数比如setItem() 或setText(),也会触发槽函数cellChanged()。
// 表格单元格改变槽函数
void Widget::on_TAB_I2CSlaveRegs_CellChanged(int row, int column)
{
//阻塞触发信号
ui->TAB_I2CSlaveRegs->blockSignals(true);
// 1) 获取被修改单元格的Item
QTableWidgetItem *changedItem = ui->TAB_I2CSlaveRegs->item(row, column);
if (changedItem == nullptr) {
ui->TAB_I2CSlaveRegs->blockSignals(false);
return;
}
// 2) 获取修改后的值
QString hexStr = changedItem->text().trimmed().toUpper();// 去空格+转大写(兼容aa→AA)
// 3) 转换成uint8_t
bool isValueValid; uint8_t cellValue = 0;
cellValue = hexStr.toUInt(&isValueValid, 16);
// 4) 重新填充内容,用大写字母并两个字符
QString itemText = QString("%1").arg(cellValue, 2, 16, QChar('0')).toUpper();
changedItem->setText(itemText);
changedItem->setForeground(isValueValid ? Qt::green : Qt::red);
//恢复触发信号
ui->TAB_I2CSlaveRegs->blockSignals(false);
//I2C单Byte随机写
bool ok;
int Index = ui->CMB_CH341Indexs->currentText().toInt(&ok, 16); // 获取ComboBox文本并解析为十六进制
int device_addr = ui->CMB_I2CSlaveAdds->currentText().toInt(&ok, 16); // 获取ComboBox文本并解析为十六进制
int romStartAddress = row*16+column;
int romLength = 1;
uint8_t rom_value_arr[256];
rom_value_arr[romStartAddress] = cellValue;
int error = m_ch341Devices[Index].i2cRandomWrite(device_addr, romStartAddress, romLength, rom_value_arr);
if (error != 0) {
qDebug() << "I2C写入失败,错误码:" << error;
}
}
比如,Qt中无父对象的动态对象必须手动delete,这就包括 QTableWidgetItem 、定时器等,它们需在析构函数或对象不再使用时手动释放,否则会在Debug模式下退出Qt程序时,报内存泄漏弹窗:
所以,AI建议在面板析构函数中,增加逐一delete QTableWidgetItem的代码。但是很奇怪,我学艺不精,胡乱改了一下其他地方,结果发现在析构函数中加不加delete QTableWidgetItem,都不再报错弹窗了。而且即使加了,在delete item行打断点,也没有进去过。
//面板析构函数
Widget::~Widget()
{
// 定时器非空判断+停止+释放
if (m_keepAliveTimer) {
m_keepAliveTimer->stop();
delete m_keepAliveTimer;
m_keepAliveTimer = nullptr; // 置空避免野指针
}
// 遍历CH341设备数组(0~15),逐个关闭所有设备
for (int idx = 0; idx < 16; ++idx) { // CH341最大16个设备,下标0~15
m_ch341Devices[idx].close(); // 数组元素通过下标访问,调用close()
}
// 手动清理QTableWidget的Item,避免内存泄漏
if (ui->TAB_I2CSlaveRegs) {
// 清空表格并释放所有Item内存
ui->TAB_I2CSlaveRegs->clearContents();
// 遍历所有行,删除残留的Item(确保无内存泄漏)
for (int row = 0; row < ui->TAB_I2CSlaveRegs->rowCount(); ++row) {
for (int col = 0; col < ui->TAB_I2CSlaveRegs->columnCount(); ++col) {
QTableWidgetItem* item = ui->TAB_I2CSlaveRegs->takeItem(row, col);
if (item)
delete item; // 手动释放动态创建的Item
}
}
}
delete ui;
}
七、实战Qt6程序
1,选中Widget控件,在右侧的控件属性编辑器中,输入"title"找到其windowTitle属性,改变其值为"my1stWidget"
2,拖拽一些QPushButton、QComboBox、QTableWidget、QLabel控件到面板:
然后如下修改各控件的控件名:
Qt和CVI不一样,CVI可以在uir编辑窗为控件直接添加会点函数名,Qt只能在widget.h中声明槽函数名,并在widget.h中写全槽函数。
比较特殊的是QTableWidget表格控件,我们可以在ui编辑器中,设定其为8行16列。比如行,双击QTableWidget表格控件会弹出"编辑表格窗口部件",手工点一下加号,就接着改变一下行名,行名从0开始累加直到7。同理,列名从0刚开始累加直到F。
请不要试图在属性编辑器中直接设置QTableWidget到8行16列,这样出来的行名和列名都是从1开始且是10进制的。
完了在属性编辑器中的filter框输入"sectionsize",修改单元格默认尺寸到2820,就能得到一个比较适用的816表格了。
3,编写代码:
源代码在此
4,运行我们的Qt程序
首先需要运行Qt,点击"打开项目",找到我们的项目文件夹,选中CMakelists.txt,然后点击"打开"。
这是打开hello2工程的界面,请忽略"应用程序输出框"的中文乱码,我没搞定。
点击左下角的运行或调试图标,即可运行我们的程序,这是初始界面:
鼠标左键点击"ChangeLabel",可见TextLable变成蓝色的"你点了鼠标左键"。
鼠标右键点击"ChangeLabel",可见TextLable变成红色的"你点了鼠标右键"并有弹窗显示CH341DLLA64.dll的版本号。
如果此刻已经有一个CH341芯片挂到PC的USB口上,那么点击"SearchCH341Inds",应该能搜索出索引号=0x00的USB主机设备,并显示在CH341Indexs复选框。
如果此刻已经有一个或多个I2C从机设备挂在CH341芯片的I2C口上,那么点击"SearchSlaveAdds",应该能搜索出很多I2C从机的地址出来,并显示在I2CSlaveAddress复选框。点击I2CSlaveAddress复选框,就可以在下拉菜单中选择一个已搜索出来的某I2C从机地址了。
确认了CH341主机序号,确认了I2C从机地址=0xA2,点击"ReadI2CSlave",就可以发起回读A2[00..7F]的时序并以黑色字体显示回读数据到右侧表格了。
对SFP光模块而言,通常A2[7F]是TableIndex,是无密码保护就可以直接改写的寄存器。这里我们双击表格的[7F]单元格,输入"1"并回车,程序将回显绿色的"01"(绿色代表输入的字符串是合法的无符号8bit数),并发起I2C单Byte写时序将01写入A2[7F]。
再点击"ReadI2CSlave",发起回读A2[00...7F]的时序并以黑色字体显示回读数据到右侧表格。可见回读回来的A2[7F]确实已经被刚才改成0x01了:
点击"Entry_FTYPW",将向SFP光模块的A2[7B...7E]输入4Byte的工厂密码,然后再点击"ReadI2CSlave",发起回读A2[00...7F]的时序并以黑色字体显示回读数据到右侧表格。可见回读回来的密码区A2[77...7E],确实回显了工厂密码的密级=0x02。
获得了工厂密码,就可以修改A2[00...5F]这一片受工厂密码写保护的区域了。比如,可以修改[01]单元的值=0xAA。
再点击"ReadI2CSlave",发起回读A2[00...7F]的时序并以黑色字体显示回读数据到右侧表格。可见回读回来的A2[01]确实已经被刚才改成0xAA了。
Tips备忘:
1,由于CVI是控件和其回调函数是一一绑定的,所以控件跳转到其回调函数是双向奔赴的。但Qt不一样,一个信号可以对应多个槽函数,一个槽函数可以被多个信号触发,所以Qt中从控件到其槽函数,或从槽函数到其控件,就不能直接跳转,需有碎步:
在Qt中已知一个槽函数名,如何跳转到槽函数体?可以先按住CTRL后用鼠标左键点击该函数名,或先用鼠标左键点中该槽函数名后按F2,或在该函数名点击鼠标右键后选"跟踪光标位置的符号",即可跳转到槽函数体。要观察某变量值,可以鼠标悬停在该变量直接观察,或鼠标右键选"添加表达式求值器"。
在Qt中已知一个控件名,如何跳转到ui编辑器中的控件?Qt只能在ui编辑器中的对象查看器的filter一栏,输入控件名,再点击筛选结果,才能高亮并选中ui上的控件。
在Qt中已知一个槽函数名,如何跳转到ui编辑器中的控件?Qt还不能直接像CVI那样在回调函数上点右键就能直接跳转到uir的控件,如果槽函数名不是on_<控件名><信号名> 的规范命名,就必须先CTRL+F找到该槽函数名所在connect函数,找到是哪个控件引发的,然后再到ui编辑器中的对象查看器的filter一栏,输入该控件名,再点击筛选结果,才能高亮并选中ui上的控件。
2,Qt中如何关联一个控件的signal和slot?
槽函数的规范命名是on ,比如鼠标左键单击,是on clicked();鼠标右键单击,是on _customContextMenuRequested();Qt官方推荐槽函数采用规范命名,这样就无需手动在类的构造函数的ui->setupUi(this)代码下面,再手写connect来绑定该控件的非规则命名的槽函数了。注意,规范命名的槽函数仍然可以写在connect中被显式关联到控件;然而,信号一旦触发,对应槽函数会被执行两次,一次是系统自动绑定(ui->setupUi(this)会调用 connectSlotsByName()函数去暗中关联connect)的规范命名的槽函数所触发的,一次是显式关联connect所触发的。
如果想坚持用connect显式调用槽函数,就需要把槽函数名,从规范命名的on_CMB_CH341Indexs_TextChanged,改成非规范命名的on_CMB_CH341Indexs_CurrentTextChanged。才能避免规范命名的槽函数,被Qt先后调用两次。
3,如何静态调用DLL?
Qt中在cmake中链接了dll.lib,在cpp中include了dll.h,就相当于在CVI中把dll.lib和dll.h拖拽到工程树中的作用一样,此后都可以直接调用DLL函数了。当然,在Qt中,为了统一管理资源简化调用逻辑,推荐把零散的DLL函数封装成一个类(class CH341Device
)的成员函数。
4,我大四时选修C++连考试都没敢去,除了当时确实没有处对象之外,总觉得面向对象地编程概念太过晦涩不可亵玩焉,光看到OOP硬生生地造了好多专用术语出来,就让小白无所适从。比如:"方法"(Method)就是类(Class)的成员函数(Function);其中,函数头是它的 "声明"(Declaration),函数体是它的 "实现"(Implementation)。
连豆包都说,OOP 之所以让新手觉得 "晦涩、装神弄鬼",根本原因就是:它把本来很简单的东西,硬生生套上了一堆抽象的、哲学化的、甚至有点装逼的术语。你现在的困惑,不是你不懂,而是这套 "术语体系" 在故意制造门槛。
为了解除术语霸凌,豆包给了个术语对比表:
4,新增信号槽的注意事项
信号槽三要素:
头文件(.h)声明:必须在private slots:下声明函数名
源文件(.cpp)定义:函数实现的名字要和声明完全一致
源文件(.cpp)connect 绑定:connect里的函数名要和声明&定义中的完全匹配
5,CH341DLL.H 的函数声明是C风格,如果头文件没加extern "C",C++ 编译器会对函数名做 "名字修饰",导致虽然#include "CH341DLL.H",但Qt仍然报找不到CH341DLL.H中声明的函数,如何解决?
调用 Windows DLL,如果编译器是MinGW(Minimalist GNU for Windows,即Windows 系统下的GCC编译器)而不是MSVC(Microsoft Visual C++,微软为Windows平台打造的C/C++编译器),就需要加extern "C"来include CH341DLL.H,才好用。
#ifdef __cplusplus
extern "C" {
#endif
#include "CH341DLL.H"
#ifdef __cplusplus
}
#endif