Qt 插件机制使用及原理

目录

1.引言

2.插件原理

3.插件实现

3.1.定义一个接口集(只有纯虚函数的类)

3.2.实现接口

4.插件的加载

4.1.静态插件

4.1.1.静态插件实现方式

4.1.2.静态插件加载的过程

4.1.3.示例

4.2.动态插件

4.2.1.动态插件的加载过程

5.定位插件

6.插件开发的优势

7.总结


1.引言

        在设计大型软件时,插件式开发都会被考虑到。无论在普通的桌面软件还是大型的游戏软件,都可以看到插件的身影。例如著名的Qt Creator 系统开发软件都用插件架构。插件系统最大的功能是在一定程度内提高了软件的灵活度和可扩展性。一个设计精良插件系统甚至可以在宿主软件不退出的情况下加入新的插件,实现热插拔的功能。这一点在我的博客C++架构设计中也有涉及:

C++架构设计-CSDN博客

那么,Qt的插件系统是怎么实现的呢?Qt 提供了两种API用于创建插件:一种是高阶 API,用于扩展 Qt 本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;一种是低阶 API,用于扩展 Qt 应用程序。本文主要是通过低阶 API 来创建 Qt 插件,并通过静态、动态两种方式来调用插件。

2.插件原理

        在C++ 中,插件一般以动态库的显示加载方式提供。利用C++多态的原理,在程序中首先声明一个插件的interface。该interface 只需要实现构造和析构函数,所有用到的功能函数都先定义为虚函数,然后在插件中实现该interface 的具体接口。那么当插件创建的时候,把插件中的子类对象赋值到宿主程序中的基类对象interface。即可实现不同的插件实现不同的功能。

3.插件实现

3.1.定义一个接口集(只有纯虚函数的类)

interfaces.h

#ifndef INTERFACES_H
#define INTERFACES_H#include <QtPlugin>QT_BEGIN_NAMESPACE
class QImage;
class QPainter;
class QWidget;
class QPainterPath;
class QPoint;
class QRect;
class QString;
class QStringList;
QT_END_NAMESPACE//! [0]
class BrushInterface
{
public:virtual ~BrushInterface() {}virtual QStringList brushes() const = 0;virtual QRect mousePress(const QString &brush, QPainter &painter,const QPoint &pos) = 0;virtual QRect mouseMove(const QString &brush, QPainter &painter,const QPoint &oldPos, const QPoint &newPos) = 0;virtual QRect mouseRelease(const QString &brush, QPainter &painter,const QPoint &pos) = 0;
};
//! [0]//! [1]
class ShapeInterface
{
public:virtual ~ShapeInterface() {}virtual QStringList shapes() const = 0;virtual QPainterPath generateShape(const QString &shape,QWidget *parent) = 0;
};
//! [1]//! [2]
class FilterInterface
{
public:virtual ~FilterInterface() {}virtual QStringList filters() const = 0;virtual QImage filterImage(const QString &filter, const QImage &image,QWidget *parent) = 0;
};
//! [2]QT_BEGIN_NAMESPACE
//! [3] //! [4]
#define BrushInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface/1.0"Q_DECLARE_INTERFACE(BrushInterface, BrushInterface_iid)
//! [3]#define ShapeInterface_iid  "org.qt-project.Qt.Examples.PlugAndPaint.ShapeInterface/1.0"Q_DECLARE_INTERFACE(ShapeInterface, ShapeInterface_iid)
//! [5]
#define FilterInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.FilterInterface/1.0"Q_DECLARE_INTERFACE(FilterInterface, FilterInterface_iid)
//! [4] //! [5]
QT_END_NAMESPACE#endif

        创建一个BrushInterface,ShapeInterface,FilterInterface基类,直接可以把它定义为纯虚接口。和普通的多态不同的是,在interfece 中我们需要定义 iid。iid 可以理解为插件的一个标识或者ID。在加载插件的过程中会对IID 进行判断,如果插件中的IID 和 interface 中的IID 不匹配,那么该插件就不会被加载。Q_DECLARE_INTERFACE把IID 和类名进行绑定,这也是必须的。作用是导出一些可以通过 定义了接口ID查找函数和几个QObject到接口的转换函数。

        宏Q_DECLARE_INTERFACE导入名为PluginName的插件,它与Q_PLUGIN_METADATA()为插件声明元数据的类的名称相对应。

