Как избежать циклических зависимостей в приложении с графическим интерфейсом с помощью fyne?

Я хочу добавить графический интерфейс в приложение командной строки, которое я написал на 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 :-)

Может ли кто-нибудь указать мне идиоматическое решение этой проблемы? Я, наверное, упускаю что-то очень, очень простое здесь...

Я не знаком с fyne, но как насчет того, чтобы сделать модель как можно более простой и вместо этого возвращать значение? go UpdateCounterLabel(model.DoTimeConsumingStuff())

medasx 20.03.2022 13:56

Это горутина, которая работает асинхронно. Насколько мне известно, он не может вернуть значение, иначе он заблокировал бы поток пользовательского интерфейса... см. stackoverflow.com/questions/20945069/… - Но, возможно, использование каналов решит проблему: -/

Christian 20.03.2022 14:36

Собственно, я имел в виду сделать model полностью независимым от вызывающего. Это был просто пример, вы могли присвоить значение новой переменной и потом вызвать UpdateCounterLabel. Я бы порекомендовал использовать канал для этого, просто хотел подчеркнуть :)

medasx 20.03.2022 14:57

Да, будь проще. Модель всегда должна быть синхронизированной, поэтому асинхронность может быть добавлена ​​по мере необходимости на уровне графического интерфейса и т. д. В этом случае работает возвращаемое значение, но обратные вызовы, вероятно, потребуются для более сложного потока данных.

andy.xyz 21.03.2022 10:23

Все ответы здесь великолепны, но вы правы, Fyne databinding был бы другим способом сделать это, поскольку он обрабатывает обратные вызовы за вас, хотя ваша модель данных будет зависеть от Fyne, что не всегда может быть желательным. Привязки данных по умолчанию просты, но они полностью расширяемы, чтобы делать все, что вы хотите :).

andy.xyz 21.03.2022 10:24
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
2
5
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Возвращаемое значение

Вы можете вернуть значение.

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)
        }
    }()      
})

Конечно, вы также можете использовать обратный вызов, который вы вызываете на каждой итерации.

Спасибо друг! Я рассмотрю все эти варианты. Высоко ценится. Я оставлю вопрос открытым на другой день для дополнительных предложений :-)

Christian 20.03.2022 17:17

Быстрый вопрос @The Fool: возможно ли сохранить ссылку на функцию обратного вызова как член структуры или как переменную внутри пакета, то есть иметь что-то вроде var myCallback func(int), а затем сеттер, например func SetCallback(callback func(int)) {myCallback = callback}? Я пробовал это, но компилятор отклонил мое определение var myCallback func(int)...

Christian 20.03.2022 20:36
var myCallback func(int), работает. У вас может быть другая проблема. Но я не думаю, что это хорошая практика. Почему вы не можете просто передать его, когда вызываете модель func?
The Fool 20.03.2022 20:51

Я, вероятно/скорее всего, могу :-) ... Мне просто было интересно, что пошло не так, когда я попробовал вышеописанное.

Christian 20.03.2022 21:42

@Christian, на самом деле в стандартной библиотеке есть пакет с обратным вызовом в виде поля структуры. pkg.go.dev/net/http/httputil#ReverseProxy, проверьте, например, поля Director и ModifyResponse.

The Fool 21.03.2022 08:02

Другие вопросы по теме