electron: 你用过电脑自带的放大镜吗?用我来实现一个吧

1,118 阅读8分钟

前言

前段时间使用electron做项目,写了一篇使用electron的总结,里面简单的记录了如何新建一个electron+vue项目、主进程与渲染进程是什么,它们如何通信如何制作悬浮小球等,比较笼统简介,本篇文章将详细讲述如何实现一个桌面放大镜的步骤。

效果展示

点击放大镜按钮,会打开放大镜功能,桌面上出现一个矩形框充当放大镜,屏幕桌面的内容在该矩形框范围内的部分会被放大显示,且该矩形框支持放大缩小、支持开灯关灯,点击关灯则矩形框范围外的桌面显示黑屏

20211016_121057.gif

代码实现

该功能实现代码基于electron+vue方式编写,需要动手品尝,请先搭建好基础项目,搭建方式可前往electron+vue开发项目总结阅读项目搭建步骤

实现原理

使用electron实现一个系统桌面放大镜,其本质上还是我们对于javascript、canvas的掌握熟练度,结合electron部分进程api完成。

  • 首先通过electron提供的desktopCapturer接口获取全屏截图,
  • 将全屏截图使用canvas绘制图像
  • 绘制矩形框,定义鼠标事件onMouseDown、onMouseMove、onMouseUp
  • 根据鼠标的动作,实施计算矩形框的位置、大小等属性,绘制选框内图形

第一步:DOM结构及CSS样式

html结构只有两部分部分:canvas画布、操作按钮组。

  • canvas画布设置为脱离文档流且隐藏,之后js控制其随着鼠标的移动在屏幕中移动
  • 操作按钮组也设置为脱离文档流且隐藏,之后js控制其定位在canva画布底部居中位置

html结构代码

<div class="micro-wrap">
    <canvas id="canvas" class="canvas"></canvas>
    <div class="operate">
      <button id="close">关灯</button>
      <button id="close">开灯</button>
    </div>
</div>

css样式代码

.micro-wrap{
  width:100%;
  height:100%;
  position:relative;
  .canvas{
    display:none;
    position:absolute;
    left:0;
    top:0;
    z-index:999;
  }
  .operate{
    display:none;
    position:absolute;
    left:50%;
    transform:translateX(-50%);
    top:10px;
    z-index:999;
    button{
      width:100px;
      height:30px;
      font-size:12px;
      background:#fff;
      border:1px solid #6249EE;
      color:#6249EE;
      cursor:pointer;
    }
  }
}

第二步:获取dom元素、获取全屏截图图片

获取上述两部分部分DOM元素,之后会将它传递到CaptureEditor中使用到。

获取全屏截图,electron提供desktopCapturer接口,在此之前,需要获得屏幕大小属性将其作为getSources方法的参数值

  1. 首先我们来获取屏幕截图生成base64格式
const { desktopCapturer,remote } = window.require('electron')
const remoteScreen = remote.screen
const getSize = function(){
  const { size, scaleFactor } = remoteScreen.getPrimaryDisplay()
  return {
    width: size.width * scaleFactor,
    height: size.height * scaleFactor
  }
}


setup(){
    onMounted(async () => {
      const sources = await desktopCapturer.getSources({
        types: ['window', 'screen'],
        thumbnailSize: getSize()
      })
      const imgSrc = sources[0].thumbnail.toDataURL()
      console.log(imgSrc)    // 屏幕截图 base64的格式字符串
    })
}
  1. 获取dom元素
setup(){
    onMounted(async () => {
        const canvas = document.getElementById('canvas')
        const operate = document.querySelector('.operate')
    })
}

第三步:创建microGlass.js文件编写CaptureEditor类

实现本篇演示效果,这一步是核心所在,下文会比较长,请耐心阅读

先把CaptureEditor类的结构整理出来

class CaptureEditor{
    constructor($canvas,imgSrc,sizeInfo){
        
        // 绑定鼠标事件
        this.onMouseDown = this.onMouseDown.bind(this)
        this.onMouseMove = this.onMouseMove.bind(this)
        this.onMouseUp = this.onMouseUp.bind(this)
        this.init()
    }
    // 初始化方法
    init(){
        
        // 监听事件
        document.addEventListener('mousedown', this.onMouseDown)
        document.addEventListener('mousemove', this.onMouseMove)
        document.addEventListener('mouseup', this.onMouseUp)
    }
    /*鼠标事件组*/
    onMouseDown(e){}
    onMouseMove(e){}
    onMouseUp(e){}
    // 拖拽鼠标触发
    onDrag(e){}
    //创建canvas副本-绘制屏幕截图图像
    generateCanvasImg(){}
    // 生成透明遮罩
    generateMaskDom(){}
    // 绘制表示放大镜的矩形框
    drawRect(){}
    // 绘制矩形四个角的锚点
    drawAnchor(){}
}

