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:
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:
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:
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) error
SetTooltip(tooltip string) error
ShowBalloonNotification(title, text string) error
Dispose() 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!