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