3.2.实现接口

basictoolsplugin.h

#ifndef BASICTOOLSPLUGIN_H
#define BASICTOOLSPLUGIN_H//! [0]
#include <interfaces.h>#include <QRect>
#include <QObject>
#include <QtPlugin>
#include <QStringList>
#include <QPainterPath>
#include <QImage>//! [1]
class BasicToolsPlugin : public QObject,public BrushInterface,public ShapeInterface,public FilterInterface
{Q_OBJECT
//! [4]Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface" FILE "basictools.json")
//! [4]Q_INTERFACES(BrushInterface ShapeInterface FilterInterface)
//! [0]//! [2]
public:
//! [1]// BrushInterfaceQStringList brushes() const override;QRect mousePress(const QString &brush, QPainter &painter,const QPoint &pos) override;QRect mouseMove(const QString &brush, QPainter &painter,const QPoint &oldPos, const QPoint &newPos) override;QRect mouseRelease(const QString &brush, QPainter &painter,const QPoint &pos) override;// ShapeInterfaceQStringList shapes() const override;QPainterPath generateShape(const QString &shape, QWidget *parent) override;// FilterInterfaceQStringList filters() const override;QImage filterImage(const QString &filter, const QImage &image,QWidget *parent) override;
//! [3]
};
//! [2] //! [3]#endif

        实现interface 也是和多态一样,只是多了两个宏 Q_PLUGIN_METADATAQ_INTERFACES。前面讲到了,因为宿主程序是不知道插件的名字的,所以如何让插件的子类在宿主程序中实例化是插件系统的关键,在Qt 中 该功能由Q_PLUGIN_METADATA 完成。

        跟踪源码发现Q_PLUGIN_METADATA的定义如下:

#define Q_PLUGIN_METADATA(x) QT_ANNOTATE_CLASS(qt_plugin_metadata, x)# ifndef Q_COMPILER_VARIADIC_MACROS
#  define QT_ANNOTATE_CLASS(type, x)
# else
#  define QT_ANNOTATE_CLASS(type, ...)
# endif
#endif

        发现根本没干什么,那么该宏应该在编译的时候由元对象系统Moc解析。在moc 解析的时候将 Q_PLUGIN_METADATA 转化为QT_MOC_EXPORT_PLUGIN 宏并插入到代码中。我们在编译后生成的" moc_basictoolsplugin.cpp" 文件中发现了此宏:

        翻看Qt的源码(5.12.12版本)找到了该宏返回了插件的元对象数据以及 qt_plugin_instance 函数的实现,该函数的函数体Q_PLUGIN_INSTANCE 定义,函数中的 _instance = new IMPLEMENTATION; 也就是插件的对象,然后通过QObject 指针 _instance 的形式返回(代码如下)。上层通过该方式(见插件的加载过程),获取插件的子类对象赋值给基类,实现多态的调用。

        下面是插件的具体实现代码:basictoolsplugin.cpp

