コンテキストメニュー「送る」C#

私が初めてWindowsアプリを作ったのはMFCダイアログベースのファイル変換ツールでした。ファイル変換ツールなので、ファイルを入力としますが、「送る」メニューに登録することでツールが簡単に使えるようになったのを覚えています。今日はそのC#版。

sendto_example

1.ファイルを受け取るアプリ

まずファイルを受けるソフトから。これは皆様が作るアプリなのでここでは簡単にTXTファイルを受け取って中身を表示するものとして作っておきます。

text_display_form

TextDisplayForm.cs

namespace SendToSupportWinFormsApp
{
    using System;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.IO;

    public partial class TextDisplayForm : Form
    {
        public TextDisplayForm()
        {
            InitializeComponent();
        }

        private void TextDisplayForm_Shown(object sender, EventArgs e)
        {
            var args = Environment.GetCommandLineArgs();
            if(args.Length >= 2) {
                textBoxArgs.Text = args.Skip(1).Aggregate((a, b) => a + b);
                if(args.Count() >= 2 && Path.GetExtension(args[1]).ToLower() == ".txt") {
                    textBoxFileText.Text = File.ReadAllText(args[1], Encoding.GetEncoding("Shift-JIS"));
                }
            }
        }
    }
}

[送る]を選択したときのファイルがプログラム名に次ぐ引数として渡されます。引数はEnvironment.GetCommandLineArgs()でstring配列として渡されますので1番目がパスとして扱えばokです。

 

.送るメニューへのショートカット追加

「送る」メニューに上のソフトが表示されるためには、スタートボタンから”プログラムとファイルの検索”に shell:sendto と入力すると開くフォルダにショートカットを作成すればokです。「送る」メニューへの登録/解除はインストール/アンインストールのタイミングが自然でしょうということで、VS2010でセットアッププロジェクトを作成します。

  1. セットアッププロジェクトを右クリック、「追加」->「プロジェクト出力」を選択
  2. 先ほど作ったWinFormsアプリのプロジェクトのプライマリ出力を選択してOK。
  3. 2でできたプライマリ出力をダブルクリック、ファイルシステムが開く
  4. ファイルシステムの左ペーンの”対象コンピューター上のファイルシステム”を右クリックして「特別なフォルダの追加」->「ユーザーの送るメニュー」を選択
  5. 同じくファイルシステムの左ペーンの”アプリケーションフォルダー”を選択、右ペーンに先ほど作ったWinFormsのプライマリ出力があるので右クリックしてショートカットを作成
  6. 5で作ったショートカットを4で作ったユーザーの送るメニューへドラッグアンドドロップ

shortcut_by_project

昔はよかったのですが、.NET4.0以降上記手順で作成されるのは”アドパタイズショートカット”というやつらしく、SendToフォルダへは登録されるものの「送る」メニューに登録されません、ありゃ困った。

 

3.プログラマならコードでしょう

2の問題のため、Orcaを使う方法やvbsを使う方法が紹介されています。が、私はマネージコードしか書きたくないのでC#の方法を紹介。簡単な話で、Installerクラスを使います。このInstallerクラスはセットアッププロジェクトではなくインストールするアプリケーションそのもの、今回でいうとWinFormsアプリのプロジェクトに追加します。ショートカットも作成するので以下を参照追加。

[COM] Windows Script Host Object Model
[.NET] System.Configuration.Install

前者はプロジェクトの参照設定に “IWshRuntimeRubrary” と表示されますが、このプロパティを開き [相互運用機能の埋め込み] をFalseにしておきます。

ひとてま2

コードはこちら。

AddSendToInstaller.cs

namespace SendToSupportWinFormsApp
{
    using System;
    using System.IO;
    using System.Configuration.Install;
    using System.Reflection;
    using System.ComponentModel;
    using IWshRuntimeLibrary;

