赞
踩
接触 go 需要已经有几年时间了,也写了不少项目功能,接触了各种各样的框架,这里就只说说 gui 图形界面相关的,踩过不少坑了,能填的填,不能填的就没有办法了,期待后面的不断优化更新,不断做到更好。
python 都有桌面程序了,于是就想用 go 写写桌面程序,搜了有不少开源的轮子。walk,webview,lurca这几个都用了用,接下来一个个说说。
walk 项目源 "github.com/lxn/walk", 需要引入依赖 declarative,如果需要做一些订制化需求的话,还需要引入 win,自带了一些简单的样式,按钮,行列格式等,写成样式比较丑,主题内容订制比较难,下面附带个小栗子
package main import ( "fmt" "io/ioutil" "log" "os" "os/exec" "strings" "syscall" "time" "github.com/lxn/walk" . "github.com/lxn/walk/declarative" "github.com/lxn/win" ) var ( mw *MyMainWindow ) // 执行程序 func openExe(Filename string) { Filename = "\"" + Filename + "\"" fmt.Println(Filename) cmd := exec.Command("cmd.exe") cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf(`/c %s`, Filename), HideWindow: true} output, err := cmd.Output() fmt.Printf("output:\n%s\n", output) if err != nil { fmt.Printf("error: %+v\n", err) } } //自定义窗口 type MyMainWindow struct { *walk.MainWindow te *walk.TextEdit //listbox使用的数据 model *EnvModel //listbox控件 listBox *walk.ListBox } //环境变量条目数据模型 type EnvItem struct { //环境变量的名字和值 name string value string } //列表数据模型 type EnvModel struct { //继承ListModelBase walk.ListModelBase //环境变量数集合 items []EnvItem } //列表数据模型的工厂方法 func NewEnvModel() *EnvModel { env := os.Environ() m := &EnvModel{items: make([]EnvItem, len(env))} for i, e := range env { j := strings.Index(e, "=") if j == 0 { continue } name := e[0:j] value := strings.Replace(e[j+1:], ";", "\r\n", -1) m.items[i] = EnvItem{name, value} } return m } //定义列表项目的单击监听 func (mw *MyMainWindow) lb_CurrentIndexChanged() { i := mw.listBox.CurrentIndex() item := &mw.model.items[i] mw.te.SetText(item.value) fmt.Println("CurrentIndex: ", i) fmt.Println("CurrentEnvVarName: ", item.name) } //定义列表项目的双击监听 func (mw *MyMainWindow) lb_ItemActivated() { value := mw.model.items[mw.listBox.CurrentIndex()].value walk.MsgBox(mw, "Value", value, walk.MsgBoxIconInformation) } //列表的系统回调方法:获得listbox的数据长度 func (m *EnvModel) ItemCount() int { return len(m.items) } //列表的系统回调方法:根据序号获得数据 func (m *EnvModel) Value(index int) interface{} { return m.items[index].name } //显示消息窗口 func ShowMsgBox(title, msg string) int { return walk.MsgBox(mw, title, msg, walk.MsgBoxOK) } //一个普通的事件回调函数 func TiggerFunc(key, value string) { ShowMsgBox(key, value) } const ( SIZE_W = 800 SIZE_H = 650 ) func main() { f, _ := os.OpenFile("a.log", os.O_APPEND|os.O_CREATE, 07777) defer f.Close() logger := log.New(f, "[info]\t", log.Ltime) logger.Println("开始输出日志信息") defer func() { if err := recover(); err != nil { errMsg := fmt.Sprintf("%#v", err) logger.Println(errMsg) ioutil.WriteFile("fuck.log", []byte(errMsg), 0644) } }() mw = &MyMainWindow{model: NewEnvModel()} MainWindow{ Icon: Bind("'three.ico'"), AssignTo: &mw.MainWindow, Title: "程序列表", //窗口菜单 MenuItems: []MenuItem{}, //工具栏 ToolBar: ToolBar{ //按钮风格:图片在字的前面 ButtonStyle: ToolBarButtonImageBeforeText, //工具栏中的工具按钮 Items: []MenuItem{ //自带子菜单的工具按钮 Menu{ //工具按钮本身的图文和监听 Text: "聊天工具", Image: "img/document-properties.png", //附带一个子菜单 Items: []MenuItem{ Action{ Text: "QQ", OnTriggered: func() { go openExe("E:\\apps\\qq\\Tencent\\QQ\\Bin\\QQScLauncher.exe") }, }, Action{ Text: "微信", OnTriggered: func() { go openExe("C:\\Program Files (x86)\\Tencent\\WeChat\\WeChat.exe") }, }, }, }, Separator{}, Menu{ //工具按钮本身的图文和监听 Text: "开发工具", Image: "img/document-properties.png", //附带一个子菜单 Items: []MenuItem{ Action{ Text: "vscode", OnTriggered: func() { go openExe("D:\\apps\\vscode\\Microsoft VS Code\\Code.exe") }, }, Action{ Text: "nginx", OnTriggered: func() { cmd := exec.Command("cmd.exe", "/C", "E:/apps/nginx-1.20.2/nginx.exe -p E:/apps/nginx-1.20.2/ -c E:/apps/nginx-1.20.2/conf/nginx.conf -e E:/apps/nginx-1.20.2/logs/error.log &") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr println(cmd.Run()) }, }, }, }, Separator{}, Menu{ //工具按钮本身的图文和监听 Text: "运维工具", Image: "img/document-properties.png", //附带一个子菜单 Items: []MenuItem{ Action{ Text: "xshell", OnTriggered: func() { go openExe("D:\\apps\\xshell7\\Xshell.exe") }, }, }, }, Separator{}, Menu{ //工具按钮本身的图文和监听 Text: "连接工具", Image: "img/document-properties.png", //附带一个子菜单 Items: []MenuItem{ Action{ Text: "向日葵", OnTriggered: func() { go openExe("E:\\apps\\xrk\\SunloginClient\\SunloginClient.exe") }, }, }, }, Separator{}, Menu{ //工具按钮本身的图文和监听 Text: "其他工具", Image: "img/document-properties.png", //附带一个子菜单 Items: []MenuItem{ Action{ Text: "谷歌", OnTriggered: func() { go openExe("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe") }, }, Action{ Text: "WPS", OnTriggered: func() { go openExe("E:\\apps\\wps\\WPS Office\\ksolaunch.exe") }, }, Action{ Text: "迅雷", OnTriggered: func() { go openExe("D:\\apps\\xunlei\\Thunder\\Program\\Thunder.exe") }, }, }, }, Separator{}, }, }, // MinSize: Size{600, 400}, Size: Size{600, 400}, Layout: VBox{}, //控件们 Children: []Widget{ //水平局部 HSplitter{ MinSize: Size{600, 300}, Children: []Widget{ ListBox{ StretchFactor: 1, //赋值给myWindow.listBox AssignTo: &mw.listBox, //要显示的数据 Model: mw.model, //单击监听 OnCurrentIndexChanged: mw.lb_CurrentIndexChanged, //双击监听 OnItemActivated: mw.lb_ItemActivated, }, TextEdit{ StretchFactor: 1, AssignTo: &mw.te, ReadOnly: true, }, }, }, HSplitter{ MaxSize: Size{600, 50}, Children: []Widget{ //图像 ImageView{ Background: SolidColorBrush{Color: walk.RGB(255, 191, 0)}, //图片文件位置 Image: "img/clock.jpg", //和四周的边距 Margin: 5, //定义最大拉伸尺寸 MinSize: Size{50, 50}, //显示模式 Mode: ImageViewModeZoom, }, //按钮 PushButton{ StretchFactor: 8, Text: "时间提醒", OnClicked: func() { currentTime := time.Now() ShowMsgBox("时间提醒", currentTime.Format("2006.01.02 15:04:05")) }, }, }, }, }, }.Create() // win.SetWindowLong(mw.Handle(), win.GWL_STYLE, win.WS_EX_CONTEXTHELP) // removes default styling xScreen := win.GetSystemMetrics(win.SM_CXSCREEN) yScreen := win.GetSystemMetrics(win.SM_CYSCREEN) win.SetWindowPos( mw.Handle(), 0, (xScreen-SIZE_W)/2, (yScreen-SIZE_H)/2, SIZE_W, SIZE_H, win.SWP_FRAMECHANGED, ) win.ShowWindow(mw.Handle(), win.SW_SHOW) mw.Run() }
接下来想要优化下页面,发现 walk 框架中自带了一个 webview 功能,这个功能可以内嵌一个网站,可以配合原声html,js,css,jquery等使用更复杂的页面功能,但是使用过程中也遇到了一些坑,因为没有使用前后端分离,使用go gin框架做web驱动,walk 的webview对于html中一些特殊字符无法识别,会报错弹窗,比如反引号,更无法使用一些前端框架,比如 vue element ui等有些好看样式的框架。无法渲染出来,对于网站中一些复杂的样式功能,纯原声自己去写还是有些麻烦的,不过这个webview也使用蛮久的,附带一个小栗子吧。
package main import ( "github.com/lxn/walk" . "github.com/lxn/walk/declarative" "github.com/lxn/win" ) const ( SIZE_W = 800 SIZE_H = 600 ) type MyMainWindow struct { *walk.MainWindow } func main() { var le *walk.LineEdit var wv *walk.WebView mw := new(MyMainWindow) MainWindow{ Visible: false, AssignTo: &mw.MainWindow, Icon: Bind("'three.ico'"), Title: "Walk WebView Example'", MinSize: Size{SIZE_W, SIZE_H}, Layout: VBox{MarginsZero: true}, Children: []Widget{ LineEdit{ AssignTo: &le, Text: Bind("wv.URL"), OnKeyDown: func(key walk.Key) { if key == walk.KeyReturn { wv.SetURL(le.Text()) } }, }, WebView{ AssignTo: &wv, Name: "wv", URL: "https://github.com/lxn/walk", }, }, }.Create() win.SetWindowLong(mw.Handle(), win.GWL_STYLE, win.WS_BORDER) // removes default styling xScreen := win.GetSystemMetrics(win.SM_CXSCREEN) yScreen := win.GetSystemMetrics(win.SM_CYSCREEN) win.SetWindowPos( mw.Handle(), 0, (xScreen-SIZE_W)/2, (yScreen-SIZE_H)/2, SIZE_W, SIZE_H, win.SWP_FRAMECHANGED, ) win.ShowWindow(mw.Handle(), win.SW_SHOW) mw.Run() }
go walk编译并且带着图标的方法比较简单,首先必须要有一个manifest 文件,然后
1. 准备 main.manifest 文件
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/> </dependentAssembly> </dependency> <asmv3:application> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> <dpiAware>true</dpiAware> </asmv3:windowsSettings> </asmv3:application> </assembly>
2.安装 rsrc 工具,加入到 path 路径
go get github.com/akavel/rsrc cd xxx/rsrc go install
3. 生成项目可执行文件
rsrc.exe -manifest main.manifest -ico main.ico -o main.syso go build -ldflags="-H windowsgui" -o localJob.exe
walk webview 上面也说了,对于前端一些特殊字符无法使用,于是就接触了第二个 gui 框架,"github.com/polevpn/webview"
这个框架是一个分支,原来的是 "github.com/webview/webview", polevpn 的对于原来的做了一些优化,使用了后,也遇到了一些问题,设置标题时不支持中文(或许可以通过其他方法设置)
但是不能支持设置左上角图标,webview.SetIcon() 无法使用,官方作者给出了一种使用 win 的解决方案,不过我试了,不管用,可能自己有某些地方没有考虑到。接下来附带使用流程
1. 只能 windows 平台使用,并且依赖 webview2,调用本地微软浏览器
2. 准备依赖文件
WebView2Loader.dll WebView2Loader.dll.lib WebView2LoaderStatic.lib
3. 准备 version_template.h
#pragma once #define VER_MAJOR 1 #define VER_MINOR 0 #define VER_PATCH 1
4. 准备 make_version.bat
@ECHO OFF cd /d %1 if exist %2 del /q %2 for /f "delims=" %%i in ('git rev-list --count HEAD') do (set REVISION=%%i) for /f "delims=" %%i in ('git rev-parse --short HEAD') do (set REVISION_HASH=%%i) if "%REVISION%" == "" ( set REVISION=0 ) (echo #define VER_REVISION %REVISION% && echo #define VER_REVISION_HASH %REVISION_HASH%) > %2
5. 准备 build.bat
@ECHO OFF cd /d %1 if exist %2 del /q %2 for /f "delims=" %%i in ('git rev-list --count HEAD') do (set REVISION=%%i) for /f "delims=" %%i in ('git rev-parse --short HEAD') do (set REVISION_HASH=%%i) if "%REVISION%" == "" ( set REVISION=0 ) (echo #define VER_REVISION %REVISION% && echo #define VER_REVISION_HASH %REVISION_HASH%) > %2
6. 准备 main 文件
package main import ( "fmt" "github.com/polevpn/webview" ) func run() { w := webview.New(800, 600, false, true) defer w.Destroy() w.SetTitle("baidu") w.SetSize(800, 600, webview.HintNone) w.Navigate("http://www.baidu.com") w.Run() }
1. 测试
go run main.go 可以直接打开 gui 进行调试
2. 编译
./build.bat, 会生成可执行文件直接运行。
因为上面 webview 框架有一点点小问题,1. 无法设置图标;2. 标题不支持中文。
package main import ( "log" "github.com/zserge/lorca" ) func main() { ui, err := lorca.New("http://www.baidu.com", "", 800, 600) if err != nil { log.Fatal(err) } ui.Eval(` //alert('1'); `) defer ui.Close() <-ui.Done() }
1. 测试
go run main.go
2. 编译
go build ldflags="-H windowsgui"
1. 提示 谷歌浏览器正在受控
mod 里找到 lurca 里的 ui.go,注释掉 "--enable-automation"
2. 登录提示是否记录密码,谷歌浏览器自带的
目前还没找到解决办法。
各个框架,都有各种优缺点,根据个人可容忍度去选择吧
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。