自定义小部件,Fyne笔记第7篇
目录
这是我的Fyne学习笔记系列第7篇。
Fyne默认的按钮卡片等小部件让开发简单的App非常容易,但实际开发者,有复杂的需求,Fyne提供了自定义小部件的接口。
什么是小部件
在Fyne中,Widget(小部件)是带状态的 canvas object(任何可以添加到画布上的图形对象)。
定义为
type Widget interface {
CanvasObject
CreateRenderer() WidgetRenderer
}
type WidgetRenderer interface {
Destroy()
Layout(Size)
MinSize() Size
Objects() []CanvasObject
Refresh()
}
CanvasObject是接口,定义了MinSize() Size,Move(Position),Position() Position,Resize(Size),Size() Size,Hide(),Visible() bool,Show(),Refresh()方法,作用是进行设置左上角坐标,设置宽高,获取最小化时的宽高,设置是否可见等。
CreateRenderer可以创建一个渲染器,来对Widget的渲染进行操控。
简单的说,这里的编程模型是,Widget负责状态和行为,比如一个按钮是否启用,被点击时发生什么,渲染器负责布局和显示,比如这个按钮是什么颜色,显示文字是否居中。
widget.BaseWidget是基础的Widget,widget.DisableableWidget增加了禁用状态,嵌入到自己实现的Widget可以复用很多Fyne自带Widget的基本功能。
一个示例说明如何自定义Widget
// 一个自定义Widget的示例
package main
import (
"fmt"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
var _ fyne.Widget = (*ExampleWidget)(nil)
// ExampleWidget 实现将空间四等分的 Widget
// 可以实现四象限图,或同样结构的画面。
// 如果处于禁用状态,只显示分隔线
//
// Widget保存状态和实现行为,布局和显示在渲染器实现。
type ExampleWidget struct {
//对应左上,右上,左下,右下,允许为nil
A1, A2, A3, A4 fyne.CanvasObject
//复用Widget的基本功能以及设置禁用
widget.DisableableWidget
}
func NewExampleWidget(a1, a2, a3, a4 fyne.CanvasObject) *ExampleWidget {
r := &ExampleWidget{A1: a1, A2: a2, A3: a3, A4: a4}
//如果没有这个步骤,窗口Resize时因为找不到这个Widget对应的渲染器,从而不会Resize这个Widget。
r.ExtendBaseWidget(r)
return r
}
// MinSize 返回最小尺寸,即显示所有子组件需要的最低宽高
func (m *ExampleWidget) MinSize() fyne.Size {
get := func(c fyne.CanvasObject) fyne.Size {
if c == nil {
return fyne.NewSize(0, 0)
}
return c.MinSize()
}
// 计算左右两列的最大宽度,上下两行的最大高度
leftWidth := max(get(m.A1).Width, get(m.A3).Width)
rightWidth := max(get(m.A2).Width, get(m.A4).Width)
topHeight := max(get(m.A1).Height, get(m.A2).Height)
bottomHeight := max(get(m.A3).Height, get(m.A4).Height)
return fyne.NewSize(leftWidth+rightWidth, topHeight+bottomHeight)
}
// CreateRenderer 创建并返回 ExampleWidget 小部件的渲染器
func (m *ExampleWidget) CreateRenderer() fyne.WidgetRenderer {
return NewExampleWidgetRender(m)
}
type ExampleWidgetRender struct {
x, y *canvas.Line // 垂直分隔线(x 轴), 水平分隔线(y 轴)
size *widget.Label // 调试用:显示当前小部件的宽高
m *ExampleWidget // 关联的小部件实例
}
func NewExampleWidgetRender(m *ExampleWidget) *ExampleWidgetRender {
r := &ExampleWidgetRender{m: m}
r.y, r.x = canvas.NewLine(color.Black), canvas.NewLine(color.Black)
r.size = widget.NewLabel("")
return r
}
const debug = false
// Destroy 渲染器被销毁时调用
func (r *ExampleWidgetRender) Destroy() {}
// Layout 根据传入的宽高,调整分隔线位置和所有子组件的位置、宽高
//
// s是小部件的可用显示宽高
func (r *ExampleWidgetRender) Layout(s fyne.Size) {
//设置x线两点在X轴中间,Y轴0和max
//设置y线两点在Y轴中间,X轴0和max
//两点直线相连形成分隔线
r.y.Position1.X = s.Width / 2
r.y.Position2.X = s.Width / 2
r.y.Position2.Y = s.Height
r.x.Position1.Y = s.Height / 2
r.x.Position2.Y = s.Height / 2
r.x.Position2.X = s.Width
//将空间均分为4等份,移动CanvasObject到所属位置
//通过Move设置位置左上角的XY点坐标,Resize设置宽高
if debug {
r.size.SetText(fmt.Sprintf("宽 %g, 高%g", s.Width, s.Height))
}
avg := fyne.NewSize(s.Width/2, s.Height/2)
if r.m.A1 != nil {
h := float32(0)
if debug {
h = r.size.MinSize().Height
}
r.m.A1.Move(fyne.NewPos(0, h)) // 移动到左上位置
r.m.A1.Resize(avg)
}
if r.m.A2 != nil {
r.m.A2.Move(fyne.NewPos(s.Width/2, 0)) // 移动到右上位置
r.m.A2.Resize(avg)
}
if r.m.A3 != nil {
r.m.A3.Move(fyne.NewPos(0, s.Height/2)) // 移动到左下位置
r.m.A3.Resize(avg)
}
if r.m.A4 != nil {
r.m.A4.Move(fyne.NewPos(s.Width/2, s.Height/2)) // 移动到右下位置
r.m.A4.Resize(avg)
}
}
// MinSize 返回在最小化时所需要的最低宽高。
func (r *ExampleWidgetRender) MinSize() fyne.Size { return r.m.MinSize() }
// Objects 返回所有需要显示的CanvasObject
func (r *ExampleWidgetRender) Objects() []fyne.CanvasObject {
ret := []fyne.CanvasObject{r.x, r.y, r.size, r.m.A1, r.m.A2, r.m.A3, r.m.A4}
if r.m.Disabled() { //如果处于禁用状态,只显示分隔线
ret = ret[:2]
}
return ret
}
// Refresh 刷新渲染器(重新绘制)
func (r *ExampleWidgetRender) Refresh() {
//对于这个示例,通常在调用ExampleWidget.Disable或Enable或Resize后
//Fyne自动调用这个方法。
//随后渲染下一帧时,调用Objects实现显示的改变。
r.Layout(r.m.Size())
}
func main() {
app := app.New()
w := app.NewWindow("自定义小部件")
content := []fyne.CanvasObject{widget.NewLabel("第二象限"), widget.NewLabel("第一象限"), widget.NewLabel("第三象限"), widget.NewLabel("第四象限")}
body := NewExampleWidget(content[0], content[1], content[2], content[3])
w.SetContent(container.NewBorder(nil, nil, nil, nil, body))
w.ShowAndRun()
}
上述功能用自定义布局也能实现,从某种角度说,自定义Widget和渲染器,是特定自定义布局更好的代码复用做法。
注:自定义布局更侧重 “通用布局规则”,而自定义 Widget 更侧重 “带状态的同类小部件”。