    [RunInstaller(true)]
    public class AddSendToInstaller : Installer
    {
        private const string shortcutFilePathKey = "shortcutFilePath";
        public override void Install(System.Collections.IDictionary stateSaver) {
            // Debug時は以下を有効に
            // System.Diagnostics.Debugger.Launch();

            string targetPath = Assembly.GetExecutingAssembly().Location;
            string fileName = Path.GetFileNameWithoutExtension(targetPath);
            string shortcutPath = Environment.GetFolderPath(Environment.SpecialFolder.SendTo) + @"\" + fileName + ".lnk";
            IWshShell shell = new WshShellClass();
            IWshShortcut shortcut = shell.CreateShortcut(shortcutPath) as IWshShortcut;
            shortcut.TargetPath = targetPath;
            shortcut.Save();

            stateSaver[shortcutFilePathKey] = shortcutPath;
            base.Install(stateSaver);
        }

        public override void Uninstall(System.Collections.IDictionary savedState) {
            // Debug時は以下を有効に
            // System.Diagnostics.Debugger.Launch();

            if(savedState.Contains(shortcutFilePathKey)) {
                string shortcutFilePath = savedState[shortcutFilePathKey] as string;
                if(System.IO.File.Exists(shortcutFilePath)) {
                    System.IO.File.Delete(shortcutFilePath);
                }
            }
            base.Uninstall(savedState);
        }
    }
}

この上でセットアッププロジェクトを変更します(2の手順で作ったショートカットは無駄ですので削除します)。

  1. セットアッププロジェクトを右クリックし、「表示」->「カスタム動作」をクリック
  2. カスタム動作のインストールとアンインストールそれぞれについて、右クリックしてカスタム動作の追加をクリックして、アプリケーションフォルダを選択してOK。

custom_action

これをしないと上で作ったインストーラクラスが無視されるので注意。これでセットアッププロジェクトを作ってインストールすると「送る」メニューにアプリが登録されます。

tuika2

実行するとこう。めでたし。

exe

ただVS2012以降セットアッププロジェクトがプロジェクトテンプレートからなくなってしまったのでVS2010までの手法ということになります。InstallShield LimitedEditionを使う必要がありますね。

Windowのキャプチャ(WinForms,WPF)

現在、台風19号が近畿圏に再接近中…。今回は息抜きに、WindowsFormsとWPFそれぞれについてwindowのキャプチャ方法を見ておきます。WPFは結構苦労しているような。

 

1.WindowsFormsの場合

ボタン押下でFormのキャプチャをするような関数は以下のような感じになります。

 Form1.cs

namespace WindowsFormsCapture
{
    using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Drawing.Imaging;
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void buttonCapture_Click(object sender, EventArgs e)
        {
            using(Bitmap image = new Bitmap(Width,Height))
            using(Graphics graphics = Graphics.FromImage(image))
            {
                graphics.CopyFromScreen(Location, Point.Empty, image.Size);
                image.Save("Capture.bmp", ImageFormat.Bmp);
            }
        }
    }
}

実際のキャプチャ画像はこちら。

Capture

 

2.WPFの場合

WPFの場合も基本は同じ、ウィンドウの位置,サイズの取得方法が変わるだけです。

MainWindow.xaml.cs

namespace WpfCapture
{
    using System.Windows;
    using System.Windows.Media;
    using System.Drawing;
    using System.Drawing.Imaging;
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var brush = new VisualBrush(this);
            using (Bitmap image = new Bitmap((int)ActualWidth, (int)ActualHeight))
            using (Graphics graphics = Graphics.FromImage(image))
            {
                graphics.CopyFromScreen(new System.Drawing.Point((int)Left, (int)Top), System.Drawing.Point.Empty, image.Size);
                image.Save("Capture.bmp", ImageFormat.Bmp);
            }
        }
    }
}

こちらも蛇足ながらキャプチャ画像はこちら。

Capture

WCFによるP2Pチャットソフト

やってみたかったWCFでP2P。P2Pの利用法としてはベタなチャットソフト作成して方法を確認しておきます。

1.通信内容を決める

WCFで通信するときはコントラクトとして通信内容をInterfaceで実装するんでした、こちら。

 ITocsChat.cs

namespace WcfP2PChat
{
    using System.ServiceModel;
    [ServiceContract(CallbackContract=typeof(ITocsChat))]
    public interface ITocsChat
    {
        [OperationContract(IsOneWay = true)]
        void Join(string userName);

        [OperationContract(IsOneWay = true)]
        void Chat(string userName, string message);

        [OperationContract(IsOneWay = true)]
        void Leave(string userName);
    }
}

