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
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
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
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:
SetIcon(icon uintptr) errorSetTooltip(tooltip string) errorShowBalloonNotification(title, text string) errorDispose() error
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!