如上,我们已经把CaptureEditor类的所有属性和方法规划好了,接下来我们要在对应的方法中逐步填充代码实现效果

  • 接收到了base64格式的图片,我们需要创建要给canvas容器作为副本绘制截图图像,该canvas图像副本将会作为放大方法的源(该图像副本不要appendChild到DOM树中)
generateCanvasImg(){
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    return new Promise(resolve => {
      const oImg = new Image()
      oImg.src = this.imgSrc
      oImg.onload = () => {
        canvas.width = oImg.width
        canvas.height = oImg.height
        ctx.drawImage(oImg,0,0)
        resolve(canvas)
      }
    })
}
  • 生成一个满屏的透明遮罩框,让我们的放大镜打开时看上去效果感更强
/** 生成透明遮罩 */
generateMaskDom(){
    const mask = document.createElement('div')
    mask.style.width = this.screenWidth + 'px'
    mask.style.height = this.screenHeight + 'px'
    mask.style.position = 'absolute'
    mask.style.left = 0 + 'px'
    mask.style.top = 0 + 'px'
    mask.style.background = 'rgba(0,0,0,.5)'
    document.body.appendChild(mask)
}
  • 绘制放大镜区域矩形框,并在矩形框范围内显示区域放大的内容。绘制矩形区域,我们要知道几个参数:初始x坐标初始y坐标矩形宽矩形高,除此之外还需要一个偏移量MARGIN。将这些数值分别作为矩形也就是canvas容器的坐标位置、宽高等样式属性值。
drawRect(){
    const {
      stx, sty, w, h
    } = this.selectRect
    // 设置canvas容器的css样式
    this.canvas.style.left = stx - MARGIN + 'px'
    this.canvas.style.top = sty - MARGIN + 'px'
    this.canvas.style.width = w + MARGIN * 2 + 'px'
    this.canvas.style.height = h + MARGIN * 2 + 'px'
    this.canvas.style.display = 'block'
    this.canvas.width = w + MARGIN * 2
    this.canvas.height = h + MARGIN * 2
    // 设置canvas的属性
    this.ctx.fillStyle = '#fff'
    this.ctx.strokeStyle = '#6249ee'
    this.ctx.lineWidth = 2
    
    const scaleGlassRectangle = {
      x: w / 2 - w * 2 / 2,
      y: h / 2 - h * 2 / 2
    }
    // drawImage方法 9个参数绘制缩放图形
    this.ctx.drawImage(this.transcriptImg,
      stx - MARGIN, sty - MARGIN,
      w, h,
      scaleGlassRectangle.x, scaleGlassRectangle.y,
      w * 2, h * 2
    )
    this.ctx.strokeRect(MARGIN, MARGIN, w, h)
}

😊到这我们已经能看到初步的效果啦,看到效果更有动力啦,继续完善,下一步我们要给矩形框的四个角加上锚点

企业微信截图_16342152584393.png

锚点分析

四个角上绘制圆形锚点,需要数据点确定四个锚点的位置。我们已经知道了canvas矩形容器的宽高,那么我们就很容易设置四个锚点的位置,定义一个二维数组代表,遍历这个数组调用Canvas 2D API绘制圆弧路径的方法绘制

drawAnchor(w,h){
    this.ctx.beginPath()
    const anchors = [
      [0, 0],
      [w, 0],
      [0, h],
      [w, h]
    ]
    anchors.forEach(([x, y], i) => {
      this.ctx.arc(x + MARGIN, y + MARGIN, RADIUS, 0, 2 * Math.PI)
      const next = anchors[(i + 1) % anchors.length]
      this.ctx.moveTo(next[0] + MARGIN + RADIUS, next[1] + MARGIN)
    })
    this.ctx.closePath()
    this.ctx.fill()
    this.ctx.stroke()
}

😀越来越完善了,继续接力,现在四个角锚点有了,我们是要鼠标在停留在这四个角上显示不同的鼠标形状,使用者一看就知道鼠标在这四个角上是代表可以拉动改变矩形大小的。

我们要监听鼠标mousemove事件,判断鼠标是否在锚点上

