Fyne 折腾手记:开发一个简易桌面应用

文章首发于个人公众号:「阿拉平平」

最近试了下用 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,就可以查询到相关信息了:


demo3.png

编译打包

下载打包工具:

$ 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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容