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:
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:
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:
RegisterClassExW
CreateWindowExW
GetModuleHandleW
And to make the window visible and interactable, we need these:
GetMessageW
TranslateMessage
DispatchMessageW
DefWindowProcW
PostQuitMessage
ShowWindow
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.