Qt使用插件QPluginLoader 机制开发
简介:
插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。
Qt 提供了2种APIs来创建插件:
一种高级API,用于为Qt本身编写插件:自定义数据库驱动程序,图像格式,文本编解码器,自定义样式等。
一种用于扩展Qt应用程序的低级API。也就是说扩展我们自己使用Qt编写的程序。这要求应用程序使用 QPluginLoader 检测和加载插件。在这种情况下,插件可以提供任意功能,不限于数据库驱动程序、图像格式、文本编解码器、样式以及扩展Qt功能的其他类型的插件。本文主要通过示例来展示第二种方式创建插件。
原理:
重载了虚函数的dll,这跟抽象工厂类类似,这便是插件的原理。qt的插件可以说是一种动态库
Qt插件特点:
核心技术:QPluginLoader
Qt插件存储在共享库(DLL)中(即以动态库的方式存在),与使用QLibrary访问的共享库相比,它具有以下优势:
面向Interface编程,内部封装,模块和整体流程开发分离,提高开发效率。
QPluginLoader 检查插件是否与应用程序相同版本的 Qt 链接。
QPluginLoader 提供对根组件对象 (instance()) 的直接访问,而不是强制您手动解析 C 函数
插件
插件主要面向接口编程,无需访问.lib文件,热插拔、利于团队开发。即使在程序运行时.dll不存在,也可以正常启动,只是相应插件功能无法正常使用而已;
动态库
动态库需要访问.lib文件,而且在程序运行时必须保证.lib存在,否则无法正常启动
开发测试环境:QT5.15.2 + MSVC 2019
在容器端:
- 定义一组接口(只有纯虚函数的类)。
- 在QT_BEGIN_NAMESPACE和QT_END_NAMESPACE之间使用Q_DECLARE_INTERFACE()宏告诉Qt的元对象系统有关接口的信息。
- 在程序中使用 QPluginLoader加载插件。
- 使用qobject_cast()测试插件是否实现给定的接口。
Q_DECLARE_INTERFACE
Q_DECLARE_INTERFACE(类名,标识符)
此宏用于把标识符与类名接口关联起来。这个标识符是唯一的,这个宏通常在被放到一个类被定后的位置。
这个是Qt实现插件必须要有的一个宏, 把标识符与类名接口关联起来。
在插件端:
声明一个插件类,该类继承自QObject和插件要提供的接口类。
使用Q_INTERFACES()宏告诉Qt的元对象系统有关接口的信息。
使用Q_PLUGIN_METADATA()宏导出插件。
Q_PLUGIN_METADATA
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.SubPlugin" FILE "SubPlugin.json")
这个宏第一次参数定义了一个uuid,保证唯一即可,第二个json是必须要有的,当无法找到指定的文件时,moc 会出现错误,即使是空的文件也行
SubPlugin.json文件存放在源代码目录下,可以记录一下插件的信息(如插件名称、插件版本、插件依赖库),如下
{
"name": "testSubPlugin",
"version": "1.0",
"dependencies": []
}
这个宏被用于声明元数据,这个元数据是被实例化插件的一部分。
这个宏需要通过对象声明被实例化接口的IID,并且要引用包含元数据内容的文件。
需要获取这些元数据(即上面的json数据),可在在通过下面的办法。
QPluginLoader *loader = new QPluginLoader(this);
loader->setFileName(plugin.absoluteFilePath());
qDebug()<<loader->metaData().keys();
QJsonObject json = loader->metaData().value("MetaData").toObject();
QVariant var = json.value("name").toVariant();
Q_INTERFACES
Q_INTERFACES(AppInterface) //声明这个插件实现是基于哪个插件接口的,使qobject_cast()能正确进行QObject*到接口指针的转换
QPluginLoader 类
~QPluginLoader()
销毁 QPluginLoader 对象。
除非显式调用 unload(),否则插件会一直保留在内存中,直到应用程序终止。
Load
加载插件,如果插件加载成功则返回true; 否则返回false。
unload
卸载插件,如果插件可以卸载则返回true,否则返回false。
这在应用程序终止时自动发生,因此通常不需要调用此函数。
如果 QPluginLoader 的其他实例正在使用相同的插件,则调用将失败,并且只有在每个实例都调用了 unload() 时才会发生卸载。
不要尝试删除根组件。 而是依靠 unload() 会在需要时自动删除它
Instance
返回插件的根组件对象,组件对象是一个 QObject。 使用 qobject_cast() 可将其转成所需的对象。如果根组件对象被销毁,则调用此函数会创建一个新实例。
该函数返回的根组件在 QPluginLoader 销毁时不会被删除。 如果要确保删除根组件,则应在不再需要访问核心组件时立即调用 unload()。 当库最终卸载时,根组件将自动删除。
metaData
返回此插件的元数据。 元数据是在编译插件时使用 Q_PLUGIN_METADATA() 宏以 json 格式指定的数据。
例子
首先创建一个Qt Subdirs Project工程PluginDemo,并添加一个app应用程序端。并在app.pro添加输出目录DESTDIR = $$PWD/../bin
- 然后添加一个appinterface.h的头文件,该类作为插件接口类。
#ifndef APPINTERFACE_H#define APPINTERFACE_H#include <QList>#include <QAction>class AppInterface{public:virtual ~AppInterface() {}// 插件的名字virtual QString name() const = 0;// 插件返回的QAction列表virtual QList<QAction *> actions() const = 0;};QT_BEGIN_NAMESPACEQ_DECLARE_INTERFACE(AppInterface, "plugindemo_app_interface")QT_END_NAMESPACE#endif // APPINTERFACE_H
- 在工程上右键,选择“新子项目”,选择Library->C++ Library,类型选择“shared Library”,qt module 选择“widgets”.插件名称为subplugin.
- 修改subplugin.pro
添加CONFIG+=plugin //目的就是为是该dll作为插件
target.path = $$PWD/../bin/plugins、INSTALL+=target //把插件dll拷贝到容器的bin目录下。需构建步骤添加 make install
- 实现接口类SubPlugin.h,注意添加Q_INTERFACES和Q_PLUGIN_METADATA宏
#ifndef SUBPLUGIN_H#define SUBPLUGIN_H#include "subPlugin_global.h"#include "../app/appinterface.h"#include <QObject>class SUBPLUGIN_EXPORT SubPlugin : public QObject, public AppInterface{public:SubPlugin();Q_OBJECTQ_INTERFACES(AppInterface) //声明这个插件实现是基于哪个插件接口的Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.SubPlugin" FILE "SubPlugin.json") //使用Q_PLUGIN_METADATA宏导出插件// AppInterface interfacepublic:virtual QString name() const override;virtual QList<QAction *> actions() const override;};#endif // SUBPLUGIN_H#include "subplugin.h"SubPlugin::SubPlugin(){}QString SubPlugin::name() const{return "SubPlugin";}QList<QAction *> SubPlugin::actions() const{QAction *aboutQt = new QAction;aboutQt->setText(tr("About Qt"));QList<QAction *> result;result << aboutQt;return result;}
- 在容器的Dialog.cpp里面添加加载插件的代码。
#include "dialog.h"#include "ui_dialog.h"#include "appinterface.h"#include <QDebug>#include <QPluginLoader>#include <QFileInfo>#include <QDir>#include <QPushButton>Dialog::Dialog(QWidget *parent): QDialog(parent), ui(new Ui::Dialog){loadPlugins();ui->setupUi(this);}Dialog::~Dialog(){delete ui;}void Dialog::loadPlugins(){QString pluginPath = QCoreApplication::applicationDirPath() + "/plugins";QStringList filters;#if defined(Q_OS_LINUX)filters << "*.so";#elif defined(Q_OS_WIN)filters << "*.dll";#endifQFileInfoList plugins = QDir(pluginPath).entryInfoList(filters,QDir::Files | QDir::NoSymLinks);for (int i = 0; i < plugins.count(); ++i) {QFileInfo plugin = plugins.at(i);QPluginLoader *loader = new QPluginLoader(this);loader->setFileName(plugin.absoluteFilePath());qDebug()<<loader->metaData().keys();QJsonObject json = loader->metaData().value("MetaData").toObject();QVariant var = json.value("name").toVariant();bool result = loader->load();qDebug() << QString("load plguin %1 result: ").arg(plugin.absoluteFilePath()) << result;if (result) {QObject *pluginInstance = loader->instance();AppInterface *appInterface = qobject_cast<AppInterface *>(pluginInstance);if (appInterface != nullptr) {qDebug() << "add actions from plugin:" << appInterface->name();QList<QAction *> actions = appInterface->actions();for (int k = 0; k < actions.count(); ++k) {QAction *ac = actions.at(k);QString strtext = ac->text();}}} else {qDebug() << loader->errorString();}}}
插件管理
自定义pluginmanager类,封装成dll
管理所有插件,包括加载、卸载等。并暴露方法给主程序。
插件管理器代码
插件接口
#ifndef PLUGININTERFACE_H#define PLUGININTERFACE_H#include <QtWidgets/qwidget.h>#include <QString>class PluginInterface{public:virtual ~PluginInterface() {}// 插件返回的QAction列表virtual QString datawrite() = 0;};Q_DECLARE_INTERFACE(PluginInterface, "PluginManager.PluginInterface")#endif // PLUGININTERFACE_H
插件管理器
#ifndef PLUGINMANAGER_H#define PLUGINMANAGER_H#include "PluginManager_global.h"#include <QObject>#include <QPluginLoader>class PLUGINMANAGER_EXPORT PluginManager:public QObject{Q_OBJECTprivate:PluginManager();public:static PluginManager *getinstance();//加载所有插件void loadAllPlugins();//卸载所有插件void unloadAllPlugins();QString datawrite();private:static PluginManager *m_instance;QHash<QString, QPluginLoader *>m_loaders; //插件路径--QPluginLoader实例};#endif // PLUGINMANAGER_H#include "pluginmanager.h"#include <QDebug>#include <QPluginLoader>#include <QFileInfo>#include <QDir>#include <QCoreApplication>#include"PluginInterface.h"PluginManager* PluginManager::m_instance=nullptr;PluginManager::PluginManager(){}PluginManager *PluginManager::getinstance(){if(m_instance==nullptr)m_instance= new PluginManager();return m_instance;}void PluginManager::loadAllPlugins(){QString pluginPath = QCoreApplication::applicationDirPath() + "/plugins";QStringList filters;#if defined(Q_OS_LINUX)filters << "*.so";#elif defined(Q_OS_WIN)filters << "*.dll";#endifQFileInfoList plugins = QDir(pluginPath).entryInfoList(filters,QDir::Files | QDir::NoSymLinks);for (int i = 0; i < plugins.count(); ++i){QFileInfo plugin = plugins.at(i);QPluginLoader *loader = new QPluginLoader(this);loader->setFileName(plugin.absoluteFilePath());qDebug()<<loader->metaData().keys();QJsonObject json = loader->metaData().value("MetaData").toObject();QVariant var = json.value("name").toVariant();bool result = loader->load();qDebug() << QString("load plguin %1 result: ").arg(plugin.absoluteFilePath()) << result;if (result){m_loaders.insert(plugin.absoluteFilePath(), loader);QObject *pluginInstance = loader->instance();PluginInterface *appInterface = qobject_cast<PluginInterface *>(pluginInstance);if (appInterface != nullptr){}}else{qDebug() << loader->errorString();}}}void PluginManager::unloadAllPlugins(){for(auto filepath : m_loaders.keys()){QPluginLoader *loader = m_loaders.value(filepath);//卸载插件,并从内部数据结构中移除if(loader->unload()){m_loaders.remove(filepath);delete loader;loader = nullptr;}}}QString PluginManager::datawrite(){QString str;for(auto filepath : m_loaders.keys()){QPluginLoader *loader = m_loaders.value(filepath);PluginInterface *Plugin = dynamic_cast<PluginInterface*>(loader->instance());if(Plugin)str = Plugin->datawrite();}return str;}
插件代码
#ifndef LOGPLUGIN_H#define LOGPLUGIN_H#include "logPlugin_global.h"#include "../PluginManager/PluginInterface.h"#include <QObject>class LOGPLUGIN_EXPORT LogPlugin: public QObject,public PluginInterface{Q_OBJECTQ_INTERFACES(PluginInterface) //声明这个插件实现是基于哪个插件接口的Q_PLUGIN_METADATA(IID "org.qt-LogPlugin" FILE "LogPlugin.json") //使用Q_PLUGIN_METADATA宏导出插件public:LogPlugin();virtual QString datawrite() override;};#endif // LOGPLUGIN_H#ifndef DBPLUGIN_H#define DBPLUGIN_H#include "DbPlugin_global.h"#include "../PluginManager/PluginInterface.h"#include <QObject>class DBPLUGIN_EXPORT DbPlugin : public QObject,public PluginInterface{public: Q_OBJECTQ_INTERFACES(PluginInterface) //声明这个插件实现是基于哪个插件接口的Q_PLUGIN_METADATA(IID "org.qt-DbPlugin" FILE "DbPlugin.json") //使用Q_PLUGIN_METADATA宏导出插件public:DbPlugin();virtual QString datawrite() override;};#endif // DBPLUGIN_H
App代码
#include "dialog.h"#include "ui_dialog.h"#include <QDebug>#include <QPluginLoader>#include <QFileInfo>#include <QDir>#include <QPushButton>#include "../PluginManager/pluginmanager.h"Dialog::Dialog(QWidget *parent): QDialog(parent), ui(new Ui::Dialog){ui->setupUi(this);}Dialog::~Dialog(){delete ui;}void Dialog::on_pushButton_clicked(){PluginManager::getinstance()->loadAllPlugins();PluginManager::getinstance()->datawrite();PluginManager::getinstance()->unloadAllPlugins();}
插件通信
插件的通信通过插件管理器来管理,插件管理器转发插件的消息
- 在插件管理器的接口头文件增加一个插件间通信的结构体,增加插件发送消息和接收消息的纯虚函数。
Q_DECLARE_METATYPE(Type)
作用:向Qt元系统注册一些非基本类型
这个宏能使Qt元系统的QMetaType知道Type类型,但前提是Type类型必须提供一个公有的默认构造函数、公有的默认拷贝构造函数和公有的默认析构函数。
- 在插件重写插件管理器声明的接收虚函数,把插件管理器声明的发送虚函数声明为信号。
- 在插件管理器的加载插件处加上connect函数,是信号槽关联起来。其中把插件发送信号关联插件管理器的接收槽函数。
同时让插件管理器的接收槽函数转发给对应的插件
- 在app程序中调用调用插件的信号从而发送消息
完整代码参见:
链接:https://pan.baidu.com/s/1fWK_oSYQTRNip4FLnTOLwg
提取码:qc0t
相关文章:

Qt使用插件QPluginLoader 机制开发
简介: 插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。 Qt 提供了2种APIs来创建插件: 一种高级API,用于为Qt本身编写插件:自定义数据库驱动程序,图像格…...

双子座 Gemini1.5和谷歌的本质
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...

二百三十、MySQL——MySQL表的索引
1 目的 梳理一下目前MySQL维度表的索引情况,当然网上也有其他博客专门讲MySQL索引的,我这边只是梳理一下目前的索引状况而已 2单列索引 2.1 索引截图 2.2 建表语句 3 联合索引 3.1 索引截图 3.2 建表语句 4 参考的优秀博客 http://t.csdnimg.cn/ZF7…...

并发编程之ThreadLocal使用及原理
ThreadLocal主要是为了解决线程安全性问题的 非线程安全举例 public class ThreadLocalDemo {// 非线程安全的private static final SimpleDateFormat sdf new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static Date parse(String strDate) throws ParseExc…...

软件测试 测试开发丨Pytest结合数据驱动-yaml,熬夜整理蚂蚁金服软件测试高级笔试题
编程语言 languages: PHPJavaPython book: Python入门: # 书籍名称 price: 55.5 author: Lily available: True repertory: 20 date: 2018-02-17 Java入门: price: 60 author: Lily available: False repertory: Null date: 2018-05-11 yaml 文件使用 查看 yaml 文件 pych…...

软考数据库---2.SQL语言
主要记忆:表、索引、视图操作语句;数据操作;通配符、转义符;授权;存储过程;触发器 这部分等等整理一下: “”" 1、 数据定义语言。 SQL DDL提供定义关系模式和视图、 删除关系和视图、 修改关系模式的…...

基于顺序表实现通讯录
上篇我们讲了顺序表是什么,和如何实现顺序表。这篇文章我们将基于顺序表来实现通讯录。 文章目录 前言一、基于顺序表是如何实现的二、通讯录的头文件和实现文件三、通讯录的实现3.1 定义通讯录结构3.2 初始化通讯录3.3 销毁通讯录3.4 通讯录添加数据3.5 查找联系人…...

咸鱼之王_手游_开服搭建架设_内购修复无bug运营版
视频演示 咸鱼之王_手游_开服 游戏管理后台界面 源码获取在文章末尾 源码获取在文章末尾 源码获取在文章末尾 或者直接下面 https://githubs.xyz/y28.html 1.安装宝塔 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh &…...

【JSON2WEB】14 基于Amis的CRUD开发30分钟速成
【JSON2WEB】系列目录 【JSON2WEB】01 WEB管理信息系统架构设计 【JSON2WEB】02 JSON2WEB初步UI设计 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代码前端框架介绍 【JSON2WEB】05 前端开发三件套 HTML CSS JavaScript 速成 【JSON2WEB】06 JSO…...

Java入门教程||Java 变量
Java 变量 Java教程 - Java变量 变量由标识符,类型和可选的初始化程序定义。变量还具有范围(可见性/生存期)。 Java变量类型 在Java中,必须先声明所有变量,然后才能使用它们。变量声明的基本形式如下所示࿱…...

基于Java的校园快递一站式服务系统 (源码+文档+包运行)
一.系统概述 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本校园快递一站式服务系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞…...

通讯录的实现(顺序表版本)
我们知道通讯录是基于顺序表的前提下,要写好通讯录我们就要深入了解好顺序表。我们先来看看什么是顺序表。(注意今天代码量有点多,坚持一下)。冲啊!兄弟们! 顺序表的简单理解 对于顺序表,我们首…...

利用Sentinel解决雪崩问题(一)流量控制
1、解决雪崩问题的常见方式有四种: 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待;舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离;熔断降级:由断路器统计业务…...
二叉树总结
递归返回值 1、如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。 2、如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 3、如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,…...

接口优化技巧
一、背景 针对老项目,去年做了许多降本增效的事情,其中发现最多的就是接口耗时过长的问题,就集中搞了一次接口性能优化。本文将给小伙伴们分享一下接口优化的通用方案 二、接口优化方案总结 1.批处理 批量思想:批量操作数据库&a…...

【工具】NPS 内网穿透搭建
背景 在日常开发中经常会涉及到使用公网某个端口进行开发调试的情况,但我们日常开发的机器IP是非公网IP,所以需要使用内网穿透的手段,使我们的服务在公网上能被访问到。 常用的内网穿透工具分两大类,一类是付费/免费服务…...

【数学】主成分分析(PCA)的详细深度推导过程
本文基于Deep Learning (2017, MIT),推导过程补全了所涉及的知识及书中推导过程中跳跃和省略的部分。 blog 1 概述 现代数据集,如网络索引、高分辨率图像、气象学、实验测量等,通常包含高维特征,高纬度的数据可能不清晰、冗余&am…...

微信跳转页面时发生报错
报错如下图所示: 解决方法:(从下面四种跳转方式中任选一种,哪种能实现效果就用哪个) 带历史回退 wx.navigateTo() //不能跳转到tabbar页面 不带历史回退 wx.redirectTo() //跳转到另一个页面wx.switchTab() //只能…...

8:系统开发基础--8.1:软件工程概述、8.2:软件开发方法 、8.3:软件开发模型、8.4:系统分析
转上一节: http://t.csdnimg.cn/G7lfmhttp://t.csdnimg.cn/G7lfm 课程内容提要: 8:知识点考点详解 8.1:软件工程概述 1.软件的生存周期 2.软件过程改进—CMM Capability Maturity Model能力成熟度模型 3.软件过程改进—CMMI—…...

【简单讲解下Symfony框架】
🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

基于PHP的连锁酒店管理系统
有需要请加文章底部Q哦 可远程调试 基于PHP的连锁酒店管理系统 一 介绍 连锁酒店管理系统基于原生PHP开发,数据库mysql,前端bootstrap。系统角色分为用户和管理员。 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销 2 个人中…...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

【LeetCode】算法详解#6 ---除自身以外数组的乘积
1.题目介绍 给定一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O…...
comfyui 工作流中 图生视频 如何增加视频的长度到5秒
comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗? 在ComfyUI中实现图生视频并延长到5秒,需要结合多个扩展和技巧。以下是完整解决方案: 核心工作流配置(24fps下5秒120帧) #mermaid-svg-yP…...

Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...