Electron 项目实战 03: 实现一个截图功能
实现效果https://dis.qidao123.com/imgproxy/aHR0cHM6Ly9pLWJsb2cuY3NkbmltZy5jbi9ibG9nX21pZ3JhdGUvODNiMWE4Zjc5YzVlNDgzN2FhYzdlYmRlODQ5NzNiOWYuZ2lm
实现思绪
[*]创建两个window,一个叫mainWindow,一个叫cutWindow
[*]mainWindow:主界面用来展示截图效果
[*]cutWindow:截图窗口,加载截图页面和截图交互逻辑
[*]mainWindow 页面点击截图,让cutWIndow 来实现详细截图逻辑
[*]cutWindow:截图完后把截图send给mainWindow页面
截图过程-时序图
https://dis.qidao123.com/imgproxy/aHR0cHM6Ly9pLWJsb2cuY3NkbmltZy5jbi9ibG9nX21pZ3JhdGUvY2Q1NTBhY2MwZGVkYzExYTkzNjFiODE3NTU4MmVjYzEucG5n
创建项目
我在网上找了一大圈,没有找到一个符合的模板,要么情况太老、要么设置各种缺失不美满、要么打包出来各种标题等等,说实话坑还真不少,偶尔间找到一个特别好的脚手架,它简单又美满。保举给各人:electron-vite ,以是接下来直接用创建下令
yarn create @quick-start/electron
安装依赖
[*]vue-router:切换加载首页和截图页面
[*]konva:完成截图交互的库
yarn add konva vue-router
核心代码
为了更好的展示添加的内容,提供如下目次布局图方便明确
目次布局
https://dis.qidao123.com/imgproxy/aHR0cHM6Ly9pLWJsb2cuY3NkbmltZy5jbi9ibG9nX21pZ3JhdGUvMTdhN2U1ZmUyN2Y5ODM1NzVjYThlMWIyYzcwMmViNmQucG5n
主历程
[*] src/main/index.js
import {
app,
shell,
BrowserWindow,
ipcMain,
screen,
desktopCapturer,
globalShortcut
} from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
let mainWindow
let cutWindow
function closeCutWindow() {
cutWindow && cutWindow.close()
cutWindow = null
}
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
console.log('loadURL:', process.env['ELECTRON_RENDERER_URL'])
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
mainWindow.on('closed', () => {
closeCutWindow()
})
}
function registerShortcut() {
//! 截图快捷键
globalShortcut.register('CommandOrControl+Alt+C', () => {
openCutScreen()
})
globalShortcut.register('Esc', () => {
closeCutWindow()
mainWindow.show()
})
globalShortcut.register('Enter', sendFinishCut)
}
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
//! 开发模式:win 环境F12 和 mac os 环境:CommandOrControl + R 打开 DevTools
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
createMainWindow()
registerShortcut()
openMainListener()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
globalShortcut.unregisterAll()
app.quit()
}
})
function getSize() {
const { size, scaleFactor } = screen.getPrimaryDisplay()
return {
width: size.width * scaleFactor,
height: size.height * scaleFactor
}
}
function createCutWindow() {
const { width, height } = getSize()
cutWindow = new BrowserWindow({
width,
height,
autoHideMenuBar: true,
useContentSize: true,
movable: false,
frame: false,
resizable: false,
hasShadow: false,
transparent: true,
fullscreenable: true,
fullscreen: true,
simpleFullscreen: true,
alwaysOnTop: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
nodeIntegration: true,
contextIsolation: false
}
})
console.log('createCutWindow:', is.dev, process.env['ELECTRON_RENDERER_URL'])
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
let url = process.env['ELECTRON_RENDERER_URL'] + '/#/cut'
console.log('createCutWindow: loadURL=', url)
cutWindow.loadURL(url)
} else {
cutWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
cutWindow.maximize()
cutWindow.setFullScreen(true)
}
function sendFinishCut() {
cutWindow && cutWindow.webContents.send('FINISH_CUT')
}
function openCutScreen() {
closeCutWindow()
mainWindow.hide()
createCutWindow()
cutWindow.show()
}
function openMainListener() {
ipcMain.on('OPEN_CUT_SCREEN', openCutScreen)
ipcMain.on('SHOW_CUT_SCREEN', async (e) => {
let sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: getSize()
})
cutWindow && cutWindow.webContents.send('GET_SCREEN_IMAGE', sources)
})
ipcMain.on('FINISH_CUT_SCREEN', async (e, cutInfo) => {
closeCutWindow()
mainWindow.webContents.send('GET_CUT_INFO', cutInfo)
mainWindow.show()
})
ipcMain.on('CLOSE_CUT_SCREEN', async (e) => {
closeCutWindow()
mainWindow.show()
})
}
渲染器
[*] scr/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
[*] src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{ path: '/', redirect: '/home' },
{
path: '/home',
name: 'home',
component: () => import('../pages/Home/index.vue')
},
{
path: '/cut',
name: 'cut',
component: () => import('../pages/Cut/index.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
[*] src/App.vue
<template>
<router-view>
</router-view>
</template>
<script setup>
</script>
<style lang="less">
@import './assets/css/styles.less';
</style>
[*] src/pages/index.vue:首页
<template>
<div class="container">
<button @click="handleCutScreen">截屏</button>
<div>
<img :src="previewImage"
style="max-width: 100%" />
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const { ipcRenderer } = window.electron;
const previewImage = ref("");
async function handleCutScreen() {
await ipcRenderer.send("OPEN_CUT_SCREEN");
ipcRenderer.removeListener("GET_CUT_INFO", getCutInfo);
ipcRenderer.on("GET_CUT_INFO", getCutInfo);
}
function getCutInfo(event, pic) {
previewImage.value = pic;
}
</script>
[*] src/pages/cut.vue:截图界面
<template>
<div class="container"
:style="'background-image:url(' + bg + ')'"
ref="container"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp">
</div>
</template>
<script setup>
import Konva from "konva";
import { ref, onMounted } from "vue";
const { ipcRenderer } = window.electron;
let container = ref(null);
let bg = ref("");
let stage, layer, rect, transformer;
onMounted(() => {
ipcRenderer.send("SHOW_CUT_SCREEN");
ipcRenderer.removeListener("GET_SCREEN_IMAGE", getSource);
ipcRenderer.on("GET_SCREEN_IMAGE", getSource);
ipcRenderer.on("FINISH_CUT", confirmCut);
});
async function getSource(event, source) {
const { thumbnail } = source;
const pngData = await thumbnail.toDataURL("image/png");
bg.value = pngData;
render();
}
function render() {
stage = createStage();
layer = createLayer(stage);
}
function createStage() {
return new Konva.Stage({
container: container.value,
width: window.innerWidth,
height: window.innerHeight,
});
}
function createLayer(stage) {
let layer = new Konva.Layer();
stage.add(layer);
layer.draw();
return layer;
}
function createRect(layer, x, y, width, height, alpha, draggable) {
let rect = new Konva.Rect({
x,
y,
width,
height,
fill: `rgba(0,0,255,${alpha})`,
draggable
});
layer.add(rect);
return rect;
}
let isDown = false;
let rectOption = {};
function onMouseDown(e) {
if (rect || isDown) {
return;
}
isDown = true;
const { pageX, pageY } = e;
rectOption.x = pageX || 0;
rectOption.y = pageY || 0;
rect = createRect(layer, pageX, pageY, 0, 0, 0.25, false);
rect.draw();
}
function onMouseMove(e) {
if (!isDown) return;
const { pageX, pageY } = e;
let w = pageX - rectOption.x;
let h = pageY - rectOption.y;
rect.remove();
rect = createRect(layer, rectOption.x, rectOption.y, w, h, 0.25, false);
rect.draw();
}
function onMouseUp(e) {
if (!isDown) {
return;
}
isDown = false;
const { pageX, pageY } = e;
let w = pageX - rectOption.x;
let h = pageY - rectOption.y;
rect.remove();
rect = createRect(layer, rectOption.x, rectOption.y, w, h, 0, true);
rect.draw();
transformer = createTransformer(rect);
layer.add(transformer);
}
function createTransformer(rect) {
let transformer = new Konva.Transformer({
nodes: ,
rotateAnchorOffset: 60,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right']
});
return transformer
}
/**
* 根据选择区域生成图片
* @param {*} info
*/
async function getCutImage(info) {
const { x, y, width, height } = info;
let img = new Image();
img.src = bg.value;
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = ctx.width = width;
canvas.height = ctx.height = height;
ctx.drawImage(img, -x, -y, window.innerWidth, window.innerHeight);
return canvas.toDataURL("image/png");
}
/**
* 确认截图
*/
async function confirmCut() {
const { width, height, x, y, scaleX = 1, scaleY = 1 } = rect.attrs;
let _x = width > 0 ? x : x + width * scaleX;
let _y = height > 0 ? y : y + height * scaleY;
let pic = await getCutImage({
x: _x,
y: _y,
width: Math.abs(width) * scaleX,
height: Math.abs(height) * scaleY,
});
ipcRenderer.send("FINISH_CUT_SCREEN", pic);
}
/**
* 关闭截图
*/
function closeCut() {
ipcRenderer.send("CLOSE_CUT_SCREEN");
}
</script>
<style lang="scss" scoped>
.container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
background-size: 100% 100%;
background-repeat: no-repeat;
border: 2px solid blue;
box-sizing: border-box;
}
</style>
总结
固然实现了核心功能,但是仅支持主屏幕截图,不支持多屏幕截图,同时还遗留诸多标题,背面单独一篇更新办理
完备demo :传送门,趁便帮助点个star,感谢~
参考文献
[*]https://juejin.cn/post/7111115472182968327
[*]https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts
[*]https://konvajs.org/docs/select_and_transform/Basic_demo.html
[*]https://stackoverflow.com/questions/40360109/content-security-policy-img-src-self-data/62213224#62213224
更多
家人们,我近来花了2个多月开源了一个文章发布助手artipub,可以帮你一键将markdown发布至多平台(发布和更新),方便各人更好的流传知识和分享你的履历。
现在已支持平台:个人博客、Medium、Dev.to(将来会支持更多平台)
官网地点:https://artipub.github.io/artipub/
堆栈地点:https://github.com/artipub/artipub
现在库已可以正常使用,欢迎各人体验、如果你有任何标题和发起都可以在Issue给我举行反馈。
如果你感爱好,特别欢迎你的到场,让我们一起美满好这个工具。
帮助点个star⭐,让更多人知道这个工具,感谢各人🙏
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金
页:
[1]