Qt 插件机制使用及原理

news2024/10/6 6:48:53

目录

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]
    // BrushInterface
    QStringList 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;

    // ShapeInterface
    QStringList shapes() const override;
    QPainterPath generateShape(const QString &shape, QWidget *parent) override;

    // FilterInterface
    QStringList 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      = lib
  CONFIG       += plugin static

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

  TEMPLATE      = app
  QTPLUGIN     += 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) held
    by 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 plugin
    loader. The function is similar to \l staticInstances()
    with the addition that a QStaticPlugin also contains
    meta 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.0

    Registers the \a plugin specified with the plugin loader, and is used
    by 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.coloradmin.cn/o/1717242.html

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

相关文章

【MySQL数据库】:MySQL复合查询

目录 基本查询回顾 多表查询 自连接 子查询 单行子查询 多行子查询 多列子查询 在from子句中使用子查询 合并查询 前面我们讲解的mysql表的查询都是对一张表进行查询&#xff0c;在实际开发中这远远不够。 基本查询回顾 【MySQL数据库】&#xff1a;MySQL基本查…

电子电气SCI期刊,中科院1区TOP,收稿范围广泛

一、期刊名称 IEEE Transactions on Smart Grid 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;工程技术 影响因子&#xff1a;9.6 中科院分区&#xff1a;1区 三、期刊征稿范围 IEEE Transactions on Smart Grid是一本跨学科期刊&#xff0c;旨在传播智…

【Linux】进程(1)

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解Linux进程&#xff08;1&#xff09;&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 1. 冯诺依曼体系结构2.操作系统&#xff08;Operator System / O…

mysql的增删查改(进阶)

一. 更复杂的新增 将从表名查询到的结果插入到表名2中 insert into 表名2 select .. from 表名1 ...; 创建一个学生表: 创建一个学生表2, 将学生表中的数据加到学生表2中: 注意: 列的类型可以匹配即可插入, 列名和列的类型不一定要完全一致 二. 查询 2.1 聚合查询 前面谈到…

深度学习知识与心得

目录 深度学习简介 传统机器学习 深度学习发展 感知机 前馈神经网络 前馈神经网络&#xff08;BP网络&#xff09; 深度学习框架讲解 深度学习框架 TensorFlow 一个简单的线性函数拟合过程 卷积神经网络CNN&#xff08;计算机视觉&#xff09; 自然语言处理NLP Wo…

LabVIEW中进行步进电机的位置控制

在LabVIEW中进行步进电机的位置控制&#xff0c;通常涉及以下几个关键步骤&#xff1a;设置硬件、配置通信、编写控制算法和实施反馈控制。以下是一个详细的介绍。 硬件设置 步进电机&#xff1a;选择合适的步进电机&#xff0c;根据负载和应用需求选择适当的步数和转矩。 驱…

【力扣】1312. 让字符串成为回文串的最少插入次数

一、题目描述 二、题解 本题我们利用动态规划的思想来解决。 1、状态表示 首先创建一个dp数组&#xff0c;dp[i][j] 表示的是将字符串 s 的 [ i, j ] 区间的这一子串&#xff0c;变成回文串的最少插入次数。 2、状态转移方程 3、初始化 根据「状态转移方程」&#xff0c;没…

煤矿输送设备无人化运维巡检解决方案

一、煤矿行业目前存在的挑战和难题 煤矿行业面临着复杂的环境&#xff0c;如粉尘、潮湿、高温、高瓦斯等&#xff0c;对巡检设备和人员安全有威胁。并且设备分布广、需要长时间作业&#xff0c;全面巡检难度大、对巡检工作的耐力和持续性要求高。而煤矿输送设备无人化运维巡检…

Comfyui图片高清放大方法

在过去的两期内容中&#xff0c;我们探讨了如何安装 ComfyUI 及其在图像生成中的应用。 本期&#xff0c;我们将深入了解如何使用 ComfyUI 对图片进行高清放大. 在开始今天的主题之前&#xff0c;请确保您已经在个人电脑上安装了 ComfyUI。同时&#xff0c;确保您已将 ESRGAN_4…

