きな粉もち.net

.NET関連仕事に携わっています。OSSのソースを読んで気がついたことを中心に呟いたりブログに投稿したりしています。最近はUiPathを使ったRPAも研究中。気軽にフォローやツッコミよろしくおねがいします! Gitはここを使っています https://github.com/kinakomotitti

基礎学習 × .NET Framework の非同期処理を見直してみた × その4

この記事の目的

この記事では、

Taskクラスを使って画面の情報を更新する方法をまとめること

を目的としています。

ついでに、Windows Forms,WPF,UWPで非同期処理を実装する方法についても調べてみました。

本題

★ことの発端

GUIアプリケーション(特にUWP)にて、以下の処理を実装するために調べ始めました。

キャンセル可能なタスクを実行。
タスクを実行中は途中経過をUIに随時反映。
・タスクの途中でキャンセルされたとき、ContinueWithで指定したタスク(TaskContinuationOptions.OnlyOnCanceled)を実行。
ContinueWithで指定したタスクにより、画面に「キャンセルされました」メッセージを表示。

・タスクがキャンセルされずに完了した場合、ContinueWithで指定したタスク(TaskContinuationOptions.OnlyOnRanToCompletion)を実行。
ContinueWithで指定したタスクにより、画面に処理結果を表示。


★Taskクラスを使った場合

TaskのContinueWithメソッドに、TaskScheduler.FromCurrentSynchronizationContext()を渡すことで、UIスレッドで処理を実行することができます。
このTaskSchedulerは、規定では、スレッドプールタスクスケジューラーを利用します。
スレッドプールのタスクスケジューラーは、スレッドプールに対して、タスクをスケジュールするため、UIコントロールにアクセスしようとすると、例外が発生します。
実装イメージは以下の通りです。

 //TaskScheduler.Defaultを指定すると、ThradPoolのスレッドでタスクが実行されるため、以下の例外がスローされる。
 //System.Exception: 'アプリケーションは、別のスレッドにマーシャリングされたインターフェイスを呼び出しました。
 this.SampleTask.ContinueWith((task, status) =>
 {
     this.TaskJobStatus_TextBlock.Text = "canceled";//コントロールにアクセス
  }, new Object(),
  CancellationToken.None,
  TaskContinuationOptions.OnlyOnRanToCompletion,
  TaskScheduler.Default); //←ここ

何もしないままでは、Task実行中にUIコントロールを更新することはできませんが、Taskクラスには、UIコントロールにアクセスできるタスクをスケジュールするためのスケジューラーが用意されています。
それがTaskScheduler.FromCurrentSynchronizationContext()です。
このスケジューラーを利用することで、GUIスレッドにてタスクを実行することが可能となります。
先ほどのコードを書き換えると、以下のようになります。
この実装だと、UIコントロールにアクセスしても例外を発生させることなく実行することができます。

this.SampleTask.ContinueWith((task, status) =>
{
    this.TaskJobStatus_TextBlock.Text = MessageManager.WriteJobStatus("OnlyOnRanToCompletion");
    this.TaskJobStatus_TextBlock.Text += $"\r\nResult : {task.Result.ToString()}";
}, new Object(),
   CancellationToken.None,
   TaskContinuationOptions.OnlyOnRanToCompletion,
   TaskScheduler.FromCurrentSynchronizationContext()); //ここ

ただ、GUIスレッドを使うことになるため、重たい処理に対してこのオプションを指定すると、
画面が固まってしまいます。
画面を固めないようにするための非同期処理でもあるので、それでは本末転倒です(-_-メ)
あくまで、【処理結果】を画面に表示するときだけ利用するべきだと思います。

また、Taskのインスタンスを生成する時にはタスクスケジューラを指定できるオーバーロードがないため、TaskFactoryのStartNewを使う必要があります。
上記のコードでは、Taskのインスタンス生成時ではなくContinueWithしている時にタスクスケジューラのオプションを指定しています。
そのため、TaskFactoryは利用していません。



では、画面を固めないように、重たい処理中の途中経過を随時画面に反映するためには、どうしたらよいのか・・・と、いうことを次から見ていきます。
アプリケーションごとに違うので、Windows Forms、WPF、UWPと順番に見ていきます。

Windows Formsの場合

