如何实现基于Electron的截图识字App(一)

3,498 阅读6分钟

背景

之前看了下face++的接口,做了人脸融合简单demo,顺便瞧了瞧其他的接口,看到一个识别图片文字的接口,突然灵机一动。

平时自己看到一些有趣的图文,里面的文字就想敲下来放印象笔记里或者朋友圈里装一下文艺。但是关键这里还要手动敲,如果截图的时候,直接就把文字复制到剪贴板就好了。于是就有了这个应用,实现并不复杂。大家跟我一起过一遍吧,喜欢的可以star~

开始

初始化一个electron应用

参考electron官网,打造你的第一个electron应用

$ yarn add electron -D

目录结构

├─ src
│  └─ main.js    #入口文件,主进程
│  └─ index.html # 渲染进程的页面

package.json的scripts字段添加以下

"start": "electron src/main.js"

入口文件

electron进程分为主进程和渲染进程。渲染进程相当于前端的UI渲染,可以类比成chrome的一个tab页,一个tab就是一个进程。渲染进程由主进程管理,主进程相当于chrome的窗口,关闭所有tab页面还在的那个窗口。有些功能模块只能由主进程来调用,渲染进程同理(比如截屏这个就只能在渲染进程)

我们的需求是

  • 只需要一个托盘叫做tray(mac右上角,win右下角),不需要打开具体的窗口界面

  • CmdOrCtrl+Shift+V为截图快捷键
const { app, BrowserWindow,  globalShortcut ,Tray,Menu,ipcMain} = require('electron')

const shell = require('electron').shell;

const path=require('path')

let win

let srcPath=path.join(__dirname,"../src")

let clip=true
//创建托盘
function createTray () {
    tray = new Tray(`${srcPath}/images/font.png`) // 指定图片的路径,在github里有
    const contextMenu = Menu.buildFromTemplate([ //Menu类型有checkbox,radio,normal等
        { label: 'clip', type: 'checkbox',click(){
            clip=!clip
        },checked:true },
        { label: 'about', click(){
            //打开默认浏览器
            shell.openExternal('https://github.com/yokiyokiyoki/clip-font-app');
        } },
        { label: 'exit',click(){
            app.quit()
        }}
    ])
    tray.setToolTip('图图识字')
    tray.setContextMenu(contextMenu)
    //注册快捷键
    globalShortcut.register('CmdOrCtrl+Shift+V', captureScreen)
    globalShortcut.register('Esc', () => {
        if (win) {
            win.close()
            win = null
        }
    })
}

function createCaptureWindow() {
    // 创建浏览器窗口,只允许创建一个(必须得创建,因为只有渲染进程才能截图)
    if(win)return console.info('只能有一个CaptureWindow')
    const { screen } = require('electron') //因为ready才可以引入
    let { width, height } = screen.getPrimaryDisplay().bounds
    win = new BrowserWindow({ 
        fullscreen: process.platform !== 'darwin' || undefined, // win
        width,
        height,
        x: 0,
        y: 0,
        transparent: true,
        frame: false,
        skipTaskbar: true,
        autoHideMenuBar: true,
        movable: false,
        resizable: false,
        enableLargerThanScreen: true, // mac
        hasShadow: false,
        webPreferences: {
            webSecurity: false //可以加载本地文件,这里不写的话,打包后会报错:不允许你加载本地文件
        }
    })

    win.setAlwaysOnTop(true, 'screen-saver') // mac
    win.setVisibleOnAllWorkspaces(true) // mac
    win.setFullScreenable(false) // mac

    // 然后加载应用的 index.html。
    win.loadFile(path.join(__dirname,'../index.html'))

    // 打开开发者工具
    win.webContents.openDevTools()

    // 当 window 被关闭,这个事件会被触发。
    win.on('closed', () => {
        win = null
    })
}
app.on('ready', createTray)
// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
    // 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
    // 否则绝大部分应用及其菜单栏会保持激活。
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', () => {
    if (win === null) {
        createCaptureWindow()
    }
})

function captureScreen(){
    if(clip){
        createCaptureWindow()
    }
}

编写index.html

上面我们通过主进程loadfile打开了index.html。这里我们可以做一个粗浅的UI,需要有尺寸信息,工具栏等。是基于全屏的html,为什么要这么做?你可以思考一下~

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>截屏</title>
  </head>
  <style>
  html, body, div {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  .bg {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
  .mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.6);
  }
  .rect {
    position: absolute;
    display: node;
    z-index: 1;
}
.size-info {
    position: absolute;
    color: #ffffff;
    font-size: 12px;
    background: rgba(40, 40, 40, 0.8);
    padding: 5px 10px;
    border-radius: 2px;
    font-family: Arial Consolas sans-serif;
    display: none;
    z-index: 2;
}
.toolbar {
    position: absolute;
    color: #343434;
    font-size: 12px;
    background: #f5f5f5;
    padding: 5px 10px;
    border-radius: 4px;
    font-family: Arial Consolas sans-serif;
    display: none;
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
    z-index: 2;
    align-items: center;
}
.toolbar>.iconfont{
  display: inline-block;
  cursor: pointer;
}
  </style>
  <body>
    <!--背景为灰色的遮罩层-->
    <div class="bg"></div>
    <div id="mask" class="mask"></div>
    <canvas class="rect"></canvas>
    <!--尺寸信息-->
    <div class="size-info">200*200</div>
    <!--toolbar-->
    <div class="toolbar">
      <div class="iconfont icon-close" >关闭</div>
      <div class="iconfont icon-check" >确认</div>
      <div class="iconfont icon-literacy" >识别</div>
    </div>
    <script src="./src/js/capture.js"></script> <!--现在还没写-->
  </body>
