【UE5 C++课程系列笔记】27——多线程基础——ControlFlow插件的基本使用
目录
步骤
一、搭建基本同步框架
二、添加委托
三、添加蓝图互动框架
四、修改为异步框架
完整代码
通过一个游戏初始化流程的示例来介绍“ControlFlows”的基本使用。
步骤
一、搭建基本同步框架
1. 勾选“ControlFlows”插件
2. 新建一个空白C++类,这里命名为“ControlFlowSubSystem”
让“ControlFlowSubSystem” 继承“GameInstanceSubsystem”,然后添加反射所需代码
重写父类“ShouldCreateSubsystem”、“Initialize”和“Deinitialize”方法
3. 在Build.cs中添加“ControlFlows”模块
引入所需库
定义一个蓝图可调用的方法“InitLevel”,表示要执行的初始化流程;定义一个布尔类型变量“bIniting”用于表示当前是否处于初始化流程;再定义5个函数用于表示初始化流程的各个步骤。
“InitLevel”实现如下:
首先,通过检查 bIniting
来判断当前是否已经处于初始化过程中。如果 bIniting
为 true
,意味着初始化正在进行或者已经执行过了,此时输出一条日志信息然后直接返回,避免重复执行初始化流程。只有当 bIniting
为 false
时,才会将其设置为 true
,表示即将开始初始化流程。
第31行代码通过调用 FControlFlowStatics::Create
静态函数创建一个 FControlFlow
类型的控制流实例 Flow
。在创建过程中,传入了 this
指针和一个字符串,这个字符串作为控制流的唯一标识符。
第33~37行代码通过多次调用 Flow.QueueStep
函数,向刚创建的控制流实例中依次添加了多个需要按顺序执行的步骤。
第40行代码调用 Flow.ExecuteFlow()
函数启动控制流的执行。此时,FControlFlow
实例会按照之前添加步骤的顺序,依次调用对应的成员函数,确保整个初始化流程按照预定的顺序有条不紊地进行,直到所有步骤都执行完毕,完成整个初始化过程。
用于表示初始化流程步骤的5个函数实现如下,当执行到最后一个步骤时。将 bIniting
改为 false
,表示初始化流程已经结束。
4. 在关卡蓝图中调用“InitLevel”函数
执行结果如下,可以通过日志信息看到完整执行了整个初始化流程。
5. 为了观察每个步骤在哪一帧执行,可以通过添加如下代码实现:
运行结果如下,可以看到所有表示流程步骤的函数都是在同一帧执行的,这可能会造成游戏帧率下降,因此这并不符合我们的需求。
二、添加委托
6. 下面先创建两个委托,通过委托来向外界传递任务进度等信息。
申明两个动态多播委托类型
在第11行代码中,FControlFlows_InitProgress
是要声明的委托类型的名称,FGuid
是UE中用于表示全局唯一标识符(GUID)的类型,在这里它作为委托参数的类型,而 InitAsyncID
是给这个参数起的变量名。当委托被调用时,会传递一个 FGuid
类型的全局唯一标识符。该委托在被调用时,还会传递一个表示进度值的浮点型数据,这个值可以用来直观地展示当前异步初始化任务已经完成的比例或者进度情况。
在第12行代码中,FControlFlows_InitResult
代表所声明的委托类型名称,FGuid
与InitAsyncID
和前面的委托类似,bool
与 bResult
用于指示异步初始化任务最终是成功还是失败,直观地告知结果状态。Message
表示委托调用时还会附带更详细的关于初始化结果的说明。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FControlFlows_InitProgress, FGuid, InitAsyncID, float, ProgressValue); //进度更新
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FControlFlows_InitResult, FGuid, InitAsyncID, bool, bResult, FString, Message); //初始化结果
声明两个委托类型的成员变量,并且通过 UPROPERTY(BlueprintAssignable)
元数据标签对这两个属性进行修饰,使其具备了在蓝图系统中可被绑定具体的回调函数的特性。
7. 为表示初始化步骤的5个成员函数添加进度值输入参数
8. 设置进行“InitLocalAsset”步骤时,初始化进度为10%;进行“InitNetInfo”步骤时,初始化进度为20%;进行“InitUserInfo”步骤时,初始化进度为50%;进行“NotifyMainUI”步骤时,初始化进度为80%;进行“FinishThisInit”步骤时,初始化进度为100%;
9. 声明InitAsyncID
用于唯一标识某次初始化的异步任务
执行“InitLevel”函数进行初始化时为InitAsyncID
赋值
当进行各初始化的步骤时,通过InitProgress
委托广播当前的初始化进度,传入对应的InitAsyncID
和本步骤的当前进度值InProgressValue
,这样外部绑定了该委托的对象就能接收到进度更新情况。
在初始化流程的最后一个步骤对应的函数中,添加如下一行代码,通过InitResult
委托广播当前的初始化结果。
三、添加蓝图互动框架
10. 添加一个控件蓝图,用于显示当前初始化进度,这里命名为“WBP_ControlFlowsMainUI”
打开“WBP_ControlFlowsMainUI”,添加如下控件,主要是添加一个文本控件用于显示进度值,按钮用于调用“InitLevel”从而开始初始化
在事件构造和结构时分别绑定和解绑“InitProgress”、“InitResult”委托
重命名委托绑定的匹配函数为“UpdateProgress”和“InitResult”
匹配函数“UpdateProgress”和“InitResult”函数逻辑如下,将委托传递的GUID、进度值和初始化结果信息打印出来,并显示进度值。如果初始化成功后就将界面隐藏,如果失败就显示进度值为0.00
当按钮点击后调用“InitLevel”开始初始化
11. 在关卡蓝图中让界面显示出来
此时运行后界面如下
点击初始化按钮后打印信息如下:
此时就完成了初始化流程的同步框架实现,接下来我们希望将同步改为异步实现。
四、修改为异步框架
12. 引入所需头文件
13. 更改“InitLocalAsset”、“InitNetInfo”、“InitUserInfo”函数逻辑如下
主要通过使用AsyncTask
函数创建了一个外层的异步任务,并指定其可以在任意线程上执行。在这个异步任务的 lambda 表达式内部首先调用FPlatformProcess::Sleep(0.2)
模拟一个耗时操作,接着又创建了一个内层的异步任务,指定在游戏线程上执行后续的耗时操作,以及向外广播初始化的进度信息。
完善“InitUserInfo”逻辑如下
第81行首先获取位于项目保存目录下的文件名为 ControlFlowsUserInfo.txt
的目标文件相对路径,然后将相对路径转换为绝对路径。
第82行将初始化任务的唯一标识 InitAsyncID
转换为字符串形式赋值给 UserInitAsyncID。
第83行获取当前的日期时间并转换为 HTTP 日期格式赋值给 UserInitDataTime
。
第84~86行创建一个 TArray<FString>
类型的数组 MyStringInfo
,并将前面获取的UserInitAsyncID
和 UserInitDataTime
字符串添加进去,准备将这些信息保存到文件中。
第87行使用 FFileHelper::SaveStringArrayToFile
函数将包含用户初始化相关信息的字符串数组 MyStringInfo
保存到指定路径 UserInfoPath
的 ControlFlowsUserInfo.txt
中,并且指定了编码选项为ForceUTF8WithoutBOM
,确保文件内容以指定的编码格式存储。
编译后运行,此时当我们点击初始化按钮后,看到输出日志信息如下,可以发现初始化流程的步骤不再是一帧内执行的了。
并且在“Saved”文件夹中多了一个名为 ControlFlowsUserInfo.txt
的文件
ControlFlowsUserInfo.txt
的内容如下,包括了初始化任务的唯一标识和文件存储时间。
如果初始化流程中的某一步失败了,通过 InitResult
委托向外广播初始化失败的结果信息,第113行调用 SubFlow->CancelFlow()标识
取消当前正在执行的控制流,从而及时终止整个初始化流程。将 bIniting
变量设置为 false
,表示当前不再处于初始化过程中,同时将 InitAsyncID
重置为默认值,为下一次可能的初始化操作做好准备。
完整代码
“ControlFlowSubSystem.h”
// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"
#include "ControlFlow.h"
#include "ControlFlowManager.h"
#include "ControlFlowNode.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Async/Async.h"
#include "ControlFlowSubSystem.generated.h"DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FControlFlows_InitProgress, FGuid, InitAsyncID, float, ProgressValue); //进度更新
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FControlFlows_InitResult, FGuid, InitAsyncID, bool, bResult, FString, Message); //初始化结果#define INITRESULT falseUCLASS()
class STUDY_API UControlFlowSubSystem : public UGameInstanceSubsystem
{GENERATED_BODY()public:virtual bool ShouldCreateSubsystem(UObject* Outer) const override;virtual void Initialize(FSubsystemCollectionBase& Collection) override;virtual void Deinitialize() override;UFUNCTION(BlueprintCallable)void InitLevel();public:UPROPERTY(BlueprintAssignable)FControlFlows_InitProgress InitProgress;UPROPERTY(BlueprintAssignable)FControlFlows_InitResult InitResult;protected:bool bIniting = false;FGuid InitAsyncID = FGuid();void InitLocalAsset(FControlFlowNodeRef SubFlow, double InProgressValue);void InitNetInfo(FControlFlowNodeRef SubFlow, double InProgressValue);void InitUserInfo(FControlFlowNodeRef SubFlow, double InProgressValue);void NotifyMainUI(FControlFlowNodeRef SubFlow, double InProgressValue);void FinishThisInit(FControlFlowNodeRef SubFlow, double InProgressValue);
};
“ControlFlowSubSystem.cpp”
// Fill out your copyright notice in the Description page of Project Settings.#include "ControlFlowSubSystem.h"bool UControlFlowSubSystem::ShouldCreateSubsystem(UObject* Outer) const
{return true;
}void UControlFlowSubSystem::Initialize(FSubsystemCollectionBase& Collection)
{Super::Initialize(Collection);
}void UControlFlowSubSystem::Deinitialize()
{Super::Deinitialize();
}void UControlFlowSubSystem::InitLevel()
{if (bIniting){UE_LOG(LogTemp, Warning, TEXT("Initing..."));return;}InitAsyncID = FGuid::NewGuid();bIniting = true;uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitFlow -- FrameIndex: %d"), FrameIndex);FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("ControlFlow_InitLevel"));Flow.QueueStep(TEXT("InitLocalAsset"), this, &UControlFlowSubSystem::InitLocalAsset, 0.1);Flow.QueueStep(TEXT("InitNetInfo"), this, &UControlFlowSubSystem::InitNetInfo, 0.2);Flow.QueueStep(TEXT("InitUserInfo"), this, &UControlFlowSubSystem::InitUserInfo, 0.5);Flow.QueueStep(TEXT("NotifyMainUI"), this, &UControlFlowSubSystem::NotifyMainUI, 0.8);Flow.QueueStep(TEXT("FinishThisInit"), this, &UControlFlowSubSystem::FinishThisInit, 1.0);UE_LOG(LogTemp, Warning, TEXT("ExecuteFlow -- FrameIndex: %d"), FrameIndex);Flow.ExecuteFlow();
}void UControlFlowSubSystem::InitLocalAsset(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitLocalAsset -- FrameIndex: %d"), FrameIndex);AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();});});
}void UControlFlowSubSystem::InitNetInfo(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitNetInfo -- FrameIndex: %d"), FrameIndex);AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();});});
}void UControlFlowSubSystem::InitUserInfo(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitUserInfo -- FrameIndex: %d"), FrameIndex);AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);FString UserInfoPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()/TEXT("ControlFlowsUserInfo.txt"));FString UserInitAsyncID = InitAsyncID.ToString();FString UserInitDataTime = FDateTime::Now().ToHttpDate();TArray<FString> MyStringInfo;MyStringInfo.Add(UserInitAsyncID);MyStringInfo.Add(UserInitDataTime);FFileHelper::SaveStringArrayToFile(MyStringInfo, *UserInfoPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();});});
}void UControlFlowSubSystem::NotifyMainUI(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("NotifyMainUI -- FrameIndex: %d"), FrameIndex);if (INITRESULT){InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();}else{AsyncTask(ENamedThreads::GameThread, [this]() {InitResult.Broadcast(InitAsyncID, false, TEXT("Init Failed"));});SubFlow->CancelFlow();bIniting = false;InitAsyncID = {};}
}void UControlFlowSubSystem::FinishThisInit(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("FinishThisInit -- FrameIndex: %d"), FrameIndex);InitProgress.Broadcast(InitAsyncID, InProgressValue);InitResult.Broadcast(InitAsyncID, true, TEXT("Init Success"));SubFlow->ContinueFlow();bIniting = false;InitAsyncID = {};
}
相关文章:

【UE5 C++课程系列笔记】27——多线程基础——ControlFlow插件的基本使用
目录 步骤 一、搭建基本同步框架 二、添加委托 三、添加蓝图互动框架 四、修改为异步框架 完整代码 通过一个游戏初始化流程的示例来介绍“ControlFlows”的基本使用。 步骤 一、搭建基本同步框架 1. 勾选“ControlFlows”插件 2. 新建一个空白C类,这里…...

有收到腾讯委托律师事务所向AppStore投诉带有【水印相机】主标题名称App的开发者吗
近期,有多名开发者反馈,收到来自腾讯科技 (深圳) 有限公司委托北京的一家**诚律师事务所卞,写给AppStore的投诉邮件。 邮件内容主要说的是,腾讯注册了【水印相机】这四个字的商标,所以你们这些在AppStore上的app&…...
标定 3
标定场景与对应的方式 标定板标定主要应用场景: (1)无法获取到执行机构物理坐标值,比如相机固定,执行机构为传送带等 (2)相机存在畸变等非线性标定情况,需要进行畸变校正 (3)标定单像素精度 (4)获取两个相机之间的坐标系关系 标定板操作步骤: (1)确定好拍…...

用 C# 绘制谢尔宾斯基垫片
谢尔宾斯基垫片是一个三角形,分解成多个小三角形,如右图所示。有几种方法可以生成这种垫片。这里展示的方法是其中一种比较令人惊讶的方法。 程序从三个点开始(图中圆圈所示)。“当前位置”从其中一个点开始。为了生成后续点&…...
java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
今天在朋友机子上运行代码,在生成token的时候,遇到了这样一个问题: Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter at io.jsonwebtoken.impl.Base64Codec.decode(Base64Codec.java:26) ~[jjwt-0.9.1.jar:0.…...

双因素身份验证技术在NPI区域邮件安全管控上的解决思路
在制造业中,NPI(New Product Introduction,新产品导入)区域是指专门负责新产品从概念到市场推出全过程的部门或团队。NPI 的目标是确保新产品能够高效、高质量地投入生产,并顺利满足市场需求。在支撑企业持续创新和竞争…...

java后端对接飞书登陆
java后端对接飞书登陆 项目要求对接第三方登陆,飞书登陆,次笔记仅针对java后端,在看本笔记前,默认已在飞书开发方已建立了应用,并获取到了appid和appsecret。后端要做的其实很简单,基本都是前端做的&…...

记录一次Android Studio的下载、安装、配置
目录 一、下载和安装 Android Studio 1、搜索下载Android studio 2、下载成功后点击安装包进行安装: 3、这里不用打勾,直接点击安装 : 4、完成安装: 5、这里点击Cancel就可以了 6、接下来 7、点击自定义安装:…...

直流无刷电机控制(FOC):电流模式
目录 概述 1 系统框架结构 1.1 硬件模块介绍 1.2 硬件实物图 1.3 引脚接口定义 2 代码实现 2.1 软件架构 2.2 电流检测函数 3 电流环功能实现 3.1 代码实现 3.2 测试代码实现 4 测试 概述 本文主要介绍基于DengFOC的库函数,实现直流无刷电机控制&#x…...

73.矩阵置零 python
矩阵置零 题目题目描述示例 1:示例 2:提示: 题解思路分析Python 实现代码代码解释提交结果 题目 题目描述 给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例…...
垃圾收集算法
分代收集理论 分代收集理论,建立在两个分代假说之上。 弱分代假说:绝大多数对象都是朝圣夕灭的。 强分代假说:熬过越多次垃圾收集的过程的对象就越难以消亡。 这两个分代假说奠定了垃圾收集器的一致设计原则:收集器应该将Java…...
SQL-leetcode-262. 行程和用户
262. 行程和用户 表:Trips --------------------- | Column Name | Type | --------------------- | id | int | | client_id | int | | driver_id | int | | city_id | int | | status | enum | | request_at | varchar | --------------------- id 是这张表的主键…...

太原理工大学软件设计与体系结构 --javaEE
这个是简答题的内容 选择题的一些老师会给你们题库,一些注意的点我会做出文档在这个网址 项目目录预览 - TYUT复习资料:复习资料 - GitCode 希望大家可以给我一些打赏 什么是Spring的IOC和DI IOC 是一种设计思想,它将对象的创建和对象之间的依赖关系…...

Leetcode 139. 单词拆分 动态规划
原题链接:Leetcode 139. 单词拆分 递归,超时 class Solution { public:bool isfind(string s,map<string,int>& mp){for(auto x:mp){string wordx.first;if(sword) return true;int nword.size();if(n>s.size()) continue;string s1s.subs…...

python异常机制
异常是什么? 软件程序在运行过程中,非常可能遇到刚刚提到的这些问题,我们称之为异常,英文是Exception,意思是例外。遇到这些例外情况,或者交异常,我们怎么让写的程序做出合理的处理,…...
运行爬虫时可能遇到哪些常见问题?
在运行Python爬虫时,可能会遇到以下一些常见问题及相应的解决方法: 1. 请求频繁被封 IP 问题描述:爬虫请求频繁时,网站可能会识别到异常行为并封禁 IP,从而导致后续请求失败。解决方法: 使用代理…...

BGP与CN2的区别 详解两者在网络传输中的应用与优势
在现代互联网环境中,选择合适的网络传输协议和解决方案对于企业的业务运行至关重要。BGP(Border Gateway Protocol)和CN2(China Telecom Next Carrier Network)是两种广泛应用的网络技术,但它们的设计理念、…...

Spring 项目 基于 Tomcat容器进行部署
文章目录 一、前置知识二、项目部署1. 将写好的 Spring 项目先打包成 war 包2. 查看项目工件(Artifact)是否存在3. 配置 Tomcat3.1 添加一个本地 Tomcat 容器3.2 将项目部署到 Tomcat 4. 运行项目 尽管市场上许多新项目都已经转向 Spring Boot࿰…...
“负载均衡”出站的功能、原理与场景案例
在企业日常网络中,外网访问速度不稳定是一个常见问题。特别是多条外网线路并行时,不合理的流量分配会导致资源浪费甚至网络拥堵。而出站负载均衡,正是解决这一问题的关键技术。 作为一种先进的网络流量管理技术,其核心是优化企业内…...

02-51单片机数码管与矩阵键盘
一、数码管模块 1.数码管介绍 如图所示为一个数码管的结构图: 说明: 数码管上下各有五个引脚,其中上下中间的两个引脚是联通的,一般为数码管的公共端,分为共阴极或共阳极;其它八个引脚分别对应八个二极管…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...
十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建
【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...