全てのコントロールに含まれるBeginInvokeメソッドを使うことで、
UIスレッドに対しコントロールの更新処理を実行することができます。
以下のブログを参考にしました。
Part 1. Windows フォームのマルチスレッド処理の基礎 – とあるコンサルタントのつぶやき

実装すると、以下のようになります。

private void LongTask()
 {
     for (int i = 0; i <= 100; i++)
     {
         // 何らかのタスクを実施...
         System.Threading.Thread.Sleep(100);
  
         // 進捗状態として画面を更新
         this.BeginInvoke(
             new UpdateProgressBarDelegate(UpdateProgressBar),
             new object[] { i });
     }
}
WPFの場合

System.Windows.Threading.Dispatcherを利用することで、
UIスレッドに対しコントロールの更新処理を実行することができます。
以下のブログを参考にしました。
非同期処理とディスパッチャーufcpp.wordpress.com


実装イメージは以下の通りです。

this.Dispatcher.BeginInvoke(new
      Action(() =>
      {
             //UIスレッドで実行すべき処理
       }));
});
★UWPの場合

最後に、UWPについてみていきます。
UWPも、WPFと同じようにDispatcherを使うことで、
UIスレッドに対しコントロールの更新処理を実行することができます。
UWPのDispacherは、Windows.UI.Core名前空間にあり、WPFのDispacherとは別のものです。
(プロパティ名がDispacherなだけで、クラス名はCoreDispatcherです。)

以下のように実装することができます。

 private int SampleMethod()
 {
     int sum = 0;
     int length = 10;
     for (int i = 0; i < length; i++)
      {
          Task.Run(async () =>
          {
              await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
              {
                  //ユーザーインターフェースを操作する
                  this.TaskStatus_TextBlock.Text = i.ToString();
              });
          });
          sum += i;
      }
      return sum;
}

まとめ

TaskからUIを更新するときは、「タスクスケジューラーのオプションを指定」。
Windows Formsのときは、「System.Windows.Forms.ControlクラスのBeginInvokeメソッド」。
WPFのときは、「System.Windows.Threading.DispatcherクラスのBeginInvokeメソッド」。
UWPのときは、「Windows.UI.Core.CoreDispatcherのRunAsyncメソッド」。



これを意識して、冒頭に書いた「やりたいこと」ができるアプリを作ってみようと思います( ゚Д゚)

Task.Start ×Task.Run × TaskFactory.StartNew

この記事の目的

この記事では、
Taskを開始するメソッドのベストチョイスを探し出すこと
を目的としています。

Google翻訳に手伝ってもらいながら訳したMSDNの説明をもとに考えていきます。

本題

★Start

MSDNの説明を訳してみました。

      • -

タスクは1回だけ起動して実行できます。
2回目にタスクをスケジュールしようとすると、例外が発生します。
Startは、Taskコンストラクタの1つを呼び出して作成されたタスクを実行するために使用されます。
通常は、作成したタスクを条件付きで実行する場合など、タスクの作成を実行から切り離す必要がある場合にこれを行います。
タスクインスタンス化を実行から分離する必要がないような一般的なケースでは、Task.RunまたはTaskFactory.StartNewメソッドのオーバーロードを呼び出すことをお勧めします。

      • -

Startを利用するのは、
タスクの作成を実行から切り離す必要がある場合
で、それ以外は、Task.RunやTaskFactory.StartNewが推奨されています。

★TaskFactory.StartNew

MSDNの説明を訳してみました。

      • -

.NET Framework 4.5以降、タスクを起動するには、Task.Runメソッドをお勧めします。
StartNewメソッドを使用するのは、実行時間の長い計算処理タスクに対して細かい制御が必要な場合のみです。
これには、以下を制御するシナリオが含まれます。

1)タスク作成オプション。
既定でTask.Runメソッドによって作成されたタスクは、TaskCreationOptions.DenyChildAttachオプションを使用して作成されます。
この動作をオーバーライドする、または他のTaskCreationOptionsオプションを提供するには、StartNewオーバーロードを呼び出します。

2)パラメータ渡しのオプション。
Task.Runメソッドのオーバーロードは、タスクデリゲートにパラメータを渡すことを許可しません。
StartNewメソッドのオーバーロードはこれを行います。

3)タスクスケジューラのオプション。
Task.Runメソッドのオーバーロードは、デフォルトのタスクスケジューラを使用します。タスクスケジューラを制御するには、スケジューラパラメータを使用してStartNewオーバーロードを呼び出します。

      • -

