electron创建透明窗口截图

56 阅读2分钟

主进程


// 截图窗口
let cutWindow: BrowserWindow | null = null;
function createCutWindow(): void {
    const { width, height } = getSize()
    cutWindow = new BrowserWindow({
        width,
        height,
        frame: false,
        transparent: true,
        fullscreen: true,
        resizable: false,
        alwaysOnTop:true,
        skipTaskbar: true,
        show: false,
        icon: join(__dirname, '../../build/icon.jpg'),
        webPreferences: {
            preload: join(__dirname, '../preload/index.js'),
            nodeIntegration: true,
            contextIsolation: false,
        }
    })
    if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
        cutWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#/cut`)
    } else {
        cutWindow.loadFile(join(__dirname, '../renderer/index.html'), { hash: '/cut' })
    }
    cutWindow.setAlwaysOnTop(true, 'pop-up-menu')
    cutWindow.setVisibleOnAllWorkspaces(true, { skipTransformProcessType: true })
    cutWindow.on('ready-to-show', () => {
        cutWindow.show()
        cutWindow.focus()
    })

}

// 关闭截图提问
function closeCutWindow() {
    cutWindow && cutWindow.close()
    cutWindow = null;
}

// 打开截图提问
ipcMain.on("open-cut-window", () => {
    closeCutWindow()
    mainWindow && mainWindow.minimize();
    createCutWindow()
})
/*
* 关闭截图提问
* */
ipcMain.on("close-cut-window", (event) => {
    closeCutWindow()
})
// 打开主窗口
ipcMain.handle('cut-open-main-window', async (_, params) => {
    if (mainWindow) {
        mainWindow.show()
        // 向主窗口发送消息,要求打开特定页面
        mainPageContainer.context.webContents.send('open-page', `/chat`)
        mainPageContainer.context.webContents.send('send-cut-data', params)
    }
})

// 截图提问:获取屏幕
ipcMain.handle("cut_get_screen_image", async () => {
    let sources = await desktopCapturer.getSources({
        types: ['screen'],
        thumbnailSize: getSize(),
    });
    return sources[0]
})


截图窗口文件

<!-- 截屏 -->
<template>
    <div ref="containerRef" class="container"  :style="'background-image:url(' + bg + ')'">
        <!--        :style="'background-image:url(' + bg + ')'"-->
        <div class="mark" v-if="bg"></div>
        <v-stage :config="stageConfig1">
            <v-layer>
                <!-- 背景图像 -->
                <v-image :config="imageConfig1" @image:load="handleImageLoad"/>
            </v-layer>
        </v-stage>
        <div class="stage-wrap" :style="stageStyle" v-if="stageConfig.width">
            <v-stage
                ref="stageRef"
                :config="stageConfig"
                @mousedown="handleMouseDown"
                @mousemove="handleMouseMove"
                @mouseup="handleMouseUp"
            >
                <v-layer ref="layerRef">
                    <!-- 背景图像 -->
                    <v-image :config="imageConfig" @image:load="handleImageLoad"/>
                    <!-- 当前正在绘制的矩形 -->
                    <v-rect
                        v-if="rectConfig"
                        :config="rectConfig"
                    />
                    <!-- 绘制所有矩形 -->
                    <v-rect
                        v-for="(item, index) in rectConfigList"
                        :key="index"
                        :config="item"
                    />

                    <v-line v-for="(line, i) in lines" :key="i" :config="line" name="line"/>



                    <!-- 绘制所有箭头 -->
                    <v-arrow
                        v-for="(arrow, index) in arrows"
                        :key="index"
                        :config="arrow"
                    />

                    <!-- 当前正在绘制的箭头 -->
                    <v-arrow
                        v-if="currentArrow"
                        :config="currentArrow"
                    />
                </v-layer>
            </v-stage>
        </div>

        <div
            v-if="isOperating"
            class="operation"
            :style="operatingStyle"
        >
            <div class="operation-box">
                <div
                    class="item"
                    :class="{ active: active === item.name }"
                    v-for="item in operationList"
                    :key="item.name"
                    @click="handleOperation(item.name)">
                    <Tooltip :content="item.title">
                        <MyIcon class="icon" :icon="item.icon" ></MyIcon>
                    </Tooltip>
                </div>
            </div>
        </div>
    </div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, computed, nextTick } from 'vue'
