2024年3月

fyne项目介绍

fyne 是一个纯 Golang 的跨平台 GUI 库,跨平台库说实话,是有很多选择的,Flutter、Electron、QT等。fyne 绝对不是一个很大众的选择。但是在我,一名后端程序员尝试使用 Electron实现一个简单的番茄时钟,痛苦地在使用 js 如何在渲染进程和主进程之间传递信息,如何在客户端退到后台的时候继续进行倒计时,vue 和哪个 electron 的库版本兼容的问题中进行挣扎的时候,我真的很希望能有一个纯 Go 实现的 GUI 库能开发 mac 的 app,而且想到这并不是不能做到的,都是使用底层的系统接口渲染,没有那个语言更高贵对吧。于是,就找到了它,这就是 fyne。

再进一步了解 fyne 的时候,感觉进入了另外一个世界。fyne,它已经不仅仅是一个开源项目,它正在逐渐形成一个自己的生态。

fyne 的 github 开源地址是
https://github.com/fyne-io/fyne
,它的官网地址是
https://fyne.io/
。 fyne 有一大堆的开源簇拥者,开源簇拥者会上传他们自己开发的 app 到
https://apps.fyne.io/,簇拥者也为
fyne 开发一系列的扩展
https://github.com/fyne-io/fyne-x
,并且 fyne 官方会定期将一些好的扩展并入标准库。不仅如此,fyne 在 2019 年开始举办 FyneConf 大会
https://conf.fyne.io/
,到 2023 年已经5 届了。

分析源码,fyne 是一个近 9w 行代码的项目,属于一个中等偏上的项目量了,有 1w 多行注释,注释量也足够多了。

------------------------------------------------------------------------------------------------------------
File                                                                     blank        comment           code
------------------------------------------------------------------------------------------------------------
SUM:                                                                     17568          10438          89688
------------------------------------------------------------------------------------------------------------

好的项目和好的产品一样,内部复杂,但是外部暴露简单,这 9w 行代码我们不一定都能理解,但是只要能理解下 fyne 宇宙中最核心的几个结构及它们的方法,就能很快使用它,就能成为一名合格的使用者了。

fyne 核心数据结构

App

App
在 fyne 库中fyne.App, 它是一个 interface,定义了一些图形化应用程序的基本结构和功能。App是整个应用的最原始的存在。对,就是宇宙万物中的一生二,二生四,四生万物中的一,有了它,你就能拥有全世界。

我们使用下列两种库方法可以从 fyne 库直接创建 fyne.App

func NewWithID(id string) fyne.App

func New() fyne.App

New 的底层实现我们暂且不论,这里的 id 参数是什么?它是代表当前 app 的一个唯一字符串,我们一般填写能唯一指定当前 app 的名字,诸如 “com.hade.toolbox” 的字符串。如果不填写,就会使用时间戳来生成一个 fake 的 id。这个 id 在 app 内部叫做 UniqueID,在创建缓存,临时文件存储等目录的时候,会使用这个 id 来创建子目录。这里的 id 必须是全局唯一的。

我们再看下App定义的接口有如下,这里直接将注释给出,

NewWindow(title string) Window: 创建一个新的窗口,并将其作为应用程序的一部分。第一个打开的窗口被视为"主窗口",当关闭时应用程序将退出。
OpenURL(url *url.URL) error: 在默认浏览器中打开指定的 URL。
Icon() Resource: 获取应用程序的图标,用于各种操作系统特定的显示。这也是新窗口的默认图标。
SetIcon(Resource): 设置应用程序使用的图标资源。
Run(): 启动应用程序的事件循环,并一直阻塞直到调用 Quit() 或最后一个窗口关闭。
Quit(): 退出应用程序,关闭所有打开的窗口。在移动设备上不执行任何操作,因为应用程序生命周期由操作系统管理。
Driver() Driver: 返回渲染该应用程序的驱动程序。通常不需要在日常工作中使用,主要用于内部功能。
UniqueID() string: 返回应用程序的唯一标识符(如果已设置)。这必须在使用 Preferences() 函数时设置。
SendNotification(*Notification): 发送一个系统通知,将在操作系统的通知区域显示。
Settings() Settings: 返回全局设置,确定主题等。
Preferences() Preferences: 返回应用程序首选项,用于存储配置和状态。
Storage() Storage: 返回特定于此应用程序的存储处理程序。
Lifecycle() Lifecycle: 返回一个类型,允许应用程序钩入生命周期事件。
Metadata() AppMetadata: 返回在编译时设置的应用程序元数据。
CloudProvider() CloudProvider: 返回当前应用程序的云提供商(如果已由开发人员注册或用户选择)。
SetCloudProvider(CloudProvider): 允许开发人员指定应用程序应如何与云服务集成。

这个接口引出了 App 的几个下级结构:

  • Window
  • Resource
  • Driver
  • Settings
  • Preferences
  • Storage
  • Lifecycle
  • AppMetadata
  • CloudProvider

这几个下级结构就是玩转 fyne 最需要了解的。

App -> Window

我们的应用 App 一般都会有好几个窗口组成,当点击某个按钮的时候,弹出一个窗口,这里的窗口,在 fyne 库中就叫做 Window 数据结构。

当然这些若干窗口的地位也不尽相同,第一个创建出来的窗口称为主窗口,其他的窗口称为子窗口。当主窗口关闭的时候,整个 app 也就关闭了。

fyne.Window 其实也是个接口,这个
Window
接口提供了创建、管理和控制应用程序窗口的基本功能,包括设置标题、全屏模式、大小调整、聚焦、内边距、图标、菜单、生命周期回调、拖放支持以及与画布和剪贴板的交互。

下面是对相关接口的详细说明:

Title() string: 返回当前窗口的标题。这通常显示在窗口装饰中。
SetTitle(string): 更新窗口的标题。
FullScreen() bool: 返回窗口是否处于全屏模式。
SetFullScreen(bool): 设置窗口是否应处于全屏模式。
Resize(Size): 根据指定的内容大小调整窗口大小。由于各种桌面或平台约束,实际大小可能与请求的不完全一致。
RequestFocus(): 尝试提升并聚焦该窗口。这只应在确定用户希望此窗口从任何当前聚焦的窗口窃取焦点时调用。
FixedSize() bool: 返回窗口是否应禁用调整大小。
SetFixedSize(bool): 设置是否应将窗口大小固定。
CenterOnScreen(): 将窗口放置在当前窗口所在的显示器中心。
Padded() bool: 返回窗口是否应有内边距,以使组件不会触及窗口边缘。
SetPadded(bool): 设置窗口是否应有内边距。用于全屏或基于图形的应用程序可能很有用。
Icon() Resource: 返回窗口图标,根据操作系统的不同会以各种方式使用,通常显示在窗口边框或任务切换器上。
SetIcon(Resource): 设置此窗口使用的图标资源。如果未设置,则应返回应用程序图标。
SetMaster(): 指示关闭此窗口应退出应用程序。
MainMenu() *MainMenu: 获取窗口的顶级菜单。
SetMainMenu(*MainMenu): 为此窗口添加顶级菜单。渲染方式取决于加载的驱动程序。
SetOnClosed(func()): 设置在窗口关闭时运行的函数。
SetCloseIntercept(func()): 设置一个函数,在关闭窗口时运行,而不是关闭窗口。在拦截器中应显式调用 Close() 来关闭窗口。
SetOnDropped(func(Position, []URI)): 设置一个窗口范围的回调函数,用于接收拖放的项目。
Show(): 显示窗口。
Hide(): 隐藏窗口,但不会销毁窗口或导致应用程序退出。
Close(): 关闭窗口。如果它是"主窗口",应用程序将退出。
ShowAndRun(): 显示窗口并运行应用程序。这应该在 main() 函数的末尾调用,因为它会阻塞。
Content() CanvasObject: 返回此窗口的内容。
SetContent(CanvasObject): 设置窗口的内容。
Canvas() Canvas: 返回用于在窗口中呈现的画布上下文。这可能有助于为窗口设置键处理程序。
Clipboard() Clipboard: 返回系统剪贴板。

大部分接口都很容易理解,其中我们关注到 Window 的几个下级数据结构:

  • MainMenu
  • CanvasObject
  • Canvas
  • Clipboard

同样的,我们需要钻入到这几个下级数据结构了解。

Window -> MainMenu

MainMenu 层级往下,有 MainMenu,Menu,MenuItem 三个层级结构。

MainMenu
结构定义了激活 Windows 窗口后,菜单栏(桌面)显示的内容。

Menu
结构定义了应用程序中标准菜单的信息。它可以用作主菜单(从
MainMenu
下拉)或弹出式菜单。

MenuItem
结构定义了菜单中的单个项目。

简要来说,MainMenu 就是整个菜单栏,Menu 是下拉的一列,MenuItem 是一列中的单行。

  1. MainMenu
    包含一个
    *Menu
    类型的切片
    Items
    ,代表顶级菜单。
  2. Menu
    包含一个
    *MenuItem
    类型的切片
    Items
    ,代表菜单项。
  3. MenuItem
    包含以下字段:
    • ChildMenu
      : 如果此菜单项有子菜单,则为该子菜单的指针。
    • IsQuit
      : 如果设置为
      true
      ,则表示此菜单项用于退出应用程序。
    • IsSeparator
      : 如果设置为
      true
      ,则表示此菜单项应用作分隔符。
    • Label
      : 菜单项的显示标签。
    • Action
      : 当菜单项被点击时要执行的函数。
    • Disabled
      : 如果设置为
      true
      ,则表示此菜单项应该被禁用。
    • Checked
      : 如果设置为
      true
      ,则表示此菜单项应该被显示为已选中状态。
    • Icon
      : 菜单项的图标资源(如果有)。
    • Shortcut
      : 与此菜单项关联的快捷键(如果有)。

这三者的关系我们用如下类图来表示:

classDiagram
class MainMenu {
+[]*Menu Items
+Refresh()
}
class Menu {
+string Label
+[]*MenuItem Items
+Refresh()
}
class MenuItem {
+*Menu ChildMenu
+bool IsQuit
+bool IsSeparator
+string Label
+func() Action
+bool Disabled
+bool Checked
+Resource Icon
+Shortcut Shortcut
}
MainMenu "1" *-- "*" Menu : contains
Menu "1" *-- "*" MenuItem : contains

以下是这三个数据结构每个接口的说明,略微枯燥,可具体使用再细看。

MainMenu
包含以下内容:

  1. Items
    : 一个
    *Menu
    类型的切片,代表顶级菜单项。每个
    Menu
    代表一个下拉菜单。

MainMenu
提供了以下方法:

  1. NewMainMenu(items ...*Menu) *MainMenu
    : 创建一个新的
    MainMenu
    实例,并传入顶级菜单项。
  2. Refresh()
    : 通知任何使用此结构的渲染菜单更新它们的显示。这对于在应用程序运行时动态更新菜单很有用。

这个结构是 Fyne 库中用于管理应用程序主菜单的核心部分。它允许开发人员定义应用程序的顶级菜单结构,并在运行时动态更新菜单。这对于创建功能丰富的桌面应用程序非常有用。

Menu
结构定义了应用程序中标准菜单的信息。它可以用作主菜单(从
MainMenu
下拉)或弹出式菜单。

Menu
结构包含以下字段:

  1. Label
    : 菜单的标签,在主菜单中显示。
  2. Items
    : 一个
    *MenuItem
    类型的切片,代表菜单中的项目。

Menu
提供了以下方法:

  1. NewMenu(label string, items ...*MenuItem) *Menu
    : 创建一个新的
    Menu
    实例,并传入标签和菜单项。
  2. Refresh()
    : 通知任何使用此菜单的窗口或系统托盘更新其显示。这对于在应用程序运行时动态更新菜单很有用。

Menu
结构是 Fyne 库中用于管理应用程序菜单的核心部分。它允许开发人员定义应用程序中的各种菜单,包括主菜单和弹出式菜单。这些菜单可以包含各种菜单项,如普通菜单项、分隔线、子菜单等。
Refresh()
方法使开发人员能够在应用程序运行时动态更新菜单,从而提高用户体验。

MenuItem
结构定义了菜单中的单个项目。它包含以下字段:

  1. ChildMenu
    : 如果此菜单项有子菜单,则为该子菜单的指针。
  2. IsQuit
    : 如果设置为
    true
    ,则表示此菜单项用于退出应用程序。
  3. IsSeparator
    : 如果设置为
    true
    ,则表示此菜单项应用作分隔符。
  4. Label
    : 菜单项的显示标签。
  5. Action
    : 当菜单项被点击时要执行的函数。
  6. Disabled
    : 如果设置为
    true
    ,则表示此菜单项应该被禁用。
  7. Checked
    : 如果设置为
    true
    ,则表示此菜单项应该被显示为已选中状态。
  8. Icon
    : 菜单项的图标资源(如果有)。
  9. Shortcut
    : 与此菜单项关联的快捷键(如果有)。