このInterfaceとP2Pの送信・応答チャネル定義をするIClientChannelの2つのInterfaceをもつInterfaceを定義しておきます。P2Pではこの2つのInterafaceを持つほうを使います。ただし、このInterfaceクラスを明示的に実装することはありません。

 ITocsChatChannel.cs

namespace WcfP2PChat
{
    using System.ServiceModel;
    public interface ITocsChatChannel : ITocsChat, IClientChannel
    {
    }
}

 

2.P2Pのエンドポイントを構成する

コードからでも問題ないと思いますが、今回はアプリケーション構成ファイルを使います。

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <system.serviceModel>
    <client>
      <endpoint name="TocsSimpleChatApp"
                address="net.p2p://TocsMesh/TocsChat"
                binding="netPeerTcpBinding"
                bindingConfiguration="TocsChatBinding"
                contract="WcfP2PChat.ITocsChat"/>
    </client>
    <bindings>
      <netPeerTcpBinding>
        <binding name="TocsChatBinding" port="0">
          <resolver mode="Auto"/>
          <security mode="None"/>
        </binding>
      </netPeerTcpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

ChatForm.cs(抜粋)

    public partial class ChatForm : Form, ITocsChat
    {
        private ITocsChatChannel m_participant;

        public ChatForm()
        {
            InitializeComponent();

            var site = new InstanceContext(this);
            var binding = new NetPeerTcpBinding("TocsChatBinding");
            var channelFactory = new DuplexChannelFactory<ITocsChatChannel>(site, "TocsSimpleChatApp");
            m_participant = channelFactory.CreateChannel();

            var statusHandler = m_participant.GetProperty<IOnlineStatus>();
            statusHandler.Online += statusHandler_Online;
            statusHandler.Offline += statusHandler_Offline;
        }
    }

多くの方に理解して頂けるようWindows Forms にしました。FormはITocsChatを実装するようにしていますが、これは他ユーザから送られてきたメッセージをFormで処理します、ということを表しています。またm_participantとしてITocsChannelオブジェクトを取得していますが、これは他ユーザ(群)を表していると考えて良いと思います。

 

3.チャットソフトを作成する

あとはこまごまとした作りこみを少々。本来なら状態に応じてコントロールの有効/無効切り替えや日時表示etc..機能を盛り込むでしょうが、わかりやすさ重視で実装するとこんな感じ。

chat

ChatForm.cs(全体) 

namespace WcfP2PChat
{
    using System;
    using System.Windows.Forms;
    using System.ServiceModel;

    public partial class ChatForm : Form, ITocsChat
    {
        private ITocsChatChannel m_participant;

        public ChatForm()
        {
            InitializeComponent();

            var site = new InstanceContext(this);
            var binding = new NetPeerTcpBinding("TocsChatBinding");
            var channelFactory = new DuplexChannelFactory<ITocsChatChannel>(site, "TocsSimpleChatApp");
            m_participant = channelFactory.CreateChannel();

            var statusHandler = m_participant.GetProperty<IOnlineStatus>();
            statusHandler.Online += statusHandler_Online;
            statusHandler.Offline += statusHandler_Offline;
        }

        void statusHandler_Online(object sender, EventArgs e){
            textBoxChatLog.Text += "接続しました" + Environment.NewLine;
        }
        void statusHandler_Offline(object sender, EventArgs e){
            textBoxChatLog.Text += "切断されました" + Environment.NewLine;
        }

        public void Join(string userName){
            textBoxChatLog.Text += userName + "さんが参加されました" + Environment.NewLine;
        }
        public void Chat(string userName, string message){
            textBoxChatLog.Text += userName + ":" + message + Environment.NewLine;
        }
        public new void Leave(string userName){
            textBoxChatLog.Text += userName + "さんが退出されました" + Environment.NewLine;
        }

        private void buttonJoin_Click(object sender, EventArgs e){
            m_participant.Join(textBoxUserName.Text);
        }
        private void buttonLeave_Click(object sender, EventArgs e){
            m_participant.Leave(textBoxUserName.Text);
        }
        private void buttonSay_Click(object sender, EventArgs e){
            m_participant.Chat(textBoxUserName.Text, textBoxSay.Text);
        }

    }
}

Formが実装するITocsChat Interfaceは他ユーザからの操作に対するcallbackとなるので上のような処理となります。これを動かした模様はこちら。

