使用Shell_NotifyIcon 建一个标准气泡通知盒图标

你是第23位浏览该文章的人 ArLi2003 csdn 2003-9-15

虽然.net 扩展并平民化了大部分的API,但就notifyicon 来讲,系统自己的notifyicon 做的必然安全,以至于 shell32.dll 新版本(此气泡功能是win98 的ie5 就具备)的功能完全得不到体现,在CSDN 我也见过不少的人都在为扩展一个更完美的notifyicon 做努力,为此笔者就在中秋前夜的一些空闲写了这个扩展控件权砖引玉,这二天就有不少朋友扔鸡蛋,于是特撰此稿来补充此控件的功能及它的具体内容以便于朋友们的二次开发,本文也定义了大部分的一些的常数和结构。

组件类特点如下

标准Shell32.dll 的气泡提示,随系统升级
支持左、右、中三键点击回调
支持右键菜单(Handle 引用,所以可以自己动态建也可以用.net 自己的ContextMenu)
支持动态图标(Handle 引用,所以可以自己画或资源/外部引用,然后定时改变,用此组件的 ModiNotifyBox 对原有的图标进行修改,它的参数和 AddNotifyBox 一样,只要改变它的icon 句柄为新 icon 的句柄就行了)
不使用静态,允许多个Icon 互不冲突,可用于多窗体软件

