NotifyIcon ve WPF aplikaci

13.7.2017

Pokud vytváříte v Microsoft Visual Studiu aplikaci typu WPF (Windows Presentation Foundation), zjistíte ve vizuálním návrháři se nenabízí komponenta NotifyIcon, která je k disposici v aplikaci typu Windows Forms.

V tomto článku a ukázkové aplikaci si ukážeme jednoduchý způsob jak vytvořit NotifyIcon včetně fukcionality skrývání okna do této ikony.

Vytvoření takovéto ikony je standardní součástí Windows API a v .NET aplikaci využijeme proto volání příslušných funkcí pomocí DllImport

Po založení projektu (Visual C# - WPF aplikace pro klasický desktop) si nejprve připravíme potřebné deklarace hodnot, struktur a funkcí z Windows API, které budeme využívat. V ukázkovém projektu jsem tento soubor nazval WinApi.cs a zde je jeho kompletní výpis:

using System;
using System.Runtime.InteropServices;

namespace WinApi
{
	internal static class UnsafeNativeMethods
	{
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "NOTIFYICONDATA.szTip")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "NOTIFYICONDATA.szInfoTitle")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "NOTIFYICONDATA.szInfo")]
		[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
		public static extern int Shell_NotifyIcon(int zprava, NativeMethods.NOTIFYICONDATA pNID);
	}

	internal static class NativeMethods
	{
		public const int WM_LBUTTONUP = 0x0202;
		public const int WM_APP = 0x8000;
		public const int wm_notify_ikona = WM_APP + 2001;

		public const int
			NIF_ICON = 0x00000002,
			NIF_MESSAGE = 0x00000001,
			NIF_TIP = 0x00000004,
			NIF_INFO = 0x00000010,
			NIF_STATE = 0x00000008,
			NIF_SHOWTIP = 0x00000080;

		public const int
			NIM_ADD = 0x00000000,
			NIM_MODIFY = 0x00000001,
			NIM_DELETE = 0x00000002,
			NIM_SETFOCUS = 0x00000003,
			NIM_SETVERSION = 0x00000004;

		public const int NOTIFYICON_VERSION_4 = 4;

		[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
		public class NOTIFYICONDATA
		{
			public int cbSize = Marshal.SizeOf(typeof(NOTIFYICONDATA));
			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2111:PointersShouldNotBeVisible")]
			public IntPtr hWnd = IntPtr.Zero;
			public int uID = 0;
			public int uFlags = 0;
			public int uCallbackMessage = 0;
			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2111:PointersShouldNotBeVisible")]
			public IntPtr hIcon = IntPtr.Zero;
			[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
			public string szTip;
			public int dwState = 0;
			public int dwStateMask = 0;
			[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
			public string szInfo;
			public int uTimeoutOrVersion = 0;
			[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
			public string szInfoTitle;
			public int dwInfoFlags = 0;
			public Guid guidItem;
			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2111:PointersShouldNotBeVisible")]
			public IntPtr hBalloonicon = IntPtr.Zero;
		}
	}
}

Nyní trochu teorie: Ikona v oznamovací oblasti panelu nástrojů se vytváří, ruší a modifikuje funkcí Shell_NotifyIcon, v jejímž parametru (je to adresa struktury NOTIFYICONDATA) se nastaví požadovaná funkčnost. Jedním z nutných parametrů je handle okna kterému ikona patří. Toto získáme s využitím třídy System.Windows.Interop.HwndSource. Do třídy hlavního okna si přidáme následující proměnnou:

private System.Windows.Interop.HwndSource hwndSource;

V obsluze události Loaded ji pak přiřadíme k okno a přidáme si vlastní funkci která bude jakýmsi bypassem procedury okna, tzn. budeme v ním moci zachytávat (a nějak na ně reagovat) všechny zprávy Windows které přijdou tomuto oknu:

this.hwndSource = (HwndSource)PresentationSource.FromVisual(this);
this.hwndSource.AddHook(WindowProc);

Dalším nutným parametrem při vytvoření NotifyIcon je číslo vlastní zprávy, přes kterou bude tato ikona komunikovat se svým oknem. Tuto zprávu máme deklarovanou ve výše zmíněném souboru WinApi.cs takto:

public const int WM_APP = 0x8000;
public const int wm_notify_ikona = WM_APP + 1;

Hodnota WM_APP je číslo od kterého začínají možné uživatelské zprávy tak aby nekolidovaly se standardními zprávami Windows.

Nyní se již můžeme podívat na funkce které vytvoří a zruší ikonu v oznamovací oblasti:

private void VytvoritNotifyIcon()
{
	WinApi.NativeMethods.NOTIFYICONDATA nid = new WinApi.NativeMethods.NOTIFYICONDATA();
	nid.hIcon = ikona.Handle;
	nid.hWnd = hwndSource.Handle;
	nid.uID = 1;
	nid.szTip = "Vývoj WMP";
	nid.uFlags = WinApi.NativeMethods.NIF_ICON | WinApi.NativeMethods.NIF_MESSAGE | WinApi.NativeMethods.NIF_TIP;
	nid.uCallbackMessage = WinApi.NativeMethods.wm_notify_ikona;
	nid.uFlags |= WinApi.NativeMethods.NIF_SHOWTIP;
	if (WinApi.UnsafeNativeMethods.Shell_NotifyIcon(WinApi.NativeMethods.NIM_ADD, nid) == 0)
		throw new System.ComponentModel.Win32Exception();
	nid.uTimeoutOrVersion = WinApi.NativeMethods.NOTIFYICON_VERSION_4;
	if (WinApi.UnsafeNativeMethods.Shell_NotifyIcon(WinApi.NativeMethods.NIM_SETVERSION, nid) == 0)
		throw new System.ComponentModel.Win32Exception();
}

private void ZrusitNotifyIcon()
{
	WinApi.NativeMethods.NOTIFYICONDATA nid = new WinApi.NativeMethods.NOTIFYICONDATA();
	nid.hWnd = hwndSource.Handle;
	nid.uID = 1;
	if (WinApi.UnsafeNativeMethods.Shell_NotifyIcon(WinApi.NativeMethods.NIM_DELETE, nid) == 0)
		throw new System.ComponentModel.Win32Exception();
}

Funkci VytvoritNotifyIcon() zavoláme nejlépe na konci kódu obsluhy události Loaded a naopak funkci ZrusitNotifyIcon() při zrušení okna, tj. v obsluze události Closing.

Nyní zbývá podívat se jak vypadá zachycení a zpracování zprávy od ikony v proceduře okna:

private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
	switch (msg)
	{
		case WinApi.NativeMethods.wm_notify_ikona:
			int llp = lParam.ToInt32() & 0x0000FFFF;
			switch (llp)
			{
				case WinApi.NativeMethods.WM_LBUTTONUP:
					if (IsVisible)
						Hide();
					else
						Show();
					break;
			}
			break;
	}
	return IntPtr.Zero;
}

Jak je zřejmé z kódu, je realizováno pouze to základní zachycení kliknutí myši, přesně řečeno reakce na puštění levého tlačítka. V reakci na toto otestujeme zda je okno právě viditelné a podle toho ho skryjeme nebo naopak znovu zobrazíme.

Ukázkový projekt (Microsoft Visual Studio Community 2017) si můžete stáhnout zde.