QT概括-Rainy

Qt 虽然经常被当做一个 GUI 库,用来开发图形界面应用程序,但这并不是 Qt 的全部;Qt 除了可以绘制漂亮的界面(包括控件、布局、交互),还包含很多其它功能,比如多线程、访问数据库、图像处理、音频视频处理、网络通信、文件操作等,这些 Qt 都已经内置了。

笔者大部分时间都在使用Qt开发各类应用,qt又恰好弥补了c++语言本身开发业务所需要的的库,纯C++一般做业务开发大量依赖第三方库,导致一个项目可能混杂十几个第三方库,每个库的线程管理机制都不尽相同,库使用文档之类的学习成本也甚高,这很让人困扰给开发者造成许多额外负担。

不过要学习qt也不是一件简单的事情,它的设计虽然已经尽可能易于使用,但不意味着简单qt就简单好学,它仍需要使用者对时间及自身的沉淀。



相关网址

官网在线下载
其它下载
关于大佬Qt总结
QML Book
QML Book中文
QCustomPlot 绘图库

ps:关于QML的学习,可以前往B站,输入QML搜索关键字,便可查阅到相关的大量学习视频。



常用小技巧

换源安装提速

- 中国科学技术大学 http://mirrors.ustc.edu.cn/qtproject/
- 清华大学 https://mirrors.tuna.tsinghua.edu.cn/qt/
- 北京理工大学 http://mirror.bit.edu.cn/qtproject/
- 中国互联网络信息中心 http://mirror.bit.edu.cn/qtproject/
// cmd命令程序可以是qt-unified-windows-x64-4.5.2-online,也可以是MaintenanceTool.exe。
[cmd] --mirror [URL]
// 示例
qt-unified-windows-x64-4.5.2-online.exe --mirror https://mirror.nju.edu.cn/qt  
PS D:\qt> .\MaintenanceTool.exe --mirror https://mirror.nju.edu.cn/qt

延迟调用对象函数

使用QMetaObject::invokeMethod()函数,进行安全调用及延迟调用。

// 记住最后一个参数必须为Qt::QueuedConnection,这样它就会进入对象的线程队列中去,否则它会立即执行的。
QMetaObject::invokeMethod(this, std::bind(&App::onOpen, this), Qt::QueuedConnection);

windows打包

可以在Qt的安装目录中,找到${QT_PATH}\qt\6.2.4\msvc2019_64\binwindeployqt.exe来进行程序打包。这里是对应msvc版本的,如果是mingw则去mingw的路径中去寻找。

D:\qt\6.2.4\msvc2019_64>windeployqt -h
Usage: windeployqt [options] [files]
Qt Deploy Tool 6.2.4The simplest way to use windeployqt is to add the bin directory of your Qt
installation (e.g. <QT_DIR\bin>) to the PATH variable and then run:windeployqt <path-to-app-binary>
If ICU, etc. are not in the bin directory, they need to be in the PATH
variable. If your application uses Qt Quick, run:windeployqt --qmldir <path-to-app-qml-files> <path-to-app-binary>Options:-?, -h, --help              Displays help on commandline options.--help-all                  Displays help including Qt specific options.-v, --version               Displays version information.--dir <directory>           Use directory instead of binary directory.--qmake <path>              Use specified qmake instead of qmake from PATH.--libdir <path>             Copy libraries to path.--plugindir <path>          Copy plugins to path.--debug                     Assume debug binaries.--release                   Assume release binaries.--pdb                       Deploy .pdb files (MSVC).--force                     Force updating files.--dry-run                   Simulation mode. Behave normally, but do notcopy/update any files.--no-patchqt                Do not patch the Qt6Core library.--ignore-library-errors     Ignore errors when libraries cannot be found.--no-plugins                Skip plugin deployment.--no-libraries              Skip library deployment.--qmldir <directory>        Scan for QML-imports starting from directory.--qmlimport <directory>     Add the given path to the QML module searchlocations.--no-quick-import           Skip deployment of Qt Quick imports.--translations <languages>  A comma-separated list of languages to deploy(de,fi).--no-translations           Skip deployment of translations.--no-system-d3d-compiler    Skip deployment of the system D3D compiler.--compiler-runtime          Deploy compiler runtime (Desktop only).--no-virtualkeyboard        Disable deployment of the Virtual Keyboard.--no-compiler-runtime       Do not deploy compiler runtime (Desktop only).--json                      Print to stdout in JSON format.--no-opengl-sw              Do not deploy the software rasterizer library.--list <option>             Print only the names of the files copied.Available options:source:   absolute path of the source filestarget:   absolute path of the target filesrelative: paths of the target files, relativeto the target directorymapping:  outputs the source and the relativetarget, suitable for use within anAppx mapping file--verbose <level>           Verbose level (0-2).Qt libraries can be added by passing their name (-xml) or removed by passing
the name prepended by --no- (--no-xml). Available libraries:
bluetooth concurrent core declarative designer designercomponents gamepad gui
qthelp multimedia multimediawidgets multimediaquick network nfc opengl
openglwidgets positioning printsupport qml qmltooling quick quickparticles
quickwidgets script scripttools sensors serialport sql svg svgwidgets test
websockets widgets winextras xml webenginecore webengine webenginewidgets 3dcore
3drenderer 3dquick 3dquickrenderer 3dinput 3danimation 3dextras geoservices
webchannel texttospeech serialbus webview shadertoolsArguments:[files]                     Binaries or directory containing the binary.

打开vim按键映射

勾选使用FakeVim
QtVim



核心知识点

对象树

对象树机制并不是继承子父类关系,而是一种对象与对象之间的父节点与字节点的关系。在这个机制下,Qt是不建议你使用栈内存创建对象的(最顶层节点对象除外),所以你创建Qt对象应该以new动态内存分配比较合适。

setParent()方法可以设置对象的上级节点关系,一旦设置了这种节点关系之后,在父节点对象在析构销毁时,则会把子节点进行释放,以此来达到内存泄露的管理问题。

不过对象树有一点限制,就是对象树的整个节点树必须都是同一个线程对象绑定。假设对象A关联线程A,对象B关联线程B它们之间是无法设置父子节点对象树关系的。要设置对象树关系必须满足,对象A关联线程A,对象B也关联线程A,它们之间关联同一个线程才可以设置它们之间的对象树关系。

信号与槽与多线程

其实信号与槽是很优秀机制,它把异步编程做了很巧妙的封装,同时提出了解决多线程解决方案及思路及设计。

在关联信号与槽时,提供了一个参数,这个参数描述了触发信号时如何执行槽函数的策略,大多数时候我们不填最后一个参数代表默认自动。

对笔者而言只关注两个点,触发信号时立即调用还是由别的线程调用?

假设对象A是发射信号方,对象B是槽函数处理方。根据线程关联机制,那么有两种情况,1.对象A和对象B关联到同一个线程。2.对象A和对象B关联在不同的线程。

如果针对的是情况1,同属于一个线程,那么它则会立即调用。只有一个线程并不存在线程的缓存一致性问题及资源互斥问题。

如果针对的是情况2,不同属一个线程,那么它不会立即调用。而是将处理投入到槽函数所在的对象事件列表中,等待时机进行调用。

它这么设计的原因是,是以单线程为模型的多线程设计。因为在单线程中不存在资源互斥的问题,但是有些数据是要在另一个线程处理的,处理后的结果需要给回这个线程。因为它的每个线程都有一个执行事件队列,我们投入设置操作由那个线程去执行,在投入执行队列中肯定是互斥的,但对于使用者来说它可以避免使用大量的锁。只需要专注于单线程开发机制,控制好线程之间的变量与模块边界。

这种机制也不是完全没有问题,比如一些全局的数据操作就是一个很大的问题。比如,警报记录这种全局的消息,你可能给每个设备单独配置了一个线程,那么在查询处理设备时它产生的异常总是需要记录下来的。如果是多个设备那么就会存在,多个设备竞争互斥一个数据结构的问题。那如果我们将这个数据结构单独配置为一个线程,修改操作只能通过信号与槽的形式,是否就解决了这个问题呢?

没有解决,读写往往是同时存在的操作,一个数据结构往往都是要具备读和写的操作,所以这种形式你写也只能通过信号与槽进行查询,然后在将结果通过信号发出,其实不用将信号发出也是可以的,我们可以使用元调用一样将操作推入到该对象的线程去执行。

最简单的方式就是将这个列表使用互斥锁保护起来,这样就不需要通过线程读写的方式来进行了,这在大多数时候都是非常有效且简单的方式。但有些时候我们往往读的操作要数倍于写操作,这个时候需要提升读的并行能力,最简单且有效的方式是读写锁,该互斥提升允许读锁的并行能力,在大量读操作的情况下效率是要优于写操作的。当然笔者在大多数时候也是优先考虑互斥量及读写锁来解决全局数据结构的访问问题。

当然对于读操作写操作更多情况下,仍有一种方案。即们设计一个数据结构作为master独立运行于单独的一个模块内,然后在其它salve模块内放置一份这个数据结构的拷贝。这样在模块有数据进行读操作直接从模块的数据数据进行读操作即可,这样不需要加锁以为该资源为线程资源,提升了读的效率。但写操作则要更加复杂一些,写操作只能将操作发送到master模块中进行修改,修改完毕要发送修到所有salve模块告诉他们那个数据已经进行修改了,让他们的数据结构进行同步操作放置数据出现不一致的情况。

线程对象绑定

继承QObject对象之后,可以使用moveThread()将对象转移到另一个线程中去。由于父节点于子节点必须同属关联同一个线程,如果转移节点的线程拥有父节点,那么需要设置setParent(nullptr)脱离父节点才可以转移,并且其节点下面的所有子节点也会一并转移到此线程进行关联。在创建对象时,会将所在的线程进行关联为线程对象,例如在主线程创建的对象默认就关联主线程。

线程事件循环

QThread对象中,如果执行start()函数,它默认执行的run()函数实现是调用exec()进行事件循环阻塞。这个事件阻塞,会等待事件进行执行调用也就是信号与槽的基础。如果你实现的是自定义QThread如果要关联其它对象那么必须要执行exec(),否则它无法进行关联的槽函数调用。

元属性系统

Qt的元属性系统非常复杂,相关的有Q_PROPERTY设置的动态属性,还要Q_INVOKABLE所设置的元属性方法。其中Q_INVOKABLE所设置的方法能被QML直接调用,Q_PORPERTY属性也是一样的。这是一个基于反射的信息系统,会调用相关连绑定的一些函数。除了这两个常用的外,还有许多元属性的宏,它是由moc生成的部分代码。比如Q_ENUM。包括信号槽传参也是一样要对应的类型进行元注册之后才可以使用。
当然Qt的元系统没有那么简单,不过也是依赖moc生成文件,里面涉及到的东西复杂且多。笔者在这里也不乱说什么。



Qt插件系统

Qt基本插件

所谓的插件就是动态库的一种延申扩展,基于系统所支持的动态加载库及卸载库的基础实现的,Qt Plugin则是qt的一种规范,或者所支持的包装格式。

当然关于插件的设计思想其实大差不差的,必须要满足一些规则。比如说,必须是dll的形式,存在在某些指定目录下在程序运行的过程中进行加载。当然插件设计者本身仍需考虑二进制兼容的问题,我们无法保证dll与.exe使用的是同一个编译器各方面的规则都完全相同。尤其是在dll与exe进行交互时,参数的设计对象等。

Qt插件实现类需要继承,QObject。

相关宏

  • Q_DECLARE_INTERFACE 这个宏将给定的标识符(字符串字面值)关联到名为ClassName的接口类,标识符必须是唯一的。
  • Q_PLUGIN_METADATA 此宏用于声明元数据,该元数据是实例化此对象的插件的一部分。
  • Q_INTERFACES 这个宏告诉Qt类实现了哪些接口。这在实现插件时使用。
  • QT_MOC_EXPORT_PLUGIN moc编译器生成的代码文件,该宏创建了dll导出函数以及创建对象实例函数。

从源码中看出Q_DECLARE_INTERFACE宏,实际上是创建了对应的对象的元信息系统函数尤其是关于qobject_cast<IFace *>(QObject *object),qt安全转换的对象真相qt_metacast()函数实际上是由moc编译器所生成的函数。

#  define Q_DECLARE_INTERFACE(IFace, IId) \template <> inline const char *qobject_interface_iid<IFace *>() \{ return IId; } \template <> inline IFace *qobject_cast<IFace *>(QObject *object) \{ return reinterpret_cast<IFace *>((object ? object->qt_metacast(IId) : nullptr)); } \template <> inline IFace *qobject_cast<IFace *>(const QObject *object) \{ return reinterpret_cast<IFace *>((object ? const_cast<QObject *>(object)->qt_metacast(IId) : nullptr)); }
#endif // Q_MOC_RUN

Q_PLUGIN_METADATA,其实就是Qt自动生成对应的元信息的宏,那个iid数值是用于qobject_cast<>()转换时用到的FILE则是一个文件的内容是JSON格式,里面描述的信息可以被QPluginLoader的metaData()获取到。

#define Q_PLUGIN_METADATA(x) QT_ANNOTATE_CLASS(qt_plugin_metadata, x)

Q_INTERFACES,也是Qt自动生成对应的元信息宏,生成的信息用于qobject_cast<>()进行类型转换查询。