タスクの実行には、Task.RunをTask.Runを利用することが推奨されています。
ただ、Taskの実行に関して、細かい制御が必要な場合には、StartNewの(たくさんある)オーバーロードが必要になるため、こちらを利用することになる。ということみたいです。

★Run

MSDNの説明を訳してみました。

      • -

Runメソッドは、デフォルト値を使用してタスクを開始するのを容易にする一連のオーバーロードを提供します。
これは、StartNewのオーバーロードに対する軽量の代替手段です。

      • -

第一候補:Task.Run。以上。的な感じです(´∀`=)

まとめ

疑っていたわけではありませんが、
いろいろなブログでも紹介されているように、
第一候補は、Run。
それ以外の特別なシナリオのとき、
適宜StartNew、Startを選択するというのがベストチョイスでした。

理由は、タスク生成時のオプションを指定する手間を省けるから。といったところでしょうか。

基礎学習 × .NET Framework の非同期処理を見直してみた × その3

この記事の目的

この記事では、
Taskクラスの基本的?な使い方をまとめること
を目的としています。

本題

★Taskクラスでよく使われるメソッドとは?

Intericodeでは以下のメソッドがよく使われる項目として表示されました。
f:id:kinakomotitti:20180608211952p:plain

InteriCodeについては、以下のページに詳細が記載されています。
www.visualstudio.com


★Task.Run

Taskのインスタンスを作成し、すぐにタスクを実行します。
Taskに後続のタスクを設定しない場合、このメソッドを利用することになります。

★Task.Start

StartメソッドはRunメソッドとは違い、Taskのインスタンスを作成した後、任意のタイミングでタスクを実行します。
こちらは、後続のタスクを設定した後にタスクを実行させたい場合に利用することになります。
また、以下のように定義することで、Runメソッドのように実行することも可能です。
ということで、今のところRunとStartの明確な使い分けができません(=_=)
人気があるのは、Runメソッドということですが(´▽`)

new Task(() => { /*何か処理*/}).Start();

★親Task、子Task

親タスク、子タスクを設定するには、タスクを定義するとき、TaskCreationOptionsのAttachedToParentオプションを指定します。

親に紐づいているタスク(子タスク)は、親タスクの完了時には完了していることが保証されますが、
紐づいていないタスクは、それが保証されません。
TaskCreationOptionsを注意深く見ないと、思わぬバグを仕込みそうです。。。
今のところ、実際の開発で使ったことも使う予定もないのでピンときません( ゚Д゚)

★条件付き実行処理

Task.ContinueWithメソッドを利用することで、元のタスクの完了時に指定したタスクを実行させることができます。
複数回ContinueWithを呼び出し、複数のタスクを元のタスクに紐づけることもできます。
この複数回ContinueWthメソッドを呼び出す方法は大きく2つあると思っています。
それらの2つの場合を意識して実装したコードと、それらの実行結果は以下の通りです。
1つ目:常に元タスクのインスタンスに対してContinueWithする場合
元タスクが完了したとき、元タスクにContinueWithが一斉に開始されます。
以下の例では、3つContinueWithしていたので、元タスク完了時にスレッドが3つ使われて処理が開始されました。

2つ目:ContinueWithの戻り値のタスクにContinueWithしていく場合

また、ContinueWithメソッドには、親Task、子タスクと同じようにTaskCreationOptionsを指定することができます。
TaskCreationOptionsには15個のオプションがあります。
Intelicodeでは確認したところ、以下の5つがよく使われているオプションでした。
f:id:kinakomotitti:20180608211937p:plain


特に人気の以下のオプションについて、実行時の違いを比較していきます。
なお、NotOnCanceledと比較できるようにするため、NotOnFaultedはOnlyOnCanceledにしました。
※Noneは省略しましたm(_ _)m
ExecuteSynchronously
NotOnRanToCompletion
NotOnCanceled
OnlyOnCanceled

ExecuteSynchronouslyの実装と実行結果↓

NotOnRanToCompletionの実装と実行結果↓

NotOnCanceled/OnlyOnCanceledの実装と実行結果↓

★Taskのキャンセル

前回のブログでまとめました(/・ω・)/
kinakomotitti.hatenablog.com


まとめ

・処理が終わったタスクの実行結果(成功、失敗、キャンセル)を確認して、
 それに合わせて次の処理を呼び出すという処理が必要な場合、Taskを利用する。
