【前端】-【electron】

介绍

electron技术架构:chromium、node.js、native.apis
在这里插入图片描述

electron工作流程

在这里插入图片描述
桌面应用就是运行在不同操作系统上的软件,软件中的功能是通过native.apis跟不同操作系统进行交互实现的,想实现什么功能调用响应的API即可
electron主要有两类进程:

  1. 主进程:main process
  2. 渲染进程:renderer process

当我们启动一个APP时,他首先会启动主进程,一般是main.js或index.js中的代码,主进程启动成功后会创建一个native ui,然后在nativeui里创建一个或多个Browse window,用于呈现界面(即web界面),每个Browse window可以看作是一个渲染进程,每个渲染进程相互独立,各自运行在自己的沙箱环境中,但是app在使用的过程中,不同窗口会进行交互,于是electron提供了IPC、RPC通信机制供窗口与窗口进行数据传输
在这里插入图片描述
只有主进程能操作原生API,能管理所有WEB界面,和这些web界面对应的渲染进程
在这里插入图片描述
渲染进程支持所有的DOM操作、node api的调用操作

环境搭建

package.json中main字段指定的文件就是app启动后,启动的主进程;script字段指定的是启动项目的命令
在这里插入图片描述

electron生命周期(app的生命周期)

ready:app初始化完成后会被调用一次,一般用于窗口初始化
dom-ready:一个窗口中的文本加载完成,与dom操作相关,有个webcontext对象可以调用
did-finsh-load:导航完成时触发,即选项卡的旋转器停止旋转时触发,且指派了onload事件。由webcontext调用
window-all-closed:所有窗口都被关闭时触发,如果我们没有监听这个方法,那么所有窗口关闭后,应用程序就默认退出,如果我们监听了这个方法,我们就可以自己决定所有窗口关闭后,应用程序是否退出,如果我们选择不退出,那么后续的before-quit、will-quit、quit都失效了
before-quit:在关闭窗口之前触发
will-quit:在窗口关闭并且应用退出时触发
quit:所有窗口被关闭时触发
closed:当窗口关闭时触发,此时应删除窗口引用

const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
        width: 800,
        height:400
    })

    mainWindow.loadFile('index.html')

    // webContents用于操作dom
    mainWindow.webContents.on('did-finish-load',() => {
        console.log("33333------did-finish-load")
    })
    mainWindow.webContents.on('dom-ready',() => {
        console.log("22222------dom-ready")
    })

    mainWindow.on('close',() => {
        console.log("88888------close")
        mainWindow = null
    })
}

app.on('ready',() => {
    console.log("11111------ready")
    createWindow()
})

app.on('window-all-closed',() => {
    console.log("44444------window-all-closed")
})

app.on('before-quit',() => {
    console.log("55555------before-quit")
})

app.on('will-quit',() => {
    console.log("66666------will-quit")
})

app.on('quit',() => {
    console.log("77777------quit")
})

执行顺序:1——2——3——8——4——5——6——7

窗口尺寸

const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
        x:100,// 设置窗口显示的位置,距离左边的距离
        y:100,// 设置窗口显示的位置,距离右边的距离
        width: 800,
        height:400,
        maxHeight:600,// 窗口最大高度
        minHeight:200,// 窗口最小高度
        maxWidth:1000,// 窗口最大宽度
        minWidth:300,// 窗口最小宽度
        resizable:false// 是否允许缩放窗口,默认为true,可以缩放窗口,设为false
    })

    mainWindow.loadFile('index.html')

    mainWindow.on('close',() => {
        console.log("88888------close")
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    console.log("window-all-closed")
    app.quit()
})

注意此时有个bug:窗口弹出来,然后有一段白屏时间,然后再出现内容,这是因为let mainWindow = new BrowserWindow()执行完成后就会显示出窗口来,此时窗口里面是没有内容的,我们设置show:false,让窗口创建好也不展示出来,然后加载index.html文件,加载完成后,监听窗口的ready-to-show方法,再把窗口展示出来

