当前位置:   article > 正文

golang gui 那些事

golang gui

一. 前言

接触 go 需要已经有几年时间了,也写了不少项目功能,接触了各种各样的框架,这里就只说说 gui 图形界面相关的,踩过不少坑了,能填的填,不能填的就没有办法了,期待后面的不断优化更新,不断做到更好。

python 都有桌面程序了,于是就想用 go 写写桌面程序,搜了有不少开源的轮子。walk,webview,lurca这几个都用了用,接下来一个个说说。

二. Walk

1. 介绍

walk 项目源 "github.com/lxn/walk", 需要引入依赖 declarative,如果需要做一些订制化需求的话,还需要引入 win,自带了一些简单的样式,按钮,行列格式等,写成样式比较丑,主题内容订制比较难,下面附带个小栗子

2. 例子1: 原生 gui

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也使用蛮久的,附带一个小栗子吧。

3. 例子2: walk 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()
}

4. 编译使用

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

三. webview 

1. 介绍

walk webview 上面也说了,对于前端一些特殊字符无法使用,于是就接触了第二个 gui 框架,"github.com/polevpn/webview"

这个框架是一个分支,原来的是 "github.com/webview/webview", polevpn 的对于原来的做了一些优化,使用了后,也遇到了一些问题,设置标题时不支持中文(或许可以通过其他方法设置)

但是不能支持设置左上角图标,webview.SetIcon() 无法使用,官方作者给出了一种使用 win 的解决方案,不过我试了,不管用,可能自己有某些地方没有考虑到。接下来附带使用流程

2. 环境准备

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

3. 测试编译

1. 测试

go run main.go 可以直接打开 gui 进行调试

2. 编译

./build.bat, 会生成可执行文件直接运行。

四. lorca

1. 介绍

因为上面 webview 框架有一点点小问题,1. 无法设置图标;2. 标题不支持中文。

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

3. 测试编译

1. 测试

go run main.go

2. 编译

go build ldflags="-H windowsgui"

4. 问题

1. 提示 谷歌浏览器正在受控

mod 里找到  lurca 里的 ui.go,注释掉 "--enable-automation"

2. 登录提示是否记录密码,谷歌浏览器自带的

目前还没找到解决办法。

五. 总结

各个框架,都有各种优缺点,根据个人可容忍度去选择吧

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/blog/article/detail/43004
推荐阅读
相关标签
  

闽ICP备14008679号