概述
最近想動(dòng)手做一些小工具,命令行輸入有諸多麻煩,沒(méi)有可視化工具直觀,于是想寫(xiě)gui程序
語(yǔ)言的話,選來(lái)選去,還是想用golang寫(xiě),鍛煉一下自己
于是找了找開(kāi)源的 golang gui框架
發(fā)現(xiàn)大多需要CGO編譯,嫌麻煩,找了幾個(gè)go 實(shí)現(xiàn)的gui框架,無(wú)需c++編譯器
以下是找到的框架列表
- gio
- goey
- walk
- govcl
- winc
gio
這個(gè)框架的代碼沒(méi)有在github
| key | value |
|---|---|
| 官網(wǎng) | https://gioui.org/ |
| 代碼 | https://git.sr.ht/~eliasnaur/gio/tree |
| 官方例子 | https://git.sr.ht/~eliasnaur/gio-example |
| 煮蛋器詳細(xì)教程 | https://jonegil.github.io/gui-with-gio/ |
| 優(yōu)點(diǎn) | 可以很細(xì)膩的調(diào)整樣式 |
| 缺點(diǎn) | 調(diào)樣式的時(shí)候,需要一層一層的寫(xiě)func(return x.Layout),感覺(jué)非常啰嗦;輸入框不支持中文,調(diào)了好久,不是很熟悉,無(wú)果,放棄了 |
| 個(gè)人評(píng)價(jià) | 文檔太少了,官網(wǎng)也很簡(jiǎn)略,寫(xiě)起來(lái)感覺(jué)很啰嗦、別扭,只想寫(xiě)一個(gè)小工具,不想整那些啰里啰嗦的樣式;倉(cāng)庫(kù)網(wǎng)站不是很熟悉,代碼看起來(lái)也很別扭 |
以下是在界面上展現(xiàn)一個(gè)輸入框、一個(gè)按鈕的代碼
(不要在意界面是否美觀,樣式我瞎寫(xiě)的)

