在Fyne An中创建一个TODO应用程序
#go #todo #fyne #desktopgui

在这个小教程中,我将解释一些通常是练习,使您可以理解如何使用UI框架(至少在网络世界中),我们将逐步逐步构建todo应用程序Go使用Fyne UI框架。

我会假设您至少有一些编程知识,但是如果您对 go 有所了解,那也很棒,我个人发现它是最简单的编程语言,您可以只需在其learning资源中找到有关如何做到这一点的更多信息。

如果您遵循here中的 fyne 的安装指令。

我们要建造什么

真的很简单,只是一个小应用程序,它可以让您保留要做的事情的清单,如果您曾经完成它们,可以检查一下。

应该像这样:
Todo Example

设置项目结构

go过去对过去如何存储在Gopath文件夹,libs,供应商,您自己的个人项目中的所有事物都有糟糕的过去,但是自V1.14以来,随着GO模块的引入,一切都变得更加有意义,并且更容易。理解。

我们想启动我们的项目,因此创建一个新文件夹,然后启动模块

$ mkdir todoapp && cd todoapp
$ go mod init todoapp
$ touch main.go

有些人建议您使用GitHub repo的URL调用该模块,您将使用版本控制它,但是如果您不打算在其他地方使用它,而是在此二进制文件中,这并不重要。

在您喜欢的代码编辑器中打开此文件夹,然后将其放在main.go

package main

import (
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/widget"
)

func main() {
    a := app.New()
    w := a.NewWindow("TODO App")

    w.SetContent(widget.NewLabel("TODOs will go here"))
    w.ShowAndRun()
}

现在在运行之前,让我们参考Fyne和整理GO模块

$ go get fyne.io/fyne/v2@latest
$ go mod tidy

完成之后,让我们尝试运行

$ go run .

您应该在桌面中看到一个看起来像这样的小应用程序窗口:
Small Hello world

没什么可看的,但是如果您到达这一点,那么您几乎都是设定的,从现在开始,这一切都很棒。

您现在可以做2件事。

  1. 为您的应用创建模型以显示
  2. 设计UI

我倾向于创建(通常使用TDD)该应用程序将首先显示的模型,因此我知道我需要在视图中绑定什么,尤其是在这种情况下,我们只有1个模型(Todo),我想我们将是所有这些都很快就开始了。

所有模型

我将制作一个名为models的文件夹,然后放入2个文件

$ ls models   

todo.go
todo_test.go

我不会太多参加测试,否则我们将永远进行,所以我只会展示todo.go声明

package models

import "fmt"

type Todo struct {
    Description string
    Done        bool
}

func NewTodo(description string) Todo {
    return Todo{description, false}
}

默认情况下,构造函数将将Done属性设置为false
我还添加了一个func String() string函数来实现Stringer接口。
因此,如果我想

func (t Todo) String() string {
    return fmt.Sprintf("%s  - %t", t.Description, t.Done)
}

现在让我们尝试在我们的Fyne应用中显示一个,但首先,让我们使该窗口更大