#include "basictoolsplugin.h"#include <QtMath>
#include <QtWidgets>#include <stdlib.h>//! [0]
QStringList BasicToolsPlugin::brushes() const
{return {tr("Pencil"), tr("Air Brush"), tr("Random Letters")};
}
//! [0]//! [1]
QRect BasicToolsPlugin::mousePress(const QString &brush, QPainter &painter,const QPoint &pos)
{return mouseMove(brush, painter, pos, pos);
}
//! [1]//! [2]
QRect BasicToolsPlugin::mouseMove(const QString &brush, QPainter &painter,const QPoint &oldPos, const QPoint &newPos)
{painter.save();int rad = painter.pen().width() / 2;QRect boundingRect = QRect(oldPos, newPos).normalized().adjusted(-rad, -rad, +rad, +rad);QColor color = painter.pen().color();int thickness = painter.pen().width();QColor transparentColor(color.red(), color.green(), color.blue(), 0);
//! [2] //! [3]if (brush == tr("Pencil")) {painter.drawLine(oldPos, newPos);} else if (brush == tr("Air Brush")) {int numSteps = 2 + (newPos - oldPos).manhattanLength() / 2;painter.setBrush(QBrush(color, Qt::Dense6Pattern));painter.setPen(Qt::NoPen);for (int i = 0; i < numSteps; ++i) {int x = oldPos.x() + i * (newPos.x() - oldPos.x()) / (numSteps - 1);int y = oldPos.y() + i * (newPos.y() - oldPos.y()) / (numSteps - 1);painter.drawEllipse(x - (thickness / 2), y - (thickness / 2),thickness, thickness);}} else if (brush == tr("Random Letters")) {QChar ch(QRandomGenerator::global()->bounded('A', 'Z' + 1));QFont biggerFont = painter.font();biggerFont.setBold(true);biggerFont.setPointSize(biggerFont.pointSize() + thickness);painter.setFont(biggerFont);painter.drawText(newPos, QString(ch));QFontMetrics metrics(painter.font());boundingRect = metrics.boundingRect(ch);boundingRect.translate(newPos);boundingRect.adjust(-10, -10, +10, +10);}painter.restore();return boundingRect;
}
//! [3]//! [4]
QRect BasicToolsPlugin::mouseRelease(const QString & /* brush */,QPainter & /* painter */,const QPoint & /* pos */)
{return QRect(0, 0, 0, 0);
}
//! [4]//! [5]
QStringList BasicToolsPlugin::shapes() const
{return {tr("Circle"), tr("Star"), tr("Text...")};
}
//! [5]//! [6]
QPainterPath BasicToolsPlugin::generateShape(const QString &shape,QWidget *parent)
{QPainterPath path;if (shape == tr("Circle")) {path.addEllipse(0, 0, 50, 50);} else if (shape == tr("Star")) {path.moveTo(90, 50);for (int i = 1; i < 5; ++i) {path.lineTo(50 + 40 * std::cos(0.8 * i * M_PI),50 + 40 * std::sin(0.8 * i * M_PI));}path.closeSubpath();} else if (shape == tr("Text...")) {QString text = QInputDialog::getText(parent, tr("Text Shape"),tr("Enter text:"),QLineEdit::Normal, tr("Qt"));if (!text.isEmpty()) {QFont timesFont("Times", 50);timesFont.setStyleStrategy(QFont::ForceOutline);path.addText(0, 0, timesFont, text);}}return path;
}
//! [6]//! [7]
QStringList BasicToolsPlugin::filters() const
{return {tr("Invert Pixels"), tr("Swap RGB"), tr("Grayscale")};
}
//! [7]//! [8]
QImage BasicToolsPlugin::filterImage(const QString &filter, const QImage &image,QWidget * /* parent */)
{QImage result = image.convertToFormat(QImage::Format_RGB32);if (filter == tr("Invert Pixels")) {result.invertPixels();} else if (filter == tr("Swap RGB")) {result = result.rgbSwapped();} else if (filter == tr("Grayscale")) {for (int y = 0; y < result.height(); ++y) {for (int x = 0; x < result.width(); ++x) {QRgb pixel = result.pixel(x, y);int gray = qGray(pixel);int alpha = qAlpha(pixel);result.setPixel(x, y, qRgba(gray, gray, gray, alpha));}}}return result;
}
//! [8]

到此为止,一个插件的实现就已经完成了。下面来看一看插件加载的过程。

4.插件的加载

4.1.静态插件