const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
    	show:false,
        width: 800,
        height:400
    })

    mainWindow.loadFile('index.html')
    // 监听窗口已经准备好去展示了
	mainWindow.on('ready-to-show',()=>{
		mainWindow.show()
	})
    mainWindow.on('close',() => {
        console.log("88888------close")
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    console.log("window-all-closed")
    app.quit()
})

窗口标题

在这里插入图片描述

const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
    	show:false,
        width: 800,
        height:400,
        frame: false,// 是否有边框
        transparent:true,// 窗体透明,只有frame: false时才生效
        icon:'lg.ico',// 图标
        title:'拉钩教育',// 窗口标题
        autoHideMenuBar: true// 隐藏菜单、选项卡
    })

    mainWindow.loadFile('index.html')
    // 监听窗口已经准备好去展示了
	mainWindow.on('ready-to-show',()=>{
		mainWindow.show()
	})
    mainWindow.on('close',() => {
        console.log("88888------close")
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    console.log("window-all-closed")
    app.quit()
})

需求:窗口里面提供一个按钮,点击按钮,再弹出一个窗口

ctr+r可以对应用进行重载
25版本后可以使用ipcMain和ipcRenderer实现新窗口

index.html代码如下:
在这里插入图片描述
版本一:主进程允许渲染进程使用BrowserWindow实现,main.js代码如下:

const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
    	show:false,
        width: 800,
        height:400,
        webPreferences:{
            nodeIntegration:true,// 允许渲染进程使用node集成环境
            contextIsolation: false,// 如果nodeIntegration:true失效,那么需要添加这行代码 
        }
    })

    mainWindow.loadFile('index.html')
    // 监听窗口已经准备好去展示了
	mainWindow.on('ready-to-show',()=>{
		mainWindow.show()
	})
    mainWindow.on('close',() => {
        console.log("88888------close")
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    console.log("window-all-closed")
    app.quit()
})

index.js代码(index.html的js代码)如下:
在这里插入图片描述
版本二:主进程允许渲染进程使用remote实现,main.js代码如下:

const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
    	show:false,
        width: 800,
        height:400,
        webPreferences:{
            enableRemoteModule:true//允许使用远程模式
        },
        autoHideMenuBar: true// 隐藏菜单、选项卡
    })

    mainWindow.loadFile('index.html')
    // 监听窗口已经准备好去展示了
	mainWindow.on('ready-to-show',()=>{
		mainWindow.show()
	})
    mainWindow.on('close',() => {
        console.log("88888------close")
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    console.log("window-all-closed")
    app.quit()
})

index.js代码(index.html的js代码)如下:

const { remote } = require('electron')
window.addEventListener('DOMContentLoaded',() => {
    // 点击按钮打开一个新窗口
    const oBtn = document.getElementsById('btn')
    oBtn.addEventListener('click',() => {
        // 创建窗口
        let indexWindow = new remote.BrowserWindow({
            width:200,
            height:200
        })
        indexWindow.loadFile('sub.html')
        indexWindow.on('close',() => {
            indexWindow = null
        })
    })
})

自定义窗口的实现

需求:点击窗口右上角的三个图标,分别执行相应操作
在这里插入图片描述
index.html代码如下,红框中为三个图标的代码
在这里插入图片描述
main.js中代码如下:

const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
    	show:false,
        width: 800,
        height:400,
        webPreferences:{
            nodeIntegration:true,// 允许渲染进程使用node集成环境
            contextIsolation: false,// 如果nodeIntegration:true失效,那么需要添加这行代码
            enableRemoteModule:true//允许使用远程模式
            
        },
        autoHideMenuBar: true// 隐藏菜单、选项卡
    })

    mainWindow.loadFile('index.html')
    // 监听窗口已经准备好去展示了
	mainWindow.on('ready-to-show',()=>{
		mainWindow.show()
	})
    mainWindow.on('close',() => {
        console.log("88888------close")
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    console.log("window-all-closed")
    app.quit()
})

index.html的js代码写在index.js中,如下:

const { remote } = require('electron')
window.addEventListener('DOMContentLoaded',() => {
    // 利用remote可以获取当前窗口对象
    let mainWindow = remote.getCurrentWindow()
    // 获取元素添加点击操作的监听
    let aBtn = document.getElementsByClassName('windowTool')[0].getElementsByTagName('div')

    aBtn[0].addEventListener('click',() => {
        // 关闭窗口
        mainWindow.close()
    })

    aBtn[1].addEventListener('click',() => {
        // 最大化
        console.log(mainWindow.isMaximized())
        // 先判断当前窗口是不是最大化,如果是,则回到原来的大小,如果不是最大化,则最大化当前窗口
        if(!mainWindow.isMaximized()){
            mainWindow.maximize()// 让当前窗口最大化
        }else{
            mainWindow.restore()// 让当前窗口回到原来的大小
        }
    })

    aBtn[2].addEventListener('click',() => {
        // 最小化
        console.log(mainWindow.isMinimized())
        if(!mainWindow.isMaximized()){
            mainWindow.minimize()// 让当前窗口最小化
        }
    })
})

阻止窗口关闭

点击右上角的关闭按钮后,弹出浮窗提示用户是否关闭,若用户选择关闭,则关闭应用,否则关闭浮窗,index.html页面如下:
在这里插入图片描述
index.html中的js代码保存在index.js中,代码如下:

const { remote } = require('electron')
window.addEventListener('DOMContentLoaded',() => {

    // 监听mainWindow.close()
    window.onbeforeunload = function(){
        let oBox = document.getElementsByClassName('isClose')[0]
        oBox.style.display = 'block'// 弹窗出现
        let yseBtn = oBox.getElementsByTagName('span')[0]
        let noBtn = oBox.getElementsByTagName('span')[1]
        yseBtn.addEventListener('click', () => {
            // 此时关闭窗口需要用destory,
            // 因为onbeforeunload用于监听mainWindow.close(),如果这里还用mainWindow.close关闭的话,会陷入死循环
            mainWindow.destory()
        })
        noBtn.addEventListener('click', () => {
            oBox.style.display = 'none'// 去掉弹窗
        })
    }
    // 利用remote可以获取当前窗口对象
    let mainWindow = remote.getCurrentWindow()
    // 获取元素添加点击操作的监听
    let aBtn = document.getElementsByClassName('windowTool')[0].getElementsByTagName('div')

    aBtn[0].addEventListener('click',() => {
        // 关闭窗口
        mainWindow.close()
    })

    aBtn[1].addEventListener('click',() => {
        // 最大化
        console.log(mainWindow.isMaximized())
        // 先判断当前窗口是不是最大化,如果是,则回到原来的大小,如果不是最大化,则最大化当前窗口
        if(!mainWindow.isMaximized()){
            mainWindow.maximize()// 让当前窗口最大化
        }else{
            mainWindow.restore()// 让当前窗口回到原来的大小
        }
    })

    aBtn[2].addEventListener('click',() => {
        // 最小化
        console.log(mainWindow.isMinimized())
        if(!mainWindow.isMaximized()){
            mainWindow.minimize()// 让当前窗口最小化
        }
    })
})

父子及模态窗口

在这里插入图片描述
index.js代码如下:

const { remote } = require('electron')
window.addEventListener('DOMContentLoaded',() => {
    // 点击按钮打开一个新窗口
    const oBtn = document.getElementsById('btn')
    oBtn.addEventListener('click',() => {
        // 创建窗口
        let indexWindow = new remote.BrowserWindow({
            parent:remote.getCurrentWindow(),// 指定子窗口的父窗口是主线程的窗口
            modal:true,// 子窗口是模态化窗口,当子窗口出现时,父窗口不能游任何操作,也不能移动
            width:200,
            height:200
        })
        indexWindow.loadFile('sub.html')
        indexWindow.on('close',() => {
            indexWindow = null
        })
    })
})

自定义菜单

main.js中代码如下:

