刚刚学习election,心血来潮做些小工具,工具包含常用的矩形、椭圆框线,箭头绘制,笔刷以及文字。

支持框选截图范围后拖动以及裁剪。

大多都是查找网上大佬提供的思路,其中箭头绘制来自大佬代码魔改如何用canvas画一个漂亮的箭头用于永中文档批注场景https://blog.csdn.net/codingMonkeyKing/article/details/51459487

第一次做这种,很多不完善的地方,闲暇时候再慢慢优化吧

效果图:

单独用main.js写工具的主线程,免得代码太多分不清

这里面遇到的坑就是打开窗口时会出现闪屏,原因是因为加载页面有一定的延迟,所以用了个比较笨的办法:主线程加载时提前开启窗口并隐藏,调用时直接显示,这种做法费内存但很有效!

为了方便调试没有开启窗口强制置顶,并开启了调试器

//main.js

const {BrowserWindow, ipcMain, globalShortcut, desktopCapturer, screen} = require('electron')

const os = require('os')

const path = require('path')

let captureWin = null;

const captureScreen = () => {

var mainScreen = screen.getPrimaryDisplay();

var allScreens = screen.getAllDisplays();

// let size = screen.getPrimaryDisplay().workAreaSize

//捕获屏幕截图

desktopCapturer.getSources({

types: ['screen'],

thumbnailSize: {width: mainScreen.size.width, height: mainScreen.size.height}

}).then(imgs => {

let imageData=imgs[0].thumbnail.toDataURL()

global._cut_img_data_temp=imageData; //临时全局变量

if (captureWin) {

captureWin.webContents.send("imageData",imageData)

captureWin.show();

if (!process.env.IS_TEST) captureWin.webContents.openDevTools()

}

})

};

const useScreenshot = () => {

//提前创建窗口防止闪屏

captureWin = new BrowserWindow({

fullscreen: true,

transparent: true,

frame: false,

resizable: false,

//enableLargerThanScreen: true,

//skipTaskbar: true,

//alwaysOnTop: true,

show: false,

icon: path.join(__static, '/favicon.ico'), // 更换图标, 这里的图标仅支持svg 和icon 图标

webPreferences: {

webSecurity: false, // 是否禁用浏览器的跨域安全特性

enableRemoteModule: true,

nodeIntegration: true, // 是否完整支持node

contextIsolation: false,//--增加改行解决我的报错

preload: __dirname + '/preload.js',

// preload:'/src/preload.js'

}

});

captureWin.loadURL(global.winURL + '#/Screenshot');

captureWin.on('ready-to-show', function () {

// cs_edit_win.webContents.send('imageData',imageData);

});

captureWin.hide()

globalShortcut.register('Esc', () => {

closeScreenshot()

})

globalShortcut.register('CmdOrCtrl+Shift+A', captureScreen)

ipcMain.on('captureScreen', (e, {type = 'start', screenId} = {}) => {

if (type === 'start') {

captureScreen()

} else if (type === 'complete') {

// nothing

} else if (type === 'select') {

captureWin.webContents.send('captureScreen', {type: 'select', screenId})

}

})

ipcMain.on('closeScreenshot', (e) => {

closeScreenshot()

})

};

const closeScreenshot=function(){

if (captureWin) {

captureWin.hide();

setTimeout(function () {

captureWin.close()

useScreenshot()

},200)

// captureWin.hide()

}

}

exports.useScreenshot = useScreenshot;

exports.captureScreen = captureScreen;

background.js

election默认主线程中引入并初始化

//background.js

....

const { useScreenshot } = require('../mainProcess/screenshot/main');

...

app.on('ready', async () => {

// 初始化截图

useScreenshot()

});

Screenshot.vue

截图窗口的操作页面

Screenshot.vue

imageEdit.js

这里基本都是截图后的工具栏中的相关操作

import $ from "jquery"

