使用 Go + HTML + CSS + JS 构建漂亮的跨平台桌面应用
这又是一个用HTML作前端的库。看起来比较符合我的想法:扩展一些功能,让HTML看起来更看桌面端应用。例如:最大化、透明、无边框、移动位置等。
似乎在Win11下有圆角窗
看网友也有办法解决Win7下面的使用问题。
支持的平台
Windows 10/11 AMD64/ARM64
MacOS 10.13+ AMD64
MacOS 11.0+ ARM64
Linux AMD64/ARM64
依赖
Wails 有许多安装前需要的常见依赖项:
Go 1.17+
[NPM (Node 15+)](https://www.likecs.com/show-902460.html)
[WebView2](https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/)
我比较讨厌过多的依赖,虽然现在npm无处不在。WebView2官方下载的居然不能安装…
使用 wails doctor 检查运行环境
安装 Wails: go install github.com/wailsapp/wails/v2/cmd/wails@latest
创建项目
wails init -n myproject
项目中运行:wails dev
编译项目:wails build
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
//go:embed frontend/src
var assets embed.FS
//go:embed build/appicon.png
var icon []byte
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
Title: "e-wails",
Width: 720,
Height: 570,
MinWidth: 720,
MinHeight: 570,
MaxWidth: 1280,
MaxHeight: 740,
DisableResize: false,
Fullscreen: false,
Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
RGBA: &options.RGBA{R: 33, G: 37, B: 43, A: 255},
Assets: assets,
LogLevel: logger.DEBUG,
OnStartup: app.startup,
OnDomReady: app.domReady,
OnShutdown: app.shutdown,
Bind: []interface{}{
app,
},
// Windows platform specific options
Windows: &windows.Options{
WebviewIsTransparent: false,
WindowIsTranslucent: false,
DisableWindowIcon: false,
},
Mac: &mac.Options{
TitleBar: mac.TitleBarHiddenInset(),
WebviewIsTransparent: true,
WindowIsTranslucent: true,
About: &mac.AboutInfo{
Title: "Vanilla Template",
Message: "Part of the Wails projects",
Icon: icon,
},
},
})
if err != nil {
log.Fatal(err)
}
}
示例程序在upx后占用2.75MB,确实比较小。如果有绿色的WebView2就比较好了。
稍作修改,删除log编译,upx占用更少:2.57MB
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
//go:embed frontend/src
var assets embed.FS
//go:embed build/appicon.png
var icon []byte
func main() {
app := NewApp()
wails.Run(&options.App{
Title: "腾图工具集",
Width: 720,
Height: 570,
MinWidth: 720,
MinHeight: 570,
MaxWidth: 1280,
MaxHeight: 740,
DisableResize: false,
Fullscreen: false,
Frameless: false, // 无边框
StartHidden: false, // 启动时隐藏窗口,直到调用显示窗口(WindowShow)
HideWindowOnClose: false, // 关闭时隐藏窗口(不关闭)
AlwaysOnTop: false, // 窗口固定在最顶层
RGBA: &options.RGBA{R: 33, G: 37, B: 43, A: 255},
Assets: assets,
LogLevel: logger.DEBUG,
OnStartup: app.startup, // 此回调在前端创建之后调用,但在index.html加载之前调用。
OnDomReady: app.domReady, // 在前端加载完毕index.html及其资源后调用此回调。
OnShutdown: app.shutdown, // 在前端被销毁之后,应用程序终止之前,调用此回调。
OnBeforeClose: app.beforeClose, // 通过单击窗口关闭按钮或调用runtime.Quit即将退出应用程序时被调用. 返回 true 将导致应用程序继续,false 将继续正常关闭。这有助于与用户确认他们希望退出程序。
Bind: []interface{}{app},
// Windows platform specific options
Windows: &windows.Options{
WebviewIsTransparent: true, // 使 WebView 背景透明
WindowIsTranslucent: true, // 将使窗口半透明
DisableWindowIcon: false,
DisableFramelessWindowDecorations: false, // 移除无边框模式下的窗口装饰
},
})
}
package main
import (
"context"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called at application startup
func (b *App) startup(ctx context.Context) {
// Perform your setup here
b.ctx = ctx
}
// domReady is called after the front-end dom has been loaded
func (b *App) domReady(ctx context.Context) {
// Add your action here
}
// shutdown is called at application termination
func (b *App) shutdown(ctx context.Context) {
// Perform your teardown here
}
func (b *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "退出?",
Message: "您确定要退出吗?",
})
if err != nil {
return false
}
return dialog != "Yes"
}
// Greet returns a greeting for the given name
func (b *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
透明效果
添加一个托盘图标
package main
import (
"context"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
ti *TrayIcon
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called at application startup
func (b *App) startup(ctx context.Context) {
// Perform your setup here
b.ctx = ctx
b.ti = NewTrayIcon()
b.ti.BalloonClickFunc = b.showWindow
b.ti.TrayClickFunc = b.showWindow
go b.ti.RunTray()
}
// domReady is called after the front-end dom has been loaded
func (b *App) domReady(ctx context.Context) {
// Add your action here
}
// shutdown is called at application termination
func (b *App) shutdown(ctx context.Context) {
// Perform your teardown here
b.ti.Dispose()
}
func (b *App) showWindow() {
//runtime.LogDebug(a.ctx, "showWindow")
runtime.WindowShow(b.ctx)
}
func (b *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "退出?",
Message: "您确定要退出吗?",
})
if err != nil {
return false
}
return dialog != "Yes"
}
// Greet returns a greeting for the given name
func (b *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
----
package main
import (
"crypto/rand"
"time"
"unsafe"
"github.com/cwchiu/go-winapi"
"golang.org/x/sys/windows"
)
const (
TrayIconMsg = winapi.WM_APP + 1
NIN_BALLOONSHOW = 0x0402
NIN_BALLOONTIMEOUT = 0x0404
NIN_BALLOONUSERCLICK = 0x0405
// NotifyIcon flags
NIF_GUID = 0x00000020
NIF_REALTIME = 0x00000040
NIF_SHOWTIP = 0x00000080
)
func (ti *TrayIcon) wndProc(hWnd winapi.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case TrayIconMsg:
switch nmsg := winapi.LOWORD(uint32(lParam)); nmsg {
case NIN_BALLOONUSERCLICK:
ti.BalloonClickFunc()
case winapi.WM_LBUTTONDOWN:
//ti.ShowBalloonNotification("title", "WM_LBUTTONDOWN")
ti.TrayClickFunc()
}
case winapi.WM_DESTROY:
winapi.PostQuitMessage(0)
default:
r := winapi.DefWindowProc(hWnd, msg, wParam, lParam)
return r
}
return 0
}
func newGUID() winapi.GUID {
var buf [16]byte
rand.Read(buf[:])
return *(*winapi.GUID)(unsafe.Pointer(&buf[0]))
}
type TrayIcon struct {
hwnd winapi.HWND
guid winapi.GUID
BalloonClickFunc func()
TrayClickFunc func()
}
func (ti *TrayIcon) createMainWindow() winapi.HWND {
hInstance := winapi.GetModuleHandle(nil)
wndClass := windows.StringToUTF16Ptr("MyWindow")
var wcex winapi.WNDCLASSEX
wcex.CbSize = uint32(unsafe.Sizeof(wcex))
wcex.LpfnWndProc = windows.NewCallback(ti.wndProc)
wcex.HInstance = hInstance
wcex.LpszClassName = wndClass
winapi.RegisterClassEx(&wcex)
hwnd := winapi.CreateWindowEx(
0,
wndClass,
windows.StringToUTF16Ptr("Tray Icons Example"),
winapi.WS_OVERLAPPEDWINDOW,
winapi.CW_USEDEFAULT,
winapi.CW_USEDEFAULT,
winapi.CW_USEDEFAULT, //400,
winapi.CW_USEDEFAULT, //300,
0,
0,
hInstance,
nil)
return hwnd
}
func (ti *TrayIcon) initData() *winapi.NOTIFYICONDATA {
var data winapi.NOTIFYICONDATA
data.CbSize = uint32(unsafe.Sizeof(data))
data.UFlags = NIF_GUID
data.HWnd = ti.hwnd
data.GuidItem = ti.guid
return &data
}
func (ti *TrayIcon) Dispose() {
winapi.Shell_NotifyIcon(winapi.NIM_DELETE, ti.initData())
}
func (ti *TrayIcon) SetIcon(icon winapi.HICON) {
data := ti.initData()
data.UFlags |= winapi.NIF_ICON
data.HIcon = icon
winapi.Shell_NotifyIcon(winapi.NIM_MODIFY, data)
}
func (ti *TrayIcon) SetTooltip(tooltip string) {
data := ti.initData()
data.UFlags |= winapi.NIF_TIP
copy(data.SzTip[:], windows.StringToUTF16(tooltip))
winapi.Shell_NotifyIcon(winapi.NIM_MODIFY, data)
}
func (ti *TrayIcon) ShowBalloonNotification(title, text string) {
data := ti.initData()
data.UFlags |= winapi.NIF_INFO
if title != "" {
copy(data.SzInfoTitle[:], windows.StringToUTF16(title))
}
copy(data.SzInfo[:], windows.StringToUTF16(text))
winapi.Shell_NotifyIcon(winapi.NIM_MODIFY, data)
}
func NewTrayIcon() *TrayIcon {
ti := &TrayIcon{guid: newGUID()}
return ti
}
func (ti *TrayIcon) RunTray() {
time.Sleep(2 * time.Second)
ti.hwnd = ti.createMainWindow()
icon := winapi.LoadIcon(winapi.GetModuleHandle(nil), winapi.MAKEINTRESOURCE(3))
data := ti.initData()
data.UFlags |= winapi.NIF_MESSAGE
data.UCallbackMessage = TrayIconMsg
winapi.Shell_NotifyIcon(winapi.NIM_ADD, data)
ti.SetIcon(icon)
ti.SetTooltip("腾图工具集")
/*
go func() {
for i := 1; i <= 3; i++ {
time.Sleep(3 * time.Second)
ti.ShowBalloonNotification(
fmt.Sprintf("Message %d", i),
"This is a balloon message",
)
}
}()
*/
//winapi.ShowWindow(ti.hwnd, winapi.SW_SHOW)
winapi.ShowWindow(ti.hwnd, winapi.SW_HIDE)
var msg winapi.MSG
for {
r := winapi.GetMessage(&msg, 0, 0, 0)
if r == 0 {
ti.Dispose()
break
}
winapi.TranslateMessage(&msg)
winapi.DispatchMessage(&msg)
}
}