WPF 基础控件之托盘
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WPF 基础控件之托盘相关的知识,希望对你有一定的参考价值。
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;
//这个是传递给托盘的鼠标消息id
private int _TrayMouseMessage;
//窗口句柄
private IntPtr _TrayWindowHandle = IntPtr.Zero;
//通过注册窗口消息可以获取唯一标识Id
private 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));
//销毁Icon
if (_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;
else
Thread.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;
else
bitsMask[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
以上是关于WPF 基础控件之托盘的主要内容,如果未能解决你的问题,请参考以下文章