类内容如下

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace ArLi.CommonPrj //要改此名要注意改 InnerClass 里的引用
{
    using System.IO;
    using System.Diagnostics;

    /// <summary>
    /// Shell_NotifyIconEx Class.
    public class Shell_NotifyIconEx
    {
        /// <summary>
        /// ArLi, last fix: 2003.9.12, reference: ArLi.CommonPrj Lib @ http://zpcity.com/arli/
        /// </summary>
        public static readonly System.Version myVersion = new System.Version(1,2); //版本声明

        private readonly InnerClass.FormTmp formTmp = null; // 这个很重要,不能放在构造里,因为它必须和此实例同等生存期才不会被中止消息循环
        private readonly IntPtr formTmpHwnd = IntPtr.Zero; // 这是上一行的句柄
        private readonly bool VersionOk = false; // 这是一个由VersionPass 返回的属性,它允许开发者检测当前机子的Shell32.dll(可能在win95 或未知平台上版本) 合适此组,不符则用.net 自己的notifyicon
        private bool forgetDelNotifyBox = false; // 这是一个私有标志,它允许开发者在程序退出时忘记调用DelNotifyBox 来清除图标时会自动在析构里清掉它。

        internal IntPtr formHwnd = IntPtr.Zero; // 这是调用此组件的主窗口句柄(当前实例有效,可多个icon 不冲突)
        internal IntPtr contextMenuHwnd = IntPtr.Zero; // 这是菜单的句柄(当前实例有效,可多个icon 不冲突)

        internal delegate void delegateOfCallBack(System.Windows.Forms.MouseButtons mb);
        internal delegateOfCallBack _delegateOfCallBack = null;

        public Shell_NotifyIconEx() // 构造
        {
            WM_NOTIFY_TRAY += 1; // 消息ID +1,避免多个ICON 消息处理冲突
            uID += 1; // 同上
            formTmp = new InnerClass.FormTmp(this); // 新实例一个消息循环
            formTmpHwnd = formTmp.Handle; // 新实例句柄
            VersionOk = this.GetShell32VersionInfo() >= 5; // 版本是否合适,此组件由于重点在气泡提示,它要求Shell32.dll 5.0(ie 5.0) 以上
        }

        ~ Shell_NotifyIconEx() { // 析构
            if (forgetDelNotifyBox) this.DelNotifyBox(); //如果开发者忘记则清理icon
        }

        #region API_Consts
        internal readonly int WM_NOTIFY_TRAY = 0x0400 + 2001; //readonly 表示只在构造可付值
        internal readonly int uID = 5000;

        // 常数定义,有VC 的可以参见 shellapi.h
        private const int NIIF_NONE = 0x00;
        private const int NIIF_INFO = 0x01;
        private const int NIIF_WARNING = 0x02;
        private const int NIIF_ERROR = 0x03;

        private const int NIF_MESSAGE = 0x01;
        private const int NIF_ICON = 0x02;
        private const int NIF_TIP = 0x04;
        private const int NIF_STATE = 0x08;
        private const int NIF_INFO = 0x10;

        private const int NIM_ADD = 0x00;
        private const int NIM_MODIFY = 0x01;
        private const int NIM_DELETE = 0x02;
        private const int NIM_SETFOCUS = 0x03;
        private const int NIM_SETVERSION = 0x04;

        private const int NIS_HIDDEN = 0x01;
        private const int NIS_SHAREDICON = 0x02;

        private const int NOTIFYICON_OLDVERSION = 0x00;
        private const int NOTIFYICON_VERSION = 0x03;

        [DllImport("shell32.dll", EntryPoint="Shell_NotifyIcon")]
        private static extern bool Shell_NotifyIcon( // 这位是主角
            int dwMessage,
            ref NOTIFYICONDATA lpData
        );

        /// <summary>
        /// 此API 的作用是当 this.focus() 无效时可以考虑使用,效果很好
        /// </summary>
        /// <param name="hwnd">this.Handle, 当前窗体句柄</param>
        [DllImport("user32.dll", EntryPoint="SetForegroundWindow")]
        public static extern int SetForegroundWindow (
            IntPtr hwnd
        );

        [StructLayout(LayoutKind.Sequential)]
        private struct NOTIFYICONDATA { // 主角用的结构
            internal int cbSize;
            internal IntPtr hwnd;
            internal int uID;
            internal int uFlags;
            internal int uCallbackMessage;
            internal IntPtr hIcon;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst=0x80)]
            internal string szTip;
            internal int dwState; // 这里往下几个是 5.0 的精华
            internal int dwStateMask;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst=0xFF)]
            internal string szInfo;
            internal int uTimeoutAndVersion;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst=0x40)]
            internal string szInfoTitle;
            internal int dwInfoFlags;
        }
        #endregion

        /// <summary>
        /// 建一个结构
        /// </summary>
        private NOTIFYICONDATA GetNOTIFYICONDATA(IntPtr iconHwnd, string sTip, string boxTitle, string boxText) {
            NOTIFYICONDATA nData = new NOTIFYICONDATA();

            nData.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(nData); // 结构的大小
            nData.hwnd = formTmpHwnd; // 处理消息循环的窗体句柄,可以移成主窗体
            nData.uID = uID; // 消息的 WParam,回调时用
            nData.uFlags = NIF_MESSAGE|NIF_ICON|NIF_TIP|NIF_INFO; // 标志,表示由消息、图标、提示、信息组成
            nData.uCallbackMessage = WM_NOTIFY_TRAY; // 消息ID,回调用
            nData.hIcon = iconHwnd; // 图标的句柄,有兴趣的话可以定时改变它变成动画ICON
            nData.uTimeoutAndVersion = 10 * 1000 | NOTIFYICON_VERSION; // 提示的超时值(几秒后自动消失)和版本
            nData.dwInfoFlags = NIIF_INFO; // 类型标志,有INFO、WARNING、ERROR,更改此值将影响气泡提示框的图标类型

            nData.szTip = sTip; // 图标的提示信息
            nData.szInfoTitle = boxTitle; // 气泡提示框的标题
            nData.szInfo = boxText; // 气泡提示框的提示内容

            return nData; // 这个嘛。。。
        }

        private int GetShell32VersionInfo() { // 返回shell32 的版本
            FileInfo fi = new FileInfo(Path.Combine(System.Environment.SystemDirectory,"shell32.dll")); //将来的平台shell32 放哪目前不得而知,碰到再改
            if (fi.Exists) {
                FileVersionInfo theVersion = FileVersionInfo.GetVersionInfo(fi.FullName);
                int i = theVersion.FileVersion.IndexOf('.');
                if (i > 0) {
                    try {
                        return int.Parse(theVersion.FileVersion.Substring(0,i));
                    }
                    catch{}
                }
            }
            return 0;
        }

        /// <summary>
        /// 加一个新图标
        /// </summary>
        /// <param name="iconHwnd">图标句柄</param>
        /// <param name="sTip">提示, 5.0 最大: 128 char</param>
        /// <param name="boxTitle">气泡标题, 最大: 64 char</param>
        /// <param name="boxText">气泡内容, 最大: 256 char</param>
        /// <returns>成功、失败或错误(-1)</returns>
        public int AddNotifyBox(IntPtr iconHwnd, string sTip, string boxTitle, string boxText) {
            if (! this.VersionOk) return -1;

            NOTIFYICONDATA nData = GetNOTIFYICONDATA(iconHwnd, sTip, boxTitle, boxText);
            if (Shell_NotifyIcon(NIM_ADD,ref nData)) {
                this.forgetDelNotifyBox = true;
                return 1;
            }
            else {
                return 0;
            }
        }

        /// <summary>
        /// 和add 差不多,不重复了
        /// </summary>
        public int DelNotifyBox() {
            if (! this.VersionOk) return -1;

            NOTIFYICONDATA nData = GetNOTIFYICONDATA(IntPtr.Zero, null, null, null);
            if (Shell_NotifyIcon(NIM_DELETE,ref nData)) {
                this.forgetDelNotifyBox = false;
                return 1;
            }
            else {
                return 0;
            }
        }

        public int ModiNotifyBox(IntPtr iconHwnd, string sTip, string boxTitle, string boxText) {
            if (! this.VersionOk) return -1;

            NOTIFYICONDATA nData = GetNOTIFYICONDATA(iconHwnd, sTip, boxTitle, boxText);
            return Shell_NotifyIcon(NIM_MODIFY,ref nData) ? 1 : 0;
        }

        #region Optional Module //这里是可选方法
        /// <summary>
        /// 连接一个已存在的 contextMenu
        /// </summary>
        /// <param name="_formHwnd">窗体句柄,用来处理菜单的消息</param>
        /// <param name="_contextMenuHwnd">菜单的句柄</param>
        public void ConnectMyMenu(IntPtr _formHwnd, IntPtr _contextMenuHwnd) {
            formHwnd = _formHwnd;
            contextMenuHwnd = _contextMenuHwnd;
        }

        /// <summary>
        /// 立即清理掉图标、委托和formtmp 资源(好象没什么资源,考虑到可能二次开发挂接就开了这个东东)
        /// </summary>
        public void Dispose() {
            _delegateOfCallBack = null;
            this.formTmp.Dispose();
        }

        /// <summary>
        /// 版本适合
        /// </summary>
        public bool VersionPass {
            get {
                return this.VersionOk;
            }
        }
        #endregion
    }
}