const { ipcRenderer } = window.electron
const containerRef = ref(null)
const type = ref("");
const bg = ref('')
const img = ref("")
const active = ref("")
const operationListData = [
    {
        title: '矩形',
        icon: 'icon-xingzhuang',
        name: "rect"
    },
    {
        title: '箭头',
        icon: 'icon-jiantou1',
        name: "arrow"
    },
    {
        title: '画笔',
        icon: 'icon-huabi',
        name: "line"
    },
    {
        title: '撤回',
        icon: "icon-chehui1",
        name: "chehui"
    },
    {
        title: "提取文字",
        icon: "icon-tiquwenzi",
        name: "tiquwenzi",
    },
    {
        title: '取消',
        icon: 'icon-quxiao1',
        name: "close"
    },
    {
        title: '完成',
        icon: 'icon-queren',
        name: "queren"
    },
    {
        title: "问问灵犀",
        icon: "icon-wenwenlingxi",
        name: "wenwenlingxi"
    }
]
const operationList = computed(() => {
    return operationListData.reduce((pre: any, item: any) => {
        if(item.name === "wenwenlingxi") {
            if(type.value === "2") {
                pre.push(item)
            }
        }else  if(item.name === "queren") {
            if(type.value === "1") {
                pre.push(item)
            }

        }else {
            pre.push(item)
        }

        return pre
    }, [])
})
// 截图信息
const cutConfig = ref({
    x: 0,
    y: 0,
    width: 0,
    height: 0
})
// 屏幕
const winScreenConfig = reactive({
    width: 0,
    height: 0
})
// 操作位置
const operatingStyle = computed(() => {
    let x = cutConfig.value.x;
    let y = Math.ceil(cutConfig.value.y) + Math.ceil(cutConfig.value.height) + 10;
    if(winScreenConfig.height - 50 <= y) {
        return {
            left: "50%",
            bottom: "70px",
            top: "unset",
            transform: "translateX(-50%)"
        }
    }
    return {
        left: `${x}px`,
        top: `${y}px`
    }
})
// 是否显示操作
const isOperating = computed(() => {
    if(isCut.value) {
        return !isDrawing.value && cutConfig.value.width
    }else {
        return true
    }
})
// 是否截图
const isCut = ref(true)
// 画布
const stageRef = ref(null)
const layerRef = ref(null)
const stageConfig = ref({})
// 样式
const stageStyle = computed(() => {
    if(!isCut.value) {
        return {
            left: `${cutConfig.value.x}px`,
            top: `${cutConfig.value.y}px`,
        }
    }
    return {}
})
// 背景图片
const imageConfig = ref({
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    image: null
})
// 是否在绘画
const isDrawing = ref(false)
// 矩形
const rectConfig = ref(null)
// 起始点
const startPoint = ref({ x: 0, y: 0 })
// 矩形列表
const rectConfigList = ref([])

// 画笔
const lines = ref([])
const currentLine = ref(null);

const arrows = ref([]);
const currentArrow = ref(null);

// 画布操作记录
const canvasRecord = ref([])


const stageConfig1 = ref({})
// 背景图片
const imageConfig1 = ref({
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    image: null
})

// 获取屏幕
async function getCurrentScreenImage() {
    let source = await ipcRenderer.invoke("cut_get_screen_image")
    const clientHeight = containerRef.value.clientHeight
    const clientWidth = containerRef.value.clientWidth
    console.log("containerRef", clientWidth, clientHeight)
    console.log("source", source)
    const { thumbnail } = source;
    const pngData = await thumbnail.toDataURL('image/png')
    const size = await thumbnail.getSize()
    console.log("size", size)
    winScreenConfig.width = size.width;
    winScreenConfig.height = size.height;
    bg.value = pngData
    stageConfig.value = {
        width: size.width,
        height: size.height
    }
    stageConfig1.value = {
        width: size.width,
        height: size.height
    }
}
function handleImageLoad(): void {
    console.log('图片已成功加载并显示');
}

