?WPF 基礎控件之托盤
控件名:NotifyIcon
作者: WPFDevelopersOrg ?- 吳鋒|驚鏵
原文鏈接: ? ?https://github.com/WPFDevelopersOrg/WPFDevelopers
框架使用大于等于
.NET40
。Visual Studio 2022
。項目使用 MIT 開源許可協議。
新建
NotifyIcon
自定義控件繼承自FrameworkElement
。創建托盤程序主要借助與 Win32API[1]:
注冊窗體對象
RegisterClassEx
。注冊消息獲取對應消息標識
Id
RegisterWindowMessage
。創建窗體(本質上托盤在創建時需要一個窗口句柄,完全可以將主窗體的句柄給進去,但是為了更好的管理消息以及托盤的生命周期,通常會創建一個獨立不可見的窗口)
CreateWindowEx
。
以下2點需要注意:
托盤控件的
ContextMenu
菜單MenuItem
在使用binding
時無效,是因為DataContext
沒有帶過去,需要重新賦值一次。托盤控件發送
ShowBalloonTip
消息通知時候需新建Shell_NotifyIcon
。
Nuget 最新[2]
Install-Package WPFDevelopers
1.0.9.1-preview

1) NotifyIcon.cs 代碼如下:
using?System;
using?System.IO;
using?System.Runtime.InteropServices;
using?System.Threading;
using?System.Windows;
using?System.Windows.Controls;
using?System.Windows.Controls.Primitives;
using?System.Windows.Data;
using?System.Windows.Input;
using?System.Windows.Media;
using?System.Windows.Media.Imaging;
using?WPFDevelopers.Controls.Runtimes;
using?WPFDevelopers.Controls.Runtimes.Interop;
using?WPFDevelopers.Controls.Runtimes.Shell32;
using?WPFDevelopers.Controls.Runtimes.User32;namespace?WPFDevelopers.Controls
{public?class?NotifyIcon?:?FrameworkElement,?IDisposable{private?static?NotifyIcon?NotifyIconCache;public?static?readonly?DependencyProperty?ContextContentProperty?=?DependencyProperty.Register("ContextContent",?typeof(object),?typeof(NotifyIcon),?new?PropertyMetadata(default));public?static?readonly?DependencyProperty?IconProperty?=DependencyProperty.Register("Icon",?typeof(ImageSource),?typeof(NotifyIcon),new?PropertyMetadata(default,?OnIconPropertyChanged));public?static?readonly?DependencyProperty?TitleProperty?=DependencyProperty.Register("Title",?typeof(string),?typeof(NotifyIcon),new?PropertyMetadata(default,?OnTitlePropertyChanged));public?static?readonly?RoutedEvent?ClickEvent?=EventManager.RegisterRoutedEvent("Click",?RoutingStrategy.Bubble,typeof(RoutedEventHandler),?typeof(NotifyIcon));public?static?readonly?RoutedEvent?MouseDoubleClickEvent?=EventManager.RegisterRoutedEvent("MouseDoubleClick",?RoutingStrategy.Bubble,typeof(RoutedEventHandler),?typeof(NotifyIcon));private?static?bool?s_Loaded?=?false;private?static?NotifyIcon?s_NotifyIcon;//這是窗口名稱private?readonly?string?_TrayWndClassName;//這個是窗口消息名稱private?readonly?string?_TrayWndMessage;//這個是窗口消息回調(窗口消息都需要在此捕獲)private?readonly?WndProc?_TrayWndProc;private?Popup?_contextContent;private?bool?_doubleClick;//圖標句柄private?IntPtr?_hIcon?=?IntPtr.Zero;private?ImageSource?_icon;private?IntPtr?_iconHandle;private?int?_IsShowIn;//托盤對象private?NOTIFYICONDATA?_NOTIFYICONDATA;//這個是傳遞給托盤的鼠標消息idprivate?int?_TrayMouseMessage;//窗口句柄private?IntPtr?_TrayWindowHandle?=?IntPtr.Zero;//通過注冊窗口消息可以獲取唯一標識Idprivate?int?_WmTrayWindowMessage;private?bool?disposedValue;public?NotifyIcon(){_TrayWndClassName?=?$"WPFDevelopers_{Guid.NewGuid()}";_TrayWndProc?=?WndProc_CallBack;_TrayWndMessage?=?"TrayWndMessageName";_TrayMouseMessage?=?(int)WM.USER?+?1024;Start();if?(Application.Current?!=?null){//Application.Current.MainWindow.Closed?+=?(s,?e)?=>?Dispose();Application.Current.Exit?+=?(s,?e)?=>?Dispose();}NotifyIconCache?=?this;}static?NotifyIcon(){DataContextProperty.OverrideMetadata(typeof(NotifyIcon),?new?FrameworkPropertyMetadata(DataContextPropertyChanged));ContextMenuProperty.OverrideMetadata(typeof(NotifyIcon),?new?FrameworkPropertyMetadata(ContextMenuPropertyChanged));}private?static?void?DataContextPropertyChanged(DependencyObject?d,?DependencyPropertyChangedEventArgs?e)?=>((NotifyIcon)d).OnDataContextPropertyChanged(e);private?void?OnDataContextPropertyChanged(DependencyPropertyChangedEventArgs?e){UpdateDataContext(_contextContent,?e.OldValue,?e.NewValue);UpdateDataContext(ContextMenu,?e.OldValue,?e.NewValue);}private?void?UpdateDataContext(FrameworkElement?target,?object?oldValue,?object?newValue){if?(target?==?null?||?BindingOperations.GetBindingExpression(target,?DataContextProperty)?!=?null)?return;if?(ReferenceEquals(this,?target.DataContext)?||?Equals(oldValue,?target.DataContext)){target.DataContext?=?newValue????this;}}private?static?void?ContextMenuPropertyChanged(DependencyObject?d,?DependencyPropertyChangedEventArgs?e){var?ctl?=?(NotifyIcon)d;ctl.OnContextMenuPropertyChanged(e);}private?void?OnContextMenuPropertyChanged(DependencyPropertyChangedEventArgs?e)?=>UpdateDataContext((ContextMenu)e.NewValue,?null,?DataContext);public?object?ContextContent{get?=>?GetValue(ContextContentProperty);set?=>?SetValue(ContextContentProperty,?value);}public?ImageSource?Icon{get?=>?(ImageSource)GetValue(IconProperty);set?=>?SetValue(IconProperty,?value);}public?string?Title{get?=>?(string)GetValue(TitleProperty);set?=>?SetValue(TitleProperty,?value);}public?void?Dispose(){Dispose(true);GC.SuppressFinalize(this);}private?static?void?OnTitlePropertyChanged(DependencyObject?d,?DependencyPropertyChangedEventArgs?e){if?(d?is?NotifyIcon?trayService)trayService.ChangeTitle(e.NewValue?.ToString());}private?static?void?OnIconPropertyChanged(DependencyObject?d,?DependencyPropertyChangedEventArgs?e){if?(d?is?NotifyIcon?trayService){var?notifyIcon?=?(NotifyIcon)d;notifyIcon._icon?=?(ImageSource)e.NewValue;trayService.ChangeIcon();}}public?event?RoutedEventHandler?Click{add?=>?AddHandler(ClickEvent,?value);remove?=>?RemoveHandler(ClickEvent,?value);}public?event?RoutedEventHandler?MouseDoubleClick{add?=>?AddHandler(MouseDoubleClickEvent,?value);remove?=>?RemoveHandler(MouseDoubleClickEvent,?value);}private?static?void?Current_Exit(object?sender,?ExitEventArgs?e){s_NotifyIcon?.Dispose();s_NotifyIcon?=?default;}public?bool?Start(){RegisterClass(_TrayWndClassName,?_TrayWndProc,?_TrayWndMessage);LoadNotifyIconData(string.Empty);Show();return?true;}public?bool?Stop(){//銷毀窗體if?(_TrayWindowHandle?!=?IntPtr.Zero)if?(User32Interop.IsWindow(_TrayWindowHandle))User32Interop.DestroyWindow(_TrayWindowHandle);//反注冊窗口類if?(!string.IsNullOrWhiteSpace(_TrayWndClassName))User32Interop.UnregisterClassName(_TrayWndClassName,?Kernel32Interop.GetModuleHandle(default));//銷毀Iconif?(_hIcon?!=?IntPtr.Zero)User32Interop.DestroyIcon(_hIcon);Hide();return?true;}///?<summary>///?????注冊并創建窗口對象///?</summary>///?<param?name="className">窗口名稱</param>///?<param?name="messageName">窗口消息名稱</param>///?<returns></returns>private?bool?RegisterClass(string?className,?WndProc?wndproccallback,?string?messageName){var?wndClass?=?new?WNDCLASSEX{cbSize?=?Marshal.SizeOf(typeof(WNDCLASSEX)),style?=?0,lpfnWndProc?=?wndproccallback,cbClsExtra?=?0,cbWndExtra?=?0,hInstance?=?IntPtr.Zero,hCursor?=?IntPtr.Zero,hbrBackground?=?IntPtr.Zero,lpszMenuName?=?string.Empty,lpszClassName?=?className};//注冊窗體對象User32Interop.RegisterClassEx(ref?wndClass);//注冊消息獲取對應消息標識id_WmTrayWindowMessage?=?User32Interop.RegisterWindowMessage(messageName);//創建窗體(本質上托盤在創建時需要一個窗口句柄,完全可以將主窗體的句柄給進去,但是為了更好的管理消息以及托盤的生命周期,通常會創建一個獨立不可見的窗口)_TrayWindowHandle?=?User32Interop.CreateWindowEx(0,?className,?"",?0,?0,?0,?1,?1,?IntPtr.Zero,?IntPtr.Zero,IntPtr.Zero,?IntPtr.Zero);return?true;}///?<summary>///?????創建托盤對象///?</summary>///?<param?name="icon">圖標路徑,可以修改托盤圖標(本質上是可以接受用戶傳入一個圖片對象,然后將圖片轉成Icon,但是算了這個有點復雜)</param>///?<param?name="title">托盤的tooltip</param>///?<returns></returns>private?bool?LoadNotifyIconData(string?title){lock?(this){_NOTIFYICONDATA?=?NOTIFYICONDATA.GetDefaultNotifyData(_TrayWindowHandle);if?(_TrayMouseMessage?!=?0)_NOTIFYICONDATA.uCallbackMessage?=?(uint)_TrayMouseMessage;else_TrayMouseMessage?=?(int)_NOTIFYICONDATA.uCallbackMessage;if?(_iconHandle?==?IntPtr.Zero){var?processPath?=?Kernel32Interop.GetModuleFileName(new?HandleRef());if?(!string.IsNullOrWhiteSpace(processPath)){var?index?=?IntPtr.Zero;var?hIcon?=?Shell32Interop.ExtractAssociatedIcon(IntPtr.Zero,?processPath,?ref?index);_NOTIFYICONDATA.hIcon?=?hIcon;_hIcon?=?hIcon;}}if?(!string.IsNullOrWhiteSpace(title))_NOTIFYICONDATA.szTip?=?title;}return?true;}private?bool?Show(){var?command?=?NotifyCommand.NIM_Add;if?(Thread.VolatileRead(ref?_IsShowIn)?==?1)command?=?NotifyCommand.NIM_Modify;elseThread.VolatileWrite(ref?_IsShowIn,?1);lock?(this){return?Shell32Interop.Shell_NotifyIcon(command,?ref?_NOTIFYICONDATA);}}internal?static?int?AlignToBytes(double?original,?int?nBytesCount){var?nBitsCount?=?8?<<?(nBytesCount?-?1);return?((int)Math.Ceiling(original)?+?(nBitsCount?-?1))?/?nBitsCount?*?nBitsCount;}private?static?byte[]?GenerateMaskArray(int?width,?int?height,?byte[]?colorArray){var?nCount?=?width?*?height;var?bytesPerScanLine?=?AlignToBytes(width,?2)?/?8;var?bitsMask?=?new?byte[bytesPerScanLine?*?height];for?(var?i?=?0;?i?<?nCount;?i++){var?hPos?=?i?%?width;var?vPos?=?i?/?width;var?byteIndex?=?hPos?/?8;var?offsetBit?=?(byte)(0x80?>>?(hPos?%?8));if?(colorArray[i?*?4?+?3]?==?0x00)bitsMask[byteIndex?+?bytesPerScanLine?*?vPos]?|=?offsetBit;elsebitsMask[byteIndex?+?bytesPerScanLine?*?vPos]?&=?(byte)~offsetBit;if?(hPos?==?width?-?1?&&?width?==?8)?bitsMask[1?+?bytesPerScanLine?*?vPos]?=?0xff;}return?bitsMask;}private?byte[]?BitmapImageToByteArray(BitmapImage?bmp){byte[]?bytearray?=?null;try{var?smarket?=?bmp.StreamSource;if?(smarket?!=?null?&&?smarket.Length?>?0){//設置當前位置smarket.Position?=?0;using?(var?br?=?new?BinaryReader(smarket)){bytearray?=?br.ReadBytes((int)smarket.Length);}}}catch?(Exception?ex){}return?bytearray;}private?byte[]?ConvertBitmapSourceToBitmapImage(BitmapSource?bitmapSource){byte[]?imgByte?=?default;if?(!(bitmapSource?is?BitmapImage?bitmapImage)){bitmapImage?=?new?BitmapImage();var?encoder?=?new?BmpBitmapEncoder();encoder.Frames.Add(BitmapFrame.Create(bitmapSource));using?(var?memoryStream?=?new?MemoryStream()){encoder.Save(memoryStream);memoryStream.Position?=?0;bitmapImage.BeginInit();bitmapImage.CacheOption?=?BitmapCacheOption.OnLoad;bitmapImage.StreamSource?=?memoryStream;bitmapImage.EndInit();imgByte?=?BitmapImageToByteArray(bitmapImage);}}return?imgByte;}internal?static?IconHandle?CreateIconCursor(byte[]?xor,?int?width,?int?height,?int?xHotspot,int?yHotspot,?bool?isIcon){var?bits?=?IntPtr.Zero;BitmapHandle?colorBitmap?=?null;var?bi?=?new?BITMAPINFO(width,?-height,?32){bmiHeader_biCompression?=?0};colorBitmap?=?Gdi32Interop.CreateDIBSection(new?HandleRef(null,?IntPtr.Zero),?ref?bi,?0,?ref?bits,?null,?0);if?(colorBitmap.IsInvalid?||?bits?==?IntPtr.Zero)?return?IconHandle.GetInvalidIcon();Marshal.Copy(xor,?0,?bits,?xor.Length);var?maskArray?=?GenerateMaskArray(width,?height,?xor);var?maskBitmap?=?Gdi32Interop.CreateBitmap(width,?height,?1,?1,?maskArray);if?(maskBitmap.IsInvalid)?return?IconHandle.GetInvalidIcon();var?iconInfo?=?new?Gdi32Interop.ICONINFO{fIcon?=?isIcon,xHotspot?=?xHotspot,yHotspot?=?yHotspot,hbmMask?=?maskBitmap,hbmColor?=?colorBitmap};return?User32Interop.CreateIconIndirect(iconInfo);}private?bool?ChangeIcon(){var?bitmapFrame?=?_icon?as?BitmapFrame;if?(bitmapFrame?!=?null?&&?bitmapFrame.Decoder?!=?null)if?(bitmapFrame.Decoder?is?IconBitmapDecoder){//var?iconBitmapDecoder?=?new?Rect(0,?0,?_icon.Width,?_icon.Height);//var?dv?=?new?DrawingVisual();//var?dc?=?dv.RenderOpen();//dc.DrawImage(_icon,?iconBitmapDecoder);//dc.Close();//var?bmp?=?new?RenderTargetBitmap((int)_icon.Width,?(int)_icon.Height,?96,?96,//????PixelFormats.Pbgra32);//bmp.Render(dv);//BitmapSource?bitmapSource?=?bmp;//if?(bitmapSource.Format?!=?PixelFormats.Bgra32?&&?bitmapSource.Format?!=?PixelFormats.Pbgra32)//????bitmapSource?=?new?FormatConvertedBitmap(bitmapSource,?PixelFormats.Bgra32,?null,?0.0);var?w?=?bitmapFrame.PixelWidth;var?h?=?bitmapFrame.PixelHeight;var?bpp?=?bitmapFrame.Format.BitsPerPixel;var?stride?=?(bpp?*?w?+?31)?/?32?*?4;var?sizeCopyPixels?=?stride?*?h;var?xor?=?new?byte[sizeCopyPixels];bitmapFrame.CopyPixels(xor,?stride,?0);var?iconHandle?=?CreateIconCursor(xor,?w,?h,?0,?0,?true);_iconHandle?=?iconHandle.CriticalGetHandle();}if?(Thread.VolatileRead(ref?_IsShowIn)?!=?1)return?false;if?(_hIcon?!=?IntPtr.Zero){User32Interop.DestroyIcon(_hIcon);_hIcon?=?IntPtr.Zero;}lock?(this){if?(_iconHandle?!=?IntPtr.Zero){var?hIcon?=?_iconHandle;_NOTIFYICONDATA.hIcon?=?hIcon;_hIcon?=?hIcon;}else{_NOTIFYICONDATA.hIcon?=?IntPtr.Zero;}return?Shell32Interop.Shell_NotifyIcon(NotifyCommand.NIM_Modify,?ref?_NOTIFYICONDATA);}}private?bool?ChangeTitle(string?title){if?(Thread.VolatileRead(ref?_IsShowIn)?!=?1)return?false;lock?(this){_NOTIFYICONDATA.szTip?=?title;return?Shell32Interop.Shell_NotifyIcon(NotifyCommand.NIM_Modify,?ref?_NOTIFYICONDATA);}}public?static?void?ShowBalloonTip(string?title,?string?content,?NotifyIconInfoType?infoType){if?(NotifyIconCache?!=?null)NotifyIconCache.ShowBalloonTips(title,?content,?infoType);}public?void?ShowBalloonTips(string?title,?string?content,?NotifyIconInfoType?infoType){if?(Thread.VolatileRead(ref?_IsShowIn)?!=?1)return;var?_ShowNOTIFYICONDATA?=?NOTIFYICONDATA.GetDefaultNotifyData(_TrayWindowHandle);_ShowNOTIFYICONDATA.uFlags?=?NIFFlags.NIF_INFO;_ShowNOTIFYICONDATA.szInfoTitle?=?title????string.Empty;_ShowNOTIFYICONDATA.szInfo?=?content????string.Empty;switch?(infoType){case?NotifyIconInfoType.Info:_ShowNOTIFYICONDATA.dwInfoFlags?=?NIIFFlags.NIIF_INFO;break;case?NotifyIconInfoType.Warning:_ShowNOTIFYICONDATA.dwInfoFlags?=?NIIFFlags.NIIF_WARNING;break;case?NotifyIconInfoType.Error:_ShowNOTIFYICONDATA.dwInfoFlags?=?NIIFFlags.NIIF_ERROR;break;case?NotifyIconInfoType.None:_ShowNOTIFYICONDATA.dwInfoFlags?=?NIIFFlags.NIIF_NONE;break;}Shell32Interop.Shell_NotifyIcon(NotifyCommand.NIM_Modify,?ref?_ShowNOTIFYICONDATA);}private?bool?Hide(){var?isShow?=?Thread.VolatileRead(ref?_IsShowIn);if?(isShow?!=?1)return?true;Thread.VolatileWrite(ref?_IsShowIn,?0);lock?(this){return?Shell32Interop.Shell_NotifyIcon(NotifyCommand.NIM_Delete,?ref?_NOTIFYICONDATA);}}private?IntPtr?WndProc_CallBack(IntPtr?hwnd,?WM?msg,?IntPtr?wParam,?IntPtr?lParam){//這是窗口相關的消息if?((int)msg?==?_WmTrayWindowMessage){}else?if?((int)msg?==?_TrayMouseMessage)?//這是托盤上鼠標相關的消息{switch?((WM)(long)lParam){case?WM.LBUTTONDOWN:break;case?WM.LBUTTONUP:WMMouseUp(MouseButton.Left);break;case?WM.LBUTTONDBLCLK:WMMouseDown(MouseButton.Left,?2);break;case?WM.RBUTTONDOWN:break;case?WM.RBUTTONUP:OpenMenu();break;case?WM.MOUSEMOVE:break;case?WM.MOUSEWHEEL:break;}}else?if?(msg?==?WM.COMMAND){}return?User32Interop.DefWindowProc(hwnd,?msg,?wParam,?lParam);}private?void?WMMouseUp(MouseButton?button){if?(!_doubleClick?&&?button?==?MouseButton.Left)RaiseEvent(new?MouseButtonEventArgs(Mouse.PrimaryDevice,Environment.TickCount,?button){RoutedEvent?=?ClickEvent});_doubleClick?=?false;}private?void?WMMouseDown(MouseButton?button,?int?clicks){if?(clicks?==?2){RaiseEvent(new?MouseButtonEventArgs(Mouse.PrimaryDevice,Environment.TickCount,?button){RoutedEvent?=?MouseDoubleClickEvent});_doubleClick?=?true;}}private?void?OpenMenu(){if?(ContextContent?!=?null){_contextContent?=?new?Popup{Placement?=?PlacementMode.Mouse,AllowsTransparency?=?true,StaysOpen?=?false,UseLayoutRounding?=?true,SnapsToDevicePixels?=?true};_contextContent.Child?=?new?ContentControl{Content?=?ContextContent};UpdateDataContext(_contextContent,?null,?DataContext);_contextContent.IsOpen?=?true;User32Interop.SetForegroundWindow(_contextContent.Child.GetHandle());}else?if?(ContextMenu?!=?null){if?(ContextMenu.Items.Count?==?0)?return;ContextMenu.InvalidateProperty(StyleProperty);foreach?(var?item?in?ContextMenu.Items)if?(item?is?MenuItem?menuItem){menuItem.InvalidateProperty(StyleProperty);}else{var?container?=?ContextMenu.ItemContainerGenerator.ContainerFromItem(item)?as?MenuItem;container?.InvalidateProperty(StyleProperty);}ContextMenu.Placement?=?PlacementMode.Mouse;ContextMenu.IsOpen?=?true;User32Interop.SetForegroundWindow(ContextMenu.GetHandle());}}protected?virtual?void?Dispose(bool?disposing){if?(!disposedValue){if?(disposing)Stop();disposedValue?=?true;}}}public?enum?NotifyIconInfoType{///?<summary>///?????No?Icon.///?</summary>None,///?<summary>///?????A?Information?Icon.///?</summary>Info,///?<summary>///?????A?Warning?Icon.///?</summary>Warning,///?<summary>///?????A?Error?Icon.///?</summary>Error}
}
2) NotifyIconExample.xaml 代碼如下:
ContextMenu
使用如下:
<wpfdev:NotifyIcon?Title="WPF開發者"><wpfdev:NotifyIcon.ContextMenu><ContextMenu><MenuItem?Header="托盤消息"?Click="SendMessage_Click"/><MenuItem?Header="退出"?Click="Quit_Click"/></ContextMenu></wpfdev:NotifyIcon.ContextMenu></wpfdev:NotifyIcon>
ContextContent
使用如下:
<wpfdev:NotifyIcon?Title="WPF開發者"><wpfdev:NotifyIcon.ContextContent><Border?CornerRadius="3"?Margin="10"?Background="{DynamicResource?BackgroundSolidColorBrush}"?Effect="{StaticResource?NormalShadowDepth}"><StackPanel?VerticalAlignment="Center"?Margin="16"><Rectangle?Width="100"?Height="100"><Rectangle.Fill><ImageBrush?ImageSource="pack://application:,,,/Logo.ico"/></Rectangle.Fill></Rectangle><StackPanel?Margin="0,16,0,0"?HorizontalAlignment="Center"?Orientation="Horizontal"><Button?MinWidth="100"?Content="關于"?Style="{DynamicResource?PrimaryButton}"?Command="{Binding?GithubCommand}"?/><Button?Margin="16,0,0,0"?MinWidth="100"?Content="退出"?Click="Quit_Click"/></StackPanel></StackPanel></Border></wpfdev:NotifyIcon.ContextContent></wpfdev:NotifyIcon>
3) NotifyIconExample.cs 代碼如下:
ContextMenu
使用如下:
private?void?Quit_Click(object?sender,?RoutedEventArgs?e){Application.Current.Shutdown();}private?void?SendMessage_Click(object?sender,?RoutedEventArgs?e){NotifyIcon.ShowBalloonTip("Message",?"?Welcome?to?WPFDevelopers.Minimal?",?NotifyIconInfoType.None);}
ContextContent
使用如下:
private?void?Quit_Click(object?sender,?RoutedEventArgs?e){Application.Current.Shutdown();}private?void?SendMessage_Click(object?sender,?RoutedEventArgs?e){NotifyIcon.ShowBalloonTip("Message",?"?Welcome?to?WPFDevelopers.Minimal?",?NotifyIconInfoType.None);}
?鳴謝 - 吳鋒

Github|NotifyIcon[3]
碼云|NotifyIcon[4]
參考資料
[1]
Win32API: https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataw
[2]Nuget : https://www.nuget.org/packages/WPFDevelopers/
[3]Github|NotifyIcon: https://github.com/WPFDevelopersOrg/WPFDevelopers/blob/master/src/WPFDevelopers.Samples/ExampleViews/MainWindow.xaml
[4]碼云|NotifyIcon: https://gitee.com/WPFDevelopersOrg/WPFDevelopers/blob/master/src/WPFDevelopers.Samples/ExampleViews/MainWindow.xaml