// 1. 导入menu模块
const { app, BrowserWindow,Menu } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
        title:"自定义菜单",
    	show:false,
        width: 800,
        height:400,
        webPreferences:{
            nodeIntegration:true,// 允许渲染进程使用node集成环境
            enableRemoteModule:true//允许使用远程模式
        },
    })

    // 2. 定义自己需要的菜单项
    let menuTemp = [
        {
            label:"文件",// 一级菜单
            submenu:[// 二级菜单
                {
                    label:"打开文件",
                    click(){// 点击该菜单要执行的逻辑
                        console.log("111111")
                    }
                },
                {type:"separator"},// 对二级菜单进行分类的分隔符
                {label:"关闭文件",},
                {
                    label:"关于",
                    role:"about"// 内部集成了一些功能(如复制、粘贴等),可通过role指定相应的值来使用
                },
            ]
        },
        {label:"编辑"}
    ]

    // 3. 利用上述模板生成一个菜单项
    let menu = Menu.buildFromTemplate(menuTemp);

    // 4. 将上述自定义菜单添加到应用里
    Menu.setApplicationMenu(menu);

    mainWindow.loadFile('index.html')
    // 监听窗口已经准备好去展示了
	mainWindow.on('ready-to-show',()=>{
		mainWindow.show()
	})
    mainWindow.on('close',() => {
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    app.quit()
})

electron中针对不同的操作系统调用的API可能不一样,通过process.platform可以查看当前操作系统

菜单角色及类型

// 1. 导入menu模块
const { app, BrowserWindow,Menu } = require('electron')

// 创建窗口
function createWindow() {
    let mainWindow = new BrowserWindow({
        title:"自定义菜单",
    	show:false,
        width: 800,
        height:400,
        webPreferences:{
            nodeIntegration:true,// 允许渲染进程使用node集成环境
            enableRemoteModule:true//允许使用远程模式
        },
    })

    // 2. 定义自己需要的菜单项
    let menuTemp = [
        {
            label:"角色",// 一级菜单
            submenu:[// 二级菜单
                {
                    label:"复制",
                    role:"copy"
                },
                {
                    label:"粘贴",
                    role:"copy"
                },
                {
                    label:"剪切",
                    role:"paste"
                },
                {
                    label:"最小化",
                    role:"minimize"
                },
            ]
        },
        {
            label:"类型",
            submenu:[// 二级菜单
                {
                    label:"选项1",
                    type:"checkbox"
                },
                {
                    label:"选项2",
                    type:"checkbox"
                },
                {
                    label:"选项3",
                    type:"checkbox"
                },
                {type:"separator"},
                {
                    label:"item1",
                    role:"radio"
                },
                {
                    label:"item2",
                    role:"radio"
                },
                {type:"separator"},
                {
                    label:"windows",
                    type:"submenu",
                    role:"windowMenu"
                },
            ]
        },
        {
            label:"其他",
            submenu:[
                {
                    label:"打开",
                    icon:"./open.png",// 设置图标
                    accelerator:"ctrl+o",// 设置快捷键
                    click(){
                        console.log("open操作执行了!!")
                    }
                }
            ]
        }
    ]

    // 3. 利用上述模板生成一个菜单项
    let menu = Menu.buildFromTemplate(menuTemp);

    // 4. 将上述自定义菜单添加到应用里
    Menu.setApplicationMenu(menu);

    mainWindow.loadFile('index.html')
    // 监听窗口已经准备好去展示了
	mainWindow.on('ready-to-show',()=>{
		mainWindow.show()
	})
    mainWindow.on('close',() => {
        mainWindow = null
    })
}

app.on('ready',createWindow)

app.on('window-all-closed',() => {
    app.quit()
})

role:"windowMenu"如下:
在这里插入图片描述
icon:"./open.png",设置图标如下:
在这里插入图片描述

动态创建菜单

实现功能:

  1. 点击创建自定义菜单,将原来的菜单替换为新的自定义菜单
  2. 输入自定义菜单项内容后,点击添加菜单项,将输入的自定义菜单项添加在菜单栏中

