webview
go桌面端开发的基石,了解webview的使用,对后续wails框架的学习有很大的帮助
在桌面端开发中,WebView 是一种特殊的组件或控件,它允许在桌面应用程序中嵌入和显示网页内容(HTML、CSS、JavaScript 等),相当于在桌面应用里内置了一个轻量级的浏览器引擎
go get github.com/webview/webview_go
基本示例
package main
import (
webview "github.com/webview/webview_go"
)
func main() {
w := webview.New(false) // 如果是true 就可以打开f12
defer w.Destroy()
w.SetTitle("枫枫知道")
w.SetSize(1200, 600, webview.HintNone)
w.Navigate("https://www.fengfengzhidao.com")
w.Run()
}
刚开始运行可能会报错
github.com/webview/webview_go: build constraints exclude all Go files in
// 开启cgo就好
go env -w CGO_ENABLED=1
cgo: C compiler “gcc” not found: exec: “gcc”: executable file not found in %PATH%
win下用Go语言的cgo时需要用到GCC编译器,windows下需要安装MinGW
下载链接 https://github.com/niXman/mingw-builds-binaries/releases
然后将mingw64的bin目录加到环境变量里面去,然后重启项目
应该就可以正常运行了
事件绑定
上面我们是使用w.Navigate直接打开一个目标地址
也可以使用SetHtml,这样就可以在html里面调后端方法了
js → go
在js中执行go的函数
package main
import (
"fmt"
webview "github.com/webview/webview_go"
"time"
)
func main() {
w := webview.New(true) // 如果是true 就可以打开f12
defer w.Destroy()
w.SetTitle("枫枫知道")
w.SetSize(1200, 600, webview.HintNone)
w.SetHtml(`
<h1>hello</h1>
<button onclick="showDate()">show Date 点我</button>
<button onclick="add(1, 2)">add 点我</button>
<button onclick="gu()">getUser 点我</button>
<script>
async function gu(){
const u = await getUser()
alert("用户id=" + u)
}
</script>
`)
w.Bind("showDate", func() {
fmt.Println(time.Now().Format(time.DateTime))
})
w.Bind("add", func(n1, n2 int) {
fmt.Println("add: ", n1, n2)
})
w.Bind("getUser", func() string {
userID := "xxx001"
fmt.Println("getUser", userID)
return userID
})
w.Run()
}
go → js
在go中执行js的函数
js部分
<h1>hello</h1>
<div id="result"></div>
<script>
function showAlert(){
alert("hello")
}
function showText(text){
document.getElementById("result").innerText = text
}
</script>
go部分
如果执行的操作在协程中,一定要把函数放到Dispatch里面去
然后可以使用embed嵌入的方式,把html代码打入go程序中
package main
import (
_ "embed"
webview "github.com/webview/webview_go"
"time"
)
//go:embed index.html
var html string
func main() {
w := webview.New(true) // 如果是true 就可以打开f12
defer w.Destroy()
w.SetTitle("枫枫知道")
w.SetSize(1200, 600, webview.HintNone)
w.SetHtml(html)
go func() {
time.Sleep(2 * time.Second)
w.Dispatch(func() {
w.Eval("showAlert()")
w.Eval("showText('这是go传递来的数据')")
})
}()
w.Run()
}
一定要注意一个误区,很多人把js那一部分叫前端,把go那部分叫后端,在桌面端开发中这样说不太严谨,严谨的说法,只要是在桌面环境中的代码,比如上面的js部分,go代码部分,都叫桌面端,或者叫前端,到现在还没有引入后端
wails
webview了解个大概就ok了,真要做一点像样的桌面端产品
还是需要使用wails这样的桌面端开发框架
Wails是一个开源项目,旨在让开发者能够使用Go和Web技术(如React、Vue等)来构建桌面应用。它提供了一种轻量级且高效的解决方案,相比传统的Electron框架,Wails构建的应用具有更小的体积和更快的启动速度。
wails官网 https://wails.io/zh-Hans/docs/gettingstarted/installation/
安装
go install github.com/wailsapp/wails/v2/cmd/wails@latest
创建项目,以vue3+ts为例
wails init -n wails_study -t vue-ts
项目结构
.
├── build/
│ ├── appicon.png // 应用程序图标
│ ├── darwin/
│ └── windows/
├── frontend/ // 前端项目文件
├── go.mod
├── go.sum
├── main.go // 主应用
└── wails.json // 项目配置
使用wails dev就可以运行项目了
前端和go部分代码修改之后,会自动重启运行
js到go方法调用
go新增一个方法
func (a *App) Add(n1, n2 int) int {
return n1 + n2
}
新增之后,wails会自动变更
js部分使用
import {Greet, Add} from '../../wailsjs/go/main/App'
async function add(){
const res = await Add(1, 2)
data.addResult = `结果是 ${res}`
}
<button class="btn" @click="add">add</button>
还可以通过Event事件实现js到go的方法调用
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
runtime.EventsOn(a.ctx, "user-click", func(events ...any) {
fmt.Println("收到点击事件,数据:", events)
})
}
import {EventsEmit} from "../../wailsjs/runtime";
EventsEmit("user-click", "xxxx", "xxx")
go到js的方法调用
通过事件注册机制
func (a *App) CallJavaScript() {
// 发送事件到前端,附带参数
runtime.EventsEmit(a.ctx, "fromGo", "hello from go")
}
import {EventsOn} from "../../wailsjs/runtime";
EventsOn("fromGo", (data)=>{
console.log("go消息", data)
})
方法绑定和事件注册怎么选
- 如果是同步调用,选择绑定方法
- 如果是异步调用,选择事件注册
菜单
func (a *App) getMenu() *menu.Menu {
m := menu.NewMenu()
fileMenu := m.AddSubmenu("文件")
fileMenu.AddText("打开文件", keys.Control("o"), func(data *menu.CallbackData) {
fmt.Println("打开文件")
})
fileMenu.AddText("保存文件", &keys.Accelerator{Key: "s", Modifiers: []keys.Modifier{
keys.ControlKey,
keys.ShiftKey,
}}, func(data *menu.CallbackData) {
fmt.Println("保存文件")
})
fileMenu.AddSeparator()
fileMenu.AddText("退出", &keys.Accelerator{}, func(data *menu.CallbackData) {
fmt.Println("退出")
})
moreMenu := m.AddSubmenu("更多")
moreMenu.AddText("关于", &keys.Accelerator{}, func(data *menu.CallbackData) {
fmt.Println("关于")
})
return m
}
菜单综合练习
- 文件操作
- 剪贴板操作
- 全屏操作
- 刷新
- 窗口大小操作
- 置顶
- 跳链接
- 跳路由
文件操作
func (a *App) getFileMenu() *menu.Menu {
m := menu.NewMenu()
fileMenu := m.AddSubmenu("文件")
fileMenu.AddText("打开文件", &keys.Accelerator{}, func(data *menu.CallbackData) {
fmt.Println("打开文件")
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "枫枫要选择文件了",
Filters: []runtime.FileFilter{
{
DisplayName: "Image Files (*.jpg, *.png)",
Pattern: "*.jpg;*.png",
},
},
})
fmt.Println(filePath, err)
})
fileMenu.AddText("保存文件", &keys.Accelerator{}, func(data *menu.CallbackData) {
fmt.Println("保存文件")
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "枫枫要保存文件了",
DefaultDirectory: "E:\\IT\\go_pro\\webview_study\\wails_study",
Filters: []runtime.FileFilter{
{
DisplayName: "Text Files (*.txt)",
Pattern: "*.txt",
},
},
})
fmt.Println(filePath, err)
err = os.WriteFile(filePath, []byte("hello"), 0644)
fmt.Println(err)
})
return m
}
剪贴板操作
func (a *App) getClipboardMenu() *menu.Menu {
m := menu.NewMenu()
clipboardMenu := m.AddSubmenu("剪贴板")
clipboardMenu.AddText("复制", keys.Control("c"), func(data *menu.CallbackData) {
runtime.ClipboardSetText(a.ctx, "设置当前时间:"+time.Now().Format(time.DateTime))
})
clipboardMenu.AddText("粘贴", keys.Control("c"), func(data *menu.CallbackData) {
text, err := runtime.ClipboardGetText(a.ctx)
fmt.Println(text, err)
})
return m
}
全屏操作
func (a *App) getScreenMenu() *menu.Menu {
m := menu.NewMenu()
screenMenu := m.AddSubmenu("屏幕")
screenMenu.AddCheckbox("全屏", false, keys.Key("f11"), func(data *menu.CallbackData) {
if runtime.WindowIsFullscreen(a.ctx) {
runtime.WindowUnfullscreen(a.ctx)
screenMenu.Items[0].SetChecked(false)
} else {
runtime.WindowFullscreen(a.ctx)
screenMenu.Items[0].SetChecked(true)
}
})
return m
}
窗口操作
- 刷新
- 显示、隐藏
- 置顶
- 执行js
func (a *App) getWindowMenu() *menu.Menu {
m := menu.NewMenu()
windowMenu := m.AddSubmenu("窗口")
windowMenu.AddText("刷新js", keys.Key("f5"), func(data *menu.CallbackData) {
fmt.Println("刷新")
runtime.WindowReload(a.ctx)
})
windowMenu.AddText("刷新应用", keys.Control("f5"), func(data *menu.CallbackData) {
fmt.Println("刷新应用")
runtime.WindowReloadApp(a.ctx)
})
windowMenu.AddText("隐藏", &keys.Accelerator{}, func(data *menu.CallbackData) {
fmt.Println("隐藏")
runtime.WindowHide(a.ctx)
go func() {
time.Sleep(2 * time.Second)
runtime.WindowShow(a.ctx)
}()
})
var isTop bool
windowMenu.AddCheckbox("置顶", false, keys.Key("f10"), func(data *menu.CallbackData) {
isTop = !isTop
runtime.WindowSetAlwaysOnTop(a.ctx, isTop)
windowMenu.Items[3].SetChecked(isTop)
})
windowMenu.AddText("执行js", &keys.Accelerator{}, func(data *menu.CallbackData) {
fmt.Println("执行js")
runtime.WindowExecJS(a.ctx, "alert('hello')")
})
return m
}
应用跳转
跳转地址
runtime.BrowserOpenURL(a.ctx, "https://www.fengfengzhidao.com")
跳转其他应用
// 跳转qq聊天
runtime.BrowserOpenURL(a.ctx, "tencent://message/?uin=qq号&Site=qq&Menu=yes")
// 跳转微信主界面
runtime.BrowserOpenURL(a.ctx, "weixin://")
路由
先下载vue-router
npm i vue-router
在src下创建router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";
export const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
name: "index",
component: ()=>import("../views/index.vue"),
},
{
path: "/home",
name: "home",
component: ()=>import("../views/home.vue"),
},
{
path: "/about",
name: "about",
component: ()=>import("../views/about.vue"),
}
],
});
然后创建对应的视图
然后在App.vue中修改
<script lang="ts" setup>
</script>
<template>
<div class="nav">
<router-link to="/">首页</router-link>
<router-link to="/home">home</router-link>
<router-link to="/about">关于</router-link>
</div>
<div class="view">
<router-view/>
</div>
</template>
在main.ts中引入router
import {createApp} from 'vue'
import App from './App.vue'
import {router} from "./router";
createApp(App).use(router).mount('#app')
在菜单中跳转路由
因为wails的v2版本不能实现新开窗口,新开标签页的功能
所以我们需要曲线救国,把一些配置项通过web页面写出来,然后通过菜单跳转路由的方式
func (a *App) getRouterMenu() *menu.Menu {
m := menu.NewMenu()
routerMenu := m.AddSubmenu("路由")
routerMenu.AddText("设置", &keys.Accelerator{}, func(data *menu.CallbackData) {
runtime.EventsEmit(a.ctx, "router", "settings")
})
return m
}
在前端里面监听事件,然后跳路由即可
import {EventsOn} from "../wailsjs/runtime";
import {router} from "./router";
EventsOn("router", function (name) {
router.push({name})
})
快捷键
程序激活的时候,菜单上的快捷键就可以正常使用
但是要想实现程序未激活的时候使用快捷键,就得借助全局快捷键了
https://github.com/makenowjust/hotkey
package main
import (
"fmt"
"github.com/MakeNowJust/hotkey"
)
func main() {
hkey := hotkey.New()
quit := make(chan bool)
hkey.Register(hotkey.Ctrl, 'Q', func() {
fmt.Println("Quit")
quit <- true
})
fmt.Println("Start hotkey's loop")
fmt.Println("Push Ctrl-Q to escape and quit")
<-quit
}
上面那个库只能实现两个按键的快捷键,不过用起来很简单
如果要实现多个按键组合按键,就得使用golang.design/x/hotkey这个库了
https://github.com/golang-design/hotkey
package main
import (
"log"
"golang.design/x/hotkey"
"golang.design/x/hotkey/mainthread"
)
func main() {
mainthread.Init(fn)
}
func fn() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
err := hk.Register()
if err != nil {
log.Fatalf("hotkey: failed to register hotkey: %v", err)
return
}
log.Printf("hotkey: %v is registered\n", hk)
<-hk.Keydown()
log.Printf("hotkey: %v is down\n", hk)
<-hk.Keyup()
log.Printf("hotkey: %v is up\n", hk)
hk.Unregister()
log.Printf("hotkey: %v is unregistered\n", hk)
}
快捷键与wails结合
package main
import (
"fmt"
"github.com/MakeNowJust/hotkey"
"github.com/wailsapp/wails/v2/pkg/runtime"
hk1 "golang.design/x/hotkey"
"golang.design/x/hotkey/mainthread"
)
func (a *App) hotKey() {
hkey := hotkey.New()
hkey.Register(hotkey.Ctrl, 'J', func() {
text, err := runtime.ClipboardGetText(a.ctx)
fmt.Println("全局快捷键触发 剪贴板的数据:", text, err)
})
mainthread.Init(func() {
hk := hk1.New([]hk1.Modifier{hk1.ModCtrl, hk1.ModShift}, hk1.KeyQ)
err := hk.Register()
if err != nil {
fmt.Println(err)
return
}
<-hk.Keydown()
<-hk.Keyup()
fmt.Println("全局快捷键触发 退出程序")
hk.Unregister()
runtime.Quit(a.ctx)
})
}
在app的startup方法中调用
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.hotKey()
}
系统托盘
https://github.com/energye/systray

