Fyne:让 Go 开发者也能玩转 GUI

fyne.io/fyne 是一个用于构建桌面和移动 GUI 应用的 Go 语言库。它提供了跨平台支持,允许开发者在 Windows、MacOS、Linux、iOS 和 Android 上使用相同的代码运行应用。

前提条件

Fyne 运行需要以下三个基本要素:

  • Go 语言环境(至少需要 Go 1.12 版本)。
  • C 语言编译器(用于连接系统的图形驱动)。
  • 系统图形驱动。

不同操作系统的安装方式不同,请根据你的操作系统选择合适的安装方式 (Go 语言安装这里就不介绍了)。

注意:这些步骤仅适用于开发阶段,Fyne 应用的最终用户 不需要 额外安装任何依赖或进行设置。

安装

安装 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

github.com/fyne-io/fyn...

fyne.io/

www.poloxue.com/gofyne/

github.com/creamlike10...

www.poloxue.com/gofyne/

相关推荐
用户07956750431414 分钟前
Python + OpenCV 识别 GIF 中旋转最快的图形:基于时间周期的动态识别
后端
长大198817 分钟前
Python+Flask 1小时搭建个人博客:比WordPress轻量10倍
后端
Go_error17 分钟前
Go 实现 Mysql AES 与 Scanner/Valuer 自动加解密
go
闪学it24 分钟前
Python测试开发进阶线上班28期
后端
Go_error24 分钟前
Badu/bus:Go 轻量级泛型发布/订阅事件总线
后端·go
Go_error27 分钟前
Go-redis:执行 Lua 脚本
后端·go
IT毕设实战小研1 小时前
电影数据可视化推荐系统
大数据·后端·爬虫·python·算法·信息可视化·课程设计
ssshooter1 小时前
AI 时代你不能不知道的 git worktree
前端·后端·面试
Cosolar1 小时前
一文弄懂 Agent Harness 与 Agent Runtime 的区别
java·后端·github