chatting

こんなに少ないコードながらに結構まともに見えます。

 

補足 – WindowsXPの場合

上記のようにWCFのP2Pでデフォルトリゾルバを使った場合、PNRP Ver2.0となりますがこれはWinXP sp3もしくはKB920342を入れる必要があるようです(こちら)。またPNRPがIPv6の上に構築されるのでIPv6もインストール必要です(こちら)。少なくともこれらが必要なのは間違いありませんが、それで正しく動くかは確認できていません。

ReactiveUI WinFormsをかじる

WPFと言えばMVVMですが実際の現場でWPFの浸透度はどうでしょうか?WinXPのサポートが切れたことで 『.NET3.0のインストールは30分も掛かってユーザに迷惑だから.NET2.0で開発する』 などと古臭い主張をする保守派への反論材料が揃いだした昨今ではまだ、WindowsFormsで新規開発しているほうがまだ多いと感じます。

Windows Formsで『MVC(おそらく正確にはMVP)で開発する』と意気込んだプロジェクトの何%が目標通りの設計、メンテナンス性が実現できているのか疑問です。Frameworkサポートとしては弱いWindows FormsでMVVMを実現させるライブラリの紹介です。

 

ReactiveUI WinForms

まずWindowsFormsアプリのプロジェクトを作成した状態で、Nugetパッケージマネージャで

PM> install-package reactiveui-winforms

としてReactiveUI WinFormsをインストールしておきます。VS2010ではダメだったのですが、VS2013ではokでした、VS2012以降などの制限があるかもしれません。

サンプルFormは以下の構成とします。

form

ReactiveUI-WinFormsではFormをただのViewとして扱い、ViewModelは以下のように別クラスに定義します。

 SampleViewModel.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using ReactiveUI;

    public class SampleViewModel : ReactiveObject
    {
        private string inputString1;
        public string InputString1 {
            get { return inputString1; }
            set { this.RaiseAndSetIfChanged(ref inputString1, value); }
        }

        private string inputString2 = "input2の初期値です";
        public string InputString2 {
            get { return inputString2; }
            set { this.RaiseAndSetIfChanged(ref inputString2, value); }
        }

        private DateTime currentDateTime = DateTime.Now;
        public DateTime CurrentDateTime {
            get { return currentDateTime; }
            set { this.RaiseAndSetIfChanged(ref currentDateTime, value); }
        }

        // 初期化コマンド
        public ReactiveCommand<object> Initialize{ get; set;}

        public SampleViewModel()
        {
            // 初期化コマンドの作成
            Initialize = ReactiveCommand.Create(this.WhenAny(vm => vm.InputString1, str => true)); // =常時有効
            Initialize.Subscribe(_ =>
            {
                InputString1 = "初期値1";
                InputString2 = "初期値2";
                CurrentDateTime = new DateTime(2020, 1, 1);
            }
            );
        }

        public override string ToString(){
            return "1:" + InputString1 + Environment.NewLine +
                   "2:" + InputString2 + Environment.NewLine +
                   CurrentDateTime.ToString();
        }
    }
}

MVVMに従い、Viewに対する操作は全てViewModelに定義します。ViewModelの基本クラスとしてReactiveUI.ReactiveObjectクラスから派生させます。

データ更新は通常INotifyPropertyChangedを実装しますが、ReactiveObjectクラスが実装するRaiseAndSetIfChangedメソッドの呼び出しでフィールドへの値のセット及びINotifyPropertyChangedのイベント発砲の処理を行います。thisを明示しないとこのメソッドが出てこず苦労しました;

データ操作はReactiveCommandとして公開します。ReactiveExtensionsを使い、いつ、どんな処理を行うかを定義します。上の例では常時有効にしていますが、これをプロパティ状態に応じてtrue/falseが切り替わるようにするとこの後でバインドするボタンの有効状態が変わります。WPFではおなじみのICommandを実装しているのでCanExecute/Executeを実装しているのでWindowsFormsであっても有効状態も同期する利点がでています。

これをバインドする先のViewはこんな感じになります。

 SampleViewForm.cs

namespace ReactiveUiForWinFormsTest
{
    using System.Windows.Forms;
    using ReactiveUI;

