This commit is contained in:
2026-06-22 11:33:18 +08:00
commit 9277e4a2b8
24 changed files with 3700 additions and 0 deletions
+452
View File
@@ -0,0 +1,452 @@
# 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)