如何在使用fyne的GUI应用程序中避免循环依赖?

huangapple go评论67阅读模式
英文:

How to avoid circular dependencies in a GUI application with fyne?

问题

我想给我用Go编写的命令行应用程序添加一个GUI,但是在使用fyne和循环依赖方面遇到了问题。

考虑以下简单的示例来说明我面临的问题:假设一个按钮触发了我的模型类上的一个耗时方法(比如获取数据),当任务完成时,我希望视图更新。

我开始实现一个非常简单和不完全解耦的解决方案,显然会遇到由Go编译器引发的循环依赖错误。请考虑以下代码:

main.go

package main

import (
	"my-gui/gui"
)

func main() {
	gui.Init()
}

gui/gui.go

package gui

import (
	"my-gui/model"
	//[...] fyne imports
)

var counterLabel *widget.Label

func Init() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Test")

	counterLabel = widget.NewLabel("0")

	counterButton := widget.NewButton("Increment", func() {
		go model.DoTimeConsumingStuff()
	})

	content := container.NewVBox(counterLabel, counterButton)

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

func UpdateCounterLabel(value int) {
	if counterLabel != nil {
		counterLabel.SetText(strconv.Itoa(value))
	}
}

model/model.go

package model

import (
	"my-gui/gui" // <--- 这个依赖是引发问题的地方
	//[...]
)

var counter = 0

func DoTimeConsumingStuff() {
	time.Sleep(1 * time.Second)
	
	counter++

	fmt.Println("Counter: " + strconv.Itoa(counter))
	gui.UpdateCounterLabel(counter)
}

所以我想知道如何正确解耦这个简单的应用程序以使其正常工作。我考虑过以下几点:

  • 使用fyne数据绑定:对于上面示例中标签文本这样的简单内容,这应该是可行的。但是如果我需要根据模型的状态以非常自定义的方式更新更多内容怎么办?比如说,我需要根据模型的条件来更新按钮的启用状态。这个能绑定到数据吗?是否可能?

  • 使用接口,就像标准的MVC设计模式一样:我也尝试过这个,但是无法真正理解。我创建了一个单独的模块,提供一个接口,然后模型类可以导入该接口。然后,我会注册一个(隐式)实现该接口的视图到模型中。但是我无法让它正常工作。我猜想我对Go接口的理解在这一点上还不够。

  • 短轮询模型:这只是一种不太好的解决方案,肯定不是Go和/或fyne开发者的意图 如何在使用fyne的GUI应用程序中避免循环依赖?

有人能指点我一个这个问题的惯用解决方案吗?我可能在这里漏掉了非常基础的东西...

英文:

I want to add a GUI to a command line application that I have written in Go but I'm running into problems with fyne and circular dependencies.

Consider this simple example to illustrate the problem I am facing: Assume that a button triggers a time-consuming method on my model class (say fetching data or so) and I want the view to update when the task has finished.

I started by implementing a very naive and not at-all-decoupled solution, which obviously runs into a circular dependency error raised by the go compiler. Consider the following code:

main.go

package main

import (
	&quot;my-gui/gui&quot;
)

func main() {
	gui.Init()
}

gui/gui.go

package gui

import (
	&quot;my-gui/model&quot;
    //[...] fyne imports
)

var counterLabel *widget.Label