ios:文本框默认的copy、past改成中文复制粘贴

问题 ios 开发&#xff0c;对于输入框的一些默认文案展示&#xff0c;如复制粘贴是英文的&#xff0c;那么如何改为中文的呢 解决 按照路径找到这个文件 ios/项目/Info.plist&#xff0c;增加 <key>CFBundleAllowMixedLocalizations</key> <true/> <…

Oracle Linux上安装ORDS

ORDS就是Oracle REST Data Services。 环境如下&#xff1a; Oracle Linux 8Oracle Database 19cIP地址为A.B.C.D 要安装最新版本的ORDS&#xff0c;当前为24.1.1。 全程参考文档&#xff1a;Installing and Configuring Oracle REST Data Services 安装ORDS 添加reposit…

固定翼飞机(固定翼飞行器)种类丰富 国家政策推动行业发展速度加快

固定翼飞机&#xff08;固定翼飞行器&#xff09;种类丰富 国家政策推动行业发展速度加快 固定翼飞机又称固定翼飞行器&#xff0c;指机翼固定于机身&#xff0c;可通过固定机翼产生升力的飞行器。固定翼飞机具有机动性强、运载量大、航程远、飞行速度快等优势&#xff0c;在农…

Redis用GEO实现附近的人功能

文章目录 ☃️概述☃️命令演示☃️API将数据库表中的数据导入到redis中去☃️实现附近功能 ☃️概述 GEO就是Geolocation的简写形式&#xff0c;代表地理坐标。Redis在3.2版本中加入了对GEO的支持&#xff0c;允许存储地理坐标信息&#xff0c;帮助我们根据经纬度来检索数据。…

排八字软件有哪些?

排八字软件有哪些&#xff1f;在市面上有很多排八字的软件可供选择&#xff0c;其中一些比较知名的有&#xff1a; 无敌八字排盘软件&#xff1a;这是一款功能强大的八字排盘软件&#xff0c;提供详细的八字解析和命理分析服务&#xff0c;且完全免费。 网易星盘&#xff1a;网…

珠宝首饰AR虚拟3D试戴增强企业商品营销效果

在西安这座古老与现代交织的城市中&#xff0c;VRAR软件开发公司相比其他城市也略多一些&#xff0c;作为专业的西安AR软件开发公司&#xff0c;我们正凭借着前沿的AR增强现实/VR虚拟现实技术&#xff0c;为客户打造独一无二的互动体验。 专业团队&#xff0c;定制开发 我们拥有…

OAK相机如何将 YOLOv9 模型转换成 blob 格式?

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是Ashely。 专…

如何做好流程优化?看这里的目的、原则和方法

流程管理的本质是通过构造卓越的业务流程让流程增值&#xff0c;为客户创造真正的价值。 但卓越的业务流程并不是一蹴而就的&#xff0c;有一个过程&#xff0c;这个过程就是业务流程和流程管理体系不断优化提升的过程&#xff08;可以参照流程成熟度评价模型&#xff09;。 …

[pdf,epub]《软件方法》2024版电子书共290页(202405更新)

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 已上传本账号CSDN资源。 或者到以下链接下载&#xff1a; http://www.umlchina.com/url/softmeth2024.html&#xff0c;或点击“阅读原文”。 如果需要提取码&#xff1a;umlc 已排…

【SpringMVC】_简单示例计算器

目录 1. 需求分析 2. 接口定义 3. 请求参数 4. 响应数据 5. 服务器代码 6. 前端页面代码 7. 运行测试 为阶段性总结与应用&#xff0c;现将以Spring MVC项目创建一个可以实现加法的计算器为例 1. 需求分析 加法计算器功能&#xff0c;对两个整数进行相加&#xff0c;需…

uniapp跨端代码编写(h5和钉钉小程序)

页面开发 差异。小程序编译机制不一样&#xff0c;我在写h5的时候&#xff0c;页面布局啥的都是用uniapp的扩展组件来修改的&#xff08;都是改的原生组件的样式&#xff09;&#xff0c;小程序编译有组件隔离&#xff0c;不能直接修改组件的原生样式&#xff0c;查了很多资料…