[.NET] [C#] Visual Studio Async CTP を試してみる

話題の Visual Studio Async CTP を試してみました。次期 .NET のバージョンに組み込まれる予定の機能の一つです。C# と VB の言語仕様に async と await という 2 つのキーワードを追加し、言語仕様として非同期処理を実装できるというものです。実は、F# 2.0 には既に async キーワードが実装されていたりしますが。

.NET の非同期処理といえば Begin/End パターンですが、それに置き換わるものかと思います。まだ全然使いこなせていませんが、コード量は明らかに減るわけで、確かにこれは便利そうです。もちろん、言語仕様を複雑化することに対する批判もありますけどね。

サンプルは、YAHOO! の 「東日本大震災 写真保存プロジェクト 写真検索 API」 を使って写真をダウンロードし、それを非同期処理で表示するというものです。主な流れはこんな感じで。

  1. REST API を読んで、画像 URL の一覧が書かれた XML を取得
  2. XML を LINQ to XML でパースして、URL を抽出
  3. 画像データをダウンロード
  4. ListView と ImageList コントロールを使って一覧表示

けっこう単純です。Async の威力を示すため、まず同期処理バージョンを先につくってから、それを非同期処理に書き換えてみます。

1. 準備

Async CTP を利用するには Visual Studio 2010 SP1 が必要です。Express 版は無料なので、持っていない人はここからダウンロードして下さい。
http://www.microsoft.com/visualstudio/en-us/products/2010-editions/express

もちろん Async CTP をインストールしなければいけません。ダウンロード場所は下記 URLです。インストールは、ウィザードにしたがって進めるだけです。Async CTP をインストールすると、大量のサンプルが付随してくるので、それを見るだけで覚えられます。Microsoft にありがちですが、むしろサンプルが大きすぎて読みづらいです。私は匙を投げましたw
http://www.microsoft.com/download/en/details.aspx?id=9983

今回は YAHOO! Japan の提供する API を利用するので、http://developer.yahoo.co.jp/ からアプリケーション ID を取得して下さい。まあ何でもいいんですけど。

2. プロジェクトの作成

Visual Studio を開き、File > New > Project メニューから Windows Forms Application プロジェクトを作成します。ここでは C# を使います。Visual Basic でも構いません。

image

Async CTP には 非同期の拡張メソッドが追加されており、それを async や await を使う新方式で呼び出す、というのが基本の使い方です。その拡張メソッドは、.AsyncCtpLibrary.dll という名前の .NET アセンブリに含まれています。

DLL ファイルは、なぜかサンプルの入っているフォルダーにしか見つからなかったので、とりあえずこれを追加します。
%userprofile%\Documents\Microsoft Visual Studio Async CTP\Samples\AsyncCtpLibrary.dll

Solution Browser から References を右クリックし、[Add Reference…] を選択して下さい。表示されるダイアログで AsyncCtpLibrary.dll を選択して [OK] をクリックして下さい。

image

プロジェクトのプロパティから、Application > Target framework の選択で [.NET Framework 4] を選択して下さい。これは System.Net などを使うためです。

image

Async 以外に使うアセンブリを追加します。先ほどと同様の Add References のダイアログで、今度は .NET タブから以下のアセンブリを追加します。

  • System.Net
  • System.Web

image

Solution Explorer がこんな感じになります。

image

3. フォームの作成

以下のコントロールを貼りつけます。

  • ListView, ImageList – 画像の一覧表示
  • TextBox – 検索キーワード
  • Buttom – 検索実行
  • TextBox – クエリとなる URL を表示
  • ComboBox x2 – 検索結果数、表示画像サイズ

めんどくさいのでコントロール名は全部デフォルトで。

image

んで、こんな感じになります。コントロールのプロパティなどはお好みで調整して下さい。

image

4. コードを書く(同期処理)

以下のようなコードを書きます。これは通常の同期処理です。

//
// Form1.cs
//
//
// References
//
//
http://msdn.microsoft.com/en-us/library/dd250937.aspx (XML)
//
http://www.atmarkit.co.jp/fdotnet/special/linqtoxml/linqtoxml_01.html (Linq to XML)
//
http://www.atmarkit.co.jp/fdotnet/dotnettips/336listviewimage/listviewimage.html (ImageList)
//

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.Xml.Linq;
using System.Net;
using System.Web;

namespace CSSandbox {
    public partial class Form1 : Form {
        const string AppId = "YAHOO! のアプリケーション ID";

        public Form1() {
            InitializeComponent();
        }

        private Size[] mImageSizeList = new Size[] {
            new Size { Width= 64, Height= 48},
            new Size { Width= 128, Height= 96},
            new Size { Width= 256, Height= 192},
        };

        private void Form1_Load(object sender, EventArgs e) {
            comboBox1.Items.Clear();
            comboBox1.Items.AddRange(new string[] {"10", "50", "100"});

            comboBox2.Items.Clear();
            for (int i = 0; i < mImageSizeList.Length; ++i)
                comboBox2.Items.Add(ImageSizeCaption(i));

            comboBox1.SelectedIndex = 0;
            comboBox2.SelectedIndex = 0;

            SetImageSize();
        }

        private string ImageSizeCaption(int index) {
            if (index >= 0 && index < mImageSizeList.Length) {
                return String.Format("{0}x{1}",
                    mImageSizeList[index].Width,
                    mImageSizeList[index].Height);
            }
            else {
                return "N/A";
            }
        }

        private void SetImageSize() {
            imageList1.ImageSize = mImageSizeList[comboBox2.SelectedIndex];
        }

        private void button1_Click(object sender, EventArgs e) {
            listView1.Items.Clear();
            imageList1.Images.Clear();
            SetImageSize();

            Cursor OldCursor = Cursor.Current;
            Cursor.Current = Cursors.WaitCursor;

            Search(textBox1.Text, 1, int.Parse(comboBox1.Text));

            Cursor.Current = OldCursor;
        }

        private void Search(string QueryString, int Start, int NumResults) {
            string requestString =
              "
http://shinsai.yahooapis.jp/v1/Archive/search?"
                + "AppId=" + AppId
                + "&query="
                + HttpUtility.UrlEncode(QueryString, Encoding.UTF8)
                + "&hard_flag=true"
                + "&sort=%2Dorg_time"
                + "&results=" + NumResults
                + "&start=" + Start;

            textBox2.Text = requestString;

            var SearchReq = HttpWebRequest.Create(requestString);
            var SearchRep = SearchReq.GetResponse();

            XElement XmlDoc = XElement.Load(SearchRep.GetResponseStream());
            XNamespace Namespace = "
http://shinsai.yahooapis.jp";

            var query = from element
                        in XmlDoc.Descendants(Namespace + "ThumbnailUrl")
                        select element;

            int ImageIndex= 0;
            foreach (var item in query) {
                var DownloadReq = System.Net.WebRequest.Create(item.Value);
                var DownloadRep = DownloadReq.GetResponse();

                if (DownloadRep.ContentType == "image/jpeg") {
                    var Original =
                      Image.FromStream(DownloadRep.GetResponseStream());
                    listView1.Items.Add(item.Value, ++ImageIndex);
                    imageList1.Images.Add(Original);
                }
            }
        }
    }
}

よく見ると、実は LINQ to XML は不要だったりします。単に使ってみたかっただけです、はい。

API の仕様はここに載っています。クエリ オプションは他にもあります。
http://developer.yahoo.co.jp/webapi/shinsai/archive/v1/search.html

さて、これでプロジェクトをビルドし、適当に検索ワードとオプションを選択して Go ボタンを押すと、画像が表示されます。

image

しかし同期処理なので、画像を表示中はウィンドウがフリーズした状態になります。表示画像数を 100 にして検索すると、10 秒以上待たされます。そこで非同期処理の登場です。

5. コードを書き直す(非同期処理)

まあ、この記事の趣旨からして、いとも簡単に非同期処理に変更できるわけですね。さて、何行ぐらい変えればいいと思いますか。

正解は 4 行です。青字で示した行が変更箇所です。

//
// Form1.cs
//
//
// References
//
//
http://msdn.microsoft.com/en-us/library/dd250937.aspx (XML)
//
http://www.atmarkit.co.jp/fdotnet/special/linqtoxml/linqtoxml_01.html (Linq to XML)
//
http://www.atmarkit.co.jp/fdotnet/dotnettips/336listviewimage/listviewimage.html (ImageList)
//

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.Xml.Linq;
using System.Net;
using System.Web;

using System.Threading.Tasks;

namespace CSSandbox {
    public partial class Form1 : Form {
        const string AppId = "YAHOO! のアプリケーション ID";

        public Form1() {
            InitializeComponent();
        }

        private Size[] mImageSizeList = new Size[] {
            new Size { Width= 64, Height= 48},
            new Size { Width= 128, Height= 96},
            new Size { Width= 256, Height= 192},
        };

        private void Form1_Load(object sender, EventArgs e) {
            comboBox1.Items.Clear();
            comboBox1.Items.AddRange(new string[] {"10", "50", "100"});

            comboBox2.Items.Clear();
            for (int i = 0; i < mImageSizeList.Length; ++i)
                comboBox2.Items.Add(ImageSizeCaption(i));

            comboBox1.SelectedIndex = 0;
            comboBox2.SelectedIndex = 0;

            SetImageSize();
        }

        private string ImageSizeCaption(int index) {
            if (index >= 0 && index < mImageSizeList.Length) {
                return String.Format("{0}x{1}",
                    mImageSizeList[index].Width,
                    mImageSizeList[index].Height);
            }
            else {
                return "N/A";
            }
        }

        private void SetImageSize() {
            imageList1.ImageSize = mImageSizeList[comboBox2.SelectedIndex];
        }

        private void button1_Click(object sender, EventArgs e) {
            listView1.Items.Clear();
            imageList1.Images.Clear();
            SetImageSize();

            Cursor OldCursor = Cursor.Current;
            Cursor.Current = Cursors.WaitCursor;

            Search(textBox1.Text, 1, int.Parse(comboBox1.Text));

            Cursor.Current = OldCursor;
        }

        private async void Search(string QueryString,
                                  int Start, int NumResults) {

            string requestString =
              "
http://shinsai.yahooapis.jp/v1/Archive/search?"
                + "AppId=" + AppId
                + "&query="
                + HttpUtility.UrlEncode(QueryString, Encoding.UTF8)
                + "&hard_flag=true"
                + "&sort=%2Dorg_time"
                + "&results=" + NumResults
                + "&start=" + Start;

            textBox2.Text = requestString;

            var SearchReq = HttpWebRequest.Create(requestString);
            // var SearchRep = SearchReq.GetResponse();
            var SearchRep = await SearchReq.GetResponseAsync();

            XElement XmlDoc = XElement.Load(SearchRep.GetResponseStream());
            XNamespace Namespace = "
http://shinsai.yahooapis.jp";

            var query = from element
                        in XmlDoc.Descendants(Namespace + "ThumbnailUrl")
                        select element;

            int ImageIndex= 0;
            foreach (var item in query) {
                var DownloadReq = System.Net.WebRequest.Create(item.Value);
                // var DownloadRep = DownloadReq.GetResponse();
                var DownloadRep = await DownloadReq.GetResponseAsync();

                if (DownloadRep.ContentType == "image/jpeg") {
                    var Original =
                      Image.FromStream(DownloadRep.GetResponseStream());
                    listView1.Items.Add(item.Value, ++ImageIndex);
                    imageList1.Images.Add(Original);
                }
          }
        }
    }
}

あら不思議。これで 100 枚の画像を表示させてもウィンドウがフリーズしません。

変更箇所が 4 行というのはけっこう少ないほうかと思います。その中でも、ポイントは GetResponseAsync でしょうか。これが Async CTP で追加された拡張メソッドの 1 つです。拡張メソッドは、ネットワーク I/O 系の操作を中心に用意されており、基本的には Windows Azure や Windows Phone 7 で利用されることを目的としているようです。モバイル アプリを作るにはけっこう強力な機能だと思います。

もちろん、実際に追加されている拡張メソッドは、Object Browser から見ることができます。

image

サンプルが適当すぎて微妙ですが、例外処理を書くときが格段に楽になります。というのも、Begin~End のようにコールバック関数を使うような実装だと、複数箇所に try~catch 文を配置しなければなりませんが、async だと、同期処理と同じように例外を捕捉できます。サンプルの例だと、GetResponseAsync を try~catch で囲めばそれで終わりです。簡単ですね。

async には、タイムアウトやキャンセル処理などを実装することもできますが、それはまたの機会に書きます。また、デバッグするとどのように見えるのか、といったところもまだ勉強中です。

しかしまあ、このサンプルだと、非同期処理で画像が追加されている最中にウィンドウを操作できるのはいいのですが、コントロールの再描画処理の層で表示がチカチカしてクールではありません。考え物です・・・。

広告

[Windows Azure] [C#] Blob を操作するコマンドライン ツールの作成

Windows Azure の Blob サービスを使うにあたって、自分用の CUI ツールを作ったので、一応ソースを載せておきます。世にはもっと便利なツールが出回っていると思いますが、特徴はこんな感じです。

  • コマンドライン
  • 非同期ダウンロード/アップロード
  • アップロード時にメタデータを指定
  • MIME の Content-Type にレジストリの HKCR から取ってきた値を指定

GUI だったら Azure Storage Explorer というのが便利そうです。

http://azurestorageexplorer.codeplex.com/

開発/動作環境はこれ。

  • OS: Windows 7 SP1 x64
  • IDE: Visual Studio 2010 SP1
  • SDK: .NET Framework 4.0 + Windows Azure SDK 1.4

作っていて気づいた注意点など。

  • Azure SDK をアセンブリに追加する際、 Target Framework を ".NET Framework 4" に変更する必要がある
    (デフォルトは ".NET Framework 4 Client Profile" になっている)
    image
  • CloudBlobClient.GetContainerReference や CloudBlobContainer.GetBlobReference では、コンテナーやブロブの存在確認はできず、存在しなくてもインスタンスが取得できる。存在確認をするためには、FetchAttributes を呼び出して、例外 StorageClientException を補足しなければならない。
    http://msdn.microsoft.com/en-us/library/microsoft.windowsazure.storageclient.cloudblobclient.getcontainerreference.aspx
  • CloudBlob からブロブ名を取得するプロパティがない?
    CloudBlob.Uri.LocalPath だと "/コンテナ名/ブロブ名" になってしまうので、CloudBlob.Uri.Segments.Last() という苦肉の策を使う。

非同期処理については、もちろん CloudBlob.BeginUploadFromStream と CloudBlob.BeginDownloadToStream を使うわけですが、コールバック関数や EndUploadFromStream, EndDownloadToStream は使わなかった。アップロードの前後で記述する関数が変わるのもおかしいから、というのが理由ですが、これって .NET 的に普通なのかが不明。なにぶん独習 C# ぐらいの知識しかないので、Begin/End パターンへの理解が乏しい。

コンテナの追加と削除、ページ ブロブやブロック ブロブとしての操作は実装していません。

もっと単純なものにする予定だったのが、無駄に凝ってしまった結果がこれ。実質半日ぐらいかかってしまった。
青字の部分は、自分の Azure アカウントに応じて変更して下さい。

最近またコーディング スタイルを変えている。テーマは脱ハンガリアン。変数名の先頭を大文字にするのに抵抗がなくなってきた。でもメンバ変数の頭には m を付けようと思っている。

//
//
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using Microsoft.Win32; // registry
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

namespace AzureBlob {
    class BlobStorageException : Exception {
        public BlobStorageException(string Message, bool Dump)
            : base(Message) {
            Console.WriteLine(Message + "\n");
            if (Dump) DumpUsage();
        }

        public BlobStorageException(string Message,
                                    bool Dump, Exception Base)
            : base(Message, Base) {
            Console.WriteLine(Message + "\n");
            if (Dump) DumpUsage();
        }

        private void DumpUsage() {
            Console.WriteLine(@"Usage:
   AzureBlob /list     <container>
   AzureBlob /info     <container> <blobname>
   AzureBlob /delete   <container> <blobname>
   AzureBlob /upload   <container> <blobname> <file> <key1:val1> <key2:val2>
   AzureBlob /download <container> <blobname> <file>
");
        }
    }

    class Program {
        static void Main(string[] args) {
            Program p = new Program();

            try {
                p.ParseArguments(args);
                Console.WriteLine("Done.\n");
            }
            catch (BlobStorageException) {
                Console.WriteLine("Failed.\n");
            }
        }

        enum CommandType { List = 0, Info, Delete, Upload, Download };
        struct ArgumentType {
            public CommandType Type;
            public string Command;
            public int MinimumArguments;
            public bool BlobMustExist;
            public ArgumentType(CommandType t, string s, int n, bool b) {
                Type = t; Command = s; MinimumArguments = n;
                BlobMustExist = b;
            }
        }
        static ArgumentType[] ArgumentTypes = {
            new ArgumentType(CommandType.List,     "/list",     2, false),
            new ArgumentType(CommandType.Info,     "/info",     3, true),
            new ArgumentType(CommandType.Delete,   "/delete",   3, true),
            new ArgumentType(CommandType.Upload,   "/upload",   4, false),
            new ArgumentType(CommandType.Download, "/download", 4, true),
        };
       
        private string PrimaryAccessKey = "ほげほげABCD==";
        private string StorageAccount = "ストレージアカウント名";
        private CloudBlobContainer GetBlobContainer(string ContainerName,
                                                    bool IsCreate) {
            CloudStorageAccount Account = new CloudStorageAccount(
              new StorageCredentialsAccountAndKey(StorageAccount,
              Convert.FromBase64String(PrimaryAccessKey)), false);
            CloudBlobClient BlobClient = Account.CreateCloudBlobClient();
            CloudBlobContainer BlobContainer =
              BlobClient.GetContainerReference(ContainerName);

            //BlobContainerPermissions Permissions =
            //  new BlobContainerPermissions();
            //Permissions.PublicAccess =
            //  BlobContainerPublicAccessType.Container;
            //mContainer.SetPermissions(Permissions);

            if (!IsCreate) {
                try {
                    BlobContainer.FetchAttributes();
                }
                catch (StorageClientException) {
                    return null;
                }
            }

            // BlobContainer.CreateIfNotExist();
            return BlobContainer;
        }

        string mFile = null;
        string mBlobName = null;
        CloudBlobContainer mBlobContainer = null;
        CloudBlob mBlob = null;

        private void ParseArguments(string[] Arguments) {
            if (Arguments.Length < 1)
                throw new BlobStorageException(
                  "Some parameter are missing.", true);

            int CommandIndex = -1;
            for ( int i=0 ; i<ArgumentTypes.Length ; ++i ) {
                if (Arguments[0].ToLower() == ArgumentTypes[i].Command) {
                    CommandIndex = i;
                    break;
                }

            }

            if (CommandIndex == -1)
                throw new BlobStorageException("Bad command.", true);

            ArgumentType Command= ArgumentTypes[CommandIndex];

            if (Arguments.Length < Command.MinimumArguments)
                throw new BlobStorageException(
                  "Some parameter are missing.", true);

            mBlobContainer = GetBlobContainer(Arguments[1], false);
            if (mBlobContainer == null)
                throw new BlobStorageException(
                  string.Format("The container `{0}` does not exist.",
                    Arguments[1]), false);

            if (Command.Type == CommandType.List) {
                OnList();
                return;
            }

            mBlobName = Arguments[2];
            mBlob = mBlobContainer.GetBlobReference(mBlobName);
            if (Command.BlobMustExist) {
                try {
                    mBlob.FetchAttributes();
                }
                catch (StorageClientException) {
                    throw new BlobStorageException(
                      string.Format("The blob `{0}` does not exist.",
                        mBlobName), false);
                }
            }

            switch (Command.Type) {
            case CommandType.Info:
                OnInfo();
                break;
            case CommandType.Delete:
                OnDelete();
                break;
            case CommandType.Upload:
                mFile = Arguments[3];
                string[] Metadata = new string[Arguments.Length – 4];
                Array.Copy(Arguments, 4, Metadata, 0, Arguments.Length – 4);
                OnUpload(Metadata);
                break;
            case CommandType.Download:
                mFile = Arguments[3];
                OnDownload();
                break;
            }
        }

        private void OnList() {
            Console.WriteLine("[Blobs in Container]");
            foreach (var b in mBlobContainer.ListBlobs())
                Console.WriteLine(b.Uri.Segments.Last());
            Console.WriteLine("");
        }

        private void OnInfo() {
            Console.WriteLine("[Basics]");
            Console.WriteLine(" URI         : {0}", mBlob.Uri.ToString());
            Console.WriteLine(" LocalPath   : {0}", mBlob.Uri.LocalPath);
            Console.WriteLine("");
            Console.WriteLine("[Properties]");
            Console.WriteLine(" Length      : {0:#,#} bytes",
              mBlob.Properties.Length);
            Console.WriteLine(" Content-Type: {0}",
              mBlob.Properties.ContentType);
            Console.WriteLine("");
            Console.WriteLine("[Metadata]");
            foreach (var key in mBlob.Metadata.AllKeys)
                Console.WriteLine(" {0}: {1}", key, mBlob.Metadata[key]);
            Console.WriteLine("");
        }

        private void OnDelete() {
            mBlob.Delete();
            Console.WriteLine("");
        }

        private void Progress(int n) {
            char[] bars = { ‘|’, ‘/’, ‘―’, ‘\’ };
            Console.SetCursorPosition(0, Console.CursorTop);
            Console.Write(bars[n % 4]);
        }

        private string TickToDuration(long Tick) {
            Tick /= (1000 * 1000 * 10); // covert fron 100nsec to sec
            return string.Format("{0:0#}m{1:0#}s", Tick / 60, Tick % 60);
        }

        private void OnDownload() {
            //mBlob.DownloadToFile(mFile);
           
            IAsyncResult Result= null;
            try {
                FileStream SerializedFile = new FileStream(
                  mFile, FileMode.Create,
                  FileAccess.ReadWrite, FileShare.None);
                Result = mBlob.BeginDownloadToStream(
                  SerializedFile, null, null); // no use of callback
            }
            catch (Exception e) {
                throw new BlobStorageException("File I/O error.", false, e);
            }

            Console.Write("| Downloading…");

            bool IsSeekable = true;
            try {
                Console.SetCursorPosition(0, Console.CursorTop);
            }
            catch {
                // not seeakable console
                IsSeekable = false;
                Console.WriteLine("");
            }

            int n = 0;
            while (Result != null && !Result.AsyncWaitHandle.WaitOne(100)) {
                if (IsSeekable) Progress(n);
                n = (n + 1) % 4;
            }

            Console.WriteLine("");
        }

        private void OnUpload(string[] Metadata) {
            // mBlob.UploadFile(mFile);

            IAsyncResult Result = null;
            try {
                FileStream SerializedFile = new FileStream(
                  mFile, FileMode.Open, FileAccess.Read, FileShare.Read);
                // mBlob.UploadFromStream(SerializedFile);
                Result = mBlob.BeginUploadFromStream(
                  SerializedFile, null, null); // no use of callback
            }
            catch (Exception e) {
                throw new BlobStorageException("File I/O error.", false, e);
            }

            Console.Write("| Uploading…");

            bool IsSeekable = true;
            try {
                Console.SetCursorPosition(0, Console.CursorTop);
            }
            catch {
                // not seeakable console
                IsSeekable = false;
                Console.WriteLine("");
            }

            long StartTime = DateTime.Now.Ticks;

            int n = 0;
            while (Result!=null && !Result.AsyncWaitHandle.WaitOne(100)) {
                if ( IsSeekable ) Progress(n);
                n = (n + 1) % 4;
            }

            long EndTime = DateTime.Now.Ticks;

            Console.WriteLine("Uploading done – [{0}]\n",
              TickToDuration(EndTime – StartTime));

            mBlob.Properties.ContentType =
              GetContentType(Path.GetExtension(mFile));
            Console.WriteLine("Content-Type set -> {0}",
              mBlob.Properties.ContentType);
            mBlob.SetProperties();

            string key, value;
            foreach (string s in Metadata) {
                int pos= s.IndexOf(‘:’);
                if (pos == 0) {
                    Console.WriteLine("Metadata {0} is skipped", s);
                    continue;
                }
                else if (pos == -1) {
                    key = s; value = "";
                    Console.WriteLine("Metadata set -> {0}", key);
                }
                else {
                    key = s.Substring(0, pos);
                    value = s.Substring(pos + 1);
                    Console.WriteLine("Metadata set -> {0}: {1}",
                      key, value);
                }

                mBlob.Metadata.Add(key, value);
            }
            mBlob.SetMetadata();

            Console.WriteLine("");
        }

        private string GetContentType(string Extenstion) {
            RegistryKey RegKey= Registry.ClassesRoot.OpenSubKey(Extenstion);
            if (RegKey == null)
                return ""; // Extension is not registered

            string RegValue = RegKey.GetValue("Content Type") as string;
            return RegValue == null ? "" : RegValue;
        }
    }
}

[.NET] [C#] ウィンドウを閉じると通知アイコンで実行されるプログラム

最近業務外でこそこそと C#.NET のプログラムを書いていて、その中で出てきた Tips。ちなみに、通知 “トレイ” という表現は全くの間違いで、通知エリアにおける通知アイコンというのが正式な表現らしいです。分かりにくいことに、通知エリアもタスクバーの一部なので、「じゃあ本来のタスクバーのボタンの名前は?」と聞かれると分かりません。タスクボタン?ですかね。

閑話休題。.NET で通知アイコンを表示させるのは簡単です。 NotifyIcon クラスを使います。下記 URL にあるサンプルを見れば一目瞭然。

http://msdn.microsoft.com/ja-jp/library/system.windows.forms.notifyicon(VS.80).aspx

このプログラムをベースに、ウィンドウを閉じたときにタスクボタンが消えて、通知アイコンだけになるプログラムを作ります。タスクボタンは、プログラムがウィンドウを持っているときに表示されているので、ウィンドウを閉じればタスクボタンは消えます。ウィンドウを閉じたときにアプリケーションが終了してしまってはまずいので、 Application.Run メソッドに Form を渡してはダメです。

さて、 NotifyIcon インスタンスはどこに持たせればいいのでしょうか。もし、ウィンドウを閉じる=破棄する というのであれば、当然 NotifyIcon は Form に持たせるわけにはいかず、 Application クラスを継承してなんちゃらかんちゃら、という風に Form の外側に持たせる必要があります。しかし、ウィンドウの × ボタンを押すたびにウィンドウが破棄され、通知アイコンをダブルクリックするなどしたときに、またウィンドウが初期状態に戻ってしまうというのも現実的な仕様ではありません。Form.Show() ではなく、 Form.ShowDialog() を使ってウィンドウをモーダル表示させると、ウィンドウを閉じても破棄されません。通知アイコンをダブルクリックなどしたときに再度 Form.ShowDialog() を呼び出すと、閉じる前の状態でウィンドウが復活します。

これを使えば Form クラスの中に NotifyIcon を持たせてもよさそうなものですが、個人的には違和感を感じます。例えアプリケーションがウィンドウを一つしか持っていなかったとしても、通知アイコンというのは概念的には「アプリケーションそのもの」を指しているわけで、そのアプリケーションのフロントエンドに過ぎないフォームオブジェクトが通知アイコンを所有しているというのは設計的におかしい気がするのです。

以上を踏まえて Application.Run のオーバーロード一覧を見ると、 Application.Run(ApplicationContext) といういかにも使えそうなものがあります。ユーザ定義の ApplicationContext クラスを作って、そこに通知アイコンなど、アプリケーション関連処理を持たせれば万事解決です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

using System.IO;
using System.Reflection;

namespace CSSandbox {
    static class Program {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            Application.Run(new MyApplicationContext(new Form1()));
        }
    }

    class MyApplicationContext : ApplicationContext {
        private Form mForm;
        private NotifyIcon mNotifyIcon;
        private ContextMenu mContextMenu;

        public MyApplicationContext(Form f) {
            mForm = f;

            InitNotifyTray();
            f.ShowDialog();
        }

        private void InitNotifyTray() {
            MenuItem menu = new MenuItem();
            menu.Index = 0;
            menu.Text = "E&xit";
            menu.Click += new System.EventHandler(menuItem1_Click);

            mContextMenu = new ContextMenu();
            mContextMenu.MenuItems.Add(menu);

            mNotifyIcon= new NotifyIcon();
            mNotifyIcon.Icon = new Icon("appicon.ico");
            mNotifyIcon.ContextMenu = mContextMenu;
            mNotifyIcon.Text = "NotifyIcon Tooltip";
            mNotifyIcon.Visible = true;

        }

        private void notifyIcon1_DoubleClick(object Sender, EventArgs e) {
            if ( !mForm.Visible )
                mForm.ShowDialog();
        }

        private void menuItem1_Click(object Sender, EventArgs e) {
            Application.Exit();
        }

    }
}

コンテキストメニューを ApplicationContext に持たせる必要もないのですが、なんとなく。

このプログラムで目的は達成したような気がしますが、アプリケーションを終了させても通知アイコンが通知エリアから消えないという現象が起きます。アプリケーションがクラッシュした時と同じようなことが起こっていて、マウスポインタをその通知アイコン上に持っていくと通知アイコンは消えます。再描画イベントが発生しない、とかなんでしょうかね。理由は不明です。

このままでは気持ち悪いので、試しに通知アイコンの破棄を明示的に行ったところ、無事に通知アイコンは自動的に消えるようになりました。すなわち、 ApplicationContext のコンストラクタに次のようにイベントハンドラを追加し、

public MyApplicationContext(Form f) {
    mForm = f;
    Application.ApplicationExit += new EventHandler(OnQuit);

    InitNotifyTray();
    f.ShowDialog();
}

OnQuit 関数を追加します。

private void OnQuit(object sender, EventArgs e) {
    mNotifyIcon.Dispose();
}

最後にもう一点。通知アイコンをアイコンファイルからロードしているあたりが何とも滑稽なので、これをリソースから読み込むように変えたほうがよさそうです。で、リソースからバイナリを読み込む方法をいろいろ調べても意外とすんなり出てこない。以下のサイトの方法を試してみてもなぜかうまくいかず、というか Win32API の世界よりめんどくさくなっている気がしておかしい。

http://support.microsoft.com/kb/319292/en
http://dobon.net/vb/dotnet/programing/resourcemanager.html

と、 30 分ほど困っていたら、偶然見つけることができました。リソースは既にインスタンスとして与えられているじゃないか、と。

  mNotifyIcon.Icon = Properties.Resources.Icon1;

瞬殺。.NET 本には書いてありそうですね。知ってて当たり前なのだろうか。