From 28dca5ac1e059ed6710332407ec934e842e660da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Wed, 16 Jul 2025 22:59:58 +0900 Subject: [PATCH] init --- .gitignore | 3 + README.md | 17 ++++ config.go | 6 ++ dist.go | 31 ++++++ go.mod | 3 + main_unix.go | 30 ++++++ main_windows.go | 261 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 351 insertions(+) create mode 100644 README.md create mode 100644 config.go create mode 100644 dist.go create mode 100644 go.mod create mode 100644 main_unix.go create mode 100644 main_windows.go diff --git a/.gitignore b/.gitignore index aaadf73..07219d8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +/dist.zip +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3afd3e --- /dev/null +++ b/README.md @@ -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 + ``` diff --git a/config.go b/config.go new file mode 100644 index 0000000..e1e6e9c --- /dev/null +++ b/config.go @@ -0,0 +1,6 @@ +package main + +const ( + Endpoint = "127.0.0.1:8381" + WindowTitle = "预览" // for windows only +) diff --git a/dist.go b/dist.go new file mode 100644 index 0000000..af8fc70 --- /dev/null +++ b/dist.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ddb31cc --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go-web-wrapper + +go 1.20 diff --git a/main_unix.go b/main_unix.go new file mode 100644 index 0000000..e2c432e --- /dev/null +++ b/main_unix.go @@ -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 {} +} diff --git a/main_windows.go b/main_windows.go new file mode 100644 index 0000000..aa238d0 --- /dev/null +++ b/main_windows.go @@ -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+1(6) + 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 +}