func Init() {
	myApp := app.New()
	myWindow := myApp.NewWindow(&quot;Test&quot;)

	counterLabel = widget.NewLabel(&quot;0&quot;)

	counterButton := widget.NewButton(&quot;Increment&quot;, func() {
		go model.DoTimeConsumingStuff()
	})

	content := container.NewVBox(counterLabel, counterButton)

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

func UpdateCounterLabel(value int) {
	if counterLabel != nil {
		counterLabel.SetText(strconv.Itoa(value))
	}
}

model/model.go

package model

import (
	&quot;my-gui/gui&quot; // &lt;-- this dependency is where it obviously hits the fan
	//[...]
)

var counter = 0

func DoTimeConsumingStuff() {
	time.Sleep(1 * time.Second)
	
	counter++

	fmt.Println(&quot;Counter: &quot; + strconv.Itoa(counter))
	gui.UpdateCounterLabel(counter)
}

So I am wondering how I could properly decouple this simple app to get it working. What I thought about:

  • use fyne data binding: That should work for simple stuff such as the label text in the example above. But what if I have to update more in a very custom way according to a model's state. Say I'd have to update a button's enabled state based on a model's condition. How can this be bound to data? Is that possible at all?

  • use interfaces as in the standard MVC design pattern: I tried this as well but couldn't really get my head around it. I created a separate module that would provide an interface which could then be imported by the model class. I would then register a view that (implicitly) implements that interface with the model. But I couldn't get it to work. I assume that my understanding of go interfaces isn't really sufficient at this point.

  • short polling the model: that's just meh and certainly not what the developers of Go and/or fyne intended 如何在使用fyne的GUI应用程序中避免循环依赖?

Can anyone please point me to an idiomatic solution for this problem? I'm probably missing something very, very basic here...

答案1

得分: 5

返回值

你可以返回这个值。

func DoTimeConsumingStuff() int {
    time.Sleep(1 * time.Second)
    counter++
    return counter
}

然后在按钮点击时,你可以生成一个匿名的 goroutine,以避免阻塞用户界面。

counterButton := widget.NewButton("增加", func() {
    go func() {
        counter := model.DoTimeConsumingStuff(counterChan)
        UpdateCounterLabel(counter)
    }()      
})

回调函数

你可以将 UpdateCounterLabel 函数作为回调函数传递给你的模型函数。

func DoTimeConsumingStuff(callback func(int)) {
    time.Sleep(1 * time.Second)
    counter++
    callback(counter)
}
counterButton := widget.NewButton("增加", func() {
    go model.DoTimeConsumingStuff(UpdateCounterLabel)
})

通道

也许你可以将一个通道传递给你的模型函数。但是根据上述方法,这似乎并不需要。只有当你有多个计数器值时,可能会需要通道。

func DoTimeConsumingStuff(counterChan chan int) {
    for i := 0; i < 10; i++ {
        time.Sleep(1 * time.Second)
        counter++
        counterChan <- counter
    }
    close(counterChan)
}

在 GUI 中,你可以从通道接收数据,同样使用 goroutine 以避免阻塞用户界面。

counterButton := widget.NewButton("增加", func() {
    go func() {
        counterChan := make(chan int)
        go model.DoTimeConsumingStuff(counterChan)
        for counter := range counterChan {
            UpdateCounterLabel(counter)
        }
    }()      
})

当然,你也可以再次使用回调函数,在每次迭代时调用它。

英文:

Return Value

You could return the value.

func DoTimeConsumingStuff() int {
    time.Sleep(1 * time.Second)
    counter++
    return counter
}

Then on button click you spawn an anonymous goroutine, in order to not block the UI.

counterButton := widget.NewButton(&quot;Increment&quot;, func() {
    go func() {
        counter := model.DoTimeConsumingStuff(counterChan)
        UpdateCounterLabel(counter)
    }()      
})

Callback

You could pass the UpdateCounterLabel function to your model function aka callback.

func DoTimeConsumingStuff(callback func(int)) {
    time.Sleep(1 * time.Second)
    counter++
    callback(counter)
}
counterButton := widget.NewButton(&quot;Increment&quot;, func() {
    go model.DoTimeConsumingStuff(UpdateCounterLabel)
})

Channel

Maybe you could also pass a channel to your model function. But with the above approach, this doesn't seem required. Potentially, if you have more than one counter value coming.

func DoTimeConsumingStuff(counterChan chan int) {
    for i := 0; i &lt; 10; i++ {
        time.Sleep(1 * time.Second)
        counter++
        counterChan &lt;- counter
    }
    close(counterChan)
}

In the GUI you can then receive from the channel, again in a goroutine in order to not block the UI.

counterButton := widget.NewButton(&quot;Increment&quot;, func() {
    go func() {
        counterChan := make(chan int)
        go model.DoTimeConsumingStuff(counterChan)
        for counter := range counterChan {
            UpdateCounterLabel(counter)
        }
    }()      
})

Of course, you could also use, again, a callback that you call on each iteration.

huangapple
  • 本文由 发表于 2022年3月20日 20:30:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/71546756.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定