.NET6、.NET7のインテリセンス(IntelliSense)やツールチップを日本語の表記にする

.NET5 まで

.NETからインテリセンスの表示が英語のみになってしまいました。
.NET Core 3.0、.NET Core 3.1、.NET 5まではローカライズされたファイルを配置すれば日本語化できます。
learn.microsoft.com

ただし、.NET 6.0からはローカライズされたファイルの配布が無くなってしまいました。

.NET6以降

探していたら中国の方がいいツールを作られていました。
github.com
Githubでは中国語のファイルしか配布されていませんが、自分でビルドすることで指定した言語のファイルを作成することができるようです。
オンラインドキュメントからインテリセンスのファイルを作成するみたいで、すごく時間がかかりますが自分で作成して使いましょう!

使うオプションの説明

ContentCompareType (デフォルトはOriginFirst)
-cc, --content-compare

OriginFirst 英語原文と翻訳後の順で表記します
LocaleFirst 翻訳後と英語原文の順で表記します
None 翻訳後の説明のみ表記します

使い方

1. dotnet ツールとしてインストール
dotnet tool install -g islocalizer
2. 日本語のインテリセンスファイルを作成

net7.0 のファイルを作ります。
日本語 英語の順で表示するようにオプションを付けました。

islocalizer build -m net7.0 -cc LocaleFirst

以下のような感じで処理が進みます。

[10:10:10 I] Start generate. PackName: null, Moniker: net7.0, Locale: ja-jp, ContentCompareType: LocaleFirst.
[10:10:10 I] Processing pack [Microsoft.AspNetCore.App.Ref:net7.0]. Progress 1/3.
[10:10:10 I] Progress PackRef[1/3]->File[1/132]. Processing [Microsoft.AspNetCore.App.Ref:7.0.13:Microsoft.AspNetCore.Antiforgery] now.
[10:10:10 I] Progress PackRef[1/3]->File[2/132]. Processing [Microsoft.AspNetCore.App.Ref:7.0.13:Microsoft.AspNetCore.Authentication.Abstractions] now.
[10:10:10 I] Progress PackRef[1/3]->File[3/132]. Processing [Microsoft.AspNetCore.App.Ref:7.0.13:Microsoft.AspNetCore.Authentication.Cookies] now.
[10:10:10 I] Progress PackRef[1/3]->File[4/132]. Processing [Microsoft.AspNetCore.App.Ref:7.0.13:Microsoft.AspNetCore.Authentication.Core] now.
[10:10:10 I] Progress PackRef[1/3]->File[5/132]. Processing [Microsoft.AspNetCore.App.Ref:7.0.13:Microsoft.AspNetCore.Authentication.OAuth] now.
[10:10:10 I] Progress PackRef[1/3]->File[6/132]. Processing [Microsoft.AspNetCore.App.Ref:7.0.13:Microsoft.AspNetCore.Authentication] now.
・・・(略)

オンラインドキュメントをダウンロードして生成しているので、全部で1時間ぐらいはかかります。
--parallel-count オプションを付けて並列処理数を増やせば早くなるかも?
あまり多すぎるとMicrosoftへのアクセスが遮断されちゃうぞ!

2回目からはキャッシュされているのでちょっとマシになる。

処理が終わるとzipファイルが生成されます。

[10:20:09 W] localization pack is saved at C:\Users\********\AppData\Local\Temp\IntelliSenseLocalizer\output\net7.0@ja-jp@None.zip.

ここのパスは覚えておきましょう。

3. 生成したインテリセンスのファイルをインストール
islocalizer install C:\Users\********\AppData\Local\Temp\IntelliSenseLocalizer\output\net7.0@ja-jp@None.zip

既定の場所にファイルを配置してくれます。

いくつかのテキストがうまく取得できていなかったので、修正プルリクしてみました

Fixed some text that was not retrieved from the Japanese online doc. by kitunechan · Pull Request #7 · stratosblue/IntelliSenseLocalizer · GitHub

Visual Studioの設定ファイル

Visual Studioの設定ファイルは2つの場所に保存されています。

CurrentSettings.vssettings

1つ目は CurrentSettings.vssettings です。
中はXML形式になっています。

VisualStudioの [ツール → オプション] から [環境 → 設定のインポートとエクスポート] で保存先を確認できます。

また、VisualStudioの ツール に [設定のインポートとエクスポート] のウイザードがあります。

Visual Studioの環境を移動させたいときに便利です。

プライベートレジストリ

2つ目はプライベートレジストリです。
以下の場所に保存されており、レジストリエディターより[ハイブの読み込み]で読み書きできます。

%LOCALAPPDATA%\Microsoft\VisualStudio\[VSバージョン]\privateregistry.bin

※ アンロードして開放しておかないとVisualStudioが起動できなくなるので注意


拡張機能を作っていると ShellSettingsManager がよく出てきますが、このプライベートレジストリに対して読み書きしているようです。
Microsoft.VisualStudio.Shell.Settings.ShellSettingsManager
ShellSettingsManager Class (Microsoft.VisualStudio.Shell.Settings) | Microsoft Learn

Git fatal: unsafe repository ('xxxxxxxxx' is owned by someone else)

fatal: unsafe repository ('xxxxxxxxx' is owned by someone else)

git v2.35.2 でセキュリティアップデートが入ったようで、コミットが開けなくなりました。
gitの対象フォルダの所有者が自分では無いことが原因のようで、まずはフォルダの所有者を確認しましょう。

どうしても所有者が自分以外になってしまう場合は、safe.directoryの設定をして例外的に許可を出せば大丈夫です。

バージョン管理システム「Git」にセキュリティ上の脆弱性、Git for Windowsユーザーやマルチユーザー環境利用者が取るべき対処法は? - GIGAZINE
いきなりgitが使えなくなった - 2022(山崎はるかのメモ)
TortoiseGitで「fatal: unsafe repository」なエラー - Qiita


事の発端:
VisualStudio内のGitが開けなくなって困った
→ SourceTreeでは開ける (内蔵Gitのためバージョンが低い)
→ VisualStudioのGitのバージョンがわからんぞ
  ↓ここにあるっぽい VisualStudioも内蔵Gitでした…

VisualStudio 2017だとGitのエラー表示してくれるけど、VS2019だと「エラーが1つあります」しかなくて困った困った

ソリューションを.Net Frameworから.NETへバージョンアップする方法

.NET5からソリューションの記述方法が変わりました。
今までのソリューションを.NET5、.NET6にするには結構めんどくさかったり。。。

マイクロソフトから「.NET アップグレード アシスタント」としてツールが出ています。

インストール

// インストール
dotnet tool install -g upgrade-assistant

// アップデート
dotnet tool update -g upgrade-assistant

使い方:

upgrade-assistant upgrade <MySolution.sln>
または
upgrade-assistant upgrade <MyProject.csproj>

後は英語の説明で動かしていけばコンバート完了です。


WPF アプリを .NET 6 にアップグレードする - .NET Core | Microsoft Docs
.NET アップグレード アシスタントを利用して .NET Framework から .NET 5 に超簡単アップグレード 【Windows Forms】 - Qiita

.NET Framework から .NET 5 に移植する - .NET Core | Microsoft Docs

[WPF] 編集可能なComboBoxのTextが消える問題

編集可能なComboBox (IsEditable="True")のItemsSorceを変更したときに、ItemsSorceから現在の項目が消えた場合にTextも消滅してしまう問題です。
あくまでもComboBoxのメニュー(DropDown)は「選択もできるよ!」のつもりなのに、メニューから消えるとTextも消えてしまいます。

<ComboBox IsEditable="True" ItemsSorce="{Binding Items}" Text="{Binding Item}" />
class vm {
  public string Item { get; set; }
  public IEnumerable<string> Items { get; set; } = new []{ "みかん", "りんご", "ぶどう" };
}

例えば ItemsSorce :[みかん、りんご、ぶどう] 、 Text:[みかん] の状態で、
ItemsSorce を [りんご、ぶどう] に変更すると Text が空欄になってしまうわけです。

イベント的にSelectionChangedしか無いのでこんな感じで対応しました。

public IEnumerable ItemsSource { get; set; }

void OnSelectionChanged( object sender, SelectionChangedEventArgs e ) {
	var comboBox = (ComboBox)sender;

	if( this.ItemsSource != comboBox.ItemsSource ) {
		this.ItemsSource = comboBox.ItemsSource;

		if( comboBox.SelectedItem == null ) {
			var text = comboBox.Text;
			comboBox.Dispatcher.BeginInvoke( (Action)( () => {
				comboBox.Text = text;
			} ) );
		}
	}
}

comboBox.Dispatcher.BeginInvoke がミソで、一旦comboBox.Textは空欄になってしまいますが再度入力し直す動きなります。
SelectedItemは先にNullになるけど、Textはまだ残っている不思議

参考:
ikriv.com

.Net 5 でインテリセンス(IntelliSense)やツールチップを日本語の表記にする

Visual Studio 2019を使い始めました。
というか.Net 5 を使い始めました。

.Net 5の初期状態だとツールチップが英語です😥

.Net Framework 4.5のときも同じような状態でしたが、これはバグだったみたい。
Visual StudioのIntelliSenseが英語になっていたので日本語に戻した - tmegos blog
c# - Visual Studio 2015 ツールチップ(summary)の日本語化 - スタック・オーバーフロー
.NET Framework 4.5でIntelliSenseのツールチップが英語になる理由 - The Grimoire of Nonsense

.Net 5(.NET Core 3.0以降)ではそもそも初期状態は英語のようです。

日本語化

日本語化する手順は公式でしっかり書かれていました。
ただ、インストールする場所(jaフォルダを配置する場所)がよくわからなくて苦労しました。
docs.microsoft.com


日本語のインテリセンスファイルをダウンロード以下のサイトよりダウンロードします。
dotnet.microsoft.com


解凍したフォルダの各jaフォルダを規定の位置にコピーします。

初期インストール場所は「%ProgramFiles%\dotnet\packs」なので、
例えば「Microsoft.WindowsDesktop.App.Ref」だと

C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\5.0.0\ref\net5.0

の中にjaフォルダをコピーしてください。

Microsoft.NETCore.App.Ref のほうも同じように

C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\5.0.0\ref\net5.0

の中にjaフォルダをコピーしてください。

ただ、NETStandard.Library.Ref には 5.0.0 がないんだけど、使わないから必要ないかな?

日本語化できました!

他の設定はデフォルトのままでした。
VisualStudioを起動している場合は、再起動が必要です。


参考サイト:
Visual Studio 2019 での .NET Core のインテリセンスを日本語化する - Alternative Architecture DOJO

[Visual Studio 2019] ソリューションエクスプローラーでAltキードラッグで、リンクファイルが作成できない

Visual Studio 2019で、リンクファイルのショートカット操作が消されてしまったようです。


https://social.msdn.microsoft.com/Forums/en-US/66715ed6-ed1b-4922-be5c-d79d3f3c22b8/altdrag-to-create-a-link-to-a-file?forum=visualstudiogeneral
https://developercommunity.visualstudio.com/t/ctrl-shift-drag-and-altdrag-could-not-add-the-exis/760536


そろそろ 2019 移行するかと思ったらしっかり踏んだ
DLL化して参照しろってことかな・・・

WPFでWindows Runtime API の Windows.winmd について

WPFWindows 8、8.1、10 のAPIを呼ぶことができます。
デスクトップ アプリからのWinRT API利用 | ++C++; // 未確認飛行 C ブログ

WPFのプロジェクトで Windows.winmd を参照するとライブラリとして使えるようになります。
WPF などの .NET Framework のアプリから UWP の API を呼ぶ - かずきのBlog@hatena
WPFアプリ(.Net Framework)でUWPのAPIを使う - Qiita

Windows SDKのインストール場所は C:\Program Files (x86)\Windows Kits\ になります。
Windows 8、8.1 はいいのですが、Windows 10 になるとメジャーアップデート毎にSDKのバージョンが違うため気をつける必要があります。

(ここからの部分を書きたいだけ)

Windows 10 のSDKをインストールすると C:\Program Files (x86)\Windows Kits\10\ にインストールされます。

Windows 10 SDK (10.0.10240) ~ (10.0.15063.468) (1507~1703)では Windows.winmd が同じ場所にインストールされてしまうので注意です。
最後にインストールしたSDKWindows.winmd が上書きされるようで、
Windows.winmd の違いはファイルの見た目では分からないので、どのバージョンのファイルか分からなくなります。

Windows 10 SDK (10.0.16299.91) (1709) からそれぞれのバージョンでフォルダ分けされるようになったのでこの問題は解決しています。

(ここまで)


まあ、Win10のサポート期間があるので、サポート対象のバージョンを使えばよさそうです。


ついでに、、、
特定のバージョンしか対応していませんが、NuGet から追加できるようになったようです。
こっちを使ったほうが簡単ですね。
.NET のプロジェクトから WinRT API を呼ぶのが凄く簡単になってます - かずきのBlog@hatena
 

MSDN コードギャラリーの提供終了

MSDN コードギャラリーのページが無くなり、新しいコードサンプルページが作成されています。
https://code.msdn.microsoft.com/
MSDN コード ギャラリーの提供終了 | Microsoft Docs

新しいコードサンプルページ
Browse code samples | Microsoft Docs

悲しいことに参考になるサンプルのリンクが消滅している状態です。

  • 10 行でズバリ !! シリーズ
  • 連載! とことん シリーズ

などなど

 
今までのページはGitHubに保存されているので、この中から探しましょう!
Microsoft Archive · GitHub
https://github.com/microsoftarchive/msdn-code-gallery-community-0-9-non-alphabetic
https://github.com/microsoftarchive/msdn-code-gallery-microsoft
https://github.com/microsoftarchive/msdn-code-gallery-community-a-c
https://github.com/microsoftarchive/msdn-code-gallery-community-d-l
https://github.com/microsoftarchive/msdn-code-gallery-community-m-r
https://github.com/microsoftarchive/msdn-code-gallery-community-s-z

[WPF] Expression Blend ライブラリのいらない言語リソースファイルたち

Expression Blend の Behavior 便利ですよね!
すっごいよく使います。

System.Windows.Interactivity.dll
Microsoft.Expression.Interactions.dll

Visual Studioから使うExpression BlendのBehavior達 - かずきのBlog@hatena


参照設定で追加してー
f:id:kitunechan:20190822154856p:plain

ビルドするとー
f:id:kitunechan:20190822155153p:plain

なにこれいらない・・・


こんな感じの謎のフォルダーができちゃいます。

de
en
es
fr
it
ja
ko
ru
zh-Hans
zh-Hant

理由は簡単 System.Windows.Interactivity.dll、Microsoft.Expression.Interactions.dll の言語リソース用のファイルです。

皆さん大体こう思いますね。
日本語しか使わないんだけど!?
ていうかいらないんだけど!?