4.1.1.静态插件实现方式

        静态插件可以把下面这个宏插入到插件应用程序的源代码中:

  Q_IMPORT_PLUGIN(qjpeg)

        或在pro文件中配置项目为静态插件:

  TEMPLATE      = libCONFIG       += plugin static

        在构建应用程序时,静态插件也必须包含在链接器中。对于Qt预定义的插件,您可以使用QTPLUGIN来添加所需的插件到您的构建中。例如:

  TEMPLATE      = appQTPLUGIN     += qjpeg qgif    # image formats

        静态插件的加载比较简单,直接使用 QPluginLoader::staticInstances() 加载,代码如下:

void PluginDialog::findPlugins(const QString &path,const QStringList &fileNames)
{label->setText(tr("Plug & Paint found the following plugins\n""(looked in %1):").arg(QDir::toNativeSeparators(path)));const QDir dir(path);const auto staticInstances = QPluginLoader::staticInstances();for (QObject *plugin : staticInstances)populateTreeWidget(plugin, tr("%1 (Static Plugin)").arg(plugin->metaObject()->className()));for (const QString &fileName : fileNames) {QPluginLoader loader(dir.absoluteFilePath(fileName));QObject *plugin = loader.instance();if (plugin)populateTreeWidget(plugin, fileName);}
}

4.1.2.静态插件加载的过程

QPluginLoader::staticInstances()为什么就可以加载所有插件呢?翻看QPluginLoader的源码:

/*!Returns a list of static plugin instances (root components) heldby the plugin loader.\sa staticPlugins()
*/
QObjectList QPluginLoader::staticInstances()
{QObjectList instances;const StaticPluginList *plugins = staticPluginList();if (plugins) {const int numPlugins = plugins->size();instances.reserve(numPlugins);for (int i = 0; i < numPlugins; ++i)instances += plugins->at(i).instance();}return instances;
}/*!Returns a list of QStaticPlugins held by the pluginloader. The function is similar to \l staticInstances()with the addition that a QStaticPlugin also containsmeta data information.\sa staticInstances()
*/
QVector<QStaticPlugin> QPluginLoader::staticPlugins()
{StaticPluginList *plugins = staticPluginList();if (plugins)return *plugins;return QVector<QStaticPlugin>();
}typedef QVector<QStaticPlugin> StaticPluginList;
Q_GLOBAL_STATIC(StaticPluginList, staticPluginList)

从以上代码可以看出:

1)首先定义了全局的向量集合StaticPluginList来存储所有的插件信息

2)然后,插件静态加载的过程中,需要要把自己的插件信息注册上来,这个也是最常见的设计手法,于是Qt库提供了注册的函数:

/*!\relates QPluginLoader\since 5.0Registers the \a plugin specified with the plugin loader, and is usedby Q_IMPORT_PLUGIN().
*/
void Q_CORE_EXPORT qRegisterStaticPluginFunction(QStaticPlugin plugin)
{staticPluginList()->append(plugin);
}

3)看到注册函数,估计你要恍然大悟了吧,为什么在4.1.1节中要使用Q_IMPORT_PLUGIN来声明静态插件,肯定是要调用qRegisterStaticPluginFunction注册自己:

#define Q_IMPORT_PLUGIN(PLUGIN) \extern const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGIN(); \class Static##PLUGIN##PluginInstance{ \public: \Static##PLUGIN##PluginInstance() { \qRegisterStaticPluginFunction(qt_static_plugin_##PLUGIN()); \} \}; \static Static##PLUGIN##PluginInstance static##PLUGIN##Instance;

果然,定义全局变量static##PLUGIN##Instance,在构造函数中调用了注册函数,注册的对象正是宏QT_MOC_EXPORT_PLUGIN定义的QStaticPlugin,下面看一下QT_MOC_EXPORT_PLUGIN

4)QT_MOC_EXPORT_PLUGIN定义