function EditTools(options) {

let self = this;

this.canvas = options.canvas;

this.ctx = options.ctx;

this.img = options.img;

this.canvasImgPosition = options.canvasImgPosition;

this.history = [];

this.historyAll = [];

this.color = "#ff0000";

this.fillColor = "#ff0000";

this.lineWidth = 1;

this.fontSize = 20;

this.initStyle();

this.typeEnum = {

move: "move", //移动

rect: "rect", //矩形

circ: "circ", //圆

brush: "brush", //画笔

arrow: "arrow", //箭头

words: "words", //文字

};

this.type = "";

this.tempParam = [];

this.canvas.onmousedown = function (event) {

if (self.type !== self.typeEnum.rect

&& self.type !== self.typeEnum.circ

&& self.type !== self.typeEnum.brush

&& self.type !== self.typeEnum.arrow

&& self.type !== self.typeEnum.words) {

return false;

}

switch (self.type) {

case self.typeEnum.rect:

self.doRect(event);

break;

case self.typeEnum.circ:

self.doCirc(event);

break;

case self.typeEnum.brush:

self.tempParam = []; //将临时参数容器类型设为数组,避免保存记录时失败

self.doBrush(event);

break;

case self.typeEnum.arrow:

self.doArrow(event);

break;

case self.typeEnum.words:

self.doWords(event);

break;

}

}

this.canvas.addEventListener('mouseup', (event) => {

if (self.type !== self.typeEnum.rect

&& self.type !== self.typeEnum.circ

&& self.type !== self.typeEnum.brush

&& self.type !== self.typeEnum.arrow) {

return false;

}

// 结束本次绘画

self.canvas.onmousemove = null;

self.canvas.onclick = null;

this.history.push({

method: self.type,

params: this.tempParam,

color: this.color

});

this.historyAll.push({

method: self.type,

params: this.tempParam,

color: this.color

});

this.tempParam = "";

this.ctx.closePath();

this.ctx.save()

})

//按键监听

this.canvas.addEventListener('keydown', (e) => {

let keyCode = e.keyCode || e.which || e.charCode;

let ctrlKey = e.ctrlKey;

if (ctrlKey) {

switch (keyCode) {

case 89://ctrl+y

self.redo();

break;

case 90://ctrl+z

self.undo();

break;

}

}

})

}

EditTools.prototype.initStyle = function () {

this.ctx.strokeStyle = this.color;

this.ctx.fillStyle = this.fillColor;

this.ctx.lineWidth = this.lineWidth;

}

EditTools.prototype.changeColor = function (color) {

this.color = color;

this.fillColor = color;

this.lineWidth = color;

this.initStyle();

}

/**

* 撤销

* @param e

*/

EditTools.prototype.undo = function (e) {

if (this.history.length > 0) {

this.ctx.clearRect(0, 0, this.width, this.height);

this.history.pop();

this.ctx.drawImage(this.img, this.canvasImgPosition.x, this.canvasImgPosition.y);

this.reDraw()

}

};

/**

* 重写

* @param e

*/

EditTools.prototype.redo = function (e) {

if (this.history.length < this.historyAll.length) {

this.ctx.clearRect(0, 0, this.width, this.height);

this.history.push(this.historyAll[this.history.length]);

this.ctx.drawImage(this.img, this.canvasImgPosition.x, this.canvasImgPosition.y);

this.reDraw()

}

};

/**

* 按照历史记录重新绘制

* @param e

*/

EditTools.prototype.reDraw = function (e) {

// 逐个执行历史动作

for (let item of this.history) {

this.ctx.strokeStyle = item.color;

this.ctx.fillStyle = item.color;

//矩形

if (item.method === this.typeEnum.rect) {

// this.ctx.beginPath();

this.ctx.strokeRect(...item.params);

// this.ctx.closePath();

}

//圆

if (item.method === this.typeEnum.circ) {

this.ctx.beginPath();

this.ctx.ellipse(...item.params);

this.ctx.stroke();

}

//画笔

if (item.method === this.typeEnum.brush) {

this.ctx.beginPath();

item.params.forEach(param => {

this.ctx.lineTo(...param);

this.ctx.stroke();

});

this.ctx.closePath();

}

//箭头

if (item.method === this.typeEnum.arrow) {

this.drawLineArrow(...item.params);

}

//文字

if (item.method === this.typeEnum.words) {

this.drawText(...item.params);

}

}

this.ctx.strokeStyle = this.color;

this.ctx.fillStyle = this.fillColor;

};

