基于 energy (lcl v3.0) 跨平台GUI框架实现的 XTA SDK GUI 源码实现
项目开源地址
- energy v2.0 系列, LCL 原生控件, CEF
- lcl v3.0 v3.0 系列为, LCL 原生控件, CEF, Webview2, Webkit2
- XTA SDK
源码分析
本文主要介绍 xtaui.go
是一个基于 LCL 框架的 Go 语言 GUI 应用程序的主文件。
它通过嵌入资源、初始化框架、创建主窗体并运行应用,实现了完整的程序生命周期管理。
本文从资源嵌入、初始化、异常处理、主窗体创建等方面对源码进行了详细分析。
1. 文件结构和包声明
源码文件为 xtaui.go
,包声明为 main
,表示这是一个可执行的 Go 程序。
2. 导入的包
embed
: 用于嵌入静态资源文件。fmt
: 标准输出包,用于打印日志。github.com/energye/lcl/inits
和github.com/energye/lcl/lcl
: 这是 LCL(Light Controls Library)框架的包,用于创建跨平台的 GUI 应用。xta/xtaui/window
: 本地包,用于定义主窗口逻辑。
3. 嵌入资源
- 使用
go:embed
指令嵌入了两个目录:assets
: 用于存放静态资源(如图片、配置文件等)。libs
: 用于存放动态链接库(如平台依赖的二进制文件)。
4. 主函数 (main()
)
- 初始化资源: 调用
inits.Init(lib, resources)
,将嵌入的资源和库文件传递给 LCL 框架进行初始化。 - 初始化应用: 调用
lcl.Application.Initialize()
,初始化 LCL 框架。 - 设置异常处理: 通过
SetOnException
方法设置全局异常处理函数,捕获并打印错误信息。 - 设置主窗体: 调用
SetMainFormOnTaskBar(true)
,确保主窗体显示在任务栏中。 - 创建和运行主窗体: 通过
CreateForm
创建主窗体(MainWindow
),并调用Run()
方法启动应用。
源码概述
xtaui.go
是一个基于 LCL 框架的 Go 语言 GUI 应用程序的主文件。它通过嵌入静态资源和动态链接库,初始化 LCL 框架,并创建和运行主窗口。
资源嵌入与初始化
-
资源嵌入:
使用go:embed
指令将assets
和libs
目录嵌入到可执行文件中。这种方式可以避免在运行时依赖外部文件,使程序更加 portable( portable,便于移植)。 -
初始化流程:
- 调用
inits.Init(lib, resources)
,将嵌入的资源传递给 LCL 框架。 - 调用
lcl.Application.Initialize()
,完成 LCL 框架的初始化。
- 调用
异常处理机制
通过 SetOnException
方法,程序全局捕获异常并打印错误信息。这种机制可以有效避免程序因未捕获的异常而崩溃,同时为调试提供日志支持。
主窗体的创建与运行
-
创建主窗体:
调用lcl.Application.CreateForm(&window.MainWindow)
创建主窗体。MainWindow
是从本地包xta/xtaui/window
中导入的结构体,用于定义窗体的外观和行为。 -
运行应用:
调用lcl.Application.Run()
启动应用程序,进入消息循环,处理用户交互。
LCL 框架简介
LCL是一个跨平台的 GUI 开发框架,支持 Windows、Linux 和 macOS 操作系统。通过 LCL,开发者可以使用 Go 语言快速构建功能丰富的桌面应用。
示例截图
程序文件大小
执行文件: 15M
压缩后: 5M
截图
程序部分源码
main 入口
package main
import (
"embed"
"fmt"
"github.com/energye/lcl/inits"
"github.com/energye/lcl/lcl"
"xta/xtaui/window"
)
//go:embed assets
var resources embed.FS
//go:embed libs
var lib embed.FS
func main() {
inits.Init(lib, resources)
lcl.Application.Initialize()
lcl.Application.SetOnException(func(sender lcl.IObject, e lcl.IException) {
fmt.Println("Exception:", e.ToString())
})
lcl.Application.SetMainFormOnTaskBar(true)
lcl.Application.CreateForm(&window.MainWindow)
lcl.Application.Run()
}
程序主窗口
package window
import (
"bufio"
"errors"
"fmt"
"github.com/energye/lcl/lcl"
"github.com/energye/lcl/rtl"
"github.com/energye/lcl/types"
"github.com/energye/lcl/types/colors"
"github.com/energye/xta/chat"
"io/fs"
"os"
"time"
_ "xta/xtaui/syso"
)
type TMainWindow struct {
lcl.TForm
message lcl.IMemo
chat lcl.IMemo
ai chat.IGiteeAI
chatBtn lcl.IButton
selFileBtn lcl.IButton
selDirDlg lcl.ISelectDirectoryDialog
saveChatBtn lcl.IButton
saveDirDlg lcl.ISaveDialog
savePathInp lcl.IMemo
saveFileBuf *bufio.Writer
fileWindow []*FileWindow
title string
}
var MainWindow TMainWindow
func (m *TMainWindow) FormCreate(sender lcl.IObject) {
m.title = "ENERGY - XTA Chat UI"
m.SetCaption(m.title)
m.SetPosition(types.PoScreenCenter)
m.SetWidth(1024)
m.SetHeight(768)
m.SetShowHint(true)
m.Constraints().SetMinWidth(types.TConstraintSize(m.Width()))
m.Constraints().SetMinHeight(types.TConstraintSize(m.Height()))
png := lcl.NewPngImage()
png.LoadFromFSFile("assets/icon.png")
lcl.Application.Icon().Assign(png)
png.Free()
m.initMainBox()
}
func (m *TMainWindow) initMainBox() {
go m.initXTASDK()
openURL := lcl.NewLinkLabel(m)
openURL.SetParent(m)
openURL.SetCaption(`<a href="https://ai.gitee.com/models"> [Gitee AI API 获取] </a>`)
openURL.SetAlign(types.AlRight)
openURL.SetTop(5)
openURL.Font().SetSize(12)
openURL.SetOnLinkClick(func(sender lcl.IObject, link string, linktype types.TSysLinkType) {
rtl.SysOpen(link)
})
open1URL := lcl.NewLinkLabel(m)
open1URL.SetParent(m)
open1URL.SetCaption(`<a href="https://github.com/energye/xta"> [XTA AI SDK] </a>`)
open1URL.SetAlign(types.AlRight)
open1URL.SetTop(5)
open1URL.Font().SetSize(12)
open1URL.SetOnLinkClick(func(sender lcl.IObject, link string, linktype types.TSysLinkType) {
rtl.SysOpen(link)
})
modules := lcl.NewComboBox(m)
modules.SetParent(m)
modules.SetLeft(150)
modules.Items().AddStrings2(chat.GiteeAIModels())
modules.SetItemIndex(17)
modules.SetHeight(35)
modules.SetWidth(300)
modules.Font().SetSize(12)
modules.SetOnChange(func(sender lcl.IObject) {
module := chat.GiteeAIModelNameEnum(modules.Items().Strings(modules.ItemIndex()))
m.ai.SetModel(module)
m.message.Lines().Add("模型: " + m.ai.Model())
m.SetCaption(m.title + " " + m.ai.Model())
})
apiKey := lcl.NewEditButton(m)
apiKey.SetParent(m)
apiKey.SetLeft(modules.Left() + modules.Width() + 5)
apiKey.SetPasswordChar(uint16('*'))
apiKey.SetHeight(35)
apiKey.SetWidth(200)
apiKey.Font().SetSize(12)
apiKey.Button().SetCaption("API KEY")
//apiKey.Button().SetLeft(100)
apiKey.Button().SetWidth(80)
apiKey.SetOnClick(func(sender lcl.IObject) {
m.ai.Options().APIKey = apiKey.Text()
})
// 消息
m.message = lcl.NewMemo(m)
m.message.SetParent(m)
m.message.SetTop(40)
m.message.SetLeft(150)
m.message.SetWidth(m.Width() - 150)
m.message.SetHeight(m.Height() - 190)
m.message.SetBorderStyle(types.BsNone)
m.message.SetReadOnly(true)
m.message.SetScrollBars(types.SsAutoBoth)
m.message.SetWordWrap(true)
m.message.SetAnchors(types.NewSet(types.AkLeft, types.AkTop, types.AkRight, types.AkBottom))
// 聊天
m.chat = lcl.NewMemo(m)
m.chat.SetParent(m)
m.chat.SetBorderStyle(types.BsNone)
m.chat.SetScrollBars(types.SsAutoBoth)
m.chat.SetTop(m.message.Top() + m.message.Height() + 3)
m.chat.SetLeft(150)
m.chat.SetWidth(m.Width())
m.chat.SetHeight(100)
m.chat.SetAnchors(types.NewSet(types.AkLeft, types.AkRight, types.AkBottom))
chatLabel := lcl.NewLabel(m)
chatLabel.SetParent(m)
chatLabel.SetCaption("发送消息")
chatLabel.SetTop(m.chat.Top() + 40)
chatLabel.SetLeft(20)
chatLabel.Font().SetSize(18)
chatLabel.Font().SetColor(colors.ClGray)
chatLabel.SetAnchors(types.NewSet(types.AkLeft, types.AkBottom))
// 发送消息
m.chatBtn = lcl.NewButton(m)
m.chatBtn.SetParent(m)
m.chatBtn.SetTop(m.chat.Top() + m.chat.Height() + 3)
m.chatBtn.SetWidth(100)
m.chatBtn.SetHeight(45)
m.chatBtn.SetCaption("发 送")
m.chatBtn.Font().SetSize(16)
m.chatBtn.SetLeft(m.Width() - 100)
m.chatBtn.SetAnchors(types.NewSet(types.AkRight, types.AkBottom))
m.chatBtn.SetOnClick(m.SendMessage)
// 选择文件
m.selDirDlg = lcl.NewOpenDialog(m)
m.selDirDlg.SetOptions(m.selDirDlg.Options().Include(types.OfShowHelp, types.OfAllowMultiSelect))
m.selDirDlg.SetTitle("XTA - AI SDK 打开文件 多选")
m.selFileBtn = lcl.NewButton(m)
m.selFileBtn.SetParent(m)
m.selFileBtn.SetTop(m.chat.Top() + m.chat.Height() + 3)
m.selFileBtn.SetWidth(150)
m.selFileBtn.SetHeight(40)
m.selFileBtn.SetCaption("选择文件/多选")
m.selFileBtn.Font().SetSize(12)
m.selFileBtn.SetLeft(150)
m.selFileBtn.SetOnClick(m.selectFileOrDir)
m.selFileBtn.SetAnchors(types.NewSet(types.AkLeft, types.AkBottom))
// 保存消息
m.saveDirDlg = lcl.NewSaveDialog(m)
m.saveDirDlg.SetFilter("文本文件(*.txt)|*.txt|所有文件(*.*)|*.*")
m.saveDirDlg.SetTitle("XTA - AI SDK 消息保存")
m.saveChatBtn = lcl.NewButton(m)
m.saveChatBtn.SetParent(m)
m.saveChatBtn.SetTop(m.chat.Top() + m.chat.Height() + 3)
m.saveChatBtn.SetLeft(m.selFileBtn.Left() + m.selFileBtn.Width() + 3)
m.saveChatBtn.SetCaption("保存消息")
m.saveChatBtn.SetWidth(100)
m.saveChatBtn.SetHeight(40)
m.saveChatBtn.Font().SetSize(12)
m.saveChatBtn.SetAnchors(types.NewSet(types.AkLeft, types.AkBottom))
m.saveChatBtn.SetOnClick(func(sender lcl.IObject) {
if m.saveDirDlg.Execute() {
m.savePathInp.SetText(m.saveDirDlg.FileName())
}
})
m.savePathInp = lcl.NewMemo(m)
m.savePathInp.SetParent(m)
m.savePathInp.SetTop(m.chat.Top() + m.chat.Height() + 3)
m.savePathInp.SetLeft(m.saveChatBtn.Left() + m.saveChatBtn.Width() + 3)
m.savePathInp.SetHeight(40)
m.savePathInp.SetWidth(300)
m.savePathInp.Font().SetSize(15)
m.savePathInp.SetWordWrap(false)
m.savePathInp.SetAnchors(types.NewSet(types.AkLeft, types.AkBottom))
var savefile *os.File
m.savePathInp.SetOnChange(func(sender lcl.IObject) {
if savefile != nil {
savefile.Close()
savefile = nil
m.saveFileBuf = nil
}
path := m.savePathInp.Text()
fe, err := os.Open(path)
if err == nil {
defer fe.Close()
st, err := fe.Stat()
if err == nil {
if !st.IsDir() {
savefile, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
m.saveFileBuf = bufio.NewWriter(savefile)
}
}
}
} else if errors.Is(err, fs.ErrNotExist) {
savefile, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
m.saveFileBuf = bufio.NewWriter(savefile)
}
}
})
// 窗口显示
m.SetOnShow(func(sender lcl.IObject) {
m.chat.SetFocus()
apiKey.SetText(m.ai.APIKey())
})
//clearHistory := lcl.NewButton(m)
//clearHistory.SetParent(m)
//clearHistory.SetOnClick(func(sender lcl.IObject) {
// m.ai.History()
//})
}
// 主窗口左侧创建文件项
func (m *TMainWindow) createFileItem(filewindow *FileWindow) {
btn := lcl.NewButton(m)
btn.SetParent(m)
caption := filewindow.fileDesc
if caption == "" {
caption = filewindow.filenames
}
btn.SetCaption(caption)
btn.SetOnClick(func(sender lcl.IObject) {
m.removeFileBtn(filewindow.id)
})
btn.SetHint("点击删除该文件项")
filewindow.fileBtn = btn
m.fileWindow = append(m.fileWindow, filewindow)
m.resortFileBtns()
}
func (m *TMainWindow) removeFileBtn(id string) {
var newwindows []*FileWindow
for _, fw := range m.fileWindow {
if fw.id == id {
lcl.RunOnMainThreadAsync(func(id uint32) {
fw.fileBtn.Free()
fw.Close()
})
} else {
newwindows = append(newwindows, fw)
}
}
m.fileWindow = newwindows
m.resortFileBtns()
}
func (m *TMainWindow) resortFileBtns() {
for i, fw := range m.fileWindow {
fw.fileBtn.SetLeft(5)
fw.fileBtn.SetWidth(130)
fw.fileBtn.SetHeight(30)
fw.fileBtn.SetTop(int32(i*30) + 5)
}
}
func (m *TMainWindow) selectFileOrDir(sender lcl.IObject) {
if m.selDirDlg.Execute() {
files := m.selDirDlg.Files()
var file []string
for i := 0; i < int(files.Count()); i++ {
fmt.Println(files.ValueFromIndex(int32(i)))
file = append(file, files.ValueFromIndex(int32(i)))
}
win := createWindow(file, func(window *FileWindow) {
m.createFileItem(window)
})
win.Show()
}
}
func nowDatetime() string {
return time.Now().Format("2006-01-02 15:04:05")
}
XTA - AI SDK
package window
import (
"bytes"
"fmt"
"github.com/energye/lcl/lcl"
"github.com/energye/xta/chat"
"os"
"strings"
)
func (m *TMainWindow) initXTASDK() {
options := chat.DefaultGiteeAIOptions
options.APIKey = os.Getenv(chat.ENV_AI_API_KEY)
m.ai = chat.NewGiteeAI(options, false)
m.ai.System("【系统角色】你具备跨领域知识整合与结构化推理能力的智能助手。始终遵循:事实准确性 > 响应速度 > 表达流畅度的优先级原则。【响应规范】1. 解析阶段:识别问题类型(事实/观点/方法需求),标注关键信息置信度2. 处理阶段:- 事实类:提供最新权威信源+时间戳- 观点类:多视角分析+概率评估 - 方法类:分步实施框架+风险预案3. 输出阶段:采用「结论-依据-延伸」结构,技术概念附带白话解释【安全协议】- 对潜在争议内容自动附加免责声明- 医疗/法律建议必须提示咨询专业人士- 实时监测对话情感倾向,对焦虑/紧急表达启动安抚话术")
isFirstRec := false
m.ai.SetOnReceive(func(message *chat.TResponse) {
if !isFirstRec {
m.message.Lines().Add("回复: " + nowDatetime())
isFirstRec = true
}
// 在异步UI线程里操作
lcl.RunOnMainThreadAsync(func(id uint32) {
if message != nil {
if message.Error != "" {
s := fmt.Sprintf("错误: %v %v", message.Error, message.ErrorType)
m.message.Lines().Add(s)
if m.saveFileBuf != nil {
m.saveFileBuf.WriteString(s)
m.saveFileBuf.Flush()
}
}
choices := message.Choices
for _, choice := range choices {
if strings.Contains(choice.Delta.Content, "\n") {
m.message.Lines().Add(choice.Delta.Content)
} else {
m.message.SetSelStart(int32(len(m.message.Lines().Text())))
m.message.SetSelText(choice.Delta.Content)
}
if m.saveFileBuf != nil {
m.saveFileBuf.WriteString(choice.Delta.Content)
m.saveFileBuf.Flush()
}
}
} else {
fmt.Println("结束")
m.message.Lines().Add("")
m.chatBtn.SetEnabled(true)
isFirstRec = false
}
})
})
m.ai.SetOnFail(func(message *chat.TResponseError) {
lcl.RunOnMainThreadSync(func() {
s := fmt.Sprintf(" 错误: %v %v %v", message.Code, message.Message, message.Type)
m.message.Lines().Add(s)
m.chatBtn.SetEnabled(true)
isFirstRec = false
})
})
lcl.RunOnMainThreadSync(func() {
m.message.Lines().Add("XTA - AI SDK 初始化完成")
m.message.Lines().Add("模型: " + m.ai.Model())
//m.message.Lines().Add("APIKEY: ........." + m.ai.APIKey()[5:10] + "............")
m.SetCaption(m.title + " " + m.ai.Model())
})
}
func (m *TMainWindow) SendMessage(sender lcl.IObject) {
msg := m.chat.Lines().Text()
if msg != "" {
m.message.Lines().Add("我: " + nowDatetime())
m.message.Lines().Add(" " + msg)
buf := bytes.Buffer{}
buf.WriteString(msg + "\n")
for _, fw := range m.fileWindow {
if fw.isSend {
continue
}
fw.isSend = true
buf.WriteString(fw.text.Text() + "\n")
buf.WriteString(strings.Join(fw.fileContent, "\n"))
}
m.sendMessage(buf.String())
m.chat.SetText("")
}
}
func (m *TMainWindow) sendMessage(content string) {
// 在协程里操作
go m.ai.ChatStream(content)
m.chatBtn.SetEnabled(false)
}