    public partial class SampleViewForm : Form, IViewFor<SampleViewModel>
    {
        public SampleViewForm(SampleViewModel viewModel)
        {
            InitializeComponent();

            ViewModel = viewModel;
            this.Bind(ViewModel, vm => vm.InputString1, v => v.input1TextBox.Text);
            this.Bind(ViewModel, vm => vm.InputString2, v => v.input2TextBox.Text);
            this.Bind(ViewModel, vm => vm.CurrentDateTime, v => v.currentDateTimePicker.Value);
            this.BindCommand(ViewModel, vm => vm.Initialize, v => v.executeButton);
            this.closeButton.Click += (_, __) => this.Close();
        }

        public SampleViewModel ViewModel{
            get;set;
        }
        object IViewFor.ViewModel{
            get { return ViewModel; }
            set { ViewModel = value as SampleViewModel; }
        }
    }
}

ViewはViewModelに依存するので、依存先のViewModelを指定したIViewFor<TViewModel>から派生させます。CloseだけはViewで処理していますが、他は全てViewCommandの処理とバインドさせておりViewが処理を担うことをなくしています。ここでのFormは前述の通りただのViewなのでViewModelはConstructor Injectionで渡してもらうのが妥当でしょう。

蛇足ですがこれを呼び出すのはこんな感じ。

Program.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using System.Windows.Forms;
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var viewModel = new SampleViewModel();
            Application.Run(new SampleViewForm(viewModel));
            MessageBox.Show(viewModel.ToString());
        }
    }
}

実行結果はご想像の通り、ちゃんとViewModelとViewのバインドはとれています。

binding__

上の例では単純にBindでバインドしただけですが、WPFでのバインド似でOneWayBindがあったり、バインド時の引数にIValueConverterみたいなものがあったり、かなり使えそうな印象です。