#region FormTmp
namespace ArLi.CommonPrj.InnerClass {
    internal class FormTmp : System.Windows.Forms.Form {

        private ArLi.CommonPrj.Shell_NotifyIconEx servicesClass = null; // 接受主CLASS 的实例句柄
        internal FormTmp(ArLi.CommonPrj.Shell_NotifyIconEx _servicesClass) {
            servicesClass = _servicesClass;
        }

        private const int WM_LBUTTONDOWN = 0x0201; // 左键
        private const int WM_RBUTTONDOWN = 0x204; // 右键
        private const int WM_MBUTTONDOWN = 0x207; // 中键

        [DllImport("user32.dll", EntryPoint="TrackPopupMenu")]
        private static extern int TrackPopupMenu ( // c# 和vb.net 好象没有了随地popup 了,只要请它老人家出马了
            IntPtr hMenu,
            int wFlags,
            int x,
            int y,
            int nReserved,
            IntPtr hwnd,
            ref RECT lprc
        );

        [StructLayout(LayoutKind.Sequential)]
        private struct RECT { // 上面那位用的结构,表示前弹出菜单可用的一个范围大小(一般是全屏幕都让它用,留着搞游戏或视频对话之类的朋友指定菜单可用的范围)
            internal int Left;
            internal int Top;
            internal int Right;
            internal int Bottom;
        }