function handleMouseDown(e): void {
    isDrawing.value = true
    operatingCanvas(e,"start")
}
function handleMouseMove(e): void {
    if (!isDrawing.value) return
    operatingCanvas(e,"move")
}
function handleMouseUp(e): void {
    if (!isDrawing.value) return
    isDrawing.value = false
    operatingCanvas(e,"stop")
}

function operatingCanvas(e,type) {
    let name = active.value;
    if(name === 'rect' || isCut.value) {
        createRect(type)
    }else if(name === 'line') {
        createPen(type)
    }else if(name === 'arrow') {
        createdArrow(e,type)
    }

}

// 创建矩形
function createRect(type: string): void {
    const pos = stageRef.value.getStage().getPointerPosition()
    switch (type) {
        // 开始
        case "start":
            startPoint.value = { x: pos.x, y: pos.y }
            rectConfig.value = {
                x: pos.x,
                y: pos.y,
                width: 0,
                height: 0,
                fill: 'transparent',
                stroke: isCut.value ? '#24be58' : "red",
                uuid: crypto.randomUUID(),
            }
            break;
        // 绘制
        case "move":
            let width = pos.x - startPoint.value.x
            let height = pos.y - startPoint.value.y

            rectConfig.value = {
                ...rectConfig.value,
                x: width > 0 ? startPoint.value.x : pos.x,
                y: height > 0 ? startPoint.value.y : pos.y,
                width: Math.abs(width),
                height: Math.abs(height)
            }
            break;
        // 结束
        case "stop":
            if(isCut.value) {
                // 记录截图位置
                cutConfig.value = {
                    x: rectConfig.value.x,
                    y: rectConfig.value.y,
                    width: rectConfig.value.width,
                    height: rectConfig.value.height
                }
                captureSelection()
            }else {
                rectConfigList.value.push({
                    ...rectConfig.value
                })
                canvasRecord.value.push({
                    ...rectConfig.value
                })
                rectConfig.value = null
            }

            break;
    }
}

// 画笔
function createPen(type: string): void {
    const pos = stageRef.value.getStage().getPointerPosition()
    switch (type) {
        // 开始
        case "start":
            currentLine.value = {
                points: [pos.x, pos.y],
                stroke: "red",
                strokeWidth: 2,
                lineCap: 'round',
                lineJoin: 'round',
                tension: 0.5,
                draggable: false,
                name: 'line',
                uuid: crypto.randomUUID()
            };
            lines.value.push(currentLine.value);
            canvasRecord.value.push(currentLine.value)
            break;
        // 绘制
        case "move":
            let newPoints = currentLine.value.points.concat([pos.x, pos.y]);
            currentLine.value.points = newPoints;
            layerRef.value.getNode().batchDraw();
            break;
        // 结束
        case "stop":
            currentLine.value = null;
            break;
    }
}

// 箭头
function createdArrow(e, type: string): void {
    const pos = stageRef.value.getStage().getPointerPosition()
    switch (type) {
        // 开始
        case "start":
            currentArrow.value = {
                points: [pos.x, pos.y, pos.x, pos.y],
                pointerLength: 10,
                pointerWidth: 10,
                fill: 'red',
                stroke: 'red',
                strokeWidth: 2,
                uuid: crypto.randomUUID()
            };
            break;
        // 绘制
        case "move":
            let movePos = e.target.getStage().getPointerPosition();
            currentArrow.value.points = [
                currentArrow.value.points[0],
                currentArrow.value.points[1],
                movePos.x,
                movePos.y
            ];
            break;
        // 结束
        case "stop":
            const stopPos = e.target.getStage().getPointerPosition();
            currentArrow.value.points = [
                currentArrow.value.points[0],
                currentArrow.value.points[1],
                stopPos.x,
                stopPos.y
            ];

            arrows.value.push({...currentArrow.value});
            canvasRecord.value.push({...currentArrow.value});

            currentArrow.value = null
            break;
    }
}