#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) \static QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance_##PLUGINCLASSNAME() \Q_PLUGIN_INSTANCE(PLUGINCLASS) \static const char *qt_plugin_query_metadata_##PLUGINCLASSNAME() { return reinterpret_cast<const char *>(qt_pluginMetaData); } \const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGINCLASSNAME() { \QT_PREPEND_NAMESPACE(QStaticPlugin) plugin = { qt_plugin_instance_##PLUGINCLASSNAME, qt_plugin_query_metadata_##PLUGINCLASSNAME}; \return plugin; \}

定义了生成插件对象的全局函数和QtPluginMetaDataFunction,并返回了QStaticPlugin:

struct Q_CORE_EXPORT QStaticPlugin
{//...// Since qdoc gets confused by the use of function// pointers, we add these dummes for it to parse instead:QObject *instance();const char *rawMetaData();//...
};

4.1.3.示例

运行上面的实例,显示出全部的插件信息,如下:

4.2.动态插件

 动态插件一般以动态库的方式来加载的。

4.2.1.动态插件的加载过程

        在Qt 中加载这些插件的流程比较复杂的,我们自己写的时候就简单多了。流程大体相似,首先定义QPluginLoader 对象。QPluginLoader 的构造函数加载插件目录下所有插件,然后调用 instance 创建插件对象,最后就可以调用插件功能了。

QString loadPlugin(QString pluginPath)
{QPluginLoader loader(pluginPath);if(! loader.load()) {qDebug()<<"load failed ";}QObject* plugin = loader.instance();if(plugin) {BrushInterface* interface = qobject_cast<BrushInterface*>(plugin);if(interface) {//...}}else {qDebug()<<"loader.instance failed!";return QString();}
}

QPluginLoader的load过程如下:

bool QPluginLoader::load()
{if (!d || d->fileName.isEmpty())return false;if (did_load)return d->pHnd && d->instance;if (!d->isPlugin())return false;did_load = true;return d->loadPlugin();
}bool QLibraryPrivate::loadPlugin()
{if (instance) {libraryUnloadCount.ref();return true;}if (pluginState == IsNotAPlugin)return false;if (load()) {instance = (QtPluginInstanceFunction)resolve("qt_plugin_instance");return instance;}if (qt_debug_component())qWarning() << "QLibraryPrivate::loadPlugin failed on" << fileName << ":" << errorString;pluginState = IsNotAPlugin;return false;
}

上述代码的核心在:

1) load() 为加载动态库,window和linux实现方式不同,分别调用各自系统的API实现,windows一般调用LoadLibrary实现,linux一般调用dlopen实现,很具体的请自行查阅资料。

2)找到函数qt_plugin_instance的地址,并保存此地址,这个就是实例化插件对象的函数。

那么qt_plugin_instance是在那里定义的呢?自然会联想到QT_MOC_EXPORT_PLUGIN:

#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)

在QT_MOC_EXPORT_PLUGIN里面就有qt_plugin_instance函数。

5.定位插件

        Qt 应用程序将会自动感知可用的插件,因为插件都被存储在标准的子目录当中。因此应用程序不需要任何查找或者加载插件的代码。

        在开发过程中,插件的目录是 QTDIR/plugins(QTDIR 是 Qt 的安装目录),每个类型的插件放在相应类型的目录下面。如果想要应用程序使用插件,但不想用标准的插件存放路径,可以在应用程序的安装过程中指定要使用的插件的路径,可以使用 QSettings,保存插件路径,在应用程序运行时读取配置文件。应用程序可以通过QCoreApplication::addLibraryPath()函数将指定的插件路径加载到应用程序中。

        使插件可加载的一种方法是在应用程序所在目录创建一个子目录,用于存放插件。如果要发布和 Qt 一起发布的插件(存放在 plugins 目录)中的任何插件,必须拷贝 plugins 目录下的插件子目录到应用程序的根目录下。 

6.插件开发的优势

  • 方便功能扩展:通过插件,可以轻松地扩展应用程序的功能,而不需要修改应用程序本身。
  • 更新量小:当底层接口不变时,只需要更新插件即可,而不需要重新发布整个应用程序。
  • 降低模块间依赖:插件与主程序之间通过接口交互,降低了模块间的依赖关系,支持并行开发。
  • 面向未来:通过插件,可以进一步演化API的功能,使API在长时间内保持可用性和适用性。

