- 投稿日:2020-07-26T23:25:56+09:00
Azure WebApps に ASP.NET Core gRPC をデプロイしてみよう
Azure App Service が gRPC をデプロイ出来ない問題があるので悶々としてたのですが gRPC-Web プロトコルになりますが Azure App Service にデプロイできるみたいです!!ASP.NET Blog で gRPC-Web for .NET now available という記事でアナウンスされていました。ということでやってみましょう。
参考ドキュメントはこちら: ブラウザー アプリでの gRPC の使用
プロジェクトの作成
ASP.NET Core Web アプリケーションで空のプロジェクトから始めてみましょう。
まず、2 つのパッケージを追加します。
- Grpc.AspNetCore
- Grpc.AspNetCore.Web
どちらも執筆時点の最新の 2.30.0 を入れました。
とりあえずシンプルなハローワールド用のSayHello.proto
をプロジェクトに追加します。SayHello.protosyntax = "proto3"; option csharp_namespace = "Grpc.HelloWorld.Web"; message HelloRequest { string name = 1; } message HelloReply { string message = 1; } service Greeter { rpc SayHello(HelloRequest) returns (HelloReply) {} }プロジェクトファイルを開いて、Protobuf タグで上記ファイルを追加しておきます。
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Grpc.AspNetCore" Version="2.30.0" /> <PackageReference Include="Grpc.AspNetCore.Web" Version="2.30.0" /> </ItemGroup> <ItemGroup> <Protobuf Include="SayHello.proto" GrpcServices="Server" /> <!-- これ--> </ItemGroup> </Project>適当にサービスも実装しておきましょう。
using Grpc.Core; using System.Threading.Tasks; namespace Grpc.HelloWorld.Web { public class GreeterService : Greeter.GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}! Powered by ASP.NET Core gRPC.", }); } } }では
Startup.cs
でサービスの登録をします。
その際にConfigure
でUseGrpcWeb
を呼ぶのとMapGrpcService
でEnableGrpcWeb
を呼びます。using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Grpc.HelloWorld.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddGrpc(); // add } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseGrpcWeb(); // add app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb(); // add }); } } }サービスが沢山あって、全部で gRPC-Web を使いたい場合は
UseGrpcWeb
メソッドでデフォルトでオンにすることも出来ます。using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Grpc.HelloWorld.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddGrpc(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); // デフォルトでオンにする app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>(); }); } } }クライアントの実装
ブラウザーから呼ぶのが主目的なんでしょうが、私は gRPC を Azure WebApps にデプロイして .NET のクライアントから呼びたいのでコンソールアプリで作ります。コンソールアプリを作って以下のパッケージを追加します。
- Grpc.Net.Client
- Grpc.Net.ClientFactory
- Google.Protobuf
- Grpc.Net.Client.Web
- Grpc.Tools
そしてコンソールアプリのプロジェクトで接続済みサービスの追加で、サーバー側のプロジェクトに追加した SayHello.proto を追加します。
クライアント側はシンプルに呼び出すだけにしました。
using Grpc.Core; using Grpc.Net.Client; using Grpc.Net.Client.Web; using System; using System.Net.Http; using System.Threading.Tasks; namespace Grpc.HelloWorld.Web.Client { class Program { static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://localhost:44338", new GrpcChannelOptions { HttpHandler = new GrpcWebHandler(new HttpClientHandler()), }); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync(new HelloRequest { Name = "okazuki" }); Console.WriteLine(reply.Message); } } }サーバー側のプロジェクトを起動してコンソールアプリを起動すると以下のような結果になります。ここまでは OK
Azure にデプロイ
Azure で適当に WebApps を作ってデプロイします。
私の場合は kaotagrpcweb という名前で WebApps を作ったので以下のようにクライアント側のコードを書き替えました。using Grpc.Core; using Grpc.Net.Client; using Grpc.Net.Client.Web; using System; using System.Net.Http; using System.Threading.Tasks; namespace Grpc.HelloWorld.Web.Client { class Program { static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://kaotagrpcweb.azurewebsites.net/", new GrpcChannelOptions { HttpHandler = new GrpcWebHandler(new HttpClientHandler()), }); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync(new HelloRequest { Name = "okazuki" }); Console.WriteLine(reply.Message); } } }今度はサーバーは Azure にいるのでローカルのサーバーは立ち上げずにコンソールアプリだけを動かします。
あっさり動き過ぎて怖いですが動きましたね!!
制限事項
最初にリンクをはったドキュメントにも書いてありますが以下の制限があります。引用します。
gRPC-Web とストリーミング
従来の HTTP/2 による gRPC では、すべての方向でストリーミングがサポートされます。 gRPC-Web では、ストリーミングのサポートが制限されています。
- gRPC-Web ブラウザー クライアントでは、クライアント ストリーミング メソッドと双方向ストリーミング メソッドの呼び出しはサポートされていません。
- Azure App Service および IIS でホストされている ASP.NET Core gRPC サービスでは、双方向ストリーミングはサポートされていません。
gRPC-Web を使用するときは、単項メソッドとサーバー ストリーミング メソッドのみを使用することをお勧めします。
フル機能が使えるわけではないので、そこは注意が必要ですが簡単にタイプセーフな RPC サービスを作れるというメリットは享受できそうです。
ブラウザーから呼びたい場合の追加設定
ブラウザーから呼び出す場合は追加の構成が必要になります。といっても REST API を作ってる人たちにはおなじみの CORS の設定です。
試しにローカルで ASP.NET Core の Blazor WebAssembly を使って先ほどの Azure にデプロイしたサービスを呼び出してみようと思います。適当に ASP.NET Core プロジェクトでホストされる Blazor WebAssembly プロジェクトを作って参照の追加と proto ファイルをプロジェクトに追加して Index.razor を以下のようにしました。
@page "/" @using Grpc.Net.Client @using Grpc.Net.Client.Web @using Grpc.HelloWorld.Web <h1>Hello, world!</h1> <input type="text" @bind-value="Name" /> <br/> <button @onclick="InvokeButton_Click">Invoke</button> <br/> <span>@Message</span> @code { private string Name { get; set; } private string Message { get; set; } private async void InvokeButton_Click(MouseEventArgs e) { var channel = GrpcChannel.ForAddress("https://kaotagrpcweb.azurewebsites.net/", new GrpcChannelOptions { HttpHandler = new GrpcWebHandler(new HttpClientHandler()), }); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync(new HelloRequest { Name = Name }); Message = reply.Message; StateHasChanged(); } }このままローカル実行して Invoke ボタンを押しても以下のように真っ赤なエラーになります。わかりやすい CORS のエラーですね。
CORS の設定が必要なことも最初に紹介したドキュメントページに書いてあります。サービス側のプロジェクトで C# で CORS の設定をしてもいいですが Azure WebApps だとポータルからも出来るでのポータルからやってみましょう。
CORS の設定ページから以下のように入れるだけです。
保存して再度ローカルから呼び出してみると…
動きました。やったね!
まとめ
gRPC のフル機能が使えるわけではないですが gRPC-Web を使えば ASP.NET Core の gRPC のサービスを Azure の AppService にもデプロイして呼び出すことが出来ました。
これは個人的にはかなり嬉しいです。
- 投稿日:2020-07-26T23:14:41+09:00
シンプルなサウンドマネージャー (Unity)
前提
- Unity 2019.4.5f1
- 使用に際して、C#でスクリプトを書く必要があります。
- C#に触りたくない場合は、Boltでも大丈夫です。
- Bolt Visual Scripting 1.4.12 で確認済み
- UIの操作音など、音源の位置が画面に固定されている場合に適しています。
できること
- BGMとSEの番号を指定して再生できます。
- BGM
- 最大同時再生数は2で固定です。(クロスフェード用)
- あらかじめ、インスペクタで、フェードイン、フェードアウト、インターバルの時間を指定します。
- 再生はループします。
- SE
- あらかじめ、インスペクタで、最大同時再生数を指定します。
- 再生時に、「同じ音でも重ねて鳴らす」、「同じ音が鳴っていたら止めてから鳴らす」、「同じ音が鳴っていない場合だけ鳴らす」ことが選択可能です。
- 再生はループしません。
- BGMとSEの音量を独立して設定できます。
- 全体の一時的なミュートが可能です。
アセットの入手 (GitHub)
ダウンロード ⇒ SoundManager.unitypackage
ソースはこちらです。導入と設定
- プロジェクトにアセットをインポートしてください。
- シーンの適当なオブジェクトに、スクリプト
Sound.cs
をアタッチしてください。- インスペクタで、
Sound Effect Clip
とSound Music Clip
のSizeを必要なだけ増やし、オーディオクリップを設定してください。- 必要に応じてパラメータを調整してください。
項目 説明 初期値 Sound Effect Max SE同時再生数 5 Sound Effect Initial Volume SE初期音量 0.5 Sound Music Initial Volume BGM初期音量 0.5 Sound Music Fade In Time BGMフェードイン時間 0 Sound Music Fade Out Time BGMフェードアウト時間 3 Sound Music Interval Time BGMインターバル時間 (フェードアウト時間が0でなく負数なら重なる) 0 Sound Effect Clip SEオーディオクリップ - Sound Music Clip BGMオーディオクリップ - 使い方
SEを再生する
同じSEでも重ねて鳴らす
Sound.Effect = number;
- 空きのチャネル(AudioSource)で再生します。
- 空きがない場合は、最も古くに再生開始されたチャネルの再生を止めて使います。
同じSEが鳴っていたら止めてから鳴らす
Sound.StopAndEffect = number;
- 以下と同じ処理です。
Sound.EffectStop = number; Sound.Effect = number;同じSEが鳴っていない場合だけ鳴らす
Sound.EffectIfNot = number;
- 既に同じ音を再生中であれば、新たに再生しません。
最後に再生中の音番号を得る
var number = Sound.Effect;
- 何も再生されていない場合は、
Sound.Silent
が得られます。指定したSEを止める
Sound.EffectStop = number;
- 複数チャネルで再生している場合は、最も古い再生チャネルだけが止まります。
全てのSEを止める
Sound.Effect = Sound.Silent; // or Sound.EffectStop = Sound.Silent;SE音量を設定する
Sound.EffectVolume = volume;
- 正規化された値(0~1f)を設定します。取得もできます。
登録されているSE数を得る
var count = Sound.EffectCount;BGMを再生する
Sound.Music = number;
- インスペクタで設定されたフェードイン、フェードアウト、インターバルの時間(秒)を勘案して曲を再生します。
- 既に再生中の曲の場合は、単に再生を継続します。
- 別の曲を再生中の場合は、まず、再生中の曲がフェードアウトします。
- フェードアウト時間が0なら即座に止まります。
- 負値は指定できません。
- 次に、インターバル時間だけ、次の再生開始を待機します。
- 負値の場合は、フェードアウト中に遡って待機を終えます。
- 待機を終えると、指定された曲のフェードインを開始します。
- フェードイン時間が0なら即座に既定音量で再生されます。
- 負値は指定できません。
- 再生中でない場合は、即座にフェードインが開始されます。
- クロスフェード中に新たな再生指示があった場合は、以下の特例処理を行います。
- 前の(フェードアウト中の)曲が再生指示された場合は、2曲のフェード方向が切り替わります。
- フェードアウト・イン中のどちらとも異なる第3の曲が再生指示された場合は、再生中だった2曲の内で音量の大きい方をフェードアウトさせ、音量の小さい方は即座に停止して次の曲の開始シーケンスに移行します。
- 例
- 全ての時間が0なら、即座に切り替わります。
- 全ての時間が1なら、前の曲が1秒でフェードアウト、1秒無音で、次の曲が開始され1秒でフェードインします。
- フェードインとフェードアウトが3でインターバルが-1だと、前の曲が3秒でフェードアウトし、その終了1秒前に次の曲が再生を開始して3秒でフェードインします。フェードアウト開始からフェードイン終了までは5秒になります。
再生中の曲番号を得る
var number = Sound.Music;
- 再生されていない場合は、
Sound.Silent
が得られます。BGM音量を設定する
Sound.MusicVolume = 0.5f;
- 正規化された値(0~1f)を設定します。取得もできます。
登録されているBGM数を得る
var count = Sound.MusicCount;BGM再生を止める
Sound.Music = Sound.Silent;
- フェードアウト時間の設定が0でない場合は、フェードアウトします。
- フェードアウト時間が0の場合は即座に止まります。
一時的に全ての音を消す、戻す
Sound.Mute = true; // 一時的に音を消す Sound.Mute = false; // 音を戻す
- 音が出ないだけで、既存の再生は継続しますし、新たな再生も有効です。
以下の素材を使わせていただきました。
どうもありがとうございました。
- SoundEffects: ©効果音ラボ https://soundeffect-lab.info/
- Music: ©魔王魂 https://maoudamashii.jokersounds.com/
- 投稿日:2020-07-26T23:14:41+09:00
シンプルなサウンドマネージャー (unity)
前提
- Unity 2019.4.5f1
- 使用に際して、C#でスクリプトを書く必要があります。
- UIの操作音など、音源の位置が画面に固定されている場合に適しています。
できること
- BGMとSEの番号を指定して再生できます。
- BGM
- 最大同時再生数は2で固定です。(クロスフェード用)
- あらかじめ、インスペクタで、フェードイン、フェードアウト、インターバルの時間を指定します。
- 再生はループします。
- SE
- あらかじめ、インスペクタで、最大同時再生数を指定します。
- 再生時に、「同じ音でも重ねて鳴らす」、「同じ音が鳴っていたら止めてから鳴らす」、「同じ音が鳴っていない場合だけ鳴らす」ことが選択可能です。
- 再生はループしません。
- BGMとSEの音量を独立して設定できます。
- 全体の一時的なミュートが可能です。
アセットの入手 (GitHub)
ダウンロード ⇒ SoundManager.unitypackage
ソースはこちらです。使い方
準備
- プロジェクトにアセットをインポートしてください。
- シーンの適当なオブジェクトに、スクリプト
Sound.cs
をアタッチしてください。- インスペクタで、
Sound Effect Clip
とSound Music Clip
のSizeを必要なだけ増やし、オーディオクリップを設定してください。- 必要に応じてパラメータを調整してください。
項目 説明 初期値 Sound Effect Max SE同時再生数 5 Sound Effect Initial Volume SE初期音量 0.5 Sound Music Initial Volume BGM初期音量 0.5 Sound Music Fade In Time BGMフェードイン時間 0 Sound Music Fade Out Time BGMフェードアウト時間 3 Sound Music Interval Time BGMインターバル時間 (フェードアウト時間が 0
でなく負数なら重なる)0 Sound Effect Clip SEオーディオクリップ - Sound Music Clip BGMオーディオクリップ - SEを再生する
同じSEでも重ねて鳴らす
Sound.Effect = number;同じSEが鳴っていたら止めてから鳴らす
Sound.StopAndEffect = number;同じSEが鳴っていない場合だけ鳴らす
Sound.EffectIfNot = number;最後に再生中の音番号を得る
var number = Sound.Effect;指定したSEを止める
Sound.EffectStop = number;全てのSEを止める
Sound.Effect = Sound.Silent;SE音量を設定する
Sound.EffectVolume = volume;登録されているSE数を得る
var count = Sound.EffectCount;BGMを再生する
Sound.Music = number;再生中の曲番号を得る
var number = Sound.Music; // (Sound.Music == Sound.Silent) であれば何も再生していないBGM音量を設定する
Sound.MusicVolume = 0.5f;登録されているBGM数を得る
var count = Sound.MusicCount;BGM再生を止める
Sound.Music = Sound.Silent;一時的に全ての音を消す、戻す
Sound.Mute = true; // 一時的に音を消す Sound.Mute = false; // 音を戻す
以下の素材を使わせていただきました。
どうもありがとうございました。
- SoundEffects: ©効果音ラボ https://soundeffect-lab.info/
- Music: ©魔王魂 https://maoudamashii.jokersounds.com/
- 投稿日:2020-07-26T21:57:19+09:00
Actionメソッドと画面遷移
ASP.NETでの画面遷移
この記事ではわかりやすさなどは追求せず、備忘録的に内容をまとめていきます。
元画面に戻す場合は?
自分が分からなくなっていたのは元画面に戻る処理の実装方法です。
例えばショッピングサイトでアカウントを作成するとき、
アカウント情報を入力画面に記入する。
送信ボタンをクリック!
↓
問題がなければ次の画面に遷移します
だけど、あれ?入力内容ちょっとまずいじゃん!いろいろ必須事項漏れてるし、使用禁止文字使ってるし、、、
よしっ、元の入力画面に戻しちゃえ!ついでにどこがまずいかのメッセージも表示しちゃえ!て場合です。
例えばアカウント作成のときにこんな画面の遷移をします。では、画面の間を繋ぐコントローラーではどう実装すべきでしょうか?特に元の画面に戻す場合は何をリターンするのが正解でしょうか?
if(/*エラーがある場合*/) { /*何か処理*/ /*戻り値は何?*/ }else/*入力にエラーがないとき*/ { /*何か処理*/ /*戻り値は何?*/ }結論 Viewメソッドを使え!
結論から言えばViewメソッドを使うが正解です。とりあえず画面遷移をさせたいならViewメソッドです!← これは言い過ぎ?
わからなかった原因
しかし最初、Viewメソッドを使うことがわかりませんでした。
その原因はViewメソッドはそのメソッド名と同じ名前のViewに遷移させるものと思っていたからです。そして、View以外(RedirectToActionとか?)の場合はモデルに付与した検証用のアノテーションが意味をなさなくなります。Indexメソッド内でView→Index.cshtml
みたいな感じ
Viewメソッドは返すView名を指定できる
ViewメソッドはオーバーロードでView名を指定するオーバーロードも用意されています。
なので、if(/*エラーがある場合*/) { /*何か処理*/ return View(“前画面”,xxx); }else/*入力にエラーがないとき*/ { /*何か処理*/ return View(“次画面”,yyy); }xxxとかyyyにはViewに渡したいモデルのインスタンスが入ります。
なので、画面遷移でViewメソッドを使ってどのViewにも返すことができました。
Viewでも検証用の属性を使えます。
- 投稿日:2020-07-26T21:33:15+09:00
[C#] 双方向辞書を自作する
前書き
Dictionary<TKey, TValue>
は,Key
からValue
への単方向辞書である.例えば図鑑番号をKey
,種族名をValue
としたポ〇モン図鑑Dictionary<int, string>
では,Key = 1
に対して"不思議種"というデータを$O(1)$操作で取得可能あるが,逆に種族名から図鑑番号を探索したいときは,$O(n)$操作の線形探索を行うしかない.今回は,Key
からValue
へのマッピングと同時に,その逆のValue
からKey
へのマッピングも保証する双方向辞書LinkedDictionary<TKey, TValue>
の作成を考える.使用例
- 以下は,双方向辞書
LinkedDictionary<int, string>
の使用例である.Key<int>
からValue<string>
へのアクセスと,Value<string>
からKey<int>
へのアクセスを$O(1)$操作で実行可能である.var pokomonLibrary = new LinkedDictionary<int, string>(); pokomonLibrary.Add(0, "ィ゛ゃゾ┛A"); pokomonLibrary.Add(6, "アネ゙デパミ"); var pokomon1 = pokomonLibrary[0]; // "ィ゛ゃゾ┛A" var pokomon2 = pokomonLibrary["ィ゛ゃゾ┛A"]; // 0 var pokomon3 = pokomonLibrary[6]; // "アネ゙デパミ" var pokomon4 = pokomonLibrary["アネ゙デパミ"]; // 6実装
クラスのの全容は GitHub へ公開した.
https://github.com/Takuto168/Takuto168park/blob/master/LinkedDictionary.cs以下に,その機能一覧と実装方法を示す.
クラスの作成
- ジェネリッククラス
LinkedDictionary<TKey, TValue>
を作成し,IDictionary<TKey, TValue>
インターフェースを継承する.- キーから値へのマッピングを保証するための
Dictionary<TKey, TValue>
フィールドと,値からキーへのマッピングを保証するためのDictionary<TValue, TKey>
フィールドを用意する.以降,これらを内部辞書と呼ぶ.LinkedDictionary.cs/// <summary> /// 双方向辞書 /// キーから値へのマッピングと,その逆の値からキーへのマッピングを同時に保証するキーと値のコレクションを表します. /// </summary> /// <typeparam name="TKey">ディクショナリ内のキーの型.</typeparam> /// <typeparam name="TValue">ディクショナリ内の値の型.</typeparam> public class LinkedDictionary<TKey, TValue> : IDictionary<TKey, TValue> { /// <summary> /// キーから値へのマッピング. /// </summary> private Dictionary<TKey, TValue> _keyToValues; /// <summary> /// 値からキーへのマッピング. /// </summary> private Dictionary<TValue, TKey> _valueToKeys; }コンストラクタ
Dictionary<TKey, TValue>
のコンストラクタに倣い,下記の条件を指定できる幾つかのパターンのコンストラクタを定義する.
- 空の
LinkedDictionary<TKey, TValue>
を作成- 容量
Capacity
を明示的に指定して作成- キーの型の既定の等値比較子
IEqualityComparer<TKey>
を指定して作成- 値の型の既定の等値比較子
IEqualityComparer<TValue>
を指定して作成IDictionary<TKey,TValue>
から要素をコピーして作成IEnumerable<KeyValuePair<TKey, TValue>>
から要素をコピーして作成#region->constructer/// <summary> /// 空で,既定の初期量を備え,キーの型と値型の既定の等値比較子を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> public LinkedDictionary() : this(0, null, null) { } /// <summary> /// 空で,指定した初期量を備え,キーの型と値型の既定の等値比較子を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="capacity">LinkedDictionary<TKey,TValue> が格納できる要素数の初期値.</param> public LinkedDictionary(int capacity) : this(capacity, null, null) { } /// <summary> /// 空で,既定の初期量を備え,指定した IEqualityComparer<TKey> を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="comparerKey">キーの比較時に使用する IEqualityComparer<TKey> 実装.キーの型の既定の EqualityComparer<TKey> を使用する場合は null.</param> public LinkedDictionary(IEqualityComparer<TKey> comparerKey) : this(0, comparerKey, null) { } /// <summary> /// 空で,既定の初期量を備え,指定した IEqualityComparer<TValue> を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="comparerValue">値の比較時に使用する IEqualityComparer<TValue> 実装.値の型の既定の EqualityComparer<TValue> を使用する場合は null.</param> public LinkedDictionary(IEqualityComparer<TValue> comparerValue) : this(0, null, comparerValue) { } /// <summary> /// 空で,既定の初期量を備え,指定した IEqualityComparer<TKey> と IEqualityComparer<TValue> を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="comparerKey">キーの比較時に使用する IEqualityComparer<TKey> 実装.キーの型の既定の EqualityComparer<TKey> を使用する場合は null.</param> /// <param name="comparerValue">値の比較時に使用する IEqualityComparer<TValue> 実装.値の型の既定の EqualityComparer<TValue> を使用する場合は null.</param> public LinkedDictionary(IEqualityComparer<TKey> comparerKey, IEqualityComparer<TValue> comparerValue) : this(0, comparerKey, comparerValue) { } /// <summary> /// 空で,指定したの初期量を備え,指定した IEqualityComparer<TKey> と IEqualityComparer<TValue> を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="capacity">LinkedDictionary<TKey,TValue> が格納できる要素数の初期値.</param> /// <param name="comparerKey">キーの比較時に使用する IEqualityComparer<TKey> 実装.キーの型の既定の EqualityComparer<TKey> を使用する場合は null.</param> /// <param name="comparerValue">値の比較時に使用する IEqualityComparer<TValue> 実装.値の型の既定の EqualityComparer<TValue> を使用する場合は null.</param> public LinkedDictionary(int capacity, IEqualityComparer<TKey> comparerKey, IEqualityComparer<TValue> comparerValue) { if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity)); this._keyToValues = new Dictionary<TKey, TValue>(capacity, comparerKey); this._valueToKeys = new Dictionary<TValue, TKey>(capacity, comparerValue); } /// <summary> /// 指定した IDictionary<TKey,TValue> から要素をコピーして格納し,キー型と値型の既定の等値比較子を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="dictionary">新しい LinkedDictionary<TKey,TValue> に要素をコピーする IDictionary<TKey,TValue>.</param> public LinkedDictionary(IDictionary<TKey, TValue> dictionary) : this(dictionary, null, null) { } /// <summary> /// 指定した IDictionary<TKey,TValue> から要素をコピーして格納し,指定した IEqualityComparer<TKey> を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="dictionary">新しい LinkedDictionary<TKey,TValue> に要素をコピーする IDictionary<TKey,TValue>.</param> /// <param name="comparerKey">キーの比較時に使用する IEqualityComparer<TKey> 実装.キーの型の既定の EqualityComparer<TKey> を使用する場合は null.</param> public LinkedDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparerKey) : this(dictionary, comparerKey, null) { } /// <summary> /// 指定した IDictionary<TKey,TValue> から要素をコピーして格納し,指定した IEqualityComparer<TValue> を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="dictionary">新しい LinkedDictionary<TKey,TValue> に要素をコピーする IDictionary<TKey,TValue>.</param> /// <param name="comparerValue">値の比較時に使用する IEqualityComparer<TValue> 実装.値の型の既定の EqualityComparer<TValue> を使用する場合は null.</param> public LinkedDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TValue> comparerValue) : this(dictionary, null, comparerValue) { } /// <summary> /// 指定した IDictionary<TKey,TValue> から要素をコピーして格納し,指定した IEqualityComparer<TKey> と IEqualityComparer<TValue> を使用する,LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="dictionary">新しい LinkedDictionary<TKey,TValue> に要素をコピーする IDictionary<TKey,TValue>.</param> /// <param name="comparerKey">キーの比較時に使用する IEqualityComparer<TKey> 実装.キーの型の既定の EqualityComparer<TKey> を使用する場合は null.</param> /// <param name="comparerValue">値の比較時に使用する IEqualityComparer<TValue> 実装.値の型の既定の EqualityComparer<TValue> を使用する場合は null.</param> public LinkedDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparerKey, IEqualityComparer<TValue> comparerValue) : this(dictionary != null ? dictionary.Count : 0, comparerKey, comparerValue) { if (dictionary == null) throw new ArgumentNullException(nameof(dictionary)); this.AddRange(dictionary); } /// <summary> /// 指定した IEnumerable<T> からコピーされた要素を格納する LinkedDictionary<TKey,TValue> クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="collection">新しい LinkedDictionary<TKey,TValue> に要素をコピーする Enumerable<KeyValuePair<TKey,TValue>>.</param> public LinkedDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection) : this(collection, null, null) { } /// <summary> /// 指定した IEnumerable<KeyValuePair<TKey,TValue>> から要素をコピーして格納し,指定した IEqualityComparer<TKey> を使用する,LinkedDictionary<TKey, TValue>クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="collection">新しい LinkedDictionary<TKey,TValue> に要素をコピーする Enumerable<KeyValuePair<TKey,TValue>>.</param> /// <param name="comparerKey">キーの比較時に使用する IEqualityComparer<TKey> 実装.キーの型の既定の EqualityComparer<TKey> を使用する場合は null.</param> public LinkedDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparerKey) : this(collection, comparerKey, null) { } /// <summary> /// 指定した IEnumerable<KeyValuePair<TKey,TValue>> から要素をコピーして格納し,指定した IEqualityComparer<TValue> を使用する,LinkedDictionary<TKey, TValue>クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="collection">新しい LinkedDictionary<TKey,TValue> に要素をコピーする Enumerable<KeyValuePair<TKey,TValue>>.</param> /// <param name="comparerValue">値の比較時に使用する IEqualityComparer<TValue> 実装.値の型の既定の EqualityComparer<TValue> を使用する場合は null.</param> public LinkedDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TValue> comparerValue) : this(collection, null, comparerValue) { } /// <summary> /// 指定した IEnumerable<KeyValuePair<TKey,TValue>> から要素をコピーして格納し,指定した IEqualityComparer<TKey> と IEqualityComparer<TValue> を使用する,LinkedDictionary<TKey, TValue>クラスの新しいインスタンスを初期化します. /// </summary> /// <param name="collection">新しい LinkedDictionary<TKey,TValue> に要素をコピーする Enumerable<KeyValuePair<TKey,TValue>>.</param> /// <param name="comparerKey">キーの比較時に使用する IEqualityComparer<TKey> 実装.キーの型の既定の EqualityComparer<TKey> を使用する場合は null.</param> /// <param name="comparerValue">値の比較時に使用する IEqualityComparer<TValue> 実装.値の型の既定の EqualityComparer<TValue> を使用する場合は null.</param> public LinkedDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparerKey, IEqualityComparer<TValue> comparerValue) : this(collection != null ? collection.Count() : 0, comparerKey, comparerValue) { if (collection == null) throw new ArgumentNullException(nameof(collection)); this.AddRange(collection); }プロパティ
- 明示的なインターフェースの実装では,基本的に内部辞書のプロパティを参照する.
- 通常の
TValue Item[TKey key]
プロパティに加え,逆探知を行うためのTKey Item[TValue value]
プロパティを用意する.ここで,set
のときは,一方の内部辞書のValue
を書き換えるとともに,他方のKey
を書き換える必要があることに留意する.#region->property/// <summary> /// ディクショナリのキーが等しいかどうかを確認するために使用する IEqualityComparer<TKey> を取得します. /// </summary> public IEqualityComparer<TKey> ComparerKey => this._keyToValues.Comparer; /// <summary> /// ディクショナリの値が等しいかどうかを確認するために使用する IEqualityComparer<TValue> を取得します. /// </summary> public IEqualityComparer<TValue> ComparerValue => this._valueToKeys.Comparer; /// <summary> /// LinkedDictionary<TKey,TValue> に格納されているキー/値ペアの数を取得します. /// </summary> public int Count => this._keyToValues.Count; /// <summary> /// 指定されたキーに関連付けられた値を取得または設定します. /// </summary> /// <param name="key">取得または設定する値のキー.</param> public TValue this[TKey key] { get => this.GetValue(key); set => this.SetValue(key, value); } /// <summary> /// 指定された値に関連付けられたキーを取得または設定します. /// </summary> /// <param name="argValue">取得または設定するキーの値.</param> public TKey this[TValue argValue] { get => this.GetKey(argValue); set => this.SetKey(argValue, value); } /// <summary> /// LinkedDictionary<TKey,TValue> 内のキーを格納しているコレクションを取得します. /// </summary> public ICollection<TKey> Keys => this._keyToValues.Keys; /// <summary> /// LinkedDictionary<TKey,TValue> 内の値を格納しているコレクションを取得します. /// </summary> public ICollection<TValue> Values => this._keyToValues.Values; /// <summary> /// IDictionary が読み取り専用かどうかを示す値を取得します. /// </summary> public bool IsReadOnly => false; /// <summary> /// LinkedDictionary<TKey,TValue> 内のキー/値ペアのコレクションを取得します. /// </summary> public IEnumerable<KeyValuePair<TKey, TValue>> KeyValuePairs { get { foreach (var item in this._keyToValues) yield return item; } }メソッド
- 明示的なインターフェースの実装を含めた
Dictionary<TKey,TValue>
の実装機能に加え,いくつかの独自メソッドを作成した.- 独自メソッドの考案に際しては,Qiitaの記事よりDictionaryの拡張メソッド 36選を参考にさせて頂いた.
キー,値の取得
- キーと値の取得に関するいくつかのメソッドを定義する.
#region->method->get/// <summary> /// 指定されたキーに関連付けられた値を取得します. /// </summary> /// <param name="key">取得または設定する値のキー.</param> public TValue GetValue(TKey key) { if (this.TryGetValue(key, out var value)) return value; throw new KeyNotFoundException(); } /// <summary> /// 指定された値に関連付けられたキーを取得します. /// </summary> /// <param name="value">取得または設定するキーの値.</param> public TKey GetKey(TValue value) { if (this.TryGetKey(value, out var key)) return key; throw new KeyNotFoundException(); } /// <summary> /// 指定されたキーに関連付けられた値の取得を試みます. /// </summary> /// <param name="key">取得する値のキー.</param> /// <param name="value">キーが見つかった場合は,指定したキーに関連付けられている値が格納されます.それ以外の場合は value パラメーターの型に対する既定の値.</param> /// <returns>指定されたキーを持つ要素が LinkedDictionary<TKey,TValue> に含まれている場合は true,含まれていない場合は false.</returns> public bool TryGetValue(TKey key, out TValue value) => this._keyToValues.TryGetValue(key, out value); /// <summary> /// 指定された値に関連付けられたキーの取得をを試みます. /// </summary> /// <param name="value">取得するキーの値.</param> /// <param name="key">値が見つかった場合は,指定した値に関連付けられているキーが格納されます.それ以外の場合は key パラメーターの型に対する既定の値.</param> /// <returns>指定された値を持つ要素が LinkedDictionary<TKey,TValue> に含まれている場合は true,含まれていない場合は false.</returns> public bool TryGetKey(TValue value, out TKey key) => this._valueToKeys.TryGetValue(value, out key); /// <summary> /// 指定されたキーに関連付けられた値を取得します. /// </summary> /// <param name="key">取得する値のキー.</param> /// <returns>値が見つかった場合は,指定した値に関連付けられているキー.それ以外の場合は value パラメーターの型に対する既定の値.</returns> public TValue GetValueOrDefault(TKey key) => this.TryGetValue(key, out var value) ? value : default(TValue); /// <summary> /// 指定された値に関連付けられたキーを取得します. /// </summary> /// <param name="value">取得するキーの値.</param> /// <returns>キーが見つかった場合は,指定した値に関連付けられている値.それ以外の場合は key パラメーターの型に対する既定の値.</returns> public TKey GetKeyOrDefault(TValue value) => this.TryGetKey(value, out var key) ? key : default(TKey); /// <summary> /// 指定されたキーに関連付けられた値を取得し,値が見つからなかった場合は指定した value を追加します. /// </summary> /// <param name="key">取得する値のキー.</param> /// <param name="value">値が見つからなかった場合に追加する値.</param> /// <returns>値が見つかった場合は指定されたキーに関連付けられた値.見つからなかった場合は追加した値.</returns> public TValue GetValueOrAdd(TKey key, TValue value) { this.TryAdd(key, value); return this._keyToValues[key]; } /// <summary> /// 指定された値に関連付けられたキーを取得し,キーが見つからなかった場合は指定した key を追加します. /// </summary> /// <param name="value">取得するキーの値.</param> /// <param name="key">キーが見つからなかった場合に追加するキー.</param> /// <returns>キーが見つかった場合は指定された値に関連付けられたキー.見つからなかった場合は追加したキー.</returns> public TKey GetKeyOrAdd(TValue value, TKey key) { this.TryAdd(key, value); return this._valueToKeys[value]; } /// <summary> /// 指定されたキーに関連付けられた値を取得し,値が見つからなかった場合は指定した TValue 型に対する既定値を追加します. /// </summary> /// <param name="key">取得する値のキー.</param> /// <returns>値が見つかった場合は指定されたキーに関連付けられた値.見つからなかった場合は TValue 型の既定値.</returns> public TValue GetValueOrAddDefault(TKey key) => this.GetValueOrAdd(key, default(TValue)); /// <summary> /// 指定された値に関連付けられたキーを取得し,キーが見つからなかった場合は指定した TKey 型に対する既定値を追加します. /// </summary> /// <param name="value">取得するキーの値.</param> /// <returns>キーが見つかった場合は指定された値に関連付けられたキー.見つからなかった場合は TKey 型の既定値.</returns> public TKey GetKeyOrAddDefault(TValue value) => this.GetKeyOrAdd(value, default(TKey));キー,値の設定
- キーと値の設定に関するいくつかのメソッドを定義する.
- キーと値の設定では,一方の内部辞書の
Value
を書き換えるとともに,他方のKey
を書き換える必要があることに留意する.#region->method->set/// <summary> /// 指定されたキーに関連付けられた値を設定します. /// </summary> /// <param name="key">設定する値のキー.</param> /// <param name="value">設定する値.</param> public void SetValue(TKey key, TValue value) { if (!this.TrySetValue(key, value)) throw new KeyNotFoundException(); } /// <summary> /// 指定された値に関連付けられたキーを設定します. /// </summary> /// <param name="value">設定するキーの値.</param> /// <param name="key">設定するキー.</param> public void SetKey(TValue value, TKey key) { if (!this.TrySetKey(value, key)) throw new KeyNotFoundException(); } /// <summary> /// 指定されたキーに関連付けられた値の設定を試みます. /// </summary> /// <param name="key">設定する値のキー.</param> /// <param name="value">設定する値.</param> /// <returns>指定されたキーを持つ要素が LinkedDictionary<TKey,TValue> に含まれている場合は true,含まれていない場合は false.</returns> public bool TrySetValue(TKey key, TValue value) { if (this._keyToValues.ContainsKey(key)) { var currentValue = this._keyToValues[key]; this._keyToValues[key] = value; this._valueToKeys.Remove(currentValue); this._valueToKeys.Add(value, key); return true; } else return false; } /// <summary> /// 指定された値に関連付けられたキーの設定を試みます. /// </summary> /// <param name="value">設定するキーの値.</param> /// <param name="key">設定するキー.</param> /// <returns>指定された値を持つ要素が LinkedDictionary<TKey,TValue> に含まれている場合は true,含まれていない場合は false.</returns> public bool TrySetKey(TValue value, TKey key) { if (this._valueToKeys.ContainsKey(value)) { var currentKey = this._valueToKeys[value]; this._valueToKeys[value] = key; this._keyToValues.Remove(currentKey); this._keyToValues.Add(key, value); return true; } else return false; } /// <summary> /// 指定されたキーに関連付けられた値を設定または追加します. /// </summary> /// <param name="key">設定する値のキー.</param> /// <param name="value">設定する値.</param> public void SetValueOrAdd(TKey key, TValue value) { if (!this.TrySetValue(key, value)) this.Add(key, value); } /// <summary> /// 指定された値に関連付けられたキーを設定または追加します. /// </summary> /// <param name="key">設定するキーの値.</param> /// <param name="value">設定するキー.</param> public void SetKeyOrAdd(TValue value, TKey key) { if (!this.TrySetKey(value, key)) this.Add(key, value); } /// <summary> /// 指定されたキーに対して, TValue 型の既定値を設定または追加します. /// </summary> /// <param name="key">設定する値のキー.</param> public void SetValueOrAddDefault(TKey key) => this.SetValueOrAdd(key, default(TValue)); /// <summary> /// 指定された値に対して, TKey 型の既定値を設定または追加します. /// </summary> /// <param name="value">設定する値のキー.</param> public void SeKeyOrAddDefault(TValue value) => this.SetValueOrAdd(default(TKey), value);要素の追加
- 要素の追加に関するいくつかのメソッドを定義する.
- 通常の
Dictionary<TKey,TValue>
では,追加するキーがnull
である場合に例外を発生させるが,LinkedDictionary<TKey,TValue>
では,キーと値のいずれかがnull
である場合に例外を発生させる.- 通常の
Dictionary<TKey,TValue>
では,追加するキーの重複によって例外を発生させるが,LinkedDictionary<TKey,TValue>
では,キーと値それぞれの重複によって例外を発生させる.- コレクションの追加を行う
AddRange
では,コレクション内の何れかのキーまたは値が重複した場合に例外を発生させる一方,TryAddRange
ではキーまたは値が重複しないもののみ追加を行う.#region->method->add/// <summary> /// 指定されたキーと値を LinkedDictionary<TKey,TValue> に追加します. /// </summary> /// <param name="key">追加する要素のキー.</param> /// <param name="value">追加する要素の値.</param> public void Add(TKey key, TValue value) { if (!this.TryAdd(key, value)) throw new ArgumentException(); } /// <summary> /// 指定された KeyValuePair<TKey, TValue> を LinkedDictionary<TKey,TValue> に追加します. /// </summary> /// <param name="item">追加する KeyValuePair<TKey, TValue>.</param> public void Add(KeyValuePair<TKey, TValue> item) => this.Add(item.Key, item.Value); /// <summary> /// 指定された Tuple<TKey, TValue> を LinkedDictionary<TKey,TValue> に追加します. /// </summary> /// <param name="item">追加する Tuple<TKey, TValue>.</param> public void Add((TKey Key, TValue Value) item) => this.Add(item.Key, item.Value); /// <summary> /// LinkedDictionary<TKey,TValue> に対して,指定されたキーと値の追加を試みます. /// </summary> /// <param name="key">追加する要素のキー.</param> /// <param name="value">追加する要素の値.</param> /// <returns>キー/値ペアが LinkedDictionary<TKey,TValue> に追加された場合は true,それ以外の場合は false.</returns> public bool TryAdd(TKey key, TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); if (value == null) throw new ArgumentNullException(nameof(value)); if (this._keyToValues.ContainsKey(key) || this._valueToKeys.ContainsKey(value)) return false; this._keyToValues.Add(key, value); this._valueToKeys.Add(value, key); return true; } /// <summary> /// LinkedDictionary<TKey,TValue> に対して,指定された KeyValuePair<TKey, TValue> の追加を試みます. /// </summary> /// <param name="item">追加する KeyValuePair<TKey, TValue>.</param> /// <returns></returns> public bool TryAdd(KeyValuePair<TKey, TValue> item) => this.TryAdd(item.Key, item.Value); /// <summary> /// LinkedDictionary<TKey,TValue> に対して,指定された Tuple<TKey, TValue> の追加を試みます. /// </summary> /// <param name="item">追加する Tuple<TKey, TValue>.</param> /// <returns></returns> public bool TryAdd((TKey Key, TValue Value) item) => this.TryAdd(item.Key, item.Value); /// <summary> /// 指定した KeyValuePair<TKey, TValue> のコレクションを追加します. /// </summary> /// <param name="collection">追加する KeyValuePair<TKey, TValue> のコレクション.</param> public void AddRange(IEnumerable<KeyValuePair<TKey, TValue>> collection) { foreach (var item in collection) this.Add(item); } /// <summary> /// 指定した KeyValuePair<TKey, TValue> のコレクションのうち,キーと値が重複しないもののみを追加します. /// </summary> /// <param name="collection">追加する KeyValuePair<TKey, TValue> のコレクション.</param> public void TryAddRange(IEnumerable<KeyValuePair<TKey, TValue>> collection) { foreach (var item in collection) this.TryAdd(item); }要素の削除
- 要素の削除に関するいくつかのメソッドを定義する.
- 要素の削除では,双方の内部辞書から削除を行うことに留意する.
#region->method->remove/// <summary> /// LinkedDictionary<TKey,TValue> からすべてのキーと値を削除します. /// </summary> public void Clear() { this._keyToValues.Clear(); this._valueToKeys.Clear(); } /// <summary> /// 指定したキーを持つ値を LinkedDictionary<TKey,TValue> から削除します. /// </summary> /// <param name="key">削除する要素のキー.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool Remove(TKey key) => this.RemoveByKey(key); /// <summary> /// 指定したキーを持つ値を LinkedDictionary<TKey,TValue> から削除します. /// </summary> /// <param name="key">削除する要素のキー.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool RemoveByKey(TKey key) => this.RemoveByKey(key, out var value); /// <summary> /// 指定した値を持つキーを LinkedDictionary<TKey,TValue> から削除します. /// </summary> /// <param name="value">削除する要素の値.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool Remove(TValue value) => this.RemoveByValue(value); /// <summary> /// 指定した値を持つキーを LinkedDictionary<TKey,TValue> から削除します. /// </summary> /// <param name="value">削除する要素の値.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool RemoveByValue(TValue value) => this.RemoveByValue(value, out var key); /// <summary> /// 指定されたキーを持つ値を LinkedDictionary<TKey,TValue> から削除し,その要素の値を value パラメーターにコピーします. /// </summary> /// <param name="key">削除する要素のキー.</param> /// <param name="value">削除された要素の値.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool Remove(TKey key, out TValue value) => this.RemoveByKey(key, out value); /// <summary> /// 指定されたキーを持つ値を LinkedDictionary<TKey,TValue> から削除し,その要素の値を value パラメーターにコピーします. /// </summary> /// <param name="key">削除する要素のキー.</param> /// <param name="value">削除された要素の値.</param> public bool RemoveByKey(TKey key, out TValue value) { if (this._keyToValues.ContainsKey(key) && this._valueToKeys.ContainsKey(this._keyToValues[key])) { value = this._keyToValues[key]; this._keyToValues.Remove(key); this._valueToKeys.Remove(value); return true; } else { value = default(TValue); return false; } } /// <summary> /// 指定された値を持つキーを LinkedDictionary<TKey,TValue> から削除し,その要素のキーを key パラメーターにコピーします. /// </summary> /// <param name="value">削除する要素の値.</param> /// <param name="key">削除する要素のキー.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool Remove(TValue value, out TKey key) => this.RemoveByValue(value, out key); /// <summary> /// 指定された値を持つキーを LinkedDictionary<TKey,TValue> から削除し,その要素のキーを key パラメーターにコピーします. /// </summary> /// <param name="value">削除する要素の値.</param> /// <param name="key">削除する要素のキー.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool RemoveByValue(TValue value, out TKey key) { if (this._valueToKeys.ContainsKey(value) && this._keyToValues.ContainsKey(this._valueToKeys[value])) { key = this._valueToKeys[value]; this._valueToKeys.Remove(value); this._keyToValues.Remove(key); return true; } else { key = default(TKey); return false; } } /// <summary> /// 指定された KeyValuePair<TKey, TValue> を LinkedDictionary<TKey,TValue> から削除します. /// </summary> /// <param name="item">削除する KeyValuePair<TKey, TValue>.</param> /// <returns>要素が見つかり削除された場合は true.それ以外の場合は false.</returns> public bool Remove(KeyValuePair<TKey, TValue> item) => this._keyToValues.ContainsKey(item.Key) && this._keyToValues[item.Key].Equals(item.Value) && this.Remove(item.Key);判定
- 要素の判定に関するいくつかのメソッドを定義する.
- 実装は,内部辞書の処理に準じる.
#region->method->determinate/// <summary> /// 指定したキーを持つ要素が LinkedDictionary<TKey,TValue> に含まれるかどうかを判断します. /// </summary> /// <param name="key">検索するキー.</param> /// <returns>指定されたキーを持つ要素が LinkedDictionary<TKey,TValue> に含まれている場合は true,含まれていない場合は false.</returns> public bool ContainsKey(TKey key) => this._keyToValues.ContainsKey(key); /// <summary> /// 指定した値を持つ要素が LinkedDictionary<TKey,TValue> に含まれるかどうかを判断します. /// </summary> /// <param name="value">検索する値.</param> /// <returns>指定された値を持つ要素が LinkedDictionary<TKey,TValue> に含まれている場合は true,含まれていない場合は false.</returns> public bool ContainsValue(TValue value) => this._valueToKeys.ContainsKey(value); /// <summary> /// 指定した KeyValuePair<TKey, TValue> が LinkedDictionary<TKey,TValue> に含まれるかどうかを判断します. /// </summary> /// <param name="item">検索する KeyValuePair<TKey, TValue></param> /// <returns></returns> public bool Contains(KeyValuePair<TKey, TValue> item) => this.KeyValuePairs.Contains(item);反復処理
- 反復処理を行うための
IEnumerator
メソッドを定義する.- 実装は,
Key
からValue
へのマッピングを示す`内部辞書の処理に準じる.#region->method->IEnumerator/// <summary> /// コレクションを反復処理する列挙子を返します. /// </summary> /// <returns>コレクションの繰り返し処理に使用できる列挙子.</returns> public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => this._keyToValues.GetEnumerator(); /// <summary> /// コレクションを反復処理する列挙子を返します. /// </summary> /// <returns>コレクションの反復処理に使用できる IEnumerator.</returns> IEnumerator IEnumerable.GetEnumerator() => this._keyToValues.GetEnumerator();コピー,変換
- コピーと変換を行うためのいくつかのメソッドを定義する.
SelectValue
とSelectKey
は,双方向辞書内のValue
またはKey
に対して変換を行い新たな双方向辞書を生成する拡張メソッドからの応用.#region->method->copy&convert/// <summary> /// 指定した配列インデックスを開始位置として,配列に ICollection<T> の要素をコピーします. /// </summary> /// <param name="array">ICollection<T> から要素がコピーされる 1 次元の配列. </param> /// <param name="index">array 内のコピーの開始位置を示すインデックス.</param> public void CopyTo(KeyValuePair<TKey, TValue>[] array, int index) { if (array == null) throw new ArgumentNullException(nameof(array)); if (index < 0 || index > array.Length) throw new ArgumentOutOfRangeException(nameof(index)); if (array.Length - index < this.Count) throw new ArgumentException(); foreach (var item in this._keyToValues) { array[index++] = new KeyValuePair<TKey, TValue>(item.Key, item.Value); } } /// <summary> /// シーケンスの各要素の値を Func<TValue, TValueResult> によって新しいフォームに射影し,既存のキーと新しい値による LinkedDictionary<TKey, TValueResult> を生成します. /// </summary> /// <typeparam name="TValueResult">変換後の要素の値の型.</typeparam> /// <param name="valueSelector">要素の値の変換を表すデリゲート.</param> /// <returns>生成された LinkedDictionary<TKey, TValueResult>.</returns> public LinkedDictionary<TKey, TValueResult> SelectValue<TValueResult>(Func<TValue, TValueResult> valueSelector) => new LinkedDictionary<TKey, TValueResult>(this.KeyValuePairs.Select(item => new KeyValuePair<TKey, TValueResult>(item.Key, valueSelector(item.Value)))); /// <summary> /// シーケンスの各要素の値を Func<TKey, TKeyResult> によって新しいフォームに射影し,新しいキーと既存の値による LinkedDictionary<TKeyResult, TValue> を生成します. /// </summary> /// <typeparam name="TKeyResult">変換後の要素のキーの型.</typeparam> /// <param name="keySelector">要素のキーの変換を表すデリゲート.</param> /// <returns>生成された LinkedDictionary<TKey, TValueResult>.</returns> public LinkedDictionary<TKeyResult, TValue> SelectKey<TKeyResult>(Func<TKey, TKeyResult> keySelector) => new LinkedDictionary<TKeyResult, TValue>(this.KeyValuePairs.Select(item => new KeyValuePair<TKeyResult, TValue>(keySelector(item.Key), item.Value)));ソート
TKey
とTValue
による既定のソートを行うメソッドを定義する.- 加えて,独自の変換デリゲートでもソートができるようにした.
#region->method->sort///// <summary> ///// 要素のキーの既定の比較子を使用して,LinkedDictionary<TKey,TValue> 全体内の要素をそのキーによって並べ替えます. ///// </summary> public void SortByKey() => this.Sort(item => item.Key); ///// <summary> ///// 要素の値の既定の比較子を使用して,LinkedDictionary<TKey,TValue> 全体内の要素をその値によって並べ替えます. ///// </summary> public void SortByValue() => this.Sort(item => item.Value); ///// <summary> ///// 指定した変換デリゲートを使用して,LinkedDictionary<TKey,TValue> 全体内の要素をその値によって並べ替えます. ///// </summary> public void Sort<T>(Func<KeyValuePair<TKey, TValue>, T> Selecter) { var collection = this.KeyValuePairs.OrderBy(Selecter).ToArray(); this.Clear(); this.AddRange(collection); }課題
TKey
とTValue
が同型であるとき,Item[TKey]
とItem[TValue]
プロパティや,Remove(TKey)
とRemove(TValue)
メソッド等で名前の競合が発生する.- 今回はそれぞれ別名のプロパティやメソッドを用意して回避可能としたが,他に良い方法は無いだろうか.例えば,
where TKey != TValue
のようなジェネリック型制約ができるとか…
- 投稿日:2020-07-26T20:33:17+09:00
[C#]リフレクションを使って基本クラスライブラリ(BCL)のクラス図を作ってみた
ソースコードからクラス図を作るツールなどはあるが、アセンブリからリフレクションでPlantUMLのクラス図を作成するツールを作ってみた。
集約関係までやるとキリが無いので継承関係のみ。
個人的にはクラス図は重視していなくて、既にありそうだけど手習いで作成した。ソースコード
実行例(ファイルがくそデカイsvgなので注意。)
感想
IDisposable
やIEquatable<T>
のようなインターフェースの実装関係を含めると、とても見れたものではないのでステレオタイプとして表現した。- それでも利用頻度の低そうな名前空間は消した。
Delegate
とAttribute
も消した。- PlantUMLは最新版を使え。1.2020.10で謎の例外が起きていたけど、1.2020.15ならOKだった。
- GraphViz/dotは最新版を使え。安定版だと謎のエラーが起きた。
- PlantUMLの名前空間レイアウトは謎。
- 投稿日:2020-07-26T20:08:13+09:00
Sandcastle を使わずにクラスリファレンスを出力するライブラリを作成しました。
このドキュメントの内容
Sandcastle を使わずにクラスリファレンスを出力するライブラリ
mxProject.Tools.ClassDoc
を作成しました。
特徴を簡単に説明します。GitHub の Readme とほぼ同じ内容です。公開先
- GitHub
- Nuget
機能
XMLコメントファイルの読み込み
VisualStudio から出力される XML コメントファイルを読み込み、リフレクションで取得された型やメンバの情報に関連付けます。次のタグをサポートしています。
- summary
- param
- returns
- paramref
- typeparamref
- exception
- remarks
inheritdoc タグ
<inheritdoc> タグによるコメントの継承をサポートしています。
次の例では、SubClass.OutputDodument のメソッドから BaseClass.OutputDodument メソッドに記述されたコメントの内容を参照できます。public class BaseClass { /// <summary> /// Output to document. /// </summary> public virtual void OutputDocument() {} } public class SubClass : BaseClass { /// <inheritdoc/> public override void OutputDocument() {} }参照タグの置き換え
<see>, <seealso>, <paramref>, <typeparamref> タグをリンク文字列などに置き換えることができます。
次のような see タグがソースコードの XML コメント内に記述されているとします。
<see cref="SampleClass"/> <see cref="SampleClass.SampleMethod"/>XML コメントファイルには次のように出力されます。cref には型やメンバを表す識別子が出力されます。
<see cref="T:SampleNamespace.SampleClass"/> <see cref="M:SampleNamespace.SampleClass.SampleMethod"/>
IClassDocumentFormatter
インターフェースの実装により、次のような文字列に置き換えて出力することができます。[SampleClass](./SampleNamespace/SampleClass.md) [SampleClass.SampleMethod](./SampleNamespace/SampleClass.md#SampleMethod)※ 異なるページ内のアンカーへ直接遷移することは実現できていません。上の例では "#SampleMethod" アンカーには遷移せず、"SampleClass.md" ページの先頭へ遷移します。
ドキュメントへの出力
ドキュメント出力は主に IClassDocumentWriter インターフェースと IClassDocumentFormatter インターフェースの機能を利用します。
ライブラリには次の実装を含んでいます。上記のリンク先のページもこれらのクラスを用いて出力したものです。
- RazorEngine を使用してファイルに出力する RazorDocumentWriter クラス
- Markdown 形式のリンクやアンカーへの文字列フォーマットを行う MarkdownFormatter クラス
使用方法
次の例では、LoadAssemblies フォルダに格納されたアセンブリとXMLコメントファイルを読み込み、クラスリファレンスを出力しています。使用しているテンプレートと出力結果は公開先を参照してください。
テンプレート
出力ファイル
static void OutputDocument() { // Razor テンプレート string typeTemplate = @".\RazorTemplates\TypeTemplate.txt"; string namespaceTemplate = @".\RazorTemplates\NamespaceTemplate.txt"; // 読み込むアセンブリ string[] dlls = new[] { @".\LoadAssemblies\mxProject.Tools.ClassDoc.dll", @".\LoadAssemblies\mxProject.Tools.ClassDoc.Razor.dll", @".\LoadAssemblies\SampleLibrary1.dll", @".\LoadAssemblies\SampleLibrary2.dll" }; // ライターを設定 RazorDocumentWriter writer = new RazorDocumentWriter(Encoding.UTF8) { RootDirectory = @".\Documents\", TypeDocumentTemplate = File.ReadAllText(typeTemplate, Encoding.UTF8), NamespaceDocumentTemplate = File.ReadAllText(namespaceTemplate, Encoding.UTF8), }; // コンテキストとフォーマッターを設定 ClassDocContext context = new ClassDocContext() { // 出力対象を決定するフィルタメソッド // ここで設定している値は ClassDocContext の初期値として設定されているものと同じです ConstructorFilter = ConstructorInfoExtensions.IsRecommendOutputToDocument, TypeFilter = TypeExtenstions.IsRecommendOutputToDocument, PropertyFilter = PropertyInfoExtensions.IsRecommendOutputToDocument, FieldFilter = FieldInfoExtensions.IsRecommendOutputToDocument, MethodFilter = MethodInfoExtensions.IsRecommendOutputToDocument, EventFilter = EventInfoExtensions.IsRecommendOutputToDocument, }; MarkdownFormatter formatter = new MarkdownFormatter(context) { // アンカーのフォーマット // ここで設定している値は MarkdownFormatter の初期値として設定されているものと同じです ConstructorAnchorDefaultFormat = "{0} Constructor", PropertyAnchorDefaultFormat = "{0} Property", FieldAnchorDefaultFormat = "{0} Field", MethodAnchorDefaultFormat = "{0} Method", EventAnchorDefaultFormat = "{0} Event", ParameterNameDefaultFormat = "`{0}`", }; // 型情報を読み込みます IReadOnlyList<TypeWithComment> types = TypeLoader.LoadTypes(dlls, context, null); // 名前空間ごとにグループ化します foreach (var group in types.GroupBy(type => type.Namespace).OrderBy(group => group.Key)) { NamespaceInfo nameSpace = new NamespaceInfo(group.First()?.Namespace, group); // 名前空間の情報をドキュメントに出力します writer.WriteNamespaceDocument(nameSpace, formatter); // 型の情報をドキュメントに出力します foreach (var type in group.OrderBy(type => type.Name)) { writer.WriteTypeDocument(type, formatter); } } }最後に
こういったライブラリの拡充が XML コメントを記述するモチベーションにつながれば幸いです。
- 投稿日:2020-07-26T20:06:48+09:00
[Microsoft] 5. ルーティング を用いたアプリ内での画面遷移 - Angularチュートリアル Tour of Heroes を Blazor で再実装する
ルーティング を用いたアプリ内での画面遷移
- ダッシュボードビューを追加します。
- ヒーロービューと、ダッシュボードビュー間で画面遷移させます。
- ヒーロー名クリックで詳細画面へ遷移させます。
- アプリの外からアプリのURLを開いたとき、該当の画面を表示させます。
Blazorのルーティング
Angularには、モジュール内での画面遷移を設定するためのルーティングモジュールがありました。
Blazorは、コンポーネントの先頭に@page "パス"
と書くだけでルーティングできます。
Heroes
コンポーネントをページにする
Heroes.razor
ファイルを移動します(移動しなくてもよいです)。git mv BlazorTourOfHeroes/Shared/Heroes.razor BlazorTourOfHeroes/Pages/Heroes.razor
ファイルの先頭に
@page
を追加してパスを設定します。Heroes.razor@page "/heroes"トップページにリンクを追加する
トップページを書き換えて
Heroes
コンポーネントへのリンクを追加します。リンクは
<NavLink>
タグを使います。index.razor@page "/" <h1>Tour of Heroes</h1> <nav> <NavLink href="heroes">Heroes</NavLink> </nav> <Message></Message>ダッシュボードビューを追加する
Pagesディレクトリ以下にコンポーネントを作成します。
dotnet new razorcomponent -o BlazorTourOfHeroes/Pages -n Dashboard
Dashboard.razor@page "/dashboard" @using BlazorTourOfHeroes.Model @using BlazorTourOfHeroes.Service @inject IHeroService HeroService <h3>Top Heroes</h3> <div class="grid grid-pad"> @foreach (var hero in heroes) { <a class="col-1-4"> <div class="module hero"> <h4>@hero.Name</h4> </div> </a> } </div> @code { private IEnumerable<Hero> heroes; protected override async Task OnInitializedAsync() { await GetHeroesAsync(); } private async Task GetHeroesAsync() { heroes = (await HeroService.GetHeroes()).Take(5); } }内容は
Heroes
コンポーネントととほぼほぼ同じです。LINQを使用して、先頭5つのみを取得するようにしています。
private async Task GetHeroesAsync() { heroes = (await HeroService.GetHeroes()).Take(5); }ダッシュボードへのパス
先頭にある
@page
で決めています。@page "/dashboard"トップページにリンクを追加する
トップページを書き換えて、ダッシュボードへのリンクを追加します。
index.razor<h1>Tour of Heroes</h1> <nav> <NavLink href="dashboard">Dashboard</NavLink> <NavLink href="heroes">Heroes</NavLink> </nav> <Message></Message>ヒーロー詳細へのナビゲーション
以下の箇所からヒーロー詳細を表示できるようにします。
- ダッシュボードのヒーロー
- ヒーロー一覧のヒーロー
- アドレスバーへ入力したURL
Heroes
コンポーネントからヒーロー詳細を削除する
Heroes
コンポーネントから<HeroDetail>
タグを削除します。ヒーロー詳細のパスを決める
HeroDetail
コンポーネントを移動します(しなくてもよいです)。git mv BlazorTourOfHeroes/Shared/HeroDetail.razor BlazorTourOfHeroes/Pages/HeroDetail.razor
@page
を追加します。Id:int
として、int型以外を受け取らないようにします。HeroDetail.razor@page "/detail/{Id:int}"
Dashboard
コンポーネントからのリンクURLにパラメータを含めるようにします。
Pages/Dashboard.razor@foreach (var hero in heroes) { <NavLink href="@("detail/" + hero.Id)" class="col-1-4"> <div class="module hero"> <h4>@hero.Name</h4> </div> </NavLink> }
Heroes
コンポーネントからのリンク
Heroes
コンポーネントのヒーロー要素は、以下のようにクリックイベント付きの<li>
になっています。置き換えます。Pages/Heroes.razor@foreach (var hero in heroes) { <li @onclick="@(_ => OnSelect(hero))" class="@(selectedHero == hero ? "selected" : "")" ><span class="badge">@hero.Id</span> @hero.Name </li> }
foreach
と<li>
はそのままにリンクにします。Pages/Heroes.razor@foreach (var hero in heroes) { <li> <NavLink href="@("detail/" + hero.Id)"> <span class="badge">@hero.Id</span> @hero.Name </NavLink> </li> }不要になったコードを削除する(しなくてもよいです)
Heroes
コンポーネントのonSelect
周りの処理は不要になったので削除します。Pages/Heroes.razorprivate List<Hero> heroes; private async Task GetHeroesAsync() { heroes = await HeroService.GetHeroes(); } protected override async Task OnInitializedAsync() { await GetHeroesAsync(); }
HeroDetail
コンポーネントへルーティングできるようにする
Heroes
コンポーネントからHeroDetail
コンポーネントのhero
プロパティをセットしてヒーローを表示するようにしていました。
HeroDetail
コンポーネントを変更し、URL中のID値からヒーローを表示するようにします。
HeroDetail
コンポーネントでHeroService
を使えるようにします。Pages/HeroDetail.razor@using BlazorTourOfHeroes.Service @inject IHeroService HeroServiceルートパラメータからIDを受け取る
プロパティに
[Parameter]
をつけると、ルートパラメータを受け取れます。HeroDetail.razor[Parameter] public int Id { get; set; }受け取ったIDからヒーローを読み込みます。
Pages/HeroDetail.razorHero Hero { get; set; } protected override async Task OnInitializedAsync() { await GetHeroAsync(); } private async Task GetHeroAsync() { this.Hero = await HeroService.GetHeroAsync(this.Id); }
HeroService
にGetHeroAsync
メソッドを追加するIDから単一のヒーローを返すメソッドを追加します。
Service/HeroService.cspublic Task<Hero> GetHeroAsync(int id) { // TODO: ヒーロー達を取得した __後で__ メッセージを送るようにする messageService.Add($"HeroService: fetched hero id={id}"); var hero = MockHeroes.Create().FirstOrDefault(x => x.Id == id); return Task.FromResult(hero); }こんなんできました
ブラウザのアドレスバーに
http://localhost:5000/detail/11
と入力します。Dr. Niceが表示されます。
前画面に戻れるようにする
詳細から一覧やダッシュボードに戻れるようにします。
「戻る」ボタンを追加します。
Pages/HeroDetail.razor<button @onclick="GoBackAsync">戻る</button>AngularのようなLocationサービスはないようなので、いつものJavaScript
window.history.back(-1)
を呼ぶようにします。
JSRuntime
をインジェクトします。Pages/HeroDetail.razor@inject IJSRuntime JSRuntime
window.history.back(-1)
を呼ぶようにします。Pages/HeroDetail.razorprivate async void GoBackAsync() { await JSRuntime.InvokeVoidAsync("history.back", -1); }
- 投稿日:2020-07-26T19:08:36+09:00
【C#】seleninmでスクレイピング
参考
PHPやPythonでスクレイピングの勉強をしていたのですが、
「Selenium」が様々な言語に対応しているという事と、
以前クラウドソーシングでC#の「Selenium」があったので、サンプルを記載しました。事前準備
Nugetパッケージで以下をインストールします。
・Selenium.Support
・Selenium.WebDriverChromeドライバをダウンロードします。
Chromeドライバから、自分の端末のChromeとバージョンが一致するものをダウンロードしてください。
今回のサンプルでは、「C:\webdriver」にフォルダに配置しています。コード
以下は、amazonのサイトで、検索結果を取得するサンプルです。
「テレビ 4K」を検索エリアに入力して、検索後、タイトルを取得&コンソール出力しています。
セレクタとかは、セレクタにわかりやすく各言語の説明が書かれています。using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Windows.Forms; namespace SeleniumCSharp { public partial class Main { public Main() { scriping(); } [Obsolete] private void scriping() { IWebDriver driver = new ChromeDriver("C:\\webdriver"); //Webページを開く driver.Navigate().GoToUrl("https://www.amazon.co.jp/"); //検索ボックスに入力 IWebElement searchtextbox = driver.FindElement(By.Id("twotabsearchtextbox")); searchtextbox.SendKeys("テレビ 4K"); //検索実行 IWebElement searchbtn = driver.FindElement(By.ClassName("nav-input")); searchbtn.Click(); //画面の表示が終わるまで待機(最大10秒) TimeSpan TIMEOUT = new TimeSpan(0, 0, 10); // 10秒 WebDriverWait wait = new WebDriverWait(driver, TIMEOUT); wait.Until(ExpectedConditions.PresenceOfAllElementsLocatedBy(By.CssSelector("span.a-size-base-plus"))); //要素を探す(タイトル) IReadOnlyList<IWebElement> spans = driver.FindElements(By.CssSelector("span.a-size-base-plus")); //各要素を出力 foreach (IWebElement span in spans) { //商品名を出力 Console.WriteLine( span.Text); } } } }
- 投稿日:2020-07-26T17:33:09+09:00
Unity スクロール領域の端が表示されているかを検出する
概要
今回はUnityのScrollRectにおいて、スクロール領域の4辺いずれかが画面に表示されているかを実装しました。
分かりやすくするため以下の例では、端が表示されているかどうかを検出し、対応する辺を黄色くしています。画面の上や左端に達したとき黄色く表示されているのが確認できると思います。
プロジェクト全体はこちらから
https://github.com/Arihide/scroll-edge-detection説明
今回作成したソースコードは以下の通りです。
using UnityEngine; using UnityEngine.UI; public class EdgeDetect : MonoBehaviour { [SerializeField] private ScrollRect scroll = null; [SerializeField] private Image top = null; [SerializeField] private Image bottom = null; [SerializeField] private Image left = null; [SerializeField] private Image right = null; private void Update() { Bounds contentBound = RectTransformUtility.CalculateRelativeRectTransformBounds(scroll.viewport, scroll.content); Rect viewportRect = scroll.viewport.rect; top.enabled = viewportRect.max.y >= contentBound.max.y; // 上までスクロールされているか? bottom.enabled = viewportRect.min.y <= contentBound.min.y; left.enabled = viewportRect.min.x <= contentBound.min.x; right.enabled = viewportRect.max.x >= contentBound.max.x; } }一番重要な箇所は
RectTransformUtility.CalculateRelativeRectTransformBounds
で、viewportからみたcontentの領域を計算している点ですね。
あとは、viewportとcontentの最大値や最小値を比較してあげれば良いです。もしかすると、Scrollbar.valueを使えば良いんじゃないかと思われる方もいらっしゃると思います。
ただこの場合だと、スクロール全体の領域が表示領域より小さい場合は4辺すべてが表示されることになりますが、それを検出することができないので、このような方法を用いています。基本的なロジックは以上の通りなのですが、このままの条件で使用すると、しばしば端にあるのに正しく判定されないことがあります。その場合は以下のように小さい値を加えて条件を緩くすると良いです。
using UnityEngine; using UnityEngine.UI; public class EdgeDetect : MonoBehaviour { [SerializeField] private ScrollRect scroll = null; [SerializeField] private Image top = null; [SerializeField] private Image bottom = null; [SerializeField] private Image left = null; [SerializeField] private Image right = null; private const float eps = 0.1f; private void Update() { Bounds contentBound = RectTransformUtility.CalculateRelativeRectTransformBounds(scroll.viewport, scroll.content); Rect viewportRect = scroll.viewport.rect; top.enabled = viewportRect.max.y >= contentBound.max.y - eps; bottom.enabled = viewportRect.min.y <= contentBound.min.y + eps; left.enabled = viewportRect.min.x <= contentBound.min.x + eps; right.enabled = viewportRect.max.x >= contentBound.max.x - eps; } }スワイプによる画像切り替えなどを実装するときに役に立つと思います。
- 投稿日:2020-07-26T16:29:01+09:00
Unity初心者がゼロからルービックキューブを作りたい
- 投稿日:2020-07-26T00:07:03+09:00
Windowsのスクリーンショットを自動保存するC#のスクリプトを少し改変したメモ
前置き
Windows 10用に使い勝手の良いキャプチャソフトを探していたところ、
「Windowsのスクリーンショットをファイルに自動保存」の記事を見つけました。
ただ試してみたところ、個人的にはこうなるともっと使いやすいな、という点があったので、元がスクリプトであるという利点を活かし少しソースコードをいじってみました。ソースコード
C#単独での実装である、実装2-3の改変です。
元記事と同様に、C:\Windows\Microsoft.NET\Framework\v[バージョン]\csc.exe /t:winexe AutoSaveSS.cs
などでコンパイルしてください。
ソースコード (326行)
AutoSaveSS.cs// original code by earthdiver1 // arranged code by Crotczet using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; using System.Media; public class AutoSaveSS { [STAThread] public static void Main() { if (Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).Length == 1) { Application.Run(new ClipboardWatcherForm()); } } } public class ClipboardWatcherForm : Form { [DllImport("user32.dll")] private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); [DllImport("user32.dll")] private static extern bool AddClipboardFormatListener(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool RemoveClipboardFormatListener(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool RegisterHotKey(IntPtr hWnd, int id, int modKey, int key); [DllImport("user32.dll")] private static extern bool UnregisterHotKey(IntPtr hWnd, int id); [DllImport("user32.dll")] private static extern uint GetClipboardSequenceNumber(); NotifyIcon _notifyIcon = new NotifyIcon(); const int MOD_ALT = 0x0001; const int MOD_CONTROL = 0x0002; const int MOD_SHIFT = 0x0004; const int MOD_WIN = 0x0008; const int MOD_NOREPEAT = 0x4000; bool _disposed; static int _idWindow = (new Random()).Next(0x1000, 0xbffe); static int _idAllDisplay = _idWindow + 1; uint _lastSeq = 0; // parameters string _imageDir = Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), @"ScreenShot"); bool _drawCursor = true; bool _showBalloonTip = false; string _fileNamePrefix = @"SS-"; string _fileDateFormat = "HHmmss.f"; string _fileNameSuffix = @""; string _imageFileExtension = "png"; bool _subFolder = true; string _subFolderNamePrefix = @"SS-"; string _subFolderDateFormat = "yyyyMMdd"; string _subFolderNameSuffix = @""; int _modWindow = MOD_NOREPEAT; int _VKWindow = 0xEC; // VK_OEM_PA2 int _modAllDisplay = MOD_CONTROL | MOD_NOREPEAT; int _VKAllDisplay = 0xEC; public ClipboardWatcherForm() { _disposed = false; SetParent(Handle, new IntPtr(-3)); // HWND_MESSAGE => message-only window _notifyIcon.ContextMenu = new ContextMenu(new MenuItem[] { new MenuItem("Exit" , (s, e) => { _notifyIcon.Visible = false; Application.Exit(); }), }); _notifyIcon.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath); _notifyIcon.Text = "AutoSaveSS"; _notifyIcon.Visible = true; if (_imageDir == null) _imageDir = Application.StartupPath; AddClipboardFormatListener(Handle); RegisterHotKey(Handle, _idWindow, _modWindow, _VKWindow); RegisterHotKey(Handle, _idAllDisplay, _modAllDisplay, _VKAllDisplay); } protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { foreach (MenuItem item in _notifyIcon.ContextMenu.MenuItems) item.Dispose(); _notifyIcon.ContextMenu.Dispose(); _notifyIcon.Dispose(); } RemoveClipboardFormatListener(Handle); UnregisterHotKey(Handle, _idWindow); UnregisterHotKey(Handle, _idAllDisplay); _disposed = true; base.Dispose(disposing); } protected override void WndProc(ref Message m) { if (m.Msg == 0x312 && (int)m.WParam == _idWindow) OnHotKeyPressed(0); // WM_HOTKEY if (m.Msg == 0x312 && (int)m.WParam == _idAllDisplay) OnHotKeyPressed(1); // WM_HOTKEY if (m.Msg == 0x31D && Clipboard.ContainsImage()) OnClipboardImageUpdate(); // WM_CLIPBOARDUPDATE base.WndProc(ref m); } protected virtual void OnHotKeyPressed(int captureDisplay) { var t = new Thread(() => { WindowScreenshot.SetClipboard(_drawCursor, captureDisplay); }); t.SetApartmentState(ApartmentState.STA); t.Start(); // t.Join(); // uncomment to avoid "System.Runtime.InteropServices.ExternalException (0x800401D0)" error } protected virtual void OnClipboardImageUpdate() { uint seq = GetClipboardSequenceNumber(); if (seq == _lastSeq) return; _lastSeq = seq; if (Clipboard.ContainsData("Text") || // don't make screenshots by excel cells, powerpoint objects, etc. Clipboard.ContainsData("HTML Format") || Clipboard.ContainsData("Object Descriptor") || Clipboard.ContainsData("FileContents") || Clipboard.ContainsData("EnterpriseDataProtectionId") ) return; var t = new Thread(() => { Image img; if (Clipboard.ContainsData("PNG")) { IDataObject data = Clipboard.GetDataObject(); img = Image.FromStream((Stream)data.GetData("PNG")); } else { img = Clipboard.GetImage(); } if (img != null) { string _imageFolder = _imageDir; if (_subFolder) { _imageFolder = Path.Combine(_imageFolder, _subFolderNamePrefix + DateTime.Now.ToString(_subFolderDateFormat) + _subFolderNameSuffix); } if (!Directory.Exists(_imageFolder)) { Directory.CreateDirectory(_imageFolder); } string filename = Path.Combine(_imageFolder, _fileNamePrefix + DateTime.Now.ToString(_fileDateFormat) + _fileNameSuffix + "."+_imageFileExtension); try { img.Save(filename, getImageFormat(_imageFileExtension)); } catch (ExternalException) { } // makeshiftly ignore "System.Runtime.InteropServices.ExternalException (0x800401D0)" error finally { if (_showBalloonTip) { _notifyIcon.ShowBalloonTip(1000, "", "Screenshot saved!", ToolTipIcon.Info); } else { SystemSounds.Beep.Play(); } } } img.Dispose(); }); t.SetApartmentState(ApartmentState.STA); t.Start(); } public static System.Drawing.Imaging.ImageFormat getImageFormat(string in_ext) { System.Drawing.Imaging.ImageFormat if_ret = new System.Drawing.Imaging.ImageFormat(Guid.NewGuid()); switch (in_ext.ToLower()) { case "bmp": if_ret = System.Drawing.Imaging.ImageFormat.Bmp; break; case "gif": if_ret = System.Drawing.Imaging.ImageFormat.Gif; break; case "jpg": case "jpeg": if_ret = System.Drawing.Imaging.ImageFormat.Jpeg; break; case "ico": if_ret = System.Drawing.Imaging.ImageFormat.Icon; break; case "png": if_ret = System.Drawing.Imaging.ImageFormat.Png; break; case "tif": case "tiff": if_ret = System.Drawing.Imaging.ImageFormat.Tiff; break; } return if_ret; } } public static class WindowScreenshot { [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] private struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public Point ptScreenPos; } [StructLayout(LayoutKind.Sequential)] private struct ICONINFO { public bool fIcon; public int xHotspot; public int yHotspot; public IntPtr hbmMask; public IntPtr hbmColor; } [DllImport("user32.dll")] private static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("dwmapi.dll")] private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute); [DllImport("user32.dll")] private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32.dll")] private static extern bool GetCursorInfo(out CURSORINFO pci); [DllImport("user32.dll")] private static extern IntPtr CopyIcon(IntPtr hIcon); [DllImport("user32.dll")] private static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo); [DllImport("user32.dll")] private static extern bool DrawIcon(IntPtr hdc, int x, int y, IntPtr hIcon); const int DWMWA_EXTENDED_FRAME_BOUNDS = 9; const int CURSOR_SHOWING = 1; static WindowScreenshot() { SetProcessDPIAware(); } public static void SetClipboard(bool drawCursor, int captureDisplay) { RECT R; var rList = new List<Rectangle>(); Rectangle rBmp = Rectangle.Empty; switch (captureDisplay) { case 0: IntPtr hWnd = GetForegroundWindow(); int status = DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, out R, Marshal.SizeOf(typeof(RECT))); if (status != 0) return; Rectangle rWindow = Rectangle.FromLTRB(R.Left, R.Top, R.Right, R.Bottom); rList.Add(rWindow); rBmp = rWindow; IntPtr h = IntPtr.Zero; int ct = 0, maxct = 20; while (true && ct++ < maxct) { h = FindWindowEx(IntPtr.Zero, h, "#32768", null); if (h == IntPtr.Zero) break; status = DwmGetWindowAttribute(h, DWMWA_EXTENDED_FRAME_BOUNDS, out R, Marshal.SizeOf(typeof(RECT))); if (status != 0) continue; Rectangle r = Rectangle.FromLTRB(R.Left, R.Top, R.Right, R.Bottom); if (!rWindow.Contains(r)) { rBmp = Rectangle.Union(rBmp, r); rList.Add(r); } } break; case 1: foreach (Screen sInfo in Screen.AllScreens) { rBmp = Rectangle.Union(rBmp, sInfo.Bounds); rList.Add(sInfo.Bounds); } break; } using (var b = new Bitmap(rBmp.Width, rBmp.Height)) { using (Graphics g = Graphics.FromImage(b)) { foreach (Rectangle r in rList) { g.CopyFromScreen(r.X, r.Y, r.X - rBmp.X, r.Y - rBmp.Y, r.Size); } if (drawCursor) { CURSORINFO cInfo; cInfo.cbSize = Marshal.SizeOf(typeof(CURSORINFO)); if (GetCursorInfo(out cInfo)) { if (cInfo.flags == CURSOR_SHOWING) { IntPtr iPtr = CopyIcon(cInfo.hCursor); ICONINFO iInfo; if (GetIconInfo(iPtr, out iInfo)) { int posX = cInfo.ptScreenPos.X - (int)iInfo.xHotspot - rBmp.X; int posY = cInfo.ptScreenPos.Y - (int)iInfo.yHotspot - rBmp.Y; DrawIcon(g.GetHdc(), posX, posY, cInfo.hCursor); } } } } } var d = new DataObject(); d.SetData(b); using (var s = new MemoryStream()) { b.Save(s, System.Drawing.Imaging.ImageFormat.Png); d.SetData("PNG", false, s); Clipboard.SetDataObject(d, true); } } rList.Clear(); } }主な変更点
タスクトレイの右クリックから設定を変更し保存しておける仕様を作るのが面倒だったため、48行目~62行目にパラメータとして設定用の変数を並べてあります。
これらは基本的に自分のニーズに合わせて設定しているため、適宜書き換えることをおすすめします。複数の作業日のスクショが混ざっているとまとめる際にややこしいので、同日のスクショを1つのサブフォルダにまとめる機能をつけました。
Excelなどでコピーした際に画像がスクショされるのを防ぐため、以下のブログ記事を参考に、クリップボードに含まれるデータフォーマットの種類で振り分けを行いました。
Windows Store Appsでクリップボードから画像を取得したいクリップボードの画像の保存時に
img
のメモリを開放しておらず、スクショ毎にメモリが膨れ上がるため、img.Dispose();
を加えました。
それに伴い「GDI+ で汎用エラーが発生しました」エラーが発生したのですが、ちょっと原因がわからなかったので例外を投げてその場凌ぎをしています。カーソルつきで全てのディスプレイをスクショできるホットキーも作成しましたが、ある条件下で不具合が起こるようです。
パラメータについて
- ファイル名・サブフォルダ名
「Prefix+日付フォーマット+Suffix」の形式で作成されます。_imageFileExtension
保存の際の拡張子を変更できます。
パラメータ変換用の関数として以下に記載のコードを加えました。
Hot Examples - C# (CSharp) System.Drawing.Imaging.ImageFormatの例_VKWindow
,_VKAllDisplay
元記事のF11だと全画面解除とかぶるため、ちょっと特殊ですがレジストリで無関係なキーをVK_OEM_PA2
に変更し、それに割り当てています。
デフォルトではアクティブウィンドウのスクショをPA2
、全体のスクショをCtrl
+PA2
に指定しています。
実際にPA2
や他の特殊なキーについてレジストリで割り当てたい場合については、手前味噌ですがこちらの記事でスキャンコードを参照した上で、適宜変更し、変数の仮想キーコードを書き換えてください。
すでにキーボードに存在する他のキーに割り当てたい場合は、仮想キーコードを調べて書き換えてください。仮想キーコードの値を参照する際は私は以下のページをおすすめします。
KT Software - 仮想キーコード一覧要改善点
マルチディスプレイ時に、ディスプレイの「表示スケール(テキスト、アプリ、その他の項目のサイズを変更する)」の値が統一されていない場合、次のいずれかの条件下で画像サイズやスクショ位置などがおかしくなるようです。
- サブディスプレイ上にカーソルがある状態でカーソルもスクショする時
- ホットキーによる全体のスクショ時
原因としては、それぞれのディスプレイの画面サイズが、全てタスクトレイのあるディスプレイの表示スケール換算で認識されるにも関わらず、実際の画面の座標は本来の各々の表示スケール換算となるためのようです。
不具合修正ができそうならば出来次第投稿すると思いますが、現状は表示スケールさえ揃えれば問題なく動作し、自分の業務には支障がないためとりあえず暫定的にこの状態で置いておきます。