SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

WPF用にNotifyIconクラスをラップしてみた

C#で常駐アプリなどを作り、タスクトレイにアイコンを出す場合には、Win Formsで用意されているNotifyIconというクラスを使います。

以前、このクラスを使って常駐アプリを作る方法を↓に書きました。

この方法だと、使うたびにWinFormsのデザイナを利用して設定したりする必要があり面倒です。
ということで、NotifyIconに関する処理を1ファイルにまとめ、簡単に再利用できるようにしてみました。

今回は、以前のサンプルと比べ、以下のような点を修正してます。

  • Formsのコンポーネントクラスではなく、NotifyIconExという普通のクラスとして実装
    • Win Formsのデザイナをわざわざ使いたくなかったので、すべてC#のコードで書くように修正しました。
    • タスクトレイ関係の処理はすべてこのクラスの中に閉じ込めました。
  • メニューを、WPFのContextMenuクラスで作れるように修正
  • バルーン表示に関わる各種プロパティ/メソッドを利用できるようにラップ
  • 各種イベント類もラップ

コード一式は以下のリポジトリに上げています。
WPFSamples/NotifyIconSample at master · sourcechord/WPFSamples · GitHub

使い方

準備

  • プロジェクトにアセンブリを追加
    • System.Windows.Forms
    • System.Drawing
  • 以下のNotifyIconExクラスをプロジェクトに加える
NotifyIconExクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NotifyIconSample
{
    public enum ToolTipIconEx
    {
        None = 0,
        Info = 1,
        Warning = 2,
        Error = 3
    }

    public class NotifyIconEx : IDisposable
    {
        private System.Windows.Forms.NotifyIcon _notify;

        public string Text
        {
            get { return this._notify.Text; }
            set { this._notify.Text = value; }
        }

        public bool Visible
        {
            get { return this._notify.Visible; }
            set { this._notify.Visible = value; }
        }

        public Uri IconPath
        {
            set
            {
                if (value == null) { return; }
                var iconStream = System.Windows.Application.GetResourceStream(value).Stream;
                this._notify.Icon = new System.Drawing.Icon(iconStream);
            }
        }

        public System.Windows.Controls.ContextMenu ContextMenu { get; set; }

        public ToolTipIconEx BalloonTipIcon
        {
            get { return (ToolTipIconEx)this._notify.BalloonTipIcon; }
            set { this._notify.BalloonTipIcon = (System.Windows.Forms.ToolTipIcon)value; }
        }

        public string BalloonTipTitle
        {
            get { return this._notify.BalloonTipTitle; }
            set { this._notify.BalloonTipTitle = value; }
        }

        public string BalloonTipText
        {
            get { return this._notify.BalloonTipText; }
            set { this._notify.BalloonTipText = value; }
        }

        public void ShowBalloonTip(int timeout)
        {
            this._notify.ShowBalloonTip(timeout);
        }

        public void ShowBalloonTip(int timeout, string tipTitle, string tipText, ToolTipIconEx tipIcon)
        {
            var icon = (System.Windows.Forms.ToolTipIcon)tipIcon;
            this._notify.ShowBalloonTip(timeout, tipTitle, tipText, icon);
        }

        public NotifyIconEx()
            : this(null) { }

        public NotifyIconEx(Uri iconPath)
            : this(iconPath, null) { }

        public NotifyIconEx(Uri iconPath, string text)
            : this(iconPath, text, null) {}

        public NotifyIconEx(Uri iconPath, string text, System.Windows.Controls.ContextMenu menu)
        {
            // 各種プロパティを初期化
            this._notify = new System.Windows.Forms.NotifyIcon();
            this.IconPath = iconPath;
            this.Text = text;
            this.ContextMenu = menu;

            // マウス右ボタンUpのタイミングで、ContextMenuの表示を行う
            // ダミーの透明ウィンドウを表示し、このウィンドウのアクティブ状態を用いてContextMenuの表示/非表示を切り替える
            this._notify.MouseUp += (s, e) =>
            {
                if (e.Button != System.Windows.Forms.MouseButtons.Right) { return; }

                var win = new System.Windows.Window()
                {
                    WindowStyle = System.Windows.WindowStyle.None,
                    ShowInTaskbar = false,
                    AllowsTransparency = true,
                    Background = System.Windows.Media.Brushes.Transparent,
                    Content = new System.Windows.Controls.Grid(),
                    ContextMenu = this.ContextMenu
                };

                var isClosed = false;
                win.Activated += (_, __) =>
                {
                    if (win.ContextMenu != null)
                    {
                        win.ContextMenu.IsOpen = true;
                    }
                };
                win.Closing += (_, __) =>
                {
                    isClosed = true;
                };

                win.Deactivated += (_, __) =>
                {
                    if (win.ContextMenu != null)
                    {
                        win.ContextMenu.IsOpen = false;
                    }
                    if (!isClosed)
                    {
                        win.Close();
                    }
                };
                
                // ダミーウィンドウ表示&アクティブ化をする。
                // ⇒これがActivatedイベントで、ContextMenuが表示される
                win.Show();
                win.Activate();
            };

            this._notify.Visible = true;
        }

        #region NotifyIconクラスの各種イベントをラップする
        public event EventHandler BalloonTipClicked
        {
            add { this._notify.BalloonTipClicked += value; }
            remove { this._notify.BalloonTipClicked -= value; }
        }

        public event EventHandler BalloonTipClosed
        {
            add { this._notify.BalloonTipClosed += value; }
            remove { this._notify.BalloonTipClosed -= value; }
        }

        public event EventHandler BalloonTipShown
        {
            add { this._notify.BalloonTipShown += value; }
            remove { this._notify.BalloonTipShown -= value; }
        }

        public event EventHandler Click
        {
            add { this._notify.Click += value; }
            remove { this._notify.Click -= value; }
        }

        public event EventHandler Disposed
        {
            add { this._notify.Disposed += value; }
            remove { this._notify.Disposed -= value; }
        }

        public event EventHandler DoubleClick
        {
            add { this._notify.DoubleClick += value; }
            remove { this._notify.DoubleClick -= value; }
        }
        #endregion

        #region IDisposable Support
        private bool disposedValue = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    this._notify.Dispose();
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }
        #endregion

    }
}

