Я хочу добавить графический интерфейс в приложение командной строки, которое я написал на Go, но у меня возникают проблемы с зависимостями fyne и циклическими.
Рассмотрим этот простой пример, чтобы проиллюстрировать проблему, с которой я столкнулся: предположим, что кнопка запускает трудоемкий метод в моем классе модели (скажем, получение данных или что-то в этом роде), и я хочу, чтобы представление обновлялось после завершения задачи.
Я начал с реализации очень наивного и совершенно не связанного решения, которое, очевидно, приводит к ошибке циклической зависимости, выдаваемой компилятором go. Рассмотрим следующий код:
main.go
package main
import (
"my-gui/gui"
)
func main() {
gui.Init()
}
графический интерфейс / 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.go
package model
import (
"my-gui/gui" // <-- this dependency is where it obviously hits the fan
//[...]
)
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 :-)
Может ли кто-нибудь указать мне идиоматическое решение этой проблемы? Я, наверное, упускаю что-то очень, очень простое здесь...
Это горутина, которая работает асинхронно. Насколько мне известно, он не может вернуть значение, иначе он заблокировал бы поток пользовательского интерфейса... см. stackoverflow.com/questions/20945069/… - Но, возможно, использование каналов решит проблему: -/
Собственно, я имел в виду сделать model
полностью независимым от вызывающего. Это был просто пример, вы могли присвоить значение новой переменной и потом вызвать UpdateCounterLabel
. Я бы порекомендовал использовать канал для этого, просто хотел подчеркнуть :)
Да, будь проще. Модель всегда должна быть синхронизированной, поэтому асинхронность может быть добавлена по мере необходимости на уровне графического интерфейса и т. д. В этом случае работает возвращаемое значение, но обратные вызовы, вероятно, потребуются для более сложного потока данных.
Все ответы здесь великолепны, но вы правы, Fyne databinding
был бы другим способом сделать это, поскольку он обрабатывает обратные вызовы за вас, хотя ваша модель данных будет зависеть от Fyne, что не всегда может быть желательным. Привязки данных по умолчанию просты, но они полностью расширяемы, чтобы делать все, что вы хотите :).
Вы можете вернуть значение.
func DoTimeConsumingStuff() int {
time.Sleep(1 * time.Second)
counter++
return counter
}
Затем при нажатии кнопки вы создаете анонимную горутину, чтобы не блокировать пользовательский интерфейс.
counterButton := widget.NewButton("Increment", 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("Increment", 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)
}
Затем в графическом интерфейсе вы можете получать данные из канала, опять же в горутине, чтобы не блокировать пользовательский интерфейс.
counterButton := widget.NewButton("Increment", func() {
go func() {
counterChan := make(chan int)
go model.DoTimeConsumingStuff(counterChan)
for counter := range counterChan {
UpdateCounterLabel(counter)
}
}()
})
Конечно, вы также можете использовать обратный вызов, который вы вызываете на каждой итерации.
Спасибо друг! Я рассмотрю все эти варианты. Высоко ценится. Я оставлю вопрос открытым на другой день для дополнительных предложений :-)
Быстрый вопрос @The Fool: возможно ли сохранить ссылку на функцию обратного вызова как член структуры или как переменную внутри пакета, то есть иметь что-то вроде var myCallback func(int)
, а затем сеттер, например func SetCallback(callback func(int)) {myCallback = callback}
? Я пробовал это, но компилятор отклонил мое определение var myCallback func(int)
...
var myCallback func(int)
, работает. У вас может быть другая проблема. Но я не думаю, что это хорошая практика. Почему вы не можете просто передать его, когда вызываете модель func?
Я, вероятно/скорее всего, могу :-) ... Мне просто было интересно, что пошло не так, когда я попробовал вышеописанное.
@Christian, на самом деле в стандартной библиотеке есть пакет с обратным вызовом в виде поля структуры. pkg.go.dev/net/http/httputil#ReverseProxy, проверьте, например, поля Director и ModifyResponse.
Я не знаком с fyne, но как насчет того, чтобы сделать модель как можно более простой и вместо этого возвращать значение?
go UpdateCounterLabel(model.DoTimeConsumingStuff())