#define Q_INTERFACES(x) QT_ANNOTATE_CLASS(qt_interfaces, x)

QT_MOC_EXPORT_PLUGIN,生成导出dll函数以及创建对象实例方法,静态插件的方法会有点差别但位置一样的。

#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \{ \static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; \if (!_instance) {    \QT_PLUGIN_RESOURCE_INIT \_instance = new IMPLEMENTATION; \} \return _instance; \}#  define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME)      \Q_EXTERN_C Q_DECL_EXPORT \const char *qt_plugin_query_metadata() \{ return reinterpret_cast<const char *>(qt_pluginMetaData); } \Q_EXTERN_C Q_DECL_EXPORT QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance() \Q_PLUGIN_INSTANCE(PLUGINCLASS)

一个示例

接口文件定义接口

#ifndef ECHOINTERFACE_H
#define ECHOINTERFACE_H#include <QObject>
#include <QString>//! [0]
class EchoInterface
{
public:virtual ~EchoInterface() = default;virtual QString echo(const QString &message) = 0;
};QT_BEGIN_NAMESPACE#define EchoInterface_iid "org.qt-project.Qt.Examples.EchoInterface"Q_DECLARE_INTERFACE(EchoInterface, EchoInterface_iid)
QT_END_NAMESPACE//! [0]
#endif

EchoPlugin插件文件

#ifndef ECHOPLUGIN_H
#define ECHOPLUGIN_H#include <QObject>
#include <QtPlugin>
#include "echointerface.h"//! [0]
class EchoPlugin : public QObject, EchoInterface
{Q_OBJECTQ_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.EchoInterface" FILE "echoplugin.json")Q_INTERFACES(EchoInterface)public:QString echo(const QString &message) override;
};
//! [0]#endif

自动生成的moc代码文件,接口转换部分代码。ps:这可是qt安全转换对象的真相哦。

void *EchoPlugin::qt_metacast(const char *_clname)
{if (!_clname) return nullptr;if (!strcmp(_clname, qt_meta_stringdata_EchoPlugin.stringdata0))return static_cast<void*>(this);if (!strcmp(_clname, "EchoInterface"))return static_cast< EchoInterface*>(this);if (!strcmp(_clname, "org.qt-project.Qt.Examples.EchoInterface"))return static_cast< EchoInterface*>(this);return QObject::qt_metacast(_clname);
}

在这里的话,只能算作是低级插件。但其实高级插件也是一样的东西,只不过是继承它指定的类,而不是自己编写类。
具体的话,你可以查阅源码编译MySQL你会发现它在main文件则是继承的QSqlDriverPlugin驱动来实现。具体可以查阅文档,一般都是实现它的create方法即可。

编译MySQL数据库Qt驱动

目前版本的Qt并不自带Mysql驱动,Mysql驱动需要自行进行编译。好在源码中提供了,Qt插件驱动的项目D:\qt\5.15.2\Src\qtbase\src\plugins\sqldrivers\mysql
ps:如果没有安装源码,请先安装源码。
ps:请自行替换为自己的QT路径。

需要对此mysql的.pro文件进行修改,按照下面的方式修改,MySQL C库设置INCLUDEPATH 和LIBS。

TARGET = qsqlmysqlHEADERS += $$PWD/qsql_mysql_p.h
SOURCES += $$PWD/qsql_mysql.cpp $$PWD/main.cpp#QMAKE_USE += mysqlOTHER_FILES += mysql.jsonPLUGIN_CLASS_NAME = QMYSQLDriverPlugin
include(../qsqldriverbase.pri)#MySQL c库的头文件路径
INCLUDEPATH += "C:\Program Files\MySQL\MySQL Server 8.0\include"#mysql c库的.lib路径
LIBS += -L"C:\Program Files\MySQL\MySQL Server 8.0\lib" -l"libmysql"

点击编译,即可将MySQL驱动插件编译完成,然后打开vs命令行,进入编译出来的目录输入指令nmake install安装到qt环境中去,最后再把libmysql.dll拷贝到qt的bin目录,这样运行MySQL驱动时就不会缺少底层依赖了。

windows c sdk获取问题。Windows平台安装的mysql,在mysql server中默认包含了c api库。



Model-View(模型视图)

MVC模式是软件工程中常见的一种软件架构模式,该模式把软件系统(项目)分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。使用MVC模式有很多优势,例如:简化后期对项目的修改、扩展等维护操作;使项目的某一部分变得可以重复利用;使项目的结构更加直观。

有三个关键的抽象类作为扩展接口。

QAbstractItemDelegate               // 呈现项交互项,即渲染显示项,以及与用户交互时的QWidget部件
QAbstractItemModel                  // 数据源模型,用来提供显示数据层面的一个模型
QAbstractItemView                   // 视图交互展示并且与交互事件

代理(QAbstractItemView)

项代理,负责项的视觉呈现以及生产用户交互的编辑代理,最后将交互的数据设置到模型中,最后模型刷新view视图更新数据显示。在自定义项代理时分为两个部分。

  • 渲染部分
 // 绘制代理项内容,option包含了widget及rect等关键数据
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const = 0  // 推荐绘制项的大小,并不一定起作用,比如listview中,你设置height是可以生效的但width则是不会考虑,tableview则是height与widht都不予以考虑。
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const = 0  
  • 交互部分
// 创建一个用户交互编辑部件,然后返回
virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const// 设置交互编辑部件里面的数据
virtual void setEditorData(QWidget *editor, const QModelIndex &index) const// 更新设置小部件基于父项部件的大小及坐标
virtual void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const// 将编辑完成的数据写入到模型中去
virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const

当然,qt也提供了QStyledItemDelegate一个带有渲染的代理项进行展示,用户只需要负责创建交互小部件即可。这也是推荐的作法。
一个代码例子,我们在项的左边画个椭圆200个像素,然后右边显示数据内容。用到了自定义绘制,用了Qt样式部件的项绘制就是项的默认呈现绘制样式表也是可以生效的。
当然QStyledItemDelegate也差不多是这个逻辑实现的。除了显示还创建了编辑交互代理,需要注意创建交互代理的流程,在编辑完成时要发送的数据。

ps:原谅笔者只展示核心的关键代码部分,完整代码有些不方便编写。