MenuItem
提供了以下方法:

  1. NewMenuItem(label string, action func()) *MenuItem
    : 创建一个新的
    MenuItem
    实例,并传入标签和点击时执行的操作。
  2. NewMenuItemSeparator() *MenuItem
    : 创建一个新的菜单分隔线项目。

MenuItem
结构是 Fyne 库中用于定义应用程序菜单项的核心部分。它提供了丰富的功能,如子菜单、快捷键、图标和选中状态等,使开发人员能够创建功能强大的应用程序菜单。这些菜单项可以被添加到
Menu
结构中,以构建应用程序的菜单层次结构。

其中 MenuItem 中的 Action 函数是我们最经常使用到的,点击某个按钮的具体行为。

Window -> CanvasObject

CanvasObject
是 Fyne 库中定义的一个接口,用于描述可以添加到画布上的任何图形对象。是的,任何图形对象,画布上的任何元素都是由一个 CanvasObject,或者由多个 CanvasObject 组成的。所有画布上的元素都实现了 CanvasObject 接口。所以CanvasObject 定义的方法,是可以作用在任何元素上的。

CanvasObject
接口定义了以下方法:

  1. Geometry
    :
    • MinSize() Size
      : 返回该对象需要被绘制的最小尺寸。
    • Move(Position)
      : 将该对象移动到相对于其父对象的给定位置。这只应在对象不在使用布局管理器的容器中时调用。
    • Position() Position
      : 返回该对象相对于其父对象的当前位置。
    • Resize(Size)
      : 将该对象调整到给定大小。这只应在对象不在使用布局管理器的容器中时调用。
    • Size() Size
      : 返回该对象的当前大小。
  2. Visibility
    :
    • Hide()
      : 隐藏该对象。
    • Visible() bool
      : 返回该对象是否可见。
    • Show()
      : 显示该对象。
  3. Refresh
    :
    • Refresh()
      : 如果该对象的内部状态发生变化而需要重新绘制,则必须调用此方法。

以上是 CanvasObject 接口的定义,而对于一些更复杂的额外的对象,fyne 又定义了另外一些基础接口,这些接口和 CanvasObject 结合起来,就能代表更为复杂的对象元素。

即按照 Golang 的接口鸭子模型,如果我们有具体实现像多个接口,那么这个实现就有这两个接口的能力。比如一个实现了 CanvasObject 和 Scrollable 两个接口的元素,那么它在画布上就是可以被滚动的元素。fyne 真是把接口玩到飞起。

  • Disableable
    : 描述可以被禁用的
    CanvasObject
  • DoubleTappable
    : 描述可以被双击的
    CanvasObject
  • Draggable
    : 描述可以被拖动的
    CanvasObject
  • Focusable
    : 描述可以响应焦点的
    CanvasObject
  • Scrollable
    : 描述可以被滚动的
    CanvasObject
  • SecondaryTappable
    : 描述可以响应二次点击(右击或长按)的
    CanvasObject
  • Shortcutable
    : 描述可以响应快捷键命令(退出、剪切、复制和粘贴)的
    CanvasObject
  • Tabbable
    : 描述需要接受 Tab 键按下事件的
    CanvasObject
  • Tappable
    : 描述可以响应点击事件的
    CanvasObject

理解了接口,我们再聊到 CanvasObject 的具体实现,这些实现才是我们具体代码中会用到的各个元素,使用好这些元素,你的 app 就能有各种各样的展示效果。

  • Container 容器
  • Widget 系列
    • Accordion
    • Button
    • Card
    • Check
    • Entry
    • FileIcon
    • Form
    • Hyperlink
    • Icon
    • Label
    • Progress bar
    • RadioGroup
    • Select
    • SelectEntry
    • Separator
    • Slider
    • TextGrid
    • Toolbar
    • List
    • Table
    • Tree
    • AppTabs
    • Scroll
    • Split
  • Dialog 系列
    • Color
    • Confirm
    • FileOpen
    • Form
    • Information
    • Custom

这些元素对应的样式,可以在官网的 doc 中找到:
https://docs.fyne.io/,用的时候再挑。

Window -> Canvas

Canvas
接口定义了一个图形画布,可以在其上添加
CanvasObject

Container
。每个画布都有一个缩放比例,在渲染过程中会自动应用。

为什么有了 Windows 还有一个 Canvas 呢?

Canvas 理解为画布,更像是 Windows 的宿主,我们可以有一个画布放在 Windows 中,也可以有一个画布放在 Image 图片中,在这个图片上进行绘制。

func TestPainter_paintImage(t *testing.T) {
	img := canvas.NewImageFromImage(makeTestImage(3, 3))

	c := test.NewCanvas()
	c.SetPadded(false)
	c.SetContent(img)
	c.Resize(fyne.NewSize(50, 50))
	p := software.NewPainter()

	target := p.Paint(c)
	test.AssertImageMatches(t, "draw_image_default.png", target)
}

Canvas
接口提供了管理画布内容、焦点、事件、缩放和覆盖层的功能,以及截屏和坐标转换等实用工具。它是 Fyne 库中构建图形用户界面的核心组件之一。

Canvas
接口定义了以下功能:

  1. 内容管理
    :
    • Content() CanvasObject
      : 返回当前设置在画布上的顶层
      CanvasObject
    • SetContent(CanvasObject)
      : 设置画布的顶层
      CanvasObject
    • Refresh(CanvasObject)
      : 通知画布重新绘制指定的
      CanvasObject
  2. 焦点管理
    :
    • Focus(Focusable)
      : 设置指定的
      Focusable
      对象为焦点。
    • FocusNext()
      : 聚焦下一个可聚焦的对象。
    • FocusPrevious()
      : 聚焦上一个可聚焦的对象。
    • Unfocus()
      : 取消当前焦点。
    • Focused() Focusable
      : 返回当前获得焦点的对象。
  3. 尺寸和缩放
    :
    • Size() Size
      : 返回画布的当前大小。
    • Scale() float32
      : 返回画布当前使用的缩放比例。
  4. 覆盖层管理
    :
    • Overlays() OverlayStack
      : 返回画布的覆盖层堆栈。
  5. 事件处理
    :
    • OnTypedRune() func(rune)
      ,
      SetOnTypedRune(func(rune))
      : 设置和获取键入字符事件的处理函数。
    • OnTypedKey() func(*KeyEvent)
      ,
      SetOnTypedKey(func(*KeyEvent))
      : 设置和获取键盘事件的处理函数。
    • AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut))
      : 添加一个快捷键及其处理函数。
    • RemoveShortcut(shortcut Shortcut)
      : 移除一个快捷键。
  6. 截屏
    :
    • Capture() image.Image
      : 捕获画布的当前内容并返回一个图像。
  7. 坐标转换
    :
    • PixelCoordinateForPosition(Position) (int, int)
      : 返回给定位置在画布上的像素坐标。
    • InteractiveArea() (Position, Size)
      : 返回中央交互区域的位置和大小。

