Creating Tray Icons using Go in Windows - Part 1


In this tutorial, we will create Windows tray icons(a.k.a. NotifyIcon) with relatively small chunk of Go code.

About Tray Icons

Tray icons are icons that are displayed on the right side of your taskbar. It looks like this:

Tray icons shown in taskbar

Tray icons shown in taskbar

You are probably familiar with tray icons already. It can be used to inform users about job’s current process, and to view them a notification.

Let’s create our own using Go!

Prerequisites

Assuming you’ve installed Go already. We use golang.org/x/sys/windows package in order to follow this tutorial, so install it:

$ go get -u golang.org/x/sys/windows

The API

To create a tray icon and to modify the state or appearance of the icon, all we have to use is Shell_NotifyIcon Windows API. It is an API that just sends a message to the OS taskbar as documented in MSDN:

Sends a message to the taskbar’s status area.

Its signature looks like this:

BOOL Shell_NotifyIconW(
  DWORD            dwMessage,
  PNOTIFYICONDATAW lpData
);

The first parameter, dwMessage specifies what action to perform. It can be one of NIM_ADD, NIM_MODIFY, NIM_DELETE, NIM_SETFOCUS and NIM_SETVERSION. We primarily use NIM_ADD, NIM_MODIFY and NIM_DELETE for creating, modifying and removing tray icon, respectively.

The second parameter, lpData holds information about the action. If we were going to create a new tray icon, we would set szTip and hIcon fields of the struct and then pass it to the API. Its type is PNOTIFYICONDATAW, that is, a pointer to NOTIFYICONDATAW struct:

typedef struct _NOTIFYICONDATAW {
  DWORD cbSize;
  HWND  hWnd;
  UINT  uID;
  UINT  uFlags;
  UINT  uCallbackMessage;
  HICON hIcon;
#if ...
  WCHAR szTip[64];
#else
  WCHAR szTip[128];
#endif
  DWORD dwState;
  DWORD dwStateMask;
  WCHAR szInfo[256];
  union {
    UINT uTimeout;
    UINT uVersion;
  } DUMMYUNIONNAME;
  WCHAR szInfoTitle[64];
  DWORD dwInfoFlags;
  GUID  guidItem;
  HICON hBalloonIcon;
} NOTIFYICONDATAW, *PNOTIFYICONDATAW;

This struct may look scary at first cause it has lots of fields, but only some combinations of those are actually used in a single API call.

For example, if you want to create a new tray icon in taskbar, you would set dwMessage to NIM_ADD and set an icon handle for hIcon field of NOTIFYICONDATA struct which lpData points to.

Types, Constants and Procedures

In order to interact with Windows APIs using Go, we should define proper types and constants that is compatible with Windows API, then find correct procedures from DLLs.

I recommend you to keep these definitions under separate file, e.g. winapi.go. This helps us to concentrate on the main logic.

Constants

Define some constants that we’re going to use, as described in MSDN:

const (
	NIM_ADD        = 0x00000000
	NIM_MODIFY     = 0x00000001
	NIM_DELETE     = 0x00000002
	NIM_SETFOCUS   = 0x00000003
	NIM_SETVERSION = 0x00000004

	NIF_MESSAGE  = 0x00000001
	NIF_ICON     = 0x00000002
	NIF_TIP      = 0x00000004
	NIF_STATE    = 0x00000008
	NIF_INFO     = 0x00000010
	NIF_GUID     = 0x00000020
	NIF_REALTIME = 0x00000040
	NIF_SHOWTIP  = 0x00000080

	NIS_HIDDEN     = 0x00000001
	NIS_SHAREDICON = 0x00000002

	NIIF_NONE               = 0x00000000
	NIIF_INFO               = 0x00000001
	NIIF_WARNING            = 0x00000002
	NIIF_ERROR              = 0x00000003
	NIIF_USER               = 0x00000004
	NIIF_NOSOUND            = 0x00000010
	NIIF_LARGE_ICON         = 0x00000020
	NIIF_RESPECT_QUIET_TIME = 0x00000080
	NIIF_ICON_MASK          = 0x0000000F

	NIN_BALLOONSHOW      = 0x0402
	NIN_BALLOONTIMEOUT   = 0x0404
	NIN_BALLOONUSERCLICK = 0x0405
)

These are from Shell_NotifyIconW and NOTIFYICONDATAW documentation.

Types