void ItemDelegate::paint(QPainter *painter,const QStyleOptionViewItem &option, const QModelIndex &index) const {// 绘制背景if (option.state & QStyle::State_Selected) {// 被选中状态设置成红色painter->setBrush(Qt::red);} else if (option.state & QStyle::State_MouseOver) {// 鼠标盘旋设置为绿色painter->setBrush(Qt::green);} else {// 默认为黑色painter->setBrush(Qt::black);}// 画一个椭圆painter->drawEllipse(option.rect.x(), option.rect.y(), 200, option.rect.height());QStyleOptionViewItem opt = option;// 设置宽度及x轴,这里不需要减去椭圆部分的,因为默认它会减去x的坐标opt.rect.setWidth(opt.rect.width());opt.rect.setX(opt.rect.x() + 200);// 拿到要显示的数据opt.text = index.data().toString();// 使用qt样式进行绘制控制外形可以 样式表能生效,重点是要传入opt.widget参数qApp->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
}QSize ItemDelegate::sizeHint(const QStyleOptionViewItem &option,const QModelIndex &/*index*/) const {return QSize(option.rect.width(), 200);
}QWidget *ItemDelegate::createEditor(QWidget *parent,const QStyleOptionViewItem &option,const QModelIndex &index) const {// 交互小部件,调用顺序1qDebug() << __FUNCTION__;// 创建一个编辑小部件QLineEdit* line = new QLineEdit(parent);// 关联小部件编辑完成时,进行提交和关闭小部件,否则的话是不会调用到sheModelData函数的QObject::connect(line, &QLineEdit::editingFinished, this, &ItemDelegate::editFinish);return line;
}void ItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const {// 交互小部件,调用顺序3qDebug() << __FUNCTION__;// 设置小部件数据显示QLineEdit* line = qobject_cast<QLineEdit*>(editor);if (line)line->setText(index.data(Qt::DisplayRole).toString());
}void ItemDelegate::setModelData(QWidget *editor,QAbstractItemModel *model,const QModelIndex &index) const {// 交互小部件,调用顺序4qDebug() << __FUNCTION__;// 将小部件的数据更新到模型中QLineEdit* line = qobject_cast<QLineEdit*>(editor);if (line)model->setData(index, line->text(), Qt::EditRole);
}void ItemDelegate::updateEditorGeometry(QWidget *editor,const QStyleOptionViewItem &option,const QModelIndex &/*index*/) const {// 交互小部件,调用顺序2qDebug() << __FUNCTION__;// 设置小部件对于父部件的位置,位置信息在opiton中editor->setGeometry(option.rect);
}void ItemDelegate::editFinish() {// 完成编辑提交数据,关闭编辑器QLineEdit* line = qobject_cast<QLineEdit*>(sender());emit commitData(line);emit closeEditor(line);
}

模型(QAbstractItemModel)

QAbstractItemModel类定义了项目模型必须使用的标准接口,以便能够与模型/视图体系结构中的其他组件进行互操作。
其实大多数使用者并不了解这个,官方提供了QStandardItemModel一个标准模型,它毕竟是易于使用则为更多人所知。相对的在于某些情况下需要自定义实现model时,也有QAbstractListModel与QAbstractTableModel来进行更为便捷的继承实现。
在qt的示例中则提供了许多例子来帮助我们理解D:\qt\Examples\Qt-6.2.4\widgets\itemviews

QAbstractItemModel必须实现的接口
不要把模型和实际数据结构混为一谈,这里的模型应该只是指定了一些接口规则。比如data()获取数据时,是区分不同的角色获得不同的数据。

// 返回列数
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0
// 返回行数
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0
// 返回指定角色的值
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0
// 返回指定行列的索引,在list和table中,parent参数始终为QModelIndex(),无效索引
virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const = 0
// 返回指定索引的父索引
virtual QModelIndex parent(const QModelIndex &index) const = 0

刷新视图相关操作函数与信号
在更改模型数据时,通知视图刷新的规则与操作。

// 信号,数据发生改变时发送,通知视图刷新
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles = QList<int>())// 标题头数据改边时发送,通知视图刷新
void headerDataChanged(Qt::Orientation orientation, int first, int last)// 布局发生改变前发出
void layoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents = QList<QPersistentModelIndex>(), QAbstractItemModel::LayoutChangeHint hint = QAbstractItemModel::NoLayoutChangeHint
// 更改持久索引
void changePersistentIndex(const QModelIndex &from, const QModelIndex &to)
// 布局改变完成,刷新视图
void layoutChanged(const QList<QPersistentModelIndex> &parents = QList<QPersistentModelIndex>(), QAbstractItemModel::LayoutChangeHint hint = QAbstractItemModel::NoLayoutChangeHint)// 重置刷新视图
void beginResetModel()
void endResetModel()// 刷新视图指定的列
void beginInsertColumns(const QModelIndex &parent, int first, int last)
void endInsertColumns()// 刷新视图指定的行
void beginInsertRows(const QModelIndex &parent, int first, int last)
void endInsertRows()// 刷新视图移动指定的行
bool beginMoveRows(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destinationParent, int destinationChild)
void endMoveRows()// 刷新视图移动的列
bool beginMoveColumns(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destinationParent, int destinationChild)
void endMoveColumns()

笔者以qt示例中的D:\qt\Examples\Qt-6.2.4\widgets\itemviews\editabletreemodel的代码进行简单的理解一下。
你问,“笔者为何不写一个新例子?”
笔者,“那是因为笔者不会写啊,还有能别的理由吗?”

treeitem.h

#ifndef TREEITEM_H
#define TREEITEM_H#include <QVariant>
#include <QList>//! [0]
// 项结构是一个树型结构的套娃设计,不要因为套娃而迷糊,虽然笔者也曾在第一次接触链表套娃时迷糊了很久。
class TreeItem
{
public:explicit TreeItem(const QList<QVariant> &data, TreeItem *parent = nullptr);~TreeItem();// 获取指定位置的子项节点TreeItem *child(int number);// 获取子节点数量int childCount() const;// 获取列的数量int columnCount() const;// 返回当前项的指定列的值,注意这个可不是model的那个data()QVariant data(int column) const;// 指定位置,插入count子项,每个子项都有columns列(扩展子项)bool insertChildren(int position, int count, int columns);// 指定位置,插入指定数量的列(扩展数据列)bool insertColumns(int position, int columns);// 返回父项TreeItem *parent();// 移除指定位置的,count子项行bool removeChildren(int position, int count);// 移除指定位置的,columns列项bool removeColumns(int position, int columns);// 返回处于父项所在的位置int childNumber() const;// 设置指定列的数据bool setData(int column, const QVariant &value);
private:QList<TreeItem *> childItems;			// 这里是一个列表存储着子项列表QList<QVariant> itemData;				// 对应不同角色的存储不同角色的值列表TreeItem *parentItem;					// 父项
};
//! [0]#endif // TREEITEM_H

treeitem.cpp

