文章首发于个人公众号:「阿拉平平」
最近试了下用 Fyne 库开发桌面应用,特此记录和分享一下。本文演示环境为 Windows,Fyne 版本为 1.2.3。
简介
Fyne 是一个 Go 语言开发的 UI 工具包。通过 Fyne,我们可以构建桌面和移动设备上运行的应用程序。
安装
在安装 Fyne 前,请确保 Go 版本在 1.12 以上。
$ go version go1.12.9 windows/amd64
安装 Fyne 库:
$ go get -u fyne.io/fyne
安装完成后,用官方的例子测试下,代码如下:
package main
import (
"fyne.io/fyne/app"
"fyne.io/fyne/widget"
)
func main() {
app := app.New()
w := app.NewWindow("Hello")
w.SetContent(widget.NewVBox(
widget.NewLabel("Hello Fyne!"),
widget.NewButton("Quit", func() {
app.Quit()
}),
))
w.ShowAndRun()
}
但是运行失败了,提示找不到 gcc 的可执行文件:
# command-line-arguments
C:\tools\Go\pkg\tool\windows_amd64\link.exe: running gcc failed: exec: "gcc": executable file not found in %PATH%
要解决这个问题,需要下载安装 MinGW,并将 bin 目录添加到环境变量中。再运行就可以看到界面了:
接下来是 Fyne 的实践部分,我打算用 Fyne 开发一个查询 IP 归属地的桌面应用,让我们一起动手试试吧。
界面开发
我们先完成界面部分的开发。
窗口
窗口可以根据自己喜好来调整,这里贴下我的代码:
package main
import (
"fyne.io/fyne"
"fyne.io/fyne/app"
"fyne.io/fyne/theme"
)
func main() {
a := app.New()
a.Settings().SetTheme(theme.LightTheme())
w := a.NewWindow("Demo")
w.Resize(fyne.NewSize(600, 500))
w.ShowAndRun()
}
说明:
SetTheme():设置应用的主题,默认为 DarkTheme。
NewWindow():初始化窗口,可修改标题。
Resize():设置窗口尺寸。
窗口调整完成后,就可以开始添加组件了。
组件
Fyne 自带了一些常用的组件,使用的时候需要导入 widget 包:
import "fyne.io/fyne/widget"
首先添加一个用来显示数据的函数,这里我命名为 info,代码如下:
func info() fyne.CanvasObject {
// 核心功能还未编写,先用假数据填充
ip := widget.NewLabel("114.114.114.114")
position := widget.NewLabel("中国江苏省南京市")
isp := widget.NewLabel("南京信风网络科技有限公司GreatbitDNS服务器")
// 初始化表单,用来显示数据
form := widget.NewForm(
&widget.FormItem{Text: "IP地址:", Widget: ip},
&widget.FormItem{Text: "所属地:", Widget: position},
&widget.FormItem{Text: "供应商:", Widget: isp},
)
// 分组
info := widget.NewGroup("Info", form)
// 返回一个支持滚动的容器
return widget.NewScrollContainer(query)
}
接着添加一个用来查询 IP 的函数,命名为 query,代码如下:
func query() fyne.CanvasObject {
// 初始化 ip 输入框
ip := widget.NewEntry()
// 设置输入框提示信息
ip.SetPlaceHolder("Please input IP address")
// 定义表单和触发事件
form := &widget.Form{
OnSubmit: func() {
fmt.Println("Form submitted")
fmt.Println("IP Address:", ip.Text)
},
}
// 将 ip 添加到表单中,之后从表单中就可以获取到 ip
form.Append("IP", ip)
// 分组
query := widget.NewGroup("Query", form)
// 返回一个支持滚动的容器
return widget.NewScrollContainer(query)
}
最后在主函数中添加 SetContent 方法,并使用 NewHBox 水平显示组件:
w.SetContent(widget.NewHBox(
info(),
query(),
))
添加完成后,看下效果:
看到这效果,我当场裂开了。看来使用 NewHBox 效果并不理想,还需要调整下界面的布局。
布局
我们可以使用 Fyne 自带的布局,先导入 layout 包:
import "fyne.io/fyne/layout"
再修改下 SetContent 的内容:
w.SetContent(fyne.NewContainerWithLayout(layout.NewGridLayoutWithColumns(2), info(), query())
简单说明下:
NewGridLayoutWithColumns:返回一个 gridLayout 结构体,可以指定列数。如果需要垂直布局,可以替换成 NewGridLayoutWithRows。
NewContainerWithLayout:返回一个 Container 实例,使布局生效。
可以看到,布局已经没问题了:
布局是没问题了,但是看到这方块字,我又裂开了。很明显,目前 Fyne 窗口无法显示中文,这个要怎么解决呢?
中文支持
首先,下载一个 TTF 格式的中文字库,这里我找了个思源字体的字库。需要注意的是,字库的格式必须是 TTF 的,否则会报错。
然后添加一个环境变量 FYNE_FONT,指定下载好的字库文件:
再看下窗口效果,舒服了:
功能实现
在之前的章节中,我们已经完成了界面的开发,现在可以去实现功能了。
接口
之前的 IP 信息我们是写死在代码里的,现在改为调用接口的方式。
关于接口,我使用的是 ALAPI 提供的接口服务,这个开源项目提供了许多实用的数据接口,有兴趣的童鞋可以移步到 Github。
请求地址:
http://v1.alapi.cn/api/ip?ip=114.114.114.114&format=json
返回数据如下:
{
"code": 200,
"msg": "success",
"data": {
"beginip": "114.114.114.114",
"endip": "114.114.114.114",
"pos": "中国江苏省南京市",
"isp": "南京信风网络科技有限公司GreatbitDNS服务器",
"location": {
"lat": 32.05838,
"lng": 118.79647
},
"rectangle": "",
"ad_info": {
"nation": "中国",
"province": "江苏省",
"city": "南京市",
"district": "",
"adcode": 320100
},
"ip": "114.114.114.114"
},
"Author": {
"name": "Alone88",
"desc": "由Alone88提供的免费API 服务,官方文档:www.alapi.cn"
}
}
接口有了,接下来我们调整下之前的代码。
代码调整
定义全局变量与结构体:
var ip = widget.NewLabel("")
var position = widget.NewLabel("")
var isp = widget.NewLabel("")
type IpInfo struct {
Code int `json:"code"`
Message string `json:"msg"`
Data `json:"data"`
}
type Data struct {
IP string `json:"ip"`
Position string `json:"pos"`
Isp string `json:"isp"`
}
说明:
ip,position 和 isp 这里定义成全局变量,是因为之后这部分信息在点击提交按钮后会发生变更。
新增 GetIpInfo 函数用来获取接口数据:
func GetIpInfo(ip string) string {
if len(ip) == 0 {
return ""
}
url := fmt.Sprintf("http://v1.alapi.cn/api/ip?ip=%s&format=json", ip)
resp, err := http.Get(url)
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// handle error
}
return string(body)
}
调整 info 函数:
func info(response string) fyne.CanvasObject {
var i IpInfo
json.Unmarshal([]byte(response),&i)
screen := widget.NewForm(
&widget.FormItem{Text: "IP地址:", Widget: ip},
&widget.FormItem{Text: "所属地:", Widget: position},
&widget.FormItem{Text: "供应商:", Widget: isp},
)
ip.SetText(i.IP)
position.SetText(i.Position)
isp.SetText(i.Isp)
info := widget.NewGroup("Info", screen)
return widget.NewScrollContainer(info)
}
说明:
info() 接受 GetIpInfo 的返回值并将数据反序列化,通过 SetText 方法更新信息。
调整 query 函数:
func query() fyne.CanvasObject {
ip := widget.NewEntry()
ip.SetPlaceHolder("Please input IP address")
form := &widget.Form{
OnSubmit: func() {
info(GetIpInfo(ip.Text))
},
}
form.Append("IP", ip)
query := widget.NewGroup("Query", form)
return widget.NewScrollContainer(query)
}
说明:
OnSubmit 中调用 info(),即点击 Submit 后获取并显示数据。
调整 main 函数:
w.SetContent(fyne.NewContainerWithLayout(layout.NewGridLayoutWithColumns(2), info(GetIpInfo("")), query()))
输入 IP 后点击 Submit,就可以查询到相关信息了:
编译打包
下载打包工具:
$ go get fyne.io/fyne/cmd/fyne
在项目目录中放入图标,执行以下命令:
$ fyne package -icon icon.png
写在后面
文中的例子写的比较简单,有许多不完善的地方,有兴趣的小伙伴可以去 GitHub 上看看,在上面可以找到更多的例子和文档。
如果有需要 MinGW for Windows x64 和中文字体的小伙伴,可以去微信公众号后台回复 fyne 获取。
References
[1] Fyne: https://github.com/fyne-io/fyne
[2] 思源字体:https://github.com/junmer/source-han-serif-ttf
[3] Github: https://github.com/anhao/ALAPI
[4] 例子: https://github.com/fyne-io/examples/
[5] 文档:: https://apps.fyne.io/