前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WPF实现基础控件之托盘的示例代码分享

WPF实现基础控件之托盘的示例代码分享

原创
作者头像
用户7718188
发布2022-11-06 20:15:57
7280
发布2022-11-06 20:15:57
举报
文章被收录于专栏:高级工程司

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

    }

}

3) NotifyIconExample.cs 代码如下:

 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);

        }

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档