Window -> Clipboard

Clipboard
接口定义了系统剪贴板的接口。它提供了以下两个方法:

  1. Content() string
    : 返回当前剪贴板的内容。
  2. SetContent(content string)
    : 设置剪贴板的内容。

这个接口为应用程序提供了与系统剪贴板进行交互的方法。通过这个接口,开发人员可以:

  1. 从剪贴板读取文本内容,例如在粘贴操作中使用。
  2. 将文本内容写入到剪贴板,例如在复制操作中使用。

Clipboard
接口是 Fyne 库中用于与系统剪贴板进行交互的核心组件。它为应用程序提供了一种标准的方式来访问和管理剪贴板内容,从而增强应用程序的复制粘贴功能。

App -> Resource

Resource
接口定义了一个单一的二进制资源,比如图像或字体。fyne 倾向于所有外部的资源,比如图片, 字体等都编译在代码中,被定义一个唯一的标识名称,拥有对应的字节数组内容,在使用的时候,就不用使用各种路径去加载,而是直接到内存中加载。

fyne 还提供了一个 bundle 命令,我们可以用 fyne 的工具对任何 png,freetype 进行打包成为一个变量,我们称这个变量为 StaticResource。

StaticResource
是 Resource 接口的具体实现,他们提供了一种标准的方式来管理应用程序中包含的二进制资源。开发人员可以使用这些接口从文件系统或 URL 加载资源,并在应用程序中使用这些资源,而无需关心底层的文件系统或网络操作。这有助于提高应用程序的可维护性和跨平台性。

这或许也是我喜欢 fyne 的原因之一,和 golang 的语言如出一辙,所有东西都是静态,避免任何的动态加载。不要再去拼凑各种动态文件路径,加载等逻辑,真是轻松的一匹。

照例对照源码列一下接口和结构的定义。

Resource 接口提供了以下方法:

  1. Name() string
    : 返回资源的唯一名称,通常与生成它的文件名匹配。
  2. Content() []byte
    : 返回资源的字节内容,不进行任何压缩,但保留资源本身的任何压缩。

StaticResource
结构包含以下字段:

  1. StaticName string
    : 资源的名称。
  2. StaticContent []byte
    : 资源的字节内容。

StaticResource
提供了以下方法:

  1. Name() string
    : 返回资源的名称。
  2. Content() []byte
    : 返回资源的字节内容。

App -> Driver

Driver
接口定义了 Fyne 渲染驱动程序的抽象概念。任何实现这个接口的驱动程序都必须提供以下方法:

  1. 窗口管理
    :
    • CreateWindow(string) Window
      : 创建一个新的 UI 窗口。
    • AllWindows() []Window
      : 返回包含所有应用程序窗口的切片。
  2. 文本渲染
    :
    • RenderedTextSize(text string, fontSize float32, style TextStyle) (size Size, baseline float32)
      : 返回渲染给定文本所需的大小和基线高度。
  3. 画布关联
    :
    • CanvasForObject(CanvasObject) Canvas
      : 返回与给定
      CanvasObject
      关联的画布。
    • AbsolutePositionForObject(CanvasObject) Position
      : 返回给定
      CanvasObject
      相对于画布左上角的绝对位置。
  4. 设备管理
    :
    • Device() Device
      : 返回应用程序当前运行的设备。
  5. 事件循环
    :
    • Run()
      : 启动驱动程序的主事件循环。
    • Quit()
      : 关闭驱动程序和打开的窗口,然后退出应用程序。
  6. 动画管理
    :
    • StartAnimation(*Animation)
      : 注册一个新的动画并请求启动它。
    • StopAnimation(*Animation)
      : 停止一个动画并从驱动程序中注销。

Driver
接口是 Fyne 库中非常重要的组件,它定义了渲染引擎的核心功能。通过实现这个接口,开发人员可以为特定的操作系统或运行环境提供定制的渲染驱动程序,从而使 Fyne 应用程序能够在不同平台上运行。

在 fyne 中,默认的驱动是 OpenGL(gLDriver)。fyne 使用的 openGL 的驱动也是使用开源库:
github.com/go-gl/glfw/v3.3/glfw
。这个开源库是 cgo 实现的,调用操作系统底层的GLFW (OpenGL Friendly Windowing)。而GLFW (OpenGL Friendly Windowing) 是一个开源的、跨平台的、轻量级的 C 库,用于创建和管理 OpenGL、OpenGL ES 和 Vulkan 图形上下文以及窗口。它主要用于游戏和其他视觉应用程序的开发。

所以这也就是 fyne 能一次开发,mac,android,window 都可运行的底气所在。

App -> Settings

Settings
接口提供了一种标准的方式来配置 Fyne 应用程序的外观和行为。开发人员可以通过实现该接口来定义应用程序的默认设置,并允许用户通过 UI 界面或其他方式进行更改。

这些设置对于确保应用程序在不同平台和用户偏好下都能提供一致的体验非常重要。例如,主题和缩放设置可以确保应用程序在不同屏幕尺寸和分辨率下保持良好的视觉效果。

Settings
接口是 Fyne 应用程序配置管理的核心,确保应用程序能够适应不同的用户需求和环境。

Settings
接口它包含以下功能:

  1. 主题管理
    :
    • Theme() Theme
      : 获取当前使用的主题。
    • SetTheme(Theme)
      : 设置使用的主题。
    • ThemeVariant() ThemeVariant
      : 获取主题的首选变体(如浅色或深色)。
  2. 缩放设置
    :
    • Scale() float32
      : 获取应用程序的缩放比例。
  3. 主色设置
    :
    • PrimaryColor() string
      : 获取用户首选的主色。
  4. 变更监听
    :
    • AddChangeListener(chan Settings)
      : 添加一个监听器,当设置发生变化时会被通知。
  5. 构建类型
    :
    • BuildType() BuildType
      : 获取应用程序的构建类型(标准、调试或发布)。
  6. 动画设置
    :
    • ShowAnimations() bool
      : 获取是否应该显示动画的设置。