function loadImage(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        // 允许跨域图片(如果需要)
        img.crossOrigin = 'anonymous';
        // 图片加载成功回调
        img.onload = () => resolve(img);
        // 图片加载失败回调
        img.onerror = (error) => reject(new Error(`图片加载失败: ${error.message}`));
        // 开始加载图片
        img.src = url;
    });
}
// 捕获选择
async function captureSelection(): void {
    const rect = rectConfig.value
    let { base64, imageData } = await getCutImage(rect)
    const loadedImage = await loadImage(base64);

    if(isCut.value) {
        imageConfig1.value = {
            width: imageData.width,
            height: imageData.height,
            image: loadedImage,
            x: rect.x,
            y: rect.y,
        }
    }else {

        imageConfig.value = {
            width: imageData.width,
            height: imageData.height,
            image: loadedImage,
            x: 0,
            y: 0,
        }
        // 重新绘制画布
        stageConfig.value = {
            width: rect.width,
            height: rect.height,
        }
        rectConfig.value = null
    }
}

// 根据选择区域生成图片
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 {
        base64: canvas.toDataURL("image/png"),
        imageData: ctx.getImageData(0, 0, width, height),
    };
}

// 操作
async function handleOperation(name): void  {
    active.value = name;
    if(isCut.value) {
        isCut.value = false;
        await captureSelection()
    }
    switch (name){
        // 取消
        case "close":
            handleCancel()
            break;
        // 确认
        case "queren":
        case "wenwenlingxi":
            saveCanvas();
            break;
        // 提前文字
        case "tiquwenzi":
            handleExtractText()
            break;
        // 撤回
        case "chehui":
            handleWithdraw()
            break;
    }
}

// 撤回
function handleWithdraw() {
    if(canvasRecord.value.length) {
        let last = canvasRecord.value.pop();
        lines.value = lines.value.filter(item =>item.uuid !== last.uuid )
        rectConfigList.value = rectConfigList.value.filter(item => item.uuid !== last.uuid)
        arrows.value =  arrows.value.filter(item => item.uuid !== last.uuid)
    }
}

// 取消
function handleCancel(): void {
    ipcRenderer.send("close-cut-window", )
}
// 画布内容转换为图片
function toDataURL() {
    const stage = stageRef.value.getStage();
    const dataURL = stage.toDataURL();
    return {
        data: dataURL
    }
}

// 保存截图
function saveCanvas(): void {
    let imgObj = toDataURL()
    ipcRenderer.send("close-cut-window",)
    ipcRenderer.invoke(
        "cut-open-main-window",
        {
            type: "img",
            data: imgObj.data
        }
    )
}

// 提取文字
function handleExtractText(): void {
    let imgObj = toDataURL()
    ipcRenderer.send("close-cut-window", )
    ipcRenderer.send("open-extract-text-window")
    window.localStorage.setItem("cutData", JSON.stringify({
        type: "img",
        data: imgObj.data
    }))
}


onMounted( () => {
    type.value = window.localStorage.getItem("cutType") || "1"
    setTimeout(()=>{
        getCurrentScreenImage()
    },500)

})
</script>
<style>
html,
body,
#app {
    margin: 0;
    padding: 0;
    background: transparent !important;
}
</style>
<style scoped lang="less">

.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;
    box-sizing: border-box;
    border: 2px solid #24be58;
    cursor: crosshair;
    user-select: none;
}
.mark {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.45);
    //opacity: 0;
}

.operation {
    position: fixed;
    top: 0;
    z-index: 1;
    .operation-box {
        display: flex;
        align-items: center;
        background-color: #fff;
        padding: 2px 5px;
        border-radius: 4px;
        box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
        .item {
            width: 32px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            & + .item{
                margin-left: 5px;
            }
            .icon {
                width: 20px;
                height: 20px;
            }
            &.active {
                .icon {
                    color: #0057ff;
                }
            }
        }

    }
}
.stage-wrap {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1;
    &::before {
        content: " ";
        position: absolute;
        top: -2px;
        left: -2px;
        right: -2px;
        bottom: -2px;
        border: 2px solid #24be58;
    }
}
.bg-transparent {
    background-color: transparent;
}
</style>