Files
SRCC/docs/fyne-go-command-tutorial.md
T
2026-06-22 11:33:18 +08:00

10 KiB
Raw Blame History

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+。


6. 参考资料