App -> Preferences

Preferences
接口定义了一组用于保存和加载应用程序用户首选项的方法。首选项就是用户进入这个 app 后的一些设置值。这些首选项值在一个终端是会进行持久化存储的,并且在 App 关闭又重新打开的时候,是还会持续生效。它也支持了多种的数据类型。

它提供了以下功能:

  1. 基本数据类型读写
    :
    • Bool(key string) bool
      : 读取指定键的布尔值。
    • BoolWithFallback(key string, fallback bool) bool
      : 读取指定键的布尔值,如果不存在则返回回退值。
    • SetBool(key string, value bool)
      : 保存指定键的布尔值。
    • 类似的方法还有
      Float

      Int

      String
  2. 列表数据类型读写
    :
    • BoolList(key string) []bool
      : 读取指定键的布尔值列表。
    • BoolListWithFallback(key string, fallback []bool) []bool
      : 读取指定键的布尔值列表,如果不存在则返回回退列表。
    • SetBoolList(key string, value []bool)
      : 保存指定键的布尔值列表。
    • 类似的方法还有
      FloatList

      IntList

      StringList
  3. 删除值
    :
    • RemoveValue(key string)
      : 删除指定键的值。
  4. 变更监听
    :
    • AddChangeListener(func())
      : 添加一个回调函数,当值发生变化时会被调用。
    • ChangeListeners() []func()
      : 获取所有已注册的变更监听器。

App -> Storage

Storage
接口定义了 Fyne 应用程序内部文件存储的相关功能。

Storage
接口提供了一个抽象层,用于管理应用程序自身的文件存储。它确保每个 Fyne 应用程序都有一个独立的沙箱存储区域,用于存储应用程序相关的文件,而不会与其他应用程序的文件产生冲突。

这个接口的实现会根据不同的操作系统平台采用不同的存储机制,比如在 Windows 上可能使用应用程序数据目录,在 Android 上可能使用内部存储空间等。但无论采用何种存储方式,开发者都可以使用统一的
Storage
接口进行文件操作。

通过使用
Storage
接口,Fyne 应用程序可以保证在不同平台上都能以一致的方式访问和管理应用程序文件,提高了跨平台的可移植性。同时,这也确保了应用程序的文件存储受到沙箱机制的保护,提高了安全性。

它提供了以下主要功能:

  1. 根 URI 访问
    :
    • RootURI() URI
      : 获取应用程序的根文件存储 URI。这个 URI 代表了应用程序独有的文件存储沙箱。
  2. 文件创建
    :
    • Create(name string) (URIWriteCloser, error)
      : 创建一个新的文件,并返回一个可写入的
      URIWriteCloser
      对象。
  3. 文件打开
    :
    • Open(name string) (URIReadCloser, error)
      : 打开一个现有的文件,并返回一个可读取的
      URIReadCloser
      对象。
  4. 文件保存
    :
    • Save(name string) (URIWriteCloser, error)
      : 打开或创建一个文件并返回一个可写入的
      URIWriteCloser
      对象。
  5. 文件删除
    :
    • Remove(name string) error
      : 删除指定名称的文件。
  6. 列出文件
    :
    • List() []string
      : 返回当前存储中所有文件的名称列表。

总之,
Storage
接口是 Fyne 框架中用于管理应用程序内部文件存储的核心组件。它抽象了底层的存储细节,为上层应用程序提供了一致的文件操作 API。

App -> Lifecycle

通过 Lifecycle 接口可以在不同阶段为应用程序设置行为。

  1. 前后台切换钩子
    :
    • SetOnEnteredForeground(func())
      : 用于设置一个回调函数,在应用程序从后台切换到前台并获得焦点时会被调用。
    • SetOnExitedForeground(func())
      : 用于设置一个回调函数,在应用程序失去输入焦点并切换到后台时会被调用。
  2. 启动/停止钩子
    :
    • SetOnStarted(func())
      : 用于设置一个回调函数,在应用程序启动并开始运行时会被调用。
    • SetOnStopped(func())
      : 用于设置一个回调函数,在应用程序停止运行时会被调用。

通过这些钩子函数,开发者可以在应用程序的生命周期关键时刻执行相应的逻辑,比如:

  • 当应用程序进入前台时,可以恢复动画、重新加载数据等。
  • 当应用程序进入后台时,可以暂停耗电的操作、保存当前状态等。
  • 当应用程序启动时,可以进行一些初始化操作。
  • 当应用程序停止时,可以执行清理工作、保存用户数据等。

App -> AppMetadata

AppMetadata
是 Fyne 框架中用于描述应用程序元信息的数据结构。它包含了以下几个重要的属性:

  1. 应用程序 ID
    :
    • ID string
      : 应用程序的唯一标识符,通常用于各种应用程序分发平台。
  2. 应用程序名称
    :
    • Name string
      : 应用程序的人类可读名称。
  3. 应用程序版本
    :
    • Version string
      : 应用程序的版本号,通常遵循语义化版本控制规范。
    • Build int
      : 应用程序的构建编号,有时会附加在版本号后面。
  4. 应用程序图标
    :
    • Icon Resource
      : 如果存在的话,包含了应用程序在构建时打包的图标资源。
  5. 发布模式
    :
    • Release bool
      : 表示该二进制文件是否是在发布模式下构建的。
  6. 自定义元数据
    :
    • Custom map[string]string
      : 包含了开发者在
      FyneApp.toml
      文件中或编译命令行上定义的自定义元数据。

AppMetadata
结构体的作用是为 Fyne 应用程序提供一种标准化的方式来描述自身的元信息。这些信息通常会被用于以下场景:

  1. 应用程序分发
    :
    应用程序 ID、名称、版本号等信息通常会被应用程序分发平台使用,例如 App Store、Google Play 等。
  2. 应用程序徽标
    :
    应用程序图标信息会被用于在操作系统界面和应用商店中显示应用程序的图标。
  3. 应用程序配置
    :
    自定义的元数据可以被应用程序使用,用于存储一些额外的配置信息。
  4. 应用程序更新
    :
    版本号和构建编号信息可以帮助用户跟踪应用程序的更新情况。

总之,
AppMetadata
是 Fyne 框架中用于描述应用程序元信息的核心数据结构,它为开发者提供了一种标准化的方式来定义应用程序的各种属性,从而更好地支持应用程序的分发、展示和配置等需求。

App -> CloudProvider

