1
0
mirror of https://github.com/fumiama/go-web-wrapper.git synced 2026-06-05 00:32:43 +08:00
This commit is contained in:
源文雨
2025-07-16 22:59:58 +09:00
parent 59331ed177
commit 28dca5ac1e
7 changed files with 351 additions and 0 deletions

3
.gitignore vendored
View File

@@ -30,3 +30,6 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/
/dist.zip
.DS_Store

17
README.md Normal file
View File

@@ -0,0 +1,17 @@
# go-web-wrapper
A simple template that wrap your static website into an executable file.
## Quick Start
1. Place `dist.zip` at project root dir (include all your website files like index.html, etc.).
2. Build project.
- Windows
```bash
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -H=windowsgui" -trimpath -o web.exe
```
- Unix-Like
```bash
# MacOS ARM
GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -trimpath -o web_darwin_arm64
# Linux AMD64
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o web_linux_amd64
```

6
config.go Normal file
View File

@@ -0,0 +1,6 @@
package main
const (
Endpoint = "127.0.0.1:8381"
WindowTitle = "预览" // for windows only
)

31
dist.go Normal file
View File

@@ -0,0 +1,31 @@
package main
import (
"archive/zip"
"bytes"
_ "embed"
"fmt"
"io/fs"
"net/http"
)
//go:embed dist.zip
var distzipbytes []byte
var distribution = func() http.FileSystem {
distzip, err := zip.NewReader(bytes.NewReader(distzipbytes), int64(len(distzipbytes)))
if err != nil {
panic(err)
}
return http.FS(fs.FS(distzip))
}()
func init() {
http.Handle("/", http.FileServer(distribution)) // frontend
go func() {
fmt.Println("server quit for", http.ListenAndServe(Endpoint, nil))
}()
fmt.Println("正在展示:", Endpoint)
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module go-web-wrapper
go 1.20

30
main_unix.go Normal file
View File

@@ -0,0 +1,30 @@
//go:build unix
package main
import (
"errors"
"os/exec"
"runtime"
)
func openBrowser(url string) error {
var cmd string
switch runtime.GOOS {
case "darwin":
cmd = "open"
case "linux":
cmd = "xdg-open"
default:
return errors.New("unsupported platform")
}
return exec.Command(cmd, url).Start()
}
func main() {
err := openBrowser("http://" + Endpoint)
if err != nil {
panic(err)
}
select {}
}

261
main_windows.go Normal file
View File

@@ -0,0 +1,261 @@
//go:build windows
package main
import (
"runtime"
"syscall"
"unsafe"
)
func main() {
runtime.LockOSThread()
// ① 隐藏控制台窗口(如果存在)
hConsole, _, _ := procGetConsoleWindow.Call()
if hConsole != 0 {
procShowWindow.Call(hConsole, SW_HIDE)
}
// ② 获取当前模块句柄
hInstance, _, _ = procGetModuleHandleW.Call(0)
// ③ 注册窗口类
className := syscall.StringToUTF16Ptr("MyWindowClass")
wndClass := WNDCLASSEX{
CbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})),
Style: 0,
LpfnWndProc: syscall.NewCallback(WndProc),
CbClsExtra: 0,
CbWndExtra: 0,
HInstance: hInstance,
HIcon: 0,
HCursor: 0,
// 背景色使用 COLOR_WINDOW+16
HbrBackground: uintptr(6),
LpszMenuName: nil,
LpszClassName: className,
HIconSm: 0,
}
ret, _, err := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wndClass)))
if ret == 0 {
panic("RegisterClassExW failed: " + err.Error())
}
// ④ 创建窗口
windowTitle := syscall.StringToUTF16Ptr(WindowTitle)
hWnd, _, err := procCreateWindowExW.Call(
0,
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(windowTitle)),
WS_OVERLAPPEDWINDOW&^WS_THICKFRAME, // 移除 WS_THICKFRAME 样式
100, 100, 400, 150, // 窗口位置和大小
0, 0,
hInstance,
0,
)
if hWnd == 0 {
panic("CreateWindowExW failed: " + err.Error())
}
// ⑤ 创建控件:文本框和按钮
createControls(hWnd)
// 显示并更新窗口
procShowWindow.Call(hWnd, SW_SHOW)
procUpdateWindow.Call(hWnd)
// ⑥ 消息循环
var msg MSG
for {
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
if ret == 0 { // 收到 WM_QUIT 消息
break
}
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&msg)))
}
}
func createControls(parent uintptr) {
// 创建宋体字体
font := createFont("宋体")
// 创建一个不可编辑的文本框,显示 "正在展示"
editClass := syscall.StringToUTF16Ptr("EDIT")
text := syscall.StringToUTF16Ptr("正在展示: " + Endpoint)
editHWnd, _, _ := procCreateWindowExW.Call(
0,
uintptr(unsafe.Pointer(editClass)),
uintptr(unsafe.Pointer(text)),
uintptr(WS_CHILD|WS_VISIBLE|ES_CENTER),
10, 20, 370, 40,
parent,
0,
hInstance,
0,
)
// 设置宋体字体
procSendMessageW.Call(editHWnd, WM_SETFONT, font, 1)
// 创建按钮控件,显示“按钮”,并指定控制 ID 为 ID_BUTTON_OPEN
buttonClass := syscall.StringToUTF16Ptr("BUTTON")
buttonText := syscall.StringToUTF16Ptr("打开页面")
buttonHWnd, _, _ := procCreateWindowExW.Call(
0,
uintptr(unsafe.Pointer(buttonClass)),
uintptr(unsafe.Pointer(buttonText)),
uintptr(WS_CHILD|WS_VISIBLE|BS_DEFPUSHBUTTON),
10, 70, 365, 30,
parent,
uintptr(ID_BUTTON_OPEN), // 这里设置按钮的ID
hInstance,
0,
)
// 设置宋体字体
procSendMessageW.Call(buttonHWnd, WM_SETFONT, font, 1)
}
// 窗口和控件样式、消息常量
const (
WS_OVERLAPPEDWINDOW = 0x00CF0000 // 标准窗口样式(包含标题栏、系统菜单、可调整大小等)
WS_CHILD = 0x40000000 // 子窗口样式
WS_VISIBLE = 0x10000000 // 可见样式
WS_BORDER = 0x00800000 // 边框样式
WS_CAPTION = 0x00C00000 // 标题栏样式
WS_SYSMENU = 0x00080000 // 系统菜单样式
WS_THICKFRAME = 0x00040000 // 可调整大小的边框样式
BS_DEFPUSHBUTTON = 0x00000001 // 默认按钮样式
ES_CENTER = 0x00000001
SW_HIDE = 0
SW_SHOW = 5
WM_DESTROY = 0x0002
WM_COMMAND = 0x0111
WM_SETFONT = 0x0030
)
const (
// 定义一个退出按钮的ID
ID_BUTTON_OPEN = 1001
)
// 结构体定义,与 WIN32 相同
type POINT struct {
X int32
Y int32
}
type MSG struct {
Hwnd uintptr
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt POINT
}
type WNDCLASSEX struct {
CbSize uint32
Style uint32
LpfnWndProc uintptr
CbClsExtra int32
CbWndExtra int32
HInstance uintptr
HIcon uintptr
HCursor uintptr
HbrBackground uintptr
LpszMenuName *uint16
LpszClassName *uint16
HIconSm uintptr
}
var (
// 加载 DLL
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
shell32 = syscall.NewLazyDLL("shell32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
// 接口函数
procDefWindowProcW = user32.NewProc("DefWindowProcW")
procDispatchMessageW = user32.NewProc("DispatchMessageW")
procGetMessageW = user32.NewProc("GetMessageW")
procRegisterClassExW = user32.NewProc("RegisterClassExW")
procCreateWindowExW = user32.NewProc("CreateWindowExW")
procShowWindow = user32.NewProc("ShowWindow")
procUpdateWindow = user32.NewProc("UpdateWindow")
procPostQuitMessage = user32.NewProc("PostQuitMessage")
procSendMessageW = user32.NewProc("SendMessageW")
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow")
shellExecute = shell32.NewProc("ShellExecuteW")
procCreateFontIndirectW = gdi32.NewProc("CreateFontIndirectW")
)
var hInstance uintptr
// 窗口过程函数,处理窗口消息
func WndProc(hWnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case WM_COMMAND:
// 当点击控件时wParam 的低 16 位为控件 ID
if uint16(wParam) == ID_BUTTON_OPEN {
// 调用 ShellExecuteW 打开默认浏览器
// 第一个参数为 0 表示没有窗口句柄,最后一个参数指定需要打开的 URL
url := "http://" + Endpoint
shellExecute.Call(0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("open"))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(url))),
0,
0,
1) // 1 表示普通窗口打开
return 0
}
case WM_DESTROY:
procPostQuitMessage.Call(0)
return 0
default:
ret, _, _ := procDefWindowProcW.Call(hWnd, uintptr(msg), wParam, lParam)
return ret
}
return 0
}
// 创建指定字体(如宋体)
func createFont(fontName string) uintptr {
font := &struct {
Height int32
Width int32
Escapement int32
Orientation int32
Weight int32
Italic byte
Underline byte
StrikeOut byte
CharSet byte
OutPrecision byte
ClipPrecision byte
Quality byte
PitchAndFamily byte
FaceName [32]uint16
}{}
font.Height = 20 // 字体高度
font.Weight = 800 // 字体粗细
font.CharSet = 1 // 默认字符集
font.OutPrecision = 4 // 输出精度
font.ClipPrecision = 2 // 裁剪精度
font.Quality = 1 // 质量
font.PitchAndFamily = 0x31 // 字体类型
copy(font.FaceName[:], syscall.StringToUTF16(fontName))
hFont, _, _ := procCreateFontIndirectW.Call(uintptr(unsafe.Pointer(font)))
return hFont
}