#include "treeitem.h"//! [0]
TreeItem::TreeItem(const QList<QVariant> &data, TreeItem *parent): itemData(data), parentItem(parent)
{}
//! [0]//! [1]
TreeItem::~TreeItem()
{// 删除全部子项,这是个便捷宏qDeleteAll(childItems);
}
//! [1]//! [2]
TreeItem *TreeItem::child(int number)
{if (number < 0 || number >= childItems.size())return nullptr;return childItems.at(number);
}
//! [2]//! [3]
int TreeItem::childCount() const
{return childItems.count();
}
//! [3]//! [4]
int TreeItem::childNumber() const
{// 父项存在,根节点父项其实是nullif (parentItem)return parentItem->childItems.indexOf(const_cast<TreeItem*>(this));return 0;
}
//! [4]//! [5]
int TreeItem::columnCount() const
{return itemData.count();
}
//! [5]//! [6]
QVariant TreeItem::data(int column) const
{if (column < 0 || column >= itemData.size())return QVariant();return itemData.at(column);
}
//! [6]//! [7]
bool TreeItem::insertChildren(int position, int count, int columns)
{if (position < 0 || position > childItems.size())return false;// 添加子项for (int row = 0; row < count; ++row) {// 创建columns的数据列QList<QVariant> data(columns);// 创建一个子项TreeItem *item = new TreeItem(data, this);// 添加子项到指定位置childItems.insert(position, item);}return true;
}
//! [7]//! [8]
bool TreeItem::insertColumns(int position, int columns)
{if (position < 0 || position > itemData.size())return false;// 当前项扩展列for (int column = 0; column < columns; ++column)itemData.insert(position, QVariant());// 子项扩展列for (TreeItem *child : qAsConst(childItems))child->insertColumns(position, columns);return true;
}
//! [8]//! [9]
TreeItem *TreeItem::parent()
{return parentItem;
}
//! [9]//! [10]
bool TreeItem::removeChildren(int position, int count)
{if (position < 0 || position + count > childItems.size())return false;// 移除指定数量的子项for (int row = 0; row < count; ++row)delete childItems.takeAt(position);return true;
}
//! [10]bool TreeItem::removeColumns(int position, int columns)
{if (position < 0 || position + columns > itemData.size())return false;// 移除当前项的指定数据列for (int column = 0; column < columns; ++column)itemData.remove(position);// 子项移除指定的数据列for (TreeItem *child : qAsConst(childItems))child->removeColumns(position, columns);return true;
}//! [11]
bool TreeItem::setData(int column, const QVariant &value)
{if (column < 0 || column >= itemData.size())return false;// 设置指定列数据itemData[column] = value;return true;
}
//! [11]

treemodel.h

#ifndef TREEMODEL_H
#define TREEMODEL_H#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>// 声明对象,这样声明之后就不需要在这边的头文件进行文件包含,但这里的类型不可以实例只能是指针或引用的类型,这是一种非常常用的操作
class TreeItem;//! [0]
class TreeModel : public QAbstractItemModel
{Q_OBJECT
public:// 构造函数,data这里是序列化文本数据TreeModel(const QStringList &headers, const QString &data, QObject *parent = nullptr);~TreeModel();
//! [0] //! [1]// 根据role获取数据QVariant data(const QModelIndex &index, int role) const override;// 获取头数据QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;// 获取索引,他们是相对于parent获取的,所以为啥要有parent参数,在list和table中parent则为null索引QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;// 获取索引处的父项索引QModelIndex parent(const QModelIndex &index) const override;// 返回指定父项索引的子项行数,list和table则为nullint rowCount(const QModelIndex &parent = QModelIndex()) const override;// 返回列数,和行数不同,列数一般都是统一固定的,所以这里parent在这里实际上没有用到int columnCount(const QModelIndex &parent = QModelIndex()) const override;
//! [1]//! [2]// 获取索引项的标志,比如是否可以编辑之类的Qt::ItemFlags flags(const QModelIndex &index) const override;// 将数据设置到指定处索引bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;// 设置标题列数据bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole) override;// 插入指定位置的columns列bool insertColumns(int position, int columns, const QModelIndex &parent = QModelIndex()) override;// 移除指定位置columns列bool removeColumns(int position, int columns, const QModelIndex &parent = QModelIndex()) override;// 插入指定索引位置的row行bool insertRows(int position, int rows, const QModelIndex &parent = QModelIndex()) override;// 移除指定位置rowh行bool removeRows(int position, int rows, const QModelIndex &parent = QModelIndex()) override;private:// 设置模型初始化数据void setupModelData(const QStringList &lines, TreeItem *parent);// 通过索引返回TreeItem对象TreeItem *getItem(const QModelIndex &index) const;TreeItem *rootItem;			// 根项
};
//! [2]#endif // TREEMODEL_H

treemodel.cpp

