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()) } ?: 0
            else -> 0
        }
    }
    override fun calcHeightInPixels(inlay: Inlay<*>): Int {
        // 获取渲染内容的高度, 如果是多行文本则需要将行高乘以行数
        return when (renderText) {
            is MutableList<*> -> super.calcHeightInPixels(inlay) * renderText.size
            else -> super.calcHeightInPixels(inlay)
        }
    }
    override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {
        val g2 = g.create() as Graphics2D
        GraphicsUtil.setupAAPainting(g2)
        textAttributes.foregroundColor = JBColor.GRAY
        val lineHeight = editor.lineHeight.toDouble()
        val fontBaseline = ceil(font.createGlyphVector(getFontMetrics().fontRenderContext, "中文").visualBounds.height)
        val linePadding = (lineHeight - fontBaseline) / 2.0
        val offsetX = targetRegion.x
        val offsetY = targetRegion.y + fontBaseline + linePadding
        val lineOffset = 0
        g2.font = font
        g2.color = JBColor.GRAY
        when (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?) {
        // 将选中的文本加入到粘贴板中, 这里使用前文中提到的 selectionModel
        editor.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 的内容。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[GYCTF2020]Ezsqli1

打开环境&#xff0c;下面有个提交表单 提交1&#xff0c;2有正确的查询结果&#xff0c;3以后都显示Error Occured When Fetch Result. 题目是sql&#xff0c;应该考察的是sql注入 简单fuzz一下 发现information_schema被过滤了&#xff0c;猜测是盲注了。 测试发现只要有东…

Qt : Style Sheet

When a style sheet is active, the QStyle returned by QWidget::style() is a wrapper “style sheet” style, not the platform-specific style. The wrapper style ensures that any active style sheet is respected and otherwise forwards the drawing operations to t…

Linux 系统相关的命令

目录 一. 系统用户相关1.1 查看当前访问的主机和用户1.2 切换用户1.2.1 设置root用户密码1.2.2 普通用户和root用户切换 1.4 系统状态1.4.1 vmstat 查看当前系统的状态1.4.2 history 查看系统中输入过的命令 二. 系统文件相关2.1 权限修改2.2 磁盘占用2.2.1 每秒钟监视当前磁盘…

在 VUE 项目中,使用 Axios 请求数据时,提示跨域,该怎么解决?

在 VUE 项目开发时&#xff0c;遇到个问题&#xff0c;正常设置使用 Axios 库请求数据时&#xff0c;报错提示跨域问题。 那在生产坏境下&#xff0c;该去怎么解决呢&#xff1f; 其可以通过以下几种方式去尝试解决&#xff1a; 1、设置允许跨域请求的响应头 1.1 在响应头中…

LINUX基础培训十九之常见服务nfs介绍

前言、本章学习目标 了解nfs服务用途掌握nfs服务器的配置掌握nfs客户端的配置使用 一、NFS简介 NFS&#xff08;Network File System&#xff09;即网络文件系统&#xff0c;它允许网络中的计算机之间通过TCP/IP网络共享资源。在NFS的应用中&#xff0c;本地NFS的客户端应用…

机器学习第一个项目-----鸢尾花数据集加载及报错解决

项目步骤 如刚开始做&#xff0c;从 “项目开始” 看&#xff1b; 如遇到问题从 “问题” 开始看&#xff1b; 问题 报错如下 ModuleNotFoundError: No module named sklearn解决过程 查看官网&#xff0c;感觉可能是python版本和skilearn版本不匹配&#xff0c;更新一下p…

使用vue_cli脚手架创建Vue项目(cmd和图形化方式)

使用vue_cli脚手架创建Vue项目&#xff08;cmd和图形化方式&#xff09; 创建项目(cmd方式) vue create vue_cli1.方向键选择manually select feature(手动选择方式创建)&#xff0c;回车 2.按空格键选择需要的组件&#xff1a;Babel、PWA、Router、Vuex、CSS&#xff0c;回…

【GitHub项目推荐--游戏模拟器(switch)】【转载】

01 任天堂模拟器 yuzu 是 GitHub 上斩获 Star 最多的开源 Nintendo Switch 模拟器 &#xff0c;使用 C 编写&#xff0c;考虑到了可移植性&#xff0c;该模拟器包括 Windows 和 Linux 端。 如果你的 PC 满足必要的硬件要求&#xff0c;该模拟器就能够运行大多数商业游戏&…

Django实战

一、开发登录表单 def login_form(request):html <html><body><form method"post">用户名:<input name "username" type"text"></input></br>密码&#xff1a;<input name "password" type…

破解Windows系统密码(保姆级教学)

前言: 本篇博客只是技术分享并非非法传播知识,实验内容均是在虚拟机中进行,并非真实环境 正文: 看到题目大家都已经晓得这篇博客是干嘛了,我也不废话了,直接上win7素材 需要windows10破解过程的关注后在下面评论"已关注,请私聊"我会私发给你 一.windows7电脑密码破解…

C++17中lambda表达式新增加支持的features

C17中对lambda表达式新增加了2种features&#xff1a;lambda capture of *this和constexpr lambda 1.lambda capture of *this: *this:拷贝当前对象,创建副本&#xff1a;捕获*this意味着该lambda生成的闭包将存储当前对象的一份拷贝 。 this:通过引用捕获。 当你需…

C语言-指针的基本知识(下)

四、指针的分类 按指针指向的数据的类型来分 1:字符指针 字符型数据的地址 char *p;//定义了一个字符指针变量&#xff0c;只能存放字符型数据的地址编号 char ch; p &ch; 2&#xff1a;短整型指针 short int *p;//定义了一个短整型的指针变量p&#xff0c…

[Raspberry Pi]如何利用ssh將樹莓派切換連接至陌生的wifi基地台?

當已習慣使用VNC遠端控制樹莓派後&#xff0c;原用來設定樹莓派的電腦螢幕和鍵盤也逐漸挪為它用。此次攜帶樹莓派外出&#xff0c;同時又希望使樹莓派連接當地的wifi AP&#xff0c;利用VNC遠端桌面切換新的wifi AP需要重新設定wifi密碼&#xff0c;但卻無法在VNC遠端桌面看到密…

响应式Web开发项目教程(HTML5+CSS3+Bootstrap)第2版 例5-2 JavaScript 获取HTML元素对象

代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>JavaScript 获取 HTML 元素对象</title> </head><body> <input type"text" value"admin" /> <br> <input …

代码随想录算法训练DAY29|回溯5

算法训练DAY29|回溯5 491.递增子序列 力扣题目链接 给定一个整型数组, 你的任务是找到所有该数组的递增子序列&#xff0c;递增子序列的长度至少是2。 示例: 输入: [4, 6, 7, 7] 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]] 说…

canvas绘制旋转的大风车

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

Android Handler完全解读

一&#xff0c;概述 Handler在Android中比较基础&#xff0c;本文笔者将对此机制做一个完全解读。读者可简单参考上述类图与时序图&#xff0c;便于后续理解。 二&#xff0c;源码解读 1&#xff0c;主线程伊始 众所周知&#xff0c;通过Zygote的fork方式&#xff0c;新创建…

Unity应用在车机上启动有概率黑屏的解决方案

问题描述 最近将游戏适配到车机上&#xff08;Android系统&#xff09;&#xff0c;碰到了一个严重bug&#xff0c;启动的时候有概率会遇到黑屏&#xff0c;表现就是全黑&#xff0c;无法进入Unity的场景。 经过查看LogCat日志&#xff0c;也没有任何报错&#xff0c;也没有任…

DLL劫持之IAT类型(Loadlibrary)

Loadlibrary Loadlibrary的底层是LoadLibraryEx 第三个参数&#xff1a; DONT_RESOLVE_DLL_REFERENCES : 这个标志用于告诉系统将DLL映射到调用进程的地址空间中&#xff0c;但是不调用DllMain并且不加载依赖Dll&#xff08;只映射自己本身&#xff09;。 LOAD_LIBRARY_AS_DA…

LVGL v9学习笔记 | 12 - 弧形控件的使用方法(arc)

一、arc控件 arc控件的API在lvgl/src/widgets/arc/lv_arc.h 中声明,以lv_arc_xxx命名。 arc控件由背景圆弧和前景圆弧组成,前景圆弧的末端有一个旋钮,前景圆弧可以被触摸调节。 1. 创建arc对象 /*** Create an arc object* @param parent pointer to an object, it w…