CloudProvider
是 Fyne 框架中用于定义和管理云服务提供商的接口。它主要提供了以下功能:

  1. 提供者信息获取
    :
    • ProviderDescription() string
      : 获取云服务提供商的详细描述信息。
    • ProviderIcon() Resource
      : 获取与该云服务关联的图标资源。
    • ProviderName() string
      : 获取云服务提供商的名称。
  2. 生命周期管理
    :
    • Cleanup(App)
      : 当云服务提供商不再被使用时,会被调用来执行清理工作。
    • Setup(App) error
      : 当云服务提供商第一次被使用时,会被调用进行初始化设置。如果初始化失败,可以返回一个错误,以退出云服务设置流程。

通过
CloudProvider
接口,Fyne 应用程序可以与不同的云服务提供商进行集成,并提供统一的用户体验。具体的集成方式包括:

  1. 用户偏好同步
    :
    • CloudProviderPreferences
      接口定义了同步用户偏好设置到云端的功能。
  2. 文档同步
    :
    • CloudProviderStorage
      接口定义了同步应用程序文档到云端的功能。

当应用程序开发者选择使用云服务提供商时,只需要实现
CloudProvider
接口以及可选的
CloudProviderPreferences

CloudProviderStorage
接口,就可以为应用程序提供云同步功能。

Fyne 框架会负责管理不同云服务提供商的生命周期,并提供统一的 API 供应用程序使用。这使得应用程序开发者可以专注于业务逻辑的实现,而不需要过多地关注云服务的集成细节。

总之,
CloudProvider
是 Fyne 框架中用于集成和管理云服务提供商的核心组件,它为应用程序提供了跨云服务的统一抽象,简化了云服务集成的开发工作。

fyne 总结

fyne 是一个对于后端 Golang 开发者极其友好的 GUI 库,对于后端开发人员,功能开发并不是难事,而前端页面绘制可能会难倒大部分开发者,fyne 提供了一种使用代码就能很好绘制界面的方法,这对于这个人群来说,是一个福音。

这里主要列了 fyne 的核心数据结构,当然每个结构里面的方法和具体实现都需要花时间琢磨。我自己用 fyne 开发了一个番茄始终,当你自己用 go 绘制了界面,用 go 开启协程进行倒计时,用go 在 mac 的 tab 栏增加倒计时显示的时候,你才会感叹到 fyne 的强大。

昨天编码的时候想到了关于无边框窗体的闪烁问题,主要是改变窗体大小的时候会闪烁,默认的窗体没这个问题。而现在无边框窗体的应用比较多,所以就找了度娘,然后结合自己的经验进行了测试,得到了这个例子,简单有效。

1、
项目目录;

2、
源码介绍;

3、
运行界面;

因为没其它界面效果,这里就不提供界面截图了。

4、
使用介绍;

根据窗体上面的提示进行使用即可。

5、
源码下载;

提供源码下载:
https://download.csdn.net/download/lzhdim/88975656

6、
其它建议;

无边框窗体的使用目前也是很流行的,所以无边框窗体的效果笔者在其它的例子里有介绍了,具体请查阅笔者相关的系列博文:
https://www.cnblogs.com/lzhdim/category/705764.html

上面介绍了C#的无边框窗体的防闪烁的代码例子,需要的读者请自己复用该例子代码。

Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍如何运用
QUdpSocket
组件实现基于UDP的组播通信。

组播是一种一对多的通信方式,允许一个发送者将数据报文发送到多个接收者,这些接收者通过共享相同的组播IP地址进行通信。在设置组播地址时需要注意,该范围被限制在
239.0.0.0~239.255.255.255
以内,这是预留给组播的地址范围。

setSocketOption 设置套接字

在Qt中使用组播,首先需要调用
setSocketOption
函数,该函数是
QUdpSocket
类的成员函数,用于设置套接字的选项。

该函数原型如下:

bool QUdpSocket::setSocketOption(
    QAbstractSocket::SocketOption option, 
    const QVariant & value
)
  • option
    :要设置的套接字选项,这里应该是
    QAbstractSocket::MulticastTtlOption
    ,表示设置多播 TTL 选项。
  • value
    :选项的值,这里应该是 TTL 的值。在 IPv4 中,TTL 是一个 8 位的字段,表示数据报在网络中允许经过的最大路由器数量。通常情况下,TTL 值越大,数据报能够传播的范围就越广。

函数返回一个
bool
类型的值,表示是否成功设置了选项。如果设置成功,返回
true
,否则返回
false

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    udpSocket=new QUdpSocket(this);

    // 设置为多播
    udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
}

bind 绑定套接字地址

接着就是对特定端口的绑定,绑定端口可以通过调用
bind
函数,该函数用于将
QUdpSocket
绑定到指定的本地地址和端口,并设置特定的绑定选项。

在我们的课件中,使用
bind()

QUdpSocket
绑定到
IPv4
的任意地址,并指定了一个组播(
Multicast
)端口,同时设置了共享地址(
ShareAddress
)选项。

该函数原型如下:

void QUdpSocket::bind(
    const QHostAddress & address, 
    quint16 port, 
    BindMode mode = DefaultForPlatform
)
  • address
    :要绑定的本地地址,这里使用
    QHostAddress::AnyIPv4
    表示绑定到
    IPv4
    的任意地址。
  • port
    :要绑定的本地端口号,这里应该是组播端口号。
  • mode
    :绑定模式,指定套接字的行为。这里使用
    QUdpSocket::ShareAddress
    表示共享地址选项,它允许多个套接字同时绑定到相同的地址和端口。

函数将
QUdpSocket
绑定到指定的地址和端口,并且允许多个套接字同时共享相同的地址和端口。

joinMulticastGroup 加入组播

joinMulticastGroup()
函数是
QUdpSocket
类的成员函数,用于将
QUdpSocket
加入指定的多播组。

该函数原型如下:

bool QUdpSocket::joinMulticastGroup(
    const QHostAddress & groupAddress, 
    const QNetworkInterface & iface = QNetworkInterface()
)
  • groupAddress
    :要加入的多播组的组播地址。
  • iface
    :要加入多播组的网络接口。默认情况下,会选择默认的网络接口。

函数返回一个
bool
类型的值,表示是否成功加入了多播组。如果成功加入多播组,返回
true
;否则返回
false
。通过调用
joinMulticastGroup()
函数,
QUdpSocket
将成为指定多播组的成员,并能够接收该多播组发送的数据报。

