基于 golang fyne 构建 windows 桌面程序
背景
链接 我用 Golang 和 go-winres 做了一个文件大搬运
经反馈,使用控制台完成某些功能控制对非程序员来说有一定难度,于是再次致敬劳动工作者和各位卷王,基于 fyne 创建了一个 windows 桌面程序。
程序设计
-
程序主体使用 Golang 开发
-
使用 fyne 进行封装(更多使用方法需自行了解)
源码
main .go
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
"log"
)
var fromVal string
var toVal string
var fileNameVal string
var fuzzyVal bool
var childVal bool
var coverVal bool
var status binding.String
func init() {
// 默认启动模糊匹配
fuzzyVal = true
}
func main() {
a := app.New()
w := a.NewWindow("BigMove @Locker")
entryFrom := &widget.Entry{PlaceHolder: "eg: c:/a/b"}
entryTo := &widget.Entry{PlaceHolder: "eg: c:/a/b/d"}
entryFileName := &widget.Entry{PlaceHolder: "full name or part"}
radioFuzzy := widget.NewRadioGroup([]string{"ENABLE", "DISABLE"}, func(v string) {
if v == "ENABLE" {
fuzzyVal = true
} else {
fuzzyVal = false
}
})
radioFuzzy.SetSelected("ENABLE")
radioFuzzy.Horizontal = true
radioChild := widget.NewRadioGroup([]string{"ENABLE", "DISABLE"}, func(v string) {
if v == "ENABLE" {
childVal = true
} else {
childVal = false
}
})
radioChild.SetSelected("DISABLE")
radioChild.Horizontal = true
radioCover := widget.NewRadioGroup([]string{"ENABLE", "DISABLE"}, func(v string) {
if v == "ENABLE" {
coverVal = true
} else {
coverVal = false
}
})
radioCover.SetSelected("DISABLE")
radioCover.Horizontal = true
// 执行状态
status = binding.NewString()
form := widget.Form{
Items: []*widget.FormItem{
{Text: "From", Widget: entryFrom},
{Text: "To", Widget: entryTo},
{Text: "FileName", Widget: entryFileName},
{Text: "FuzzyMode", Widget: radioFuzzy},
{Text: "ChildMode", Widget: radioChild},
{Text: "CoverMode", Widget: radioCover},
},
OnSubmit: func() {
// 操作目录地址校验
fromVal = entryFrom.Text
if fromVal == "" {
log.Printf("[ERROR] form validation: need From directory\n")
validationAlert("need From directory")
return
}
// 目标目录校验
toVal = entryTo.Text
if toVal == "" {
log.Printf("[ERROR] form validation: need To directory\n")
validationAlert("need To directory")
return
}
// 文件名校验
fileNameVal = entryFileName.Text
if fileNameVal == "" {
log.Printf("[ERROR] form validation: need FileName or part\n")
validationAlert("need FileName or part")
return
}
validationAlert("running...")
// 执行命令
ok, err := doBigMove()
if err != nil {
log.Printf("[ERROR] do big move fail, exp: %s\n", err.Error())
validationAlert("error: move file fail, cause: " + err.Error())
return
}
if ok {
log.Printf("ok\n")
validationAlert("success: move file finish")
} else {
log.Printf("fail\n")
validationAlert("error: move file fail")
}
},
}
// 创建布局
content := container.NewVBox(&form, widget.NewLabelWithData(status))
w.SetContent(content)
w.Resize(fyne.NewSize(500, 300))
w.CenterOnScreen()
w.ShowAndRun()
}
func validationAlert(msg string) {
status.Set(msg)
}
domove.go
package main
import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync/atomic"
)
// doBigMove 执行大搬运
func doBigMove() (bool, error) {
checkOk, err := beforeCheck()
if !checkOk {
return false, err
}
// 读取操作目录信息
fromDir, err := os.ReadDir(fromVal)
if err != nil {
return false, errors.New(fmt.Sprintf("读取操作目录失败 err: %s", err.Error()))
}
// 判断是否需要执行子目录任务
if childVal {
doWithChild(fromVal, fromDir)
} else {
doCurrent(fromVal, fromDir)
}
return true, nil
}
// beforeCheck 前置检查
func beforeCheck() (bool, error) {
// 操作目录检查
if fromVal == "" {
return false, errors.New("须指定操作目录 from")
}
// 目标目录检查
if toVal == "" {
return false, errors.New("须指定操作目录 to")
}
if !isFolderExists(toVal) {
err := os.MkdirAll(toVal, os.ModePerm)
if err != nil {
return false, errors.New(fmt.Sprintf("创建目标目录失败 err: %s", err.Error()))
}
}
return true, nil
}
// doCurrent 操作当前目录
func doCurrent(dir string, fromDir []os.DirEntry) {
for _, f := range fromDir {
// 跳过子目录
if f.IsDir() {
continue
}
// 操作文件
moveFile(dir, f)
}
}
// doWithChild 操作包含子目录
func doWithChild(dir string, fromDir []os.DirEntry) {
for _, f := range fromDir {
// 检查子目录
if f.IsDir() {
childDir := filepath.Join(dir, f.Name())
fromChildDir, err := os.ReadDir(childDir)
if err != nil {
log.Printf("[ERROR] 读取目录[%s]失败 err: %s", f.Name(), err.Error())
continue
}
// 优先处理子目录
doWithChild(childDir, fromChildDir)
continue
}
// 操作文件
moveFile(dir, f)
}
}
// moveFile 移动文件
func moveFile(dir string, f os.DirEntry) {
// 仅处理文件,此为双重校验
if f.IsDir() {
return
}
// 文件名匹配
fileName := f.Name()
if fileNameVal != "" {
if fuzzyVal {
if !strings.Contains(fileName, fileNameVal) {
return
}
} else {
if fileName != fileNameVal {
return
}
}
}
// 移动文件
srcFilePathName := filepath.Join(dir, fileName)
srcFile, err := os.Open(srcFilePathName)
if err != nil {
log.Printf("[ERROR] 读取文件[%s]失败 err: %s", srcFilePathName, err.Error())
return
}
defer srcFile.Close() // release
// 创建目标文件
dstFilePathName := filepath.Join(toVal, fileName)
// 非覆盖模式,需检查序号
if !coverVal {
var index int32
dirs, err := os.ReadDir(toVal)
if err != nil {
log.Printf("[ERROR] 读取目标文件[%s]失败 err: %s", toVal, err.Error())
return
}
// 检查该目录下文件,确定增补序号
for _, dir := range dirs {
if strings.Contains(dir.Name(), fileName) {
// 精确匹配
atomic.AddInt32(&index, 1)
} else if strings.Contains(dir.Name(), ").") {
// 序号扩展匹配
curNamePre := dir.Name()[:strings.Index(dir.Name(), "(")]
compareNamePre := fileName[:strings.Index(fileName, ".")]
if compareNamePre == curNamePre {
atomic.AddInt32(&index, 1)
}
}
}
if index > 0 {
ns := strings.Split(fileName, ".")
baseName := ns[0]
var baseExt string
if len(ns) > 1 { // for non ext file
baseExt = ns[1]
}
dstFilePathName = filepath.Join(toVal, fmt.Sprintf("%s(%d).%s", baseName, index, baseExt))
}
}
// 复制文件并删除原文件
dstFile, err := os.Create(dstFilePathName)
if err != nil {
log.Printf("[ERROR] 创建目标文件[%s]失败 err: %s", dstFilePathName, err.Error())
return
}
defer dstFile.Close() // release
_, err = io.Copy(dstFile, srcFile)
if err != nil {
log.Printf("[ERROR] 复制文件内容[%s]失败 err: %s", srcFilePathName, err.Error())
return
}
err = dstFile.Sync()
if err != nil {
log.Printf("[ERROR] 移动文件[%s]失败 err: %s", srcFilePathName, err.Error())
return
}
}
// isFolderExists 检查目录是否存在
func isFolderExists(folderName string) bool {
info, err := os.Stat(folderName)
if os.IsNotExist(err) {
return false
}
if err != nil {
return false
}
return info.IsDir()
}
FyneApp.toml
[Details]
Icon = "./images/fyne.png"
Name = "BigMove"
ID = "com.locker.bigmove"
Version = "1.1.0"
Build = 7
[Development]
HelperText = "This binary was built with debug symbols"
[Release]
HelperText = "This binary was built without debug symbols"
[LinuxAndBSD]
GenericName = "Toolkit Demo"
Categories = ["Development"]
Comment = "A demo of Fyne and its capabilities."
Keywords = ["bigmove", "fyne"]
编译脚本
- 安装 fyne
go install fyne.io/fyne/v2@latest
- 打包
fyne package -os windows -icon images/fyne.png
界面展示
后来
朋友在软件的帮助下……工作得挺好。
我们都很高兴。