mirror of
https://github.com/fumiama/go-web-wrapper.git
synced 2026-06-05 00:32:43 +08:00
init
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,3 +30,6 @@ go.work.sum
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
|
||||
/dist.zip
|
||||
.DS_Store
|
||||
|
||||
17
README.md
Normal file
17
README.md
Normal 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
6
config.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package main
|
||||
|
||||
const (
|
||||
Endpoint = "127.0.0.1:8381"
|
||||
WindowTitle = "预览" // for windows only
|
||||
)
|
||||
31
dist.go
Normal file
31
dist.go
Normal 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)
|
||||
}
|
||||
30
main_unix.go
Normal file
30
main_unix.go
Normal 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
261
main_windows.go
Normal 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+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
|
||||
}
|
||||
Reference in New Issue
Block a user