概述
最近想动手做一些小工具,命令行输入有诸多麻烦,没有可视化工具直观,于是想写gui程序
语言的话,选来选去,还是想用golang写,锻炼一下自己
于是找了找开源的 golang gui框架
发现大多需要CGO编译,嫌麻烦,找了几个go 实现的gui框架,无需c++编译器
以下是找到的框架列表
- gio
- goey
- walk
- govcl
- winc
gio
这个框架的代码没有在github
key | value |
---|---|
官网 | https://gioui.org/ |
代码 | https://git.sr.ht/~eliasnaur/gio/tree |
官方例子 | https://git.sr.ht/~eliasnaur/gio-example |
煮蛋器详细教程 | https://jonegil.github.io/gui-with-gio/ |
优点 | 可以很细腻的调整样式 |
缺点 | 调样式的时候,需要一层一层的写func(return x.Layout),感觉非常啰嗦;输入框不支持中文,调了好久,不是很熟悉,无果,放弃了 |
个人评价 | 文档太少了,官网也很简略,写起来感觉很啰嗦、别扭,只想写一个小工具,不想整那些啰里啰嗦的样式;仓库网站不是很熟悉,代码看起来也很别扭 |
以下是在界面上展现一个输入框、一个按钮的代码
(不要在意界面是否美观,样式我瞎写的)
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 |
---|---|
官网 | https://bitbucket.org/rj/goey/src/v0.9.0/ |
官方例子 | https://bitbucket.org/rj/goey/src/v0.9.0/example/ |
优点 | 写起来还算舒服,默认样式够用,不用怎么调整 |
缺点 | 控件的value的更新,需要重新绘制整个窗口,感觉很麻烦、耦合 |
个人评价 | 官方例子丰富,上手简单,支持minifest,就是重新绘制窗口这个很别扭 |
[图片上传失败...(image-77c39f-1683305015690)]
代码写起来还算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 |
---|---|
官网 | https://github.com/lxn/walk |
官方例子 | https://github.com/lxn/walk/tree/master/examples |
优点 | 写起来还算舒服,支持控件内容单独刷新,控件还算丰富 |
缺点 | 样式有一些bug,包括调整不生效,显示空白等 ;必须配合 manifest 才能显示界面;上次更新还是2021年 |
个人评价 | 页面有一些小bug,不过作为小工具无伤大雅;功能完善:包括控件的种类、动态创建控件等;我用的就是这个 |
代码
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: "漫画下载器",
Size: Size{Width: 400, Height: 600},
Layout: VBox{},
MenuItems: []MenuItem{
Menu{
Text: "&Help",
Items: []MenuItem{
Action{
Text: "About",
OnTriggered: func() { aboutAction_Triggered() },
},
Action{
Text: "页面空白",
OnTriggered: func() { tips() },
},
},
},
},
Children: []Widget{
Label{Text: "页面空白的话,切换一下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: "网址", 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, "错误", "网址为空", walk.MsgBoxIconError)
return
}
if path == "" {
walk.MsgBox(nil, "错误", "网址为空", walk.MsgBoxIconError)
return
}
outTE.SetText("开始下载\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,
"提示",
"页面空白的话,切换一下tab",
walk.MsgBoxOK|walk.MsgBoxIconInformation)
}
govcl
key | value |
---|---|
官网 | https://z-kit.cc/ |
官方例子 | https://github.com/ying32/govcl/tree/master/samples |
优点 | 界面设计用的是可视化工具Lazarus,再转go代码,无需写界面代码,专心写逻辑就行;作者是国人,更新还算及时 |
缺点 | 上手的时候,安装配套工具需要花一定时间,装好了就还好;界面代码生成完成之后很难用代码修改 |
个人评价 | 开发过程和c# winform 类似,有界面设计工具 |
winc
key | value |
---|---|
官网 | https://github.com/tadvi/winc |
官方例子 | https://github.com/tadvi/winc/tree/master/examples |
优点 | 写起来还算舒服 |
缺点 | 控件有点少(也可能是我不熟悉,没找到grid),每个控件都需要手动设置 position(也可能是我不熟悉),有点麻烦 |
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()
}