func main() {
    a := app.New()
    w := a.NewWindow("TODO App")

        // ADDING THIS HERE
    w.Resize(fyne.NewSize(300, 400))

现在看起来应该更像这个
Bigger app

太好了,现在让我们创建一个待办事项并在同一窗口中显示。

    w.Resize(fyne.NewSize(300, 400))

    t := models.NewTodo("Show this on the window")

    w.SetContent(widget.NewLabel(t.String()))

这就是我们得到的
Stringified todo

辉煌。

现在是时候创建实际的接口了。

UX设计

Fyne,就像许多其他桌面UI框架一样,可以包含该布局,以定义窗口中可用空间周围的小部件和项目。

有负载,它们都在here和“容器/布局”部分中展示。

让我们尝试一下,让我们将其推入中心。

// make sure you import the right one
import "fyne.io/fyne/v2/container"
// this ↑

        w.SetContent(
        container.NewCenter(
            widget.NewLabel(t.String()),
        ),
    )
    w.ShowAndRun()

在API示例中,您可以看到此表格也可以实现

    w.SetContent(
        container.New(
            layout.NewCenterLayout(),
            widget.NewLabel(t.String()),
        ),
    )
    w.ShowAndRun()

我个人不喜欢的,第一个是第二种的句法糖。

无论如何,该应用现在看起来像这样:
Centered

很好!但是,我们希望侧面有一个文本条目和一个按钮,以输入并添加todo到列表中吗?好吧,让我们组合东西并使用边框布局。

    w.SetContent(
        container.NewBorder(
            nil, // TOP of the container

            // this will be a the BOTTOM of the container
            widget.NewButton("Add", func() { fmt.Println("Add was clicked!") }),

            nil, // Right
            nil, // Left

            // the rest will take all the rest of the space
            container.NewCenter(
                widget.NewLabel(t.String()),
            ),
        ),
    )
    w.ShowAndRun()

这将在底部添加一个按钮,然后单击它将在控制台标准输出“添加W时添加W!”

在控制台标准上打印一个按钮。

这就是它的样子:
Image description

现在,让我们在按钮侧面添加条目,在另一个容器类型的Hbox中,这将水平堆叠

    w.SetContent(
        container.NewBorder(
            nil, // TOP of the container

            container.NewHBox(
                widget.NewEntry(),
                widget.NewButton("Add", func() { fmt.Println("Add was clicked!") }),
            ),

            nil, // Right
            nil, // Left

            // the rest will take all the rest of the space
            container.NewCenter(
                widget.NewLabel(t.String()),
            ),
        ),
    )
    w.ShowAndRun()

HBox
但不幸的是,它看起来不正确,我们希望他们占用所有可用空间。

我们可以再尝试一些:

            container.NewGridWithColumns(
                2,
                widget.NewEntry(),
                widget.NewButton("Add", func() { fmt.Println("Add was clicked!") }),
            ),

看起来像这样
grid


            container.NewBorder(
                nil, // TOP
                nil, // BOTTOM
                nil, // Left
                // RIGHT ↓
                widget.NewButton("Add", func() { fmt.Println("Add was clicked!") }),
                // take the rest of the space
                widget.NewEntry(),
            ),

在边界底部筑巢另一个边界:
Nested borders

这并不重要,这是个人喜好的问题,但我会选择最后一个。

清理和创作

我们现在已经设计了UI的外观,但是让我们尝试清理代码,因为容器树已经很混乱。

我将在.SetContent呼叫之前移动按钮和文本输入创建:

    newtodoDescTxt := widget.NewEntry()
    newtodoDescTxt.PlaceHolder = "New Todo Description..."
    addBtn := widget.NewButton("Add", func() { fmt.Println("Add was clicked!") })

    w.SetContent(
        container.NewBorder(
            nil, // TOP of the container

            container.NewBorder(
                nil, // TOP
                nil, // BOTTOM
                nil, // Left
                // RIGHT ↓
                addBtn,
                // take the rest of the space
                newtodoDescTxt,
            ),

以前声明这些小部件将允许我们在将其添加到实际内容树之前修改属性并将其添加到它们。

在此示例中,我在输入文本中添加了一个占位符,因此更清楚的是:
desc placeholder

现在,让我们制作"Add"按钮做某事,如果文本为空或太短,甚至可以将其禁用。

    newtodoDescTxt := widget.NewEntry()
    newtodoDescTxt.PlaceHolder = "New Todo Description..."
    addBtn := widget.NewButton("Add", func() { fmt.Println("Add was clicked!") })
    addBtn.Disable()

    newtodoDescTxt.OnChanged = func(s string) {
        addBtn.Disable()

        if len(s) >= 3 {
            addBtn.Enable()
        }
    }

如果文本长度小于3个字符,则将禁用该按钮:

disabled btn

并启用它,否则
enabled

太好了!现在,让我们尝试构建我们缺少的UX的最后一部分,即Todos的列表。

Fyne的列表

有两种列表窗口小部件可以用于此目的,第一个是静态列表,第二个是链接到将显示的数据链接到的。

Simple List具有简单的API。

            widget.NewList(
                // func that returns the number of items in the list
                func() int {
                    return len(data)
                },
                // func that returns the component structure of the List Item
                func() fyne.CanvasObject {
                    return widget.NewLabel("template")
                },
                // func that is called for each item in the list and allows
                // you to show the content on the previously defined ui structure
                func(i widget.ListItemID, o fyne.CanvasObject) {
                    o.(*widget.Label).SetText(data[i])
                }),

在我们的特定示例中,我们希望每个列表项目都有一个用于TODO说明的标签和一个复选框,以说明该待办事项是否已标记为已完成。

只是尝试一下,让我们创建这个data作为todos片。

    data := []models.Todo{
        models.NewTodo("Some stuff"),
        models.NewTodo("Some more stuff"),
        models.NewTodo("Some other things"),
    }

// then on the last func of list we just replace `data[i]` with

                func(i widget.ListItemID, o fyne.CanvasObject) {
                    o.(*widget.Label).SetText(data[i].Description)
                }),

看起来像这样:
Simple List

您可以看到,我们正在获取o CanvasObject并将其施放到*widget.Label上,这是因为我们知道,尽管我们需要LabelCheckbox,但我以前说过的是创建该特定小部件的功能,他们需要要进入一个容器,因此我们可以使用底部的栏和空间进行操作,以便标签占用大部分空间。

                func() fyne.CanvasObject {
                    return container.NewBorder(
                        nil, nil, nil,
                        // left of the border
                        widget.NewCheck("", func(b bool) {}),
                        // takes the rest of the space
                        widget.NewLabel(""),
                    )
                },

类似的东西。

,但不幸的是,这会抛出编译时间,因为o CanvasObject不再是*widget.Label

我们需要将其施放到Container,然后将小部件嵌套在使用索引中,然后将它们全部投入到我们所知道的(正如我们定义的,我们应该知道)。

                func(i widget.ListItemID, o fyne.CanvasObject) {
                    ctr, _ := o.(*fyne.Container)
                    // ideally we should check `ok` for each one of those casting
                    // but we know that they are those types for sure
                    l := ctr.Objects[0].(*widget.Label)
                    c := ctr.Objects[1].(*widget.Check)
                    l.SetText(data[i].Description)
                    c.SetChecked(data[i].Done)
                }),

这就是现在的样子

with box

我个人发现,在API中铸造的东西很奇怪,并且在容器中获得组件的顺序有点受欢迎,错过了,可能需要尝试一些时间来查看[1][1]Label还是Check for Real17 。

无论如何,我们到达那里,但是如果我们在data中添加另一个todo,列表不会反映更改,因为我们只是在使用静态的简单列表,如果我们想使其动态,我们需要为应用程序,我们需要使用binding

绑定和动态列表

绑定在docs中很好地说明了简单类型,但对于结构类型而言,绑定却不是,这是我第一次尝试Fyne时感到烦恼。

您真正要做的是创建一个DataList,添加我们的todos切片中的项目,并使用另一个小部件API渲染列表。

它看起来与我们在这里所做的事情非常相似,但是在我们的情况下,由于我们创建的数据类型是Untyped类型,我们将不得不添加一个步骤,而不是原始类型,以投放数据项到我们自己的models.Todo结构。

这是您如何创建数据列表

    data := []models.Todo{
        models.NewTodo("Some stuff"),
        models.NewTodo("Some more stuff"),
        models.NewTodo("Some other things"),
    }
    todos := binding.NewUntypedList()
    for _, t := range data {
        todos.Append(t)
    }

如果您可以在创建上设置项目,那将是很好的,但是API尚不允许这样做。

那么这就是列表创建的样子

            widget.NewListWithData(
                // the binding.List type
                todos,
                // func that returns the component structure of the List Item
                // exactly the same as the Simple List
                func() fyne.CanvasObject {
                    return container.NewBorder(
                        nil, nil, nil,
                        // left of the border
                        widget.NewCheck("", func(b bool) {}),
                        // takes the rest of the space
                        widget.NewLabel(""),
                    )
                },
                // func that is called for each item in the list and allows
                // but this time we get the actual DataItem we need to cast
                func(di binding.DataItem, o fyne.CanvasObject) {
                    ctr, _ := o.(*fyne.Container)
                    // ideally we should check `ok` for each one of those casting
                    // but we know that they are those types for sure
                    l := ctr.Objects[0].(*widget.Label)
                    c := ctr.Objects[1].(*widget.Check)
                    diu, _ := di.(binding.Untyped).Get()
                    todo := diu.(models.Todo)

                    l.SetText(todo.Description)
                    c.SetChecked(todo.Done)
                }),

另一个铸件是我很难理解的

                    diu, _ := di.(binding.Untyped).Get()
                    todo := diu.(models.Todo)

我们得到了一个dataitem,它是下面的binding.Untyped,我们需要Get(),将其铸造为模型,然后我们最终可以使用它。

我通常将列表中的功能移动以分开功能并在模型包上制作一个小方法来处理该类型的铸件,因此看起来有点混乱。

这样的东西

// in models
func NewTodoFromDataItem(item binding.DataItem) Todo {
    v, _ := item.(binding.Untyped).Get()
    return v.(Todo)
}

// so in the list function will look like so
                func(di binding.DataItem, o fyne.CanvasObject) {
                    ctr, _ := o.(*fyne.Container)
                    // ideally we should check `ok` for each one of those casting
                    // but we know that they are those types for sure
                    l := ctr.Objects[0].(*widget.Label)
                    c := ctr.Objects[1].(*widget.Check)
                    /*
                        diu, _ := di.(binding.Untyped).Get()
                        todo := diu.(models.Todo)
                    */
                    todo := models.NewTodoFromDataItem(di)
                    l.SetText(todo.Description)
                    c.SetChecked(todo.Done)
                }),

