創(chuàng)建一個 Golang app 是一件簡單又輕松的事情煮嫌,但是有時候你想給你的應(yīng)用錦上添花:創(chuàng)建一個 GUI!
在本篇文章中抱虐,我將通過使用 astilectron 工具中的 bootstrap 以及 bundler 給一個簡單的 Golang 程序添加 GUI昌阿。
我們的帶有 GUI 的 Golang app 能夠打開一個文件夾并且展示其中的內(nèi)容。
你可以在這里找到完成后的 代碼 :
第一步:組織項目結(jié)構(gòu)
文件夾結(jié)構(gòu)如下:
|--+ resources
|--+ app
|--+ static
|--+ css
|--+ base.css
|--+ js
|--+ index.js
|--+ lib
|--+ ... (all the css/js libs we need)
|--+ index.html
|--+ icon.icns
|--+ icon.ico
|--+ icon.png
|--+ bundler.json
|--+ main.go
|--+ message.go
你將看到,我們需要3種不同格式的圖標(biāo)以完成不同平臺的編譯:
.icns
用于 darwin
平臺
.ico
用于 windows
平臺
.png
用于 linux
平臺
我們將使用以下CSS/JS庫
第二步:搭建基礎(chǔ)架構(gòu)
Go
首先我們需要在 main.go
中導(dǎo)入 astilectron 的 bootstrap 源碼包 :
package main
import (
"flag"
"github.com/asticode/go-astilectron"
"github.com/asticode/go-astilectron-bootstrap"
"github.com/asticode/go-astilog"
"github.com/pkg/errors"
)
// Vars
var (
AppName string
BuiltAt string
debug = flag.Bool("d", false, "enables the debug mode")
w *astilectron.Window
)
func main() {
// Init
flag.Parse()
astilog.FlagInit()
// Run bootstrap
astilog.Debugf("Running app built at %s", BuiltAt)
if err := bootstrap.Run(bootstrap.Options{
AstilectronOptions: astilectron.Options{
AppName: AppName,
AppIconDarwinPath: "resources/icon.icns",
AppIconDefaultPath: "resources/icon.png",
},
Debug: *debug,
Homepage: "index.html",
MenuOptions: []*astilectron.MenuItemOptions{{
Label: astilectron.PtrStr("File"),
SubMenu: []*astilectron.MenuItemOptions{
{Label: astilectron.PtrStr("About")},
{Role: astilectron.MenuItemRoleClose},
},
}},
OnWait: func(_ *astilectron.Astilectron, iw *astilectron.Window, _ *astilectron.Menu, _ *astilectron.Tray, _ *astilectron.Menu) error {
w = iw
return nil
},
WindowOptions: &astilectron.WindowOptions{
BackgroundColor: astilectron.PtrStr("#333"),
Center: astilectron.PtrBool(true),
Height: astilectron.PtrInt(700),
Width: astilectron.PtrInt(700),
},
}); err != nil {
astilog.Fatal(errors.Wrap(err, "running bootstrap failed"))
}
}
2個全局變量 AppName
和 BuiltAt
將會通過 bundler 打包自動添加進(jìn)去懦冰。
隨后我們將發(fā)現(xiàn)我們的主頁變成了 index.html
灶轰,我們將有一個含有2個項目( about
和 close
)的菜單并且會出現(xiàn)一個 700x700
, 中心對齊的
, #333
背景色的窗口刷钢。
我們要在 go 上添加 debug
選項笋颤,因為我們需要使用 HTML/JS/CSS 調(diào)試工具。
最后我們將指向 astilectron.Window
的指針存入全局變量 w
内地,以備后續(xù)在使用 OnWait
選項時伴澄,它包含一個在窗口、菜單及其他所有對象被創(chuàng)建時立即執(zhí)行的回調(diào)函數(shù)阱缓。
HTML
現(xiàn)在我們需要在 resources/app/index.html
中創(chuàng)建我們的 HTML 主頁:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="static/css/base.css"/>
<link rel="stylesheet" href="static/lib/astiloader/astiloader.css">
<link rel="stylesheet" href="static/lib/astimodaler/astimodaler.css">
<link rel="stylesheet" href="static/lib/astinotifier/astinotifier.css">
<link rel="stylesheet" href="static/lib/font-awesome-4.7.0/css/font-awesome.min.css">
</head>
<body>
<div class="left" id="dirs"></div>
<div class="right">
<div class="title"><span id="path"></span></div>
<div class="panel"><span class="stat" id="files_count"></span> file(s)</div>
<div class="panel"><span class="stat" id="files_size"></span> of file(s)</div>
<div class="panel" id="files_panel">
<div class="chart_title">Files repartition</div>
<div id="files"></div>
</div>
</div>
<script src="static/js/index.js"></script>
<script src="static/lib/astiloader/astiloader.js"></script>
<script src="static/lib/astimodaler/astimodaler.js"></script>
<script src="static/lib/astinotifier/astinotifier.js"></script>
<script src="static/lib/chart/chart.min.js"></script>
<script type="text/javascript">
index.init();
</script>
</body>
</html>
這里沒什么特殊的地方非凌,我們聲明我們的 css
和 js
文件,我們設(shè)置html文件結(jié)構(gòu)并且我們需要確保我們的 js
腳本通過 index.init()
進(jìn)行了初始化
CSS
現(xiàn)在我們需要在 resources/app/static/css/base.css
文件中創(chuàng)建我們的 CSS:
* {
box-sizing: border-box;
}
html, body {
background-color: #fff;
color: #333;
height: 100%;
margin: 0;
width: 100%;
}
.left {
background-color: #333;
color: #fff;
float: left;
height: 100%;
overflow: auto;
padding: 15px;
width: 40%;
}
.dir {
cursor: pointer;
padding: 3px;
}
.dir .fa {
margin-right: 5px;
}
.right {
float: right;
height: 100%;
overflow: auto;
padding: 15px;
width: 60%;
}
.title {
font-size: 1.5em;
text-align: center;
word-wrap: break-word;
}
.panel {
background-color: #f1f1f1;
border: solid 1px #e1e1e1;
border-radius: 4px;
margin-top: 15px;
padding: 15px;
text-align: center;
}
.stat {
font-weight: bold;
}
.chart_title {
margin-bottom: 5px;
}
JS
然后我們在 resources/app/static/js/index.js
中創(chuàng)建 JS :
let index = {
init: function() {
// Init
asticode.loader.init();
asticode.modaler.init();
asticode.notifier.init();
}
};
通過 init
方法正確的將庫初始化
第三步:建立起 GO 與 Javascript 間的通信
萬事俱備荆针,只欠東風(fēng):我們需要將 GO 與 Javascript 建立起通信
Javascript 通信 GO
為了讓 Javascript 與 Go 進(jìn)行通信敞嗡,首先從 Javascript 向 GO 發(fā)送一條消息,并且在 GO 接受到消息后執(zhí)行回調(diào)函數(shù):
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will send a message to GO
astilectron.sendMessage({name: "event.name", payload: "hello"}, function(message) {
console.log("received " + message.payload)
});
})
同時我們在 GO 中監(jiān)聽來自 Javascript 的消息航背,并且通過 bootstrap 的 MessageHandler
給 Javascript 發(fā)送消息:
func main() {
bootstrap.Run(bootstrap.Options{
MessageHandler: handleMessages,
})
}
// handleMessages handles messages
func handleMessages(_ *astilectron.Window, m bootstrap.MessageIn) (payload interface{}, err error) {
switch m.Name {
case "event.name":
// Unmarshal payload
var s string
if err = json.Unmarshal(m.Payload, &path); err != nil {
payload = err.Error()
return
}
payload = s + " world"
}
return
}
這是一個簡單的例子將在js的輸出中打印出 received hello world
喉悴。
在這種情形中,我們需要更多的邏輯因為我們想要允許打開一個文件夾并且展示其中的內(nèi)容玖媚。
因此我們將下面的代碼加入到 resources/app/static/js/index.js
中:
let index = {
addFolder(name, path) {
let div = document.createElement("div");
div.className = "dir";
div.onclick = function() { index.explore(path) };
div.innerHTML = `<i class="fa fa-folder"></i><span>` + name + `</span>`;
document.getElementById("dirs").appendChild(div)
},
init: function() {
// Wait for astilectron to be ready
document.addEventListener('astilectron-ready', function() {
// Explore default path
index.explore();
})
},
explore: function(path) {
// Create message
let message = {"name": "explore"};
if (typeof path !== "undefined") {
message.payload = path
}
// Send message
asticode.loader.show();
astilectron.sendMessage(message, function(message) {
// Init
asticode.loader.hide();
// Check error
if (message.name === "error") {
asticode.notifier.error(message.payload);
return
}
// Process path
document.getElementById("path").innerHTML = message.payload.path;
// Process dirs
document.getElementById("dirs").innerHTML = ""
for (let i = 0; i < message.payload.dirs.length; i++) {
index.addFolder(message.payload.dirs[i].name, message.payload.dirs[i].path);
}
// Process files
document.getElementById("files_count").innerHTML = message.payload.files_count;
document.getElementById("files_size").innerHTML = message.payload.files_size;
document.getElementById("files").innerHTML = "";
if (typeof message.payload.files !== "undefined") {
document.getElementById("files_panel").style.display = "block";
let canvas = document.createElement("canvas");
document.getElementById("files").append(canvas);
new Chart(canvas, message.payload.files);
} else {
document.getElementById("files_panel").style.display = "none";
}
})
}
};
一旦 Javascript 的 astilectron
命名空間準(zhǔn)備好粥惧,它執(zhí)行新的 explore
方法,該方法會給 GO 發(fā)送一條消息最盅,接收返回的信息,并且更新相應(yīng)的 HTML 起惕。
然后我們將下面代碼加入到 message.go
中:
package main
import (
"encoding/json"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"sort"
"strconv"
"github.com/asticode/go-astichartjs"
"github.com/asticode/go-astilectron"
"github.com/asticode/go-astilectron-bootstrap"
)
// handleMessages handles messages
func handleMessages(_ *astilectron.Window, m bootstrap.MessageIn) (payload interface{}, err error) {
switch m.Name {
case "explore":
// Unmarshal payload
var path string
if len(m.Payload) > 0 {
// Unmarshal payload
if err = json.Unmarshal(m.Payload, &path); err != nil {
payload = err.Error()
return
}
}
// Explore
if payload, err = explore(path); err != nil {
payload = err.Error()
return
}
}
return
}
// Exploration represents the results of an exploration
type Exploration struct {
Dirs []Dir `json:"dirs"`
Files *astichartjs.Chart `json:"files,omitempty"`
FilesCount int `json:"files_count"`
FilesSize string `json:"files_size"`
Path string `json:"path"`
}
// PayloadDir represents a dir payload
type Dir struct {
Name string `json:"name"`
Path string `json:"path"`
}
// explore explores a path.
// If path is empty, it explores the user's home directory
func explore(path string) (e Exploration, err error) {
// If no path is provided, use the user's home dir
if len(path) == 0 {
var u *user.User
if u, err = user.Current(); err != nil {
return
}
path = u.HomeDir
}
// Read dir
var files []os.FileInfo
if files, err = ioutil.ReadDir(path); err != nil {
return
}
// Init exploration
e = Exploration{
Dirs: []Dir{},
Path: path,
}
// Add previous dir
if filepath.Dir(path) != path {
e.Dirs = append(e.Dirs, Dir{
Name: "..",
Path: filepath.Dir(path),
})
}
// Loop through files
var sizes []int
var sizesMap = make(map[int][]string)
var filesSize int64
for _, f := range files {
if f.IsDir() {
e.Dirs = append(e.Dirs, Dir{
Name: f.Name(),
Path: filepath.Join(path, f.Name()),
})
} else {
var s = int(f.Size())
sizes = append(sizes, s)
sizesMap[s] = append(sizesMap[s], f.Name())
e.FilesCount++
filesSize += f.Size()
}
}
// Prepare files size
if filesSize < 1e3 {
e.FilesSize = strconv.Itoa(int(filesSize)) + "b"
} else if filesSize < 1e6 {
e.FilesSize = strconv.FormatFloat(float64(filesSize)/float64(1024), 'f', 0, 64) + "kb"
} else if filesSize < 1e9 {
e.FilesSize = strconv.FormatFloat(float64(filesSize)/float64(1024*1024), 'f', 0, 64) + "Mb"
} else {
e.FilesSize = strconv.FormatFloat(float64(filesSize)/float64(1024*1024*1024), 'f', 0, 64) + "Gb"
}
// Prepare files chart
sort.Ints(sizes)
if len(sizes) > 0 {
e.Files = &astichartjs.Chart{
Data: &astichartjs.Data{Datasets: []astichartjs.Dataset{{
BackgroundColor: []string{
astichartjs.ChartBackgroundColorYellow,
astichartjs.ChartBackgroundColorGreen,
astichartjs.ChartBackgroundColorRed,
astichartjs.ChartBackgroundColorBlue,
astichartjs.ChartBackgroundColorPurple,
},
BorderColor: []string{
astichartjs.ChartBorderColorYellow,
astichartjs.ChartBorderColorGreen,
astichartjs.ChartBorderColorRed,
astichartjs.ChartBorderColorBlue,
astichartjs.ChartBorderColorPurple,
},
}}},
Type: astichartjs.ChartTypePie,
}
var sizeOther int
for i := len(sizes) - 1; i >= 0; i-- {
for _, l := range sizesMap[sizes[i]] {
if len(e.Files.Data.Labels) < 4 {
e.Files.Data.Datasets[0].Data = append(e.Files.Data.Datasets[0].Data, sizes[i])
e.Files.Data.Labels = append(e.Files.Data.Labels, l)
} else {
sizeOther += sizes[i]
}
}
}
if sizeOther > 0 {
e.Files.Data.Datasets[0].Data = append(e.Files.Data.Datasets[0].Data, sizeOther)
e.Files.Data.Labels = append(e.Files.Data.Labels, "other")
}
}
return
}
在接收到正確的信息時涡贱,它將執(zhí)行新的 explore
方法,并返回關(guān)于目錄的有價值的信息惹想。
建立從 Go 向 Javascript 通信
為了建立從 GO 向 Javascript 的通信问词,我們首先需要從 GO 中向 Javascript 發(fā)送一條消息并且在 Javascript 收到消息后執(zhí)行回調(diào)。
// This will send a message and execute a callback
// Callbacks are optional
bootstrap.SendMessage(w, "event.name", "hello", func(m *bootstrap.MessageIn) {
// Unmarshal payload
var s string
json.Unmarshal(m.Payload, &s)
// Process message
log.Infof("received %s", s)
})
同時我們在 Javascript 中監(jiān)聽來自 GO 的消息并發(fā)送一個選項消息給 GO:
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will listen to messages sent by GO
astilectron.onMessage(function(message) {
// Process message
if (message.name === "event.name") {
return {payload: message.message + " world"};
}
});
})
這個簡單的例子將在 GO 的輸出中打印 received hello world
嘀粱。在我們的項目里激挪,我們先將下面的代碼加入到 main.go
中:
func main() {
bootstrap.Run(bootstrap.Options{
MenuOptions: []*astilectron.MenuItemOptions{{
Label: astilectron.PtrStr("File"),
SubMenu: []*astilectron.MenuItemOptions{
{
Label: astilectron.PtrStr("About"),
OnClick: func(e astilectron.Event) (deleteListener bool) {
if err := bootstrap.SendMessage(w, "about", htmlAbout, func(m *bootstrap.MessageIn) {
// Unmarshal payload
var s string
if err := json.Unmarshal(m.Payload, &s); err != nil {
astilog.Error(errors.Wrap(err, "unmarshaling payload failed"))
return
}
astilog.Infof("About modal has been displayed and payload is %s!", s)
}); err != nil {
astilog.Error(errors.Wrap(err, "sending about event failed"))
}
return
},
},
{Role: astilectron.MenuItemRoleClose},
},
}},
OnWait: func(_ *astilectron.Astilectron, iw *astilectron.Window, _ *astilectron.Menu, _ *astilectron.Tray, _ *astilectron.Menu) error {
w = iw
go func() {
time.Sleep(5 * time.Second)
if err := bootstrap.SendMessage(w, "check.out.menu", "Don't forget to check out the menu!"); err != nil {
astilog.Error(errors.Wrap(err, "sending check.out.menu event failed"))
}
}()
return nil
},
})
}
它使得關(guān)于選項變成可點(diǎn)擊的,并且渲染出一個有合適內(nèi)容的模態(tài)框锋叨,在 GO app 完成初始化 5 s 后它會顯示一個提示框垄分。
最后我們將下面的代碼加入到
resources/app/static/js/index.js
中:
let index = {
about: function(html) {
let c = document.createElement("div");
c.innerHTML = html;
asticode.modaler.setContent(c);
asticode.modaler.show();
},
init: function() {
// Wait for astilectron to be ready
document.addEventListener('astilectron-ready', function() {
// Listen
index.listen();
})
},
listen: function() {
astilectron.onMessage(function(message) {
switch (message.name) {
case "about":
index.about(message.payload);
return {payload: "payload"};
break;
case "check.out.menu":
asticode.notifier.info(message.payload);
break;
}
});
}
};
它將監(jiān)聽 GO 發(fā)送過來的消息并做出相應(yīng)的反應(yīng)。
第四步: 打包到 app
現(xiàn)在代碼已經(jīng)完成娃磺,我們需要確保我們能夠以最好的方式把我們的 Golang GUI app 呈現(xiàn)給我們的用戶:
- 一個 MacOSX app 給
darwin
用戶 - 一個含有好看圖標(biāo)的
.exe
給windows
用戶 - 一個簡單的源碼文件給
linux
用戶
幸運(yùn)的是薄湿,我們可以通過 astilectron 的 bundler 來進(jìn)行操作。
首先我們通過下面命令進(jìn)行安裝:
$ go get -u github.com/asticode/go-astilectron-bundler/...
然后我們在 main.go
中給 bootstrap 添加配置項:
func main() {
bootstrap.Run(bootstrap.Options{
Asset: Asset,
RestoreAssets: RestoreAssets,
})
}
然后我們創(chuàng)建配置文件,命名為 bundler.json
:
{
"app_name": "Astilectron demo",
"icon_path_darwin": "resources/icon.icns",
"icon_path_linux": "resources/icon.png",
"icon_path_windows": "resources/icon.ico",
"output_path": "output"
}
最后我們在項目文件夾下運(yùn)行下面的命令(確保 $GOPATH/bin
在你的 $PATH
中)
$ astilectron-bundler -v
第五步: 實(shí)際效果
啊哈豺瘤!結(jié)果在 output/<os>-<arch>
文件夾下吆倦,快來去試一試 :)
你當(dāng)然可以打包你的 Golang GUI app 給其他環(huán)境,在 bundler 打包文檔中查看如何將程序打包到其他環(huán)境
結(jié)論
感謝 astilectron 的 bootstrap 和 bundler 坐求,通過一點(diǎn)對組織結(jié)構(gòu)變動工作蚕泽,給你的 Golang 程序添加 GUI 從未如此簡單。
需要指出的是這種方法有 2 個主要的缺點(diǎn):
- 代碼包的大小至少有 50 MB桥嗤,第一次執(zhí)行后须妻,文件大小至少將有 200 MB
- 內(nèi)存的消耗有些瘋狂,因為 Electron 并不擅長對內(nèi)存的管理
但是如果你準(zhǔn)備掌握它砸逊,那么你在給你的程序添加 GUI 時將非常便利璧南!
GUI 編碼愉快!
via: https://medium.com/@social_57971/how-to-add-a-gui-to-your-golang-app-in-5-easy-steps-c25c99d4d8e0
作者:Asticode
譯者:fengchunsgit
校對:rxcai
本文由 GCTT 原創(chuàng)編譯师逸,Go 中文網(wǎng) 榮譽(yù)推出
[1]: http://p0khjtoyx.bkt.clouddn.com/0.png?e=1512614718&token=U9_WlpL9xDhIuC0OvwzgYz5OmwFSaT0mRch0uuLm:El4EfsIVIW0SLlHyFWyp0NGX_34