这是最新修改的能正常运行的 main.go 代码: package main import ( "fmt" "image" "log" "net/http" "os" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "main.go/dataModel/CookieModel" "main.go/dataModel/ShopModel" "main.go/dataModel/SkuModel" "main.go/dataModel/UserModel" "main.go/tuuz/database" ) // 全局状态 type AppState struct { CurrentUser UserModel.UserInfo Shops []ShopModel.Account ProductTabs *container.AppTabs StatusBar *widget.Label ShopListBinding binding.UntypedList LoginForm *widget.Form } func main() { os.Setenv("PLAYWRIGHT_BROWSERS_PATH", "./browsers") database.Init() UserModel.UserInit() ShopModel.ShopInit() CookieModel.CreateCookieInfoTable() SkuModel.ProductInit() myApp := app.New() myWindow := myApp.NewWindow("店铺管理工具") myWindow.Resize(fyne.NewSize(1200, 800)) // 初始化应用状态 appState := &AppState{} // 创建状态栏 appState.StatusBar = widget.NewLabel("就绪") statusBar := container.NewHBox(layout.NewSpacer(), appState.StatusBar) // 创建主布局 mainContent := createMainUI(myWindow, appState) // 设置整体布局 content := container.NewBorder(nil, statusBar, nil, nil, mainContent) myWindow.SetContent(content) // 启动时尝试自动登录(如果有保存的用户) go tryAutoLogin(appState) myWindow.ShowAndRun() } // 创建主UI布局 func createMainUI(window fyne.Window, appState *AppState) fyne.CanvasObject { // 左侧面板(登录 + 店铺列表) leftPanel := createLeftPanel(window, appState) // 右侧面板(商品TAB展示) appState.ProductTabs = container.NewAppTabs() rightPanel := container.NewBorder( widget.NewLabel("商品信息"), nil, nil, nil, appState.ProductTabs, ) // 主布局(左右分割) split := container.NewHSplit(leftPanel, rightPanel) split.SetOffset(0.3) // 左侧占30% return split } // 创建左侧面板 func createLeftPanel(window fyne.Window, appState *AppState) fyne.CanvasObject { // 登录表单 loginPanel := createLoginForm(appState) // 店铺列表 shopListPanel := createShopListPanel(appState) // 左侧布局(上下分割) return container.NewVSplit(loginPanel, shopListPanel) } // 创建登录表单 func createLoginForm(appState *AppState) fyne.CanvasObject { usernameEntry := widget.NewEntry() passwordEntry := widget.NewPasswordEntry() // 这是正确的密码输入框创建方式 // 尝试从数据库加载用户 user, err := UserModel.Api_find_by_username(usernameEntry.Text) if err == nil && user.LoginName != "" { usernameEntry.SetText(user.LoginName) } // 创建表单 form := &widget.Form{ Items: []*widget.FormItem{ {Text: "邮箱", Widget: usernameEntry}, {Text: "密码", Widget: passwordEntry}, // 这里直接使用 passwordEntry }, OnSubmit: func() { appState.StatusBar.SetText("登录中...") go func() { // shops, err := service.LoginOrAutoLogin(usernameEntry.Text, passwordEntry.Text) // if err != nil { // appState.StatusBar.SetText("登录失败: " + err.Error()) // return // } // // 更新UI状态 // appState.CurrentUser, _ = UserModel.Api_find_by_username(usernameEntry.Text) // appState.Shops = shops // appState.StatusBar.SetText(fmt.Sprintf("登录成功! 共 %d 个店铺", len(shops))) shops := ShopModel.Api_select_struct(nil) if len(shops) == 0 { appState.StatusBar.SetText(fmt.Sprintf("获取已经存在的店铺信息为空")) return } appState.Shops = shops // 更新店铺列表 updateShopListBinding(appState) }() }, } appState.LoginForm = form return container.NewVBox( widget.NewLabel("登录面板"), form, ) } // 尝试自动登录 func tryAutoLogin(appState *AppState) { // 获取所有用户 users := UserModel.Api_select_struct(nil) if len(users) == 0 { appState.StatusBar.SetText(fmt.Sprintf("获取已经存在的账号为空")) return } // 尝试使用第一个用户自动登录 user := users[0] appState.StatusBar.SetText(fmt.Sprintf("尝试自动登录: %s...", user.LoginName)) // 更新登录表单 if appState.LoginForm != nil { // 获取用户名输入框 usernameItem := appState.LoginForm.Items[0] usernameEntry, ok := usernameItem.Widget.(*widget.Entry) if !ok { appState.StatusBar.SetText("自动登录失败: 用户名控件类型错误") return } // 获取密码输入框 - 这里使用 *widget.Entry 类型 passwordItem := appState.LoginForm.Items[1] passwordEntry, ok := passwordItem.Widget.(*widget.Entry) if !ok { appState.StatusBar.SetText("自动登录失败: 密码控件类型错误") return } usernameEntry.SetText(user.LoginName) passwordEntry.SetText(user.LoginPass) // 触发登录 appState.StatusBar.SetText("正在自动登录...") appState.LoginForm.OnSubmit() } } // 修改后的异步加载店铺头像函数 func loadShopAvatar(img *canvas.Image, url string) { if url == "" { // 使用默认头像 fyne.DoAndWait(func() { img.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "account") img.Refresh() }) return } // 创建HTTP客户端(可设置超时) client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { log.Printf("加载头像失败: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("头像请求失败: %s", resp.Status) return } // 解码图片 imgData, _, err := image.Decode(resp.Body) if err != nil { log.Printf("解码头像失败: %v", err) return } fyne.DoAndWait(func() { img.Image = imgData img.Refresh() }) } // 修改后的 createShopListPanel 函数 func createShopListPanel(appState *AppState) fyne.CanvasObject { // 创建绑定数据 appState.ShopListBinding = binding.NewUntypedList() // 创建列表控件 - 使用自定义模板 list := widget.NewListWithData( appState.ShopListBinding, func() fyne.CanvasObject { // 创建包含头像和名称的水平容器 avatar := canvas.NewImageFromResource(nil) avatar.SetMinSize(fyne.NewSize(40, 40)) avatar.FillMode = canvas.ImageFillContain nameLabel := widget.NewLabel("") statusIcon := widget.NewIcon(nil) return container.NewHBox( avatar, container.NewVBox( nameLabel, ), layout.NewSpacer(), statusIcon, ) }, // 修改后的列表项更新函数 func(item binding.DataItem, obj fyne.CanvasObject) { // 安全类型断言 hbox, ok := obj.(*fyne.Container) if !ok { log.Println("错误:传入对象不是容器") return } // 检查容器结构是否如预期 if len(hbox.Objects) < 4 { log.Println("错误:容器子元素数量不足") return } // 获取头像组件(直接类型断言) avatar, ok := hbox.Objects[0].(*canvas.Image) if !ok { log.Println("错误:第一个子元素不是图像") return } // 获取名称标签(通过嵌套容器) nameContainer, ok := hbox.Objects[1].(*fyne.Container) if !ok || len(nameContainer.Objects) == 0 { log.Println("错误:名称容器无效") return } nameLabel, ok := nameContainer.Objects[0].(*widget.Label) if !ok { log.Println("错误:名称标签无效") return } // 获取状态图标 statusIcon, ok := hbox.Objects[3].(*widget.Icon) if !ok { log.Println("错误:状态图标无效") return } // 获取店铺数据 val, err := item.(binding.Untyped).Get() if err != nil { log.Printf("获取数据失败: %v", err) return } shop, ok := val.(ShopModel.Account) if !ok { log.Println("错误:数据类型不匹配") return } // 设置店铺名称 nameLabel.SetText(shop.AccountName) // 设置状态图标 if shop.CanLogin { statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "success")) } else { statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error")) } // 异步加载头像(使用原有loadShopAvatar函数) go loadShopAvatar(avatar, shop.AccountAvatar) }, ) // 添加点击事件 list.OnSelected = func(id widget.ListItemID) { shop := appState.Shops[id] appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName)) go func() { // 加载商品数据 products, err := loadProductsForShop(shop) if err != nil { appState.StatusBar.SetText("加载商品失败: " + err.Error()) return } // 更新UI appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products))) addOrUpdateProductTab(appState, shop, products) }() } return container.NewBorder( widget.NewLabel("店铺列表"), nil, nil, nil, list, ) } // 更新店铺列表绑定数据 func updateShopListBinding(appState *AppState) { // 清空绑定数据 appState.ShopListBinding.Set(make([]interface{}, 0)) // 添加新数据 values := make([]interface{}, len(appState.Shops)) for i, shop := range appState.Shops { values[i] = shop } appState.ShopListBinding.Set(values) } // 为店铺加载商品数据 func loadProductsForShop(shop ShopModel.Account) ([]SkuModel.DataItem, error) { // 获取店铺的Cookie信息 // cookieInfo, found := CookieModel.Api_find_by_subject_id(shop.SubjectID) // if !found { // return nil, fmt.Errorf("未找到店铺的Cookie信息") // } // 模拟API调用获取商品数据 time.Sleep(1 * time.Second) // 模拟网络延迟 // 这里应该是实际的API调用 // products, err := SkuModel.GetSkuList(shop.SubjectID, cookieInfo.Token, cookieInfo.VerifyFp) // 模拟返回数据 return []SkuModel.DataItem{ {ProductID: "1001", Name: "商品A", MarketPrice: 999, DiscountPrice: 100}, {ProductID: "1002", Name: "商品B", MarketPrice: 1999, DiscountPrice: 50}, {ProductID: "1003", Name: "商品C", MarketPrice: 2999, DiscountPrice: 30}, }, nil } // 添加或更新商品TAB func addOrUpdateProductTab(appState *AppState, shop ShopModel.Account, products []SkuModel.DataItem) { tabTitle := shop.AccountName //fmt.Sprintf("%s (%d)", shop.AccountName, shop.AccountID) // 检查是否已存在该TAB for _, tab := range appState.ProductTabs.Items { if tab.Text == tabTitle { // 更新现有TAB tab.Content = createProductList(products) appState.ProductTabs.Refresh() return } } // 创建新TAB newTab := container.NewTabItem(tabTitle, createProductList(products)) appState.ProductTabs.Append(newTab) appState.ProductTabs.Select(newTab) } // 创建商品列表 func createProductList(products []SkuModel.DataItem) fyne.CanvasObject { // 创建表格 table := widget.NewTable( func() (int, int) { return len(products) + 1, 4 // 行数=商品数+表头,列数=4 }, func() fyne.CanvasObject { return widget.NewLabel("模板文本") }, func(id widget.TableCellID, cell fyne.CanvasObject) { label := cell.(*widget.Label) if id.Row == 0 { // 表头 switch id.Col { case 0: label.SetText("商品ID") case 1: label.SetText("商品名称") case 2: label.SetText("价格") case 3: label.SetText("库存") } label.TextStyle.Bold = true return } // 数据行 product := products[id.Row-1] switch id.Col { case 0: label.SetText(product.ProductID) case 1: label.SetText(product.Name) case 2: label.SetText(fmt.Sprintf("¥%.2f", product.MarketPrice/100)) case 3: label.SetText(fmt.Sprintf("%d", product.MarketPrice)) } }, ) // 设置列宽 table.SetColumnWidth(0, 100) table.SetColumnWidth(1, 300) table.SetColumnWidth(2, 100) table.SetColumnWidth(3, 100) return table } 提示的警告日志为: 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 请帮修复这些错误或警告
时间: 2025-07-22 20:27:30 浏览: 9
<think>我们注意到日志中反复出现以下警告:
"*** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] ***"
这个警告通常是因为我们在非主线程(例如后台goroutine)中直接更新UI组件导致的。
在Fyne中,所有UI更新必须在主线程中执行,否则可能导致未定义行为或崩溃。
根据日志,问题出现在main.go的341行和405行。我们需要检查这些位置的代码。
根据提供的代码,341行在列表项更新函数中(即createShopListPanel中的列表项更新函数):
在更新店铺名称和状态图标时,我们直接调用了SetText和SetResource,这些操作是在后台goroutine中执行的(因为列表项更新可能由后台数据绑定触发)。
同样,405行在loadShopAvatar函数中,我们在异步加载图片后,使用fyne.DoAndWait更新图片,但请注意,在列表项更新函数中,我们并没有在fyne.DoAndWait中更新名称和状态。
解决方案:
1. 在列表项更新函数中,所有更新UI的操作(包括设置文本、图标等)都应该放在fyne.Do或fyne.DoAndWait中执行。
2. 在loadShopAvatar函数中,我们已经使用了fyne.DoAndWait,所以没有问题。
具体修改如下:
修改列表项更新函数(在createShopListPanel中):
将设置名称和状态图标的代码包裹在fyne.DoAndWait中。
同时,我们注意到在loadShopAvatar函数中,当url为空时,我们使用了fyne.DoAndWait来设置默认头像,这没有问题。
修改后的列表项更新函数部分:
```go
func(item binding.DataItem, obj fyne.CanvasObject) {
// ... 之前的类型断言和错误检查保持不变 ...
// 获取店铺数据
val, err := item.(binding.Untyped).Get()
if err != nil {
log.Printf("获取数据失败: %v", err)
return
}
shop, ok := val.(ShopModel.Account)
if !ok {
log.Println("错误:数据类型不匹配")
return
}
// 在UI线程中更新组件
fyne.DoAndWait(func() {
// 设置店铺名称
nameLabel.SetText(shop.AccountName)
// 设置状态图标
if shop.CanLogin {
statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "success"))
} else {
statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error"))
}
})
// 异步加载头像(使用原有loadShopAvatar函数)
go loadShopAvatar(avatar, shop.AccountAvatar)
},
```
另外,405行对应的loadShopAvatar函数中,我们使用fyne.DoAndWait更新图片,这没有问题。但是,我们还需要检查是否有其他地方在后台线程更新UI。
例如,在自动登录过程中,我们更新了状态栏和登录表单,这些操作是在后台goroutine中进行的(在tryAutoLogin函数中,我们是在后台触发了登录操作)。因此,我们也需要将这些UI更新放在主线程中。
修改tryAutoLogin函数中更新UI的部分:
```go
// 尝试自动登录
func tryAutoLogin(appState *AppState) {
// ... 前面的代码不变 ...
// 更新登录表单
if appState.LoginForm != nil {
// 获取用户名输入框
usernameItem := appState.LoginForm.Items[0]
usernameEntry, ok := usernameItem.Widget.(*widget.Entry)
if !ok {
// 使用主线程更新状态栏
fyne.DoAndWait(func() {
appState.StatusBar.SetText("自动登录失败: 用户名控件类型错误")
})
return
}
// 获取密码输入框 - 这里使用 *widget.Entry 类型
passwordItem := appState.LoginForm.Items[1]
passwordEntry, ok := passwordItem.Widget.(*widget.Entry)
if !ok {
fyne.DoAndWait(func() {
appState.StatusBar.SetText("自动登录失败: 密码控件类型错误")
})
return
}
fyne.DoAndWait(func() {
usernameEntry.SetText(user.LoginName)
passwordEntry.SetText(user.LoginPass)
appState.StatusBar.SetText("正在自动登录...")
})
// 触发登录(注意:登录操作内部有goroutine,但登录操作中更新UI的部分也需要确保在主线程)
appState.LoginForm.OnSubmit()
}
}
```
同样,在登录表单的OnSubmit回调中,我们在后台goroutine中更新了状态栏和店铺列表,这些更新也需要放在主线程中。
修改登录表单的OnSubmit回调:
```go
OnSubmit: func() {
// 在主线程中更新状态栏
fyne.DoAndWait(func() {
appState.StatusBar.SetText("登录中...")
})
go func() {
// ... 模拟登录过程 ...
// 更新UI状态(需要在主线程中执行)
fyne.DoAndWait(func() {
appState.Shops = shops
updateShopListBinding(appState)
appState.StatusBar.SetText(fmt.Sprintf("登录成功! 共 %d 个店铺", len(shops)))
})
}()
},
```
另外,在店铺列表的点击事件中,我们在后台goroutine中更新了状态栏和商品标签页,同样需要放在主线程中。
修改店铺列表的OnSelected回调:
```go
list.OnSelected = func(id widget.ListItemID) {
shop := appState.Shops[id]
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName))
})
go func() {
// 加载商品数据
products, err := loadProductsForShop(shop)
if err != nil {
fyne.DoAndWait(func() {
appState.StatusBar.SetText("加载商品失败: " + err.Error())
})
return
}
// 更新UI(需要在主线程中执行)
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products)))
addOrUpdateProductTab(appState, shop, products)
})
}()
}
```
总结:任何更新UI的操作(包括设置文本、图标、刷新组件等)都必须放在主线程中执行。我们可以使用`fyne.Do`或`fyne.DoAndWait`来确保代码在主线程执行。
注意:`fyne.Do`会异步在主线程执行,而`fyne.DoAndWait`会同步等待主线程执行完成。
根据上述修改,我们重新整理代码。
由于代码较长,我们只修改关键部分。请按照上述修改点更新代码。
修改后,应该可以消除非主线程更新UI的警告。
如果问题仍然存在,请检查其他可能更新UI的地方(如updateShopListBinding函数,但该函数内部只是操作绑定数据,绑定数据会自动触发列表更新,而列表更新中的UI操作我们已经放在主线程了)。
另外,在loadProductsForShop函数中,我们模拟了网络延迟,但该函数不涉及UI操作,所以没有问题。
最后,在addOrUpdateProductTab函数中,我们更新了标签页,这个操作必须在主线程中执行,但我们在调用该函数时已经使用了fyne.DoAndWait,所以没有问题。
按照上述修改后,重新运行程序,观察警告是否消失。</think># 修复 Fyne 非主线程 UI 更新问题
根据您提供的错误日志,问题主要出在非主线程更新 UI 组件导致的警告。Fyne 要求所有 UI 更新必须在主线程执行,否则会触发这些警告。以下是完整的修复方案:
```go
package main
import (
// ... 其他导入保持不变 ...
)
// ... 其他代码保持不变 ...
// 修改后的异步加载店铺头像函数
func loadShopAvatar(img *canvas.Image, url string) {
if url == "" {
// 使用默认头像
fyne.DoAndWait(func() {
img.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "account")
img.Refresh()
})
return
}
// 创建HTTP客户端(可设置超时)
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
log.Printf("加载头像失败: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("头像请求失败: %s", resp.Status)
return
}
// 解码图片
imgData, _, err := image.Decode(resp.Body)
if err != nil {
log.Printf("解码头像失败: %v", err)
return
}
// 在主线程更新UI
fyne.DoAndWait(func() {
img.Image = imgData
img.Refresh()
})
}
// 修改后的 createShopListPanel 函数
func createShopListPanel(appState *AppState) fyne.CanvasObject {
// 创建绑定数据
appState.ShopListBinding = binding.NewUntypedList()
// 创建列表控件 - 使用自定义模板
list := widget.NewListWithData(
appState.ShopListBinding,
func() fyne.CanvasObject {
// 创建包含头像和名称的水平容器
avatar := canvas.NewImageFromResource(nil)
avatar.SetMinSize(fyne.NewSize(40, 40))
avatar.FillMode = canvas.ImageFillContain
nameLabel := widget.NewLabel("")
statusIcon := widget.NewIcon(nil)
return container.NewHBox(
avatar,
container.NewVBox(
nameLabel,
),
layout.NewSpacer(),
statusIcon,
)
},
// 修改后的列表项更新函数
func(item binding.DataItem, obj fyne.CanvasObject) {
// 安全类型断言
hbox, ok := obj.(*fyne.Container)
if !ok {
log.Println("错误:传入对象不是容器")
return
}
// 检查容器结构是否如预期
if len(hbox.Objects) < 4 {
log.Println("错误:容器子元素数量不足")
return
}
// 获取头像组件(直接类型断言)
avatar, ok := hbox.Objects[0].(*canvas.Image)
if !ok {
log.Println("错误:第一个子元素不是图像")
return
}
// 获取名称标签(通过嵌套容器)
nameContainer, ok := hbox.Objects[1].(*fyne.Container)
if !ok || len(nameContainer.Objects) == 0 {
log.Println("错误:名称容器无效")
return
}
nameLabel, ok := nameContainer.Objects[0].(*widget.Label)
if !ok {
log.Println("错误:名称标签无效")
return
}
// 获取状态图标
statusIcon, ok := hbox.Objects[3].(*widget.Icon)
if !ok {
log.Println("错误:状态图标无效")
return
}
// 获取店铺数据
val, err := item.(binding.Untyped).Get()
if err != nil {
log.Printf("获取数据失败: %v", err)
return
}
shop, ok := val.(ShopModel.Account)
if !ok {
log.Println("错误:数据类型不匹配")
return
}
// 在主线程中更新UI组件
fyne.DoAndWait(func() {
// 设置店铺名称
nameLabel.SetText(shop.AccountName)
// 设置状态图标
if shop.CanLogin {
statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "success"))
} else {
statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error"))
}
})
// 异步加载头像(使用原有loadShopAvatar函数)
go loadShopAvatar(avatar, shop.AccountAvatar)
},
)
// 添加点击事件 - 使用主线程更新UI
list.OnSelected = func(id widget.ListItemID) {
shop := appState.Shops[id]
// 在主线程更新状态栏
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName))
})
go func() {
// 加载商品数据
products, err := loadProductsForShop(shop)
if err != nil {
fyne.DoAndWait(func() {
appState.StatusBar.SetText("加载商品失败: " + err.Error())
})
return
}
// 在主线程更新UI
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products)))
addOrUpdateProductTab(appState, shop, products)
})
}()
}
return container.NewBorder(
widget.NewLabel("店铺列表"),
nil, nil, nil,
list,
)
}
// 为店铺加载商品数据
func loadProductsForShop(shop ShopModel.Account) ([]SkuModel.DataItem, error) {
// 获取店铺的Cookie信息
// cookieInfo, found := CookieModel.Api_find_by_subject_id(shop.SubjectID)
// if !found {
// return nil, fmt.Errorf("未找到店铺的Cookie信息")
// }
// 模拟API调用获取商品数据
time.Sleep(1 * time.Second) // 模拟网络延迟
// 这里应该是实际的API调用
// products, err := SkuModel.GetSkuList(shop.SubjectID, cookieInfo.Token, cookieInfo.VerifyFp)
// 模拟返回数据
return []SkuModel.DataItem{
{ProductID: "1001", Name: "商品A", MarketPrice: 999, DiscountPrice: 100},
{ProductID: "1002", Name: "商品B", MarketPrice: 1999, DiscountPrice: 50},
{ProductID: "1003", Name: "商品C", MarketPrice: 2999, DiscountPrice: 30},
}, nil
}
// 添加或更新商品TAB - 确保在主线程调用
func addOrUpdateProductTab(appState *AppState, shop ShopModel.Account, products []SkuModel.DataItem) {
tabTitle := shop.AccountName
// 检查是否已存在该TAB
for _, tab := range appState.ProductTabs.Items {
if tab.Text == tabTitle {
// 更新现有TAB
tab.Content = createProductList(products)
appState.ProductTabs.Refresh()
return
}
}
// 创建新TAB
newTab := container.NewTabItem(tabTitle, createProductList(products))
appState.ProductTabs.Append(newTab)
appState.ProductTabs.Select(newTab)
}
// 创建商品列表
func createProductList(products []SkuModel.DataItem) fyne.CanvasObject {
// 创建表格
table := widget.NewTable(
func() (int, int) {
return len(products) + 1, 4 // 行数=商品数+表头,列数=4
},
func() fyne.CanvasObject {
return widget.NewLabel("模板文本")
},
func(id widget.TableCellID, cell fyne.CanvasObject) {
label := cell.(*widget.Label)
if id.Row == 0 {
// 表头
switch id.Col {
case 0:
label.SetText("商品ID")
case 1:
label.SetText("商品名称")
case 2:
label.SetText("价格")
case 3:
label.SetText("库存")
}
label.TextStyle.Bold = true
return
}
// 数据行
product := products[id.Row-1]
switch id.Col {
case 0:
label.SetText(product.ProductID)
case 1:
label.SetText(product.Name)
case 2:
label.SetText(fmt.Sprintf("¥%.2f", product.MarketPrice/100))
case 3:
label.SetText(fmt.Sprintf("%d", product.MarketPrice))
}
},
)
// 设置列宽
table.SetColumnWidth(0, 100)
table.SetColumnWidth(1, 300)
table.SetColumnWidth(2, 100)
table.SetColumnWidth(3, 100)
return table
}
// 尝试自动登录 - 添加主线程UI更新
func tryAutoLogin(appState *AppState) {
// 获取所有用户
users := UserModel.Api_select_struct(nil)
if len(users) == 0 {
fyne.DoAndWait(func() {
appState.StatusBar.SetText("获取已经存在的账号为空")
})
return
}
// 尝试使用第一个用户自动登录
user := users[0]
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("尝试自动登录: %s...", user.LoginName))
})
// 更新登录表单
if appState.LoginForm != nil {
// 获取用户名输入框
usernameItem := appState.LoginForm.Items[0]
usernameEntry, ok := usernameItem.Widget.(*widget.Entry)
if !ok {
fyne.DoAndWait(func() {
appState.StatusBar.SetText("自动登录失败: 用户名控件类型错误")
})
return
}
// 获取密码输入框 - 这里使用 *widget.Entry 类型
passwordItem := appState.LoginForm.Items[1]
passwordEntry, ok := passwordItem.Widget.(*widget.Entry)
if !ok {
fyne.DoAndWait(func() {
appState.StatusBar.SetText("自动登录失败: 密码控件类型错误")
})
return
}
// 在主线程更新UI
fyne.DoAndWait(func() {
usernameEntry.SetText(user.LoginName)
passwordEntry.SetText(user.LoginPass)
appState.StatusBar.SetText("正在自动登录...")
})
// 触发登录
appState.LoginForm.OnSubmit()
}
}
```
## 问题分析与修复说明
### 问题原因
错误日志显示:
```
*** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] ***
```
这表明代码中存在在非主线程更新 UI 的问题。Fyne 要求所有 UI 更新必须在主线程执行,否则会导致未定义行为。
### 关键修复点
#### 1. 列表项更新函数中的 UI 操作
```go
// 在主线程中更新UI组件
fyne.DoAndWait(func() {
// 设置店铺名称
nameLabel.SetText(shop.AccountName)
// 设置状态图标
if shop.CanLogin {
statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "success"))
} else {
statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error"))
}
})
```
#### 2. 店铺列表点击事件处理
```go
list.OnSelected = func(id widget.ListItemID) {
shop := appState.Shops[id]
// 在主线程更新状态栏
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName))
})
go func() {
// ... 加载商品数据 ...
// 在主线程更新UI
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products)))
addOrUpdateProductTab(appState, shop, products)
})
}()
}
```
#### 3. 自动登录函数中的 UI 更新
```go
func tryAutoLogin(appState *AppState) {
// ... 获取用户数据 ...
fyne.DoAndWait(func() {
appState.StatusBar.SetText(fmt.Sprintf("尝试自动登录: %s...", user.LoginName))
})
// ... 更新登录表单 ...
// 在主线程更新UI
fyne.DoAndWait(func() {
usernameEntry.SetText(user.LoginName)
passwordEntry.SetText(user.LoginPass)
appState.StatusBar.SetText("正在自动登录...")
})
// ... 触发登录 ...
}
```
### 为什么使用 `fyne.DoAndWait`
1. **保证线程安全**:确保 UI 更新在主线程执行
2. **同步操作**:等待操作完成后再继续执行后续代码
3. **避免竞态条件**:防止多个 goroutine 同时修改 UI 状态
4. **兼容性**:适用于所有 Fyne 版本
### 需要特别注意的位置
1. **列表项更新函数**:所有对 UI 组件的操作都需要包裹在 `fyne.DoAndWait` 中
2. **异步回调**:任何在 goroutine 中更新 UI 的地方都需要使用 `fyne.DoAndWait`
3. **状态栏更新**:`appState.StatusBar.SetText()` 必须在主线程调用
4. **标签页操作**:添加或更新标签页需要在主线程执行
### 其他优化建议
1. **添加错误恢复机制**:
```go
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
}
}()
```
2. **优化图片加载**:
```go
// 添加图片缓存
var imageCache sync.Map
func loadShopAvatar(img *canvas.Image, url string) {
if url == "" {
// ... 默认头像 ...
return
}
// 检查缓存
if cached, ok := imageCache.Load(url); ok {
fyne.DoAndWait(func() {
img.Image = cached.(image.Image)
img.Refresh()
})
return
}
// ... 加载图片 ...
// 保存到缓存
imageCache.Store(url, imgData)
}
```
3. **添加加载指示器**:
```go
// 在加载数据时显示加载动画
func showLoadingIndicator(show bool) {
if show {
// 显示加载动画
} else {
// 隐藏加载动画
}
}
```
这些修改应该能解决所有关于非主线程更新 UI 的警告问题。
阅读全文
相关推荐
