不要在意界面是否美觀,我沒(méi)認(rèn)真調(diào)整
package main
import (
"image/color"
"log"
"os"
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget/material"
"gioui.org/unit"
"gioui.org/widget"
)
func main() {
go func() {
w := app.NewWindow()
err := run(w)
if err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
func run(w *app.Window) error {
for {
e := <-w.Events()
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
myWindow(e)
}
}
}
func myWindow(e system.FrameEvent) {
var ops op.Ops
th := material.NewTheme(gofont.Collection())
gtx := layout.NewContext(&ops, e)
var startButton widget.Clickable
var input1 widget.Editor
// layout.Dimensions
layout.Flex{
// Vertical alignment, from top to bottom
Axis: layout.Vertical,
// Empty space is left at the start, i.e. at the top
Spacing: layout.SpaceStart,
}.Layout(gtx,
// We insert two rigid elements:
// First a button ...
layout.Rigid(
func(gtx layout.Context) layout.Dimensions {
margins := layout.Inset{
Top: unit.Dp(25),
Right: unit.Dp(25),
Bottom: unit.Dp(25),
Left: unit.Dp(25),
}
return margins.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
// ... and borders ...
border := widget.Border{
Color: color.NRGBA{R: 204, G: 204, B: 204, A: 255},
CornerRadius: unit.Dp(3),
Width: unit.Dp(2),
}
input := material.Editor(th, &input1, "ha")
return border.Layout(gtx, input.Layout)
})
},
),
layout.Rigid(
func(gtx layout.Context) layout.Dimensions {
margins := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
return margins.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
btn := material.Button(th, &startButton, "Start")
return btn.Layout(gtx)
})
},
),
)
e.Frame(gtx.Ops)
}
goey
代碼在 bitbucket
| key | value |
|---|---|
| 官網(wǎng) | https://bitbucket.org/rj/goey/src/v0.9.0/ |
| 官方例子 | https://bitbucket.org/rj/goey/src/v0.9.0/example/ |
| 優(yōu)點(diǎn) | 寫(xiě)起來(lái)還算舒服,默認(rèn)樣式夠用,不用怎么調(diào)整 |
| 缺點(diǎn) | 控件的value的更新,需要重新繪制整個(gè)窗口,感覺(jué)很麻煩、耦合 |
| 個(gè)人評(píng)價(jià) | 官方例子豐富,上手簡(jiǎn)單,支持minifest,就是重新繪制窗口這個(gè)很別扭 |
[圖片上傳失敗...(image-77c39f-1683305015690)]
代碼寫(xiě)起來(lái)還算ok
package main
import (
"fmt"
"strconv"
"bitbucket.org/rj/goey"
"bitbucket.org/rj/goey/base"
"bitbucket.org/rj/goey/loop"
)
var (
mainWindow *goey.Window
feetValue string
meterValue string
)
func main() {
err := loop.Run(createWindow)
if err != nil {
fmt.Println("Error: ", err.Error())
}
}
func createWindow() error {
// Add the controls
mw, err := goey.NewWindow("Feet to Meters", render())
if err != nil {
return err
}
mainWindow = mw
return nil
}
func update() {
err := mainWindow.SetChild(render())
if err != nil {
fmt.Println("Error: ", err.Error())
}
}
func render() base.Widget {
return &goey.Padding{
Insets: goey.DefaultInsets(),
Child: &goey.Align{Child: &MinSizedBox{Child: &goey.VBox{
AlignMain: goey.MainCenter,
Children: []base.Widget{
&goey.HBox{
AlignMain: goey.Homogeneous,
AlignCross: goey.CrossCenter,
Children: []base.Widget{
&goey.Empty{},
&goey.TextInput{Value: feetValue, OnChange: func(v string) { feetValue = v }, OnEnterKey: func(v string) { feetValue = v; calculate() }},
&goey.Label{Text: "feet"},
},
}, &goey.HBox{
AlignMain: goey.Homogeneous,
AlignCross: goey.CrossCenter,
Children: []base.Widget{
&goey.Label{Text: "is equivalent to"},
&goey.Label{Text: meterValue},
&goey.Label{Text: "meters"},
},
}, &goey.HBox{
AlignMain: goey.Homogeneous,
AlignCross: goey.CrossCenter,
Children: []base.Widget{
&goey.Empty{},
&goey.Empty{},
&goey.Button{Text: "Calculate", Default: true, OnClick: calculate},
},
},
},
}}},
}
}
func calculate() {
feet, err := strconv.ParseFloat(feetValue, 64)
if err != nil {
meterValue = "(error)"
} else {
meterValue = fmt.Sprintf("%f", feet*0.3048)
}
update()
}
walk
代碼在 github
| key | value |
|---|---|
| 官網(wǎng) | https://github.com/lxn/walk |
| 官方例子 | https://github.com/lxn/walk/tree/master/examples |
| 優(yōu)點(diǎn) | 寫(xiě)起來(lái)還算舒服,支持控件內(nèi)容單獨(dú)刷新,控件還算豐富 |
| 缺點(diǎn) | 樣式有一些bug,包括調(diào)整不生效,顯示空白等 ;必須配合 manifest 才能顯示界面;上次更新還是2021年 |
| 個(gè)人評(píng)價(jià) | 頁(yè)面有一些小bug,不過(guò)作為小工具無(wú)傷大雅;功能完善:包括控件的種類(lèi)、動(dòng)態(tài)創(chuàng)建控件等;我用的就是這個(gè) |

