65.9K
CodeProject 正在变化。 阅读更多。
Home

可执行文件的智能通知器 | .NET Toast

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (50投票s)

2016 年 8 月 25 日

CPOL

7分钟阅读

viewsIcon

55171

downloadIcon

4711

一个相当简单的通知器(MessageBox的替代品),用于更好地表示可执行文件中的消息。其理念是提供一个类似于Android的“Toast”的解决方案,其中通知不会侵扰。它适用于Windows Form和WPF。

 

引言

该库的目的是提供一个新颖且简单的解决方案来表示应用程序的通知消息,而不是使用传统的MessageBox

MessageBox是一个非常强大的消息通知器,但它缺乏样式和实践。许多客户要求UI与当今市场保持一致。这个想法诞生于2008年,当时的一次大学作业:从该库的v3版本开始,还发布了一个WPF版本,它只是Windows Form(.NET 2.0)版本的一个简单移植(因此需要更多时间才能完成)。但是,您可以将其用于您的应用程序,尤其是用于调试目的。

另一个目标是让通知的内容易于更新:有时,有必要将MessageBox放在一个循环中,以(例如)快速报告错误。这通常会导致消息通知的瀑布。在这种情况下,我们需要一个可以保持打开状态、置顶显示、位置舒适且内容可更新的通知,这样就可以更新假设的错误计数器。

功能

  • 最多四种简单通知类型及相应颜色
    • Info(信息)
    • Error(错误)
    • 警告
    • Confirm
  • 可拖动通知器
  • 对话框样式通知器
  • 可定位通知器
  • 内容/标题相同的消息现在作为多条笔记处理:您将在笔记的标题中看到一个更新计数器
  • 全屏淡入背景(实验性)
  • 添加了SimpleLogger:一个自定义日志记录器,用于VS控制台或文件

Using the Code

要使用此代码,只需将引用添加到Notifier.dll库到您的项目中

// Common
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

// 1. Add the NS of the notifier
using Notify;

要使用WPF版本,请使用

using WPFNotify;

然后,要创建一个笔记,只需

 Notifier.Show("Note text", Notifier.Type.INFO, "TitleBar");

您必须指明笔记的文本、类型和标题。此外,还可以使用一个简单的调用,只带文本(默认类型为“Info”)。

您可以在4种样式中选择

  1. 警告
  2. 好的
  3. Error(错误)
  4. Info(信息)

preview

通知堆叠在活动屏幕的右下角。

在上图中,您可以看到此通知库的一个关键特性:您现在可以将相同的笔记作为一条笔记处理,并带有计数器更新。

此外,创建调用会返回该笔记的ID。我们可以使用此ID来更改通知内容

short ID = Notifier.Show("Note text", Notifier.Type.INFO, "TitleBar");

因此,可以使用此ID来引用已打开的通知以更新其内容

Notifier.Update(ID, "New Note text", Notifier.Type.OK, "New TitleBar");

简而言之

// Create a simple note
short ID = Notifier.Show("Hello World");

// Change the created note
Notifier.Update(ID, "Hello Mars");

它还引入了“关闭全部”功能:要访问它,请按关闭图标左侧的菜单图标打开通知菜单

menu icon

在打开的菜单中,选择“关闭全部”以关闭所有打开的通知;这将重置ID计数器。

查看代码

Notifier.dll库由一个Windows Form及其资源文件组成。在资源文件中存储着笔记的背景图像和图标。

WPFNotifier.dll库由一个Windows Presentation Foundation及其资源文件组成。在资源文件中存储着笔记的背景图像和图标。

以下代码描述指的是Windows Form版本。

全局变量

让我们从GLOBALS部分开始

