WPF 基础控件之托盘
框架使用大于等于.NET40
。
Visual Studio 2022
。
项目使用 MIT 开源许可协议。
新建NotifyIcon
自定义控件继承自FrameworkElement
。
创建托盘程序主要借助与 Win32API:
- 注册窗体对象
RegisterClassEx
。 - 注册消息获取对应消息标识
Id
RegisterWindowMessage
。 - 创建窗体(本质上托盘在创建时需要一个窗口句柄,完全可以将主窗体的句柄给进去,但是为了更好的管理消息以及托盘的生命周期,通常会创建一个独立不可见的窗口)
CreateWindowEx
。
以下2点需要注意:
- 托盘控件的
ContextMenu
菜单MenuItem
在使用binding
时无效,是因为DataContext
没有带过去,需要重新赋值一次。 - 托盘控件发送
ShowBalloonTip
消息通知时候需新建Shell_NotifyIcon
。 - Nuget 最新
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);
}
实现效果
以上就是WPF实现基础控件之托盘的示例代码的详细内容,更多关于WPF托盘的资料请关注编程网其它相关文章!