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 基础控件之托盘的主要内容,如果未能解决你的问题,请参考以下文章

WPF 托盘闪烁

WPF 托盘图标右键弹出的ContextMenu如何关闭

C# winform 缩小到托盘 无法关机?

WPF之托盘图标的设定

WPF基础之资源

C++ Builder托盘控件