タスクトレイにアイコン表示

        private NotifyIconEx _notify;

        public MainWindow()
        {
            InitializeComponent();

            var iconPath = new Uri("pack://application:,,,/NotifyIconSample;component/Icon1.ico", UriKind.Absolute);
            this._notify = new NotifyIconEx(iconPath, "Notify Title");
        }

タスクトレイ登録時には、アイコンが必要になります。
プロジェクトに、「ビルドアクション:Resource」に設定したicoファイルを追加しておき、Uri指定することでアイコンを設定できます。
f:id:minami_SC:20170211125013p:plain

お片付け

Disposeメソッドを呼び出すと、タスクトレイアイコンを破棄します。

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);

            // ウィンドウを閉じる際に、タスクトレイのアイコンを削除する。
            this._notify.Dispose();
        }

Disposeをせずにアプリを終了すると、タスクトレイにアイコンが残るので注意してください。
(タスクトレイのアイコン上にマウスカーソルを乗せると消えますが・・・)

タスクトレイのアイコンからContextMenu表示

以下のようにすると、XAML上で定義したContextMenuを表示できます。

MainWindow.xaml
    <Window.Resources>
        <ContextMenu x:Key="sampleWinMenu">
            <MenuItem Header="ShowBalloon" Click="MenuItem_Show_Balloon_Click" />
            <Separator />
            <MenuItem Header="Show" Click="MenuItem_Show_Click" />
            <MenuItem Header="Exit" Click="MenuItem_Exit_Click" />
        </ContextMenu>
    </Window.Resources>
MainWindow.xaml.cs
        public MainWindow()
        {
            InitializeComponent();

            var iconPath = new Uri("pack://application:,,,/NotifyIconSample;component/Icon1.ico", UriKind.Absolute);
            var menu = (ContextMenu)this.FindResource("sampleWinMenu");
            this._notify = new NotifyIconEx(iconPath, "Notify Title", menu);
        }

ContextMenuは、タスクトレイのアイコン右クリックで表示されます。
f:id:minami_SC:20170211125036p:plain

バルーン表示

以下のメソッドでバルーン表示できます。

this._notify.ShowBalloonTip(1000, "tipTitle", "tipText", ToolTipIconEx.Info);

ちなみに、Windows10ではバルーンはこんな風に表示されます。
f:id:minami_SC:20170211125027p:plain

以前のものより、だいぶカッコよくなってますね!!

タスクトレイのメニューについて

FormsのNotifyIconクラスでメニューを出す場合、FormsのContextMenuStripクラスを使ってメニューを構築します。

ですが、WPFでの開発に慣れてくると、Formsのデザイナで作るこのContextMenuStripクラスは使いにくく感じます。
ContextMenuもXAML上で定義したいですよね。。。

ということで、NotifyIconのMouseUpイベントなどを使い、WPFのContextMenuを表示するようにしました。

しかし、ここで問題が、、、

タスクトレイ上から、WPFのContextMenuを表示すると、メニューが非アクティブ状態になっても消えずに残り続けてしまいます。

原因や対処方法などは、以下のリンクを参考にやってみました。
http://blogs.wankuma.com/youryella/archive/2009/11/01/182630.aspx
http://copycodetheory.blogspot.jp/2012/07/notify-icon-in-wpf-applications.html

ContextMenuはアプリケーションの持つウィンドウのアクティブ状態などを監視して、自動で閉じる処理を行っているようです。
なので、アクティブなウィンドウがない状態では、ContextMenuが非アクティブになっても閉じることができないようです。

ということで、上記リンクなどを参考に以下のような処理を行っています。

  • ContextMenu表示時に透明なダミーウィンドウを作りアクティブ化
  • 上記ウィンドウが非アクティブになったらContextMenuを閉じる(& ダミーのウィンドウも閉じる)

だいぶトリッキーですね・・・orz

その他

ここで作ったサンプルコードでは、タスクトレイのコンテキストメニューを自動で閉じるためにトリッキーな処理をしています。

手軽にタスクトレイやトレイのメニューを使うにはいいかもしれませんが、
この手のタスクトレイ関係の機能をガッツリと作りこみたい場合には、↓のライブラリのようなものを別途使用したほうがよいかもしれません。
http://www.hardcodet.net/wpf-notifyicon
https://www.nuget.org/packages/Hardcodet.NotifyIcon.Wpf/