# Fyne + Go 命令执行与输出捕获教程 ## 1. Fyne 快速入门 ### 安装 Fyne ```bash go get fyne.io/fyne/v2@latest ``` ### 最小 Fyne 窗口 ```go package main import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/widget" ) func main() { myApp := app.New() myWindow := myApp.NewWindow("Hello Fyne") myWindow.SetContent(widget.NewLabel("Hello, World!")) myWindow.Resize(fyne.NewSize(400, 300)) myWindow.ShowAndRun() } ``` ### 常用控件 | 控件 | 说明 | |------|------| | `widget.NewLabel(text)` | 文本标签 | | `widget.NewButton(text, callback)` | 按钮 | | `widget.NewEntry()` | 单行输入框 | | `widget.NewMultiLineEntry()` | 多行输入框 | | `widget.NewCheck(text, callback)` | 复选框 | | `widget.NewSelect(options, callback)` | 下拉选择 | | `widget.NewProgressBar()` | 进度条 | ### 布局容器 | 容器 | 说明 | |------|------| | `container.NewVBox(...)` | 垂直排列 | | `container.NewHBox(...)` | 水平排列 | | `container.NewBorder(top, bottom, left, right, center)` | 边框布局 | | `container.NewGridWithColumns(cols, ...)` | 网格布局 | --- ## 2. Go 执行外部命令并获取输出 ### 核心包: `os/exec` Go 标准库 `os/exec` 包提供了执行外部命令的功能。 ### 2.1 基础方法 #### 获取命令输出 (一次性) ```go package main import ( "fmt" "os/exec" ) func main() { // cmd := exec.Command("cmd", "/c", "dir") // Windows cmd := exec.Command("ls", "-l") // Linux/macOS output, err := cmd.Output() // 返回 []byte if err != nil { fmt.Println("执行出错:", err) return } fmt.Println(string(output)) } ``` #### 合并 stdout + stderr ```go output, err := cmd.CombinedOutput() // stdout + stderr 合并 ``` #### 分别获取 stdout 和 stderr ```go var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() fmt.Println("stdout:", stdout.String()) fmt.Println("stderr:", stderr.String()) ``` ### 2.2 实时读取输出 (逐行) 适用于长时间运行的命令或需要实时显示进度的场景。 ```go func runCommandWithLiveOutput(name string, args ...string) error { cmd := exec.Command(name, args...) stdout, err := cmd.StdoutPipe() if err != nil { return err } stderr, err := cmd.StderrPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } // 并发读取 stdout go func() { scanner := bufio.NewScanner(stdout) for scanner.Scan() { fmt.Println("[OUT]", scanner.Text()) } }() // 并发读取 stderr go func() { scanner := bufio.NewScanner(stderr) for scanner.Scan() { fmt.Println("[ERR]", scanner.Text()) } }() return cmd.Wait() } ``` ### 2.3 设置超时 ```go func runWithTimeout(timeout time.Duration, name string, args ...string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() cmd := exec.CommandContext(ctx, name, args...) output, err := cmd.Output() if ctx.Err() == context.DeadlineExceeded { return "", fmt.Errorf("命令执行超时 (%v)", timeout) } return string(output), err } ``` ### 2.4 设置工作目录和环境变量 ```go cmd := exec.Command("make", "build") cmd.Dir = "/path/to/project" // 设置工作目录 cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") // 追加环境变量 ``` ### 2.5 Windows 特殊处理 Windows 下 `cmd` 命令需要 `/c` 前缀: ```go // Windows cmd := exec.Command("cmd", "/c", "dir") // 或者使用 PowerShell cmd := exec.Command("powershell", "-Command", "Get-ChildItem") ``` 通用跨平台封装: ```go func shellCommand(command string) *exec.Cmd { if runtime.GOOS == "windows" { return exec.Command("cmd", "/c", command) } return exec.Command("sh", "-c", command) } ``` --- ## 3. Fyne + 命令执行完整示例 ### 3.1 简单的命令执行 GUI ```go package main import ( "bytes" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" "os/exec" "runtime" ) func main() { myApp := app.New() myWindow := myApp.NewWindow("命令执行器") myWindow.Resize(fyne.NewSize(600, 400)) cmdEntry := widget.NewEntry() cmdEntry.SetPlaceHolder("输入命令,如: ping 127.0.0.1") outputLabel := widget.NewMultiLineEntry() outputLabel.Disable() // 只读 outputLabel.Wrapping = fyne.TextWrapWord runBtn := widget.NewButton("执行", func() { command := cmdEntry.Text if command == "" { return } var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("cmd", "/c", command) } else { cmd = exec.Command("sh", "-c", command) } var out bytes.Buffer var errOut bytes.Buffer cmd.Stdout = &out cmd.Stderr = &errOut err := cmd.Run() if err != nil { outputLabel.SetText("错误: " + err.Error() + "\n" + errOut.String()) return } outputLabel.SetText(out.String()) }) content := container.NewVBox( widget.NewLabel("输入要执行的命令:"), cmdEntry, runBtn, widget.NewLabel("输出:"), outputLabel, ) myWindow.SetContent(content) myWindow.ShowAndRun() } ``` ### 3.2 带实时输出的进阶版本 ```go package main import ( "bufio" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" "os/exec" "runtime" "sync" ) func main() { myApp := app.New() myWindow := myApp.NewWindow("实时命令输出") myWindow.Resize(fyne.NewSize(700, 500)) cmdEntry := widget.NewEntry() cmdEntry.SetPlaceHolder("输入命令...") output := widget.NewMultiLineEntry() output.Disable() output.Wrapping = fyne.TextWrapWord var mu sync.Mutex appendOutput := func(text string) { mu.Lock() defer mu.Unlock() current := output.Text if len(current) > 0 { current += "\n" } output.SetText(current + text) } runBtn := widget.NewButton("执行", func() { command := cmdEntry.Text if command == "" { return } var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("cmd", "/c", command) } else { cmd = exec.Command("sh", "-c", command) } stdout, _ := cmd.StdoutPipe() stderr, _ := cmd.StderrPipe() cmd.Start() go func() { scanner := bufio.NewScanner(stdout) for scanner.Scan() { appendOutput(scanner.Text()) } }() go func() { scanner := bufio.NewScanner(stderr) for scanner.Scan() { appendOutput("[ERR] " + scanner.Text()) } }() cmd.Wait() }) clearBtn := widget.NewButton("清空", func() { output.SetText("") }) content := container.NewBorder( container.NewVBox( widget.NewLabelWithStyle("命令执行器 (实时输出)", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), widget.NewSeparator(), cmdEntry, container.NewHBox(runBtn, clearBtn), ), nil, nil, nil, output, ) myWindow.SetContent(content) myWindow.ShowAndRun() } ``` ### 3.3 带进度条的长时间任务 ```go package main import ( "context" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" "os/exec" "time" ) func main() { myApp := app.New() myWindow := myApp.NewWindow("带超时的命令执行") myWindow.Resize(fyne.NewSize(500, 300)) progress := widget.NewProgressBar() statusLabel := widget.NewLabel("就绪") go func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "ping", "-n", "5", "127.0.0.1") // Windows // cmd := exec.CommandContext(ctx, "ping", "-c", "5", "127.0.0.1") // Linux/macOS // 模拟进度更新 go func() { for i := 0; i <= 100; i++ { progress.SetValue(float64(i) / 100.0) time.Sleep(100 * time.Millisecond) } }() output, err := cmd.Output() if ctx.Err() == context.DeadlineExceeded { statusLabel.SetText("执行超时!") return } if err != nil { statusLabel.SetText("错误: " + err.Error()) return } statusLabel.SetText("完成! 输出: " + string(output)) }() content := container.NewVBox( widget.NewLabel("任务进度:"), progress, statusLabel, ) myWindow.SetContent(content) myWindow.ShowAndRun() } ``` --- ## 4. `os/exec` 关键方法对比 | 方法 | 用途 | 特点 | |------|------|------| | `cmd.Run()` | 执行命令并等待完成 | stdout/stderr 需手动设置 | | `cmd.Output()` | 执行并返回 stdout | stderr 为空时出错 | | `cmd.CombinedOutput()` | 执行并返回 stdout+stderr | stderr 合并到输出 | | `cmd.Start()` + `cmd.Wait()` | 手动控制启停 | 可配合 Pipe 实现实时输出 | | `cmd.StdoutPipe()` | 获取 stdout 管道 | 需在 Start 前调用 | | `cmd.StderrPipe()` | 获取 stderr 管道 | 需在 Start 前调用 | | `exec.CommandContext()` | 带 context 的命令 | 支持超时/取消 | --- ## 5. 常见问题 ### Q: `cmd.Output()` 报 "exit status 1" **A:** 命令执行失败且 stderr 有内容时,`Output()` 会直接报错。改用 `CombinedOutput()` 或手动设置 `cmd.Stdout` / `cmd.Stderr`。 ### Q: 如何执行 `cd` 命令? **A:** `cd` 是 shell 内置命令,不能用 `exec.Command` 直接执行。应使用 `cmd.Dir` 设置工作目录。 ### Q: Fyne 编译后文件太大? **A:** 使用 `go build -ldflags="-s -w"` 压缩,或使用 `fyne package` 打包。 ### Q: Fyne 不支持 Windows 7? **A:** Fyne v2.4+ 需要 Windows 10 1809+。 --- ## 6. 参考资料 - [Fyne 官方文档](https://developer.fyne.io/) - [Go os/exec 文档](https://pkg.go.dev/os/exec) - [Fyne GitHub](https://github.com/fyne-io/fyne)