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); | |
} |
实现效果