Creating Tray Icons using Go in Windows - Part 2


Continued from the last post - Creating Tray Icons using Go in Windows - Part 1, we will show some balloon notificationsto users and interact with them.

But before we start, let’s add some tooltip for our tray icon first.

Tooltip

Tooltips is a text displayed when you hover mouse over the tray icon. It looks like this:

Tooltip shown over tray icon

Tooltip shown over tray icon

A tooltip helps users to identify what application the tray icon belongs to. It’s really easy to add one, just set szTip member of NOTIFYICONDATA struct and set NIF_TIP flag for uFlags member.

To set sz* members(UTF-16 string, type of [...]uint16), we use windows.StringToUTF16. It converts UTF-8 encoded Go string(string) into UTF-16 encoded word slice([]uint16).

Note that it’s actually deprecated:

StringToUTF16 is deprecated. Use UTF16FromString instead. If s contains a NUL byte this function panics instead of returning an error.

But in this tutorial I’ll use StringToUTF16 instead of UTF16FromString, because using the later one is too verbose:

// Using StringToUTF16
Foo(StringToUTF16("Bar"))

// Using UTF16FromString
u, err := UTF16FromString("Bar")
if err != nil {
    panic(err)
}
Foo(u)

IMPORTANT NOTE: If you’re writing production level code, prefer UTF16FromString though.

So to set a tooltip for our tray icon, this short code is just enough:

copy(data.SzTip[:], windows.StringToUTF16("Tray Icons!"))

And don’t forget to set NIF_TIP flag or the szTip field we set will be ignored:

data.UFlags = NIF_ICON | NIF_TIP

So now the code should look like:

var data NOTIFYICONDATA

data.CbSize = uint32(unsafe.Sizeof(data))
data.UFlags = NIF_ICON | NIF_TIP
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

copy(data.SzTip[:], windows.StringToUTF16("Tray Icons!"))

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

Run the code, hover your mouse over the icon and check the result!

Balloon Notifications

Balloon notifications are popup messages displayed in right-bottom corner of your screen. It looks like:

Example balloon notification

Example balloon notification

To show a balloon notification, we set NIF_INFO flag for uFlags and set szInfo. You can optionally set szInfoTitle member too, this will be the title of the balloon message, as shown above(Title).

So this code will create a tray icon, with an icon, and displays a balloon notification immediately after:

var data NOTIFYICONDATA

data.CbSize = uint32(unsafe.Sizeof(data))
data.UFlags = NIF_ICON | NIF_TIP | NIF_INFO
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

copy(data.SzTip[:], windows.StringToUTF16("Tray Icons!"))
copy(data.SzInfoTitle[:], windows.StringToUTF16("Title"))
copy(data.SzInfo[:], windows.StringToUTF16("This is a balloon message"))

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

Balloon Notification Icons

Along with balloon notifcations, you can optionally set notification’s icon. In Windows 10, if you don’t specify an icon when firing a balloon notification, it’ll use the tray icon’s icon as a default. Take a look at possible options at here.

Updating Tray Icon

Now we can create a tray icon, set a tooltip for it, display a balloon notification. …at the same time. How can we display multiple notifications one after another, indicating a job’s status? NIM_MODIFY is used for that purpose.

Think of a tray icon’s lifecycle as this diagram:

A tray icon’s lifecycle

A tray icon’s lifecycle

We create a tray icon using NIM_ADD for dwMessage parameter with some initial information like an icon or tooltip. Then during our tray icon is alive in taskbar, we use NIM_MODIFY to show balloon notifications or change its visibility. If our jobs is finished, delete the tray icon with NIM_DELETE.

The word modify in NIM_MODIFY seems weird at first, but think about it as NIM_UPDATE.

Refactoring Code

Before testing if our application can fire multiple balloon notifications, let’s refactor the code. Our main code has become too large!

We’re gonna make TrayIcon struct to represent our tray icon. It will have these exported methods:

Remember how OS identifies each tray icon over multiple Shell_NotifyIcon calls? It uses either hWnd + uID combination or guidItem. If guidItem presents then it has higher priority than hWnd + uID, and it is recommended way according to the documentation:

Windows 7 and later: A registered GUID that identifies the icon. This value overrides uID and is the recommended method of identifying the icon. The NIF_GUID flag must be set in the uFlags member.

To use GUIDs, here is an utility function that generates random GUID:

func newGUID() GUID {
	var buf [16]byte
	rand.Read(buf[:])
	return *(*GUID)(unsafe.Pointer(&buf[0]))
}

It uses unsafe magic to cast byte array into GUID struct. Seeing its C equivalent will help you understand it:

return *((GUID*)&buf);

And to have our newGUID function returning different result every time our application starts, change the seed of the random generator based on current time:

func init() {
    rand.Seed(time.Now().UnixNano())
}

This code will be run every time we start our application.

So we can now start writing code, let’s define our struct first:

type TrayIcon struct {
    hwnd uintptr
    guid GUID
}

hwnd member is still necessary even if we use guidItem instead of hWnd + uID because one tray icon should be associated with a window in order to persist in the taskbar and interact with users.

We used same NOTIFYICONDATA over multiple Shell_NotifyIcon calls before, but for our TrayIcon struct, we can’t do that anymore. Instead, we should prepare NOTIFYICONDATA per every call. But most part of it(CbSize, HWnd, GUIDItem) will be the same over multiple calls for each tray icon, so we’re gonna make a helper method to create a base NOTIFYICONDATA instance. This will reduce redundancy of our code:

func (ti *TrayIcon) initData() *NOTIFYICONDATA {
    var data NOTIFYICONDATA
    data.CbSize = uint32(unsafe.Sizeof(data))
    data.UFlags = NIF_GUID
    data.HWnd = ti.hwnd
    data.GUIDItem = ti.guid
    return &data
}

We set UFlags to NIF_GUID, and later we’ll add more flags using bit OR operator(|). And it also gets rid of concern about zero-filling Sz* members before copying data into them.

Next step is to create New method for our TrayIcon struct:

func NewTrayIcon(hwnd uintptr) (*TrayIcon, error) {
    ti := &TrayIcon{hwnd: hwnd, guid: newGUID()}
    if _, err := Shell_NotifyIcon(NIM_ADD, ti.initData()); err != nil {
        return nil, err
    }
    return ti, nil
}

Simple enough, isn’t it? Let’s add other methods right away.

func (ti *TrayIcon) Dispose() error {
    _, err := Shell_NotifyIcon(NIM_DELETE, ti.initData())
    return err
}

func (ti *TrayIcon) SetIcon(icon uintptr) error {
    data := ti.initData()
    data.UFlags |= NIF_ICON
    data.HIcon = icon
    _, err := Shell_NotifyIcon(NIM_MODIFY, data)
    return err
}

func (ti *TrayIcon) SetTooltip(tooltip string) error {
	data := ti.initData()
	data.UFlags |= NIF_TIP
	copy(data.SzTip[:], windows.StringToUTF16(tooltip))
	_, err := Shell_NotifyIcon(NIM_MODIFY, data)
	return err
}

func (ti *TrayIcon) ShowBalloonNotification(title, text string) error {
	data := ti.initData()
	data.UFlags |= NIF_INFO
	if title != "" {
		copy(data.SzInfoTitle[:], windows.StringToUTF16(title))
	}
	copy(data.SzInfo[:], windows.StringToUTF16(text))
	_, err := Shell_NotifyIcon(NIM_MODIFY, data)
	return err
}

And that’s it! Now we can use our TrayIcon in main function:

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

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

	ti, err := NewTrayIcon(hwnd)
	if err != nil {
		panic(err)
	}
	defer ti.Dispose()

	ti.SetIcon(icon)
	ti.SetTooltip("Tray Icon!")
	ti.ShowBalloonNotification("Title", "This is a balloon message")

	ShowWindow(hwnd, SW_SHOW)

	var msg MSG
	for {
		if r, err := GetMessage(&msg, 0, 0, 0); err != nil {
			panic(err)
		} else if r == 0 {
			break
		}
		TranslateMessage(&msg)
		DispatchMessage(&msg)
	}
}

Note that I slightly modified window message loop to shorten the code.

To fire multiple balloon notifications, we can do like this in our main code:

go func() {
    for i := 1; i <= 3; i++ {
        time.Sleep(3 * time.Second)
        ti.ShowBalloonNotification(
            fmt.Sprintf("Message %d", i),
            "This is a balloon message",
        )
    }
}()

This is just for testing purpose, you can use it to display a job’s progress, etc.

User Interactions

Finally, it’s time to respond to user interactions. If a user clicks our tray icon or click the balloon notifcation we’d like to do something meaningful like bringing up our application window to the front. You can also show a context menu but I’ll not cover that in this tutorial, still you can find many third party packages on GitHub.

When user clicks the tray icon or the balloon notification, we can catch that message in our wndProc. To enable this feature, we set NIF_MESSAGE for uFlags and a random message id for uCallbackMessage in our NOTIFYICONDATA. It is recommended to use value above WM_APP(0x8000) for uCallbackMessage.

First we update our NewTrayIcon function to set uCallbackMessage when creating a tray icon:

const TrayIconMsg = WM_APP + 1

func NewTrayIcon(hwnd uintptr) (*TrayIcon, error) {
	ti := &TrayIcon{hwnd: hwnd, guid: newGUID()}
	data := ti.initData()
	data.UFlags |= NIF_MESSAGE
	data.UCallbackMessage = TrayIconMsg
	if _, err := Shell_NotifyIcon(NIM_ADD, data); err != nil {
		return nil, err
	}
	return ti, nil
}

To handle the message, we modify our wndProc:

func wndProc(hWnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case TrayIconMsg:
		switch nmsg := LOWORD(uint32(lParam)); nmsg {
		case NIN_BALLOONUSERCLICK:
			fmt.Println("user clicked the balloon notification")
		case WM_LBUTTONDOWN:
			fmt.Println("user clicked the tray icon")
		}
	case WM_DESTROY:
		PostQuitMessage(0)
	default:
		r, _ := DefWindowProc(hWnd, msg, wParam, lParam)
		return r
	}
	return 0
}

Visit MSDN for more details about window message handling. In short, if we get a window message we have defined(TrayIconMsg = WM_APP + 1), the lParam contains tray icon-specific event id in its lower 16 bits. So we retrieve the event id using LOWORD macro and check the event. There are also other events, so see the documentation for those.

Conclusion

Now we have full understanding about how tray icons basically work. Hope you will find it helpful in your life. And I also recommend you to use well-known packages on GitHub, since it has more powerful functionalities that can be hard to implement by own.

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

Thank you for reading!