index.html代码如下:
在这里插入图片描述
index.js(渲染进程,所以主进程main.js中创建窗口时要开启:enableRemoteModule:true)代码如下:

const { remote } = require('electron');
const Menu = remote.Menu;
const MenuItem = remote.MenuItem;

window.addEventListener('DOMContentLoaded',()=>{
    // 获取相应的元素
    let addMenu = document.getElementById("addMneu");
    let menuCon = document.getElementById("menuCon");
    let addItem = document.getElementById("addItem");

    // 自定义全局变量存放菜单项
    let menuItem = new Menu();
    // 生成自定义的菜单
    addMenu.addEventListener("click",()=>{
        // 创建菜单
        let menuFile = new MenuItem({label:"文件",type:"normal"});
        let menuEdit = new MenuItem({label:"编辑",type:"normal"});
        let customMenu = new MenuItem({label:"自定义菜单项",type:"normal"}); 
        // 将创建好的自定义菜单添加至menu
        let menu = new Menu();
        menu.append(menuFile);
        menu.append(menuEdit);
        menu.append(customMenu);
        // 将menu放置于app中显示
        Menu.setApplicationMenu(menu);
    })

    // 动态添加菜单项
    addItem.addEventListener("click",()=>{
        // 获取当前input输入框当中的内容
        let con = menuCon.value.trim();
        if(con){
            menuItem.append(new MenuItem(label:con,type:"normal"))
            menuCon.value="";// 清空输入框的内容
        }
    })
})

自定义右键菜单

实现功能:右击窗口,弹出自定义菜单:
在这里插入图片描述
index.html代码如下:
在这里插入图片描述
index.js代码如下:

const { remote } = require('electron');
const Menu = remote.Menu;

// 定义菜单的内容
let contextTemp = [
    {label:"run code"},
    {label:"转到定义"},
    {type:"separator"},
    {
        label:"其他功能",
        click(){
            console.log("其他功能选项被点击了!!")
        }
    }
]

// 依据上述内容来创建menu
let menu = Menu.buildFromTemplate(contextTemp)
// 鼠标右击监听
window.addEventListener("DOMContentLoaded",()=>{
    window.addEventListener("contextmenu",(ev)=>{
        ev.preventDefault();// 阻止原生事件
        menu.popup({// 弹出窗口
            window: remote.getCurrentWindow()// 配置具体弹出哪个窗口,remote.getCurrentWindow()获取当前远程窗口
        },false)
    })
})

在窗口中打开开发者工具可以看到控制台输出了:其他功能选项被点击了!!

主进程与渲染进程

主进程与渲染进程间通信

一、实现:渲染进程发送消息触发主进程的行为
index.html代码如下:
在这里插入图片描述
主进程使用ipcMain.on("事件名",监听到事件后执行的方法)监听渲染进程发送的消息,main.js代码如下:
请添加图片描述
渲染进程使用ipcRenderer.send("事件名",参数)向主进程发送一条【异步】消息
请添加图片描述
渲染进程使用ipcRenderer.sendSync("事件名",参数)向主进程发送一条【同步】消息,因为是同步消息,所以渲染进程会等到主进程返回后再执行之后的代码
请添加图片描述
主进程使用ev.returnValue向渲染进程返回信息

主进程使用ipcMain.send("事件名",参数)或者e v.sender.send("事件名",参数)触发渲染进程的事件

请添加图片描述
二、实现:主进程发送消息触发渲染进程的行为
在主进程中自定义一个菜单并替换掉原有的菜单:
请添加图片描述
每个窗口都有个webContents,用于控制当前窗口的所有内容。主进程中使用BrowserWindow.getfovusedWindow获取正在显示的渲染进程窗口,BrowserWindow.getfovusedWindow.webContents获取正在显示的渲染进程窗口的所有内容,BrowserWindow.getfovusedWindow.webContents.send("事件名",参数)主进程向渲染进程发送一个事件

渲染进程中使用getCurrentWindow用于获取正在显示的渲染进程窗口

渲染进程使用ipcRenderer.on("事件名",接收到事件后执行的方法)监听事件
请添加图片描述
请添加图片描述

