indows程序,其交互的下一层就是Windows操作系统,所以需要用到诸多的Windows API。其它操作系统的应用程序,就要用到其它操作系统的API进行程序开发。构成应用程序GUI的所有元素都必须调用操作系统函数,以编程方式建立。
GUI应用程序除了交互方式增加了图形和鼠标以外,整个程序也是通过消息循环与操作系统交互形成事件驱动的。
Windows API是在C还是主要通用语言的年代开发的,很久以后C++才出现,因此经常用在Windows和应用程序之间传递数据的是结构而不是类。
最简单的仅使用Windows API的Windows程序而言,需要编写两个函数。一个是WinMain()函数,程序的执行是从这里开始的,基本的程序初始化工作也是在这里完成的。另一个是WindowProc()函数,该函数是由Windows调用的,用来给应用程序传递消息,应用程序的大多数专用代码都在这里。
新建一个win32 Application的新的空工程,并插入一个cpp文件,以下代码就是上述两个函数的构建:
#include <windows.h> #include <tchar.h> LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); // Listing OFWIN_1 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; // Structure to hold our window's attributes static LPCTSTR szAppName=_T("OFWin") ; // Define window class name HWND hWnd; // Window handle MSG msg; // Windows message structure wc.cbSize=sizeof(WNDCLASSEX); // Set structure size // Redraw the window if the size changes wc.style=CS_HREDRAW | CS_VREDRAW; // Define the message handling function wc.lpfnWndProc=WindowProc; wc.cbClsExtra=0; // No extra bytes after the window class wc.cbWndExtra=0; // structure or the window instance wc.hInstance=hInstance; // Application instance handle // Set default application icon wc.hIcon=LoadIcon(NULL, IDI_APPLICATION); // Set window cursor to be the standard arrow wc.hCursor=LoadCursor(NULL, IDC_ARROW); // Set gray brush for background color wc.hbrBackground=static_cast<HBRUSH>(GetStockObject(GRAY_BRUSH)); wc.lpszMenuName=NULL; // No menu wc.lpszClassName=szAppName; // Set class name wc.hIconSm=NULL; // Default small icon // Now register our window class RegisterClassEx(&wc); // Now we can create the window hWnd=CreateWindow( szAppName, // the window class name _T("A Basic Window the Hard Way"), // The window title WS_OVERLAPPEDWINDOW, // Window style as overlapped CW_USEDEFAULT, // Default screen position of upper left CW_USEDEFAULT, // corner of our window as x,y. CW_USEDEFAULT, // Default window size width ... CW_USEDEFAULT, // ... and height NULL, // No parent window NULL, // No menu hInstance, // Program Instance handle NULL // No window creation data ); ShowWindow(hWnd, nCmdShow); // Display the window UpdateWindow(hWnd); // Redraw window client area // The message loop while (GetMessage(&msg, NULL, 0, 0)==TRUE) // Get any messages { TranslateMessage(&msg); // Translate the message DispatchMessage(&msg); // Dispatch the message } return static_cast<int>(msg.wParam); // End, so return to Windows } // Listing OFWIN_2 LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) // Process selected messages { case WM_PAINT: // Message is to redraw the window HDC hDC; PAINTSTRUCT PaintSt; // Structure defining area to be drawn hDC=BeginPaint(hWnd, &PaintSt) ; // Prepare to draw the window // Get upper left and lower right of client area RECT aRect; // A working rectangle GetClientRect(hWnd, &aRect); SetBkMode(hDC, TRANSPARENT); // Set text background mode // Now draw the text in the window client area DrawText( hDC, // Device context handle _T("仅使用Windows API的Windows程序:WinMain() + WindowProc()"), -1, // Indicate null terminated string &aRect, // Rectangle in which text is to be drawn DT_SINGLELINE | // Text format - single line DT_CENTER | // - centered in the line DT_VCENTER); // - line centered in aRect EndPaint(hWnd, &PaintSt); // Terminate window redraw operation return 0; case WM_DESTROY: // Window is being destroyed PostQuitMessage(0); return 0; } return DefWindowProc(hWnd, message, wParam, lParam); }
如果是直接新建CPP文件运行,编译时,工程→设置→连接,将下面的“console”改“为windows”:
运行效果:
Windows API覆盖了Windows与应用程序之间通信的所有方面。VC以面向对象的方式以C++语言重新组织了这些API函数,并提供了在C++中使用该接口更容易的方法,且带有更多的默认功能。
MFC提供了许多的Application Wizard,让用户几乎不需要写任何代码,就可以完成不同类别应用程序框架的构建,但这样会让用户对MFC程序的本质只有模糊的概念。为了更清楚到认识到MFC程序的本质,以下是不使用Application Wizard建立一个简单的MFC应用程序:
#include <afxwin.h> // For the class library // Application class definition class COurApp: public CWinApp { public: virtual BOOL InitInstance(); }; // Window class definition class COurWnd: public CFrameWnd { public: // Constructor COurWnd() { Create(NULL, _T("Our Dumb MFC Application")); } }; // Function to create an instance of the main application window BOOL COurApp::InitInstance(void) { m_pMainWnd=new COurWnd; // Construct a window object... m_pMainWnd->ShowWindow(m_nCmdShow); // ...and display it return TRUE; } // Application object definition at global scope COurApp AnApplication; // Define an application object
从以上代码可以看出,通过包含afxwin.h,派生一个应用程序类COurApp与一个窗口类COurWnd即建立了MFC程序框架。用MFC类库构建,相对于API函数库,更简洁、清晰和便利。相同的是,看起来复杂的GUI,其构件GUI窗口元素都是通过API绘图函数或类库的绘图函数绘制而成。
参考:http://www.wrox.com/WileyCDA/WroxTitle/Ivor-Horton-s-Beginning-Visual-C-2013.productCd-1118845714.html
-End-
语言基础:运行环境
运行环境
windows:
windows是大家最熟悉的平台,使用起来最得心应手,windows开发C语言也是大家入手最快的,windows下最常见的一些开发环境如下: 1、Turbo C Turbo C是美国Borland公司的产品,主要版本有:2.0.0\tc for win等,是学生党们听说过的最多版本,现在基本上没人用了 2、Visual C++ 6.0(vc6.0) vc6.0是微软历史上最成功的一款集成开发环境,c/c++/mfc/vb/vf等语言都可以在这个环境下得到很好的支持,学习windows下编程必备的神器。虽然它也存在很多问题: 体积有些大(上百MB),有时候编译程序时会假死(需要重启软件),对windows 8支持不是很好(但绝对还是可以用的)。 3、visual Studio 2002 2005 2008 2010 2012 实际上准确的来说,vc6.0是visual Studio 6.0的一个组件 4、其它开发环境 DEV c++、code::blocks、C-Free、各种各样的单片机、arm单板开发环境(ADS、IAR..) DevC++是一款windows下C/C++的免费开发工具,它体积小巧,容易上手,尤其适合新手使用(本人用的就是这款)想要一起学习C++的可以加群248894430,群内有各种资料满足大家
linux\unix:
讲完了windows下的开发环境,下面我们来看看另一个世界——unix世界如何进行c语言学习 光明料理界有微软这座大山,黑暗料理界也有他的高山仰止,不同于windows的一站式购物,unix世界追求的是自由、自主、自信 windows下大部分的开发环境都称之为:IDE(Integrated Development Environment),集成开发环境,编辑、编译、调试、管理一站解决 unix下没有很友好的集成开发环境(即使有,大家也很少使用,使用集成开发环境的会被耻笑的),unix下使用的是:vi/emacs + gcc + gdb + make
DevCX++的安装
http://jingyan.baidu.com/article/948f5924014f72d80ff5f908.html
新建工程
1.点击菜单栏的“文件->新建->工程…”
2.选择“Console Application”、“C工程”,名称命名为“hello”,然后点击“确定”。
想要一起学习C++的可以加群248894430,群内有各种资料满足大家
使用Electron开发客户端程序已经有一段时间了,整体感觉还是非常不错的,其中也遇到了一些坑点,本文是从【运行原理】到【实际应用】对Electron进行一次系统性的总结。【多图,长文预警~】
本文所有实例代码均在我的github electron-react上,结合代码阅读文章效果更佳。另外electron-react还可作为使用Electron + React + Mobx + Webpack技术栈的脚手架工程。
github:https://github.com/ConardLi/electron-react
桌面应用程序,又称为 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些区别。桌面应用程序 将 GUI 程序从GUI 具体为“桌面”,使冷冰冰的像块木头一样的电脑概念更具有 人性化,更生动和富有活力。
我们电脑上使用的各种客户端程序都属于桌面应用程序,近年来WEB和移动端的兴起让桌面程序渐渐暗淡,但是在某些日常功能或者行业应用中桌面应用程序仍然是必不可少的。
传统的桌面应用开发方式,一般是下面两种:
直接将语言编译成可执行文件,直接调用系统API,完成UI绘制等。这类开发技术,有着较高的运行效率,但一般来说,开发速度较慢,技术要求较高,例如:
一开始就有本地开发和UI开发。一次编译后,得到中间文件,通过平台或虚机完成二次加载编译或解释运行。运行效率低于原生编译,但平台优化后,其效率也是比较可观的。就开发速度方面,比原生编译技术要快一些。例如:
不过,上面两种对前端开发人员太不友好了,基本是前端人员不会涉及的领域,但是在这个【大前端】的时代,前端开发者正在想方设法涉足各个领域,使用WEB技术开发客户端的方式横空出世。
使用WEB技术进行开发,利用浏览器引擎完成UI渲染,利用Node.js实现服务器端JS编程并可以调用系统API,可以把它想像成一个套了一个客户端外壳的WEB应用。
在界面上,WEB的强大生态为UI带来了无限可能,并且开发、维护成本相对较低,有WEB开发经验的前端开发者很容易上手进行开发。
本文就来着重介绍使用WEB技术开发客户端程序的技术之一【electron】
Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库。 Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的。
当然,我们也要认清它的缺点:性能比原生桌面应用要低,最终打包后的应用比原生应用大很多。
兼容性
虽然你还在用WEB技术进行开发,但是你不用再考虑兼容性问题了,你只需要关心你当前使用Electron的版本对应Chrome的版本,一般情况下它已经足够新来让你使用最新的API和语法了,你还可以手动升级Chrome版本。同样的,你也不用考虑不同浏览器带的样式和代码兼容问题。
Node环境
这可能是很多前端开发者曾经梦想过的功能,在WEB界面中使用Node.js提供的强大API,这意味着你在WEB页面直接可以操作文件,调用系统API,甚至操作数据库。当然,除了完整的Node API,你还可以使用额外的几十万个npm模块。
跨域
你可以直接使用Node提供的request模块进行网络请求,这意味着你无需再被跨域所困扰。
强大的扩展性
借助node-ffi,为应用程序提供强大的扩展性(后面的章节会详细介绍)。
现在市面上已经有非常多的应用在使用Electron进行开发了,包括我们熟悉的VS Code客户端、GitHub客户端、Atom客户端等等。印象很深的,去年迅雷在发布迅雷X10.1时的文案:
从迅雷X 10.1版本开始,我们采用Electron软件框架完全重写了迅雷主界面。使用新框架的迅雷X可以完美支持2K、4K等高清显示屏,界面中的文字渲染也更加清晰锐利。从技术层面来说,新框架的界面绘制、事件处理等方面比老框架更加灵活高效,因此界面的流畅度也显著优于老框架的迅雷。至于具体提升有多大?您一试便知。
你可以打开VS Code,点击【帮助】【切换开发人员工具】来调试VS Code客户端的界面。
Electron 结合了 Chromium、Node.js 和用于调用操作系统本地功能的API。
Chromium是Google为发展Chrome浏览器而启动的开源项目,Chromium相当于Chrome的工程版或称实验版,新功能会率先在Chromium上实现,待验证后才会应用在Chrome上,故Chrome的功能会相对落后但较稳定。
Chromium为Electron提供强大的UI能力,可以在不考虑兼容性的情况下开发界面。
Node.js是一个让JavaScript运行在服务端的开发平台,Node使用事件驱动,非阻塞I/O模型而得以轻量和高效。
单单靠Chromium是不能具备直接操作原生GUI能力的,Electron内集成了Nodejs,这让其在开发界面的同时也有了操作系统底层API的能力,Nodejs 中常用的 Path、fs、Crypto 等模块在 Electron 可以直接使用。
为了提供原生系统的GUI支持,Electron内置了原生应用程序接口,对调用一些系统功能,如调用系统通知、打开系统文件夹提供支持。
在开发模式上,Electron在调用系统API和绘制界面上是分离开发的,下面我们来看看Electron关于进程如何划分。
Electron区分了两种进程:主进程和渲染进程,两者各自负责自己的职能。
Electron 运行package.json的 main 脚本的进程被称为主进程。一个 Electron 应用总是有且只有一个主进程。
职责:
可调用的API:
由于 Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。 每个Electron 中的 web页面运行在它自己的渲染进程中。
主进程使用 BrowserWindow 实例创建页面。 每个 BrowserWindow 实例都在自己的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
你可以把渲染进程想像成一个浏览器窗口,它能存在多个并且相互独立,不过和浏览器不同的是,它能调用Node API。
职责:
可调用的API:
在上面的章节我们提到,渲染进和主进程分别可调用的Electron API。所有Electron的API都被指派给一种进程类型。 许多API只能被用于主进程中,有些API又只能被用于渲染进程,又有一些主进程和渲染进程中都可以使用。
你可以通过如下方式获取Electron API
const { BrowserWindow, ... }=require('electron')
复制代码
下面是一些常用的Electron API:
在后面的章节我们会选择其中常用的模块进行详细介绍。
你可以同时在Electron的主进程和渲染进程使用Node.js API,)所有在Node.js可以使用的API,在Electron中同样可以使用。
import {shell} from 'electron';
import os from 'os';
document.getElementById('btn').addEventListener('click', ()=> {
shell.showItemInFolder(os.homedir());
})
复制代码
有一个非常重要的提示: 原生Node.js模块 (即指,需要编译源码过后才能被使用的模块) 需要在编译后才能和Electron一起使用。
主进程和渲染进程虽然拥有不同的职责,然是他们也需要相互协作,互相通讯。
例如:在web页面管理原生GUI资源是很危险的,会很容易泄露资源。所以在web页面,不允许直接调用原生GUI相关的API。渲染进程如果想要进行原生的GUI操作,就必须和主进程通讯,请求主进程来完成这些操作。
ipcRenderer 是一个 EventEmitter 的实例。 你可以使用它提供的一些方法,从渲染进程发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。
在渲染进程引入ipcRenderer:
import { ipcRenderer } from 'electron';
复制代码
异步发送:
通过 channel 发送同步消息到主进程,可以携带任意参数。
在内部,参数会被序列化为 JSON,因此参数对象上的函数和原型链不会被发送。
ipcRenderer.send('async-render', '我是来自渲染进程的异步消息');
复制代码
同步发送:
const msg=ipcRenderer.sendSync('sync-render', '我是来自渲染进程的同步消息');
复制代码
注意: 发送同步消息将会阻塞整个渲染进程,直到收到主进程的响应。
主进程监听消息:
ipcMain模块是EventEmitter类的一个实例。 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。
ipcMain.on:监听 channel,当接收到新的消息时 listener 会以 listener(event, args...) 的形式被调用。
ipcMain.on('sync-render', (event, data)=> {
console.log(data);
});
复制代码
在主进程中可以通过BrowserWindow的webContents向渲染进程发送消息,所以,在发送消息前你必须先找到对应渲染进程的BrowserWindow对象。:
const mainWindow=BrowserWindow.fromId(global.mainId);
mainWindow.webContents.send('main-msg', `ConardLi]`)
复制代码
根据消息来源发送:
在ipcMain接受消息的回调函数中,通过第一个参数event的属性sender可以拿到消息来源渲染进程的webContents对象,我们可以直接用此对象回应消息。
ipcMain.on('sync-render', (event, data)=> {
console.log(data);
event.sender.send('main-msg', '主进程收到了渲染进程的【异步】消息!')
});
复制代码
渲染进程监听:
ipcRenderer.on:监听 channel, 当新消息到达,将通过listener(event, args...)调用 listener。
ipcRenderer.on('main-msg', (event, msg)=> {
console.log(msg);
})
复制代码
ipcMain 和 ipcRenderer 都是 EventEmitter 类的一个实例。EventEmitter 类是 NodeJS 事件的基础,它由 NodeJS 中的 events 模块导出。
EventEmitter 的核心就是事件触发与事件监听器功能的封装。它实现了事件模型需要的接口, 包括 addListener,removeListener, emit 及其它工具方法. 同原生 JavaScript 事件类似, 采用了发布/订阅(观察者)的方式, 使用内部 _events 列表来记录注册的事件处理器。
我们通过 ipcMain和ipcRenderer 的 on、send 进行监听和发送消息都是 EventEmitter 定义的相关接口。
remote 模块为渲染进程(web页面)和主进程通信(IPC)提供了一种简单方法。 使用 remote 模块, 你可以调用 main 进程对象的方法, 而不必显式发送进程间消息, 类似于 Java 的 RMI 。
import { remote } from 'electron';
remote.dialog.showErrorBox('主进程才有的dialog模块', '我是使用remote调用的')
复制代码
但实际上,我们在调用远程对象的方法、函数或者通过远程构造函数创建一个新的对象,实际上都是在发送一个同步的进程间消息。
在上面通过 remote 模块调用 dialog 的例子里。我们在渲染进程中创建的 dialog 对象其实并不在我们的渲染进程中,它只是让主进程创建了一个 dialog 对象,并返回了这个相对应的远程对象给了渲染进程。
Electron并没有提供渲染进程之间相互通信的方式,我们可以在主进程中建立一个消息中转站。
渲染进程之间通信首先发送消息到主进程,主进程的中转站接收到消息后根据条件进行分发。
在两个渲染进程间共享数据最简单的方法是使用浏览器中已经实现的HTML5 API。 其中比较好的方案是用Storage API, localStorage,sessionStorage 或者 IndexedDB。
就像在浏览器中使用一样,这种存储相当于在应用程序中永久存储了一部分数据。有时你并不需要这样的存储,只需要在当前应用程序的生命周期内进行一些数据的共享。这时你可以用 Electron 内的 IPC 机制实现。
将数据存在主进程的某个全局变量中,然后在多个渲染进程中使用 remote 模块来访问它。
在主进程中初始化全局变量:
global.mainId=...;
global.device={...};
global.__dirname=__dirname;
global.myField={ name: 'ConardLi' };
复制代码
在渲染进程中读取:
import { ipcRenderer, remote } from 'electron';
const { getGlobal }=remote;
const mainId=getGlobal('mainId')
const dirname=getGlobal('__dirname')
const deviecMac=getGlobal('device').mac;
复制代码
在渲染进程中改变:
getGlobal('myField').name='code秘密花园';
复制代码
多个渲染进程共享同一个主进程的全局变量,这样即可达到渲染进程数据共享和传递的效果。
主进程模块BrowserWindow用于创建和控制浏览器窗口。
mainWindow=new BrowserWindow({
width: 1000,
height: 800,
// ...
});
mainWindow.loadURL('http://www.conardli.top/');
复制代码
你可以在这里查看它所有的构造参数。
无框窗口是没有镶边的窗口,窗口的部分(如工具栏)不属于网页的一部分。
在BrowserWindow的构造参数中,将frame设置为false可以指定窗口为无边框窗口,将工具栏隐藏后,就会产生两个问题:
可以通过指定titleBarStyle选项来再将工具栏按钮显示出来,将其设置为hidden表示返回一个隐藏标题栏的全尺寸内容窗口,在左上角仍然有标准的窗口控制按钮。
new BrowserWindow({
width: 200,
height: 200,
titleBarStyle: 'hidden',
frame: false
});
复制代码
默认情况下, 无边框窗口是不可拖拽的。我们可以在界面中通过CSS属性-webkit-app-region: drag手动制定拖拽区域。
在无框窗口中, 拖动行为可能与选择文本冲突,可以通过设定-webkit-user-select: none;禁用文本选择:
.header {
-webkit-user-select: none;
-webkit-app-region: drag;
}
复制代码
相反的,在可拖拽区域内部设置 -webkit-app-region: no-drag则可以指定特定不可拖拽区域。
通过将transparent选项设置为true, 还可以使无框窗口透明:
new BrowserWindow({
transparent: true,
frame: false
});
复制代码
使用 webview 标签在Electron 应用中嵌入 "外来" 内容。外来内容包含在 webview 容器中。 应用中的嵌入页面可以控制外来内容的布局和重绘。
与 iframe 不同, webview 在与应用程序不同的进程中运行。它与您的网页没有相同的权限, 应用程序和嵌入内容之间的所有交互都将是异步的。
dialog 模块提供了api来展示原生的系统对话框,例如打开文件框,alert框,所以web应用可以给用户带来跟系统应用相同的体验。
注意:dialog是主进程模块,想要在渲染进程调用可以使用remote
dialog.showErrorBox用于显示一个显示错误消息的模态对话框。
remote.dialog.showErrorBox('错误', '这是一个错误弹框!')
复制代码
dialog.showErrorBox用于调用系统对话框,可以为指定几种不同的类型: "none", "info", "error", "question" 或者 "warning"。
在 Windows 上, "question" 与"info"显示相同的图标, 除非你使用了 "icon" 选项设置图标。 在 macOS 上, "warning" 和 "error" 显示相同的警告图标
remote.dialog.showMessageBox({
type: 'info',
title: '提示信息',
message: '这是一个对话弹框!',
buttons: ['确定', '取消']
}, (index)=> {
this.setState({ dialogMessage: `【你点击了${index ? '取消' : '确定'}!!】` })
})
复制代码
dialog.showOpenDialog用于打开或选择系统目录。
remote.dialog.showOpenDialog({
properties: ['openDirectory', 'openFile']
}, (data)=> {
this.setState({ filePath: `【选择路径:${data[0]}】 ` })
})
复制代码
这里推荐直接使用HTML5 API,它只能在渲染器进程中使用。
let options={
title: '信息框标题',
body: '我是一条信息~~~',
}
let myNotification=new window.Notification(options.title, options)
myNotification.onclick=()=> {
this.setState({ message: '【你点击了信息框!!】' })
}
复制代码
通过remote获取到主进程的process对象,可以获取到当前应用的各个版本信息:
获取当前应用根目录:
remote.app.getAppPath()
复制代码
使用node的os模块获取当前系统根目录:
os.homedir();
复制代码
Electron提供的clipboard在渲染进程和主进程都可使用,用于在系统剪贴板上执行复制和粘贴操作。
以纯文本的形式写入剪贴板:
clipboard.writeText(text[, type])
复制代码
以纯文本的形式获取剪贴板的内容:
clipboard.readText([type])
复制代码
desktopCapturer用于从桌面捕获音频和视频的媒体源的信息。它只能在渲染进程中被调用。
下面的代码是一个获取屏幕截图并保存的实例:
getImg=()=> {
this.setState({ imgMsg: '正在截取屏幕...' })
const thumbSize=this.determineScreenShotSize()
let options={ types: ['screen'], thumbnailSize: thumbSize }
desktopCapturer.getSources(options, (error, sources)=> {
if (error) return console.log(error)
sources.forEach((source)=> {
if (source.name==='Entire screen' || source.name==='Screen 1') {
const screenshotPath=path.join(os.tmpdir(), 'screenshot.png')
fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error)=> {
if (error) return console.log(error)
shell.openExternal(`file://${screenshotPath}`)
this.setState({ imgMsg: `截图保存到: ${screenshotPath}` })
})
}
})
})
}
determineScreenShotSize=()=> {
const screenSize=screen.getPrimaryDisplay().workAreaSize
const maxDimension=Math.max(screenSize.width, screenSize.height)
return {
width: maxDimension * window.devicePixelRatio,
height: maxDimension * window.devicePixelRatio
}
}
复制代码
应用程序的菜单可以帮助我们快捷的到达某一功能,而不借助客户端的界面资源,一般菜单分为两种:
Electron为我们提供了Menu模块用于创建本机应用程序菜单和上下文菜单,它是一个主进程模块。
你可以通过Menu的静态方法buildFromTemplate(template),使用自定义菜单模版来构造一个菜单对象。
template是一个MenuItem的数组,我们来看看MenuItem的几个重要参数:
推荐:最好指定role与标准角色相匹配的任何菜单项,而不是尝试手动实现click函数中的行为。内置role行为将提供最佳的本地体验。
下面的实例是一个简单的额菜单template。
const template=[
{
label: '文件',
submenu: [
{
label: '新建文件',
click: function () {
dialog.showMessageBox({
type: 'info',
message: '嘿!',
detail: '你点击了新建文件!',
})
}
}
]
},
{
label: '编辑',
submenu: [{
label: '剪切',
role: 'cut'
}, {
label: '复制',
role: 'copy'
}, {
label: '粘贴',
role: 'paste'
}]
},
{
label: '最小化',
role: 'minimize'
}
]
复制代码
使用Menu的静态方法setApplicationMenu,可创建一个应用程序菜单,在 Windows 和 Linux 上,menu将被设置为每个窗口的顶层菜单。
注意:必须在模块ready事件后调用此 API app。
我们可以根据应用程序不同的的生命周期,不同的系统对菜单做不同的处理。
app.on('ready', function () {
const menu=Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
})
app.on('browser-window-created', function () {
let reopenMenuItem=findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled=false
})
app.on('window-all-closed', function () {
let reopenMenuItem=findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled=true
})
if (process.platform==='win32') {
const helpMenu=template[template.length - 1].submenu
addUpdateMenuItems(helpMenu, 0)
}
复制代码
使用Menu的实例方法menu.popup可自定义弹出上下文菜单。
let m=Menu.buildFromTemplate(template)
document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e)=> {
e.preventDefault()
m.popup({ window: remote.getCurrentWindow() })
})
复制代码
在菜单选项中,我们可以指定一个accelerator属性来指定操作的快捷键:
{
label: '最小化',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}
复制代码
另外,我们还可以使用globalShortcut来注册全局快捷键。
globalShortcut.register('CommandOrControl+N', ()=> {
dialog.showMessageBox({
type: 'info',
message: '嘿!',
detail: '你触发了手动注册的快捷键.',
})
})
复制代码
CommandOrControl代表在macOS上为Command键,以及在Linux和Windows上为Control键。
很多情况下程序中使用的打印都是用户无感知的。并且想要灵活的控制打印内容,往往需要借助打印机给我们提供的api再进行开发,这种开发方式非常繁琐,并且开发难度较大。第一次在业务中用到Electron其实就是用到它的打印功能,这里就多介绍一些。
Electron提供的打印api可以非常灵活的控制打印设置的显示,并且可以通过html来书写打印内容。Electron提供了两种方式进行打印,一种是直接调用打印机打印,一种是打印到pdf。
并且有两种对象可以调用打印:
上面两种方式同时拥有print和printToPdf方法。
contents.print([options], [callback]);
复制代码
打印配置(options)中只有简单的三个配置:
首先要将我们使用的打印机名称配置好,并且要在调用打印前首先要判断打印机是否可用。
使用webContents的getPrinters方法可获取当前设备已经配置的打印机列表,注意配置过不是可用,只是在此设备上安装过驱动。
通过getPrinters获取到的打印机对象:electronjs.org/docs/api/st…
我们这里只管关心两个,name和status,status为0时表示打印机可用。
print的第二个参数callback是用于判断打印任务是否发出的回调,而不是打印任务完成后的回调。所以一般打印任务发出,回调函数即会调用并返回参数true。这个回调并不能判断打印是否真的成功了。
if (this.state.curretnPrinter) {
mainWindow.webContents.print({
silent: silent, printBackground: true, deviceName: this.state.curretnPrinter
}, ()=> { })
} else {
remote.dialog.showErrorBox('错误', '请先选择一个打印机!')
}
复制代码
printToPdf的用法基本和print相同,但是print的配置项非常少,而printToPdf则扩展了很多属性。这里翻了一下源码发现还有很多没有被贴进文档的,大概有三十几个,包括可以对打印的margin,打印页眉页脚等进行配置。
contents.printToPDF(options, callback)
复制代码
callback函数在打印失败或打印成功后调用,可获取打印失败信息或包含PDF数据的缓冲区。
const pdfPath=path.join(os.tmpdir(), 'webviewPrint.pdf');
const webview=document.getElementById('printWebview');
const renderHtml='我是被临时插入webview的内容...';
webview.executeJavaScript('document.documentElement.innerHTML=`' + renderHtml + '`;');
webview.printToPDF({}, (err, data)=> {
console.log(err, data);
fs.writeFile(pdfPath, data, (error)=> {
if (error) throw error
shell.openExternal(`file://${pdfPath}`)
this.setState({ webviewPdfPath: pdfPath })
});
});
复制代码
这个例子中的打印是使用webview完成的,通过调用executeJavaScript方法可动态向webview插入打印内容。
上面提到,使用webview和webcontent都可以调用打印功能,使用webcontent打印,首先要有一个打印窗口,这个窗口不能随时打印随时创建,比较耗费性能。可以将它在程序运行时启动好,并做好事件监听。
此过程需和调用打印的进行做好通信,大致过程如下:
可见通信非常繁琐,使用webview进行打印可实现同样的效果但是通信方式会变得简单,因为渲染进程和webview通信不需要经过主进程,通过如下方式即可:
const webview=document.querySelector('webview')
webview.addEventListener('ipc-message', (event)=> {
console.log(event.channel)
})
webview.send('ping');
const {ipcRenderer}=require('electron')
ipcRenderer.on('ping', ()=> {
ipcRenderer.sendToHost('pong')
})
复制代码
之前专门为ELectron打印写过一个DEMO:electron-print-demo有兴趣可以clone下来看一下。
下面是几个针对常用打印功能的工具函数封装。
/**
* 获取系统打印机列表
*/
export function getPrinters() {
let printers=[];
try {
const contents=remote.getCurrentWindow().webContents;
printers=contents.getPrinters();
} catch (e) {
console.error('getPrintersError', e);
}
return printers;
}
/**
* 获取系统默认打印机
*/
export function getDefaultPrinter() {
return getPrinters().find(element=> element.isDefault);
}
/**
* 检测是否安装了某个打印驱动
*/
export function checkDriver(driverMame) {
return getPrinters().find(element=> (element.options["printer-make-and-model"] || '').includes(driverMame));
}
/**
* 根据打印机名称获取打印机对象
*/
export function getPrinterByName(name) {
return getPrinters().find(element=> element.name===name);
}
复制代码
崩溃监控是每个客户端程序必备的保护功能,当程序崩溃时我们一般期望做到两件事:
electron为我们提供给了crashReporter来帮助我们记录崩溃日志,我们可以通过crashReporter.start来创建一个崩溃报告器:
const { crashReporter }=require('electron')
crashReporter.start({
productName: 'YourName',
companyName: 'YourCompany',
submitURL: 'https://your-domain.com/url-to-submit',
uploadToServer: true
})
复制代码
当程序发生崩溃时,崩溃报日志将被储存在临时文件夹中名为YourName Crashes的文件文件夹中。submitURL用于指定你的崩溃日志上传服务器。 在启动崩溃报告器之前,您可以通过调用app.setPath('temp', 'my/custom/temp')API来自定义这些临时文件的保存路径。你还可以通过crashReporter.getLastCrashReport()来获取上次崩溃报告的日期和ID。
我们可以通过webContents的crashed来监听渲染进程的崩溃,另外经测试有些主进程的崩溃也会触发该事件。所以我们可以根据主window是否被销毁来判断进行不同的重启逻辑,下面是整个崩溃监控的逻辑:
import { BrowserWindow, crashReporter, dialog } from 'electron';
// 开启进程崩溃记录
crashReporter.start({
productName: 'electron-react',
companyName: 'ConardLi',
submitURL: 'http://xxx.com', // 上传崩溃日志的接口
uploadToServer: false
});
function reloadWindow(mainWin) {
if (mainWin.isDestroyed()) {
app.relaunch();
app.exit(0);
} else {
// 销毁其他窗口
BrowserWindow.getAllWindows().forEach((w)=> {
if (w.id !==mainWin.id) w.destroy();
});
const options={
type: 'info',
title: '渲染器进程崩溃',
message: '这个进程已经崩溃.',
buttons: ['重载', '关闭']
}
dialog.showMessageBox(options, (index)=> {
if (index===0) mainWin.reload();
else mainWin.close();
})
}
}
export default function () {
const mainWindow=BrowserWindow.fromId(global.mainId);
mainWindow.webContents.on('crashed', ()=> {
const errorMessage=crashReporter.getLastCrashReport();
console.error('程序崩溃了!', errorMessage); // 可单独上传日志
reloadWindow(mainWindow);
});
}
复制代码
有的时候我们并不想让用户通过点关闭按钮的时候就关闭程序,而是把程序最小化到托盘,在托盘上做真正的退出操作。
首先要监听窗口的关闭事件,阻止用户关闭操作的默认行为,将窗口隐藏。
function checkQuit(mainWindow, event) {
const options={
type: 'info',
title: '关闭确认',
message: '确认要最小化程序到托盘吗?',
buttons: ['确认', '关闭程序']
};
dialog.showMessageBox(options, index=> {
if (index===0) {
event.preventDefault();
mainWindow.hide();
} else {
mainWindow=null;
app.exit(0);
}
});
}
function handleQuit() {
const mainWindow=BrowserWindow.fromId(global.mainId);
mainWindow.on('close', event=> {
event.preventDefault();
checkQuit(mainWindow, event);
});
}
复制代码
这时程序就再也找不到了,任务托盘中也没有我们的程序,所以我们要先创建好任务托盘,并做好事件监听。
windows平台使用ico文件可以达到更好的效果
export default function createTray() {
const mainWindow=BrowserWindow.fromId(global.mainId);
const iconName=process.platform==='win32' ? 'icon.ico' : 'icon.png'
tray=new Tray(path.join(global.__dirname, iconName));
const contextMenu=Menu.buildFromTemplate([
{
label: '显示主界面', click: ()=> {
mainWindow.show();
mainWindow.setSkipTaskbar(false);
}
},
{
label: '退出', click: ()=> {
mainWindow.destroy();
app.quit();
}
},
])
tray.setToolTip('electron-react');
tray.setContextMenu(contextMenu);
}
复制代码
在很多情况下,你的应用程序要和外部设备进行交互,一般情况下厂商会为你提供硬件设备的开发包,这些开发包基本上都是通过C++ 编写,在使用electron开发的情况下,我们并不具备直接调用C++代码的能力,我们可以利用node-ffi来实现这一功能。
node-ffi提供了一组强大的工具,用于在Node.js环境中使用纯JavaScript调用动态链接库接口。它可以用来为库构建接口绑定,而不需要使用任何C++代码。
注意node-ffi并不能直接调用C++代码,你需要将C++代码编译为动态链接库:在 Windows下是 Dll ,在 Mac OS下是 dylib ,Linux 是 so 。
node-ffi 加载 Library是有限制的,只能处理 C风格的 Library。
下面是一个简单的实例:
const ffi=require('ffi');
const ref=require('ref');
const SHORT_CODE=ref.refType('short');
const DLL=new ffi.Library('test.dll', {
Test_CPP_Method: ['int', ['string',SHORT_CODE]],
})
testCppMethod(str: String, num: number): void {
try {
const result: any=DLL.Test_CPP_Method(str, num);
return result;
} catch (error) {
console.log('调用失败~',error);
}
}
this.testCppMethod('ConardLi',123);
复制代码
上面的代码中,我们用ffi包装C++接口生成的动态链接库test.dll,并使用ref进行一些类型映射。
使用JavaScript调用这些映射方法时,推荐使用TypeScript来约定参数类型,因为弱类型的JavaScript在调用强类型语言的接口时可能会带来意想不到的风险。
借助这一能力,前端开发工程师也可以在IOT领域一展身手了~
一般情况下,我们的应用程序可能运行在多套环境下(production、beta、uat、moke、development...),不同的开发环境可能对应不同的后端接口或者其他配置,我们可以在客户端程序中内置一个简单的环境选择功能来帮助我们更高效的开发。
具体策略如下:
const envList=["moke", "beta", "development", "production"];
exports.envList=envList;
const urlBeta='https://wwww.xxx-beta.com';
const urlDev='https://wwww.xxx-dev.com';
const urlProp='https://wwww.xxx-prop.com';
const urlMoke='https://wwww.xxx-moke.com';
const path=require('path');
const pkg=require(path.resolve(global.__dirname, 'package.json'));
const build=pkg['build-config'];
exports.handleEnv={
build,
currentEnv: 'moke',
setEnv: function (env) {
this.currentEnv=env
},
getUrl: function () {
console.log('env:', build.env);
if (build.env==='production' || this.currentEnv==='production') {
return urlProp;
} else if (this.currentEnv==='moke') {
return urlMoke;
} else if (this.currentEnv==='development') {
return urlDev;
} else if (this.currentEnv==="beta") {
return urlBeta;
}
},
isDebugger: function () {
return build.env==='development'
}
}
复制代码
最后也是最重要的一步,将写好的代码打包成可运行的.app或.exe可执行文件。
这里我把打包氛围两部分来做,渲染进程打包和主进程打包。
一般情况下,我们的大部分业务逻辑代码是在渲染进程完成的,在大部分情况下我们仅仅需要对渲染进程进行更新和升级而不需要改动主进程代码,我们渲染进程的打包实际上和一般的web项目打包没有太大差别,使用webpack打包即可。
这里我说说渲染进程单独打包的好处:
打包完成的html和js文件,我们一般要上传到我们的前端静态资源服务器下,然后告知服务端我们的渲染进程有代码更新,这里可以说成渲染进程单独的升级。
注意,和壳的升级不同,渲染进程的升级仅仅是静态资源服务器上html和js文件的更新,而不需要重新下载更新客户端,这样我们每次启动程序的时候检测到离线包有更新,即可直接刷新读取最新版本的静态资源文件,即使在程序运行过程中要强制更新,我们的程序只需要强制刷新页面读取最新的静态资源即可,这样的升级对用户是非常友好的。
这里注意,一旦我们这样配置,就意味着渲染进程和主进程打包升级的完全分离,我们在启动主窗口时读取的文件就不应该再是本地文件,而是打包完成后放在静态资源服务器的文件。
为了方便开发,这里我们可以区分本地和线上加载不同的文件:
function getVersion (mac,current){
// 根据设备mac和当前版本获取最新版本
}
export default function () {
if (build.env==='production') {
const version=getVersion (mac,current);
return 'https://www.xxxserver.html/electron-react/index_'+version+'.html';
}
return url.format({
protocol: 'file:',
pathname: path.join(__dirname, 'env/environment.html'),
slashes: true,
query: { debugger: build.env==="development" }
});
}
复制代码
具体的webpack配置这里就不再贴出,可以到我的github electron-react的/scripts目录下查看。
这里需要注意,在开发环境下我们可以结合webpack的devServer和electron命令来启动app:
devServer: {
contentBase: './assets/',
historyApiFallback: true,
hot: true,
port: PORT,
noInfo: false,
stats: {
colors: true,
},
setup() {
spawn(
'electron',
['.'],
{
shell: true,
stdio: 'inherit',
}
)
.on('close', ()=> process.exit(0))
.on('error', e=> console.error(e));
},
},//...
复制代码
主进程,即将整个程序打包成可运行的客户端程序,常用的打包方案一般有两种,electron-packager和electron-builder。
electron-packager在打包配置上我觉得有些繁琐,而且它只能将应用直接打包为可执行程序。
这里我推荐使用electron-builder,它不仅拥有方便的配置 protocol 的功能、内置的 Auto Update、简单的配置 package.json 便能完成整个打包工作,用户体验非常不错。而且electron-builder不仅能直接将应用打包成exe app等可执行程序,还能打包成msi dmg等安装包格式。
你可以在package.json方便的进行各种配置:
"build": {
"productName": "electron-react", // app中文名称
"appId": "electron-react",// app标识
"directories": { // 打包后输出的文件夹
"buildResources": "resources",
"output": "dist/"
}
"files": [ // 打包后依然保留的源文件
"main_process/",
"render_process/",
],
"mac": { // mac打包配置
"target": "dmg",
"icon": "icon.ico"
},
"win": { // windows打包配置
"target": "nsis",
"icon": "icon.ico"
},
"dmg": { // dmg文件打包配置
"artifactName": "electron_react.dmg",
"contents": [
{
"type": "link",
"path": "/Applications",
"x": 410,
"y": 150
},
{
"type": "file",
"x": 130,
"y": 150
}
]
},
"nsis": { // nsis文件打包配置
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"shortcutName": "electron-react"
},
}
复制代码
执行electron-builder打包命令时,可指定参数进行打包。
--mac, -m, -o, --macos macOS打包
--linux, -l Linux打包
--win, -w, --windows Windows打包
--mwl 同时为macOS,Windows和Linux打包
--x64 x64 (64位安装包)
--ia32 ia32(32位安装包)
复制代码
关于主进程的更新你可以使用electron-builder自带的Auto Update模块,在electron-react也实现了手动更新的模块,由于篇幅原因这里就不再赘述,如果有兴趣可以到我的github查看main下的update模块。
electron-builder打包出来的App要比相同功能的原生客户端应用体积大很多,即使是空的应用,体积也要在100mb以上。原因有很多:
第一点;为了达到跨平台的效果,每个Electron应用都包含了整个V8引擎和Chromium内核。
第二点:打包时会将整个node_modules打包进去,大家都知道一个应用的node_module体积是非常庞大的,这也是使得Electron应用打包后的体积较大的原因。
第一点我们无法改变,我们可以从第二点对应用体积进行优化:Electron在打包时只会将denpendencies的依赖打包进去,而不会将 devDependencies 中的依赖进行打包。所以我们应尽可能的减少denpendencies中的依赖。在上面的进程中,我们使用webpack对渲染进程进行打包,所以渲染进程的依赖全部都可以移入devDependencies。
另外,我们还可以使用双packajson.json的方式来进行优化,把只在开发环境中使用到的依赖放在整个项目的根目录的package.json下,将与平台相关的或者运行时需要的依赖装在app目录下。具体详见two-package-structure。
本项目源码地址:https://github.com/ConardLi/electron-react
希望你阅读本篇文章后可以达到以下几点:
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。
想阅读更多优质文章、可关注我的github博客,你的star?、点赞和关注是我持续创作的动力!
github:https://github.com/ConardLi/ConardLi.github.io
原链接:https://juejin.im/post/5cfd2ec7e51d45554877a59f
*请认真填写需求信息,我们会在24小时内与您取得联系。