Win32 API and GUI in idiomatic Go.
Windigo is designed to be familiar to C/C++ Win32 programmers, using the same concepts, and an API as close as possible to the original Win32 API. This allows most C/C++ Win32 tutorials and examples to be translated to Go.
Notably, Windigo is written 100% in pure Go – CGo is not used, just native syscalls.
Windigo is based in Go 1.20 because it's the last Go version to support systems older than Windows 10, and many people requested that.
go get -u github.com/rodrigocfd/windigo
Native FFI items currently implemented:
| Native FFI item | Count |
|---|---|
| Handles | 32 |
| Structs | 206 |
| Functions | 685 |
| COM interfaces | 78 |
| COM methods | 535 |
| Const types | 419 |
| Const values | 11,134 |
In the examples below, error checking is ommited for brevity.
GUI window
The example below creates a window programmatically, and handles the button click. Also, it uses the minimal.syso provided in the _resources folder.
package main
import (
"fmt"
"runtime"
"github.com/rodrigocfd/windigo/co"
"github.com/rodrigocfd/windigo/ui"
)
func main() {
runtime.LockOSThread() // important: Windows GUI is single-threaded
ShowMainWindow()
}
// This struct represents our main window.
type MyWindow struct {
wnd *ui.Main
lblName *ui.Static
txtName *ui.Edit
btnShow *ui.Button
}
// Displays the main window, blocking until it is closed.
func ShowMainWindow() int {
wnd := ui.NewMain( // create the main window
ui.OptsMain().
Title("Hello you").
Center(true).
Size(ui.Dpi(340, 80)).
ClassIconId(101), // ID of icon resource, see _resources folder
)
lblName := ui.NewStatic( // create the child controls
wnd,
ui.OptsStatic().
Text("Your name").
Position(ui.Dpi(10, 22)),
)
txtName := ui.NewEdit(
wnd,
ui.OptsEdit().
Position(ui.Dpi(80, 20)).
Width(ui.DpiX(150)),
)
btnShow := ui.NewButton(
wnd,
ui.OptsButton().
Text("&Show").
Position(ui.Dpi(240, 19)),
)
me := &MyWindow{wnd, lblName, txtName, btnShow}
me.events()
return wnd.RunAsMain()
}
func (me *MyWindow) events() {
me.btnShow.On().BnClicked(func() {
msg := fmt.Sprintf("Hello, %s!", me.txtName.Text())
me.wnd.Hwnd().MessageBox(msg, "Saying hello", co.MB_ICONINFORMATION)
})
}To compile the final .exe file, run the command:
go build -trimpath -ldflags "-s -w -H=windowsgui"
Registry access
package main
import (
"github.com/rodrigocfd/windigo/co"
"github.com/rodrigocfd/windigo/win"
)
func main() {
// Open a registry key
hKey, _ := win.HKEY_CURRENT_USER.RegOpenKeyEx(
"Control Panel\\Mouse",
co.REG_OPTION_NONE,
co.KEY_READ) // open key as read-only
defer hKey.RegCloseKey()
// Read a single value from this key
regVal, _ := hKey.RegQueryValueEx("Beep") // data can be string, uint32, etc.
if strVal, ok := regVal.Sz(); ok { // try to extract a string value
println("Beep is", strVal)
}
// Enumerate all values under this key
namesVals, _ := hKey.RegEnumValue()
for _, nameVal := range namesVals {
if str, ok := nameVal.Val.Sz(); ok { // does it contain a string?
println("Value str", nameVal.Name, str)
} else if num, ok := nameVal.Val.Dword(); ok { // does it contain an uint32?
println("Value int", nameVal.Name, num)
} else {
println("Value other", nameVal.Name)
}
}
}Enumerating running processes
The example below takes a process snapshot to list the running processes:
package main
import (
"github.com/rodrigocfd/windigo/co"
"github.com/rodrigocfd/windigo/win"
)
func main() {
hSnap, _ := win.CreateToolhelp32Snapshot(co.TH32CS_SNAPPROCESS, 0)
defer hSnap.CloseHandle()
processes, _ := hSnap.EnumProcesses()
for _, nfo := range processes {
println("PID:", nfo.Th32ProcessID, "name:", nfo.SzExeFile())
}
println(len(processes), "found")
}Taking a screenshot
This complex example takes a screenshot using GDI and saves it to a BMP file.
package main
import (
"unsafe"
"github.com/rodrigocfd/windigo/co"
"github.com/rodrigocfd/windigo/win"
)
func main() {
cxScreen := win.GetSystemMetrics(co.SM_CXSCREEN)
cyScreen := win.GetSystemMetrics(co.SM_CYSCREEN)
hdcScreen, _ := win.HWND(0).GetDC()
defer win.HWND(0).ReleaseDC(hdcScreen)
hBmp, _ := hdcScreen.CreateCompatibleBitmap(int(cxScreen), int(cyScreen))
defer hBmp.DeleteObject()
hdcMem, _ := hdcScreen.CreateCompatibleDC()
defer hdcMem.DeleteDC()
hBmpOld, _ := hdcMem.SelectObjectBmp(hBmp)
defer hdcMem.SelectObjectBmp(hBmpOld)
_ = hdcMem.BitBlt(
win.POINT{X: 0, Y: 0},
win.SIZE{Cx: cxScreen, Cy: cyScreen},
hdcScreen,
win.POINT{X: 0, Y: 0},
co.ROP_SRCCOPY,
)
bi := win.BITMAPINFO{
BmiHeader: win.BITMAPINFOHEADER{
Width: cxScreen,
Height: cyScreen,
Planes: 1,
BitCount: 32,
Compression: co.BI_RGB,
},
}
bi.BmiHeader.SetBiSize()
bmpObj, _ := hBmp.GetObject()
bmpSize := bmpObj.CalcBitmapSize(bi.BmiHeader.BitCount)
rawMem, _ := win.GlobalAlloc(co.GMEM_FIXED|co.GMEM_ZEROINIT, bmpSize)
defer rawMem.GlobalFree()
bmpSlice, _ := rawMem.GlobalLockSlice()
defer rawMem.GlobalUnlock()
_, _ = hdcScreen.GetDIBits(
hBmp,
0,
int(cyScreen),
bmpSlice,
&bi,
co.DIB_COLORS_RGB,
)
var bfh win.BITMAPFILEHEADER
bfh.SetBfType()
bfh.SetBfOffBits(uint32(unsafe.Sizeof(bfh) + unsafe.Sizeof(bi.BmiHeader)))
bfh.SetBfSize(bfh.BfOffBits() + uint32(bmpSize))
fout, _ := win.FileOpen(
"C:\\Temp\\screenshot.bmp",
co.FOPEN_RW_OPEN_OR_CREATE,
)
defer fout.Close()
_, _ = fout.Write(bfh.Serialize())
_, _ = fout.Write(bi.BmiHeader.Serialize())
_, _ = fout.Write(bmpSlice)
}Component Object Model (COM)
Windigo has full support for C++ COM objects. The cleanup is performed by a win.OleReleaser object, which calls Release on multiple COM objects at once, much like an arena allocator. Every function which produces a COM object requires a win.OleReleaser to take care of its lifetime.
The example below uses COM objects to display the system native Open File window:
package main
import (
"github.com/rodrigocfd/windigo/co"
"github.com/rodrigocfd/windigo/win"
)
func main() {
runtime.LockOSThread() // important: Windows GUI is single-threaded
_, _ := win.CoInitializeEx(
co.COINIT_APARTMENTTHREADED | co.COINIT_DISABLE_OLE1DDE)
defer win.CoUninitialize()
releaser := win.NewOleReleaser() // will release all COM objects created here
defer releaser.Release()
var fod *win.IFileOpenDialog
_ = win.CoCreateInstance(
releaser,
&co.CLSID_FileOpenDialog,
nil,
co.CLSCTX_INPROC_SERVER,
&fod,
)
defOpts, _ := fod.GetOptions()
_ = fod.SetOptions(defOpts |
co.FOS_FORCEFILESYSTEM |
co.FOS_FILEMUSTEXIST,
)
_ = fod.SetFileTypes([]win.COMDLG_FILTERSPEC{
{Name: "Text files", Spec: "*.txt"},
{Name: "All files", Spec: "*.*"},
})
_ = fod.SetFileTypeIndex(1)
if ok, _ := fod.Show(win.HWND(0)); ok { // in real applications, pass the parent HWND
item, _ := fod.GetResult(releaser)
fileName, _ := item.GetDisplayName(co.SIGDN_FILESYSPATH)
println(fileName)
}
}COM Automation
Windigo has bindings to IDispatch COM interface and VARIANT parameters, allowing you to invoke Automation methods.
The example below manipulates an Excel spreadsheet, saving a copy of it:
package main
import (
"github.com/rodrigocfd/windigo/co"
"github.com/rodrigocfd/windigo/win"
)
func main() {
_, _ = win.CoInitializeEx(
co.COINIT_APARTMENTTHREADED | co.COINIT_DISABLE_OLE1DDE)
defer win.CoUninitialize()
rel := win.NewOleReleaser()
defer rel.Release()
clsId, _ := win.CLSIDFromProgID("Excel.Application")
var excel *win.IDispatch
_ = win.CoCreateInstance(
rel,
&clsId,
nil,
co.CLSCTX_LOCAL_SERVER,
&excel,
)
books, _ := excel.InvokeGetAsIDispatch(rel, "Workbooks")
file, _ := books.InvokeMethodAsIDispatch(rel, "Open", "C:\\Temp\\foo.xlsx")
_, _ = file.InvokeMethod(rel, "SaveAs", "C:\\Temp\\foo copy.xlsx")
_, _ = file.InvokeMethod(rel, "Close")
}The library is divided in four packages:
| Package | Description |
|---|---|
co |
Native Win32 constants, all typed. |
ui |
High-level UI windows and controls. |
win |
Native Win32 structs, handles and functions. |
wstr |
String and UTF-16 wide string management. |
Package dependency:
flowchart BT
internal/utl([internal/utl]) --> co
ui --> win
win --> internal/dll([internal/dll])
win --> internal/utl
win --> wstr
Licensed under MIT license, see LICENSE.md for details.
