IntelliJ IDE 插件开发 | (五)VFS 与编辑器

系列文章

  • IntelliJ IDE 插件开发 |(一)快速入门
  • IntelliJ IDE 插件开发 |(二)UI 界面与数据持久化
  • IntelliJ IDE 插件开发 |(三)消息通知与事件监听
  • IntelliJ IDE 插件开发 |(四)来查收你的 IDEA 使用报告吧
  • IntelliJ IDE 插件开发 |(五)VFS 与编辑器

前言

在前几篇文章中主要介绍了关于 IntelliJ IDE 插件开发的基础知识,这部分内容对开发一些小功能的插件的开发已经足够。不过,如果想要开发一些与具体编程语言相关的、提升开发效率的插件(例如MybatisX),那么前几篇的内容就不足以支撑了。而从本篇开始,则会介绍实现相关功能所需要的知识: VFS、编辑器、PSI、自定义语言等,最后再以两三个插件的实战开发(例如老生常谈的代码生成)进行结尾,本文涉及到的完整代码已上传到GitHub。

VFS(Virtual File System)

VFS(虚拟文件系统)可以看作是 IntelliJ 平台对各种类型(本地磁盘的文件、DIFF 信息文件、远程文件)文件操作的封装,通过提供一致的 API 以及文件变更事件,让开发人员可以专注于对文件的处理。此外之所以使用虚拟二字,也是因为我们在对文件操作时并没有直接修改源文件,而是修改了源文件所对应的快照,然后 VFS 会通过同步或者异步的方式去修改源文件。关于 VFS 主要有以下注意点:

  1. 文件快照是应用级别的,因此一个文件即使被多个项目使用也只会对应一份快照。

  2. 文件快照内容和源文件内容并非实时对应,例如文件已从资源管理器中删除,但 IntelliJ 平台只有接收并处理了文件删除事件后才会从快照中删除指定文件。

  3. VFS 通过操作系统的 File Watcher 去感知文件的变化(基于时间戳),可以通过下述步骤去查看被监听的文件根节点:

    image-20240105140441390

    image-20240105140448931

  4. 如果在代码中访问了被忽略的文件(例如下图中配置的),VFS 就会直接加载并返回文件的内容而非是快照的内容:

    image-20240105140934840

  5. 如果需要保证代码是在文件刷新后完成,则可以使用以下方式:

    val virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE)
    virtualFile?.let {it.refresh(true, true) {// 文件刷新完成}
    }
    

    refresh 的方法签名如下,第三个参数 postRunnable 用于传递我们要执行的代码:

    /*** The same as {@link #refresh(boolean, boolean)} but also runs {@code postRunnable}* after the operation is completed. The runnable is executed on event dispatch thread inside write action.*/
    public abstract void refresh(boolean asynchronous, boolean recursive, @Nullable Runnable postRunnable);
    

获取 VirtualFile 对象的方式

以下内容来自官网:

ContextAPI
ActionAnActionEvent.getData(PlatformDataKeys.VIRTUAL_FILE) AnActionEvent.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY) for multiple selection
DocumentFileDocumentManager.getFile()
PSI FilePsiFile.getVirtualFile()(may return null if the PSI file exists only in memory)
File NameFilenameIndex.getVirtualFilesByName()
Local File System PathLocalFileSystem.findFileByIoFile() VirtualFileManager.findFileByNioPath()/refreshAndFindFileByNioPath() (2020.2+)

在上面的示例中就是使用的第一种方式:AnActionEvent.getData(PlatformDataKeys.VIRTUAL_FILE) 。

VirtualFile 在文件的基础内容上还增加了类型、文件系统等扩展信息:

class VFSAction : AnAction() {override fun actionPerformed(e: AnActionEvent) {val virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE)virtualFile?.let {it.refresh(true, true) {// 文件刷新完成Utils.info("""文件路径: ${it.path}文件类型: ${it.fileType}文件系统: ${it.fileSystem}文件后缀: ${it.extension}文件时间戳: ${it.timeStamp}""".trimIndent())}}}
}

监听文件变更事件

只需要在项目启动监听中注册我们的文件变更监听事件即可:

class ProjectStartListener: ProjectActivity {override suspend fun execute(project: Project) {project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {override fun after(events: MutableList<out VFileEvent>) {for (event in events) {Utils.info("变更的文件: ${event.file?.path}")}}})}}

VFS 的监听是应用级别,因此当打开多个项目的时候,每个项目中的文件变更事件都会被所有的项目共享,因此需要注意对文件进行过滤处理。

通过以上简单的代码我们就可以获取所有的文件变更事件,做我们想做的事了(例如统一在文件前增加版权信息)。

编辑器

在第四篇文章中为了实现对用户的编码活动的统计,我们简单使用了 Editor 对象,主要是对其文档和光标活动的监听。而本文文章则会详细介绍关于 Editor 中的一些基础概念,主要是几个模型对象和文档对象,在正式介绍前,先说一下获取 Editor 对象的两种方式:

// 通过 project 获取
var editor = FileEditorManager.getInstance(project).selectedEditor
// 通过 Action 的事件对象获取
val editor = e.getData(PlatformDataKeys.EDITOR)

下面正式开始介绍 Editor 中的一些对象

Document

document 对象主要提供了获取和替换文件文本的一些方法(还可以添加对文档内容的监听):

image-20240105154024382

下面会结合模型对象介绍其使用。

CaretModel(插入符模型)

CaretModel(插入符模型) 用于获取编辑器中光标所处的位置,如果细分还可以分为 Logical Position(逻辑位置)和 Visual Position(视觉位置)。下面以一个简单的代码样例展示区别:

val editor = e.getData(PlatformDataKeys.EDITOR) ?: return
val caretModel = editor.caretModel
Utils.info("""逻辑位置:<br/>${"-".repeat(20)}<br/>行号: ${caretModel.logicalPosition.line + 1}<br/>列号: ${caretModel.logicalPosition.column + 1}<br/><br/>视觉位置:<br/>${"-".repeat(20)}<br/>行号: ${caretModel.visualPosition.line + 1}<br/>列号: ${caretModel.visualPosition.column + 1}<br/>
""".trimIndent())

image-20240122160210509

可以看出来逻辑位置对应光标在文件中所处的真实位置(5 行 1 列),而视觉位置则如表面意思,由于存在折叠代码块(中间3行),所以行号为 3。除此之外,可以看到上述代码中,不管是caretModel.logicalPosition还是caretModel.visualPosition在获取行号和列号的时候都在结果加 1,这是因为行号和列号在代码中是从 0 开始计算。这里要注意的是,上面的折叠代码只是影响视觉位置结果的一种情况,如果文件开启自动断行(Soft-Wrap line),也同样会影响视觉位置的结果:

image-20240122161633034

使用类似阿拉伯语这种右向文字也会影响逻辑位置和视觉位置的结果。

除了以上两种定位方式,我们还可以通过caretModel.offset直接获取光标所在位置的全局偏移量:

image-20240122162645983

这里可以看到第 3 行开始位置对应的偏移量是 22(20个显示的字符加上两个换行符,空白字符都会被统计)。

除了单光标,多光标的时候(例如按住 alt 结合鼠标左键进行多行选择),每一个光标的位置也是按照上述规则进行计算:

caretModel.allCarets.forEach { Utils.info("""偏移量:<br/>${"-".repeat(20)}<br/>偏移量: ${it.offset}<br/>""".trimIndent())
}

image-20240122163704843

最后以 Doucument 和 CaretModel 结合使用的例子结尾:

// 写操作需要放入 WriteCommandAction.runWriteCommandAction 中
WriteCommandAction.runWriteCommandAction(e.project) {// 插入字符串并移动光标位置到结尾val msg = "庄周de蝴蝶"document.insertString(caretModel.offset, msg)caretModel.moveToOffset(caretModel.offset + msg.length)
}

上述代码实现了在光标处插入指定文本信息并移动光标位置的效果。

InlayModel(嵌入模型)

InlayModel(嵌入模型)用于在代码行中嵌入各种信息,例如下图中msg:就属于嵌入信息,通过使用嵌入信息可以在不改变文本内容的情况下给用户一个直观的提示内容:

image-20240123143514386

除了上面这种使用方式,我们还可以使用 InlayModel 模拟各种 gpt 插件生成代码的效果,代码及效果如下(使用到了 caretModel 中的偏移量和视觉位置等内容):

// 清除所有嵌入信息
val inlayModel = editor.inlayModel
inlayModel.getInlineElementsInRange(0, editor.document.textLength).forEach { Disposer.dispose(it) }
inlayModel.getBlockElementsInRange(0, editor.document.textLength).forEach { Disposer.dispose(it) }
// 分别增加单行和多行嵌入信息
val offset = caretModel.offset
val column = caretModel.visualPosition.column
inlayModel.addInlineElement(offset, DemoRender(editor, "庄周de蝴蝶"))
inlayModel.addBlockElement(offset, InlayProperties(),DemoRender(editor, mutableListOf("first line", "second line", "third line"), column.toFloat()))
// 移动光标位置到初始位置
caretModel.moveToVisualPosition(VisualPosition(caretModel.visualPosition.line, column))class DemoRender<T>(private val editor: Editor,private val renderText: T,private var wordCount: Float = 0f
): EditorCustomElementRenderer {// 设置字体private val font = Font("Microsoft YaHei", Font.ITALIC, editor.colorsScheme.editorFontSize)override fun calcWidthInPixels(p0: Inlay<*>): Int {// 获取渲染内容的宽度, 如果是多行文本则取最长文本行的宽度return when (renderText) {is String -> calcTextWidth(renderText)is MutableList<*> -> renderText.maxOfOrNull { calcTextWidth(it.toString()) } ?: 0else -> 0}}override fun calcHeightInPixels(inlay: Inlay<*>): Int {// 获取渲染内容的高度, 如果是多行文本则需要将行高乘以行数return when (renderText) {is MutableList<*> -> super.calcHeightInPixels(inlay) * renderText.sizeelse -> super.calcHeightInPixels(inlay)}}override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {val g2 = g.create() as Graphics2DGraphicsUtil.setupAAPainting(g2)textAttributes.foregroundColor = JBColor.GRAYval lineHeight = editor.lineHeight.toDouble()val fontBaseline = ceil(font.createGlyphVector(getFontMetrics().fontRenderContext, "中文").visualBounds.height)val linePadding = (lineHeight - fontBaseline) / 2.0val offsetX = targetRegion.xval offsetY = targetRegion.y + fontBaseline + linePaddingval lineOffset = 0g2.font = fontg2.color = JBColor.GRAYwhen (renderText) {is String -> {g2.drawString(renderText, offsetX.toFloat(), (offsetY + lineOffset).toFloat())}is MutableList<*> -> {// 多行文本渲染的时候设置缩进val tabSize = editor.settings.getTabSize(editor.project)val startOffset = calcTextWidth("Z") * (wordCount + tabSize)renderText.forEach {g2.drawString(it.toString(), startOffset, (offsetY + lineOffset).toFloat())g2.translate(0.0, lineHeight)}}else -> return}g2.dispose()}private fun calcTextWidth(text: String): Int {return getFontMetrics().stringWidth(text) }private fun getFontMetrics(): FontMetrics {val editorContext = FontInfo.getFontRenderContext(editor.contentComponent)val context = FontRenderContext(editorContext.transform, AntialiasingType.getKeyForCurrentScope(false), editorContext.fractionalMetricsHint)return FontInfo.getFontMetrics(font, context)}
}

动画

这里需要知道 addInlineElement 用于添加单行的嵌入信息,addBlockElement 用于添加多行嵌入信息,渲染内容需要实现 EditorCustomElementRenderer 类中的 paint 方法即可。通过使用 Graphics2D 对象,我们可以添加文本,图形甚至是图片信息,这里就不再逐一介绍。

image-20240123145515809

SoftWrapModel(自动断行模型)

SoftWrapModel(自动断行模型)主要用于获取自动断行相关信息,其包含的方法较少,也很少会用到,简单了解即可,如下是一个简单的使用及效果:

val softWrapModel = editor.softWrapModel
Utils.info("""是否开启自动断行: ${softWrapModel.isSoftWrappingEnabled}<br/>当前行是否存在自动断行: ${softWrapModel.getSoftWrapsForLine(caretModel.offset).isNotEmpty()}<br/>
""".trimIndent())

image-20240123161016895

MarkupModel(标记模型)

MarkupModel(标记模型)主要用于用于将指定行(或者某个区间)设置为高亮,下面直接展示其使用方式和效果,很容易理解和使用:

val markupModel = editor.markupModel
markupModel.removeAllHighlighters()
// 设置当前行为红色高亮背景
val lineAttr = TextAttributes()
lineAttr.backgroundColor = JBColor.RED
val line = caretModel.logicalPosition.line
markupModel.addLineHighlighter(line, HighlighterLayer.ERROR, lineAttr)// 设置指定范围为绿色高亮背景
// 最后一个参数指定为 HighlighterTargetArea.EXACT_RANGE 则为精确范围
// 指定为 HighlighterTargetArea.LINES_IN_RANGE 则会将偏移量所在的整行都进行设置
val rangeAttr = TextAttributes()
rangeAttr.backgroundColor = JBColor.BLUE
markupModel.addRangeHighlighter(0, 6, HighlighterLayer.SELECTION, rangeAttr, HighlighterTargetArea.EXACT_RANGE)

动画

SelectionModel(选中模型)

SelectionModel(选中模型)用于处理编辑器中文本选中相关的操作,下面是一个简单的使用样例:

// 先移除所有文本选中, 然后将偏移量为 0 ~ 6 的内容进行选中
val selectionModel = editor.selectionModel
selectionModel.removeSelection(true)
selectionModel.setSelection(0, 6)
Utils.info("""选中的文本内容: ${selectionModel.selectedText}<br/>选中文本的开始和结束位置: (${selectionModel.selectionStart}, ${selectionModel.selectionEnd})<br/>
""".trimIndent())

动画

FoldingModel(折叠模型)

FoldingModel(折叠模型)用于对代码块进行折叠操作,通过结合 SelectionModel,可以很方便地对选中的文本进行折叠,下面是使用方式及效果:

val selectionModel = editor.selectionModel
val foldingModel = editor.foldingModel
// 批量折叠操作需要放在 runBatchFoldingOperation 中
foldingModel.runBatchFoldingOperation {// 移除所有的折叠foldingModel.allFoldRegions.forEach { foldingModel.removeFoldRegion(it) }// 对选中文本进行折叠并设置显示的提示文本foldingModel.addFoldRegion(selectionModel.selectionStart, selectionModel.selectionEnd, "庄周de蝴蝶")
}

动画

IndentsModel(缩进模型)

IndentsModel(缩进模型)用于获取文本中的缩进信息,使用方式也很简单,如下所示:

val indentsModel = editor.indentsModel
val guide = indentsModel.caretIndentGuide ?: return
Utils.info("""缩进级别: ${guide.indentLevel}<br/>开始行: ${guide.startLine + 1}<br/>结束行: ${guide.endLine + 1}<br/>
""".trimIndent())

动画

ScrollingModel(滚动模型)

ScrollingModel(滚动模型)可以用于获取编辑器的可视区域范围以及执行滚动操作,使用方法如下:

val scrollingModel = editor.scrollingModel
scrollingModel.scrollToCaret(ScrollType.CENTER)
scrollingModel.runActionOnScrollingFinished {scrollingModel.visibleArea.let {Utils.info("""可视区域左上角坐标: (${it.x}, ${it.y})<br/>可视区域宽度: ${it.width}<br/>可视区域高度: ${it.height}<br/>""".trimIndent())}
}

动画

编辑器事件

在第三篇文章中我们简单了解了 IntelliJ 中的事件监听,当时只介绍了项目的启用与关闭事件,这里再简单介绍一下关于编辑器的事件。

EditorActionHandler

IntelliJ 平台默认为我们提供了一些编辑器事件,例如复制、粘贴等,可以在IdeActions类中看到:

image-20240124141755359

相应地,使用方式也很简单,只需要实现 EditorActionHandler 类重写其中的 doExecute 方法即可,下面代码展示了对编辑器的复制事件进行监听并提示:

class EditorCopyListener: EditorActionHandler(), ProjectActivity {override suspend fun execute(project: Project) {// 注册复制监听EditorActionManager.getInstance().setActionHandler(IdeActions.ACTION_EDITOR_COPY, this)}override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {// 将选中的文本加入到粘贴板中, 这里使用前文中提到的 selectionModeleditor.selectionModel.copySelectionToClipboard()// 显示复制文本Messages.showInfoMessage("Copy text: ${CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)}", "Copy")}
}

动画

这里需要注意的是,在重写了编辑器的事件后(这里是复制事件),则该事件就不再生效了,因此这里通过editor.selectionModel.copySelectionToClipboard()这行代码手动了复制的操作,在重写其它编辑器事件的时候也需要注意这个问题。

TypedHandlerDelegate

除了使用 IntelliJ 中已经定义好的事件,还可以通过实现 TypedHandlerDelegate 来对键盘中输入的字符进行监听(无法监听空白字符,例如回车、TAB等,也无法在出现编码提示时进行监听):

class KeyboardListener: TypedHandlerDelegate() {override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {Messages.showInfoMessage("Input character: $c", "Input")return Result.CONTINUE}}

动画

同时需要在plugin.xml中进行配置:

<extensions defaultExtensionNs="com.intellij"><typedHandler implementation="cn.butterfly.vfs.listener.KeyboardListener"/>
</extensions>

总结

本文主要介绍了关于 VFS 和编辑器相关的知识,其中编辑器相关的内容则需要重点关注,这是开发各类插件都离开的东西,毕竟我们开发的插件大多都要和编辑器里的代码打交道,同时如果有错误之处,也欢迎一起交流讨论。

题外话

本来准备下篇文章就开始介绍关于 PSI 的知识,不过想一下还是决定先写一篇在第三篇文章中提到的关于内部工具(能极大提升初学者开发插件的效率,我个人是这么认为的)使用的文章,然后再继续介绍关于 PSI 的内容。

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

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

相关文章

【GitHub项目推荐--不错的 React 开源项目】【转载】

用 React Flow 连接你的想法 用 React Flow 连接你的想法&#xff0c;这是一个高度可定制的库&#xff0c;基于 React 用于构建基于节点的 交互式 UI、编辑器、流程图和图表。 开源地址&#xff1a;https://github.com/wbkd/react-flow Bulletproof React 一个简单、可扩展且…

PyTorch深度学习实战(33)——条件生成对抗网络(Conditional Generative Adversarial Network, CGAN)

PyTorch深度学习实战&#xff08;33&#xff09;——条件生成对抗网络 0. 前言1. 条件生成对抗网络1.1 模型介绍1.2 模型与数据集分析 2. 实现条件生成对抗网络小结系列链接 0. 前言 条件生成对抗网络 (Conditional Generative Adversarial Network, CGAN) 是一种生成对抗网络…

IP报文格式(全网最详细)

IP报文格式 报文格式 图1 IP头格式 表1 IP头字段解释 字段长度含义Version4比特 4&#xff1a;表示为IPV4&#xff1b;6&#xff1a;表示为IPV6。IHL4比特首部长度&#xff0c;如果不带Option字段&#xff0c;则为20&#xff0c;最长为60&#xff0c;该值限制了记录路由选项。…

AutoGen实战应用(二):多代理协作(Multi-Agent Collaboration)

AutoGen是微软推出的一个全新工具&#xff0c;它用来帮助开发者创建基于大语言模型(LLM)的复杂应用程序. AutoGen能让LLM在复杂工作流程启用多个角色代理来共同协作完成人类提出的任务。在我之前的一篇博客: AutoGen实战应用(一)&#xff1a;代码生成、执行和调试 中我们通过一…

【RabbitMQ】交换机的概念及使用

一、引言 1、什么是交换机 RabbitMQ中&#xff0c;交换机是一个核心概念&#xff0c;主要用来将生产者生产出来的消息&#xff0c;传送到对应的队列中。实际上&#xff0c;生产者生产的消息从不会直接发送到队列&#xff0c;而是发送到交换机。交换机一方面接收来自生产者的消…

SpringBoot项目配置SSL后,WebSocket连接失败的解决方案

SpringBoot项目配置SSL后&#xff0c;WebSocket连接应使用wss协议&#xff0c;而不是ws协议。在前端配置WebSocket时&#xff0c;URL以wss://开头。

嵌入式学习第十二天

8.数组指针和指针数组&#xff08;2&#xff09;: &#xff08;1&#xff09;指针数组&#xff1a; int *a[5]; char *str[5]; 指针数组主要用来操作字符串数组,通过将指针数组的每个元素存放字符串的首地址实现对多个字符串的操作 二维数组主要用来存储字符串数组…

Nodejs前端学习Day3_准备工作

妈的&#xff0c;这几天真tm冷&#xff0c;前天上午还下了一整天的雪&#xff0c;大雪 文章目录 前言一、Node.js简介1.1何为1.2有什么 二、Node.js可以做什么三、学习路线四、下载nodejs4.1小坑记录4.2LTS和Current版本的不同 五、什么是终端六、在nodejs中执行js代码七、powe…

Kubernetes(K8S)各种攻击方法

1. 准备工作 1.1. metarget使用 项目地址(教程):https://github.com/Metarget/metarget/blob/master/README-zh.md 注意:推荐在Ubuntu 18.04(推荐)安装。 1.1.1. 安装metarget git clone https://github.com/Metarget/metarget.git cd metarget/ sudo apt install pyt…

[BUUCTF]-PWN:hitcon2014_stkof解析

又是一道堆题&#xff0c;先看保护 关键信息&#xff0c;64位&#xff0c;没开pie。再看ida 大致就是alloc创建堆块&#xff0c;free释放堆块&#xff0c;fill填充堆块内容&#xff0c;以及一个看起来没啥用的函数&#xff0c;当然我也没利用这个函数去解题 这里有两种解法 解…

【VB测绘程序设计】案例8——IF选择结构练习排序(附源代码)

【VB测绘程序设计】案例6——IF选择结构练习排序(附源代码) 文章目录 前言一、界面显示二、程序说明三、程序代码四、数据演示总结前言 本文主要掌握Val()函数转换,inputBox函数、IF条件句的练习,输入3个数,按大到小排序并打印。 一、界面显示 二、程序说明 利用inpu…

[BJDCTF2020]The mystery of ip

hint 猜测ip和XFF有关 加一个XFF 下面这一步是看了wp出来的&#xff1a;存在ssti 这里尝试用jinja的注入方法&#xff0c;页面回显了是php的smarty框架 查了一下smarty的注入方法&#xff0c;发现可以直接执行php命令 在根目录找到flag

RPC教程 6.负载均衡

1.负载均衡策略 假设有多个服务实例&#xff0c;而每个实例都提供相同的功能&#xff0c;为了提高整个系统的吞吐量&#xff0c;每个实例部署在不同的机器上。客户端可以选择任意一个实例进行调用&#xff0c;获取想要的结果。那如何选择呢&#xff1f;取决于负载均衡的策略。…

Visual Studio如何修改成英文版

1、打开 Visual Studio Installer 2、点击修改 3、找到语言包&#xff0c;选择需要的语言包&#xff0c;而后点击修改 4、等待下载 5、 安装完成后启动Visual Studio 6、在工具-->选项-->环境-->区域设置-->English并确定 7、重启 Visual Studio&#xff0c;配置…

数据结构与算法教程,数据结构C语言版教程!(第六部分、数据结构树,树存储结构详解)三

第六部分、数据结构树&#xff0c;树存储结构详解 数据结构的树存储结构&#xff0c;常用于存储逻辑关系为 "一对多" 的数据。 树存储结构中&#xff0c;最常用的还是二叉树&#xff0c;本章就二叉树的存储结构、二叉树的前序、中序、后序以及层次遍历、线索二叉树、…

QT实现USB摄像头接入显示

一、UVC协议简介 UVC全称是USB Video Class&#xff08;USB视频类&#xff09;&#xff0c;是一种标准化的USB视频设备通信协议&#xff0c;它定义了摄像头与主机之间的数据传输协议和格式。 UVC协议的出现&#xff0c;解决了摄像头厂商之间互不兼容&#xff0c;以及摄像头应…

(二十八)ATP应用测试平台——使用electron集成vue3桌面应用程序

前言 Electron 是一个开源的框架&#xff0c;它允许使用 Web 技术&#xff08;HTML、CSS 和 JavaScript&#xff09;构建跨平台的桌面应用程序。通过 Electron&#xff0c;开发者可以使用前端技术栈来创建具有原生应用程序体验的桌面应用。 Electron可以在 Windows、Mac 和 L…

【前端web入门第一天】02 HTML图片标签 超链接标签 音频标签 视频标签

文章目录: 1.HTML图片标签 1.1 图像标签-基本使用1.2 图像标签-属性1.3 路径 1.3.1 相对路径 1.3.2 绝对路径 2.超链接标签 3.音频标签 4.视频标签 1.HTML图片标签 1.1 图像标签-基本使用 作用:在网页中插入图片。 <img src"图片的URL">src用于指定图像…

HCS 华为云Stack产品组件

HCS 华为云Stack产品组件 Cloud Provisioning Service(CPS) 负责laas的云平台层的部署和升级是laas层中真正面向硬件设备&#xff0c;并将其池化软件化的部件。 Service OM 资源池(计算/存储/网络)以及基础云服务(ECS/EVS/PC)的管理工具。 ManageOne ManageOne包括服务中心…

数据结构(1)--> 顺序表

定义&#xff1a; 顺序表存储定义&#xff1a; 把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构&#xff0c;顺序表功能的实现借助于数组&#xff0c;通过对数组进行封装&#xff0c;从而实现增删查改的功能&#xff0c;严格意义上来说&#xff08;数组无法实现…