多言語対応(C#、Windows Forms編)

チラホラ見かけた情報だけだと実務には足りない気がしたので、Windows Formsでの多言語対応について少々。まず前提として、デフォルトの言語を決めておき、それ以外の文言を必要に応じて追加するという方法を取ります。

デフォルトは日本語か英語でしょう、多国語対応がタイ語、スペイン語やポルトガル語などまで意識するようなものなら英語をデフォルトとしたほうがよさそうです。

 

1.Formのリソース

よくあるのがコチラ。Formを多言語対応する場合、

  1. FormのLanguageプロパティが(規定値)の状態で、Formを作成します。
  2. FormのLocalizableプロパティをTrueにし、Languageプロパティを多言語対応する言語に変更した上で、1で編集したFormを編集しなおします。
  3. 必要言語分、2を繰り返します。

form_propertyというステップ。

ステップ2でLanguageを変更後、Formのコントロールを初めて変更するタイミングで新たなリソースファイルが作成されます。

multiple_resource_files

この例では規定の言語用のTestForm.resxと、日本語用のTestForm.ja.resx、ドイツ語用のTestForm.de.resxができています。これをビルドすると実行ファイルに[ja]とか、[de]とかいうフォルダがあり、リソースだけを含んだDLL(サテライトアセンブリと呼びます)ができます。実行時にCurrentUICultureに基づいて自動的に読みだしてくれます。

 

2.その他のリソース

MessageBoxやファイル出力などForm以外でも文言が必要な場合はあります。そのような文言も多言語対応する場合はResources.resxファイルに格納します。これも初期状態では規定の言語用のファイルしかありませんので、多言語対応する言語ごとに作り直します。

resource_2

多言語用のresxファイルは、規定の言語用のResources.resxファイルをコピーしてプロジェクトに張り付けした後、適切なカルチャコードを加えてPropertiesフォルダに格納します。カルチャーコードはネット上で調べてもわかるし、Formを多言語対応する時にはカルチャコードがわかるのでそれを真似て作ればOKです。

resources_13

アクセスするときはこんな感じ。

Console.WriteLine(Properties.Resources.IDS_BUTTON);

これも同じくビルドすると先ほど同様サテライトアセンブリに組込まれるので、あとはCurrentCultureに基づいてロードされます。

 

3.文言翻訳運用

確かにメインは以上なんですが、『これでお終い』って言われると・・・?オレオレ翻訳ならOkだけど、普通は翻訳家・翻訳業者に頼んだり、プルーフするはずで実際の運用だとどう考えてもこの辺のリソースをまとめる作業が必要になる。

例えば文言リストを一覧形式で保持する必要があるならExcelで運用しといて、翻訳が完了したらresxファイルを作るって手がある。つまり、Excelにこんな感じで文字列一覧を作って、

string_talbe_2

このExcelファイルを読みこむを↓のような感じで作って、

ExcelToResx.cs

namespace MultiLanguageWindowsFormsApp.Tool
{
    using System.Resources;
    using System.Linq;
    using LinqToExcel;

    class ExcelToResx
    {
        private string excelPath;
        public ExcelToResx(string excelPath)
        {
            this.excelPath = excelPath;
        }

        // language="日本語"とか。
        public void Save(string filePath, string language)
        {
            var factory = new ExcelQueryFactory(excelPath);
            var query = factory.Worksheet("Sheet1");
            using (var writer = new ResXResourceWriter(filePath))
            {
                foreach (var item in query){
                    writer.AddResource(item["ID"], item[language]);
                }
            }
        }
    }
}

Resources.rexもForm.resxも上のExcelにまとめて作ってやって、どのリソースファイルなのかを示す列を作っておけば簡単です。LinqToExcelなんだから、ファイルでLoop回しながらSelectするという手があります、あとは出力されるresxファイルをプロジェクトに登録しなおせばOK。

文字数制限チェックはExcelVBAなどでもチェックできます。また、ResXResourceWriterは書き込み専用で “追記” ができないので、リソースファイルの使い方によってはFormで参照する文字列も全てResource.resxに定義しておき、FormのコンストラクタでResourcesを参照する構成にすれば翻訳対象のresxファイルが一つになるので管理しやすいかもしれません。

C#による最速Splash Screenの作成

C#でSplash Screenを作る方法のメモです。Splash Screenというとアプリが起動時に表示されて消えていくアレです、DLLのロードや初期化の進捗も表示するリッチなものもありますがイメージが出て・消えていくだけのシンプルなものを最短の手数で作ってみます。

1.Windows Formsの場合

WindowsFormsの場合、SplashScreenはFormを使います。

  1. Splash対象の画像をリソースに登録
  2. 新規Formを作成後、FormBorderStyleをNoneにしBackgroundに1の画像を指定
  3. 画像ファイルに合うようFormを適宜リサイズ

Program.cs

namespace WindowsFormsAppWithSplash
{
    using System;
    using System.Windows.Forms;

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            SplashForm splash = new SplashForm();
            splash.Show();
            var mainform = new Form1();
            mainform.Shown += (_, __) => splash.Close();
            Application.Run(mainform);
        }
    }
}

わずか3分!(実測)。ここで、MainFormのコンストラクタで必要なDLLのロードをする想定です。最速なのでフェードインフェードアウトはもちろんナシ。

 

2.WPFの場合

  1. Splash対象の画像をリソースに追加(WindowsFormsの場合と同じ)
  2. 画像ファイルを右クリック、プロパティのビルドアクションをSplashScreenに設定

wpf_splash

コードはなし!わずか30秒(実測)。こちらも重たい処理はMainFormで行う前提です。自作コードはありませんが、これはツールが吐き出してくれているから。

 App.g.cs(おそらくobjフォルダの中)

namespace WpfAppWithSplash {
    public partial class App : System.Windows.Application {
        /// <summary>
        /// Application Entry Point.
        /// </summary>
        [System.STAThreadAttribute()]
        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
        public static void Main() {
            SplashScreen splashScreen = new SplashScreen("resources/penguins.jpg");
            splashScreen.Show(true);
            WpfAppWithSplash.App app = new WpfAppWithSplash.App();
            app.InitializeComponent();
            app.Run();
        }
    }
}

テキスト表示などもうちょっとサポートがあれば・・と思わなくもない。

C# Taskのキソ

C#には非同期処理を扱うThreadクラスがあります。Threadクラスは生身のThreadオブジェクトを表しますが、スレッドの生成・削除にはコストが掛かります。そのためThreadではなく、ThreadPoolにプールされているThreadを利用して処理を依頼するというのが定石でした。

