当前位置: 首页 > news >正文

基于 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/initsgithub.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 指令将 assetslibs 目录嵌入到可执行文件中。这种方式可以避免在运行时依赖外部文件,使程序更加 portable( portable,便于移植)。

  • 初始化流程

    1. 调用 inits.Init(lib, resources),将嵌入的资源传递给 LCL 框架。
    2. 调用 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)
}

相关文章:

  • 考研操作系统-----外存文件
  • MATLAB图像处理:图像分割方法
  • 使用conda update python将python3.6更新到python3.7版本出现bug:
  • 计算机组成原理第二章
  • 如何将ubuntu下的一个目录,保存目录结构为一个git仓库并上传
  • Python大数据可视化:基于大数据技术的共享单车数据分析与辅助管理系统_flask+hadoop+spider
  • docker部署dify结合deepseek构建知识库
  • three.js+WebGL踩坑经验合集(8.1):用于解决z-fighting叠面问题的polygonOffset远没我们想象中那么简单
  • 基于2025Python电商商品评论数据采集与分析可视化系统
  • IT : 是工作還是嗜好? Delphi 30周年快乐!
  • 【Java】分布式锁Redis和Redisson
  • c++ gcc工具链
  • 缓存穿透、缓存击穿、缓存雪崩的区别与解决方案
  • 2025年-G4-Lc78--121. 买卖股票的最佳时机--(java版)
  • Blazor-设置组件焦点
  • Fisco-Bcos单群组区块链部署
  • Yuque-DL:一款强大的语雀资源下载工具
  • 003 注释
  • Chrome插件开发流程
  • 机试刷题_字符串的排列【python】
  • 阿坝州委书记徐芝文已任四川省政府党组成员
  • 学者的“好运气”:读本尼迪克特·安德森《椰壳碗外的人生》
  • 人民日报钟声:通过平等对话协商解决分歧的重要一步
  • 石家庄推动城市能级与民生福祉并进
  • 河南省平顶山市副市长许红兵主动投案,接受审查调查
  • 纽约大学朗格尼医学中心的转型带来哪些启示?