Electron内嵌网页实现打印预览功能

前言

目前在业务碰到一个需求,之前是一个网页的应用,现在要求把这个应用内嵌到Electron里面打包成一个安装包,由于之前应用内涉及到大量的打印功能,且采用的都是window.print()的方式来实现的,这种方式在谷歌等现代浏览器上本身就能够实现预览,虽然内嵌到Electron后还是能成功打印,但是无法进行预览,只会出现如下图的系统页面

所以希望在不调整之前系统的打印逻辑前提下,能够实现在Electron中进行打印预览并成功打印,经过摸索,得出了以下的解决方案:

1. 创建Electron应用,内嵌网页

首先创建一个electron窗口,直接通过mainWindow.loadURL()方法嵌入线上应用,这里便于演示,我采用加载本地静态html的方式来实现

JavaScript 复制代码
// main.js
const { app, BrowserWindow, ipcMain, screen } = require('electron');

let mainWindow; // 主窗口
let previewWindow; // 预览窗口

function createWindow() {

    // 获取主屏幕的尺寸
    const primaryDisplay = screen.getPrimaryDisplay()
    const { width, height } = primaryDisplay.workAreaSize

    mainWindow = new BrowserWindow({
        width: width,
        height: height,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
        }
    });


    // 作为演示,采用本地静态文件
    mainWindow.loadFile('index.html')
    // 可通过以下方式内嵌网页
    //mainWindow.loadURL(`http://www.example.com`)

    // 打开开发者工具
    mainWindow.webContents.openDevTools();
}

app.whenReady().then(() => {
    createWindow();

    app.on('activate', function () {
        if (BrowserWindow.getAllWindows().length === 0) createWindow();
    });
});

// 退出
app.on('window-all-closed', function () {
    if (process.platform !== 'darwin') app.quit();
});

2. 拦截系统打印

这里通过重写window.print()方法,拦截系统自带的打印功能,自己来写一个在electron内用于实现打印预览的页面,需要获取到需要打印的dom节点,同时为了保证样式生效,我们这里同时把当前页面的所有style标签和link样式文件都获取到,一并传给预览页面

html 复制代码
<!-- index.html -->
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>打印预览示例</title>
    <style>
        .print-button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
    </style>
</head>

<body>
    <h1>打印预览示例</h1>
    <button class="print-button" onclick="showPreview()">打印预览</button>
    <div id="printTable">
        打印内容
    </div>

    <script>
        const { ipcRenderer } = require('electron');

        // 拦截默认打印行为
        const originalPrint = window.print;
        window.print = function () {
            console.log('渲染进程拦截到打印请求');
            try {
                // 获取要打印的内容
                const printContent = document.getElementById("printTable").innerHTML

                // 获取所有<style>标签
                const styleEles = Array.from(document.querySelectorAll('style'));
                let styleTags = [];
                styleEles.forEach((style, index) => {
                    styleTags.push({
                        id: style.id || `style-${index}`,
                        content: style.textContent,
                    });
                });

                // 获取所有<link>标签中的样式表
                const linkEles = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));

                let linkTags = [];
                linkEles.forEach((link, index) => {
                    linkTags.push({
                        id: link.id || `stylesheet-${index}`,
                        href: link.href,
                    });
                });

                // 调用渲染进程中的预览方法,并把打印内容和样式一起传过去
                ipcRenderer.invoke('show-preview', {
                    printContent: printContent,
                    styleTags: styleTags,
                    linkTags: linkTags
                });
            } catch (error) {
                console.error('打印错误:', error);
            }
        };

        function showPreview() {
            window.print();
        }
    </script>
</body>

</html>

3. 加载打印预览界面

获取到要打印的内容后,单独开启一个无边框和菜单栏的窗口,窗口内加载一个用于展示要打印内容的静态文件preview.html,在这个静态文件里面处理需要展示的内容和打印的逻辑

JavaScript 复制代码
// main.js

// 创建预览窗口
function createPreviewWindow(printData) {
    // 获取主屏幕的尺寸
    const primaryDisplay = screen.getPrimaryDisplay()
    const { width, height } = primaryDisplay.workAreaSize

    previewWindow = new BrowserWindow({
        width: parseInt(width * 0.7),
        height: parseInt(height * 0.9),
        frame: false,
        autoHideMenuBar: true,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
        }
    });

    // 加载预览页面
    previewWindow.loadFile('preview.html');

    // 等待页面加载完成后发送打印数据
    previewWindow.webContents.on('did-finish-load', () => {
        previewWindow.webContents.send('print-data', printData);
    });

    previewWindow.webContents.openDevTools()
}

// 处理打印预览请求
ipcMain.handle('show-preview', async (event, printData) => {
    try {
        // 调用创建预览窗口
        createPreviewWindow(printData);
        return { success: true };
    } catch (error) {
        console.error('预览错误:', error);
        return { success: false, error: error.message };
    }
});

4. 预览页面的实现

预览页面可以自由编写布局和样式,主要逻辑是把需要打印的内容和样式加入到页面中,同时加载本地可用的打印机和其他打印配置

html 复制代码
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>打印预览</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            display: flex;
            height: 100vh;
            overflow: hidden;
            background-color: #f0f0f0;
        }

        .main-container {
            display: flex;
            width: 100%;
            height: 100%;
        }

        .print-document {
            flex-grow: 1;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
            overflow-y: auto;
        }
        
        /* 这里不能缺少,用于打印的时候不把侧边栏也打印出来 */
        @media print {
            .print-sidebar {
                display: none;
            }
        }
    </style>
</head>