・ContinueWithを利用したタスク処理を実装・レビューするときは、
T askCreationOptionsをよく見て、適切なオプションであるか確認する。

基礎学習 × .NET Framework の非同期処理を見直してみた × その2

この記事の目的

この記事では、
.【NET Framework】非同期処理のキャンセル処理についてまとめること
を目的としています。

本題

★はじめに

ここでは、「プログラミング.NET Framework 第4版」を読んで学んだことを中心にアウトプットします。

「その2」ということで、今回は、Threadクラス、ThreadPoolクラス、Taskクラスを使った非同期処理のキャンセルの実装方法とそれぞれの注意点をまとめてみます。

.NET Frameworkの非同期処理概要・キャンセル編

教科書にはこうありました。

    • -

.NET Frameworkはキャンセル処理の標準的なパターンを提供しています。
このパターンは協調的(cooperative)です。

キャンセルしたい処理を実行するコードと、
処理をキャンセルしようとするコードは、
両方ともこれから説明する型を使用しなければなりません。

    • -


基本的に、.NET Frameworkで非同期処理をキャンセルするためには、
System.Threading.CancellationTokenSource
を使った実装をする必要があります。

ということで、ここではNET Framework 4 以降で導入された、
「キャンセルのための統一されたモデル」にだけ絞って実装を整理していきます。

★Threadクラス

Threadクラスについても、CancellationTokenを使った協調的なキャンセル処理を実装することができます。

試しに、Static変数を使って、キャンセルされたことを非同期処理を実行しているスレッドに通知したり(以下のコードの「パターン1;その1」)、Abortメソッドを使い、スレッドそのものを停止させる(以下のコードの「パターン1;その2」)という処理の実装もしてみましたが、特に意味はなさそうです(´▽`)

非同期処理を実行するメソッドにCancellationTokenを渡し、
非同期処理を実行するメソッド内の処理で、CancellationTokenのIsCancellationRequestedプロパティにアクセスすることで、処理がキャンセルされたことを知ることができます(以下のコードの「パターン2」)


★ThreadPoolクラス

ThreadPoolクラスでも、Threadクラスと同じように、協調的なキャンセルのパターンが利用することができます。

当然ですが、キャンセル処理を想定していないメソッドはキャンセルすることができません(以下のコードの「キャンセルできない」とコメントがあるところ)


★Taskクラス

Taskクラスでも、Thread/ThreadPoolクラスと同じように、CancellationTokenを利用して協調的なキャンセル処理を実装することができます。

TaskとThread/ThreadPoolの違いは、Taskには以下のステータスの違いがある点です。
・完了:処理をすべて実行し、処理が正常に終わったことを示す
・失敗:処理が何らかの理由で失敗したことを示す
・キャンセル:処理をすべて実行できなった(キャンセルされた)ことを示す

そのため、Taskでは、これらのステータスを知るため、ThrowIfCancellationRequestedメソッドを利用します。
ThrowIfCancellationRequestedを呼び出すと、System.Threading.Tasks.TaskCanceledExceptionがスローされます。
この例外は、TaskのResultプロパティにアクセスしたとき、System.AggregateException のInnerExceptionsリストに格納されてリスローされます。





その2、まとめ

よくよく調べてみると、.NET Framework4以降に追加されたCancellation Tokenによる協調的なキャンセルモデルにより、Taskも、Threadも、ThreadPoolも、同じような実装で処理をキャンセルすることができました。

ということで、今回のまとめは以下の通りです。

  • 非同期処理のキャンセルはCancellationTokenを利用する
  • Thread/ThtreadPoolを利用するときは、IsCancellationRequested。
  • Taskを利用するときは、IsCancellationRequestedで判断したのち、ThrowIfCancellationRequestedで例外をスローする。

基礎学習 × .NET Framework の非同期処理を見直してみた × その1

この記事の目的

この記事では、
.NET Frameworkの非同期処理についての基本的なことをまとめること
を目的としています。

また、基本的なことをまとめる中で、
「非同期処理プログラミングを学ぶ上で、どこから手を出していけばよいか」
を自分なりに考え直してみます。

本題

★はじめに

ここでは、「プログラミング.NET Framework 第4版」を読んで学んだことをアウトプットしています。

「その1」ということで、今回は、多数ある実装方法のうち、
Thread、ThreadPool、Taskクラスの概要と、
それらを使って非同期処理を開始するための実装についてまとめます。

さらに、これまで個人的に後回しにしていたAsync/Awaitについてもまとめてみます。

.NET Frameworkの非同期処理概要

I/O バインドのニーズ (ネットワークからのデータの要求、データベースへのアクセスなど) がある場合、非同期プログラミングを利用することになります。 CPU バインドのコードにも、コストのかかる計算の実行など、非同期コードに適したシナリオがあります。

docs.microsoft.com

Microsoft Docsには上記のように紹介されています。
このように、計算処理の実行時間を短縮したり、
GUIアプリケーションで、処理中でも画面を操作不能にしないようにするときにも
非同期処理が必要となる場面が多くあります。

★Threadクラス

Threadクラスは、専用スレッドを作成することができるクラスです。
実行をバックグラウンド・フォアグラウンドで実行するように指定することができたり*1
スレッドの優先度を指定して実行することができたりと、
スレッドに対していろいろな設定を行うことができます。

しかし、ThreadPoolのように、”前に使ったスレッドをつかいまわす”ことがで来ません。
それにより、スレッドを作成するためのコストが毎回かかってくるため、パフォーマンスの面で課題があります。
これらの理由から、Threadクラスでの非同期処理の実装は推奨されていないようです。

そんなThreadクラスを使った実装は以下のようになります。

★ThreadPoolクラス

ThreadPoolクラスを利用して非同期処理を実装すると、CLRで用意されたスレッドプールを利用することができます。
スレッドプールは、以下のような特徴があります。

アプリケーション自身が使用できるスレッドのセットと考えることができます。

  • -

スレッドプールのスレッドがタスクを完了したとき、そのスレッドは破棄されずにスレッドプールに戻され、別の要求に応答するために待機し続けます。

スレッドプールにスレッドがない場合は、スレッドの作成が行われますが、
スレッドがある場合は、あるものを使うため、スレッド作成にかかるコストがかかりません。

ちなみに、スレッドプールのスレッドは規定で、バックグラウンドで実行されます。

そんなThreadPoolクラスを使った実装は以下のようになります。

★Taskクラス

ThreadPoolクラスでは、スレッドの再利用、非同期処理のキャンセル、タイムアウトなど、よく使いそうな機能がサポートされています。
しかし、非同期処理が完了したという通知を呼び出し元にできないという問題があります。
また、処理の結果を呼び出し元に返すこともできません。

それらの問題を解決するために登場したのがTaskクラスです。
Taskクラスを利用することで、ThreadPoolクラスのいいところはそのままに、
呼び出し元に処理(タスク)の完了通知や、結果を返すことができます。
タスクの完了通知が受け取れることで、
タスクの完了を待って、後続の処理を実行することができるようになります。
Taskクラスのこれらの機能は、また別枠で整理していこうと思います(´▽`)