#include "treemodel.h"
#include "treeitem.h"#include <QtWidgets>//! [0]
TreeModel::TreeModel(const QStringList &headers, const QString &data, QObject *parent): QAbstractItemModel(parent)
{// 设置列头标题QList<QVariant> rootData;for (const QString &header : headers)rootData << header;// 创建根节点rootItem = new TreeItem(rootData);// 解析data数据,进行配置初始化setupModelData(data.split('\n'), rootItem);
}
//! [0]//! [1]
TreeModel::~TreeModel()
{// 删除根节点delete rootItem;
}
//! [1]//! [2]
int TreeModel::columnCount(const QModelIndex &parent) const
{Q_UNUSED(parent);return rootItem->columnCount();
}
//! [2]QVariant TreeModel::data(const QModelIndex &index, int role) const
{// 索引无效,则返回无效的值if (!index.isValid())return QVariant();// 如果数据角色不是指定的显示角色也不是可编辑的角色,就返回无效的值。角色判断是根据自己的场景来设置的,在自定义model时要结合自身的实际情况if (role != Qt::DisplayRole && role != Qt::EditRole)return QVariant();// 通过索引获取到原本的节点TreeItem *item = getItem(index);// 节点返回数据return item->data(index.column());
}//! [3]
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{// 索引是无效的,则返回项的数据是不可用if (!index.isValid())return Qt::NoItemFlags;// 索引返回可编辑属性 加上一些默认属性,这里可以由视图判断绘制是否选中提供代理编辑return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}
//! [3]//! [4]
TreeItem *TreeModel::getItem(const QModelIndex &index) const
{// 索引有效,那就获取到索创建createIndex()索引时所传递的进来的void* 指针参数if (index.isValid()) {TreeItem *item = static_cast<TreeItem*>(index.internalPointer());if (item)return item;}return rootItem;
}
//! [4]QVariant TreeModel::headerData(int section, Qt::Orientation orientation,int role) const
{// 如果是水平,并且角色为显示时,那就返回根节点所保存的列表头数据if (orientation == Qt::Horizontal && role == Qt::DisplayRole)return rootItem->data(section);return QVariant();
}//! [5]
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{// 这里判断父索引时候的,要考虑到下面代码中的createIndex(parentItem->childNumber(), 0, parentItem),列参数传递是0,否则你会无法理解if (parent.isValid() && parent.column() != 0)return QModelIndex();
//! [5]//! [6]// 通过索引获取员的项TreeItem *parentItem = getItem(parent);if (!parentItem)return QModelIndex();// 获取指定行的项,之后创建索引返回TreeItem *childItem = parentItem->child(row);if (childItem)return createIndex(row, column, childItem);return QModelIndex();
}
//! [6]bool TreeModel::insertColumns(int position, int columns, const QModelIndex &parent)
{// 插入指定的列数,这里最后必须-1,它是前开后闭假设,position=0;columns=1,那么范围是[0,0] = 所以 [0, 0 + 1 - 1]// beginInsertColumns()必须调用,这是局部刷新beginInsertColumns(parent, position, position + columns - 1);const bool success = rootItem->insertColumns(position, columns);endInsertColumns();return success;
}bool TreeModel::insertRows(int position, int rows, const QModelIndex &parent)
{TreeItem *parentItem = getItem(parent);if (!parentItem)return false;        // 这里和上面一样,只是改为了行beginInsertRows(parent, position, position + rows - 1);const bool success = parentItem->insertChildren(position,rows,rootItem->columnCount());endInsertRows();return success;
}//! [7]
QModelIndex TreeModel::parent(const QModelIndex &index) const
{// 索引无效if (!index.isValid())return QModelIndex();// 这里是获取父项TreeItem *childItem = getItem(index);TreeItem *parentItem = childItem ? childItem->parent() : nullptr;// 如果父项等于根项,或者父项为null,那么都返回无效的项,无效的项在这里代表最顶层的项if (parentItem == rootItem || !parentItem)return QModelIndex();// 创建父项索引返回return createIndex(parentItem->childNumber(), 0, parentItem);
}
//! [7]bool TreeModel::removeColumns(int position, int columns, const QModelIndex &parent)
{// beginRemoveColumns()参考前面的注释来理解,这里是局部刷新移除的列beginRemoveColumns(parent, position, position + columns - 1);const bool success = rootItem->removeColumns(position, columns);endRemoveColumns();// 如果列数为0,那么就删除全部节点if (rootItem->columnCount() == 0)removeRows(0, rowCount());return success;
}bool TreeModel::removeRows(int position, int rows, const QModelIndex &parent)
{TreeItem *parentItem = getItem(parent);if (!parentItem)return false;// beginRemoveRows()参考前面的注释来理解,这里是局部刷新移除的行beginRemoveRows(parent, position, position + rows - 1);const bool success = parentItem->removeChildren(position, rows);endRemoveRows();return success;
}//! [8]
int TreeModel::rowCount(const QModelIndex &parent) const
{// 父项有效,并且父项的列数大于0,那么返回0行。因为这个父项有问题,因为获取父项的函数,返回的索引列数都为0if (parent.isValid() && parent.column() > 0)return 0;const TreeItem *parentItem = getItem(parent);// 返回子项数return parentItem ? parentItem->childCount() : 0;
}
//! [8]bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{// 设置数据时,角色要为可编辑角色if (role != Qt::EditRole)return false;TreeItem *item = getItem(index);// 设置数据bool result = item->setData(index.column(), value);// 设置数据完成后要发出数据改变的信号来刷新视图if (result)emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});return result;
}bool TreeModel::setHeaderData(int section, Qt::Orientation orientation,const QVariant &value, int role)
{if (role != Qt::EditRole || orientation != Qt::Horizontal)return false;const bool result = rootItem->setData(section, value);// 列头数据被修改,发出信号通知视图if (result)emit headerDataChanged(orientation, section, section);return result;
}void TreeModel::setupModelData(const QStringList &lines, TreeItem *parent)
{// 这个函数不用在意,这是示例初始化模型数据,此时模型还未设置到视图中,所以不用发出视图刷新相关的指示信号操作QList<TreeItem *> parents;QList<int> indentations;parents << parent;indentations << 0;int number = 0;while (number < lines.count()) {int position = 0;while (position < lines[number].length()) {if (lines[number].at(position) != ' ')break;++position;}const QString lineData = lines[number].mid(position).trimmed();if (!lineData.isEmpty()) {// Read the column data from the rest of the line.const QStringList columnStrings =lineData.split(QLatin1Char('\t'), Qt::SkipEmptyParts);QList<QVariant> columnData;columnData.reserve(columnStrings.size());for (const QString &columnString : columnStrings)columnData << columnString;if (position > indentations.last()) {// The last child of the current parent is now the new parent// unless the current parent has no children.if (parents.last()->childCount() > 0) {parents << parents.last()->child(parents.last()->childCount()-1);indentations << position;}} else {while (position < indentations.last() && parents.count() > 0) {parents.pop_back();indentations.pop_back();}}// Append a new item to the current parent's list of children.TreeItem *parent = parents.last();parent->insertChildren(parent->childCount(), 1, rootItem->columnCount());for (int column = 0; column < columnData.size(); ++column)parent->child(parent->childCount() - 1)->setData(column, columnData[column]);}++number;}
}


Qt日志系统

我们经常用的qDebug()打印日志调试信息函数,就是日志系统里面的。在Qt的日志系统中,消息是分为日志器类别,然后每个日志器类别在区分日志等级。我们常用的qDebug()打印消息则属于default日志类别。
系统许多模块他们都有单独的日志类别,虽然他们平常不打印出来。

相关宏、函数

// 安装日志输出最终处理函数,qInstallMessageHandler(nullptr)则设置回默认的处理函数。
QtMessageHandler qInstallMessageHandler(QtMessageHandler handler)// 设置过滤的规则,这是默认规则才生效的,如果安装了自定义的过滤函数,则不会生效
void QLoggingCategory::setFilterRules(const QString &rules)// 设置安装过滤处理函数
QLoggingCategory::CategoryFilter installFilter(QLoggingCategory::CategoryFilter filter)// 设置消息格式
void qSetMessagePattern(const QString &pattern)// 默认的日志器
QLoggingCategory *defaultCategory()// 声明一个外部的日志器
Q_DECLARE_LOGGING_CATEGORY(name)// 创建日志器,这里只是用宏来包装了,所以看起来很黑科技
Q_LOGGING_CATEGORY(name, string, msgType)
Q_LOGGING_CATEGORY(name, string)// 日志器输出消息,和qDebug()其实差不多的,qDebug() 相当于 qCDebug(QLoggingCategory::defaultCategory())
qCCritical(category, const char *message, ...)
qCCritical(category)
qCDebug(category, const char *message, ...)
qCDebug(category)
qCInfo(category, const char *message, ...)
qCInfo(category)
qCWarning(category, const char *message, ...)
qCWarning(category)