//坐标修正

// EditTools.prototype.correctPosition=function(x,y){

// this.ctx.translate(x,y);

// this.ctx.drawImage(this.img, this.canvasImgPosition.x-x, this.canvasImgPosition.y-y);

//

// }

EditTools.prototype.move = function (e) {

this.canvas.setAttribute("ctrl-type", this.typeEnum.move);

this.canvas.style.cursor = "move";

this.historyAll = [];

this.history = [];

};

/**

* 初始化到上一次的绘制,用于绘制清除痕迹

* @param e

*/

EditTools.prototype.clearAndUpdateParam = function (...param) {

this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

this.ctx.drawImage(this.img, this.canvasImgPosition.x, this.canvasImgPosition.y);

this.tempParam = param;

this.reDraw()

}

EditTools.prototype.saveBrushPath = function (...param) {

this.tempParam.push(param)

};

EditTools.prototype.saveDrawText = function (...param) {

let self=this;

this.history.push({

method: self.type,

params: param,

color: self.color

});

this.historyAll.push({

method: self.type,

params: param,

color: self.color

});

self.tempParam = "";

}

EditTools.prototype.drawRect = function (e) {

this.canvas.setAttribute("ctrl-type", this.typeEnum.rect);

this.canvas.style.cursor = "crosshair";

this.type = this.typeEnum.rect;

};

EditTools.prototype.doRect = function (e) {

let that = this;

let offstL = this.canvas.offsetLeft;

let offstT = this.canvas.offsetTop;

let startX = e.clientX - offstL, startY = e.clientY - offstT;

this.canvas.onmousemove = function (e) {

let param = [startX, startY, e.clientX - offstL - startX, e.clientY - offstT - startY]

that.clearAndUpdateParam(...param);

that.ctx.strokeRect(...param);

};

};

EditTools.prototype.drawCirc = function (e) {

this.canvas.setAttribute("ctrl-type", this.typeEnum.circ);

this.canvas.style.cursor = "crosshair";

this.type = this.typeEnum.circ;

};

EditTools.prototype.doCirc = function (ev) {

let that = this;

let start = that.getCanvasPos(that.canvas, ev);

this.canvas.onmousemove = function (e) {

let mouse = that.getCanvasPos(that.canvas, e);

let center = {

x: (mouse.x - start.x) / 2 + start.x,

y: (mouse.y - start.y) / 2 + start.y,

};

let radiusX = Math.abs(e.clientX - ev.clientX) / 2;

let radiusY = Math.abs(e.clientY - ev.clientY) / 2;

//(起点x,起点y,半径x,半径y,旋转的角度,起始角,结果角,顺时针还是逆时针)

let param = [center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2]

that.clearAndUpdateParam(...param);

that.ctx.beginPath();

that.ctx.ellipse(...param);

that.ctx.stroke();

};

};

/**

* 获取鼠标在canvas上的坐标

* @param canvas

* @param event

* @returns {{x: number, y: number}}

*/

EditTools.prototype.getCanvasPos = function (canvas, event) {

let rect = canvas.getBoundingClientRect();

return {

x: event.clientX - rect.left * (canvas.width / rect.width),

y: event.clientY - rect.top * (canvas.height / rect.height)

};

};

/**

* 画笔

* @param e

*/

EditTools.prototype.brush = function (e) {

let that = this;

this.canvas.setAttribute("ctrl-type", this.typeEnum.circ);

this.canvas.style.cursor = "default";

this.type = this.typeEnum.brush;

};