// 开始组播
void MainWindow::on_pushButton_start_clicked()
{
    // 获取IP
    QString IP= ui->lineEdit_address->text();
    groupAddress=QHostAddress(IP);

    // 获取端口
    quint16 groupPort = ui->lineEdit_port->text().toUInt();

    // 绑定端口
    if (udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress))
    {
        // 加入组播
        udpSocket->joinMulticastGroup(groupAddress);
        ui->plainTextEdit->appendPlainText("[*] 加入组播 " + IP + ":" + QString::number(groupPort));
    }
}

leaveMulticastGroup 退出组播

leaveMulticastGroup()
函数用于将
QUdpSocket
从指定的多播组中移除。通过调用该函数,
QUdpSocket
将不再是指定多播组的成员,不再接收该多播组发送的数据报。

该函数原型如下:

bool QUdpSocket::leaveMulticastGroup(
    const QHostAddress & groupAddress, 
    const QNetworkInterface & iface = QNetworkInterface()
)
  • groupAddress
    :要离开的多播组的组播地址。
  • iface
    :要离开多播组的网络接口。默认情况下,会选择默认的网络接口。

函数返回一个
bool
类型的值,表示是否成功离开了多播组。如果成功离开多播组,返回
true
;否则返回
false

// 关闭组播
void MainWindow::on_pushButton_stop_clicked()
{
    // 退出组播
    udpSocket->leaveMulticastGroup(groupAddress);
    udpSocket->abort();
    ui->plainTextEdit->appendPlainText("[-] 退出组播");
}

writeDatagram 发送数据报

writeDatagram()
函数是
QUdpSocket
类的成员函数,用于发送数据报到指定的多播组。通过调用该函数,可以将数据报发送到指定的多播组和端口,让其他成员接收到该数据报。

其函数原型如下:

qint64 QUdpSocket::writeDatagram(
    const QByteArray & datagram, 
    const QHostAddress & groupAddress, 
    quint16 port
)
  • datagram
    :要发送的数据报的内容,通常是一个
    QByteArray
    对象。
  • groupAddress
    :要发送到的多播组的组播地址。
  • port
    :要发送到的多播组的端口号。

函数返回一个
qint64
类型的值,表示实际发送的字节数。如果发送成功,返回发送的字节数;否则返回 -1。

// 发送组播消息
void MainWindow::on_pushButton_send_clicked()
{
    quint16 groupPort = ui->lineEdit_port->text().toUInt();
    QString msg=ui->lineEdit_msg->text();
    QByteArray datagram=msg.toUtf8();

    udpSocket->writeDatagram(datagram,groupAddress,groupPort);
}

readDatagram 接收数据报

readDatagram()
函数是
QUdpSocket
类的成员函数,用于从套接字中读取数据报,并将其存储到指定的缓冲区中。通常情况下,可以使用这个函数来接收来自其他主机的数据报。通过使用该函数可从套接字中读取数据报,并获取数据报的源地址和端口号。

其函数原型如下:

qint64 QUdpSocket::readDatagram(
    char * data, qint64 maxSize, 
    QHostAddress * address = nullptr, 
    quint16 * port = nullptr
)
  • data
    :指向用于存储接收数据的缓冲区的指针。
  • maxSize
    :缓冲区的最大大小,即最多可以接收的字节数。
  • address
    :指向用于存储发送数据报的源地址的
    QHostAddress
    对象的指针。
  • port
    :指向用于存储发送数据报的源端口号的
    quint16
    类型的指针。

该函数返回一个
qint64
类型的值,表示实际接收的字节数。如果接收成功,返回接收的字节数;否则返回 -1。

// 读取数据报
void MainWindow::onSocketReadyRead()
{
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        QHostAddress peerAddr;
        quint16 peerPort;
        udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);

        QString str=datagram.data();

        QString peer="[从 "+peerAddr.toString()+":"+QString::number(peerPort)+" 发送] ";

        ui->plainTextEdit->appendPlainText(peer+str);
    }
}

读者可自行运行课件程序,并在多台电脑中配置相同网段,当点击发送消息时所有同网段的程序都将收到广播,如下图所示;

本文介绍基于
Python

ArcPy
模块,读取
Excel
表格数据并生成带有
属性表

矢量要素图层
,同时配置该图层的
坐标系
的方法。

1 任务需求

首先,我们来明确一下本文所需实现的需求。

现有一个记录
北京市部分PM2.5浓度监测站点
信息的
Excel
表格数据,格式为
.xls
;文件内包含站点编号、
X

Y
坐标、站点名称等四列数据,部分数据如下所示。

image

我们需要将该表格文件中所记录的全部站点信息导入到
Python
中,并将全部站点创建为一个点要素的
矢量图层
;此外,需要同时可以指定该矢量图层的
投影坐标系
,并将表格文件中的四列信息作为矢量图层
属性表的字段与内容

2 代码实现

接下来,我们就基于
Python

ArcPy
模块,进行详细代码的撰写与介绍。

首先,需要说明的是:当初在编写代码的时候,为了方便执行,所以希望代码后期可以在
ArcMap
中直接通过工具箱运行,即用到
Python程序脚本新建工具箱与自定义工具
的方法;因此,代码中对于一些需要初始定义的变量,都用到了
arcpy.GetParameterAsText()
函数。大家如果只是希望在
IDLE
中运行代码,那么直接对这些变量进行具体赋值即可。关于
Python程序脚本新建工具箱与自定义工具
,大家可以查看
ArcMap将Python写的代码转为工具箱与自定义工具
详细了解。

上面提到需要初始定义的变量一共有四个,其中
arcpy.env.workspace
参数表示当前工作空间,
excel_path
参数表示存储有北京市
PM2.5
浓度监测站点信息的
Excel
数据文件,
spatial_reference_txt
参数表示需要对站点矢量数据进行投影的坐标系类型(在本文中我们以“
WGS 1984 UTM Zone 50N
”投影为例),
shapefile_name
参数表示投影后站点矢量数据的具体文件。

# -*- coding: cp936 -*-
# @author: ChuTianjia

import xlrd
import arcpy

arcpy.env.workspace=arcpy.GetParameterAsText(0)
excel_path=arcpy.GetParameterAsText(1) # 站点信息表格文件
shapefile_name=arcpy.GetParameterAsText(3) # 需要生成的矢量要素的路径与名称

file_data=xlrd.open_workbook(excel_path)
sheet_data=file_data.sheets()[0]
sheet_row_num=sheet_data.nrows

point_geometry_list=[]
point_object=arcpy.Point()