安装自定义的最终输入处理

QtMessageHandler qInstallMessageHandler(QtMessageHandler handler)

官方示例,其实不用过多解释,基本上有点编程基础的人一看就懂了,如果没有特别的需求一般我们不会更改它。

 #include <qapplication.h>#include <stdio.h>#include <stdlib.h>void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg){QByteArray localMsg = msg.toLocal8Bit();const char *file = context.file ? context.file : "";const char *function = context.function ? context.function : "";switch (type) {case QtDebugMsg:fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);break;case QtInfoMsg:fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);break;case QtWarningMsg:fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);break;case QtCriticalMsg:fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);break;case QtFatalMsg:fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);break;}}int main(int argc, char **argv){qInstallMessageHandler(myMessageOutput);QApplication app(argc, argv);...return app.exec();}

设置过滤的规则,这是默认规则才生效的,如果安装了自定义的过滤函数,则不会生效

void QLoggingCategory::setFilterRules(const QString &rules)// 语法规则 <category>[.<type>] = true|false// 例子,driver.usb是日志器的名称
QLoggingCategory::setFilterRules(QStringLiteral("driver.usb.debug=true"));// 设置多个日志器的控制
QLoggingCategory::setFilterRules("*.debug=false\n""driver.usb.debug=true");// 其它的设置方式,请去查阅官方文档。								

设置安装过滤处理函数

// 如果你不想配置日志规则想按照自己的方式来控制日志器的过滤,可以直接安装自己的过滤处理函数,笔者建议请谨慎操作。
QLoggingCategory::CategoryFilter installFilter(QLoggingCategory::CategoryFilter filter)// 它默认的过滤处理函数
void QLoggingRegistry::defaultCategoryFilter(QLoggingCategory *cat)
{const QLoggingRegistry *reg = QLoggingRegistry::instance();Q_ASSERT(reg->categories.contains(cat));QtMsgType enableForLevel = reg->categories.value(cat);// NB: note that the numeric values of the Qt*Msg constants are//     not in severity order.bool debug = (enableForLevel == QtDebugMsg);bool info = debug || (enableForLevel == QtInfoMsg);bool warning = info || (enableForLevel == QtWarningMsg);bool critical = warning || (enableForLevel == QtCriticalMsg);// hard-wired implementation of//   qt.*.debug=false//   qt.debug=falseif (const char *categoryName = cat->categoryName()) {// == "qt" or startsWith("qt.")if (strcmp(categoryName, "qt") == 0 || strncmp(categoryName, "qt.", 3) == 0)debug = false;}const auto categoryName = QLatin1String(cat->categoryName());for (const auto &ruleSet : reg->ruleSets) {for (const auto &rule : ruleSet) {int filterpass = rule.pass(categoryName, QtDebugMsg);if (filterpass != 0)debug = (filterpass > 0);filterpass = rule.pass(categoryName, QtInfoMsg);if (filterpass != 0)info = (filterpass > 0);filterpass = rule.pass(categoryName, QtWarningMsg);if (filterpass != 0)warning = (filterpass > 0);filterpass = rule.pass(categoryName, QtCriticalMsg);if (filterpass != 0)critical = (filterpass > 0);}}cat->setEnabled(QtDebugMsg, debug);cat->setEnabled(QtInfoMsg, info);cat->setEnabled(QtWarningMsg, warning);cat->setEnabled(QtCriticalMsg, critical);
}

设置消息打印格式

void qSetMessagePattern(const QString &pattern)

请查看这个格式,比如你想打印,类型、日志器、消息、行号。

 qSetMessagePattern("%{type} %{category} %{message} %{line}");

在这里插入图片描述

声明一个外部的日志器,实际上代码是这样的

Q_DECLARE_LOGGING_CATEGORY(name)// 这个函数是外部的
#define Q_DECLARE_LOGGING_CATEGORY(name) \extern const QLoggingCategory &name();

创建日志器

Q_LOGGING_CATEGORY(name, string, msgType)
Q_LOGGING_CATEGORY(name, string)// 这里实际上是一个函数name就是函数的名称,后面的都是QLoggingCategory对象的参数
#define Q_LOGGING_CATEGORY(name, ...) \const QLoggingCategory &name() \{ \static const QLoggingCategory category(__VA_ARGS__); \return category; \}// 没有规定函数名称和日志器一定要相同的,所以你可以这样。
Q_LOGGING_CATEGORY(rainy, "Jie")// 使用的时候是这样的,它的日志器名称为Jie
qCDebug(rainy()) << "hello rainy";

比如你一般来说只需要自定义一个属于自己的日志类别,那么下面的代码可以给你带来参考。

// 在使用日志类别的地方声明这个函数,比如这里就叫rainy
Q_DECLARE_LOGGING_CATEGORY(rainy)// 任意cpp文件中去实现这个函数,rainy是函数名称,后面是构造日志器的参数
Q_LOGGING_CATEGORY(rainy, "Jie")// 获取日志器,你可以通过日志器来配置日志过滤或者检查判断开启了那些过滤
rainy()// 打印日志的话,可以这样样子。
qCDebug(rainy()) << "hello rainy";

未完待续....

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【JasperReports笔记06】JasperReport报表开发之常见的组件元素(Table、Subreport、Barcode等)

这篇文章&#xff0c;主要介绍JasperReport报表开发之常见的组件元素&#xff08;Table、Subreport、Barcode等&#xff09;。 目录 一、基础组件元素 1.1、StaticText 1.2、TextField 1.3、Image 1.4、Break分页 1.5、Rectangle矩形区域 1.6、Ellipse椭圆区域 1.7、Li…

Mybatis中 list.size() = 1 但显示 All elements are null

一、Bug展示 二、原因分析 2.1.情形一&#xff1a;Mybatis的XML中返回类型映射错误 <select id"selectByDesc" parameterType"com.task.bean.OrderInfo"resultType"com.task.bean.OrderInfo">select MER_ID,SETTLE_DATE,ICE_NAME,ORDER_S…

探索未来世界,解密区块链奥秘!

你是否曾好奇&#xff0c;区块链是如何影响着我们的生活与未来&#xff1f;想要轻松了解这个引领着技术革命的概念吗&#xff1f;那么这本令人着迷的新书《区块链导论》绝对值得你拥有&#xff01; 内容丰富多彩&#xff0c;让你轻松掌握&#xff1a; **1章&#xff1a;区块链…

文件传输协议

文章目录 一、FTP1. 定义2. 端口3. 数据传输方式主动方式被动方式 二、TFTP三、常用命令 首先可以看下思维导图&#xff0c;以便更好的理解接下来的内容。 一、FTP 1. 定义 文件传输协议&#xff08;FTP&#xff09;是一种用于在客户端和服务器之间进行文件传输的标准网络协…

使用yarn build 打包vue项目时静态文件或图片未打包成功

解决Vue项目使用yarn build打包时静态文件或图片未打包成功的问题 1. 检查vue.config.js文件 首先&#xff0c;我们需要检查项目根目录下的vue.config.js文件&#xff0c;该文件用于配置Vue项目的打包和构建选项。在这个文件中&#xff0c;我们需要确认是否正确地配置了打包输…

openCV实战-系列教程13:文档扫描OCR识别下(图像轮廓/模版匹配)项目实战、源码解读

&#x1f9e1;&#x1f49b;&#x1f49a;&#x1f499;&#x1f49c;OpenCV实战系列总目录 有任何问题欢迎在下面留言 本篇文章的代码运行界面均在Pycharm中进行 本篇文章配套的代码资源已经上传 上篇内容&#xff1a; openCV实战-系列教程11&#xff1a;文档扫描OCR识别上&am…

【Linux】0基础从获取docker,一步一步到部署PaddleSpeech

一、利用VMware安装ubuntu 1.安装VMware 具体操作详细安装VMware的方式 另外附部分VMware密匙 4A4RR-813DK-M81A9-4U35H-06KND NZ4RR-FTK5H-H81C1-Q30QH-1V2LA JU090-6039P-08409-8J0QH-2YR7F 4Y09U-AJK97-089Z0-A3054-83KLA 4C21U-2KK9Q-M8130-4V2QH-CF810 MC60H-DWH…

jvm与锁

今天是《面霸的自我修养》的第二弹&#xff0c;内容是Java并发编程中关于Java内存模型&#xff08;Java Memory Model&#xff09;和锁的基础理论相关的问题。这两块内容的八股文倒是不多&#xff0c;但是难度较大&#xff0c;接下来我们就一起一探究竟吧。 数据来源&#xff…

HCIP-HCS华为私有云的使用

1、概述 华为公有云&#xff08;HC&#xff09;、华为私有云&#xff08;HCS&#xff09;华为混合云&#xff08;HCSO&#xff09;。6.3 之前叫FusionSphere OpenStack&#xff0c;6.3.1 版本开始叫FusionCloud&#xff0c;6.5.1 版本开始叫Huawei Cloud Stack (HCS)华为私有云…

算法通关村第9关【黄金】| 两道有挑战的问题

1. 将有序数组转换为二叉搜索树 思路&#xff1a;二分法&#xff0c;这个算法保证了每次选择的中间元素都能保持左右子树的高度差不超过 1&#xff0c;从而构建一个高度平衡的二叉搜索树。这个过程类似于分治法&#xff0c;通过递归不断将大问题分解成小问题并解决。 找到数组…

链路聚合原理

文章目录 一、定义二、功能三、负载分担四、分类五、常用命令 首先可以看下思维导图&#xff0c;以便更好的理解接下来的内容。 一、定义 在网络中&#xff0c;端口聚合是一种将连接到同一台交换机的多个物理端口捆绑在一起&#xff0c;形成一个逻辑端口的技术。通过端口聚合&…

Redis 主从复制和哨兵模式

一、概念 主从复制&#xff0c;是指将一台 Redis 服务器的数据&#xff0c;复制到其他的 Redis 服务器。前者称为主节点&#xff08;master/leader&#xff09;&#xff0c;后者称为从节点&#xff08;slave/follower&#xff09;。数据的复制是单向的&#xff0c;只能由主节点…

学习JAVA打卡第四十五天

StringBuffer类 StringBuffer对象 String对象的字符序列是不可修改的&#xff0c;也就是说&#xff0c;String对象的字符序列的字符不能被修改、删除&#xff0c;即String对象的实体是不可以再发生变化&#xff0c;例如&#xff1a;对于 StringBuffer有三个构造方法&#xff…

JPA在不写sql的情况下实现模糊查询

本文已收录于专栏 《Java》 目录 背景介绍概念说明单字段模糊匹配&#xff1a;多字段模糊匹配&#xff1a; 实现过程代码实现1.写一个实体类去实现Specification接口&#xff0c;重写toPredicate方法2.定义一个接口去继承JpaRepository接口&#xff0c;并指定返回的类型和参数类…

家宽用户家庭网的主要质量问题是什么?原因有哪些

1 引言 截至2020年底&#xff0c;我国家庭宽带&#xff08;以下简称“家宽”&#xff09;普及率已达到96%。经过一年多的发展&#xff0c;当前&#xff0c;家庭宽带的市场空间已经饱和。运营商在家宽市场的竞争也随之从新增用户数的竞争转移到家宽品质的竞争。 早期运营商的家…

多张图片转为pdf怎么弄?

多张图片转为pdf怎么弄&#xff1f;在网络传输过程中&#xff0c;为了避免图片格式文件出现差错&#xff0c;并确保图片的清晰度和色彩不因不同设备而有所改变&#xff0c;常见的做法是将图片转换为PDF格式。然而&#xff0c;当涉及到多张图片时&#xff0c;逐一转换将会变得相…

IP基本原理(上)

文章目录 一、IP的定义二、IP的作用1.标识节点和链路2.寻址和转发3.适应各种数据链路 三、IP头部封装格式四、MTU五、IP地址1.定义2.格式2.1 点分十进制和二进制关系与转换2.2 由网络位主机位组成2.3 网络位长度决定网段 3.分类3.1 A类3.2 B类3.3 C类3.4 D类3.5 E类 4.特殊地址…

职场中的团队建设:超越任务,铸就默契

团队建设在职场中的重要性日益凸显。无论是初创公司还是大型企业&#xff0c;都需要一个高效、和谐且有创新能力的团队来推动业务发展。本文将深入探讨团队建设的活动和策略&#xff0c;帮助您构建一个卓越的团队。 1. 团队建设的重要性 提高团队凝聚力 团队凝聚力不仅仅是团…

手写数字识别之网络结构

目录 手写数字识别之网络结构 数据处理 经典的全连接神经网络 卷积神经网络 手写数字识别之网络结构 无论是牛顿第二定律任务&#xff0c;还是房价预测任务&#xff0c;输入特征和输出预测值之间的关系均可以使用“直线”刻画&#xff08;使用线性方程来表达&#xff09…

SSM - Springboot - MyBatis-Plus 全栈体系(三)

第二章 SpringFramework 一、技术体系架构 1. 总体技术体系 1.1 单一架构 一个项目&#xff0c;一个工程&#xff0c;导出为一个war包&#xff0c;在一个Tomcat上运行。也叫all in one。 单一架构&#xff0c;项目主要应用技术框架为&#xff1a;Spring , SpringMVC , Myba…