fyne.io/fyne 是一个用于构建桌面和移动 GUI 应用的 Go 语言库。它提供了跨平台支持,允许开发者在 Windows、MacOS、Linux、iOS 和 Android 上使用相同的代码运行应用。
前提条件
Fyne 运行需要以下三个基本要素:
- Go 语言环境(至少需要 Go 1.12 版本)。
- C 语言编译器(用于连接系统的图形驱动)。
- 系统图形驱动。
不同操作系统的安装方式不同,请根据你的操作系统选择合适的安装方式 (Go 语言安装这里就不介绍了)。
注意:这些步骤仅适用于开发阶段,Fyne 应用的最终用户 不需要 额外安装任何依赖或进行设置。
- Windows: 安装 Mingw-w64 (www.mingw-w64.org/),推荐使用%25EF%25BC%258C%25E6%258E%25A8%25E8%258D%2590%25E4%25BD%25BF%25E7%2594%25A8 "https://www.mingw-w64.org/)%EF%BC%8C%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8") MSYS2 (www.msys2.org/) 构建环境。
- MacOS: 需要从 Mac App Store 安装 Xcode。
- Linux: Debian、Ubuntu、Raspberry Pi OS:
sudo apt-get install gcc libgl1-mesa-dev xorg-dev libxkbcommon-dev。更多 Linux 发行版的安装命令详见文档 (docs.fyne.io/started/)。%25E3%2580%2582 "https://docs.fyne.io/started/)%E3%80%82")
安装
安装 Fyne 库及其辅助工具:
bash
go get fyne.io/fyne/v2@latest
go install fyne.io/fyne/v2/cmd/fyne@latest
运行示例
如果想在编写自己的应用程序之前先体验 Fyne,可以运行官方示例应用:
bash
go run fyne.io/fyne/v2/cmd/fyne_demo@latest
注意:第一次运行时需要编译部分 C 代码,因此可能会比较慢,后续构建会重用缓存,速度会快很多。执行后自动下载相应的依赖。
组件系统
Fyne 的 UI 组件以 fyne.CanvasObject 为基础,主要有:
- Widget(控件):预定义的 UI 组件(按钮、标签、输入框)。
- Container(容器):提供布局管理(垂直、水平、网格、边框)。
- Canvas(画布):提供基本的绘图能力(渲染图形和文本)。
Fyne 中可以绘制的所有内容都是 fyne.CanvasObject 类型,以上都实现了 fyne.CanvasObject 接口:
scss
type CanvasObject interface {
MinSize() Size
Move(Position)
Position() Position
Resize(Size)
Size() Size
Hide()
Visible() bool
Show()
Refresh()
}
简而言之,需要记住的就是,把 Widget (控件)装载在 Container(容器)中,来构建 UI 组成用户界面。
打包
需提供 .png 文件作为图标。
go
// Windows 打包命令:
fyne package -os windows -icon myapp.png
// MacOS 打包命令:
fyne package -os darwin -icon myapp.png
// Linux 打包命令:
fyne package -os linux -icon myapp.png
示例
1.我们通过一个简单的示例,一步一步来实现一个计算文件 hash 值的应用程序,来了解这个库的用法:
css
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func main() {
a := app.New()
w := a.NewWindow("File Hasher")
w.SetContent(InitUI(w))
w.Resize(fyne.NewSize(710, w.Canvas().Size().Height))
w.ShowAndRun()
}
func InitUI(w fyne.Window) fyne.CanvasObject {
fileEntry := widget.NewEntry()
fileEntry.SetPlaceHolder("Enter File Path")
fileOpenBtn := widget.NewButtonWithIcon("Open", theme.FileIcon(), func() {})
fileHashBtn := widget.NewButtonWithIcon("Hash", theme.MailSendIcon(), func() {})
return container.NewBorder(nil, nil, fileOpenBtn, fileHashBtn, fileEntry)
}
- app.New():创建一个 Fyne 应用。
- a.NewWindow("File Hasher"):创建窗口并命名 "File Hasher"。
- w.SetContent(InitUI(w)):调用 InitUI(w) 创建界面并设置窗口内容。
- w.Resize(fyne.NewSize(710, w.Canvas().Size().Height)):设置窗口的宽和高,一般设置为具体的值。这里使用 w.Canvas().Size().Height 会根据窗口内容自动计算一个合适的高度(通常由内部组件的最小尺寸决定)。
- w.ShowAndRun():显示窗口并启动应用。
go
func InitUI(w fyne.Window) fyne.CanvasObject
- InitUI(w):负责创建 UI 组件并布局,返回 fyne.CanvasObject。
css
fileEntry := widget.NewEntry()
fileEntry.SetPlaceHolder("Enter File Path")
- widget.NewEntry():创建一个输入框,用于输入或显示文件路径。
- SetPlaceHolder("Enter File Path"):设置输入框为空时显示的文本。
css
fileOpenBtn := widget.NewButtonWithIcon("Open", theme.FileIcon(), func() {})
fileHashBtn := widget.NewButtonWithIcon("Hash", theme.MailSendIcon(), func() {})
- fileOpenBtn :创建打开文件按钮,按钮的标签为 "Open",图标为 theme.FileIcon(),当前 func() {} 为空,为点击该按钮所触发的函数,后续可补全逻辑,如弹出文件选择框。
- fileHashBtn :创建计算哈希按钮,按钮的标签为 "Hash",图标为 theme.MailSendIcon()。当前 func() {} 为空,后续可补全逻辑,如计算文件的 hash 值。
go
container.NewBorder(nil, nil, fileOpenBtn, fileHashBtn, fileEntry)
container.NewBorder 创建边框布局:
- 上 (top):nil(顶部留空)。
- 下 (bottom):nil(底部留空)。
- 左 (left):fileOpenBtn("Open" 按钮)。
- 右 (right):fileHashBtn("Hash" 按钮)。
- 中 (center):fileEntry(输入框)。
运行一下:
2.下一步,继续补全代码,实现文件选择功能:
go
func InitUI(w fyne.Window) fyne.CanvasObject {
fileEntry := widget.NewEntry()
fileEntry.SetPlaceHolder("Enter File Path")
openFile := func() {
file, err := zenity.SelectFile(
zenity.Title("Open File"),
)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
fileEntry.SetText(file)
}
fileOpenBtn := widget.NewButtonWithIcon("Open", theme.FileIcon(), openFile)
fileHashBtn := widget.NewButtonWithIcon("Hash", theme.MailSendIcon(), func() {})
return container.NewBorder(nil, nil, fileOpenBtn, fileHashBtn, fileEntry)
}
- openFile:点击 "Open" 按钮所触发的处理函数,实现弹出文件选择框功能。
- fileEntry.SetText(file):选择文件后,文件路径显示在输入框。
- zenity.Title("Open File"):选择文件窗口命名为 "Open File"。
- zenity.Error(err.Error(), zenity.Title("error")):报错会弹出一个 "error" 窗口并显示错误信息。
文件选择框这里使用了 github.com/ncruces/zenity 库来实现。(Fyne 自带文件选择控件,这里并没有使用)。
zenity (github.com/ncruces/zenity) 是一个 Go 语言库,用于创建文件选择、消息框、输入框等界面交互窗口。适用于 Windows / MacOS / Linux,且不依赖 GUI 框架(直接调用系统 API)。
运行一下:
3.下一步,实现计算文件 hash 值的功能,并提供一个标签栏展示结果值:
go
func InitUI(w fyne.Window) fyne.CanvasObject {
fileEntry := widget.NewEntry()
fileEntry.SetPlaceHolder("Enter File Path")
fileOpenBtn := widget.NewButtonWithIcon("Open", theme.FileIcon(), func() {})
fileHashBtn := widget.NewButtonWithIcon("Hash", theme.MailSendIcon(), func() {})
MD5Label := widget.NewLabel("MD5") // MD5 标签
MD5Hash := widget.NewLabel("") // MD5 结果值标签
MD5CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 MD5 结果值按钮
w.Clipboard().SetContent(MD5Hash.Text)
})
openFile := func() {
file, err := zenity.SelectFile(
zenity.Title("Open File"),
)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
fileEntry.SetText(file)
}
fileOpenBtn.OnTapped = openFile
hashFile := func() {
MD5Hash.SetText("")
_, err := os.Stat(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
md5Hash := md5.New()
_, err = io.Copy(md5Hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
MD5Hash.SetText(hex.EncodeToString(md5Hash.Sum(nil)))
}
fileHashBtn.OnTapped = hashFile
fileRow := container.NewBorder(nil, nil, fileOpenBtn, fileHashBtn, fileEntry)
MD5HashRow := container.NewBorder(nil, nil, MD5Label, MD5CopyBtn, MD5Hash)
return container.NewVBox(fileRow, MD5HashRow)
}
补全了计算文件 hash 按钮所触发的处理函数,并且创建一个标签用来显示 "MD5",创建一个标签显示 MD5 结果值,创建一个按钮来复制 MD5 结果值。
go
MD5Label := widget.NewLabel("MD5") // MD5 标签
MD5Hash := widget.NewLabel("") // MD5 结果值标签
MD5CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 MD5 结果值按钮
w.Clipboard().SetContent(MD5Hash.Text)
})
- MD5Label:创建一个标签用来显示 "MD5"。
- MD5Hash:创建一个标签用来显示 MD5 结果值。
- MD5CopyBtn:创建一个按钮来复制 MD5 结果值。
- w.Clipboard().SetContent(MD5Hash.Text):将标签中的 hash 结果值复制到剪贴板。
go
hashFile := func() {
MD5Hash.SetText("")
_, err := os.Stat(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
md5Hash := md5.New()
_, err = io.Copy(md5Hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
MD5Hash.SetText(hex.EncodeToString(md5Hash.Sum(nil)))
}
fileHashBtn.OnTapped = hashFile
- MD5Hash.SetText(""):计算 hash 值函数中,每次调用前先把标签显示的内容置空。
- os.Stat(fileEntry.Text):判断文件是否存在。
- MD5Hash.SetText(hex.EncodeToString(md5Hash.Sum(nil))):hash 值写入到结果值标签。
- fileHashBtn.OnTapped = hashFile:原本应该由 widget.NewButtonWithIcon 传参添加处理函数,改为通过 OnTapped 字段赋值添加处理函数。(后续 hashFile 函数又依赖了 fileHashBtn 按钮。)
go
MD5HashRow := container.NewBorder(nil, nil, MD5Label, MD5CopyBtn, MD5Hash)
使用 container.NewBorder 创建边框布局来展示 MD5 值:
- 左 (left):MD5Label("MD5" 标签)。
- 右 (right):MD5CopyBtn(复制结果值按钮)。
- 中 (center):MD5Hash(MD5 结果值)。
scss
container.NewVBox(fileRow, MD5HashRow)
- container.NewVBox:采用水平布局,根据参数从上到下堆叠,第一行为 fileRow,第二行为 MD5HashRow。
运行一下:
4.下一步,继续拓展功能,同时显示 SHA1,SHA256 计算值结果:
scss
func InitUI(w fyne.Window) fyne.CanvasObject {
fileEntry := widget.NewEntry() // 文件路径输入框
fileEntry.SetPlaceHolder("Enter File Path")
fileOpenBtn := widget.NewButtonWithIcon("Open", theme.FileIcon(), func() {}) // 打开文件按钮
fileHashBtn := widget.NewButtonWithIcon("Hash", theme.MailSendIcon(), func() {}) // 计算哈希按钮
MD5Label := widget.NewLabel("MD5") // MD5 标签
SHA1Label := widget.NewLabel("SHA1") // SHA1 标签
SHA256Label := widget.NewLabel("SHA256") // SHA256 标签
MD5Hash := widget.NewLabel("") // MD5 结果值标签
SHA1Hash := widget.NewLabel("") // SHA1 结果值标签
SHA256Hash := widget.NewLabel("") // SHA256 结果值标签
MD5CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 MD5 结果值按钮
w.Clipboard().SetContent(MD5Hash.Text)
})
SHA1CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 SHA1 结果值按钮
w.Clipboard().SetContent(SHA1Hash.Text)
})
SHA256CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 SHA256 结果值按钮
w.Clipboard().SetContent(SHA256Hash.Text)
})
openFile := func() {
file, err := zenity.SelectFile(
zenity.Title("Open File"),
)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
fileEntry.SetText(file)
}
fileOpenBtn.OnTapped = openFile
hashFile := func() {
MD5Hash.SetText("")
SHA1Hash.SetText("")
SHA256Hash.SetText("")
_, err := os.Stat(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
md5Hash := md5.New()
_, err = io.Copy(md5Hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
MD5Hash.SetText(hex.EncodeToString(md5Hash.Sum(nil)))
}()
go func() {
defer wg.Done()
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
sha1Hash := sha1.New()
_, err = io.Copy(sha1Hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
SHA1Hash.SetText(hex.EncodeToString(sha1Hash.Sum(nil)))
}()
go func() {
defer wg.Done()
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
sha256Hash := sha256.New()
_, err = io.Copy(sha256Hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
SHA256Hash.SetText(hex.EncodeToString(sha256Hash.Sum(nil)))
}()
wg.Wait()
}
fileHashBtn.OnTapped = hashFile
fileRow := container.NewBorder(nil, nil, fileOpenBtn, fileHashBtn, fileEntry)
MD5HashRow := container.NewBorder(nil, nil, MD5Label, MD5CopyBtn, MD5Hash)
SHA1HashRow := container.NewBorder(nil, nil, SHA1Label, SHA1CopyBtn, SHA1Hash)
SHA256HashRow := container.NewBorder(nil, nil, SHA256Label, SHA256CopyBtn, SHA256Hash)
return container.NewVBox(fileRow, MD5HashRow, SHA1HashRow, SHA256HashRow)
}
- container.NewVBox:采用水平布局,根据参数从上到下堆叠,第一行为 fileRow,第二行为 MD5HashRow,第三行为 SHA1HashRow,第四行为 SHA256HashRow。
运行一下:
5.下一步,把显示各 hash 值的标签对齐(这里省略之前部分代码):
go
func InitUI(w fyne.Window) fyne.CanvasObject {
... ...
labelSize := fyne.Size{
Width: SHA256Label.MinSize().Width,
Height: SHA256Label.MinSize().Height,
}
fileRow := container.NewBorder(nil, nil, fileOpenBtn, fileHashBtn, fileEntry)
MD5HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, MD5Label), MD5CopyBtn, MD5Hash)
SHA1HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA1Label), SHA1CopyBtn, SHA1Hash)
SHA256HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA256Label), SHA256CopyBtn, SHA256Hash)
return container.NewVBox(fileRow, MD5HashRow, SHA1HashRow, SHA256HashRow)
}
- labelSize:由于 "SHA256" 标签最长,取该标签的宽度和高度。
- container.NewGridWrap:把 "MD5","SHA1","SHA256" 标签控件由原本直接传入 container.NewBorder,改为通过 container.NewGridWrap 布局改变标签长度后传入,这样通过统一这三个标签的宽度后,标签值将对齐。
运行一下:
6.下一步,把控件和控件、控件和边框之间的距离进行调节(其实到这里基本功能已经实现了,下面是进一步的优化细节,为了展示 fyne 更多功能):
scss
func InitUI(w fyne.Window) fyne.CanvasObject {
... ...
spacer := canvas.NewRectangle(color.Transparent)
spacer.SetMinSize(fyne.NewSize(1, 1))
fileRow := container.NewBorder(nil, nil, container.NewHBox(spacer, fileOpenBtn, spacer), container.NewHBox(spacer, fileHashBtn, spacer), fileEntry)
MD5HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, MD5Label), container.NewHBox(MD5CopyBtn, spacer), MD5Hash)
SHA1HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA1Label), container.NewHBox(SHA1CopyBtn, spacer), SHA1Hash)
SHA256HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA256Label), container.NewHBox(SHA256CopyBtn, spacer), SHA256Hash)
return container.NewVBox(fileRow, spacer, MD5HashRow, SHA1HashRow, SHA256HashRow)
}
- spacer := canvas.NewRectangle(color.Transparent):创建了一个透明的间隔小部件,可用于布局目的,以在其他控件之间添加一些填充或间距。
- spacer.SetMinSize(fyne.NewSize(1, 1)):设置最小尺寸为 1x1 像素。
- container.NewHBox(spacer, fileOpenBtn, spacer):把 fileOpenBtn 由原本直接传入 container.NewBorder ,改为通过 container.NewHBox 布局后传入。container.NewHBox 为垂直布局,根据参数从从左到右排列,所以就是在 fileOpenBtn 打开文件按钮左右各加一个空白间距。fileHashBtn 哈希按钮同样这样处理,三个复制按钮同样在右边加一个空白间距。
- container.NewVBox(firstRow, spacer, MD5HashRow, SHA1HashRow, SHA256HashRow):采用水平布局,根据参数从上到下堆叠,在 firstRow 和 MD5HashRow 之间加了一个spacer 空白间距。
运行一下:
7.下一步,计算 hash 值过程中结果标签栏位置显示进度条:
scss
func InitUI(w fyne.Window) fyne.CanvasObject {
... ...
MD5ProgressBar := widget.NewProgressBarInfinite()
SHA1ProgressBar := widget.NewProgressBarInfinite()
SHA256ProgressBar := widget.NewProgressBarInfinite()
MD5ProgressBar.Hide()
SHA1ProgressBar.Hide()
SHA256ProgressBar.Hide()
... ...
hashFile := func() {
... ...
go func() {
MD5ProgressBar.Show()
defer func() {
wg.Done()
MD5ProgressBar.Hide()
}()
... ...
}()
go func() {
SHA1ProgressBar.Show()
defer func() {
wg.Done()
SHA1ProgressBar.Hide()
}()
... ...
}()
go func() {
SHA256ProgressBar.Show()
defer func() {
wg.Done()
SHA256ProgressBar.Hide()
}()
... ...
}()
wg.Wait()
}
... ...
fileRow := container.NewBorder(nil, nil, container.NewHBox(spacer, fileOpenBtn, spacer), container.NewHBox(spacer, fileHashBtn, spacer), fileEntry)
MD5HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, MD5Label), container.NewHBox(MD5CopyBtn, spacer), MD5Hash, MD5ProgressBar)
SHA1HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA1Label), container.NewHBox(SHA1CopyBtn, spacer), SHA1Hash, SHA1ProgressBar)
SHA256HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA256Label), container.NewHBox(SHA256CopyBtn, spacer), SHA256Hash, SHA256ProgressBar)
return container.NewVBox(fileRow, spacer, MD5HashRow, SHA1HashRow, SHA256HashRow)
}
- widget.NewProgressBarInfinite():创建一个无限循环的进度条控件。
- MD5ProgressBar.Hide():首先进度条控件设置为不可见。
- MD5ProgressBar.Show():计算 hash 值时设为可见。
- defer MD5ProgressBar.Hide():计算结束后设置为不可见。
- MD5ProgressBar:把进度条控件放置在 container.NewBorder 布局的最后一位参数,和 hash 结果值位置重叠,都位于布局中间位置。由于设置了计算结束后进度条控件隐藏,并不影响 hash 结果值显示。
运行一下:
8.下一步,最后完善一下细节,文件输入框,打开文件按钮,计算哈希按钮,这三个控件计算过程中禁止与其交互点击,全部计算结束后恢复交互。完整代码:
scss
package main
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/ncruces/zenity"
"image/color"
"io"
"os"
"sync"
)
func main() {
a := app.New()
w := a.NewWindow("File Hasher")
w.SetContent(InitUI(w))
w.Resize(fyne.NewSize(710, w.Canvas().Size().Height))
w.ShowAndRun()
}
func InitUI(w fyne.Window) fyne.CanvasObject {
fileEntry := widget.NewEntry() // 文件路径输入框
fileEntry.SetPlaceHolder("Enter File Path")
fileOpenBtn := widget.NewButtonWithIcon("Open", theme.FileIcon(), func() {}) // 打开文件按钮
fileHashBtn := widget.NewButtonWithIcon("Hash", theme.MailSendIcon(), func() {}) // 计算哈希按钮
MD5Label := widget.NewLabel("MD5") // MD5 标签
SHA1Label := widget.NewLabel("SHA1") // SHA1 标签
SHA256Label := widget.NewLabel("SHA256") // SHA256 标签
MD5Hash := widget.NewLabel("") // MD5 结果值标签
SHA1Hash := widget.NewLabel("") // SHA1 结果值标签
SHA256Hash := widget.NewLabel("") // SHA256 结果值标签
MD5CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 MD5 结果值按钮
w.Clipboard().SetContent(MD5Hash.Text)
})
SHA1CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 SHA1 结果值按钮
w.Clipboard().SetContent(SHA1Hash.Text)
})
SHA256CopyBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { // 复制 SHA256 结果值按钮
w.Clipboard().SetContent(SHA256Hash.Text)
})
MD5ProgressBar := widget.NewProgressBarInfinite()
SHA1ProgressBar := widget.NewProgressBarInfinite()
SHA256ProgressBar := widget.NewProgressBarInfinite()
MD5ProgressBar.Hide()
SHA1ProgressBar.Hide()
SHA256ProgressBar.Hide()
openFile := func() {
file, err := zenity.SelectFile(
zenity.Title("Open File"),
)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
fileEntry.SetText(file)
}
fileOpenBtn.OnTapped = openFile
hashFile := func() {
go func() {
MD5Hash.SetText("")
SHA1Hash.SetText("")
SHA256Hash.SetText("")
fileEntry.Disable()
fileOpenBtn.Disable()
fileHashBtn.Disable()
defer func() {
fileEntry.Enable()
fileOpenBtn.Enable()
fileHashBtn.Enable()
}()
_, err := os.Stat(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
MD5ProgressBar.Show()
defer func() {
wg.Done()
MD5ProgressBar.Hide()
}()
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
md5Hash := md5.New()
_, err = io.Copy(md5Hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
MD5Hash.SetText(hex.EncodeToString(md5Hash.Sum(nil)))
}()
go func() {
SHA1ProgressBar.Show()
defer func() {
wg.Done()
SHA1ProgressBar.Hide()
}()
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
sha1hash := sha1.New()
_, err = io.Copy(sha1hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
SHA1Hash.SetText(hex.EncodeToString(sha1hash.Sum(nil)))
}()
go func() {
SHA256ProgressBar.Show()
defer func() {
wg.Done()
SHA256ProgressBar.Hide()
}()
f, err := os.Open(fileEntry.Text)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
defer f.Close()
sha256hash := sha256.New()
_, err = io.Copy(sha256hash, f)
if err != nil {
zenity.Error(err.Error(), zenity.Title("error"))
return
}
SHA256Hash.SetText(hex.EncodeToString(sha256hash.Sum(nil)))
}()
wg.Wait()
}()
}
fileHashBtn.OnTapped = hashFile
labelSize := fyne.Size{
Width: SHA256Label.MinSize().Width,
Height: SHA256Label.MinSize().Height,
}
spacer := canvas.NewRectangle(color.Transparent)
spacer.SetMinSize(fyne.NewSize(1, 1))
fileRow := container.NewBorder(nil, nil, container.NewHBox(spacer, fileOpenBtn, spacer), container.NewHBox(spacer, fileHashBtn, spacer), fileEntry)
MD5HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, MD5Label), container.NewHBox(MD5CopyBtn, spacer), MD5Hash, MD5ProgressBar)
SHA1HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA1Label), container.NewHBox(SHA1CopyBtn, spacer), SHA1Hash, SHA1ProgressBar)
SHA256HashRow := container.NewBorder(nil, nil, container.NewGridWrap(labelSize, SHA256Label), container.NewHBox(SHA256CopyBtn, spacer), SHA256Hash, SHA256ProgressBar)
return container.NewVBox(fileRow, spacer, MD5HashRow, SHA1HashRow, SHA256HashRow)
}
- fileEntry.Disable():计算文件 hash 函数开始时 fileEntry 等控件设置为不可与其交互。
- defer fileEntry.Enable():计算结束后恢复可用。
- hashFile:计算文件 hash 函数内部改为通过 go func() {} 调用(github.com/fyne-io/fyn...
运行一下:
这篇文章只介绍了 fyne 一小部分功能,希望此示例能帮助你快速了解使用方法。下一篇文章中会介绍一下 fyne 自定义控件并以此重构代码。更多功能和技巧请阅读官方文档(docs.fyne.io/)。更多示例(http...
References