#region GLOBALS      
        public enum Type { INFO, WARNING, ERROR, OK }                   // Set the type of the Notifier

        class NoteLocation                                              // Helper class to handle Note position
        {
            internal int X;
            internal int Y;

            internal Point initialLocation;                             // Mouse bar drag helpers
            internal bool mouseIsDown = false;

            public NoteLocation(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
        }

        static List<Notifier> notes             = new List<Notifier>(); // Keep a list of the opened Notifiers

        private NoteLocation noteLocation;                              // Note position
        private short ID                        = 0;                    // Note ID
        private string description              = "";                   // Note default Description
        private string title                    = "Notifier";           // Note default Title
        private Type type                       = Type.INFO;            // Note default Type

        private bool isDialog = false;                                  // Note is Dialog
        private BackDialogStyle backDialogStyle = BackDialogStyle.None; // DialogNote default background
        private Form myCallerApp;                                       // Base Application for Dialog Note

        private Color Hover = Color.FromArgb(0, 0, 0, 0);               // Default Color for hover
        private Color Leave = Color.FromArgb(0, 0, 0, 0);               // Default Color for leave

        private int timeout_ms                  = 0;                    // Temporary note: timeout
        private AutoResetEvent timerResetEvent  = null;                 // Temporary note: reset event

        private Form inApp = null;                                      // In App Notifier: the note is binded to the specified container
#endregion

该部分包括用于保存笔记位置及其信息的帮助类。

那个...

        class NoteLocation                                              // Helper class to handle Note position
        {
            internal int X;
            internal int Y;

            internal Point initialLocation;                             // Mouse bar drag helpers
            internal bool mouseIsDown = false;

            public NoteLocation(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
        }

...用于存储所有通知的位置,因为每次创建笔记时,都需要检查可用位置以在空闲空间显示它。为了保存通知,我们使用了一个static容器和一个static ID计数器,以确保每个Notifier都有其ID

          static List<Notifier> notes             = new List<Notifier>(); // Keep a list of the opened Notifiers

创建和绘制

在表单创建时调用的OnLoad方法中

            BackColor       = Color.Blue;                               // Initial default graphics 
            TransparencyKey = Color.FromArgb(128, 128, 128);            // Initial default graphics 
            FormBorderStyle = FormBorderStyle.None;                     // Initial default graphics 

我们设置了表单的透明度,这样我们就可以使用自定义背景来实现更好的平滑效果和透明边框。

            this.Tag = "__Notifier|" + ID.ToString("X4");               // Save the note identification in the Tag field

            setNotifier(description, type, title);  

TAG用于标识表单的通知类型。它对“Update”操作很有用。

要设置笔记的内容,使用此函数

        private void setNotifier(string description, 
                                 Type noteType, 
                                 string title, 
                                 bool isUpdate = false)
        {
            this.title          = title;
            this.description    = description;
            this.type           = noteType;

            noteTitle.Text      = title;                                // Fill the Notifier data title
            noteContent.Text    = description;                          // Fill the Notifier data description
            noteDate.Text       = DateTime.Now + "";                    // Fill the Notifier data Timestamp

#region ADJUST COLORS
            switch (noteType)
            {
                case Type.ERROR:
                    icon.Image = global::Notify.Properties.Resources.ko;
                    Leave = Color.FromArgb(200, 60, 70);
                    Hover = Color.FromArgb(240, 80, 90);
                    break;
                case Type.INFO:
                    icon.Image = global::Notify.Properties.Resources.info;
                    Leave = Color.FromArgb(90, 140, 230);
                    Hover = Color.FromArgb(110, 160, 250);
                    break;
                case Type.WARNING:
                    icon.Image = global::Notify.Properties.Resources.warning;
                    Leave = Color.FromArgb(200, 200, 80);
                    Hover = Color.FromArgb(220, 220, 80);
                    break;
                case Type.OK:
                    icon.Image = global::Notify.Properties.Resources.ok;
                    Leave = Color.FromArgb(80, 200, 130);
                    Hover = Color.FromArgb(80, 240, 130);
                    break;
            }

            buttonClose.BackColor = Leave;                              // Init colors
            buttonMenu.BackColor  = Leave;
            noteTitle.BackColor   = Leave;

            this.buttonClose.MouseHover += (s, e) =>                    // Mouse hover
            {
                this.buttonClose.BackColor = Hover;
                this.buttonMenu.BackColor = Hover;
                this.noteTitle.BackColor = Hover;
            };
            this.buttonMenu.MouseHover += (s, e) =>
            {
                this.buttonMenu.BackColor = Hover;
                this.buttonClose.BackColor = Hover;
                this.noteTitle.BackColor = Hover;
            }; this.noteTitle.MouseHover += (s, e) =>
            {
                this.buttonMenu.BackColor = Hover;
                this.buttonClose.BackColor = Hover;
                this.noteTitle.BackColor = Hover;
            };

            this.buttonClose.MouseLeave += (s, e) =>                    // Mouse leave
            {
                this.buttonClose.BackColor = Leave;
                this.buttonMenu.BackColor = Leave;
                this.noteTitle.BackColor = Leave;
            };
            this.buttonMenu.MouseLeave += (s, e) =>
            {
                this.buttonMenu.BackColor = Leave;
                this.buttonClose.BackColor = Leave;
                this.noteTitle.BackColor = Leave;
            };
            this.noteTitle.MouseLeave += (s, e) =>
            {
                this.buttonMenu.BackColor = Leave;
                this.buttonClose.BackColor = Leave;
                this.noteTitle.BackColor = Leave;
            };
#endregion

#region DIALOG NOTE
            if (isDialog)
            {
                Button ok_button    = new Button();                     // Dialog note comes with a simple Ok button
                ok_button.FlatStyle = FlatStyle.Flat;
                ok_button.BackColor = Leave;
                ok_button.ForeColor = Color.White;
                Size                = new Size(Size.Width,              // Resize the note to contain the button
                                               Size.Height + 50);
                ok_button.Size      = new Size(120, 40);
                ok_button.Location  = new Point(Size.Width / 2 - ok_button.Size.Width / 2, 
                                                Size.Height - 50);
                ok_button.Text      = DialogResult.OK.ToString();
                ok_button.Click     += onOkButtonClick;
                Controls.Add(ok_button);

                noteDate.Location   = new Point(noteDate.Location.X,    // Shift down the date location
                                                noteDate.Location.Y + 44); 


                noteLocation        = new NoteLocation(Left, Top);      // Default Center Location
            }
#endregion

#region NOTE LOCATION
            if (!isDialog && !isUpdate)
            {
                NoteLocation location = adjustLocation(this);           // Set the note location

                Left = location.X;                                      // Notifier position X    
                Top  = location.Y;                                      // Notifier position Y 
            }
#endregion
        }

该函数设置所有通知元素与所需内容。我们需要处理不同的事情

  1. 调整样式
  2. 调整位置
  3. 处理对话框样式笔记

对话框样式与简单的停靠通知不同:它需要一个按钮来检查用户事件获取以及关闭按钮。此外,对话框样式会锁定应用程序GUI直到笔记关闭。

可选地,它将有一个淡入的黑色背景(这目前无法通过简单的messageBox实现)覆盖在应用程序或整个屏幕上(在演示中尝试一下)。

#region ADJUST LOCATION中, 我们为我们的通知找到了一个位置

        private NoteLocation adjustLocation(Notifier note)
        {
            Rectangle notesArea;
            int nMaxRows    = 0, 
                nColumn     = 0,
                nMaxColumns = 0,
                xShift      = 25;                                                     // Custom note overlay
            //  x_Shift     = this.Width + 5;                                         // Full visible note (no overlay)
            bool add = false;

            if (inApp != null && inApp.WindowState ==  FormWindowState.Normal)        // Get the available notes area, based on the type of note location
            {
                notesArea = new Rectangle(inApp.Location.X, 
                                          inApp.Location.Y, 
                                          inApp.Size.Width, 
                                          inApp.Size.Height);
            }
            else
            {
                notesArea = new Rectangle(Screen.GetWorkingArea(note).Left,
                                          Screen.GetWorkingArea(note).Top,
                                          Screen.GetWorkingArea(note).Width,
                                          Screen.GetWorkingArea(note).Height);
            }

            nMaxRows    = notesArea.Height / Height;                                  // Max number of rows in the available space
            nMaxColumns = notesArea.Width  / xShift;                                  // Max number of columns in the available space

            noteLocation = new NoteLocation(notesArea.Width  +                        // Initial Position X                                        
                                            notesArea.Left   -
                                            Width,
                                            notesArea.Height +                        // Initial Position Y
                                            notesArea.Top    -
                                            Height);

            while (nMaxRows > 0 && !add)                                              // Check the latest available position (no overlap)
            {
                for (int nRow = 1; nRow <= nMaxRows; nRow++)
                {
                    noteLocation.Y =    notesArea.Height +
                                        notesArea.Top    -
                                        Height * nRow;

                    if (!isLocationAlreadyUsed(noteLocation, note))
                    {
                        add = true; break;
                    }

                    if (nRow == nMaxRows)                                            // X shift if no more column space
                    {
                        nColumn++;
                        nRow = 0;

                        noteLocation.X =  notesArea.Width           +
                                          notesArea.Left            - 
                                          Width - xShift * nColumn;
                    }

                    if (nColumn >= nMaxColumns)                                      // Last exit condition: the screen is full of note
                    {
                        add = true; break;
                    }
                }
            }

            noteLocation.initialLocation = new Point(noteLocation.X,                  // Init the initial Location, for drag & drop
                                                     noteLocation.Y);             
            return noteLocation;
        }

首先,我们检查是否是笔记的更新:在这种情况下,不需要评估位置,因为已更新的笔记已显示。

然后,我们看看它是否是对话框笔记:对话框使用底部定义的位置,而停靠的笔记使用第一个分支

  1. 首先,我们获取屏幕区域并计算可用网格维度(行和列)。
  2. 然后,我们开始位置循环:如果一行已满,我们将通知沿屏幕的X轴移动一个自定义值(是否与通知重叠 - 如果您不希望重叠,请取消注释该行)。
  3. 如果整个屏幕已满,则在最后使用的位置创建通知。

最后,我们设置笔记的位置。

让我们看看有趣的部分,Update函数。

更新

要更新通知,如前所述,我们需要它的ID

有了笔记ID,就可以轻松找到所需的通知并进行更新

        public static void Update(short ID, 
                                  string desc, 
                                  Type noteType, 
                                  string title)
        {
            foreach (var note in notes)
            {
                if (note.Tag != null &&                                     // Get the node
                    note.Tag.Equals("__Notifier|" + ID.ToString("X4")))
                {
                    if (note.timerResetEvent != null)                            // Reset the timeout timer (if any)
                        note.timerResetEvent.Set();

                    Notifier myNote = (Notifier)note;
                    myNote.setNotifier(desc, noteType, title, true);        // Set the new note content
                }
            }
        }       

我们遍历通知列表,并检查TAG属性中设置的ID:然后调用setNotifier部分,该部分通过更改笔记内容(或类型)来处理更新。

临时笔记

一个非常有用的函数:我们可以创建一个笔记并指示它是否是自动关闭笔记。我将此用于一些易失性信息。

技术上,这是通过BackgroundWorkerAutoResetEvent实现的。在Show调用中,我们定义了它

                if (not.timeout_ms >= 500)                                          // Start autoclose timer (if any)
                {
                    not.timerResetEvent      = new AutoResetEvent(false);

                    BackgroundWorker timer   = new BackgroundWorker();
                    timer.DoWork             += timer_DoWork;
                    timer.RunWorkerCompleted += timer_RunWorkerCompleted;
                    timer.RunWorkerAsync(not);                                      // Timer (temporary notes)
                }

然后,在指定毫秒超时时间结束后很容易关闭

#region TIMER
        //-------------------------------------------------------------------------------------------------------------------------------
        //                                  Background Worker to handle the timeout of the note
        //-------------------------------------------------------------------------------------------------------------------------------
        private static void timer_DoWork(object sender, DoWorkEventArgs e)
        {
            Notifier not = (Notifier)e.Argument;
            bool timedOut = false;
            while (!timedOut)
            {
                if (!not.timerResetEvent.WaitOne(not.timeout_ms))
                    timedOut = true;                                        // Time is out
            }
            e.Result = e.Argument;
        }

        //-------------------------------------------------------------------------------------------------------------------------------
        //                                  Background Worker to handle the timeout event
        //-------------------------------------------------------------------------------------------------------------------------------
        private static void timer_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            Notifier not = (Notifier) e.Result;
            not.closeMe();                                                  // Close the note
        }
#endregion

后台工作者处理timeout的后台部分:timeout事件是一个自动重置事件:它对更新部分很有用。让我们考虑一个显示相同消息的笔记,如我们所知,它将使用笔记计数器处理:但是如果这个笔记是临时笔记怎么办?对于每一个新通知,计时器都会重置,因此该笔记至少可以显示计数器期望的时间。

应用内笔记

v4版本(WinForm和WPF)引入的新功能是能够将笔记附加到容器应用程序。您可以在演示中尝试此功能,以便更好地理解此功能。通常,笔记会固定在屏幕的右下角,但如果您启用“应用内”功能,您可以将笔记固定到指定FormWindow的右下角。

简单日志记录器

为了获得更强大的用户通知功能,可以使用包含的logger,称为SimpleLogger

一个非常基础的.NET日志记录器。可以为日志选择文件名和日志消息级别。

在你的类中,包含logger

Logger logger = new Logger();

……或指定文件名

Logger logger = new Logger("myManager.log");

可选:设置日志级别(请记住CRITICAL < ERROR < WARNING < INFO < VERBOSE),所以如果你将级别设置为WARNING,你将在日志中得到CRITICALERRORWARNING;如果你将级别设置为CRITICAL,你只会得到CRITICAL。默认值为VERBOSE

logger.setLoggingLevel(Logger.LEVEL.INFO);

使用它

logger.log(Logger.LEVEL.ERROR, e.StackTrace);

关注点

一些未来的发展如下

  • 笔记的起始位置可配置(X、Y、屏幕角落)
  • WPF:一些效果(例如淡入淡出)
  • 修复重绘过程,该过程会导致每次鼠标拖动多次重绘
  • 日志记录器:记录到控制台

历史

  • 2018年8月5日:v1.4 - 添加了InApp功能和更好的多屏幕支持(WinForm和WPF)
  • 2018年1月12日:v1.3 - 添加了WPF版本 - 添加了临时笔记支持
  • 2017年7月10日:v1.2 - 添加了showDialog、可拖动通知器等功能。改进了图形部分,更新了相同笔记的计数器。添加了SimpleLogger
  • 2016年9月1日:v1.1 - 以静态方式更改了调用者样式,添加了“关闭全部”功能和通知ID:现在可以回忆和更改已打开通知的内容
  • 2016年8月25日:v1.0 - 第一个版本发布
© . All rights reserved.