7.总结

        Qt插件机制是一种强大且灵活的功能扩展方式,它允许开发者通过创建和加载插件来增强Qt应用程序的功能。通过遵循一定的规范,开发者可以轻松地创建和使用插件,从而满足各种复杂和多变的需求。

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

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

相关文章

GPT-4o有点坑

GPT-4o有点坑 0. 前言1. GPT-4o简介2. GPT-4o带来的好处2.1 可以上传图片和文件2.2 更丰富的功能以及插件 3. "坑"的地方3.1 使用时间短3.2 GPT-4o变懒了 4. 总结 0. 前言 原本不想对GPT-4o的内容来进行评论的&#xff0c;但是看了相关的评论一直在说&#xff1a;技…

Ai晚班车531

1.中央网信办等三部门&#xff1a;加快推进大模型、生成式人工智能标准研制。 2.中国石油与中国移动、华为、科大讯飞签署合作协议。 3.Opera浏览器与谷歌云合作&#xff0c;接入 Gemini 大模型。 4.谷歌 Gemini 加持Chromebook Plus。 5.英飞凌&#xff1a;开发 8kW和12kW…

改进YOLOv8系列:构建新型单头transformer模块,加入到骨干尾部

改进YOLOv8系列:构建新型单头transformer模块,加入到骨干尾部 需要修改的代码self attention代码创建yaml文件测试是否创建成功本文提供了改进 YOLOv8注意力系列包含不同的注意力机制以及多种加入方式,在本文中具有完整的代码和包含多种更有效加入YOLOv8中的yaml结构,读者…

【论文导读】Grid Graph Reduction for Efficient Shortest Pathfinding(2023 Access)

Grid Graph Reduction for Efficient Shortest Pathfinding 作者&#xff1a;CHAN-YOUNG KIM AND SANGHOON SULL 文章提出了一种“基于模式识别的网格阻塞”&#xff08; Pattern-Based Blocking on grid graphs&#xff0c;PBGG&#xff09;的预处理方法&#xff0c;以加快最…

数据分析案例-在线食品订单数据可视化分析与建模分类

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

[羊城杯 2021]BabySmc

