选择
优质文章,及时送达
本文原载于 SegmentFault 社区
作者:薛勤
原始冲动
最近一直在学习 Electron 开发桌面应用程序,目的是想做一个桌面编辑器,虽然一直在使用 Typora 这款神器,但无奈 Typora 太过国际化,在国内水土不服,无法满足我的一些需求。
比如实现本地图片上传到云端(mac 版可以借助 iPic),无法帮我把本地图片和文章一起发布到像 SegmentFault 等国内知名博客平台,要么使用一些免费或付费的图床,借助类似 iPic 的工具,把图片一键上传到云端。
我个人也尝试过七牛云的免费 10G 存储空间,但是说实话,这些免费的空间到最后一定是为了让你成为付费用户,各种限制各种吐槽在网上很容易可以搜索到。
免费的图床如新浪微博等,还算是比较好的图床工具,相比一些网络上的压根不知道啥公司甚至是归属个人的免费图床,新浪应该是比较靠谱的,相对来说可以保证图片的存活时间,我个人用过一些免费的图床网站,记得印象深刻的就是服务器出问题,网站挂个公告,曾经的图片再去访问就是默认的 404。
虽然新浪家大业大不是说倒闭就倒闭的,图片相对稳定可靠,不过新浪的图片服务器会检测访问来源 Referer 来防止外部网站引用,造成访问 403。
总结起来就是一句话,图片还是随着文章一键发布到博客平台比较好。要丢一起丢~
心理挣扎
缘起这个动机,但是下定决心依旧是困难重重。
我个人是一个 Java 工程师,虽说搞过 Andorid、HTML 前端,但对前端深感不适的我果断放弃了。对于桌面程序开发,我连 Swing 都不会,造一个 Markdown 编辑器有点难,何况还要加上这些定制功能。
犹犹豫豫,还是决定去尝试一下。于是调研写跨平台的一些途径。
先尝试 Swing,不过 Swing 不好实现我期望的一些功能,改成 JavaFX 倒是可以,不过说实话,写起来很累,太过繁琐,就放弃了。最后把目光瞄向 electron,就它了,HTML+Js+Css,听起来就很简单,事实证明,无论是测试还是打包都很方便。
决定之后,便开始进行 Electron 的系统学习。
迈出第一步
第一步就是安装 Electron 的本地开发环境,这也是大多数应用开发的第一步。
你需要安装 Node.js 在你的本地电脑,Electron 也是依赖于 Node.js 的环境,严格来说, Electron 通过将 Chromium 和 Node.js 合并到同一个运行时环境中,并将其打包为Mac,Windows 和 Linux 系统下的应用来实现这一目的。
关于 Electron 的具体开发流程,这里不再赘述,你完全可以在开发中使用Web前端开发的思维,除了在处理多个窗口之间交互的时候,就不得不了解Eelctron的进程机制。
主进程和渲染进程
Electron 运行 package.json 的 main 脚本的进程被称为主进程。在主进程中运行的脚本通过创建 web 页面来展示用户界面。一个 Electron 应用总是有且只有一个主进程。
由于 Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。每个 Electron 中的 web 页面运行在它自己的渲染进程中。
在普通的浏览器中,web 页面通常在沙盒环境中运行,并且无法访问操作系统的原生资源。然而 Electron 的用户在 Node.js 的 API 支持下可以在页面中和操作系统进行一些底层交互。
主进程与渲染进程的区别
主进程使用 BrowserWindow 实例创建页面。每个 BrowserWindow 实例都在自己的渲染进程里运行页面。当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
主进程管理所有的 web 页面和它们对应的渲染进程。每个渲染进程都是独立的,它只关心它所运行的 web 页面。
在页面中调用与 GUI 相关的原生 API 是不被允许的,因为在 web 页面里操作原生的 GUI 资源是非常危险的,而且容易造成资源泄露。如果你想在 web 页面里使用 GUI 操作,其对应的渲染进程必须与主进程进行通讯,请求主进程进行相关的 GUI 操作。
主进程与渲染进程通信
那么进程间如何通讯?
Electron 为主进程( main process)和渲染器进程(renderer processes)通信提供了多种实现方式,如可以使用 ipcRenderer 和 ipcMain 模块发送消息,使用 remote 模块进行 RPC 方式通信。
你还可以用 Electron 内的 IPC 机制实现。将数据存在主进程的某个全局变量中,然后在多个渲染进程中使用 remote 模块来访问它。
示例代码:
// 在主进程中
global.sharedObject = {
someProperty: 'default value'
}
// 在第一个页面中
require('electron').remote.getGlobal('sharedObject').someProperty = 'new value'
// 在第二个页面中
console.log(require('electron').remote.getGlobal('sharedObject').someProperty)
使用 Electron 的 API
Electron 在主进程和渲染进程中提供了大量 API 去帮助开发桌面应用程序, 在主进程和渲染进程中,你可以通过require的方式将其包含在模块中以此,获取 Electron 的 API
const electron = require('electron')
所有 Electron 的 API 都被指派给一种进程类型。许多 API 只能被用于主进程或渲染进程中,但其中一些 API 可以同时在上述两种进程中使用。每一个 API 的文档都将声明你可以在哪种进程中使用该 API。
Electron 中的窗口是使用 BrowserWindow 类型创建的一个实例, 它只能在主进程中使用。
// 这样写在主进程会有用,但是在渲染进程中会提示'未定义'
const { BrowserWindow } = require('electron')
const win = new BrowserWindow
因为进程之间的通信是被允许的, 所以渲染进程可以调用主进程来执行任务。Electron 通过 remote 模块暴露一些通常只能在主进程中获取到的 API。为了在渲染进程中创建一个 BrowserWindow 的实例,通常使用 remote 模块为中间件:
// 这样写在渲染进程中时行得通的,但是在主进程中是'未定义'
const { remote } = require('electron')
const { BrowserWindow } = remote
const win = new BrowserWindow
Tips:关注微信公众号:Java后端,每日技术博文推送。
使用 Node.js 的 API
Electron 同时在主进程和渲染进程中对 Node.js 暴露了所有的接口。这里有两个重要的定义:
1) 所有在 Node.js 可以使用的 API,在 Electron 中同样可以使用。在 Electron 中调用如下代码是有用的:
const fs = require('fs')
const root = fs.readdirSync('/')
// 这会打印出磁盘根级别的所有文件
// 同时包含'/'和'C:\'。
console.log(root)
2) 你可以在你的应用程序中使用 Node.js 的模块。选择您最喜欢的 npm 模块。npm 提供了目前世界上最大的开源代码库,那里包含良好的维护、经过测试的代码,提供给服务器应用程序的特色功能也提供给 Electron。
例如,在你的应用程序中要使用官方的 AWS SDK,你需要首先安装它的依赖:
npm install --save aws-sdk
然后在你的 Electron 应用中,通过 require 引入并使用该模块,就像构建 Node.js 应用程序那样:
// 准备好被使用的S3 client模块
const S3 = require('aws-sdk/clients/s3')
有一个非常重要的提示: 原生 Node.js 模块 (即指,需要编译源码过后才能被使用的模块) 需要在编译后才能和 Electron 一起使用。
最终产品杀青落地
终于搞明白了 Electron 的应用架构,那么接着就要进入产品的开发阶段。比较庆幸的是,ELectron 的 UI 完全由 CSS+HTML 组成,这部分可用的框架太多了,我选择了又老又知名的 BootStarp 框架搭建界面 UI,还引用了 JS 框架 JQuery。选择了 electron-store 作为本地存储文件,至于最关键的 Markdown 语法解析,对比了一番主流解析框架,最终选择了 markdown-it。贴一下效果图:
这款软件我给他起名为 JustWrite,意思就是现在就写,也是在督促自己吧,毕竟犹豫徘徊,等于白来。
现在软件的功能除了包含一键发布本地文章加本地图片到 SegmentFault 等平台,我还打算将他打造为一个体验不错的 Markdown 写作软件。现在你阅读的这篇文章,就是我使用 JustWrite 书写的,使用的字体是我个人喜欢的幼圆体,除此之外,还有六款风格迥异的字体可以切换使用。字号也是可以动态放大或者缩小,还可以关闭右侧预览,专注于写作,如下图所示:
这些截图是我截屏后使用快捷键 Ctrl+V 一键粘贴的,图片会自动放到当前 md文件所在目录下的 picture 文件夹内。
关于 JustWrite 从构思到实践的心路历程大致就以上这些了,这次开发 JustWrite 也让我过了一把产品经理的瘾,基本已经满足了我的日常需求。如果你有更好的想法和创意也可以告诉我,说不定第二天就会实现了。
GitHub:
https://github.com/yueshutong/JustWrite
前我发了一篇文章讲述跨平台的GUI技术,其中提到了javafx。对此很多人表示疑惑,认为javafx是落伍的开发技术。对此,我想专门写一篇文章来做个介绍。
其实很多人并没有听说过javafx。现在最新的java教材,在讲到GUI技术的时候,还是以swing和awt为主。swing和awt是上一代的Java GUI技术,现在很多银行、国企的嵌入式设备还跑着swing和awt写出来的程序。javafx是为了替代swing而产生的库,它同时支持Windows, MacOS, Linux三种平台的客户端程序开发。对于移动端(主要是IOS和Android),开源的javafxports以及相关的商业软件归于Gluon旗下,对于手机端做了额外的适配,使得javafx也可以在移动设备上运行。所以,javafx是真正的跨平台客户端开发技术。纵观其他客户端技术,Electron只支持Windows, MacOS和Linux,Qt对于移动端的支持尚不完善,C#开发GUI程序就更不必说了。
和传统的java GUI技术相比,javafx在美观度和运行效率上都有大幅度的提升。javafx对各平台的GPU图形API做了封装,支持在各种平台上的硬件加速,因此开发者完全可以使用javafx开发大型的三维系统软件。下面是javafx两个案例程序,分别展示了图表和三维图形。
javafx图表
javafx 3D
javafx是典型的前后端分离的开发模式。通过fxml绘制界面,css修饰页面的样式,java程序则用作后端控制。这种模式与web开发是极为相似的,也提高了程序的可维护性。
有的同学可能会认为用java做客户端程序不合理。但是Matlab、IDEA、Eclipse等软件都是java写出来的,而且它们都取得了重大的成功。其实技术是用来实现目的的,只要能完成需求,客户不会管你用的什么技术实现的。而且由于javafx是真正跨平台的GUI技术,开发软件的时候,不需要给windows, Mac, Linux, Android, IOS分别配团队,只需要一份代码就可以了(或者只需要做少量平台适配相关的修改)。Office、visual studio这种大型软件,因为没有使用跨平台的技术开发,微软现在想把它们迁移到新的平台就非常困难。
2018年JDK11发布之后,Oracle将javafx归于openjdk项目之中,目的是为了加速javafx的发展速度。目前,jdk8中集成了javafx,而后续版本的jdk则移除了javafx,目的是实现模块化。如果想在jdk11中使用javafx,可以通过maven导入javafx的依赖,也可以自行下载javafx并放到jdk中,总体而言并不麻烦。不过初学者还是使用jdk8比较好,熟悉之后再升级到jdk11。
除了使用javafx库中的组件进行开发以外,开发者还可以使用javafx中的webview开发程序,这也是很多微信小程序和移动端程序常用的开发模式。虽然这种开发方式降低了运行速度,但是开发速度则大大提高,因为前端的工具链对于开发GUI程序实在是过于友好。javafx的webview加载和运行速度其实还算比较快的,笔者曾经对不同框架下的webview性能做过测试,发现2012年发布的javafx webview比2020年的Qt、安卓的webview速度还快,仅次于Electron(毕竟Electron是正经的套壳浏览器)。javafx的webview对HTML5的支持非常好,对css的支持稍微差点,但是基本不影响使用。
使用IDEA + scenebuilder是目前最好的开发javafx程序的方案。其中IDEA支持css和fxml的语法提示和高亮,scenebuilder是所见即所得的fxml生成器,可以通过拖拽的方式绘制界面。不过我不推荐拖拽组件,因为这种方式构建界面是有限制的,不能做出复杂的界面效果,而且自适应屏幕大小的时候也比较麻烦。绘制网页的时候也是这样,一般都不用编辑器,手写HTML + CSS是最好的。
如果想要学习javafx的话,可以看我上面一篇关于跨平台GUI技术对比的文章。后面我也会陆续更新javafx相关的技术指导。
到目前为止,我们编写的程序都是通过键盘接收输入,在控制台屏幕上显示结果。绝大多数用户并不喜欢这种交互方式。现代的程序早已不采用这种操作方法,网络程序更是如此。
从本章开始,我们将介绍如何编写使用图形用户界面(GUI)的Java程序。其中,主要讲述如何编写定义屏幕上的窗口大小和位置的程序;如何在窗口中采用多种字体显示文本;如何显示图像等等。这些都是需要掌握的编程技能,在后续章节中,将会使用这些技术编写一些很有趣味的程序。
在Java 1.0刚刚出现的时候,包含了一个用于基本GUI程序设计的类库,Sun将它称为抽象窗
口工具箱(Abstract Window Toolkit,AWT)。基本AWT库采用将处理用户界面元素的任务委派给每个目标平台(Windows、Solaris、Macintosh等等)的本地GUI工具箱的方式,由本地GUI工具箱负责用户界面元素的创建和动作。例如,如果使用最初的AWT在Java窗口中放置一个文本框,就会有一个低层的“对等体”文本框,用它来实际地处理文本输入。从理论上说,结果程序可以运行在任何平台上,但观感(look and feel)的效果却依赖于目标平台,因此,Sun公司的口号是“一次编写,随处使用”。
对于简单的应用程序来说,基于对等体方法的效果还是不错的,但是,要想编写依赖于本地用户界面元素的高质量、可移植的图形库就会显现出缺陷了。例如,菜单、滚动条和文本域这些用户界面元素,在不同的平台上,操作行为存在着一些微妙的差别。因此,要想给予用户一致的、可预见性的界面操作方式是相当困难的。而且,有些图形环境(如X11/Motif)并没有像Windows或Macintosh这样丰富的用户界面组件集合。这也就将基于对等体的可移植库限制在了“最小公分母”的范围内。其结果使AWT构建的GUI应用程序看起来没有Windows或Macintosh应用程序显示的那么漂亮,也没有提供那些平台用户所认知的功能。更加糟糕的是,在不同平台上的AWT用户界面库中存在着不同的bug。研发人员必须在每一个平台上测试他们的应用程序,因此人们嘲弄地将AWT称为“一次编写,到处调试”。
在1996年,Netscape创建了一种称为IFC(Internet Foundation Class)的GUI库,它采用了与AWT完全不同的工作方式。它将按钮、菜单这样的用户界面元素绘制在空白窗口上,而对等体只需要创建和绘制窗口。因此,Netscape的IFC部件在程序运行的所有平台上的外观和动作都一样。Sun与Netscape合作完善了这种方式,创建了一个名为Swing(有时称为Swing集)的用户界面库。Swing可作为Java 1.1的扩展部分使用,现已成为JDK 1.2标准库的一部分,
就像Duke Ellington所说的那样:“如果没有Swing,Java图形界面就没有任何意义”。现在,Swing是不对等基于GUI工具箱的正式名字。它已是Java基础类库(Java Foundation Class,JFC)的一部分。完整的JFC十分庞大,其中包含的内容远远大于Swing GUI工具箱。JFC特性不仅仅包含了Swing组件,而且还包含了一个可访问的API、一个2D API和一个可拖拽的API。
注意:Swing没有完全替代AWT,而是基于AWT架构之上。Swing仅仅提供了能力更加强大的用户界面组件。尤其在采用Swing编写的程序中,还需要使用基本的AWT处理事件。从现在开始,“Swing”是指“被绘制的”非对等体用户界面类;“AWT”是指像事件处理这样的窗口工具箱的低层机制。
当然,在用户屏幕上显示基于Swing用户界面的元素要比显示AWT的基于对等体组件的速度慢一些。鉴于以往的经验,对于任何一台现代的计算机来说,微小的速度差别无妨大碍。另外,由于下列几点无法抗拒的原因,驱使人们选择Swing:
• Swing拥有一个丰富、便捷的用户界面元素集合。
• Swing对低层平台依赖的很少,因此与平台相关的bug很少。
• Swing给予不同平台的用户一致的感观效果。
所有这些意味着Swing拥有履行Sun提出的“一次编写,到处运行”承诺的能力。
不过,上面第三点存在着一个潜在的问题:如果在所有平台上用户界面元素看起来都一样,那么,它们就有可能与本地控件不一样,而这些平台的用户对此可能并不熟悉。
Swing采用了一种很巧妙的方式来解决这个问题。在程序员编写Swing程序时,可以为程序指定专门的“观感”。
例如,图7-1和图7-2展示了同一个程序在Windows 和Motif平台下运行的观感。
注意:尽管本书并没有打算介绍有关设定“观感”的方式,但Java程序员可以对已存在的观感进行扩展,甚至还可以设计全新的观感。设计Swing组件的绘制方式是一个很繁琐的过程。有些程序员已经做过一些这样的工作,尤其是将Java移植到信息亭(kiosk)终端和手持设备这样的非传统平台上。请参阅 http://www.javooto.com,其中包含了一系列有趣的观感实现。
JDK 5.0引入了一种被称为Synth的新观感方式,使用它处理比较容易。在Synth中,可以通过指定图像文件和XML描述符定义一种新观感,而不需要编写任何代码。
此外,Sun开发了一种被称为“Metal”的独立于平台的观感。现在,市场上人们将它称为“Java观感”。不过,绝大多数程序员还继续沿用着“Metal”这个术语,在本书中也将这样称呼。
有些人批评Metal有点笨重,而在版本5.0中看起来却焕然一新(请看图7-3)。
现在,Metal外观支持多种主题,每一种主题的颜色和字体都有微小的变化。
默认的主题叫做“Ocean”。在本文中,所有的图形程序都将采用Swing的Metal观感和Ocean主题。
注意:绝大多数Java用户界面程序设计都采用Swing,但有一个特别的例外。Eclipse集成开发环境使用了一种与AWT类似,且被称为SWT的图形工具箱,它可以映射到不同平台的本地组件上。
有关SWT的描述可以在网站http://www.eclipse.org/ articles/找到。
最后,给大家一个忠告,如果使用过Visual Baisc或C# 编写Microsoft Windows应用程序,就应该了解这些产品提供的图形布局工具和资源编辑器带来的便利。这些工具可以用来设计应用程序的外观,然后生成大部分(有时是全部)GUI代码。尽管也有一些Java程序设计的GUI构造器,但它们与相应的Windows工具相比较起来还很不成熟。不管怎样,要想完全地掌握图形用户界面程序(乃至有效地使用这些工具),就需要知道如何手工地创建用户界面。当然,通常需要编写大量的代码。
在Java中,顶层窗口(就是没有包含在其他窗口中的窗口)被称为框架(frame)。在AWT库中有一个称为Frame的类,用于描述顶层窗口。这个类的Swing版本名为JFrame,它扩展于Frame类。JFrame是极少数几个不绘制在画布上的Swing组件之一。因此,它的修饰部件(按钮、标题栏、图标等)由用户的窗口系统绘制,而不是由Swing绘制。
警告:大多数的Swing组件类都以“J”开头,例如,JButton、JFrame等等。在Java中有Button和Frame这样的类,但它们属于AWT组件。如果偶然地忘记了书写“J”,程序仍然可以进行编译和运行,但是将Swing和AWT组件混合在一起使用将会导致视觉和行为的不一致。
在本节中,将介绍有关Swing的JFrame的常用方法。例7-1给出了一个在屏幕中显示一个空框架的简单程序。如图7-4所示。
例7-1 SimpleFrameTest.java
下面逐行地讨论一下这个程序。
Swing类位于javax.swing包中。包名javax表示这是一个Java扩展包,而不是核心包。Swing类实际上是对Java 1.1的扩展。由于Swing类不是核心层次的一部分,所以尽可能地将Swing类加载到Java 1.1兼容的浏览器中(浏览器的安全管理器不允许添加任何以“java.”开头的包)。在Java 2平台上,Swing包不再是扩展部分,而是核心层的一部分。任何与Java 2兼容的Java实现都必须提供Swing类。不过,为了与Java 1.1代码兼容,保留了javax名字。(实际上,Swing包最早是com.sun.java.swing,后来在Java 2的beta版本中简化为java.awt.swing,最后在Java 2后期的beta版本中又改回为com.java.swing,在Java程序员的抗议呼声下,最终改为javax.swing。)
在默认情况下,框架的大小为0×0像素,这种框架没有什么实际意义。我们定义了一个子类SimpleFrame,它的构造器将框架大小设置为300×200像素。在SimpleFrameTest类的main方法中,程序将由创建一个SmpleFrame对象开始运行。
接下来,我们定义了用户关闭这个框架时的响应动作。对于这个程序而言,我们只是让程序退出。选择这个响应动作的语句是:
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
在包含多个框架的程序中,不能因为用户关闭了其中的一个框架就让程序退出。在默认情况下,用户关闭窗口时只是将框架隐藏了起来,而程序并没有终止。
简单地构造一个框架并不自动显示出来,框架起初是不可见的。这就给程序员了一个机会,可以在框架第一次显示之前往其中添加组件。为了显示框架,main方法需要调用框架的setVisible方法。
然后,main方法退出。需要注意,退出main并没有终止程序,终止的只是主线程。目前显示的框架激活了用户界面线程,以保持程序处于激活状态。
注意:在JDK 5.0以前的版本中,可以使用JFrame类从超类Window继承show方法。Window类的超类是Component,其中也有一个show方法。在JDK 1.2中不提倡使用Component.show。如果想要显示一个组件,建议调用setVisible(true)。然而,JDK 1.4以前的版本,并没有反对使用Window.show方法。事实上,这个方法很实用,它可以让窗口可见,且置于其他窗口的前面。遗憾的是,由于不提倡使用它,随之也失去了这一好处,JDK 5.0也不赞成使用show显示窗口。
图7-4中显示的是运行例7-1程序的结果,它只是一个很乏味的顶层窗口。在这个图中看到的标题栏和外框装饰(比如,重置窗口大小的拐角)都是由操作系统绘制的,而不是Swing库。如果在X Windows下运行同样的程序,对框架的装饰是不一样的。Swing库负责绘制框架内的所有内容。在这个程序中,只用默认的背景色填充了框架。
注意:在JDK 1.4中,可以调用frame.setUndecorated(true) 关闭所有框架装饰。
注意:在前面的例子中编写了两个类,一个用于定义框架类,另一个包含了创建和显示框架对象的main方法。在很多程序中,经常会发现main方法被包装成一个很简捷的类,如下所示:
从某种意义上说,调用框架类中的main方法的代码启动程序是比较简单的。这样不必引入其他的辅助类。然而,有相当多的程序员感觉这种程序风格有点混乱。
因此,更愿意将启动程序的类与定义用户界面的类分开。
明天讲框架定位和在面板中显示信息~~
*请认真填写需求信息,我们会在24小时内与您取得联系。