image.png
代碼
package main
import (
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
)
func main() {
var outTE *walk.TextEdit
var urlInput *walk.LineEdit
var pathInput *walk.LineEdit
var proxyInput *walk.LineEdit
var startBtn, stopBtn *walk.PushButton
var mw *MainWindow
mw = &MainWindow{
Title: "漫畫(huà)下載器",
Size: Size{Width: 400, Height: 600},
Layout: VBox{},
MenuItems: []MenuItem{
Menu{
Text: "&Help",
Items: []MenuItem{
Action{
Text: "About",
OnTriggered: func() { aboutAction_Triggered() },
},
Action{
Text: "頁(yè)面空白",
OnTriggered: func() { tips() },
},
},
},
},
Children: []Widget{
Label{Text: "頁(yè)面空白的話,切換一下tab", TextColor: walk.RGB(102, 178, 255)},
TabWidget{
Pages: []TabPage{
{
Title: "下載",
// Layout: Grid{Columns: 2},
Visible: true,
Layout: VBox{},
Children: []Widget{
Composite{
Layout: Grid{Columns: 2},
Children: []Widget{
Label{Text: "網(wǎng)址", TextColor: walk.RGB(255, 0, 127)},
LineEdit{
Text: "",
AssignTo: &urlInput,
},
Label{Text: "保存路徑"},
LineEdit{
Text: ``,
AssignTo: &pathInput,
},
Label{Text: "代理"},
LineEdit{
Text: "",
AssignTo: &proxyInput, ToolTipText: "比如 socks5://127.0.0.1:1080",
},
},
},
PushButton{
Text: "下載",
AssignTo: &startBtn,
MaxSize: Size{Width: 400, Height: 100},
MinSize: Size{Width: 300, Height: 30},
Background: SolidColorBrush{Color: walk.RGB(0x5F, 0x69, 0x8E)},
OnClicked: func() {
url := urlInput.Text()
path := pathInput.Text()
if url == "" {
walk.MsgBox(nil, "錯(cuò)誤", "網(wǎng)址為空", walk.MsgBoxIconError)
return
}
if path == "" {
walk.MsgBox(nil, "錯(cuò)誤", "網(wǎng)址為空", walk.MsgBoxIconError)
return
}
outTE.SetText("開(kāi)始下載\r\n")
},
},
PushButton{
Text: "停止",
AssignTo: &stopBtn,
MaxSize: Size{Width: 400, Height: 100},
MinSize: Size{Width: 300, Height: 30},
Background: SolidColorBrush{Color: walk.RGB(0x5F, 0x69, 0x8E)},
Enabled: false,
OnClicked: func() {
},
},
Composite{
Layout: Grid{Columns: 2},
Children: []Widget{
Label{Text: "log", TextColor: walk.RGB(255, 0, 127)},
TextEdit{AssignTo: &outTE, ReadOnly: true, VScroll: true},
},
},
},
},
{
Title: "About",
// Layout: Grid{Columns: 2},
Layout: VBox{},
Visible: true,
Children: []Widget{
TextEdit{Text: "哈哈哈",
ReadOnly: true},
},
},
},
},
},
}
mw.Run()
}
func aboutAction_Triggered() {
walk.MsgBox(nil,
"提示",
"An example that demonstrates a main window that supports multiple pages.",
walk.MsgBoxOK|walk.MsgBoxIconInformation)
}
func tips() {
walk.MsgBox(nil,
"提示",
"頁(yè)面空白的話,切換一下tab",
walk.MsgBoxOK|walk.MsgBoxIconInformation)
}
govcl
| key | value |
|---|---|
| 官網(wǎng) | https://z-kit.cc/ |
| 官方例子 | https://github.com/ying32/govcl/tree/master/samples |
| 優(yōu)點(diǎn) | 界面設(shè)計(jì)用的是可視化工具Lazarus,再轉(zhuǎn)go代碼,無(wú)需寫(xiě)界面代碼,專心寫(xiě)邏輯就行;作者是國(guó)人,更新還算及時(shí) |
| 缺點(diǎn) | 上手的時(shí)候,安裝配套工具需要花一定時(shí)間,裝好了就還好;界面代碼生成完成之后很難用代碼修改 |
| 個(gè)人評(píng)價(jià) | 開(kāi)發(fā)過(guò)程和c# winform 類(lèi)似,有界面設(shè)計(jì)工具 |
winc
| key | value |
|---|---|
| 官網(wǎng) | https://github.com/tadvi/winc |
| 官方例子 | https://github.com/tadvi/winc/tree/master/examples |
| 優(yōu)點(diǎn) | 寫(xiě)起來(lái)還算舒服 |
| 缺點(diǎn) | 控件有點(diǎn)少(也可能是我不熟悉,沒(méi)找到grid),每個(gè)控件都需要手動(dòng)設(shè)置 position(也可能是我不熟悉),有點(diǎn)麻煩 |

image.png
package main
import (
"github.com/tadvi/winc"
)
func main() {
mainWindow := winc.NewForm(nil)
mainWindow.SetSize(400, 300) // (width, height)
mainWindow.SetText("Yellow Demo")
label := winc.NewLabel(mainWindow)
label.SetText("hei")
label.SetPos(10, 10)
edt := winc.NewEdit(mainWindow)
edt.SetPos(50, 20)
// Most Controls have default size unless SetSize is called.
edt.SetText("edit text")
btn := winc.NewPushButton(mainWindow)
btn.SetText("Show or Hide")
btn.SetPos(40, 50) // (x, y)
btn.SetSize(100, 40) // (width, height)
btn.OnClick().Bind(func(e *winc.Event) {
if edt.Visible() {
edt.Hide()
} else {
edt.Show()
edt.SetText(edt.Text() + "0,")
}
})
mainWindow.Center()
mainWindow.Show()
mainWindow.OnClose().Bind(wndOnClose)
winc.RunMainLoop() // Must call to start event loop.
}
func wndOnClose(arg *winc.Event) {
winc.Exit()
}