onMouseMove(e){
    if(this.selectRect){
      const { pageX, pageY } = e
      let selectAnchor
      let selectIndex = -1
      if (this.anchors) {
        this.anchors.forEach(([x, y], i) => {
          if (Math.abs(pageX - x) <= 10 && Math.abs(pageY - y) <= 10) {
            selectAnchor = [x, y]
            selectIndex = i
          }
        })
      }
      if (selectAnchor) {
        this.selectAnchorIndex = selectIndex
        document.body.style.cursor = ANCHORS[selectIndex].cursor
        return
      }
    }
}

😲this.anchors是什么?ANCHORS又是什么? 这两个东西之前没有过对它的介绍:

this.anchors是一个二维数组,它是将锚点在canvas容器上的坐标转换成了页面坐标。在前面绘制锚点方法内添加上转换代码

// drawAnchor方法内新增
this.anchors = anchors.map(([x, y]) => {
  return [this.selectRect.stx + x, this.selectRect.sty + y]
})

ANCHORS是自定义的以对象为数组项的数组,当鼠标停留在哪个锚点上,将鼠标样式cursor属性的值设置成对应的

const ANCHORS = [
  { row: 'stx', col: 'sty', cursor: 'nwse-resize' },
  { row: 'edx', col: 'sty', cursor: 'nesw-resize' },
  { row: 'stx', col: 'edy', cursor: 'nesw-resize' },
  { row: 'edx', col: 'edy', cursor: 'nwse-resize' }
]

😃很棒,鼠标经过四个锚点时会变成可拖拉的样子,很明显的告诉用户这是可以缩放大小的。噢~ 大家有没有发现什么bug呢? 当鼠标经过了锚点位置 变成可拖拉状后,离开锚点,还是最后一次的鼠标样子,并没有回复原始状。

是的,除了锚点可拖拉外,鼠标离开需要回复原状,还有矩形选框需要可以随着鼠标移动而移动啊,那么鼠标在选框范围内就要改变成可移动的状态

20211015_142737.gif

鼠标在选框范围内和在选框范围外的样式变化,很容易嘛,就是获取鼠标位置和选框范围做对比

if (pageX > stx && pageX < edx && pageY > sty && pageY < edy) {
    document.body.style.cursor = 'move'
} else {
    document.body.style.cursor = 'auto'
}

表示可拖拉变换大小,可拖拽移动位置的鼠标效果是做出来了。 那就要真的能拖拉改变大小啊、真的能拖拽移动啊。 话不多说,继续开搞

拖拽移动分析:

不管是拖拽移动还是拖来形变,都需要在鼠标按下的状态来执行相关逻辑。

  • 鼠标按下,存储鼠标状态
  • 鼠标按下,判断点击的位置来确定当前的动作类型
  • 鼠标按下状态时移动鼠标,调用 onDrag方法
onMouseDown(e){
    this.mouseDown = true
    const { pageX, pageY } = e
    // 记录鼠标点击的开始位置信息
    this.startPoint = {
      x: pageX,
      y: pageY
    }
    const {
      stx, sty, edx, edy, w, h
    } = this.selectRect
    // 判断鼠标点击位置是否在矩形框范围内,来确定动作类型
    if (pageX > stx && pageX < edx && pageY > sty && pageY < edy) {
      this.action = MOVING_RECT
      this.startDragRect = {
        x: pageX,
        y: pageY,
        selectRect: {
          stx, sty, edx, edy, w, h
        }
      }
    } else {
      this.action = CREATE_RECT
    }
}

onDrag方法内,当前动作为MOVING_RECT状态:

  • 获取矩形框的宽、高
  • 获取开始点的x、y坐标
  • 移动实时计算新的起始x,起始y,结束x,结束y
  • 用新的值重新绘制矩形框
// onDrag方法
if (this.action === MOVING_RECT) {
  const { w, h } = this.selectRect
  const { x, y } = this.startPoint
  let newStx = this.startDragRect.selectRect.stx + pageX - x
  let newSty = this.startDragRect.selectRect.sty + pageY - y
  let newEdx = newStx + w
  let newEdy = newSty + h
  // 边界处理
  if (newStx < 0) {
    newEdx = w
  } else if (newEdx > this.screenWidth) {
    newStx = newEdx - w
  }
  if (newSty < 0) {
    newEdy = h
  } else if (newEdy > this.screenHeight) {
    newSty = newEdy - h
  }
  this.selectRect = {
    w, h, stx: newStx, sty: newSty, edx: newEdx, edy: newEdy
  }
  this.drawRect()
  return
}