C#4.0で追加されたTaskは内部的にはこのThreadPoolを使い、もう一段高い抽象層を作り出すのに一役買っています。ある処理が終わったら別のスレッドでこの処理を行い、、といったコードが書きやすくなっています。C#5.0で追加されたasync/awaitはこのTaskクラスをメソッド定義のように簡単に作れるようにしたものです。要はこのTaskクラス、使い方を十分理解しておく必要があるということです。

スレッドの処理には、スレッドの作成・開始,開始時への値の設定から始まり、進捗報告・キャンセル対応・完了時の値取得・待合わせ・例外処理といろんな要素があります。それらを盛り込んでみたソースはこちら。

 TaskBasicForm.cs

namespace Task1stTouch
{
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;

    public partial class TaskBasicForm : Form
    {
        private Task<SampleTaskResult> task;
        private CancellationTokenSource cancelSouce;
        private TaskScheduler uiTaskScheduler;
        private bool canceled;

        public TaskBasicForm(){
            InitializeComponent();
        }

        private void createAndStartbutton_Click(object sender, EventArgs e){
            // 初期化
            canceled = false;
            cancelSouce = new CancellationTokenSource();    // キャンセルサポート時に必要
            uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); // UIコントロール操作に必要

            // 作成&開始
            task = Task<SampleTaskResult>.Factory.StartNew(
                TaskExecuteAsyncronously, 
                new SampleTaskArg() { Seed = 10 }, 
                cancelSouce.Token );

            // task.Waitで待てるが、UIスレッドで待つことになるためNG.
            // キャンセル時or完了時の処理はtask自体に登録しておく.
            // Task終了時に演算結果が出る場合、結果を使用する位置で待つ.
            cancelSouce.Token.Register(() => canceled = true);
            task.ContinueWith(_ => statusLabel.Text = canceled ? "キャンセルされました" : "完了しました", uiTaskScheduler);
        }

        // 非同期実行されるメソッド
        private SampleTaskResult TaskExecuteAsyncronously(object o)
        {
            // 引数取得
            SampleTaskArg arg = o as SampleTaskArg;
            for (int i = 0; i < 100; i ++)
            {
                // 進捗報告
                var progressbarUpdateTask = new Task( () => progressBar.Value = i+1);
                progressbarUpdateTask.Start(uiTaskScheduler);
                // キャンセル要求チェック
                if (cancelSouce.IsCancellationRequested){
                    return null;
                }
                // 本来したい重たい処理
                Thread.Sleep(20);
            }
            // 処理完了時の値
            return new SampleTaskResult() { Value = arg.Seed * 10 };
        }

        private void cancelButton_Click(object sender, EventArgs e)
        {
            if (cancelSouce == null){
                return;
            }
            cancelSouce.Cancel();
        }

        private void resultButton_Click(object sender, EventArgs e)
        {
            if (task == null || canceled){
                return;
            }
            // task.Resultにアクセスすると結果がまだの場合待ちます.
            statusLabel.Text = "結果は" + task.Result.Value + "です.";
        }
    }
}

dialog

まずTaskの作成と開始はそれぞれコンストラクタとStartメソッドで独立することもできますが上のように同時にもできます。非同期処理が値を返す場合はTask<T>、値を返さない場合はTaskとします。非同期処理開始後、完了をWaitで待つ例がありますが、WindowsFormsやWPF等のUIを持つアプリはメッセージポンプを回さないといけないのでWaitは禁物です。

キャンセルや進捗表示は準備が必要です。キャンセルはCancellationTokenSourceオブジェクトが必要です。非同期実行されるメソッド内部でキャンセル状態を繰り返しチェックする処理は必要です。処理が複雑になるならcancelTokenのThrowIfcancellationRequestedを使うとキャンセル要求チェック&キャンセル要求時は例外スローができ、すっきりします。

進捗表示はあまり誉められた方法ではありません。プログレスバーはUIスレッドのものなのでInvoke的なことが必要です。.NET4.5以降(?)にはEventProgress<T>というものもありますが.NET4.0時点ではこれを自作するか、上のようにUI更新タスクを投げまくるかしかありません。多分。

結果取得はtaksk.Resultだけ。非同期処理が完了していない場合、ここで待ちに入ります。そのため上で記載したとおり完了時に算出される計算結果は必要なタイミングで要求するのが良いでしょう。