# Read Spatial Coordinate Information
spatial_reference_txt=arcpy.GetParameterAsText(2) # 指定投影坐标系
spatial_reference=arcpy.SpatialReference()
spatial_reference.loadFromString(spatial_reference_txt)

# Import the Coordinates of Each Point
for i in range(1,sheet_row_num):
    x=sheet_data.row(i)[1].value
    y=sheet_data.row(i)[2].value
    point_object.X=float(x)
    point_object.Y=float(y)
    point_geometry=arcpy.PointGeometry(point_object,spatial_reference)
    point_geometry_list.append(point_geometry)

arcpy.CopyFeatures_management(point_geometry_list,shapefile_name)

# Import the Filed Information
field_list=["X","Y","ID_Own","Name"]
arcpy.AddField_management(shapefile_name,field_list[0],"FLOAT")
arcpy.AddField_management(shapefile_name,field_list[1],"FLOAT")
arcpy.AddField_management(shapefile_name,field_list[2],"SHORT")
arcpy.AddField_management(shapefile_name,field_list[3],"TEXT")

with arcpy.da.UpdateCursor(shapefile_name,field_list) as cursor:
    n=1
    for row in cursor:
        row[0]=sheet_data.row(n)[1].value
        row[1]=sheet_data.row(n)[2].value
        row[2]=sheet_data.row(n)[0].value
        row[3]=sheet_data.row(n)[3].value
        cursor.updateRow(row)
        n+=1

3 运行结果

执行上述代码,即可得到包含有表格文件中所列全部站点的点要素矢量图层文件,且其属性表中包含了原有表格文件中全部列所对应的字段与内容。

查看该图层属性,可以看到其已经具有了我们在代码中所指定的投影坐标系。

至此,大功告成。

前言

上一篇文章我们讲到了
Scrutor
第一个核心功能
Scanning
,本文讲解的是
Scrutor
第二个核心的功能
Decoration
装饰器模式在依赖注入中的使用。

  • 装饰器模式允许您向现有服务类中添加新功能,而无需改变其结构
Install-Package Scrutor

本文的完整源代码在文末

Decoration 依赖注入代理模式

首先首先一个 获取 User 的服务

定义 User 类

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }

    public int Age { get; set; }

    public string? Email { get; set; }
}

定义接口和实现

public interface IUserService
{
    List<User> GetAllUsers();
}

public class UserService : IUserService
{
    public List<User> GetAllUsers()
    {
        Console.WriteLine("GetAllUsers方法被调用~");
        List<User> users = [
        new User(){
            Id= 1,
            Name="张三",
            Age=18,
            Email="zhangsan@163.com"
        },
        new User(){
            Id= 2,
            Name="李四",
            Age=19,
            Email="lisi@163.com"
        },
        ];
        return users!;
    }
}

现在有了我们的获取全部用户的服务了,需求是在不破坏当前类的添加装饰器模式,为
GetAllUsers
接口添加缓存。

创建装饰器类

public class UserDecorationService(IUserService userService, IMemoryCache cache) : IUserService
{
    public List<User> GetAllUsers()
    {
        Console.WriteLine("GetAllUsers代理方法被调用~");
        return cache.GetOrCreate("allUser", cacheEntry =>
           {
               cacheEntry.SetAbsoluteExpiration(
                    TimeSpan.FromMinutes(5));
               var allUsers = userService.GetAllUsers();
               return allUsers ?? [];
           })!;
    }
}

DI 容器添加服务

 builder.Services.AddTransient<IUserService, UserService>();
 builder.Services.AddMemoryCache();
 builder.Services.Decorate<IUserService, UserDecorationService>();

创建接口测试一下

app.MapGet("/GetAllUsers", ([FromServices] IUserService userService) => userService.GetAllUsers()).WithSummary("获取全部用户接口");

调用第一次

GetAllUsers代理方法被调用~
GetAllUsers方法被调用~

第二次调用

GetAllUsers代理方法被调用~

可以看出第一次没缓存装饰器类和我们 UserService 都调用了,第二次因为只有了缓存所以只调用了装饰器类,可以看出我们的装饰器模式生效了。

依赖注入装饰器底层核心实现

    /// <summary>
    /// Decorates all registered services using the specified <paramref name="strategy"/>.
    /// </summary>
    /// <param name="services">The services to add to.</param>
    /// <param name="strategy">The strategy for decorating services.</param>
    public static bool TryDecorate(this IServiceCollection services, DecorationStrategy strategy)
    {
        Preconditions.NotNull(services, nameof(services));
        Preconditions.NotNull(strategy, nameof(strategy));

        var decorated = false;

        for (var i = services.Count - 1; i >= 0; i--)
        {
            var serviceDescriptor = services[i];

            if (IsDecorated(serviceDescriptor) || !strategy.CanDecorate(serviceDescriptor))
            {
                continue;
            }

            var serviceKey = GetDecoratorKey(serviceDescriptor);
            if (serviceKey is null)
            {
                return false;
            }

            // Insert decorated
            services.Add(serviceDescriptor.WithServiceKey(serviceKey));

            // Replace decorator
            services[i] = serviceDescriptor.WithImplementationFactory(strategy.CreateDecorator(serviceDescriptor.ServiceType, serviceKey));

            decorated = true;
        }


        return decorated;
    }

这个代码是在
dotNet8
的环境下编译的,可以看出做了几件事:
第一
IServiceCollection
集合倒序遍历,找到符合条件的
ServiceType
核心代码一

// Insert decorated
services.Add(serviceDescriptor.WithServiceKey(serviceKey));

将原先的
ServiceDescription
作为基础,添加了
ServiceKey
后再进行添加操作,新的服务描述符会被添加到服务集合的末尾,

核心代码二

 // Replace decorator
 services[i] = serviceDescriptor.WithImplementationFactory(strategy.CreateDecorator(serviceDescriptor.ServiceType, serviceKey));

这一步是将原有的服务描述符替换为一个新的服务描述符,新的服务描述符使用装饰器工厂方法创建,实现了服务的装饰功能。

用的时候

app.MapGet("/GetAllUsers", ([FromServices] IUserService userService) => userService.GetAllUsers()).WithSummary("获取全部用户接口");


这样就可以获取到装饰器类提供服务,之前看到
services.Add(serviceDescriptor.WithServiceKey(serviceKey));
在代码的最后添加了一个服务,那 IOC 获取的时候肯定是从后面优先获取,这地方用了
dotNet8
的键控依赖注入(
KeyedService
),以
ServiceType
获取服务只会获取到我们提供的装饰器实例,这一手简直是神来之笔