<body>
    <div class="main-container">
        <div id="print-container" class="print-document">

        </div>
        <div class="print-sidebar">
            <div class="sidebar-header">
                <h2>打印</h2>
            </div>
            <div class="print-section">
                <label>目标打印机</label>
                <div class="select-wrapper">
                    <select id="printer"></select>
                </div>
            </div>
            <div class="print-section">
                <label>页面</label>
                <div class="select-wrapper">
                    <select id="pageSize">
                        <option value="A4">全部</option>
                        <option value="A3">A3</option>
                        <option value="B5">B5</option>
                        <option value="letter">Letter</option>
                    </select>
                </div>
            </div>
            
             <!-- 其它你需要的配置项 -->
             
            <div class="sidebar-actions">
                <button class="print-button" onclick="print()">打印</button>
                <button class="cancel-button" onclick="closePreview()">取消</button>
            </div>
        </div>
    </div>

    <script>
        const { ipcRenderer } = require('electron');

        // 接收打印数据
        ipcRenderer.on('print-data', (event, data) => {

            if (data.printContent) {
                // 把要打印的内容放到页面上
                const printEle = document.getElementById('print-container')
                printEle.innerHTML = data.printContent
            }

            const head = document.head || document.getElementsByTagName('head')[0];

            // 如果有style标签,把style标签放到页面中
            if (data.styleTags && data.styleTags.length > 0) {

                data.styleTags.forEach((style, index) => {
                    const styleElement = document.createElement('style');
                    styleElement.textContent = style.content;
                    head.appendChild(styleElement);
                });
            }

            // 如果有link标签,添加link标签
            if (data.linkTags && data.linkTags.length > 0) {
                data.linkTags.forEach((link, index) => {
                    const linkElement = document.createElement('link');
                    linkElement.rel = 'stylesheet';
                    linkElement.type = 'text/css';
                    linkElement.href = link.href;
                    head.appendChild(linkElement);
                });
            }

            // 加载本机可用的打印机列表
            loadPrinters();
        });

        // 加载打印机列表
        async function loadPrinters() {
            try {
                const printers = await ipcRenderer.invoke('get-printers');
                const printerSelect = document.getElementById('printer');
                printerSelect.innerHTML = '';
                printers.forEach(printer => {
                    const option = document.createElement('option');
                    option.value = printer.name;
                    option.textContent = printer.name;
                    printerSelect.appendChild(option);
                });
            } catch (error) {
                console.error('加载打印机列表失败:', error);
            }
        }

        // 打印功能
        async function print() {
            try {
                const options = {
                    deviceName: document.getElementById('printer').value,
                    pageSize: document.getElementById('pageSize').value,
                    landscape: document.getElementById('orientation').value === 'landscape',
                    color: document.getElementById('color').value === 'true',
                    printBackground: true,
                    marginType: 'printableArea'
                };

                // 调用主线程的打印功能
                await ipcRenderer.invoke('do-print', options);

            } catch (error) {
                alert('打印错误:' + error.message);
            }
        }

        // 关闭预览窗口
        function closePreview() {
            ipcRenderer.invoke('close-preview');
        }

        // 初始加载打印机
        document.addEventListener('DOMContentLoaded', () => {
            loadPrinters();
        });
    </script>
</body>

</html>

以下是获取打印机列表和执行打印的逻辑

js 复制代码
// main.js

// 获取打印机列表
ipcMain.handle('get-printers', async () => {
    try {
        const printers = await mainWindow.webContents.getPrintersAsync();
        return printers;
    } catch (error) {
        console.error('获取打印机列表失败:', error);
        return [];
    }
});

// 处理实际打印请求
ipcMain.handle('do-print', async (event, options) => {
    try {
        const printOptions = {
            silent: true, //静默打印
            printBackground: true,
            deviceName: options.deviceName || '',
            copies: options.copies || 1,
            pageSize: options.pageSize || 'A4',
            margins: {
                marginType: options.marginType || 'printableArea'
            },
            landscape: options.landscape || false,
            scale: options.scale || 1.0,
            color: options.color || true,
            headerFooter: options.headerFooter || false,
            pageRanges: options.pageRanges || '',
            collate: options.collate || true,
            duplex: options.duplex || 'none'
        };

        await previewWindow.webContents.print(printOptions);
    } catch (error) {
        console.error('打印错误:', error);
    }
});

通过以上方式就实现了不改变原来应用调用window.print()的方式,实现打印预览和打印的逻辑,具体业务中,可以把重写window.print()这一段逻辑单独写成一个js文件,全局加载,如果希望保留原有的打印,可以判断一下是否在electron的环境中,如果在electron的环境才进行方法覆盖,这样在就能保证无论是通过浏览器打开或者在electron打开,都可以完成对应的预览功能。

相关推荐
张童瑶1 天前
Vue Electron 使用来给若依系统打包成exe程序,出现登录成功但是不跳转页面(已解决)
javascript·vue.js·electron
朝阳391 天前
Electron-vite【实战】MD 编辑器 -- 编辑区(含工具条、自定义右键快捷菜单、快捷键编辑、拖拽打开文件等)
javascript·electron·编辑器
朝阳391 天前
Electron-vite【实战】MD 编辑器 -- 大纲区(含自动生成大纲,大纲缩进,折叠大纲,滚动同步高亮大纲,点击大纲滚动等)
javascript·electron·编辑器
持久的棒棒君3 天前
npm安装electron下载太慢,导致报错
前端·electron·npm
LEAFF3 天前
Electron License 激活系统集成开发步骤
vue.js·typescript·electron
Lstmxx3 天前
Electron:使用数据流的形式加载本地视频
前端·electron·node.js
rookiefishs3 天前
如何控制electron的应用在指定的分屏上打开🧐
前端·javascript·electron
高山我梦口香糖4 天前
[electron]预脚本不显示内联script
前端·javascript·electron
卸任6 天前
Electron自制翻译工具:增加中英互译
前端·react.js·electron