秋来冬风的博客

自定义小部件,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 更侧重 “带状态的同类小部件”。

Tags:
Categories: