开发推广

WPF实现基础程式的托盘(含基础代码)

开发技术动态随笔 ·

这里主要为大家详细介绍了如何利用WPF实现托盘这一基础控件,文中的示例代码讲解详细,对我们学习或工作有一定帮助,感兴趣的朋友可以了解一下;

首先什么是WDF?

基于Windows 的用户界面框架,属于.NET Framework 3.0的一部分

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

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
}

}

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>

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实现基础程式.gif