无论如何,现在让我们做最后一步,如何添加新的todo?
只需使用addBtn func上的数据列表就这样

addBtn := widget.NewButton("Add", func() {
        todos.Append(models.NewTodo(newtodoDescTxt.Text))
        newtodoDescTxt.Text = ""
    })

单击它后,它将神奇地将其添加到列表中,并在列表组件上显示一个新列表项目。
作为一个小型功能,我们还需要清除文本条目,因此我们准备添加另一个。

我们还可以使用Prepend方法,而不是Append,因此todo将在列表中获得第一名,而不是最后一个。

其他笔记

  • 如果要更改实际项目,最好创建*models.Todo的切片,以便他们使用这些片而不是克隆。

  • 目前,数据库中没有Remove API

// to remove them all for example you should do something like this
        list, _ := todos.Get()
    list = list[:0]
    t.Set(list)

如果您对代码示例感兴趣,我在此处将其上传到我的github:github.com/vikkio88/fyne-tutorials/tree/main/todoapp

如果您想使用clover观看一些语法糖和性感的DB持久层的更复杂示例,则可以查看此gtodos

我还添加了一个没有DB的示例分支

最后,如果您对一个更复杂的应用程序示例感兴趣,我发布了here关于我在Fyne重写密码管理器并在几天之内进行管理以使用github Actions创建和分发应用程序。
Muscurd-ig.

那是所有人,请让我知道您的想法或是否不清楚。

下次见。