渲染进程间通信

不同进程运行在不同的沙箱环境中,想要通信可以借助主进程,也可以把数据放在公共可以访问的地方,如localStorage
请添加图片描述
一、实现:渲染进程1向主进程发送一个事件并将数据保存在localStorage中,主进程接收到后,打开窗口2(渲染进程2),在窗口2中使用localStorage中的数据。
渲染进程1的代码index.js:
请添加图片描述
主进程main.js代码:
请添加图片描述
渲染进程2的html代码subWin1.html:
请添加图片描述
渲染进程2的代码subWin1.js:
请添加图片描述
现在有个问题:subWin1.html与主窗口不相关,关闭主窗口subWin1.html并不会关闭,这是因为subWin1.html不是主窗口的子窗口,当然也可以把subWin1.html做成模态窗口(此处展示将subWin1.html做成子窗口)。在主进程定义一个变量mainId,用于存放主窗口的id(每个窗口都有一个唯一的id属性),在主窗口创建subWin1时,为其parent属性指定其父窗口的id,即主窗口的id
请添加图片描述
请添加图片描述
请添加图片描述

BrowserWindow.fromId("窗口id")通过窗口id获取窗口,类似于document.getElementById

基于本地存储的渲染进程通信

一、实现:渲染进程1向主进程发送一个事件,主进程接收到后,打开窗口2(渲染进程2),在窗口2中发送数据给渲染进程1。
窗口2中先新增一个按钮
请添加图片描述
窗口2(subWin1.js)使用ipcRenderer.send("事件名",参数)发送事件给主进程
请添加图片描述
主进程(main.js)使用ipcMain.on("事件名",接收到事件后执行的方法)接收事件,并根据窗口id获得窗口1的id,然后使用该窗口的webContents.send("事件名",参数)发送事件
请添加图片描述
窗口1接收事件
请添加图片描述
二、实现:窗口1通过主进程发送数据给渲染进程2
窗口1(index.js)使用ipcRenderer.send("事件名",参数)发送事件给主进程
请添加图片描述
因为窗口2是主进程在接收到窗口1的事件后创建的(见前述),所以主进程在创建窗口2的时候,就可以获得窗口2的实例,主进程直接监听窗口2加载完成的事件,监听到了就向窗口2发送事件和数据
请添加图片描述
窗口2接收事件
请添加图片描述

dialog模块

实现:点击窗口按钮,弹出dialog。index.html中新增两个按钮,代码如下:
请添加图片描述
dialog属性详见官方文档

  1. title:dialog标题
  2. properties:dialog相关属性,如openFile选择文件、openDirectory选择文件夹、multiSelections多选
  3. filter:指定一个文件类型数组,用于规定用户可见或可选的特定类型范围。dialog右下角可选项(见本节最后一张图)

请添加图片描述
同步dialog和异步dialog属性一样,异步dialog返回一个promise,所以我们可以使用.then().catch()捕捉他
请添加图片描述
请添加图片描述

shell与iframe

一、shell:采用默认应用(通过调用不同操作系统)管理文件、文件夹、URL。

webview标签基于chrome浏览器内核,该内核的架构还在变化,所以webview不稳定,建议使用iframe代替webview

二、实现:shell操作文件夹、URL。
在index.html中添加两个按钮
请添加图片描述
使用shell.openExternal(url)打开URL
请添加图片描述
引入path,使用shell.showItemInFolder(文件夹路径)打开文件夹
请添加图片描述
请添加图片描述
三、实现:使用iframe实现窗口中加入原生界面。index.html中代码:
请添加图片描述
样式:
请添加图片描述

消息通知

一、应用的消息通知:在使用应用的时候,经常会在操作系统右下角弹出应用给出的消息通知,在electron中,可以借助h5的notification来实现。index.html中新增一个按钮,点击后右下角弹出消息通知,代码:
请添加图片描述
main.js中使用window.notification(标题,配置项对象)弹出消息通知,代码:
请添加图片描述

全局快捷键

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值