削除したり、そもそもコピーされないようにしたりすることで回避しているようです。
[C#] ビルド後イベントで不要なxmlや言語フォルダを削除する - ざこノート
c# - When compiling WPF application language folders are copied to build folder - Stack Overflow
 

コピーされないようにする

大本から削除してコピーされないようにするパターンを紹介します。
コピー元になるファイルたちは以下の場所にインストールされます。
.netのバージョンによって v4.0 と v4.5 があるようです。

C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.0\Libraries
C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.5\Libraries

削除するのは戻せなくなるので、新しいフォルダに移動させていまいました。
f:id:kitunechan:20190822160332p:plain

これでビルド時に不要な言語ファイルがコピーされなくなります。
特にエラーも発生しないのでおすすめです。

Visual Studio Version Selector がぶっ壊れた

Visual Studioのソリューションファイル.slnはVisual Studio Version Selectorが関連付けられています。
Visual Studio Version Selectorは.slnファイル内に記載されているVisual Studioのバージョンを読み取り、適切なVisual Studioのバージョンを起動します。

調子が悪いとこんな画面が開きます
f:id:kitunechan:20190426093137p:plain
選択も何も、空っぽなんですが!?
OKを押しても反応がなく、キャンセルを押すと「開けませんでした。」とエラーウインドウが出る始末


新しいバージョンのVisual Studioをインストールしようとしたらこうなりました。。。

原因はVisual Studioの変更や更新が途中で止まっていることのようで、
Visual Studio Installer で止まっている変更や更新を進めたら正しくVisual Studioが起動しました。

最悪は修復すれば直るようです?
参考:
How do I add versions to “Visual Studio Version Selector”, my list is empty - Stack Overflow

ExceptionのHResultについて

C#でファイル操作などのWindowsに関わる操作を行うと内部的にはWindows APIが呼ばれます。
WindowsAPIはC#ではないので、エラーになったときにはC++的な形でエラーコードを返します。
それをC#でラッピングしてあるので、C#でExceptionとしてエラーを取得する事ができます。

C#のExceptionのメッセージだと大まかな分け方しかしていないので詳細に知りたい場合はHResultの値を調べます。

try{ 
}catch(Exception e ){
  e.HResult // <- こいつについての話
}

例えばファイルを置換に失敗した場合 IOException になります。

try{
  System.IO.File.Replace(sourceFileName, destFileName, null);
}catch( IOException e ){
  //  e.Message → 置換されるファイルを削除できません。
  //  e.HResult → -2147023721
}

int型の表示でマイナスの値が表示されますが、所詮ビットデータなので2進数表示で確認します。
f:id:kitunechan:20181204143437p:plain

更に上位下位として半分ずつにします。

2進数:1000000000000111 0000010010010111
16進数:0x8007 0x0497

で、これがなんなんだっていうと 上位の0x8007はWin32エラーコードということを示しています。
下位の0x0497がエラーの番号です。

ERROR_UNABLE_TO_REMOVE_REPLACED 1175 (0x497)
置換されたファイルを削除できませんでした。置換されるファイルと置換ファイルの名前は、元のまま変更されていません。

ReplaceFileA function | Microsoft Docs

こんな感じでWindows APIのエラーコードにたどり着くことができます。


参考:
ASCII.jp:Windowsで表示されるエラーコードの見方|Windows Info

無理やりワンライナー

こんな感じのときってあると思います。

var result = "";
if( 条件1 ){
  result = "1つ目";
}else if( 条件2 ){
  result = "2つ目";
}

この初期化めんどくさくない?

var result = "";

三項演算子

ただ、三項演算子で書くと「(◕‿‿◕)ワケガワカラナイヨ・・・」

var result = (条件1) ? "1つ目" : (条件2) ? "2つ目" : "";

メソッド

メソッドに切り出したいけど、メソッド名考えるの大変なんだよなー

var result = GetResult( 条件 );

string GetResult( 条件 ){
  if( 条件1 ){
    return "1つ目";
  }else if( 条件2 ){
    return "2つ目";
  }
  return "";
}

匿名メソッド&即実行

Funcで無理やり関数宣言&即実行じゃー

var result =
  ( (Func<string>)( () => {
    return "1";
  }) )();

または

var result = 
  ( new Func<string>( ()=>{
    return "2";
  }) )();

()括弧多すぎ問題


そしてStaticへ・・・

そしてこうなった・・・

var result = Func.Run( ()=>{
    return "1";
  } );

public static class Func {
  public static T Run<T>( Func<T> func ) {
    return func();
  }
}

他の便利な使い方

初期化のときにメソッドの形で書けます。

public class Item {
  public string Value { get; set; } = Func.Run( () => { return ""; } );
}

初期化のときにメソッドの形で書けます。その2

var item = new Item(){
  Value = Func.Run( () => { return ""; } ),
}

[Rx] 変更通知の合成はObservable.Merge<object>です。

WPFでBindingする時にLivetを使っていたのですが、ReactivePropertyが面白そうだったのではじめました。
Livetだと自分の好きなタイミングで変更通知イベントを出せていたのですが、Rxだとそのタイミングもロジックとして書く必要があります。

要は、AプロパティとBプロパティのどちらかが変わった時に、Cプロパティの変更通知を出したい みたいな

Livet

Livet で書くとこんな感じ

public class LivetViewModel : ViewModel {
  public int A {
    get => _A;
    set {
      if( _A != value ){
        _A = value;
        this.RaisePropertyChanged();
        this.RaisePropertyChanged( nameof(this.C) );
      }
    }
  }
  int _A;
  
  public int B {
    get => _B;
    set {
      if( _B != value ){
        _B = value;
        this.RaisePropertyChanged();
        this.RaisePropertyChanged( nameof(this.C) );
      }
    }
  }
  int _B;
  
  // A, B が変わった時にCの値が変わる
  public int C => A + B;
}

ReactiveProperty

だいたいCombineLatestを使えと書いてあるのでこんな感じ

public class RxViewModel {
  public RxViewModel() {
    this.C = this.A.CombineLatest(this.B, (a,b)=> a+b).ToReactiveProperty();
  }

  public ReactiveProperty<int> A { get; } = new ReactiveProperty<int>();
  public ReactiveProperty<int> B { get; } = new ReactiveProperty<int>();
  public ReactiveProperty<int> C { get; }
}

ReadOnlyとかSlimとかしっかり使うとこんな感じ

public class RxViewModel2 {
  public RxViewModel2() {
    this.C = this.A.CombineLatest(this.B, (a,b)=> a+b).ToReadOnlyReactivePropertySlim();
  }

  public ReactivePropertySlim<int> A { get; } = new ReactivePropertySlim<int>();
  public ReactivePropertySlim<int> B { get; } = new ReactivePropertySlim<int>();
  public ReadOnlyReactivePropertySlim<int> C { get; }
}

ただし、CombineLatest だと全ての値がそろわないと通知が始まらないので「値がない場合~」とかはできません。

やっと本題 Observable.Merge<object> を使いましょう。

public class RxMerge {
  public RxMerge() {
    this.C = Observable.Merge( this.A, this.B )
              .Select(x => this.A.Value + this.B.Value ).ToReadOnlyReactivePropertySlim();
  }

  public ReactivePropertySlim<int> A { get; } = new ReactivePropertySlim<int>();
  public ReactivePropertySlim<int> B { get; } = new ReactivePropertySlim<int>();
  public ReadOnlyReactivePropertySlim<int> C { get; }
}

CombineLatest の謎のラムダ式 (a,b)=> a+b も消えてスッキリしました。

全ての型が同じ場合は型推論ジェネリックの<T>部分を書かなくていいですが、複数の型が混ざる場合は<object>とすれば問題ありません。


そしてそして、本当にやりたかったことは ReactiveCommand のコマンドが実行可能かどうかという CanExecute 部分の書き方なのだった・・・

Observable.Merge<object>(
    // 変更通知 が来たら条件判定したいReactiveProperty
    this.A, this.B
  ).Select( x => {
    // True になる条件
    return this.A.Value != 0 && this.B.Value != 0;
  } )
	.ToReactiveCommand()

複数の条件が複雑に混ざり合うような時にとても重宝します。


追記 2018/10/18
ジェネリックのキャストの都合上IObservable<[struct]>がIObservable<object>にキャストできないので無理やり変換します。

こんなの用意して

public static Observable {
  public static IObservable<object> Cast<T>( this IObservable<T> source ) where T: struct {
    return source.Select( x => (object)x );
  }
}

こうだ!

public class RxMerge {
  public RxMerge() {
    this.C = Observable.Merge( this.A.Cast(), this.B )
                .Select(x => this.A.Value + int.Parse( this.B.Value ) ).ToReadOnlyReactivePropertySlim();
  }

  public ReactivePropertySlim<int> NumItem { get; } = new ReactivePropertySlim<int>();
  public ReactivePropertySlim<string> StrItem { get; } = new ReactivePropertySlim<string>();
  public ReadOnlyReactivePropertySlim<int> C { get; }
}


2019/01/29 追記
ToUnit() で IObservable に変換するのがデフォらしい。
いまさら聞けないReactive Extensions.3 - Qiita

Unit は戻り値なしのvoid的なやつだそうです。
でもデバッグ的にはIObservable<object> のほうが便利だなー

// ReactiveProperty.dll
namespace Reactive.Bindings.Extensions {
	public static class ToUnitObservableExtensions {
		public static IObservable<Unit> ToUnit<T>( this IObservable<T> self );
	}
}

// こんな感じ
Observable.Merge( this.A.ToUnit(), this.B.ToUnit() )

[WPF] タッチ長押しで右クリックをやめる(□を表示させない)

少ないですがWindowsタブレット用のアプリでドラッグやタッチしっぱなしの操作がメインのときにタッチ長押しの右クリック機能が邪魔なときがあります。

対象のXamlの添付プロパティにこれを使いするだけ

Stylus.IsPressAndHoldEnabled = false

こんな感じです。

<Grid Stylus.IsPressAndHoldEnabled="False">
</Grid>

WindowsFormsではこの設定はなさそうなので珍しくWPFの利点でしょうか

参考
[C#/VB/XAML] WPF 4 における TouchDown や TouchUp など基本的なイベントを使用して、マルチタッチ対応アプリケーションを作成 in C#, VB.NET

他にもStylusクラスには面白そうなものがあるのでご覧あれ
Stylus Class (System.Windows.Input) | Microsoft Docs