(原) Wails 又一个Web前台GUI

原创文章,请后转载,并注明出处。

Github

中文官网

中文文档

使用 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)
	}

}

相关文章