</html>

至此我们已经可以通过npm start进行开发,通过快捷键CmdOrCtrl+Shift+V看到这个页面了,并且托盘也已经现在左上角。如果报错,请审视上述之流程。

截屏功能实现

我们把逻辑写在capture.js里面

electron 提供了截取屏幕的 API(desktopCapturer),可以轻松的获取每个屏幕(存在外接显示器的情况)和每个窗口的图像信息。可以先去了解一下~

原理(其实很简单,就是两个canvas):

1.通过electron提供截取全屏生成的dataURL,用drawImage存在一个特殊的canvas(姑且称之为全屏canvas)里面(dataURL和cavas的相爱相杀),再把这个dataURL赋给背景,让空白背景图,变成刚刚全屏的样子(假象),再加上半透明的遮罩。

2.通过另外一个canvas(选区canvas)来制作选区,鼠标位置来确定——getImageData(x,y,w,h),这个选区在全屏canvas里的imgData,然后把全屏canvas的数据导过来这个选区canvas——putImageData(imgData, x, y)

const {  desktopCapturer, screen } = require('electron')
const { bounds: { width, height } } = screen.getPrimaryDisplay()
const path=require('path')

const {Draw} = require(`${__dirname}/src/js/draw.js`)

desktopCapturer.getSources({
    types: ['screen'],
    thumbnailSize: {
        width, height
    }
}, async(error, sources) => {
    if (error) return console.log(error)
    let screenImgUrl = sources[0].thumbnail.toDataURL() //获取dataURL
    
    let bg=document.querySelector('.bg')
    let rect=document.querySelector('.rect')
    let sizeInfo=document.querySelector('.size-info')
    let toolbar=document.querySelector('.toolbar')
    /** 
     * 绘制类
     * ScreenImgUrl是整个屏幕base64格式的快照
     * bg是背景dom
     * width是屏幕宽高
     * rect是选区canvas
     * sizeInfo 尺寸信息容器
     * toolbar 工具栏
    */
    let draw=new Draw(screenImgUrl,bg,width,height,rect,sizeInfo,toolbar)
    document.addEventListener('mousedown',draw.startRect.bind(draw))
    document.addEventListener('mousemove',draw.drawingRect.bind(draw))
    document.addEventListener('mouseup',draw.endRect.bind(draw))
})

Draw类写了截图选区是如何实现的,这里实在有些长,可以移步github。主要就是通过鼠标画一个矩形,然后把全屏canvas的数据通过位置定位到具体,然后导入。

如果我们点击工具栏中的关闭,其实是需要销毁整个窗口的。这里涉及到了渲染进程和主进程间通信的问题,需要IPC模块——IPCMainipcRenderer ~

//渲染进程发送消息
ipcRenderer.send('clip-page', { type: type, message: msg })

//主进程接收
ipcMain.on('clip-page', (event, {type,msg}) => {
    if(type==='close'){
        if (win) {
            win.close()
            win = null
        }
    }
})

打包

我们在开发模式下试了下,觉得应该没有问题了。这时候来打包成各平台上的应用,供用户实际使用~

这里我们选用electron-builder,在package.json新建一个脚本命令,同时新增一个build字段(electron-builder自动读取)

"scripts": {
    "start": "electron src/main.js",
    "build": "electron-builder",
},
"build":{
    //名字
    "productName": "clip-font-app",
    "appId": "Personal.DesktopApp.ClipFont.1.0.0",
    "directories": {
        //打包目录
      "output": "dist"
    },
    "files": [
    //所有文件
      "./**/**"
    ],
    //win下的安装向导
    "nsis": {
      "oneClick": false,
      "allowElevation": true,
      "allowToChangeInstallationDirectory": true,
      "installerIcon": "src/images/icon.ico",
      "uninstallerIcon": "src/images/icon.ico",
      "installerHeaderIcon": "src/images/icon.ico",
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "ClipFont"
    },
    "dmg": {
      "contents": [
        {
          "x": 410,
          "y": 150,
          "type": "link",
          //是否拖到应用目录
          "path": "/Applications"
        },
        {
          "x": 130,
          "y": 150,
          "type": "file"
        }
      ]
    },
    "mac": {
      "icon": "src/images/icon.icns"
    },
    "win": {
      "icon": "src/images/icon.ico",
      "target": [
        {
          "target": "nsis",
          "arch": [
            "ia32"
          ]
        }
      ]
    },
    "linux": {
      "icon": "src/images"
    },
    //下载源为淘宝镜像,国内某些原因可能会导致失败
    "electronDownload": {
      "mirror": "http://npm.taobao.org/mirrors/electron/"
    }
}

然后我们npm run build,由于我使用的是mac,然后打包下来的是dmg~,双击dmg安装了。

效果

我们来使用快捷键(CmdOrCtrl+Shift+V),点击识别~

然后复制到剪贴板试试,效果还是可以的~

后续

当然一个截图工具的工具栏怎么会那么简单呢?

  • 工具栏完善,会包括下载等功能
  • 支持多窗口截屏
  • 拖拽节点