震惊!Go语言居然可以这样玩Windows窗口,告别臃肿GUI库
摘要: 本文介绍如何用Go语言直接调用Windows API创建原生窗口应用。通过加载user32.dll和kernel32.dll,定义窗口类结构体和消息处理回调函数,演示了注册窗口类、创建窗口和实现基本消息循环的过程。这种底层开发方式可避免第三方GUI库的依赖,实现轻量级高性能的窗口应用。文章详细讲解了WM_PAINT和WM_DESTROY等关键消息的处理方法,并提供了完整的代码示例来创建一个
嘿,各位Gopher们!今天咱们来点刺激的——不用任何花里胡哨的GUI库,直接用Go和Windows API面对面交流,打造一个原汁原味的Windows窗口应用!这感觉就像用汇编写Hello World一样酷炫,快来一起看看吧!
为什么要这么做?直接调用API不香吗?
咱们Go程序员都是追求极致的主儿,对吧?用第三方GUI库确实方便,但总感觉隔了层纱窗看风景。直接调用Windows API就不一样了,那叫一个通透!
- 轻装上阵:不用拖家带口装一堆GUI库,编译出来的程序小得能钻门缝
- 速度飞起:没有中间商赚差价,直接和Windows系统对话,性能杠杠的
- 涨姿势:深入了解Windows窗口系统的运作原理,从此成为办公室里的Windows编程大神
- 为所欲为:API的功能任你调用,想干啥就干啥(当然,违法乱纪的事情咱们不干哈)
准备工作
在开始之前,咱们得先确保环境没问题。就像上战场前得检查装备一样:
- 你得有个能用的Go开发环境(建议1.16以上版本)
- 你的电脑得是Windows 11系统(其实Windows 10也差不多,别太纠结)
- 最重要的——你得有一颗勇敢的心,准备好和Windows API正面刚!
第一部分:从零开始,先搞个空白窗口耍耍(main.go)
咱们先从最简单的开始——创建一个啥都没有的空白窗口。别小看这个空白窗口,它可是所有Windows应用的基础,就像盖房子得先打地基一样。
首先,咱们来看看main.go的完整代码,然后再一步步拆解:
先呈上完整的main.go代码,咱们边看边聊:
package main
import (
"fmt"
"syscall"
"unsafe"
)
// Windows常量定义
const (
// 窗口样式
WS_OVERLAPPEDWINDOW = 0x00CF0000
WS_VISIBLE = 0x10000000
// 窗口位置和大小
CW_USEDEFAULT = 0x80000000
// 显示命令
SW_SHOWNORMAL = 1
// 窗口消息
WM_PAINT = 0x000F
WM_DESTROY = 0x0002
WM_QUIT = 0x0012
// 资源ID
COLOR_WINDOW = 5
IDI_APPLICATION = 32512
IDC_ARROW = 32512
)
// Windows结构体定义
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
}
type PAINTSTRUCT struct {
hdc syscall.Handle
fErase int32
rcPaint struct{ left, top, right, bottom int32 }
fRestore int32
fIncUpdate int32
rgbReserved [32]byte
}
type MSG struct {
hWnd syscall.Handle
message uint32
wParam uintptr
lParam uintptr
time uint32
pt struct{ x, y int32 }
}
// 全局变量 - 确保在整个程序生命周期中保持有效
var (
// 预加载DLL
user32DLL, _ = syscall.LoadLibrary("user32.dll")
kernel32DLL, _ = syscall.LoadLibrary("kernel32.dll")
// 函数指针定义
RegisterClassExW, _ = syscall.GetProcAddress(user32DLL, "RegisterClassExW")
CreateWindowExW, _ = syscall.GetProcAddress(user32DLL, "CreateWindowExW")
ShowWindow, _ = syscall.GetProcAddress(user32DLL, "ShowWindow")
UpdateWindow, _ = syscall.GetProcAddress(user32DLL, "UpdateWindow")
GetMessageW, _ = syscall.GetProcAddress(user32DLL, "GetMessageW")
TranslateMessage, _ = syscall.GetProcAddress(user32DLL, "TranslateMessage")
DispatchMessageW, _ = syscall.GetProcAddress(user32DLL, "DispatchMessageW")
DefWindowProcW, _ = syscall.GetProcAddress(user32DLL, "DefWindowProcW")
PostQuitMessage, _ = syscall.GetProcAddress(user32DLL, "PostQuitMessage")
BeginPaint, _ = syscall.GetProcAddress(user32DLL, "BeginPaint")
EndPaint, _ = syscall.GetProcAddress(user32DLL, "EndPaint")
LoadIconW, _ = syscall.GetProcAddress(user32DLL, "LoadIconW")
LoadCursorW, _ = syscall.GetProcAddress(user32DLL, "LoadCursorW")
GetModuleHandleW, _ = syscall.GetProcAddress(kernel32DLL, "GetModuleHandleW")
)
// 窗口过程回调函数
func WndProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case WM_PAINT:
// 处理WM_PAINT消息
var ps PAINTSTRUCT
psPtr := uintptr(unsafe.Pointer(&ps))
// 调用BeginPaint
_, _, _ = syscall.Syscall(
BeginPaint,
2,
uintptr(hwnd),
psPtr,
0,
)
// 确保总是调用EndPaint
defer syscall.Syscall(
EndPaint,
2,
uintptr(hwnd),
psPtr,
0,
)
return 0
case WM_DESTROY:
// 窗口销毁时发送退出消息
syscall.Syscall(PostQuitMessage, 1, 0, 0, 0)
return 0
default:
// 所有其他消息交给默认窗口过程
ret, _, _ := syscall.Syscall6(
DefWindowProcW,
4,
uintptr(hwnd),
uintptr(msg),
wparam,
lparam,
0,
0,
)
return ret
}
}
func main() {
// 确保程序结束时释放DLL
defer func() {
if user32DLL != 0 {
syscall.FreeLibrary(user32DLL)
}
if kernel32DLL != 0 {
syscall.FreeLibrary(kernel32DLL)
}
}()
// 获取模块句柄
var hInstance uintptr
if ret, _, _ := syscall.Syscall(GetModuleHandleW, 1, 0, 0, 0); ret != 0 {
hInstance = ret
} else {
fmt.Println("无法获取模块句柄")
return
}
// 准备窗口类名称
className := syscall.StringToUTF16Ptr("GoWindowClass")
// 创建回调函数指针
wndProcPtr := syscall.NewCallback(WndProc)
// 加载默认图标和光标
var hIcon, hCursor uintptr
hIcon, _, _ = syscall.Syscall(LoadIconW, 2, 0, IDI_APPLICATION, 0)
hCursor, _, _ = syscall.Syscall(LoadCursorW, 2, 0, IDC_ARROW, 0)
// 填充窗口类结构
wndClass := WNDCLASSEX{
cbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})),
style: 0x0002 | 0x0001, // CS_HREDRAW | CS_VREDRAW
lpfnWndProc: wndProcPtr,
cbClsExtra: 0,
cbWndExtra: 0,
hInstance: hInstance,
hIcon: hIcon,
hCursor: hCursor,
hbrBackground: COLOR_WINDOW + 1, // 获取系统颜色笔刷
lpszMenuName: nil,
lpszClassName: className,
hIconSm: hIcon,
}
// 注册窗口类
if atom, _, err := syscall.Syscall(
RegisterClassExW,
1,
uintptr(unsafe.Pointer(&wndClass)),
0,
0,
); atom == 0 {
fmt.Printf("注册窗口类失败: %v\n", err)
return
}
// 准备窗口标题
windowTitle := syscall.StringToUTF16Ptr("Go窗口应用")
// 创建窗口
var hwnd uintptr
var err syscall.Errno
hwnd, _, err = syscall.SyscallN(
CreateWindowExW,
0, // 扩展样式
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(windowTitle)),
WS_OVERLAPPEDWINDOW|WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
640,
480,
0, // 父窗口
0, // 菜单
hInstance,
0, // 附加数据
)
if hwnd == 0 {
fmt.Printf("创建窗口失败: %v\n", err)
return
}
// 显示并更新窗口
syscall.Syscall(ShowWindow, 2, hwnd, SW_SHOWNORMAL, 0)
syscall.Syscall(UpdateWindow, 1, hwnd, 0, 0)
// 消息循环 - 标准GetMessage实现
var msg MSG
for {
// 使用syscall.Syscall直接调用GetMessage
ret, _, _ := syscall.Syscall6(
GetMessageW,
4,
uintptr(unsafe.Pointer(&msg)),
0,
0,
0,
0,
0,
)
// 检查返回值
if ret == 0 {
// 收到WM_QUIT消息,退出循环
break
}
// 转换和分发消息
syscall.Syscall(TranslateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
syscall.Syscall(DispatchMessageW, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
}
}
哇,看起来代码有点多,但别慌!咱们把它拆成几个小块慢慢消化,就像吃火锅一样,一口一口来才香。
1. 导包和常量定义
首先,咱们需要导入三个关键的包:
fmt:这个就不用多说了吧,打印错误信息用的syscall:这可是咱们的主角,用来直接调用Windows APIunsafe:处理指针操作的神器,和C语言交互时必不可少
然后是一堆常量定义,这些就像是Windows的密码本,每个数字都代表着特殊的含义:
- 窗口样式:比如
WS_OVERLAPPEDWINDOW就是带边框、标题栏的标准窗口 - 消息类型:比如
WM_PAINT是让窗口重绘的消息,WM_DESTROY是窗口要被销毁时的消息 - 资源ID:系统自带的图标、光标等资源的ID
2. 定义结构体
接下来,咱们得按照Windows的要求定义几个结构体,这就像是给Windows准备的表格,得按照它的格式来填写信息:
WNDCLASSEX:窗口类的结构体,就像是窗口的设计图纸PAINTSTRUCT:绘图时用的结构体,保存绘图相关的信息MSG:消息结构体,Windows就是通过这个结构体给咱们的程序发送各种消息的
3. 加载DLL和函数指针
这部分就像是咱们在Windows系统里"借"工具来用。Windows把各种功能都放在DLL文件里,咱们需要先找到这些文件,然后取出里面的工具:
- 首先加载
user32.dll和kernel32.dll这两个核心DLL - 然后从里面"借"出各种函数指针,比如创建窗口的函数、显示窗口的函数等等
- 注意那些带
W后缀的函数,那是Unicode版本,现在Windows都用这个版本,咱们也得跟上潮流
4. 实现窗口过程回调函数
窗口过程就像是窗口的"大脑",负责处理所有发送到窗口的消息。想象一下,Windows就像是一个总机接线员,不断地把各种电话(消息)转接给咱们的窗口,而窗口过程就是负责接这些电话的人:
- 当收到
WM_PAINT消息时,说明窗口需要重新画一下了 - 当收到
WM_DESTROY消息时,说明窗口要被关闭了,这时候咱们要告诉程序该退出了 - 对于不认识的消息,咱们就交给Windows默认的处理函数,让系统自己搞定
5. 主函数实现
主函数就像是整个程序的"导演",负责协调各个部分的工作:
- 首先,咱们得做好收尾工作,用
defer确保程序结束时释放借的DLL,就像用完工具要归还一样 - 然后获取程序的模块句柄,这是Windows识别咱们程序的身份证
- 接下来给窗口起个名字,"GoWindowClass"听起来就很专业
- 创建窗口类,这就像是给窗口下订单,告诉Windows咱们想要什么样的窗口
- 注册窗口类,把咱们的订单提交给Windows系统
- 创建窗口实例,Windows根据咱们的订单生成一个实实在在的窗口
- 显示并更新窗口,让它在屏幕上显现出来
- 最后,进入消息循环,就像是开了一个客服热线,24小时接听Windows发来的消息
6. 编译和运行
好了,代码写完了,咱们来编译运行一下,看看成果:
go build -o simple_window.exe main.go
双击运行生成的exe文件,你将看到一个干净利落的空白窗口!虽然它现在还什么都不能干,但这已经是一个真正的Windows应用程序了,是不是很有成就感?
第二部分:进阶!给窗口加点料,让它变得更有趣(main2.go)
空白窗口虽然很酷,但总感觉少了点什么。就像买了个新房子,总得装修一下才能住得舒服。咱们来给窗口添加一些控件,让它变得能和用户交互!
先看看main2.go的完整代码,这个版本可高级多了,有编辑框和按钮哦:
package main
import (
"fmt"
"syscall"
"unsafe"
)
// Windows常量定义
const (
// 窗口样式
WS_OVERLAPPEDWINDOW = 0x00CF0000
WS_VISIBLE = 0x10000000
WS_CHILD = 0x40000000
WS_BORDER = 0x00800000
WS_TABSTOP = 0x00010000
WS_GROUP = 0x00020000
// 编辑框样式
ES_LEFT = 0x0000
ES_AUTOHSCROLL = 0x0080
// 按钮样式
BS_PUSHBUTTON = 0x00000000
// 窗口位置和大小
CW_USEDEFAULT = 0x80000000
// 显示命令
SW_SHOWNORMAL = 1
// 窗口消息
WM_PAINT = 0x000F
WM_DESTROY = 0x0002
WM_QUIT = 0x0012
WM_CREATE = 0x0001
WM_COMMAND = 0x0111
BM_CLICK = 0x00F5
// 控件ID
IDC_EDIT = 1001
IDC_BUTTON1 = 1002
IDC_BUTTON2 = 1003
// 对话框样式
MB_OK = 0x00000000
MB_ICONINFORMATION = 0x00000040
// 资源ID
COLOR_WINDOW = 5
IDI_APPLICATION = 32512
IDC_ARROW = 32512
)
// Windows结构体定义
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
}
type PAINTSTRUCT struct {
hdc syscall.Handle
fErase int32
rcPaint struct{ left, top, right, bottom int32 }
fRestore int32
fIncUpdate int32
rgbReserved [32]byte
}
type MSG struct {
hWnd syscall.Handle
message uint32
wParam uintptr
lParam uintptr
time uint32
pt struct{ x, y int32 }
}
// 全局变量 - 确保在整个程序生命周期中保持有效
var (
// 预加载DLL
user32DLL, _ = syscall.LoadLibrary("user32.dll")
kernel32DLL, _ = syscall.LoadLibrary("kernel32.dll")
// 函数指针定义
RegisterClassExW, _ = syscall.GetProcAddress(user32DLL, "RegisterClassExW")
CreateWindowExW, _ = syscall.GetProcAddress(user32DLL, "CreateWindowExW")
ShowWindow, _ = syscall.GetProcAddress(user32DLL, "ShowWindow")
UpdateWindow, _ = syscall.GetProcAddress(user32DLL, "UpdateWindow")
GetMessageW, _ = syscall.GetProcAddress(user32DLL, "GetMessageW")
TranslateMessage, _ = syscall.GetProcAddress(user32DLL, "TranslateMessage")
DispatchMessageW, _ = syscall.GetProcAddress(user32DLL, "DispatchMessageW")
DefWindowProcW, _ = syscall.GetProcAddress(user32DLL, "DefWindowProcW")
PostQuitMessage, _ = syscall.GetProcAddress(user32DLL, "PostQuitMessage")
BeginPaint, _ = syscall.GetProcAddress(user32DLL, "BeginPaint")
EndPaint, _ = syscall.GetProcAddress(user32DLL, "EndPaint")
LoadIconW, _ = syscall.GetProcAddress(user32DLL, "LoadIconW")
LoadCursorW, _ = syscall.GetProcAddress(user32DLL, "LoadCursorW")
GetModuleHandleW, _ = syscall.GetProcAddress(kernel32DLL, "GetModuleHandleW")
GetDlgItem, _ = syscall.GetProcAddress(user32DLL, "GetDlgItem")
GetWindowTextW, _ = syscall.GetProcAddress(user32DLL, "GetWindowTextW")
MessageBoxW, _ = syscall.GetProcAddress(user32DLL, "MessageBoxW")
)
// 全局控件句柄
var (
hEdit uintptr
hBtn1 uintptr
hBtn2 uintptr
)
// 窗口过程回调函数
func WndProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case WM_CREATE:
// 创建编辑框
editClass := syscall.StringToUTF16Ptr("EDIT")
editText := syscall.StringToUTF16Ptr("")
hEdit, _, _ = syscall.SyscallN(
CreateWindowExW,
0,
uintptr(unsafe.Pointer(editClass)),
uintptr(unsafe.Pointer(editText)),
uintptr(WS_CHILD|WS_VISIBLE|WS_BORDER|ES_LEFT|ES_AUTOHSCROLL),
20, 20, 400, 25,
uintptr(hwnd),
uintptr(IDC_EDIT),
0,
0,
)
// 创建按钮1
btnClass := syscall.StringToUTF16Ptr("BUTTON")
btn1Text := syscall.StringToUTF16Ptr("显示内容")
hBtn1, _, _ = syscall.SyscallN(
CreateWindowExW,
0,
uintptr(unsafe.Pointer(btnClass)),
uintptr(unsafe.Pointer(btn1Text)),
uintptr(WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON),
20, 60, 100, 30,
uintptr(hwnd),
uintptr(IDC_BUTTON1),
0,
0,
)
// 创建按钮2
btn2Text := syscall.StringToUTF16Ptr("退出程序")
hBtn2, _, _ = syscall.SyscallN(
CreateWindowExW,
0,
uintptr(unsafe.Pointer(btnClass)),
uintptr(unsafe.Pointer(btn2Text)),
uintptr(WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON),
140, 60, 100, 30,
uintptr(hwnd),
uintptr(IDC_BUTTON2),
0,
0,
)
return 0
case WM_COMMAND:
// 处理控件消息
cmd := wparam & 0xFFFF
if cmd == IDC_BUTTON1 {
// 按钮1被点击,获取编辑框内容并显示
var text [1024]uint16
length, _, _ := syscall.Syscall(
GetWindowTextW,
3,
hEdit,
uintptr(unsafe.Pointer(&text[0])),
1024,
)
if length > 0 {
// 显示消息框
title := syscall.StringToUTF16Ptr("编辑框内容")
syscall.Syscall6(
MessageBoxW,
4,
uintptr(hwnd),
uintptr(unsafe.Pointer(&text[0])),
uintptr(unsafe.Pointer(title)),
uintptr(MB_OK|MB_ICONINFORMATION),
0,
0,
)
}
return 0
} else if cmd == IDC_BUTTON2 {
// 按钮2被点击,退出程序
syscall.Syscall(PostQuitMessage, 1, 0, 0, 0)
return 0
}
case WM_PAINT:
// 处理WM_PAINT消息
var ps PAINTSTRUCT
psPtr := uintptr(unsafe.Pointer(&ps))
// 调用BeginPaint
_, _, _ = syscall.Syscall(
BeginPaint,
2,
uintptr(hwnd),
psPtr,
0,
)
// 确保总是调用EndPaint
defer syscall.Syscall(
EndPaint,
2,
uintptr(hwnd),
psPtr,
0,
)
return 0
case WM_DESTROY:
// 窗口销毁时发送退出消息
syscall.Syscall(PostQuitMessage, 1, 0, 0, 0)
return 0
default:
// 所有其他消息交给默认窗口过程
ret, _, _ := syscall.Syscall6(
DefWindowProcW,
4,
uintptr(hwnd),
uintptr(msg),
wparam,
lparam,
0,
0,
)
return ret
}
return 0
}
func main() {
// 确保程序结束时释放DLL
defer func() {
if user32DLL != 0 {
syscall.FreeLibrary(user32DLL)
}
if kernel32DLL != 0 {
syscall.FreeLibrary(kernel32DLL)
}
}()
// 获取模块句柄
var hInstance uintptr
if ret, _, _ := syscall.Syscall(GetModuleHandleW, 1, 0, 0, 0); ret != 0 {
hInstance = ret
} else {
fmt.Println("无法获取模块句柄")
return
}
// 准备窗口类名称
className := syscall.StringToUTF16Ptr("GoEditButtonWindowClass")
// 创建回调函数指针
wndProcPtr := syscall.NewCallback(WndProc)
// 加载默认图标和光标
var hIcon, hCursor uintptr
hIcon, _, _ = syscall.Syscall(LoadIconW, 2, 0, IDI_APPLICATION, 0)
hCursor, _, _ = syscall.Syscall(LoadCursorW, 2, 0, IDC_ARROW, 0)
// 填充窗口类结构
wndClass := WNDCLASSEX{
cbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})),
style: 0x0002 | 0x0001, // CS_HREDRAW | CS_VREDRAW
lpfnWndProc: wndProcPtr,
cbClsExtra: 0,
cbWndExtra: 0,
hInstance: hInstance,
hIcon: hIcon,
hCursor: hCursor,
hbrBackground: COLOR_WINDOW + 1, // 获取系统颜色笔刷
lpszMenuName: nil,
lpszClassName: className,
hIconSm: hIcon,
}
// 注册窗口类
if atom, _, err := syscall.Syscall(
RegisterClassExW,
1,
uintptr(unsafe.Pointer(&wndClass)),
0,
0,
); atom == 0 {
fmt.Printf("注册窗口类失败: %v\n", err)
return
}
// 准备窗口标题
windowTitle := syscall.StringToUTF16Ptr("编辑框和按钮示例")
// 创建窗口
var hwnd uintptr
var err syscall.Errno
hwnd, _, err = syscall.SyscallN(
CreateWindowExW,
0, // 扩展样式
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(windowTitle)),
uintptr(WS_OVERLAPPEDWINDOW|WS_VISIBLE),
CW_USEDEFAULT,
CW_USEDEFAULT,
500,
150, // 调整窗口大小以适应控件
0, // 父窗口
0, // 菜单
hInstance,
0, // 附加数据
)
if hwnd == 0 {
fmt.Printf("创建窗口失败: %v\n", err)
return
}
// 显示并更新窗口
syscall.Syscall(ShowWindow, 2, hwnd, SW_SHOWNORMAL, 0)
syscall.Syscall(UpdateWindow, 1, hwnd, 0, 0)
// 消息循环 - 标准GetMessage实现
var msg MSG
for {
// 使用syscall.Syscall直接调用GetMessage
ret, _, _ := syscall.Syscall6(
GetMessageW,
4,
uintptr(unsafe.Pointer(&msg)),
0,
0,
0,
0,
0,
)
// 检查返回值
if ret == 0 {
// 收到WM_QUIT消息,退出循环
break
}
// 转换和分发消息
syscall.Syscall(TranslateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
syscall.Syscall(DispatchMessageW, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
}
}
这个版本比刚才的高级多了,咱们来看看它有什么新花样:
1. 增加控件相关的常量和变量
首先,咱们增加了一些新的常量:
- 控件样式:比如
WS_CHILD表示这是个子窗口控件,WS_BORDER给编辑框加上边框 - 新的消息类型:
WM_CREATE和WM_COMMAND,用于创建控件和处理控件事件 - 控件ID:给每个控件分配一个唯一的ID,就像每个人的身份证号一样
然后,咱们定义了几个全局变量来保存控件的句柄:
var (
hEdit uintptr // 编辑框的句柄
hBtn1 uintptr // 按钮1的句柄
hBtn2 uintptr // 按钮2的句柄
)
2. 加载更多的API函数
为了处理控件,咱们需要借一些新的工具(函数):
GetWindowTextW:用来获取编辑框里的文本内容MessageBoxW:用来显示消息框,就像弹出一个小提示一样
3. 增强窗口过程,让它变得更聪明
这是最关键的部分!咱们给窗口过程增加了两个新的消息处理:
-
WM_CREATE消息处理:当窗口刚被创建时,咱们在这里"装修"窗口,添加各种控件:
- 创建一个编辑框,让用户可以输入文字
- 创建两个按钮,一个用来显示内容,一个用来退出程序
- 每个控件都有自己的位置、大小和样式,就像布置房间一样
-
WM_COMMAND消息处理:当用户点击按钮时,Windows会发送这个消息给咱们:
- 当用户点击"显示内容"按钮时,咱们就读取编辑框里的文字,然后用消息框显示出来
- 当用户点击"退出程序"按钮时,咱们就告诉程序该结束了
这样一来,窗口就不再是个只会摆酷的空壳子,而是一个能和用户交流的小应用了!
4. 调整主函数,适应新窗口
主函数大部分和之前差不多,就像是换了个装修风格但房子结构没变。咱们主要改了这几点:
- 给窗口类起了个新名字,避免和之前的程序冲突
- 改了窗口标题,现在叫"编辑框和按钮示例"
- 调整了窗口大小,让它刚好能放下咱们的控件,不大不小正合适
5. 编译运行,体验成果
好了,代码写完了,咱们来编译运行看看效果:
go build -o controls_window.exe main2.go
双击运行,你会看到一个漂亮的窗口,上面有一个编辑框和两个按钮。现在你可以:
- 在编辑框里输入你想说的话,比如"Go语言真酷!"
- 点击"显示内容"按钮,哇!你的话会在一个消息框里弹出来
- 点击"退出程序"按钮,程序就会乖乖地关闭了
是不是很有成就感?你已经成功用Go语言创建了一个能和用户交互的Windows应用程序!
至于怎样取消启动时的黑窗口,你就百度一下吧!这里卖个关子!
深度解析:Windows编程那些事儿
1. 消息循环:窗口的"客服热线"
Windows是一个基于消息的操作系统,就像是一个巨大的客服中心。而咱们的消息循环就是专门负责接听Windows打来的各种电话(消息):
var msg MSG
for {
// 接听电话
ret, _, _ := syscall.Syscall6(GetMessageW, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0)
if ret == 0 {
// 收到结束通话的信号,退出循环
break
}
// 转接电话给对应的部门(窗口过程)
syscall.Syscall(TranslateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
syscall.Syscall(DispatchMessageW, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
}
没有这个循环,窗口就像是个没接电话线的电话,Windows根本联系不上它,它也就什么都干不了。
2. 窗口过程:窗口的"大脑"
窗口过程是处理所有消息的地方,就像是窗口的大脑,决定了窗口如何响应各种情况:
func WndProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
// 根据不同的消息类型做出不同的反应
}
// 返回处理结果
}
这个函数的设计非常巧妙,它使用switch-case结构来处理不同类型的消息,就像是大脑中的不同区域负责处理不同的信息一样。
3. Unicode vs ANSI:字符编码的小秘密
你可能注意到了,咱们用的函数都是带W后缀的,比如CreateWindowExW。这个W可不是随便加的,它代表Unicode版本。
- 老版本的Windows用的是ANSI编码(函数名带
A后缀),只能处理英文字符 - 现代Windows都用Unicode编码(函数名带
W后缀),可以处理世界上几乎所有的文字
所以,咱们也要与时俱进,用Unicode版本的函数,这样程序才能更好地支持中文等非英语字符。
4. 内存管理:小心驶得万年船
在Go中调用Windows API时,内存管理是个需要特别注意的问题:
- 用完记得还:用
defer确保DLL在程序结束时被正确释放,就像用完东西要放回原位一样 - 指针操作要谨慎:使用
unsafe.Pointer进行指针转换时要格外小心,弄错了可能会导致程序崩溃 - 字符串编码要转换:Windows API用的是UTF-16编码,而Go用的是UTF-8,所以要用
syscall.StringToUTF16Ptr进行转换
升级你的Go Windows编程技能
现在你已经掌握了基础,想不想让你的程序更上一层楼?这里有几个进阶建议:
-
加强错误处理:咱们的示例代码为了简洁,省略了一些错误处理。在实际项目中,你应该像个谨慎的侦探一样,仔细检查每一个API调用的返回值。
-
面向对象编程:可以创建一些结构体来封装窗口和控件,这样代码会更清晰,就像把各种功能分门别类地放进不同的抽屉里。
-
封装API:你可以把常用的Windows API调用封装成更友好的Go函数,这样下次用的时候就不用再写那些复杂的
syscall了,就像给自己做了一套顺手的工具。 -
实现事件系统:可以设计一个事件系统,让你的程序更像现代的GUI应用,比如点击按钮时触发一个回调函数,而不是直接在窗口过程里处理。
总结:Go和Windows的完美结合
恭喜你!你已经成功地用Go语言创建了Windows窗口应用程序,而且是直接调用Windows API的那种!这可不简单,很多Gopher可能都没尝试过呢!
虽然直接调用API比用第三方库麻烦一些,但它让你真正理解了Windows窗口系统是如何工作的,就像是学会了开车的原理而不仅仅是怎么开车。而且,这样做出来的程序非常轻量,运行速度也更快。
现在,你已经掌握了Go语言在Windows平台上的高级技能,这将是你简历上的一个亮点。无论你是想开发轻量级工具,还是想深入了解Windows编程,这都是一项非常有价值的技能。
所以,赶紧动手试试吧!创建你自己的Go Windows应用,让你的代码在Windows桌面上跑起来!
好了,祝你在Go Windows编程的道路上越走越远!记得常回来看看,分享你的成果哦!
代码在手,天下我有! 🚀
往期部分文章列表
- 剪贴板监控记:用 Go 写一个 Windows 剪贴板监控器
- 一文讲透 Go 的 defer:你的“善后管家“,别让他变成“背锅侠“!
- 你知道程序怎样优雅退出吗?—— Go 开发中的"体面告别"全指南
- 用golang解救PDF文件中的图片只要200行代码!
- 200KB 的烦恼,Go 语言 20 分钟搞定!—— 一个程序员的图片压缩自救指南
- 从"CPU 烧开水"到优雅暂停:Go 里 sync.Cond 的正确打开方式
- 时移世易,篡改天机:吾以 Go 语令 Windows 文件"返老还童"记
- golang圆阵列图记:天灵灵地灵灵图标排圆形
- golang解图记
- 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
- 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
- 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
- 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
更多推荐



所有评论(0)