运行就是输入flag 不知道怎么跳过去的 这个应该就是smc加密的函数了 运行完这个函数才能继续往下 int __cdecl main(int argc, const char **argv, const char **envp) {__int64 v3; // rbx__int64 v4; // r12__int64 v5; // r13unsigned __int64 v6; // raxchar v7; // spcha…

企业如何进行快递运费对账?

在电子面单寄件取代手写纸质面单之后&#xff0c;加上月结寄件模式的推行&#xff0c;企业快递运费对账&#xff0c;成了行政的一个难题...... 早期的手写纸质面单寄件&#xff0c;企业行政或者财务相关人员&#xff0c;遵循寄前审批&#xff0c;寄后报销的原则进行对账。随着电…

FinalShell无法连接Linux

Linux使用Vmware会创建一个网络&#xff0c;让两个子网处于一个网关&#xff0c;这样就能在windows中连接Linux&#xff0c;只有在这种情况下才能FinalShell才能连接Linux

KT6368A双模蓝牙芯片上电到正常发送AT指令或指令复位需要多久

一、简介 KT6368A芯片上电到正常发送AT指令&#xff0c;或者开启蓝牙广播被搜索到&#xff0c;或者指令复位需要多久等等系列问题总结 详细描述 其实这些问题归结到一起&#xff0c;就还是一个问题&#xff0c;芯片上电需要多久的时间 在另外一份文档里面&#xff0c;是有描…

Java设计模式 _行为型模式_访问者模式

一、访问者模式 1、访问者模式 访问者模式&#xff08;Visitor Pattern&#xff09;是一种行为型模式。它允许在不修改已有类结构的情况下&#xff0c;向类中添加新的操作。访问者模式通过将操作封装在一个访问者对象中&#xff0c;使得可以在不改变各个元素类的前提下&#x…

RedisTemplate实战应用--队列等

一、RedisTemplate队列插入 1、从集合左边插入值 https://blog.csdn.net/weixin_43658899/article/details/121040307 leftPush(K key, V value) redisTemplate.opsForList().leftPush("leftdatakey","bbbb");2、从集合左边开始在v1值后边插入新值v2 le…

使用 Django 连接 MySQL 数据库

文章目录 步骤一&#xff1a;安装必要的库和驱动步骤二&#xff1a;配置数据库连接步骤三&#xff1a;执行数据库迁移步骤四&#xff1a;开始使用 MySQL 数据库创建一个模型迁移模型到数据库使用模型进行数据操作创建新记录&#xff1a;查询记录&#xff1a;更新记录&#xff1…

Mac安装第三方软件的命令安装方式

场景&#xff1a; 打开终端命令行&#xff0c;sudo xattr -rd com.apple.quarantine&#xff0c;注意最后quarantine 后面加一个空格&#xff01;然后打开Finder&#xff08;访达&#xff09;&#xff0c;点击左侧的 应用程序&#xff0c;找到相关应用&#xff0c;拖进终端qua…

(超实用)京东订单数据分析案例-维度下钻

1&#xff0c;数据介绍&#xff0c;字段了解 尽可能熟悉业务&#xff0c;多知道字段的含义&#xff0c;字段字段间的逻辑关系&#xff0c;后期数据分析思路才能更清晰&#xff0c;结果才能更准确 2&#xff0c;订单数据分析基本思路 维度下钻 3&#xff0c;代码实现全流程思路…

华为telnet的两种认证方式

华为telnet的两种认证方式 实验拓扑&#xff1a; 实验要求&#xff1a; 1.采用普通密码认证实现telnet 远程登录机房设备R3 2.采用AAA认证服务方式实现telnet 远程登录机房设备R3 实验步骤&#xff1a; 1.完成基本配置&#xff08;设备接口配置IP&#xff0c;此步骤略过&#…

Facebook的隐私保护挑战:用户数据安全的新时代

在全球范围内&#xff0c;Facebook已经成为了不可忽视的社交媒体巨头&#xff0c;它连接着超过20亿的活跃用户。然而&#xff0c;随着其影响力的不断扩大&#xff0c;关于用户隐私和数据安全的问题也愈加引人关注。本文将深入探讨Facebook面临的隐私保护挑战&#xff0c;以及它…

解析前端开发中同源策略与配置代理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 在前端开发中&#xff0c;跨域请求是一个常见的问题。同源策略限制了浏览器中一个页面…

Unity中的MVC框架

基本概念 MVC全名是Model View Controller 是模型(model)-视图(view)-控制器(controller)的缩写 是一种软件设计规范&#xff0c;用一种业务逻辑、数据、界面显示 分离的方法组织代码 将业务逻辑聚集到一个部件里面&#xff0c;在改进和个性化定制界面及用户交互的同时&#x…

【嵌入式硬件】DRV8874电机驱动

目录 1 芯片介绍 1.1 特性简介 1.2 引脚配置 1.3 最佳运行条件 2 详细说明 2.1 PMODE配置控制模式 2.1.1 PH/EN 控制模式 2.1.2 PWM 控制模式 2.1.3 独立半桥控制模式 2.2 电流感测和调节 2.2.1 IPROPI电流感测 2.2.2 IMODE电流调节 3.应用 3.1设计要求 3.2 设计…

AI换脸FaceFusion一键云部署指南

大家好&#xff0c;从我开始分享到现在&#xff0c;收到很多朋友的反馈说配置很低玩不了AI。本篇是一个云端部署AI项目的指南&#xff0c;帮助大家在云端进行AI项目的部署。我会从云平台的选择、代码部署、保存镜像几个方面进行详细的介绍。没有代码基础的小白也不用担心&#…