EditTools.prototype.doBrush = function (e) {

let that = this;

let offstL = this.canvas.offsetLeft;

let offstT = this.canvas.offsetTop;

this.ctx.beginPath();

this.canvas.onmousemove = function (e) {

that.ctx.lineTo(e.clientX - offstL, e.clientY - offstT);

that.ctx.stroke();

that.saveBrushPath(e.clientX - offstL, e.clientY - offstT)

};

};

/**

* 箭头

* @param e

*/

EditTools.prototype.arrow = function (e) {

let that = this;

this.canvas.setAttribute("ctrl-type", this.typeEnum.arrow);

this.canvas.style.cursor = "default";

this.type = this.typeEnum.arrow;

};

EditTools.prototype.doArrow = function (e) {

let that = this;

let offstL = this.canvas.offsetLeft;

let offstT = this.canvas.offsetTop;

this.canvas.onmousemove = function (ev) {

let param = [that.ctx, e.clientX - offstL, e.clientY - offstT, ev.clientX - offstL, ev.clientY - offstT];

that.clearAndUpdateParam(...param);

that.drawLineArrow(...param)

};

};

/**

*

* @param {*canvas context 对象} ctx

* @param {*起点横坐标} fromX

* @param {*起点纵坐标} fromY

* @param {*终点横坐标} toX

* @param {*终点纵坐标} toY

* 以下注释以终点在坐标第一象限内,且方向为右上方

*/

EditTools.prototype.drawLineArrow = function (ctx, fromX, fromY, toX, toY) {

let headlen = 0.2 * 1.41 * Math.sqrt((fromX - toX) * (fromX - toX) + (fromY - toY) * (fromY - toY));//箭头头部长度

headlen = headlen > 20 ? 20 : headlen;//箭头头部最大值

let theta = 30;//自定义箭头线与直线的夹角

let arrowX, arrowY;//箭头线终点坐标

// 计算各角度和对应的箭头终点坐标

let angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI;

let angle1 = (angle + theta) * Math.PI / 180;

let angle2 = (angle - theta) * Math.PI / 180;

let topX = headlen * Math.cos(angle1);

let topY = headlen * Math.sin(angle1);

let botX = headlen * Math.cos(angle2);

let botY = headlen * Math.sin(angle2);

let toLeft = fromX > toX;

let toUp = fromY > toY;

//箭头最上点

arrowX = toX + topX;

arrowY = toY + topY;

//箭头下拐点

let arrowX1 = toX + botX;

let arrowY1 = toY + botY;

//箭头上拐点

let arrowX2 = toUp ? arrowX + 0.25 * Math.abs(arrowX1 - arrowX) : arrowX - 0.25 * Math.abs(arrowX1 - arrowX);

let arrowY2 = toLeft ? arrowY - 0.25 * Math.abs(arrowY1 - arrowY) : arrowY + 0.25 * Math.abs(arrowY1 - arrowY);

//箭头最下点

let arrowX3 = toUp ? arrowX + 0.75 * Math.abs(arrowX1 - arrowX) : arrowX - 0.75 * Math.abs(arrowX1 - arrowX);

let arrowY3 = toLeft ? arrowY - 0.75 * Math.abs(arrowY1 - arrowY) : arrowY + 0.75 * Math.abs(arrowY1 - arrowY);

ctx.beginPath();

//起点-起点,闭合

ctx.moveTo(fromX, fromY);

ctx.lineTo(arrowX2, arrowY2);

ctx.lineTo(arrowX, arrowY);

ctx.lineTo(toX, toY);

ctx.lineTo(arrowX1, arrowY1);

ctx.lineTo(arrowX3, arrowY3);

ctx.lineTo(fromX, fromY);

ctx.closePath();

ctx.fill();

ctx.stroke();

};

/**

* 箭头

* @param e

*/

