虽然这不是golang的特长,不过偶尔也会涉及到带界面的需求,但舍近求远去用其它语言还是比较不爽的。MS在界面设计上一直都比较简单,但go就比较麻烦一些。
今天学习的是 github.com/AllenDang/giu,看看它是否符合我们大部份gui需求。
它给出的截图是这样的,看起来控件还是比较丰富的。
来一个Hello World,惯例。
package main
import (
"fmt"
g "github.com/AllenDang/giu"
)
func onClickMe() {
fmt.Println("Hello world!")
}
func onImSoCute() {
fmt.Println("Im sooooooo cute!!")
}
func loop() {
g.SingleWindow("hello world", g.Layout{
g.Label("Hello world from giu"),
g.Line(
g.Button("点我噻", onClickMe),
g.Button("I'm so cute", onImSoCute)),
})
}
func main() {
wnd := g.NewMasterWindow("Hello world", 400, 200, g.MasterWindowFlagsNotResizable, nil) #定义一个不能重置大小的窗口
wnd.Main(loop)
}
看代码还是比较简单,符合习惯。不过中文是咋的呢?
giu是一个即时模式的GUI框架,意思是部件不保存内部状态,估计得靠自己。
布局
type Widget interface {
Build()
}
拆分布局
可随意重置大小布局示例。示例splitter
package main
import (
g "github.com/AllenDang/giu"
)
func loop() {
g.SingleWindow("splitter", g.Layout{
g.SplitLayout("Split", g.DirectionHorizontal, true, 200, #可拆分的垂直布局,带边框,左方200像素
g.Layout{ #布局1,左方部份
g.Label("Left panel"), #一个标签
g.Line(g.Button("Button1", nil), g.Button("Button2", nil)), #两个按钮,没有回调
},
g.SplitLayout("Right panel", g.DirectionVertical, true, 200, #右方布局又嵌套可拆分布局,按水平布局,上方为200像素
g.Layout{}, #布局1没内容
g.SplitLayout("HSplit", g.DirectionHorizontal, true, 200, #布局2又嵌套垂直布局
g.Layout{}, #布局1没内容
g.SplitLayout("VSplit", g.DirectionVertical, true, 100, #布局2还嵌套水平布局
g.Layout{},
g.Layout{},
),
),
),
),
})
}
func main() {
wnd := g.NewMasterWindow("Splitter", 800, 600, 0, nil)
wnd.Main(loop)
}
不过层层嵌套还是比较烦。
部件
库能不能用,一方面还得看控件够不够,是不是简单易用。示例widgets
package main
import (
"fmt"
g "github.com/AllenDang/giu"
"github.com/AllenDang/giu/imgui"
)
var (
name string
items []string
itemSelected int32
checked bool
checked2 bool
dragInt int32
multiline string
radioOp int
)
func btnClickMeClicked() {
fmt.Println("Click me is clicked")
}
func comboChanged() {
fmt.Println(items[itemSelected])
}
func contextMenu1Clicked() {
fmt.Println("Context menu 1 is clicked")
}
func contextMenu2Clicked() {
fmt.Println("Context menu 2 is clicked")
}
func btnPopupCLicked() {
g.OpenPopup("Confirm")
}
func loop() {
g.SingleWindowWithMenuBar("Overview", g.Layout{
g.MenuBar( #菜单定义
g.Layout{
g.Menu("File", g.Layout{
g.MenuItem("Open"), #不知道这里它是如何响应点击的?
g.MenuItem("Save"),
// You could add any kind of widget here, not just menu item.
g.Menu("Save as ...", g.Layout{ #子菜单
g.MenuItem("Excel file"),
g.MenuItem("CSV file"),
g.Button("Button inside menu", nil), #嵌入按钮
},
),
},
),
},
),
g.Label("One line label"), #标签
g.LabelWrapped("Auto wrapped label with very long line...............................................this line should be wrapped."), #这个标签会自动折行
g.Line( #一行
g.InputText("##name", 0, &name), #输入框
g.Button("Click Me", btnClickMeClicked), #按钮
g.Tooltip("I'm a tooltip"), #按钮提示信息
),
g.Line( #另一行
g.Checkbox("Checkbox", &checked, func() { #定义了两个多选框
fmt.Println(checked)
}),
g.Checkbox("Checkbox 2", &checked2, func() {
fmt.Println(checked2)
}),
g.Dummy(30, 0), #与前面控件间隔
g.RadioButton("Radio 1", radioOp == 0, func() { radioOp = 0 }), #三个单选框
g.RadioButton("Radio 2", radioOp == 1, func() { radioOp = 1 }),
g.RadioButton("Radio 3", radioOp == 2, func() { radioOp = 2 }),
),
g.ProgressBar(0.8, -1, 0, "Progress"), #一个进度条控件
g.DragInt("DragInt", &dragInt), #一个拖动整数控件,&dragInt为取值变量
g.SliderInt("Slider", &dragInt, 0, 100, ""), #一个水平滚动条取整数控件
g.Combo("Combo", items[itemSelected], items, &itemSelected, 0, 0, comboChanged), #下拉选择控件
g.Line( #新起一行内容
g.Button("Popup Modal", btnPopupCLicked), #按钮,唤起一个弹出窗
g.PopupModal("Confirm", g.Layout{
g.Label("Confirm to close me?"),
g.Line(
g.Button("Yes", func() { imgui.CloseCurrentPopup() }),
g.Button("No", nil),
),
}),
g.Label("Right click me to see the context menu"), #右键菜单
g.ContextMenu(g.Layout{
g.Selectable("Context menu 1", contextMenu1Clicked),
g.Selectable("Context menu 2", contextMenu2Clicked),
}),
),
g.TabBar("Tabbar Input", g.Layout{ #多标签框
g.TabItem("Multiline Input", g.Layout{
g.Label("This is first tab with a multiline input text field"),
g.InputTextMultiline("##multiline", &multiline, -1, -1, 0, nil, nil), #多行输入框
}),
g.TabItem("Tree", g.Layout{ #另一个标签框
g.TreeNode("TreeNode1", imgui.TreeNodeFlagsCollapsingHeader|imgui.TreeNodeFlagsDefaultOpen, g.Layout{ #树形选择控件
g.Label("Tree node 1"),
g.Label("Tree node 1"),
g.Label("Tree node 1"),
g.Button("Button inside tree", nil),
}),
g.TreeNode("TreeNode2", 0, g.Layout{
g.Label("Tree node 2"),
g.Label("Tree node 2"),
g.Label("Tree node 2"),
g.Button("Button inside tree", nil),
}),
}),
g.TabItem("ListBox", g.Layout{
g.ListBox("ListBox1", 0, 0, []string{"List item 1", "List item 2", "List item 3"}, nil, nil), #列表框
}),
g.TabItem("Table", g.Layout{
g.Table("Table", true, g.Rows{ #表格
g.Row(g.LabelWrapped("Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog"), g.Label("Age"), g.Label("Loc")),
g.Row(g.LabelWrapped("Second Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog"), g.Label("Age"), g.Label("Loc")),
g.Row(g.Label("Name"), g.Label("Age"), g.Label("Location")),
g.Row(g.Label("Allen"), g.Label("33"), g.Label("Shanghai/China")),
g.Row(g.Checkbox("check me", &checked, nil), g.Button("click me", nil), g.Label("Anything")),
}),
}),
g.TabItem("Group", g.Layout{
g.Line(
g.Group(g.Layout{
g.Label("I'm inside group 1"),
}),
g.Group(g.Layout{
g.Label("I'm inside group 2"),
}),
),
}),
}),
})
}
func main() {
items = make([]string, 100) #生成下拉选框数据
for i := range items {
items[i] = fmt.Sprintf("Item %d", i)
}
w := g.NewMasterWindow("Overview", 800, 600, 0, nil)
w.Main(loop)
}
还提供定制控件,示例customwidget。看起来不复杂。
还有一些扩展控件,示例extrawidgets
旋转的loading
package main
import (
g "github.com/AllenDang/giu"
)
var (
showPD bool = true
radius float32 = 20
)
func loop() {
g.SingleWindow("Extra Widgets", g.Layout{
g.Checkbox("Show ProgressIndicator", &showPD, nil),
g.Condition(showPD, g.Layout{
g.SliderFloat("Radius", &radius, 10, 100, ""),
g.Line(
g.ProgressIndicator("pd1", "", 20+radius, 20+radius, radius),
g.ProgressIndicator("pd2", "", 20+radius, 20+radius, radius),
),
g.ProgressIndicator("pd3", "Loading...", 0, 0, radius),
}, nil),
})
}
func main() {
wnd := g.NewMasterWindow("Extra Widgets", 800, 600, g.MasterWindowFlagsNotResizable, nil)
wnd.Main(loop)
}
注意部件的ID需要是唯一的。
事件处理
在控件中获取鼠标和键盘事件
giu.Button("Click Me", nil)
// Place the event handling APIs right after the button to capture key/mouse events.
giu.Custom(func() {
if giu.IsItemHovered() {
// Do event handling here.
}
}
在示例widgets中,菜单并没有任何响应,可以类似的将以上代码添加在某项菜单之后,即可以对相关事件进行响应。例如下面的代码片段:
g.MenuBar(
g.Layout{
g.Menu("File", g.Layout{
g.MenuItem("Open"),
g.MenuItem("Save"),
g.Custom(func() {
if g.IsItemHovered() {
fmt.Println("Save...")
}
}),
// You could add any kind of widget here, not just menu item.
g.Menu("Save as ...", g.Layout{
g.MenuItem("Excel file"),
g.MenuItem("CSV file"),
g.Button("Button inside menu", nil),
g.Custom(func() {
if g.IsItemHovered() {
fmt.Println("!!!")
}
}),
},
),
},
),
},
),
库中定义了以下事件:IsItemHovered、IsItemActive、IsKeyDown、IsKeyPressed、IsKeyReleased、IsMouseDown、IsMouseClicked、IsMouseReleased、IsMouseDoubleClicked、IsWindowAppearing
要注意的是,点击等事件可能并不只是针对某一控件。
g.MenuBar(
g.Layout{
g.Menu("File", g.Layout{
g.MenuItem("Open"),
g.MenuItem("Save"),
// You could add any kind of widget here, not just menu item.
g.Menu("Save as ...", g.Layout{
g.MenuItem("Excel file"),
g.MenuItem("CSV file"),
g.Button("Button inside menu", nil),
},
),
},
),
g.Custom(func() {
if g.IsMouseClicked(0) {
fmt.Println("Save...", time.Now())
}
}),
g.Custom(func() {
if g.IsItemHovered() {
fmt.Println("!!!", time.Now())
}
}),
},
),
响应了File的鼠标移动事件,响应所有菜单项的点击事件,包括Button按钮的点击事件。这在使用中还需要判断是点击了哪一个菜单。
多线程处理
提供giu.Call, giu.CallErr, giu.CallVar来调用另一协程中GUI相关代码。
在另一协程中更新界面,需要调用gui.Update()。
package main
import (
"fmt"
"math/rand"
"time"
"github.com/AllenDang/giu"
)
var (
counter int
)
func refresh() {
ticker := time.NewTicker(time.Second * 1)
for {
counter = rand.Intn(100)
giu.Update()
<-ticker.C
}
}
func loop() {
giu.SingleWindow("Update", giu.Layout{
giu.Label("Below number is updated by a goroutine"),
giu.Label(fmt.Sprintf("%d", counter)),
})
}
func main() {
wnd := giu.NewMasterWindow("Update", 400, 200, giu.MasterWindowFlagsNotResizable, nil)
go refresh()
wnd.Main(loop)
}
对话框
信息框
giu.SingleWindow("Msgbox demo", giu.Layout{
// You layout.
...
giu.PrepareMsgbox(),
})
定制对话框
使用giu.PopupModal 构建,使用giu.OpenPopup和giu.CloseCurrentPopup控制。
操作系统相关对话框
比如文件打开对话框,保存对话框等。示例跳到另一个地方去了https://github.com/sqweek/dialog
绘图
func loop() {
g.SingleWindow("canvas", g.Layout{
g.Label("Canvas demo"),
g.Custom(func() {
canvas := g.GetCanvas()
pos := g.GetCursorScreenPos()
color := color.RGBA{200, 75, 75, 255}
canvas.AddLine(pos, pos.Add(image.Pt(100, 100)), color, 1)
canvas.AddRect(pos.Add(image.Pt(110, 0)), pos.Add(image.Pt(200, 100)), color, 5, g.CornerFlags_All, 1)
canvas.AddRectFilled(pos.Add(image.Pt(220, 0)), pos.Add(image.Pt(320, 100)), color, 0, 0)
pos0 := pos.Add(image.Pt(0, 110))
cp0 := pos.Add(image.Pt(80, 110))
cp1 := pos.Add(image.Pt(50, 210))
pos1 := pos.Add(image.Pt(120, 210))
canvas.AddBezierCurve(pos0, cp0, cp1, pos1, color, 1, 0)
p1 := pos.Add(image.Pt(160, 110))
p2 := pos.Add(image.Pt(120, 210))
p3 := pos.Add(image.Pt(210, 210))
p4 := pos.Add(image.Pt(210, 150))
// canvas.AddTriangle(p1, p2, p3, color, 2)
canvas.AddQuad(p1, p2, p3, p4, color, 1)
p1 = p1.Add(image.Pt(120, 60))
canvas.AddCircleFilled(p1, 50, color)
p1 = pos.Add(image.Pt(10, 400))
p2 = pos.Add(image.Pt(50, 440))
p3 = pos.Add(image.Pt(200, 500))
canvas.PathLineTo(p1)
canvas.PathLineTo(p2)
canvas.PathBezierCurveTo(p2.Add(image.Pt(40, 0)), p3.Add(image.Pt(-50, 0)), p3, 0)
canvas.PathStroke(color, false, 1)
}),
})
}
字体相关
终于讲到关心的一个问题了。
package main
import (
g "github.com/AllenDang/giu"
"github.com/AllenDang/giu/imgui"
)
func loadFont() {
fonts := g.Context.IO().Fonts()
ranges := imgui.NewGlyphRanges()
builder := imgui.NewFontGlyphRangesBuilder()
builder.AddText("铁憨憨你好!") #只加载需要的汉字
// builder.AddRanges(fonts.GlyphRangesChineseFull()) #加载所有的汉字
builder.BuildRanges(ranges)
fontPath := "./yahei.ttf"
fonts.AddFontFromFileTTFV(fontPath, 12, imgui.DefaultFontConfig, ranges.Data())
}
func loop() {
g.SingleWindow("dynamic load font", g.Layout{
g.Label("你好啊世界!铁憨憨"),
})
}
func main() {
wnd := g.NewMasterWindow("Dynamic load font", 400, 200, g.MasterWindowFlagsNotResizable, loadFont)
wnd.Main(loop)
}
要使用多种字体
var (
font1 imgui.Font
font2 imgui.Font
)
func initFont() {
fonts := giu.Context.IO().Fonts()
font1 = fonts.AddFontFromFileTTFV(
"/System/Library/Fonts/STHeiti Light.ttc", <- Font file path
14, <- Font size
imgui.DefaultFontConfig, <- Usually you don't need to modify this
fonts.GlyphRangesChineseFull()) <- Add Chinese glyph ranges to display chinese characters
font2 = fonts.AddFontFromFileTTFV(
"/System/Library/Fonts/STHeiti Light.ttc",
24,
imgui.DefaultFontConfig,
fonts.GlyphRangesDefault()) <- Add a big font for ASCII
}
func loop() {
...
giu.PushFont(font2)
giu.Label("I'm a label with big font")
giu.PopFont()
...
}
风格相关
官方文档就没有写了。看看示例吧
setstyle中讲了标签的颜色设置,这与风格也没啥关系样。
giu.SingleWindow("set style", giu.Layout{
giu.LabelV("I'm a styled label", false, &color.RGBA{0x36, 0x74, 0xD5, 255}, nil),
giu.Label("I'm a normal label"),
})
余下的示例
还有几个示例没有在官方讲,明天继续。大体上看,还是足够使用了,不知道兼容性如何。需要在Win中在看看效果。