そんなTaskクラスを使った実装は以下のようになります。

★Async/Await

Async/Await修飾子を使った非同期処理の実装はとてもシンプルで、
ほぼ、同期処理の実装と同じように実装することができる。

ただ、シンプルなだけ、内部で行われていることを把握しようとするととても大変です。
参考書には以下のように説明があります。

メソッドをasyncで修飾すると、基本的に、コンパイラはメソッドのコードを、非同期ステートマシンを実装する型に変換します。これによって、スレッドがいくつかのコードを非同期ステートマシン内で実行するようにし、完了に必要なコードすべてを実行することなく制御を戻すことができるようになります。

非同期ステートマシン・・・

わかりそうでわからない・・・

ここが理解できるところを一つのゴールにして今後も調べていきます(=_=)
ここでは、公式のDocsにあった実装時の注意点で気になった内容を確認して、お茶を濁そうと思いますw

async メソッドの本体に await キーワードが含まれないと、何も行われません
これは、忘れてはならない重要なことです。 await が async メソッドの本体で使われていない場合、C# コンパイラは警告を生成しますが、コードは通常のメソッドと同様にコンパイルされて実行されます。 また、非同期メソッドに対して C# コンパイラが生成するステート マシンは何も行わないので、非常に非効率的であることにも注意してください。

docs.microsoft.com

awaitのないメソッドはただの同期処理として扱われるようです。
以下、試してみた実装と結果です。

★結局、どんな時にどれを使えばいいのか

ここにあるだけで、3つの実装方法があります。
結局どれを選択していけばよいのか迷いますが、
実装効率、Docsを参考にすると、非同期処理は、Task、Taskを利用するのが一番だと思います。
また、Taskを利用するにあたり、Async/Await修飾子を利用することで、煩雑になりがちな非同期処理をシンプルに実装することができます。
パフォーマンス問題や、特殊なシナリオを実現する必要があるときだけ、Threadクラスなどを視野に入れていけばよいのかなと思います。

その1、まとめ

非同期処理を実装するための技術として、歴史的にはいろいろなものがありますが、
Taskクラスを利用する非同期処理を把握するのが一番!(のはず)

それ以外は、出てきたときにまた考えますw

UWP × GlobalHooks × 使えない理由を検証してみた

この記事の目的

この記事では、
UWPでGlobal Hookが機能するかについての検証結果をまとめること
を目的としています。

本題

★きっかけ

WPFでGlobalHookを使ったツールを作成しました。

作ったツールは、以下の機能を有しています。
1)GlobalHookを使って、ユーザー操作(マウスクリックやキーダウンなど)イベントを取得する。
2)イベントをきっかけにスクリーンショットを取得する。
3)エクセルファイルに2)で取得したスクショを時系列順に貼り付ける。

主に、何かのアプリのテストを手動で実行し、そのエビデンスを作成するときや、
何かの作業(特にGUIで操作するもの)を行うとき、その操作手順を記録するとき
に利用することを目的としたツール*1となっています。


このツールの配布の方法を考えたとき、GitHubからダウンロードするより、
UWPのほうが便利かと思い、WPF→UWPへつくりかえようと考えました。

UWPへのつくりかえにあたり、大きな問題となったのは、
スクリーンショットの取得
・エクセルファイルの出力
GlobalHookを使ったユーザー操作イベントの取得
です。

ということで、3つ目のGlobalHookが機能するかについて調べ始めました。

調べ始めてすぐに、StackOverFlowにて、GlobalHookがUWPで使えないという回答を見つけました。
stackoverflow.com


(自分が正しく議論が理解できていたら)
UWPではGlobalHookの選択肢はないということになります。

使えないってどういうことか・・・
コンパイルが通らないレベル?
実行時にエラーになるレベル?

という疑問があったので、どんなふうに”使えない”のか実際にサンプルを作って試してみることにしました。


★開発環境情報

Windows 10 1803
Visual Studio 2017 15.7.2
Grobal Hook Classはここのクラスを使いました。
github.com


★実装

UWPの画面自体はとてもシンプルで、
テキストブロックが1つだけある画面を作成しました。
テキストブロックの名前は"Sample"です。
f:id:kinakomotitti:20180526145633p:plain

画面が準備できたので、コードビハインドの実装をしていきます。

Hook時の操作を実装するメソッドを以下のように定義します。
以下の例では、マウスの左クリックイベントを拾うように定義します。

        void MouseHook_LeftButtonDown(GrobalHooks.MouseHook.MSLLHOOKSTRUCT mouseStruct) 
       {
            //TEXT BLOCKに実行時間を表示させる
            this.Sample.Text = DateTime.Now.ToString(”yyyyMMdd-HHmmss.ffffff”);
        }


次に、Hookのイベントを登録する処理を定義します。

        public void InitializeHooks()
        {
            // register events
            mouseHook.LeftButtonDown += new GrobalHooks.MouseHook.MouseHookCallback(MouseHook_LeftButtonDown);

            //開始
            mouseHook.Install();
            keyboardHook.Install();
        }


InitializeHooksメソッドを画面クラスの初期化時に呼び出します。

        public MainPage()
        {
            this.InitializeComponent();
            this.InitializeHooks();
        }
★動作確認!

実行したところのイメージは以下のようになります。
※初期表示時は”AAA”と表示しています。
 クリックすると、フックしたイベントが実行され、時刻を更新します。
 表示形式は、yyyyMMdd-HHmmss.ffffffです。

f:id:kinakomotitti:20180526145957g:plain



とりあえず・・・・


動いた(´▽`)
なんやかんやでUWPでもGlobalHookは動作することがわかりました!


★問題

しかし、そう簡単にはいかないようです(´・ω・`)


フックしたイベントは、UWPのアプリウィンドウの領域だけでしか発生しませんでした。。。
つまり、ウィンドウの領域外の部分での操作はフックできません。。。


これでは、ツールの目的を達成することができません。。。


上記の動作が本当だとしたら、考えられる対応としては、
UWPアプリを全画面表示させ、背景を透明にする
ことでしょうか( ゚Д゚)

そこまでいったら、グローバルフックではなく、画面のクリックイベントを拾えばよいですねw

まとめ

GlobalHookはUWPでも動作することが確認できた。
ただし、UWPのウインドウの中の操作に限る。←これが問題。。。
これがStackOverFlowでいわれていた”使えない”の理由なのだと感じました。

UWPだけでなく、フルスクリーンに対しての操作をフックしたい場合は
WPFWindows Formsを利用する必要がありそうです(=_=)

*1:つまらないものですが、プレリリース版は以下のURLで公開しています。 github.com

UWP × Windows.Graphics.Capture × スクリーンショットの取得サンプルを作ってみた

この記事の目的

この記事では、
UWPでスクリーンショットを取得するまでの手順をまとめること
を目的としています。

本題

★背景

WPFスクリーンショットを取得するアプリを作っていましたが、
配布をすることと、興味があったことから、
UWPで同じことができるアプリを作ってみようと思ったのがきっかけです。

UWPでスクリーンショットを取得する方法を調べてみると、Docsでちょうどよい記事を見つけました。
docs.microsoft.com


よく読んでみると、UWPでスクリーンショットを取得する機能は、
Windows 10 Version 1803で新しく追加された機能らしいです。

タイミングが良すぎましたw
記事に従ってちゃかっと実装完了♪



とか思っていましたが、そう簡単にいかなかったです。
ということで、公式の手順と、自分なりに試行錯誤した手順をまとめてみました。



★準備・その1

当たり前ですが、Visual Studio を開き、UWPのプロジェクトを作成しておきます。
何でもいいですが、わかりやすいように空白のアプリを選択します。
f:id:kinakomotitti:20180523220257p:plain


★準備・その2

ここは、公式の手順にある通りです。

1. Right-click Package.appxmanifest in the Solution Explorer.
2. Select Open With...
3. Choose XML (Text) Editor.
4. Select OK.
5. In the Package node, add the following attribute: xmlns:uap6="http://schemas.microsoft.com/appx/manifest/uap/windows10/6"
6. Also in the Package node, add the following to the IgnorableNamespaces attribute: uap6
7. In the Capabilities node, add the following element: 

5.6.を実装後は以下のようになります。
f:id:kinakomotitti:20180523220304p:plain



7.を実装後は以下のようになります。
f:id:kinakomotitti:20180523220311p:plain



マニュフェストファイルって、コードを直接編集するのですね。
GUIからしか操作したことがなかったので、少し新鮮な気持ちでした。


★準備・その3

対象のプロジェクトに、NugetでWin2D.uwpをインストールします。
f:id:kinakomotitti:20180523220325p:plain



★サンプルコード

MainPage.xaml.csに以下のコードを追加します。
長いコードなのはご容赦ください。。。
公式コードをベースとしていますが、
エラーが出ていたところを修正しています。
また、出力結果の確認をするために、スクリーンショットpng形式で出力する実装を追加しました。

using Microsoft.Graphics.Canvas;
using System;
using System.Threading.Tasks;
using Windows.Graphics;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX;
using Windows.Storage;
using Windows.UI.Composition;
using Windows.UI.Xaml.Controls;

namespace App1
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        // Capture API objects.
        private SizeInt32 _lastSize;
        public GraphicsCaptureItem item;
        private GraphicsCaptureItem _item;
        private Direct3D11CaptureFramePool _framePool;
        private GraphicsCaptureSession _session;

        // Non-API related members.
        private CanvasDevice _canvasDevice;
        private CompositionDrawingSurface _surface;

        public MainPage()
        {
            this.InitializeComponent();
            StartCaptureAsync();
        }

        public async Task StartCaptureAsync()
        {
            // The GraphicsCapturePicker follows the same pattern the 
            // file pickers do. 
            var picker = new GraphicsCapturePicker();
            GraphicsCaptureItem item = await picker.PickSingleItemAsync();

            // The item may be null if the user dismissed the 
            // control without making a selection or hit Cancel. 
            if (item != null)
            {
                StartCaptureInternal(item);
            }
        }

        private void StartCaptureInternal(GraphicsCaptureItem item)
        {
            // Stop the previous capture if we had one.
            StopCapture();

            _item = item;
            _lastSize = _item.Size;
   
            //初期化がないとPoolのCreateで落ちるので追加。
            _canvasDevice = new CanvasDevice(); 
            _framePool = Direct3D11CaptureFramePool.Create(
               _canvasDevice, // D3D device 
               DirectXPixelFormat.B8G8R8A8UIntNormalized, // Pixel format 
               2, // Number of frames 
               _item.Size); // Size of the buffers 

            _framePool.FrameArrived += (s, a) =>
            {
                // The FrameArrived event is raised for every frame on the thread
                // that created the Direct3D11CaptureFramePool. This means we 
                // don't have to do a null-check here, as we know we're the only 
                // one dequeueing frames in our application.  

                // NOTE: Disposing the frame retires it and returns  
                // the buffer to the pool.

                using (var frame = _framePool.TryGetNextFrame())
                {
                    ProcessFrame(frame);
                }
            };

            _item.Closed += (s, a) =>
            {
                StopCapture();
            };

            _session = _framePool.CreateCaptureSession(_item);
            _session.StartCapture();
        }

        public void StopCapture()
        {
            _session?.Dispose();
            _framePool?.Dispose();
            _item = null;
            _session = null;
            _framePool = null;
        }

        private async Task ProcessFrame(Direct3D11CaptureFrame frame)
        {
            // Resize and device-lost leverage the same function on the
            // Direct3D11CaptureFramePool. Refactoring it this way avoids 
            // throwing in the catch block below (device creation could always 
            // fail) along with ensuring that resize completes successfully and 
            // isn’t vulnerable to device-lost.   
            bool needsReset = false;
            bool recreateDevice = false;

            if ((frame.ContentSize.Width != _lastSize.Width) ||
                (frame.ContentSize.Height != _lastSize.Height))
            {
                needsReset = true;
                _lastSize = frame.ContentSize;
            }

            try
            {
                // Take the D3D11 surface and draw it into a  
                // Composition surface.

                // Convert our D3D11 surface into a Win2D object.
                var canvasBitmap = CanvasBitmap.CreateFromDirect3D11Surface(
                    _canvasDevice,
                    frame.Surface);

               //出力結果を確認するために、ファイルを「Pictures」フォルダに出力するように変更。
               //ここから
                string filename = $"{ DateTime.Now.ToString("yyyMMdd_HHmmss.fffffff")}.png";
                StorageFolder pictureFolder = KnownFolders.SavedPictures;
                var file = await pictureFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
               //ここまで

                using (var fileStream = await file.OpenAsync(FileAccessMode.ReadWrite))
                {
                    await canvasBitmap.SaveAsync(fileStream, CanvasBitmapFileFormat.Png, 1f);
                }
                // Helper that handles the drawing for us, not shown. 
                //FillSurfaceWithBitmap(_surface, canvasBitmap);
            }
            // This is the device-lost convention for Win2D.
            catch (Exception e) when (_canvasDevice.IsDeviceLost(e.HResult))
            {
                // We lost our graphics device. Recreate it and reset 
                // our Direct3D11CaptureFramePool.  
                needsReset = true;
                recreateDevice = true;
            }

            if (needsReset)
            {
                ResetFramePool(frame.ContentSize, recreateDevice);
            }
        }
       //公式コードでは、第一引数のsizeは「Vector2」型で定義されていましたが、
  //コンパイルが通らないので、「SizeInt32」型に変更しました。
        private void ResetFramePool(SizeInt32 size, bool recreateDevice)
        {
            do
            {
                try
                {
                    if (recreateDevice)
                    {
                        _canvasDevice = new CanvasDevice();
                    }

                    _framePool.Recreate(
                        _canvasDevice,
                        DirectXPixelFormat.B8G8R8A8UIntNormalized,
                        2,
                        size);
                }
                // This is the device-lost convention for Win2D.
                catch (Exception e) when (_canvasDevice.IsDeviceLost(e.HResult))
                {
                    _canvasDevice = null;
                    recreateDevice = true;
                }
            } while (_canvasDevice == null);
        }
    }
}

まとめ

UWPでスクリーンショットが取得できる。
スクリーンショット機能は、Windows10 1803から。
Windows10のバージョンに合わせて、SDKのインストールも忘れずに行う必要がある。