10 KiB
10 KiB
Fyne + Go 命令执行与输出捕获教程
1. Fyne 快速入门
安装 Fyne
go get fyne.io/fyne/v2@latest
最小 Fyne 窗口
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 基础方法
获取命令输出 (一次性)
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
output, err := cmd.CombinedOutput() // stdout + stderr 合并
分别获取 stdout 和 stderr
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 实时读取输出 (逐行)
适用于长时间运行的命令或需要实时显示进度的场景。
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 设置超时
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 设置工作目录和环境变量
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 前缀:
// Windows
cmd := exec.Command("cmd", "/c", "dir")
// 或者使用 PowerShell
cmd := exec.Command("powershell", "-Command", "Get-ChildItem")
通用跨平台封装:
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
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 带实时输出的进阶版本
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 带进度条的长时间任务
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+。