        protected override void WndProc(ref Message msg) {
            if (msg.Msg == servicesClass.WM_NOTIFY_TRAY) { // 如果消息相符
                if ((int)msg.WParam == servicesClass.uID) { // 并且消息的WParam 相符
                    System.Windows.Forms.MouseButtons mb = System.Windows.Forms.MouseButtons.None;
                    if((int)msg.LParam == WM_LBUTTONDOWN) { //如果点击的是左键
                        mb = System.Windows.Forms.MouseButtons.Left;
                    }
                    else if((int)msg.LParam == WM_MBUTTONDOWN) { //中键
                        mb = System.Windows.Forms.MouseButtons.Middle;
                    }
                    else if((int)msg.LParam == WM_RBUTTONDOWN) { //右键
                        if (servicesClass.contextMenuHwnd != IntPtr.Zero) { //如果有定义过菜单关联
                            RECT r = new RECT(); 
                            r.Left = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Left;
                            r.Right = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Right;
                            r.Top = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Top;
                            r.Bottom = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Right;

                            TrackPopupMenu(servicesClass.contextMenuHwnd, 2, System.Windows.Forms.Cursor.Position.X, System.Windows.Forms.Cursor.Position.Y, 0, servicesClass.formHwnd, ref r);
                        }
                        else { //如果没有定义过菜单关联
                            mb = System.Windows.Forms.MouseButtons.Right;
                        }
                    }

                    if (mb != System.Windows.Forms.MouseButtons.None && servicesClass._delegateOfCallBack != null) {
                        servicesClass._delegateOfCallBack(mb); // 执行回调
                        return;
                    }
                }
            }
            base.WndProc(ref msg);
        }
    }
}
#endregion

用法示例

简单示例

private void button2_Click(object sender, System.EventArgs e) {
    new ArLi.CommonPrj.Shell_NotifyIconEx().AddNotifyBox(this.Icon.Handle,this.Text,"zpcity.com/arli","单击这里开始,我将带你畅游API 世界");
}

支持菜单和回调的示例

private void GetPoc1(MouseButtons mb) { // 回调处理
    if (mb == MouseButtons.Left) {
        MessageBox.Show("来自菜单1");
    }
}
private ArLi.CommonPrj.Shell_NotifyIconEx o1 = new ArLi.CommonPrj.Shell_NotifyIconEx(); //这个放外面是用在 o.DelNotifyBox
private void button1_Click(object sender, System.EventArgs e) {
    o1.AddNotifyBox(this.Icon.Handle,this.Text,"菜单1","单击这里开始,我将带你畅游API 世界"); 
    o1.ConnectMyMenu(this.Handle,this.contextMenu1.Handle); // 挂上菜单,可选
    o1._delegateOfCallBack = new ArLi.CommonPrj.Shell_NotifyIconEx.delegateOfCallBack(GetPoc1); //定义回调
}

多个notifyicon 的高级应用

private void GetPoc1(MouseButtons mb) { // 回调处理
    if (mb == MouseButtons.Left) {
        MessageBox.Show("来自菜单1");
    }
}
private ArLi.CommonPrj.Shell_NotifyIconEx o1 = new ArLi.CommonPrj.Shell_NotifyIconEx(); //这个放外面是用在 o.DelNotifyBox
private void button1_Click(object sender, System.EventArgs e) {
    o1.AddNotifyBox(this.Icon.Handle,this.Text,"菜单1","单击这里开始,我将带你畅游API 世界");
    o1.ConnectMyMenu(this.Handle,this.contextMenu1.Handle); // 挂上菜单,可选
    o1._delegateOfCallBack = new ArLi.CommonPrj.Shell_NotifyIconEx.delegateOfCallBack(GetPoc1); //定义回调
}
private void GetPoc2(MouseButtons mb) {
    if (mb == MouseButtons.Left) {
        MessageBox.Show("来自菜单2");
    }
}
private ArLi.CommonPrj.Shell_NotifyIconEx o2 = new ArLi.CommonPrj.Shell_NotifyIconEx(); //第二个nofityicon 和上面一样
private void button2_Click(object sender, System.EventArgs e) {
    o2.AddNotifyBox(this.Icon.Handle,this.Text,"菜单2","单击这里开始,我将带你畅游API 世界");
    o2.ConnectMyMenu(this.Handle,this.contextMenu2.Handle);
    o2._delegateOfCallBack = new ArLi.CommonPrj.Shell_NotifyIconEx.delegateOfCallBack(GetPoc2);
}

编译成DLL 组件给其它语言

csc /t:library /doc:./cls_Shell_NotifyIconEx.xml cls_Shell_NotifyIconEx.cs

关于检测版本是否相符

if (! o2.VersionPass) { // 有可能是win98 第一版或95
    //todo: 用.net 自己的notifyicon
}

关于ModiNotifyBox

它的用法和 AddNotifyBox 基本一样,它主要用来修改原有的图标,比如 MailCheck 模块要用到的实时多次提示、动画图标等等

Contributors: FHL