windigo

repository·master·Indexed 20 days ago

https://github.com/rodrigocfd/windigo

A pure Go implementation of the Win32 API and GUI programming model that allows developers to write Windows applications without using CGo. It includes a high-level 'ui' package for GUI windows and controls, a 'win' package for native Win32 structs and functions (including Windows Registry and process enumeration), support for COM objects and Automation via IDispatch and VARIANT, and a 'co' package for strictly typed Win32 constants.

Tokens
2.8K
Snippets
7
Records
8
Agent score
21%

What's inside windigo

  1. Windigo Package Architecture

    master

    Windigo is organized into four primary packages:

    • co: Contains all native Win32 constants, strictly typed.
    • ui: Provides high-level, idiomatic Go abstractions for UI windows and controls.
    • win: The core package containing native Win32 structs, handles, and functions.
    • wstr: Manages string conversions, specifically for UTF-16 wide strings required by the Win32 API.
  2. How COM objects and OleReleaser work

    master

    Windigo provides full support for Component Object Model (COM) objects.

    Lifetime Management: Because COM objects require manual reference counting, Windigo uses a win.OleReleaser object to manage their lifetimes. A win.OleReleaser acts like an arena allocator: you pass it to functions that produce COM objects, and calling releaser.Release() will call Release on all associated COM objects at once. This ensures proper cleanup and prevents memory leaks.

    Initialization: Before using COM, you must initialize the COM library using win.CoInitializeEx (typically with co.COINIT_APARTMENTTHREADED) and ensure you call win.CoUninitialize when finished.

    Example: Using a COM File Open Dialog:

    package main
    
    import (
    	"github.com/rodrigocfd/windigo/co"
    	"github.com/rodrigocfd/windigo/win"
    )
    
    func main() {
    	runtime.LockOSThread()
    
    	_, _ = win.CoInitializeEx(
    		co.COINIT_APARTMENTTHREADED | co.COINIT_DISABLE_OLE1DDE)
    	defer win.CoUninitialize()
    
    	releaser := win.NewOleReleaser()
    	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 {
    		item, _ := fod.GetResult(releaser)
    		fileName, _ := item.GetDisplayName(co.SIGDN_FILESYSPATH)
    		println(fileName)
    	}
    }
  3. How COM Automation works with IDispatch and VARIANT

    master

    Windigo supports COM Automation through bindings to the IDispatch interface and VARIANT parameters. This allows you to invoke methods on COM objects dynamically using InvokeMethod or InvokeGetAsIDispatch.

    When using automation, you still use win.OleReleaser to manage the lifetime of the objects returned by these calls. This is useful for controlling external applications like Microsoft Excel.

    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")
    }
  4. Use Win32 resources for native applications

    master

    Windigo provides several resources to help build native Win32 applications, including icons and manifests.

    Available Resources

    • gopher.ico: A default icon.
    • win10.exe.manifest: A manifest file to ensure the application is recognized as a Windows 10 application.
    • minimal.res: A compiled Win32 resource script containing both the icon and the manifest.
    • minimal.syso: A pre-compiled .syso file containing the icon and manifest. To use it, place it in the root folder of your Go project. You can load the icon using the resource ID 101.

    Managing Resources

    • Editing: You can create or edit .res files using Visual Studio or Resource Hacker.
    • Compiling: To convert a .res file into a .syso file, use the windres tool.
    windres.exe -i minimal.res -o minimal.syso
  5. Create a GUI window with Windigo

    master

    Windigo provides a high-level ui package for creating Windows GUI applications.

    Important Requirements:

    • You must call runtime.LockOSThread() at the start of your main function because the Windows GUI is single-threaded.
    • To compile a final .exe without a console window appearing, use the following build command:
    go build -trimpath -ldflags "-s -w -H=windowsgui"

    Example of creating a main window with child controls (Static text, Edit field, and Button) and handling a button click event:

    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()
    }
    
    type MyWindow struct {
    	wnd     *ui.Main
    	blName *ui.Static
    	xtName *ui.Edit
    	btnShow *ui.Button
    }
    
    func ShowMainWindow() int {
    	wnd := ui.NewMain(
    		ui.OptsMain().
    			Title("Hello you").
    			Center(true).
    			Size(ui.Dpi(340, 80)).
    			ClassIconId(101),
    	)
    
    	blName := ui.NewStatic(
    		wnd,
    		ui.OptsStatic().
    			Text("Your name").
    			Position(ui.Dpi(10, 22)),
    	)
    	xtName := 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)
    	})
    }
  6. Access the Windows Registry

    master

    Use the win package to interact with the Windows Registry. You can open keys using RegOpenKeyEx, query values with RegQueryValueEx, and enumerate values with RegEnumValue.

    When querying values, you can attempt to extract specific types using methods like .Sz() for strings or .Dword() for uint32 values.

    package main
    
    import (
    	"github.com/rodrigocfd/windigo/co"
    	"github.com/rodrigocfd/windigo/win"
    )
    
    func main() {
    	hKey, _ := win.HKEY_CURRENT_USER.RegOpenKeyEx(
    		"Control Panel\\Mouse",
    		co.REG_OPTION_NONE,
    		co.KEY_READ)
    	defer hKey.RegCloseKey()
    
    	regVal, _ := hKey.RegQueryValueEx("Beep")
    
    	if strVal, ok := regVal.Sz(); ok {
    		println("Beep is", strVal)
    	}
    
    	namesVals, _ := hKey.RegEnumValue()
    	for _, nameVal := range namesVals {
    		if str, ok := nameVal.Val.Sz(); ok {
    			println("Value str", nameVal.Name, str)
    		} else if num, ok := nameVal.Val.Dword(); ok {
    			println("Value int", nameVal.Name, num)
    		} else {
    			println("Value other", nameVal.Name)
    		}
    	}
    }
  7. Enumerate running processes

    master

    You can take a process snapshot using win.CreateToolhelp32Snapshot with the co.TH32CS_SNAPPROCESS flag. The resulting handle can then be used to call EnumProcesses() to retrieve a list of running processes, including their PIDs and executable names.

    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")
    }