package main
import (
_ "embed"
"fmt"
"github.com/energye/systray"
)
func main() {
fmt.Println("运行中")
systray.Run(onReady, onExit)
}
//go:embed icon.ico
var homeIcon []byte
//go:embed favicon.ico
var i1 []byte
//go:embed app.ico
var i2 []byte
func onReady() {
systray.SetIcon(homeIcon)
systray.SetTitle("Awesome App")
systray.SetTooltip("Pretty awesome超级棒")
systray.SetOnClick(func(menu systray.IMenu) {
fmt.Println("单击")
})
systray.SetOnRClick(func(menu systray.IMenu) {
menu.ShowMenu()
})
menu1 := systray.AddMenuItem("打开主界面", "")
menu1.SetIcon(i1)
menu1.Click(func() {
fmt.Println("打开主界面")
})
menu2 := systray.AddMenuItem("基本设置", "")
menu2.SetIcon(i2)
menu2.Click(func() {
fmt.Println("基本设置")
})
systray.AddMenuItem("退出", "").Click(func() {
onExit()
})
}
func onExit() {
// clean up here
systray.Quit()
}
系统托盘和wails结合
package main
import (
_ "embed"
"fmt"
"github.com/energye/systray"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
func (a *App) systray() {
systray.Run(a.onReady, a.onExit)
}
//go:embed testdata/icon.ico
var homeIcon []byte
func (a *App) onReady() {
systray.SetIcon(homeIcon)
systray.SetTitle("Awesome App")
systray.SetTooltip("Pretty awesome超级棒")
systray.SetOnClick(func(menu systray.IMenu) {
fmt.Println("单击")
})
systray.SetOnRClick(func(menu systray.IMenu) {
menu.ShowMenu()
})
menu1 := systray.AddMenuItem("打开主界面", "")
menu1.Click(func() {
fmt.Println("打开主界面")
runtime.Show(a.ctx)
})
menu2 := systray.AddMenuItem("隐藏", "")
menu2.Click(func() {
fmt.Println("隐藏")
runtime.Hide(a.ctx)
})
systray.AddMenuItem("退出", "").Click(func() {
a.onExit()
})
}
func (a *App) onExit() {
// clean up here
systray.Quit()
runtime.Quit(a.ctx)
}
系统提示

可以使用notify这个库做简单通知
package main
import (
"github.com/martinlindhe/notify"
)
func main() {
notify.Alert("APP", "title", "msg", "D:\IT\fengfeng\test\wails_study\build\appicon.png")
}
如果有中文的话,显示会乱码,因为在windows中使用的编码是gbk
package main
import (
"github.com/martinlindhe/notify"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// 将 UTF-8 字符串转换为 GBK 编码(适用于 Windows)
func toGBK(s string) string {
result, _, err := transform.String(simplifiedchinese.GBK.NewEncoder(), s)
if err != nil {
return s // 转换失败时返回原字符串
}
return result
}
func main() {
// 对中文内容进行编码转换
title := toGBK("收到一条消息")
message := toGBK("有人给你发消息了")
appName := toGBK("APP")
notify.Alert(appName, title, message, "")
}
如果是要做一些复杂的通知,比如可以点击通知消息,那可以使用toast.v1这个库
package main
import (
"fmt"
"gopkg.in/toast.v1"
"log"
)
func main() {
notification := toast.Notification{
AppID: "fengfengzhidao",
Title: "你收到一条消息",
Message: "有人给你点赞了",
Icon: "D:\\IT\\fengfeng\\test\\wails_study\\build\\appicon.png",
Actions: []toast.Action{
{
Type: "protocol", // 协议类型
Label: "查看详情", // 按钮文字
Arguments: "https://www.fengfengzhidao.com", // 点击后打开该网页
},
},
}
err := notification.Push()
if err != nil {
log.Fatalln(err)
}
fmt.Println(err)
}
有时候会不通知,看看是不是图标路径不对、字符串编码不对
无边框
有时候系统自带的菜单和操作逻辑很难做的很酷炫
比如菜单上还有输入框,显示用户头像这些,原生系统菜单几乎是做不出来的
所以可以直接把标题栏和菜单去掉,直接通过web的形式实现边框
那么就需要实现几个逻辑
- 窗口拖动
- 窗口最小化、窗口关闭

项目开启无边框
err := wails.Run(&options.App{
Frameless: true,
})
前端部分设置
<script lang="ts" setup>
import {WindowMinimise, WindowMaximise, Quit} from "../wailsjs/runtime";
</script>
<template>
<div class="nav" style="--wails-draggable:drag" >
导航栏
<div class="action">
<span @click="WindowMinimise">-</span>
<span @click="WindowMaximise">□</span>
<span @click="Quit">x</span>
</div>
</div>
<div class="view">
<router-view></router-view>
</div>
</template>
<style>
body{
margin: 0;
}
.nav{
background-color: #333333;
display: flex;
justify-content: center;
align-items: center;
height: 40px;
color: white;
position: relative;
.action{
position: absolute;
right: 20px;
font-size: 14px;
span{
margin-left: 15px;
cursor: pointer;
}
}
}
</style>
模板
我们之前的创建项目的命令,前端部分没有路由,没有pinia,没有UI库,没有菜单
写复杂的项目的时候,每次都需要我们自己去把这些东西配好,很麻烦
wails init -n wails_study -t vue-ts
可以用github上wails的第三方模板
https://wails.io/zh-Hans/docs/community/templates
可以在这上面选择你喜欢的模板
wails init -n "Your Project Name" -t https://github.com/misitebao/wails-template-vue
也可以自己按照项目库的结构,做一个自己项目需要的模板
比如我可能会需要 一个UI组件库,pinia,axios,并且配置好代理
wails init -n "Project Name" -t https://github.com/fengfengzhidao/wails-template-arcodesign-ts
如果拉取失败的话,可以设置一下代理
set http_proxy=http://127.0.0.1:7890
set https_proxy=http://127.0.0.1:7890
set http_proxy=
set https_proxy=
wails v3版本
最大的两个升级就是
- 支持多窗口了
- 集成了系统托盘
截至目前还是alpha版本
文档: https://v3alpha.wails.io/whats-new/#multiple-windows
安装
go install -v github.com/wailsapp/wails/v3/cmd/wails3@latest
本来想深入讲的,但是我发现v3.0.0-alpha.36的很多小版本,api有很大的变化
并且操作逻辑和api也和v2变化很大
所以还是等v3版本正式发布之后,再做深入讲解的视频
桌面端项目开发
开发一个备忘录的桌面端软件吧
- 笔记基本操作
- 新建笔记(标题 + 内容)、编辑、删除、复制
- 自动保存(每 3 秒 / 输入暂停时)
- 快捷键支持(新建
Ctrl+N、保存Ctrl+S、删除Ctrl+D)
- 分类管理
- 创建 / 删除笔记本(如「工作」「生活」)
- 笔记归属到指定笔记本(新建时选择 / 后续修改)
- 左侧导航栏展示笔记本列表,点击切换
- 本地存储与备份
- 所有数据默认存储在本地 SQLite 数据库(用户目录下,如
~/.memo/data.db) - 手动备份:支持导出选中笔记为 Markdown 文件(单文件 / 批量打包)
- 所有数据默认存储在本地 SQLite 数据库(用户目录下,如
参考文档
go运行webview https://blog.csdn.net/qq_43660595/article/details/139641147
go_webview https://bbs.itying.com/topic/6876c4174715aa0088487c64