type NOTIFYICONDATA struct {
	CbSize           uint32
	HWnd             uintptr
	UID              uint32
	UFlags           uint32
	UCallbackMessage uint32
	HIcon            uintptr
	SzTip            [128]uint16
	DwState          uint32
	DwStateMask      uint32
	SzInfo           [256]uint16
	UVersion         uint32
	SzInfoTitle      [64]uint16
	DwInfoFlags      uint32
	GuidItem         GUID
	HBalloonIcon     uintptr
}

As you see above, there is also GUID struct. Let’s define this too:

type GUID struct {
	Data1 uint32
	Data2 uint16
	Data3 uint16
	Data4 [8]byte
}

So we are all set for the data types.

Procedures

Shell_NotifyIconW is in the shell32.dll as documented in MSDN. We’re going to load shell32.dll first and find Shell_NotifyIconW's address. Store these as global variable:

var (
	libshell32 = windows.NewLazySystemDLL("shell32.dll")
	procShell_NotifyIconW = libshell32.NewProc("Shell_NotifyIconW")
)

Good. Now we could call the API directly by using: procShell_NotifyIconW.Call(). But, it’s a good habit to wrap an API call into a Go function like so:

func Shell_NotifyIcon(
	dwMessage uint32,
	lpData *NOTIFYICONDATA) (int32, error) {
	r, _, err := procShell_NotifyIconW.Call(
		uintptr(dwMessage),
		uintptr(unsafe.Pointer(lpData)))
	if r == 0 {
		return 0, err
	}
	return int32(r), nil
}

In this way, we can keep type signatures for the API and take advantage of IDE’s auto-completion.

Our First Tray Icon

Now it’s time to test if we can create a tray icon. Code is so simple:

var data NOTIFYICONDATA

data.CbSize = uint32(unsafe.Sizeof(data))

if _, err := Shell_NotifyIcon(NIM_ADD, &data); err != nil {
	panic(err)
}

If it runs without panic, a tray icon would be shown in taskbar. If you’re using Windows 10, you would see nothing but a transparent box, because we didn’t set an icon for it. Windows 7 users can see a default application icon, but it’s good to have our own icon displayed.

Loading Icon

We’re gonna load an icon resource from a file. You can use any icon you want as long as it’s an .ico file. Or you can use a sample icon: Download Sample Icon. Just place it under the same directory where your main.go lives in.

We use LoadImageW API to load icon from a file. It is in user32.dll, and to load it, we do the same process that we have done for Shell_NotifyIconW. To keep the tutorial concise, I’m not gonna put the whole API and constant definitions here. You can find the full code for LoadImage here.

You can then use LoadImage to load our icon file like this:

icon, err := LoadImage(
	0,
	windows.StringToUTF16Ptr("icon.ico"),
	IMAGE_ICON,
	0,
	0,
	LR_DEFAULTSIZE|LR_LOADFROMFILE)
if err != nil {
	panic(err)
}

Setting Icon

Let’s set an icon for our tray icon:

var data NOTIFYICONDATA

data.CbSize = uint32(unsafe.Sizeof(data))
data.UFlags = NIF_ICON

icon, err := LoadImage(
	0,
	windows.StringToUTF16Ptr("icon.ico"),
	IMAGE_ICON,
	0,
	0,
	LR_DEFAULTSIZE|LR_LOADFROMFILE)
if err != nil {
	panic(err)
}
data.HIcon = icon

if _, err := Shell_NotifyIcon(NIM_ADD, &data); err != nil {
	panic(err)
}

This will result in:

Custom icon shown in taskbar

Custom icon shown in taskbar

Cool! But wait, if you hover your mouse on the icon, it will disappear. This is because it doesn’t have an associated window that receives window messages for tray icon. To fix this problem, we should create an empty window which acts as a message receiver.

Creating an Associated Window

To be honest, this part is truely a mess. Just to create a single empty window, we have to import at least these APIs:

And to make the window visible and interactable, we need these:

And also constants and types. I can even write a separate tutorial about creating a simple window. So I’m gonna omit the definitions in this tutorial. Of course you can find the full code at here.

If you’re all set with those definitions, let’s create a window. Here’s the function that creates main window:

func createMainWindow() (uintptr, error) {
	hInstance, err := GetModuleHandle(nil)
	if err != nil {
		return 0, err
	}

	wndClass := windows.StringToUTF16Ptr("MyWindow")

	var wcex WNDCLASSEX

	wcex.CbSize = uint32(unsafe.Sizeof(wcex))
	wcex.LpfnWndProc = windows.NewCallback(wndProc)
	wcex.HInstance = hInstance
	wcex.LpszClassName = wndClass
	if _, err := RegisterClassEx(&wcex); err != nil {
		return 0, err
	}

	hwnd, err := CreateWindowEx(
		0,
		wndClass,
		windows.StringToUTF16Ptr("Tray Icons Example"),
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		400,
		300,
		0,
		0,
		hInstance,
		nil)
	if err != nil {
		return 0, err
	}

	return hwnd, nil
}

I’m not gonna go deeper with it, since it’s just a boilerplate needed for creating an empty window.

Look at how we use wndProc for WNDCLASSEX.LpfnWndProc:

wcex.LpfnWndProc = windows.NewCallback(wndProc)

This wndProc is a callback function that handles window messages for our main window, and tray icon window. Here’s the definition of it:

func wndProc(hWnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case WM_DESTROY:
		PostQuitMessage(0)
	default:
		r, _ := DefWindowProc(hWnd, msg, wParam, lParam)
		return r
	}
	return 0
}

What it does is so simple: if message is WM_DESTROY, make our program to quit and for other messages, just leave the default behavior untouched.

And last thing to make our window interactable, we should run a message loop. Think of it as an infinite loop which grabs window messages sent from OS to our application and dispatches it to proper window. It’s code is simple enough:

var msg MSG

for {
	r, err := GetMessage(&msg, 0, 0, 0)
	if err != nil {
		panic(err)
	}
	if r == 0 {
		break
	}

	TranslateMessage(&msg)
	DispatchMessage(&msg)
}

Note that the message loop should go at the end of our main function.

Tray Icon Associated with Window

Now we are all ready to go. Let’s create our non-disappearing tray icon!

First, create an empty main window:

hwnd, err := createMainWindow()
if err != nil {
	panic(err)
}

Then prepare our NOTIFYICONDATA struct(Note that we set HWnd member with the window handle we created above):

var data NOTIFYICONDATA

data.CbSize = uint32(unsafe.Sizeof(data))
data.UFlags = NIF_ICON
data.HWnd = hwnd

icon, err := LoadImage(
	0,
	windows.StringToUTF16Ptr("icon.ico"),
	IMAGE_ICON,
	0,
	0,
	LR_DEFAULTSIZE|LR_LOADFROMFILE)
if err != nil {
	panic(err)
}
data.HIcon = icon

And let it be displayed:

if _, err := Shell_NotifyIcon(NIM_ADD, &data); err != nil {
	panic(err)
}

Finally, show our main window so that users can see and close the window, then run the message loop:

ShowWindow(hwnd, SW_SHOW)

var msg MSG

for {
	r, err := GetMessage(&msg, 0, 0, 0)
	if err != nil {
		panic(err)
	}
	if r == 0 {
		break
	}

	TranslateMessage(&msg)
	DispatchMessage(&msg)
}

And that’s it. It wasn’t that hard, was it? Hover your mouse over the tray icon, and check it’s not dissapearing anymore.

Removing Tray Icon

Has anyone noticed that even after our application has exited, the tray icon is still visible in the taskbar? And you have to hover your mouse over the icon to remove it from the taskbar. You may have experienced this situation several times before, while using other softwares. The reason is that we didn’t remove the tray icon explicitly. It can commonly happen when programmers(like us) forgot to remove tray icons explicitly or the application just crashed and has no chance to run its cleanup routine.

Removing the tray icon is easy, just pass NIM_DELETE to Shell_NotifyIconW:

if _, err := Shell_NotifyIcon(NIM_DELETE, &data); err != nil {
	panic(err)
}

But wait, how does OS know which tray icon we wanna delete? It identifies each tray icon using either a combination of (hWnd + uID) or guidItem. So in our case, code above can delete our tray icon using hWnd of our main window plus uID which is 0 by default.

Now we can safely delete the tray icon when our application exits, doing:

defer func() {
	if _, err := Shell_NotifyIcon(NIM_DELETE, &data); err != nil {
		panic(err)
	}
}()

Conclusion

So far we’ve created a tray icon that persists in the taskbar while our application is running. In the next part of the tutorial, we’ll show some balloon notifications and interact with users.

You can find full running example code used in this part at here.

Next: Creating Tray Icons using Go in Windows - Part 2