(原) Golang Gui: giu

原创文章,请后转载,并注明出处。

虽然这不是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中在看看效果。

相关文章