拖拉形变分析:

  • 确定鼠标是停留在锚点上按下移动,当前动作设置为 RESIZE
  • 鼠标按下,将当前信息存储在startPoint中
  • 调用onDrag方法 在上面处理鼠标经过锚点鼠标样式变成可拖拉状时,我们有定义一个selectAnchorIndex属性,默认值为-1,当经过锚点值设置为对应的index,在此处可以用这个属性判断设置当前动作
onMouseDown(e){
    if (this.selectAnchorIndex !== -1) {
      this.startPoint = {
        x: pageX,
        y: pageY,
        selectRect: {
          stx, sty, edx, edy, w, h
        },
        rawRect: {
          stx, sty, edx, edy, w, h
        }
      }
      this.action = RESIZE
      return
    }
}

onDrag方法内,当前动作为RESIZE状态:

  • 确定当前拖拉的是哪个锚点
  • 拉动对锚点,用对应的锚点坐标计算形变
  • 拖拉实时计算新的起始x,起始y,结束x,结束y
  • 用新的值重新绘制矩形框
// onDrag方法
if(this.action === RESIZE){
    const { row, col } = ANCHORS[this.selectAnchorIndex]
    if(row){
        this.startPoint.rawRect[row] = this.startPoint.selectRect[row]+pageX - this.startPoint.x
        selectRect.stx = this.startPoint.rawRect.stx
        selectRect.edx = this.startPoint.rawRect.edx
        if (selectRect.stx > selectRect.edx) {
          const x = selectRect.edx
          selectRect.edx = selectRect.stx
          selectRect.stx = x
        }
        selectRect.w = selectRect.edx - selectRect.stx
        this.startPoint.rawRect.w = selectRect.w
    }
    if(col){
        this.startPoint.rawRect[col] = this.startPoint.selectRect[col] + pageY - this.startPoint.y
        selectRect.sty = this.startPoint.rawRect.sty
        selectRect.edy = this.startPoint.rawRect.edy
        if (selectRect.sty > selectRect.edy) {
          const y = selectRect.edy
          selectRect.edy = selectRect.sty
          selectRect.sty = y
        }
        selectRect.h = selectRect.edy - selectRect.sty
        this.startPoint.rawRect.h = selectRect.h
    }
    this.drawRect()
    return
}

😆快要大功告成啦,移动、形变、放大效果都出来了。这时候我们在矩形框范围外按住鼠标拖动,发现是没有任何效果的,我们期望在举行范围外按住鼠标拖动,会以该点为起始拖出一个新的矩形框,旧框消失

20211016_103205.gif

onMouseDown中我们已经对不同的点击点设置了对应的当前动作,因此在onDrag方法中,直接编写逻辑代码。

  • 鼠标事件event中获取pageXpageY
  • 获取到鼠标第一次按下的的位置 startPoint
  • 对于横坐标 用pageXstartPoint.x去比较执行相应赋值
  • 对于纵坐标 用pageYstartPoint.y去比较执行相应的赋值
  • 计算出举行的宽、高,调用绘制矩形方法
if(this.action === CREATE_RECT){
  let stx, sty, edx, edy, w, h
  if (this.startPoint.x > pageX) {
    stx = pageX
    edx = this.startPoint.xelse {
    stx = this.startPoint.x
    edx = pageX
  }
  if (this.startPoint.y > pageY) {
    sty = pageY
    edy = this.startPoint.yelse {
    sty = this.startPoint.y
    edy = pageY
  }
  w = edx - stx
  h = edy - sty
  this.selectRect = {
    stx, sty, edx, edy, w, h
  }
  this.drawRect()
}

😛最后一步,加上开灯、关灯按钮就完成啦。 按钮操作,只要获取到按钮元素,给其添加上点击事件操作遮罩层的样式即可,并且按钮组要跟随矩形框,在绘制矩形框方法内计算位置,并将位置值作为按钮组的left、top值设置定位位置

//init方法中
const oCloseBtn = document.getElementById('close')
const oOpenBtn = document.getElementById('open')
oCloseBtn.addEventListener('click'() => {
  this.maskDom.style.background = 'rgba(0,0,0)'
})
oOpenBtn.addEventListener('click'() => {
  this.maskDom.style.background = 'rgba(0,0,0,.5)'
})

// drawRect方法中
this.btnGroup.style.display = 'block'
this.btnGroup.style.left = (stx + w / 2) + 'px'
this.btnGroup.style.top = (sty + h) + 'px'