EditTools.prototype.words = function (e) {

let that = this;

this.canvas.setAttribute("ctrl-type", this.typeEnum.words);

this.canvas.style.cursor = "text";

this.type = this.typeEnum.words;

};

EditTools.prototype.doWords = function (ev) {

let that = this;

let x = ev.offsetX, y = ev.offsetY;

this.createTextInput(this.canvas, x, y, function (setX, setY, text,maxWidth) {

let param=[that.ctx,text,setX, setY,maxWidth]

that.drawText(...param);

that.saveDrawText(...param);

})

}

EditTools.prototype.drawText=function(ctx,t,x,y,w){

let that=this;

//参数说明

let chr = t.split("")

let temp = ""

let row = []

for (let a = 0; a

if(/\r|\r\n|\n/.test(chr[a]) ){

chr[a]="";

row.push(temp);

temp = chr[a];

}else if(ctx.measureText(temp).width < w && ctx.measureText(temp+(chr[a])).width <= w){

temp += chr[a];

}else{

row.push(temp);

temp = chr[a];

}

}

row.push(temp)

for(let b=0;b

ctx.font = that.fontSize+"px emoji";

ctx.fillText(row[b],x,y+(b+1)*that.fontSize);//每行字体y坐标间隔20

}

}

/**

* 创建文字输入框

*/

EditTools.prototype.createTextInput = function (canvas, startX, startY, onInputOver) {

let that = this;

if ($("._textInput").length > 0) {

return false

}

let rect = canvas.getBoundingClientRect();

let textareaMaxW = that.canvas.width - startX - 12;

let textareaMaxH = that.canvas.height - startY - 18;

//原生js元素操作实在麻烦,改用jq

let textarea = $("");

textarea.css({

position: "absolute",

left: rect.left + startX,

top: rect.top + startY - 15,

outline: "none",

border: "2px solid " + that.color,

resize: "none",

padding: "0 5px",

"font-size": that.fontSize,

color: that.color,

width: 80,

"max-width": textareaMaxW,

"font-family": "emoji",

height: 30

}).blur(function () {

onInputOver && typeof onInputOver == "function"

&& onInputOver(startX, startY - 15 , $(this).val(),textareaMaxW);

$(this).remove();

}).bind('input propertychange', function () {

let text = $(this).val();

let maxRow = 0

text.split(/\r|\r\n|\n/).forEach(e => {

if (e.length > maxRow) {

maxRow = e.length

}

})

if (maxRow * that.fontSize > $(this).width()) {

if (textareaMaxW > $(this).width()) {

$(this).css("width", maxRow * that.fontSize + that.fontSize)

}

} else if ($(this).width() - maxRow * that.fontSize > that.fontSize) {

$(this).css("width", maxRow * that.fontSize + that.fontSize)

}

$(this).css("height", $(this)[0].scrollHeight)

})

$("body").append(textarea)

setTimeout(function () {

textarea.focus();

})

};

// EditTools.prototype.doArrow = function (e) {

//

// };

export {EditTools}

没有接触过桌面程序的开发,vue和electron也是刚学,是个搞java的后端狗

当中的思路和方法基本都是自己琢磨出来的,在此发文也仅作为自己的学习笔记。其中也有很多明显的问题,需要后面慢慢的调整

方法很笨,别杠别喷,杠就是你对!!

======================================================

2023年2月15日补充:

preload.js

dist目录下新建文件 preload.js 保证渲染线程能正常调用

global.electron = require('electron');

window.ipcRenderer = require('electron').ipcRenderer;

window.Menu = require('electron').Menu;

window.remote = require('electron').remote;

window.Elapp = require('electron').app;

window.protocol = require('electron').protocol;

window.BrowserWindow = require('electron').BrowserWindow;

window.desktopCapturer = require('electron').desktopCapturer;

window.electronScreen = require('electron').screen;

window.clipboard = require('electron').clipboard;

window.nativeImage = require('electron').nativeImage;