20211123のC#に関する記事は10件です。

Unity Test Runnerの使い方を理解する

動作確認環境 Windows10 Unity 2019.4.13f1 Unity Test Runner とは? Unityのテスト実行ツール NUnit という.NET用テスティングフレームワークが使われている EditMode, PlayMode のテスト実行環境がある Unity Test Runner のインストール方法 Unity2019.1 以前ならば、標準でインストールされているので対応不要 Unity2019.3 以降ならば、Package Manager から Test Framework と検索してインストールする Windowの開き方 Window > General > Test Runner を選択するとウィンドウを開くことができる 以下のようなウィンドウが開けばOK Create Playmode Test Assembly Folder ボタンを押すと、 TestフォルダとAssembly Definition Fileが作成される 作成された Test フォルダ配下にスクリプトを配置していく EditMode プレイモードを経由せずに、Unityエディタで即実行できる すぐ実行できる Start() や Update() など MonoBehavior の関数は呼ばれない メソッドに [Test] アトリビュートをつけるとテストメソッドと認識される スクリプトの配置場所は… Editor のチェックを入れた Assembly Definition File(.asmdefファイル) を配置したフォルダ配下 PlayMode Unityエディタのプレイモードで実行できる MonoBehaviour を組み合わせたテストができる テスト用の Scene が生成・実行される Unity エディタがクラッシュすると、 Scene ファイルが残ってしまう スクリプトの配置場所は… Create Playmode Test Assembly Folder ボタンを押して作成したフォルダ Assembly Definition Fileの Assemply Definition References の UnityEditor.TestRunner を消す(画像の選択部分を消す) フォルダ構成例 Assets/ └ Tests/ ┝ PlayMode.asmdef ┝ TestCodeInPlayMode.cs ┝ Editor/ │ ┝ Editor.asmdef │ └ TestCodeInEditorMode.cs テストの書き方 通常は Test アトリビュート、コルーチンの場合は UnityTest アトリビュートを付けたメソッドを定義 判定の書き方は Constraint Model と Classic Model がある using System.Collections; using NUnit.Framework; using UnityEngine.TestTools; public class EditorModeExample { [Test] public void ExampleTest() { var a = 10; var b = 10; Assert.That(a == b); } [UnityTest] public IEnumerator ExampleTestEnumerator() { Assert.That(1 < 10); yield return null; Assert.That(2 < 10); yield return null; Assert.That(3 < 10); } } Classic Model Assert.True() や Assert.AreEqual() などが使える古い書き方 こっちはもう使わない 基本的には後述の Constraint Model を使う Constraint Model Assert.That() を使う Assert.That() には多くのオーバーライドがある Classic Model より Constraint Model を使う理由 複雑な条件が来た場合、Classic Model より柔軟に対応できる テストコードを結果と期待値を記述するという内容に画一化できる 旧モデルはサポートされなくなってきている テストの実行方法 実行したい関数かクラスを選択して、ダブルクリックもしくは右クリック > Run をクリックする 緑のチェックマークになれば、テスト成功 赤いバツマークになれば、テスト失敗 テスト成功時 テスト失敗時 テストの前後処理の書き方 対応するアトリビュートを使用することで可能 書き方 説明 [SetUp] 各テスト実行前に1回ずつ呼ばれる [TearDown] 各テスト実行前に1回ずつ呼ばれる [UnitySetUp] 各テスト実行前に1回ずつ呼ばれる(コルーチン) [UnityTearDown] 各テスト実行前に1回ずつ呼ばれる(コルーチン) [OneTimeSetUp] 最初のテスト実行前に1回だけ呼ばれる [OneTimeTearDown] 最後のテスト実行後に1回だけ呼ばれる コード例 // このクラスに定義された各テストが実行される前に、テストごとに一回ずつ呼ばれる [SetUp] public void Setup() { Debug.Log("SetUp"); } // このクラスに定義された各テストの実行終了後に、テストごとに一回ずつ呼ばれる [TearDown] public void TearDown() { Debug.Log("TearDown"); } // このクラスに定義された各テストが実行される前に、テストごとに一回ずつ呼ばれる(コルーチン) [UnitySetUp] public IEnumerator UnitySetup() { Debug.Log("UnitySetup"); yield break; } // このクラスに定義された各テストの実行終了後に、テストごとに一回ずつ呼ばれる(コルーチン) [UnityTearDown] public IEnumerator UnityTearDown() { Debug.Log("UnityTearDown"); yield break; } // このクラスに定義されたテストのうち最初のテストが実行される前に一回呼ばれる [OneTimeSetUp] public void OneTimeSetUp() { Debug.Log("OneTimeSetUp"); } // このクラスに定義されたテストのうち最後のテストが実行された後に一回呼ばれる [OneTimeTearDown] public void OneTimeTearDown() { Debug.Log("OneTimeTearDown"); } 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】本物の関数ポインタの使い方と関数アドレスの取り方

歴史 C#9でひっそり関数ポインタがC#に追加されました。 別に関数ポインタ自体は新しい機能ではなく、大昔からあります。ただし、C#から扱う構文が追加されたのがC#9です。 C#の関数には、staticな関数と、非staticな関数(いわゆるメソッド)があります。 これらは、似て非なるものです。 class Program() { public static void Hoge() { } public void Fuga() { } } 上記の例では、HogeとFugaはどちらも同じ引数の関数のように見えます。 しかし、実際には(低レベルには)Fuga関数はProgramのインスタンスを引数として(暗黙的に)取っているのです。 よって、Fuga関数を呼び出すには、関数ポインタの他に、Programのインスタンスが必要です。 実際にILレベルでは、非静的関数を呼び出すにはスタック上にインスタンス=マネージドポインタを積まなければなりません。 疑似ILコード ldc.i4.0 stloc.0 ldloca.s 0 call System.Int32.ToString pop 0.ToString(); C#では、これらの違いを意識することなく使えるような関数ポインタに相当する機能として、delegateが導入されました。 delegateは、内部的に関数ポインタと、その関数を呼び出す上で必要な引数への参照を保持しています。 これにより、delegateは、インスタンスメソッドが存在し、さらにGCのために参照を管理しなければならないC#における「安全な関数ポインタ」としての役割を果たすのです。 ところが、delegateは主にインスタンスメソッドの呼び出しの方に最適化されており、静的関数の呼び出しが遅いです。 本来なら余分な処理が入って複雑なインスタンスメソッドの呼び出しより、静的関数の呼び出しのほうが遅いです。 このあたりはすでに記事が出ているので詳細は譲るとして、これでは不便だという声も出てきました。 IL的には、関数のアドレスをスタックにロードするldftn命令と、スタック上の関数ポインタを介して関数を呼び出すcalli命令があり 対応するC#構文さえあれば、delegateでない本当の関数ポインタを使用するのは可能でした。 (実際、C#9以前もILを手打ちすれば可能。) 構文 新しく関数ポインタ型が導入されました。型名は delegate* <TArg0,TArg1...,TResult> です。TArgには引数の型を順番に、TResultは戻り値の型に置き換えてください。 関数ポインタもポインタですので、unsafeキーワードとコンパイルオプションが必須です。 キーワードを使い回していますが、delegateでもジェネリック型でもなく、ただのポインタです。 そのため、void*やnint(IntPtr)へのキャストが可能です。 関数のアドレスは、&(関数)で取れます。例えば delegate* <double,double> sin = &Math.Sin; のように使います。前述したように、インスタンスメソッドの呼び出しは単純ではないので、今回の関数ポインタ構文ではstaticな関数のアドレスしか取得できません。そもそも関数ポインタが欲しかったのが静的メソッド用ですから、いい妥協点でしょう。 呼び出しは関数ポインタ型の変数に({引数})です。 var result = sin(0.5); 先ほどの例で作成した関数ポインタを呼び出す場合は上記のようになります。 リフレクションから関数ポインタ リフレクションで取得したMethodInfoからも関数のアドレスが取得できます。 static void Hoge<T0,T1,T2,TRes>() { Assembly asm = typeof(Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime).Assembly; var method = asm.GetType("WebAssembly.JSInterop.InternalCalls")?.GetMethod("InvokeJS", BindingFlags.Public | BindingFlags.Static); var del = (delegate*<IntPtr, IntPtr, T0, T1, T2, TRes>)methodInfo.MakeGenericMethod(typeof(T0), typeof(T1), typeof(T2), typeof(TRes)) .MethodHandle.GetFunctionPointer(); } この例では、internalなクラスに定義されていて通常アクセスできない関数への関数ポインタを無理やり作成しています。 MethodInfoのMethodHandleプロパティからMethodHandleを取得し、さらにGetFunctionPointerメソッドを呼ぶことで関数のアドレスが取れます。ジェネリックメソッドの場合はあらかじめMakeGenericMethodしないとエラーになります。 その後、アドレスをキャスト演算子でキャストすれば目的の関数ポインタが得られます。 最初に関数ポインタさえとってしまえば、そのあとの呼び出しのコストが最小限に抑えらえるので、静的関数がターゲットならば実用的ではないでしょうか。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新卒2年目までに学んだ、コーディングで意識すること

本記事はCraft Egg Advent Calendar 2021の12/3の記事です。 12/2の記事は@ishiguro_takuyaさんの[Unity] UniversalRenderPipelineについて調べた覚え書き でした。 はじめに 株式会社Craft EggでUnityクライアントエンジニアをしている豊田です。 今回は私が新卒入社してから現在の2年目までに、プルリクエストでチームからもらったコメントや技術書をなどの学びを元に、コーディングで気をつけていることをまとめてみました。 before/afterのコードの例を出すことで、新卒エンジニアが遭遇しやすいコーディングアンチパターンとその改善例の参考になれば幸いです。 コーディング規約やしきたりはチームによって異なりはしますが、できるだけ一般的な内容を取り上げました。 ※本記事で取り上げるコードはサンプルです。重要でない部分の命名は「hoge」としたり、簡略化しています。 表面上の改善 リーダブルコードには、第1部に「表面上の改善」が紹介されています。 命名規則やコードの体裁など、第三者(未来の自分自身も)がコードを理解するために最初に意識できる観点だと思います。 適切な変数名にする 意味が分かる命名にする boolは「is~」「should~」「can~」「exists~」「has~」など コールバックは「onCloseDialog」など「on~」 略しすぎない。一般的な略称ならOK(× s_pos → ◯ startPosや、startPosition) 英文法に誤りが無いか気をつける(× onClosedDialog → ◯ onCloseDialog) など 適切な関数名にする 基本的に動詞始まりにする 初期化は「Initialize」「Setup」 生成は「Create~」 登録は「Register~」 変換は「ConvertHogeToFoo」 「Add◯◯」なら「Remove◯◯」というように、名前と処理を対にして理解しやすくする 「~IfNeed」「Check~」のような命名の関数を避ける(処理を分けられないか検討する) 英文法に誤りが無いか気をつける など 一次変数でも分かりやすい命名にする 一次変数などスコープが小さくてもできるだけ意味の分かる命名にします。 before UserData u = GetUserData(userId); after UserData userData = GetUserData(userId); ※ラムダ式の中でも同様 before filterUsers = userList .Where(x => x.Id % 2 == 0) .ToList(); after filterUsers = userList .Where(user => user.Id % 2 == 0) .ToList(); ネストを浅くする 例外は先に早期returnすることで思考をクリアにできます。 before private void Hoge() { if (hoge != null) { Foo(); if(foo != null) { // メインの処理が続く } } } after private void Hoge() { if (hoge == null) { return; } Foo(); if (foo == null) { return; } // メインの処理が続く } なお、早期returnは関数の出口が複数でき、利用側の期待する処理が行われず罠にハマることがあるため、何でも早期returnするのではなく、処理を分離できないか検討するなどしてケースバイケースで用いるのが良いと思います。 三項演算子を使う before if (IsHoge()) { foo = bar; } else { foo = null; } after foo = IsHoge() ? bar : null; foreachをLINQに置き換える before private List<ItemData> CreateItemDataList(List<uint> itemIds) { List<ItemData> itemDataList = new List<ItemData>(); foreach (uint id in itemIds) { itemDataList.Add(new ItemData(id)); } return itemDataList; } after private List<ItemData> CreateItemDataList(List<uint> itemIds) { return itemIds .Select(id => new ItemData(id)) .ToList(); } 配列やListに対してforeachで処理する場合の多くはLINQで書けます。 文字列補間を使う C#の6.0以上で使える文字列補間は、書きやすく見やすいです。 https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/tokens/interpolated before Debug.LogError("userId : " + userId + ", userName : " + userName + "のユーザでエラー"); after Debug.LogError($"userId : {userId}, userName : {userName}のユーザでエラー"); コメントアウト 自明なコメントは書かない Initialize(); // 初期化 と書かれていてもコメントの有無で情報量が変わりません(むしろノイズになりうる)。 できるだけコードで説明することを心がけます。 処理の内容ではなく理由を書く 特殊な実装をしていたら、なぜそうしているかのコメントがあると、読んだ人をびっくりさせません。 なお、低レベルのコードを要約したり、処理のまとまりを説明する場合など、ケースによって処理の内容は書くことはあると思います。 問題のあるコードにコメントを書く こちらも読んだ人をびっくりさせることを防ぎます。 やむを得ず綺麗じゃない実装をしているのなら、しれっとコードに紛れ込ませるのではなくTODOやHACKなどを添えて問題点を説明します。 安全性、Nullチェック Get関数は失敗を考慮する before UserData userData = GetUserData(userId); userData.HogeMethod(); この例だと、userDataの取得に失敗した時にNullエラーになりかねないので、Nullチェックを挟みます。 after UserData userData = GetUserData(userId); if (userData == null) { Debug.LogError( "UserData is null. userId : " + userId ); // 場合に合わせて例外処理を行う return; } userData.HogeMethod(); Null演算子を使って簡潔にする before if (userData != null && userData.Card != null) { // userData.Cardにアクセスする処理 } after if (userData?.Card != null) { // userData.Cardにアクセスする処理 } ※ リストのNull、空チェックも同様に簡潔にできます before if (userList != null && userList.Count != 0) { // userListにアクセスする処理 } after if ((userList?.Count ?? 0 ) != 0) { // userListにアクセスする処理 } なお、MonoBehaviourなどUnityEngine.Objectを継承したものに対してのnullチェックは以下のような問題もあるので、Null演算子は使わずに== nullを使います。 エラー通知は必要な時にはっきり行う ログは、エラーが起きた時にエラー出力をさせるなど、必要な時に行います。 正常挙動である時はログは出力させません。 なお、追いにくい低レイヤの処理には正常ログを入れる場合もあるかと思います(問題が起きた時に追いやすくする)。 制御の改善 処理の対象を絞ってLoop処理を回す before foreach (var hoge in hogeList) { if(!hoge.IsFoo) { continue; } hoge.HogeMethod(); } 先にIsFooで絞ってからforを回したほうが、どういうデータを処理したいループなのかが分かりやすいです。 after foreach (var hoge in hogeList.Where(hoge => hoge.IsFoo)) { hoge.HogeMethod(); } 状態を保持する変数はenumで定義する フラグを乱立すると状態が分かり辛く、ソースを読み解くのが大変になります。 before private bool isProcessing; private bool isInterrupted; private bool isInitialized; after private enum StateType { None, Processing, Interrupted, Initialized } private StateType currentState; 設計の改善 モジュールを「純粋」にして、モジュール間を「疎遠」にすることを意識します。 凝集度と結合度の話ですが、以下の要素は基本的に凝集度を高く結合度を低くするための実践例だと思います。 長すぎるメソッド、クラスを書かない 一つの関数が長くなっているのであれば、まず分離できないかを検討します。 before public void Setup() { // UI、コンポーネントの初期化が数行に渡って書かれる ... // ロードの準備が数行に渡って書かれる ... // ロード処理が数行に渡って書かれる ... } after public void Setup() { SetupComponents(); PrepareLoad(); Load(); } 必要ないものはpublicにしない クラスが、利用側の知る必要のない内部の詳細部分を隠蔽すれば、やりとりがシンプルになりコード全体の複雑性を下げることができます。利用側からみても、使い方がシンプルになり使い勝手が良くなります。 まずprivateで書いてみて、公開する必要があればpublicにする、がいいかもしれません。 ロジックとデータは近くに置く before(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } // コンストラクタは省略 } before(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.Price * purchaseData.Count}円です") このbeforeの例では、データとなるPurchaseDataクラスと、利用側の2つのコードがあります。 データのクラスは、DB由来のモデルクラスと捉えてもいいです。 改善したいのは、Debug.Log内の金額の計算を利用側で行っている点で、ロジックがデータの外側に実装されてしまっています。 after(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } public uint GetAmount() { return Price * Count; } // コンストラクタは省略 } after(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.GetAmount()}円です") afterでは、合計金額を計算するロジックをデータのクラス側に移動しました。 例えばこれに、「軽減税率を適用するか」という「データ」を増やし、消費税計算の「ロジック」の実装が必要になった時に、改修の対象はPurchaseData内のみですので、利用側はロジックの変更を知らなくて済みます。 引数で処理の内容が変わる関数を避ける 以下のように引数を元に処理が変わる関数だと、利用側が関数のロジックを把握する必要があり、ブラックボックス化できません(制御結合になっている)。 制御結合になっている例 public void hoge(bool flag) { if(flag) { // 処理Aが続く } else { // 処理Bが続く } } ただし、システムによっては、制御結合を避けられない場合もあるので、集まっている機能を精査しながらより結合度を下げられないか検討します。 無駄な引数を送らない before例ではスタンプ結合になっています。関数にはできるだけ使うものだけ送るようにします。 before public void SetUserNameLabel(UserData userData) { userNameLabel.text = userData.Name; } after public void SetUserNameLabel(string userName) { userNameLabel.text = userName; } Unity関連 GetComponent、Find系の関数を使わない パフォーマンスに悪いので、事前にSerializeFieldで参照を持たせます。 Update()は使わない コールバックやコルーチンで実現できないか検討をします Animatiorの引数はhash値を使う Tips的な項目ですが、animatorの引数はhash値を使った方がパフォーマンス的に良いです。 Animator.StringToHash で取得した値を保持しておくのがミソ(先輩の言葉を引用) before private void PlayRunAnimation() { animator.Play("run"); } after private readonly int AnimationHashRun = Animator.StringToHash("run"); private void PlayRunAnimation() { animator.Play(AnimationHashRun); } さいごに コードの可読性、保守性、安全性や柔軟性を上げるための手法、設計の原則は今回記事に取り上げたことの他にも様々あると思います。 業務でのプルリクエストでのコメントで得た学びの他、「リーダブルコード」と「プリンシプルオブプログラミング」からも執筆にあたって参考にしました。これらの書籍は1年目で読んだのですが、とても勉強になりました。 今回は局所的なコードの改善事例を取り上げましたが、今後インタフェースを使った実装の分離や、デザインパターンを用いた実践なども体系的にまとめてアウトプットしてみたいです。 アドベントカレンダーの明日の記事は @kai_yamamoto です。お楽しみに! 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#とコンポーネントを使用してExcel文書をHTMLに変換する方法

C#とコンポーネントを使用してExcel文書をHTMLに変換する方法 背景 作業には、財務を対象として処理する場合、大量のデータを計算したり統計したりすることがあります。皆さんの知ってるとおり、Excelファイルは非常に人気があり、データ処理の分野で広く使用されています。Excelを使用すると、保存されているデータに対して数学的な計算を実行できますが、データを表示する場合、Excelを直接使用することは特に適切ではありません。他の形式で表示されたいなら、HTMLはいい選択かもしれません。故に無料のコンポーネント、Spire.XLS for .NETを見つけてその変換を実現しました。では詳細を紹介します。 Spire.XLS for .NETとは? Spire.XLS for .NETは、開発者がC#やVB.NETプラットホームでExcelの文書ファイルを迅速かつ高品質で作成・編集・変換・印刷するために設計された専門的な Excelライブラリです。 中には、無料版のFree Spire.XLS for .NETと商用版のSpire.XLS for .NETがありますが、基本的な機能を搭載しているので、無料試用でも日常の仕事には結構だと思います。 コード一覧: 1 まずはWorkbookのインスタンスを作成します。WorkbookはExcel文書を展示できます Workbook workbook = new Workbook(); 2 最初のSheetを取得します Worksheet sheet = workbook.Worksheets[0]; 3 最初のSheetのセルA1を「TEXT」に設定します sheet.Range["A1"].Text = "TEST"; 4 最後は文書を保存します workbook.SaveToFile("result.xlsx", FileFormat.Version2010); このライブラリを使用してExcelファイルを開いて作成すると非常に便利です。以下の生成されたファイルのスクリーンショットを見てください。 HTMLに変換する よし、既にExcel文書を作成した以上、これからはExcelをHTMLに変換します。 1 先ずはWorkbookのインスタンスを作成し、Excel文書を表示します。 Workbook workbook = new Workbook(); 2 サンプルファイルをロードします workbook.LoadFromFile("sample.xlsx"); Worksheet sheet = workbook.Worksheets[0]; 3 一ページ目のテーブルをHTMLに変換します sheet.SaveToHtml("result.html"); 元ファイル: 作成されたHTMLファイル: 以上です、最後まで読んでいただきありがとうございます!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AmazonMusic】APIが無いけど再生中の楽曲情報をジャケット込みで無理矢理取得してみた

はじめに サブスクで曲聴いてると、タイトルは疎かアーティスト名さえよく知らんまま「これ好き~~~~」とか言ってて何かと不便です。 かと言ってクライアントの画面を出し続けるのは邪魔ですし、それじゃあ「楽曲情報を取得してDBに投げてPHPで表示できるようにしとけば、余ってるAndroid端末か何かで適当にいつでも見れるんじゃね」ということで、まずは楽曲情報の取得ができないか検証してみました。 結論から言うと、不可能ではないことがわかりましたが百点満点の利便性、確実性は望めない感じです。 今回使うあれそれ C# UWP デスクトップ版AmazonMusic Windows10 WPFではなくUWP デスクトップ用に作るのでWPFでええじゃろと思っていましたが、今回必要な機能が残念ながらUWPの方にしか無いようです。 一応UWPの機能をWPFで使う方法もあるらしいものの、面倒くさそうだし何やら制約もあるとのことで調査と検証の手間を惜しんで今回はUWPでいきます。 まずはタイトルなどの基本的な情報を取得してみる さて、早速本題に入りますが悲しいことにAmazonMusicには公開APIがありません。以前は非公式のものがあったそうですがAmazon側の仕様変更により現在は使えないらしいです。 ではどうするのかと言いますと、デスクトップ版のAmazonMusicが出して来る通知を取得してわちゃわちゃします。 事前準備(AmazonMusic側) 再生中の曲が変わったタイミングでAmazonMusicから通知が来るように設定してあげます。 デスクトップ版のAmazonMusicを起動したら、右上のアイコンを右クリックしてメニューを展開、「設定」を選択しましょう。 「再生中の楽曲情報を表示する」をONにします。 ※デスクトップ版のAmazonMusicはそれなりの頻度でアップデートされているので、今後仕様が変わることもあるかもしれません。2021年11月現在での手順です。 事前準備(Windows側) 設定>システム>通知とアクションからAmazonMusicの通知を許可します。 ※まだ一度も通知が来ていない場合は一覧に無いかもです。 事前準備(UWPプロジェクト側) VisualStudioでUWPのプロジェクトを作成したら、まずソリューションエクスプローラーからPackage.appxmanifestを開きます。 「機能」タブに移動し「ユーザー通知リスナー」のチェックボックスを選択状態にしましょう(これが前述の「WPFには無い機能」です)。 この設定手順を踏まないと通知情報を取得できません。 ※初回起動時に通知取得許可の是非を問う画面が出て来るので、許可してあげてください。 実装 検証用コード全文 MainPage.xaml <Page x:Class="AmazonMusicObserver.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:AmazonMusicObserver" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Width="1200" Height="750" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid> <TextBox x:Name="logTextBox" Margin="10,10,10,10" Text="" TextWrapping="Wrap" IsReadOnly="True" /> </Grid> </Page> MainPage.xaml.cs using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading; using System.Threading.Tasks; using Windows.Foundation.Metadata; using Windows.UI.Notifications; using Windows.UI.Notifications.Management; using Windows.UI.Xaml.Controls; namespace AmazonMusicObserver { public sealed partial class MainPage : Page { //ターゲットとするAppInfo.AppUserModelId const string TGTID = "Amazon.Music"; //コントロールアクセス用オブジェクト static TextBox logTB; public MainPage() { this.InitializeComponent(); logTB = this.FindName("logTextBox") as TextBox; MainAsync(); } /// <summary> /// 通知一覧から、対象外であるAppInfo.AppUserModelIdを持つ通知を除去する。 /// また、利便性のため最新通知が先頭へ来るように逆順ソートする。 /// </summary> /// <param name="origin">加工前の通知一覧</param> /// <returns>加工後の通知一覧</returns> static List<UserNotification> removeNonTgtNotification(IReadOnlyList<UserNotification> origin) { List<UserNotification> temp = new List<UserNotification>(); foreach (var item in origin.ToArray()) { if (item.AppInfo.AppUserModelId.Equals(TGTID)) { temp.Add(item); } } temp.Reverse(); return temp; } /// <summary> /// AmazonMusicからの通知であることを前提に、通知のテキスト要素を受け取って解析し画面等の更新処理を走らせる /// </summary> /// <param name="temp">通知のテキスト要素</param> static void updateWork(IReadOnlyList<AdaptiveNotificationText> temp) { try { logTB.Text += "\nタイトル:" + temp[0].Text; //2021年11月現在、楽曲のタイトルが格納されている logTB.Text += "\nアーティスト:" + temp[1].Text; //2021年11月現在、楽曲のアーティスト名が格納されている logTB.Text += "\nアルバム:" + temp[2].Text; //2021年11月現在、楽曲のアルバム名が格納されている logTB.Text += "\n-------"; Debug.WriteLine(temp[0].Text); Debug.WriteLine(temp[1].Text); Debug.WriteLine(temp[2].Text); } catch (Exception e) { logTB.Text += "\n" + e.Message; } } //AsyncなMain private static async Task<int> MainAsync() { ////////////////////////////////////////////////////// //通知リスナーにアクセスできるかどうかの諸々のチェック ////////////////////////////////////////////////////// if (!ApiInformation.IsTypePresent("Windows.UI.Notifications.Management.UserNotificationListener")) { Debug.WriteLine("IsTypePresent: NG"); return -1; } Debug.WriteLine("IsTypePresent: OK"); UserNotificationListener listener = UserNotificationListener.Current; Debug.WriteLine("listener: "); Debug.WriteLine(listener); UserNotificationListenerAccessStatus accessStatus = await listener.RequestAccessAsync(); Debug.WriteLine("accessStatus: "); Debug.WriteLine(accessStatus); if (accessStatus != UserNotificationListenerAccessStatus.Allowed) { Debug.WriteLine("アクセス拒否"); return -1; } Debug.WriteLine("アクセス許可"); ////////////////////////////////////////////////////// //通知リスナーにアクセスできるかどうかの諸々のチェックここまで ////////////////////////////////////////////////////// //初回の比較用に通知の履歴を取得 IReadOnlyList<UserNotification> oldList = await listener.GetNotificationsAsync(NotificationKinds.Toast); //無関係な要らん通知を除去しつつ逆順ソートで成形 oldList = removeNonTgtNotification(oldList); //通知の一覧を取得できるだけで、良い感じのイベントも差分を取得するメソッドも存在しないらしい。 //ので、ひたすら通知を取得&比較し続けて自力で差分を見付ける while (true) { IReadOnlyList<UserNotification> notifs = await listener.GetNotificationsAsync(NotificationKinds.Toast); //無関係な要らん通知を除去しつつ逆順ソートで成形 notifs = removeNonTgtNotification(notifs); //今回取得した通知一覧と前回取得した通知一覧を比較、それぞれの最新同士でタイムスタンプを比較して差分の有無を確認 if (DateTimeOffset.Compare(notifs[0].CreationTime, oldList[0].CreationTime) > 0) { //差分あり。最新通知のオブジェクトを取得 NotificationBinding toastBinding = notifs[0].Notification.Visual.GetBinding(KnownNotificationBindings.ToastGeneric); if (toastBinding != null) { //テキスト要素を取得して更新処理に渡す IReadOnlyList<AdaptiveNotificationText> textElements = toastBinding.GetTextElements(); updateWork(textElements); } } else { Debug.WriteLine("更新無し"); } //比較用リストを更新 oldList = notifs; Thread.Sleep(1000); } } } } 動作確認 こんな感じで、通知と同じ内容でテキストボックスが更新されます(著作権的なあれでジャケット部分はぼかしました)。 問題点 AmazonMusicからの通知に依存しているので、 通知内容の構成に変更があれば対応する必要がある 通知が表示されないと何もできない AmazonMusicのウィンドウが非アクティブでないと通知は出ない(2021年11月現在) 単純に再生が終わって次の曲が始まったケース、またはキーボードによるメディアコントロールで次の曲の再生を始めたケースでのみ通知を表示できる 必然的に、クリックで開始した曲では絶対に通知は表示されない 通知を出したくないケースに対応できない なんかたまに通知出ないことあるし、通知が出るべきときに100%確実に通知が出る保証は無いよね……? ジャケットを取得してみる とまあ色々と難ありですが、基本的な楽曲情報を取得できなくはないことがわかりました。 次はジャケットだとかカバーアートだとかアルバムアートなどと呼ばれる画像を取得します。 通知からテキスト要素をさっくり取得できたので画像も簡単に取れるだろうと思っていましたが、なんか無理でした。 どなたか情報持っていればコメントしていただければ幸いです。 なんか無理でしたで終わってはあれなのでプランB、「AmazonMusicで再生してるんだからAmazonのストアページに行けば画像あるよな作戦」でいきます。 いい感じに無理矢理度が上がってきましたね。 まずはAmazonのストア検索結果ページURLを生成する とりあえず適当に検索してみましょう。検索の精度を上げたいので、「デジタルミュージック」で絞り込みます。 出て来ましたね。 さて、気になるURLは…… でした。 &__mk_ja_JP=カタカナ&ref=nb_sb_nossは無くても検索結果に変化が無いっぽいです(たぶん)。 手入力した検索ワードはそのままURLに入る(実際にはURLエンコードを経ていますが)ので、ここを通知から取得した楽曲情報にしてしまえば検索結果ページのURLになると考えられます。 通知のテキスト要素をURLエンコードし、ちゃんと表示できるURLを生成するメソッドが下記です(最終的なコード全文を後述するので、何をしてるか興味無い人は読み飛ばしちゃって大丈夫です)。 検索結果ページURL生成処理 static Uri createStoreSearchResultPageURI(IReadOnlyList<AdaptiveNotificationText> temp) { //2021年11月現在、取得した楽曲情報からデジタルミュージックに限定したAmazonMusic検索結果ページURLを下記コードで生成可能 string paramStr = temp[0].Text + " " + temp[1].Text + " " + temp[2].Text; return new Uri("https://www.amazon.co.jp/s?k=" + System.Web.HttpUtility.UrlEncode(paramStr) + "&i=digital-music"); } ストア検索結果ページURLからページソースを取得して画像URLを取り出す 画像の表示を内部でどうこうしてたら詰みなので、まずは本当に外側から画像URLが取れるのか確認します。 みんな大好きデベロッパーツールで表示すると…… やったぜ! 外部からでも画像URLを取得できることがわかりました。 2021年11月現在、下記の構成になっているようです。 <img alt="" class="s-prefetch-image" src="https://m.media-amazon.com/images/I/71g2PZQJQJL._AC_UY218_.jpg" srcset="https://m.media-amazon.com/images/I/71g2PZQJQJL._AC_UY218_.jpg 1x, https://m.media-amazon.com/images/I/71g2PZQJQJL._AC_UY327_FMwebp_QL65_.jpg 1.5x, https://m.media-amazon.com/images/I/71g2PZQJQJL._AC_UY436_FMwebp_QL65_.jpg 2x, https://m.media-amazon.com/images/I/71g2PZQJQJL._AC_UY545_FMwebp_QL65_.jpg 2.5x, https://m.media-amazon.com/images/I/71g2PZQJQJL._AC_UY654_FMwebp_QL65_.jpg 3x" /> srcsetで3倍までのサイズ別で画像URLを持っていますね。 ここまでわかればコード上からどうにでもなります。 Amazonのストア検索結果ページURLから、検索結果1件目が持つ最大サイズの画像URLを取得 //URLからページソースを取得 string temp = new WebClient().DownloadString(対象ページのURL) temp = temp.Substring(temp.IndexOf("2.5x,"),temp.IndexOf("3x\"")); temp = temp.Split(" 3x\"",StringSplitOptions.None)[0]; temp = temp.Split(", ", StringSplitOptions.None)[1]; Uri imgURI = new Uri(temp); 今回は検証が主目的なので、解析部分はガバガバです。 2021年11月現在、上記で問題無く取得できています。 動作確認 前述の検証用コードに画像表示機能も追加して動作確認しました(コード全文は後述)。 通知から取得した楽曲情報で画像を取得できていることがわかります。 問題点 検索結果に依存しているので、 再生中楽曲のジャケット(通知の画像)と一致しないケースもある 複数のアルバムに収録されている同名曲である場合 検索結果が正確でない場合 検索結果ページのURLやページソースの構成に変更があれば対応する必要がある などの問題があり、特に画像の不一致については極端な例ではこんなケースも。 ジャケットですらねえ!!!! 検索ワードが長かったり記号含んでたりするのが良くないんだと思います。 最終的なコード全文 MainPage.xaml <Page x:Class="AmazonMusicObserver.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:AmazonMusicObserver" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Width="1200" Height="750" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="287*"/> <ColumnDefinition Width="913*"/> </Grid.ColumnDefinitions> <TextBox Grid.Column="0" x:Name="logTextBox" Margin="10,10,10,10" Text="" TextWrapping="Wrap" /> <WebView Grid.Column="1" x:Name="webview" Margin="10,10,10,10"/> </Grid> </Page> MainPage.xaml.cs using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading; using System.Threading.Tasks; using Windows.Foundation.Metadata; using Windows.UI.Notifications; using Windows.UI.Notifications.Management; using Windows.UI.Xaml.Controls; namespace AmazonMusicObserver { public sealed partial class MainPage : Page { //ターゲットとするAppInfo.AppUserModelId const string TGTID = "Amazon.Music"; //コントロールアクセス用オブジェクト static TextBox logTB; static WebView webView; public MainPage() { this.InitializeComponent(); logTB = this.FindName("logTextBox") as TextBox; webView = this.FindName("webview") as WebView; MainAsync(); } /// <summary> /// 通知一覧から、対象外であるAppInfo.AppUserModelIdを持つ通知を除去する。 /// また、利便性のため最新通知が先頭へ来るように逆順ソートする。 /// </summary> /// <param name="origin">加工前の通知一覧</param> /// <returns>加工後の通知一覧</returns> static List<UserNotification> removeNonTgtNotification(IReadOnlyList<UserNotification> origin) { List<UserNotification> temp = new List<UserNotification>(); foreach (var item in origin.ToArray()) { if (item.AppInfo.AppUserModelId.Equals(TGTID)) { temp.Add(item); } } temp.Reverse(); return temp; } /// <summary> /// AmazonMusicからの通知であることを前提に、通知のテキスト要素を受け取って解析し画面等の更新処理を走らせる /// </summary> /// <param name="temp">通知のテキスト要素</param> static void updateWork(IReadOnlyList<AdaptiveNotificationText> temp) { try { logTB.Text += "\nタイトル:" + temp[0].Text; //2021年11月現在、楽曲のタイトルが格納されている logTB.Text += "\nアーティスト:" + temp[1].Text; //2021年11月現在、楽曲のアーティスト名が格納されている logTB.Text += "\nアルバム:" + temp[2].Text; //2021年11月現在、楽曲のアルバム名が格納されている logTB.Text += "\n-------"; Debug.WriteLine(temp[0].Text); Debug.WriteLine(temp[1].Text); Debug.WriteLine(temp[2].Text); //通知情報から画像情報が取得できなかったのでAmazonから無理矢理取得する。 //まずは通知から取得した楽曲情報を使って検索結果ページのリンクを生成。 Uri storeSearchResultPageURI = createStoreSearchResultPageURI(temp); //検索結果ページのリンクからソースを取得、そこから1件目のサムネイル用リンクを取得 Uri imgURI = createImgURI( new WebClient().DownloadString(storeSearchResultPageURI)); //webビューアでサムネイル用のリンク先を表示 webView.Navigate(imgURI); } catch (Exception e) { logTB.Text += "\n" + e.Message; } } static Uri createStoreSearchResultPageURI(IReadOnlyList<AdaptiveNotificationText> temp) { //2021年11月現在、取得した楽曲情報からデジタルミュージックに限定したAmazonMusic検索結果ページURLを下記コードで生成可能 string paramStr = temp[0].Text + " " + temp[1].Text + " " + temp[2].Text; return new Uri("https://www.amazon.co.jp/s?k=" + System.Web.HttpUtility.UrlEncode(paramStr) + "&i=digital-music"); } /// <summary> /// 受け取ったページソースから画像リンクを生成 /// </summary> /// <param name="temp">ページソース</param> /// <returns>画像リンク</returns> static Uri createImgURI(string temp) { //2021年11月現在、AmazonMusic検索結果ページから取得したソースより下記コードで検索結果1件目のサムネイル用リンクを最大(3倍)サイズで取得可能 temp = temp.Substring(temp.IndexOf("2.5x,"),temp.IndexOf("3x\"")); temp = temp.Split(" 3x\"",StringSplitOptions.None)[0]; temp = temp.Split(", ", StringSplitOptions.None)[1]; Debug.WriteLine(temp); return new Uri(temp); } //AsyncなMain private static async Task<int> MainAsync() { ////////////////////////////////////////////////////// //通知リスナーにアクセスできるかどうかの諸々のチェック ////////////////////////////////////////////////////// if (!ApiInformation.IsTypePresent("Windows.UI.Notifications.Management.UserNotificationListener")) { Debug.WriteLine("IsTypePresent: NG"); return -1; } Debug.WriteLine("IsTypePresent: OK"); UserNotificationListener listener = UserNotificationListener.Current; Debug.WriteLine("listener: "); Debug.WriteLine(listener); UserNotificationListenerAccessStatus accessStatus = await listener.RequestAccessAsync(); Debug.WriteLine("accessStatus: "); Debug.WriteLine(accessStatus); if (accessStatus != UserNotificationListenerAccessStatus.Allowed) { Debug.WriteLine("アクセス拒否"); return -1; } Debug.WriteLine("アクセス許可"); ////////////////////////////////////////////////////// //通知リスナーにアクセスできるかどうかの諸々のチェックここまで ////////////////////////////////////////////////////// //初回の比較用に通知の履歴を取得 IReadOnlyList<UserNotification> oldList = await listener.GetNotificationsAsync(NotificationKinds.Toast); //無関係な要らん通知を除去しつつ逆順ソートで成形 oldList = removeNonTgtNotification(oldList); //通知の一覧を取得できるだけで、良い感じのイベントも差分を取得するメソッドも存在しないらしい。 //ので、ひたすら通知を取得&比較し続けて自力で差分を見付ける while (true) { IReadOnlyList<UserNotification> notifs = await listener.GetNotificationsAsync(NotificationKinds.Toast); //無関係な要らん通知を除去しつつ逆順ソートで成形 notifs = removeNonTgtNotification(notifs); //今回取得した通知一覧と前回取得した通知一覧を比較、それぞれの最新同士でタイムスタンプを比較して差分の有無を確認 if (DateTimeOffset.Compare(notifs[0].CreationTime, oldList[0].CreationTime) > 0) { //差分あり。最新通知のオブジェクトを取得 NotificationBinding toastBinding = notifs[0].Notification.Visual.GetBinding(KnownNotificationBindings.ToastGeneric); if (toastBinding != null) { //テキスト要素を取得して更新処理に渡す IReadOnlyList<AdaptiveNotificationText> textElements = toastBinding.GetTextElements(); updateWork(textElements); } } else { Debug.WriteLine("更新無し"); } //比較用リストを更新 oldList = notifs; Thread.Sleep(1000); } } } } 今後の展開 それなりに問題を抱えながらも一応取得自体はできたので、あとはこれらをDBに投げ込めばPHPで良い感じに別端末でもweb表示できるようになります。 根本的に解決したい場合 モバイル版のAmazonMusicを使う なうぷれ系のアプリを見ると、同じく通知領域を経由して再生中楽曲の情報を画像込みでしっかり取得できています。 操作はPCがいい、という場合もエミュなりリモートなりでなんとかなりそうです。 APIが公開されているサブスクに乗り換える 詳しくは見ていませんが、SpotifyはAPIが充実しているっぽいです。 ディスプレイ付きのスマートスピーカーを導入する PCの画面を占有しません。 Echo Show、同アカウント別端末で再生中の楽曲情報を表示する機能あったりしないかな……無いよな……。 おしまい 以上、お疲れ様でした! 参考 音楽系API(主に音楽配信サービス)まとめ Windowsの通知センターの内容を取得する(通知リスナー)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】WebアプリケーションのレイアウトはCSS Grid Layoutで決まり!

はじめに この記事ではC#+WPF開発者向けにCSS Grid Layoutを紹介します。C#開発者以外のみなさんごめんなさい。 Webアプリケーションのレイアウト、どうするの? 今までデスクトップアプリケーション開発しかしてこなかったC#erのみなさんがある日突然Webアプリケーションを開発することになったときにぶつかる問題、それがレイアウト。 WPFならできるんじゃ~俺にWPFを返せ! と発狂?したくなります(なりました)。そこでおすすめしたいのがCSS Grid Layout。 CSS Grid Layoutとは? おおよそWPFの Grid です。説明終わり。 ちゃんと説明すると、割と新しめのCSSでのレイアウト手法です。比率を含むような様々な範囲で幅・高さを指定して要素を分割できます。WPF(C#のデスクトップアプリケーション用フレームワーク)に近い使用感ということで、従来のWebページというよりはSPAなどのモダンなWebアプリケーションの開発に向いていると思います。 使ってみよう Blazor WebAssemblyでWebアプリケーションを作成中の方へ Blazor WebAssemblyの初期のテンプレートでは自動的にFlexboxというレイアウト手法が適用されており、競合しますのでまずそちらを解除します。 SharedディレクトリのMainLayout.razorを開きます。以下の例は.NET6の場合です。.NETのバージョンにより多少違いがあります。 MainLayout.razor @inherits LayoutComponentBase <div class="page"> <div class="sidebar"> <NavMenu /> </div> <main> <article class="content px-4"> @Body </article> </main> </div> Article タグのclass属性の値、content px-4 をBootStrapが認識し勝手にFlexboxレイアウトにされるのでclass属性ごと消します。 (ここまでBlazor WebAssemblyの方のみ) まず、ページの最上位に適当な要素( div )を配置し、IDをつけます。 次に、設定したIDに対してCSSを当てて、Grid Layoutを設定します。 index.razor @page "/" <style> #primaryEditorContainer { display: grid; width: 100%; height: 100vh; max-height: 100vh; margin: 0px; padding: 6px 12px; } </style> <div id="primaryEditorContainer"> </div> display:grid でCSS Grid Layoutになります。 一般にはWebページのレイアウトは無限に長くできる「ページ」のようなものがあり、それに向かってレイアウトするイメージです。 しかし、Webアプリケーションではそのような無限に長くなるページではなく、有限の大きさのデバイスのスクリーンに向かってレイアウトしたいでしょう。 width: 100%; height: 100vh; max-height: 100vh; vw vh はデバイスのスクリーンの大きさを基準とした%単位の大きさです。100vhと記述することでスクリーンの大きさと同じ大きさになります。 この例では横方向は(ナビゲーションバーが入るので)厳密にスクリーンとは一致させていません。 ここまでの設定により、最上位として設定した div 要素はちょうど、WPFのテンプレートで最上位にある Grid のように振る舞います。 子要素を割り付ける レイアウトの定義 子要素の大きさの設定方法を説明します。WPFならば MainWindows.xaml <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> </Grid> ですが、CSSでは #primaryEditorContainer { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; } のように、grid-template-{なんちゃら}を使います。 frは見慣れない単位ですが、比率を表すものです。 WPF CSS 1* 1fr auto auto 150px 150px と、単位系はほぼwpfのGridと互換です。 子要素の配置 子要素側の設定は、WPFの場合 MainWindow.xaml <Grid Grid.Row="0"> ですが、 Index.Razor <div style="grid-row:1"> となります。今回はWPFぽくインラインで設定しましたが、適当なIDを設定して一括で当てても構いません。 インデックスの番号は1始まりです。 あくまでCSSですので、RowとColumnを同時に指定したければ;で区切って同時に詰め込んでOKです。 MainWindow.xaml <Grid Grid.Row="0" Grid.Column="0"> Index.Razor <div style="grid-row:1;grid-column:1;"> 要素のはみ出し さて、Gridで指定した大きさより子要素の実際の大きさのほうが大きかった場合何が起こるでしょうか。 WPFの場合、はみ出した分が単純に描画されません。その代わり、全体のレイアウトは保たれます。 一方、CSS Grid Layoutでは親要素が勝手に大きくなります。すべてのコンテンツが見える代わりに、レイアウトが崩壊します。 height:100vh指定でも平気で画面以上のサイズになります。 はみ出し方を調整するには overflow-{x or y}:{options}を子要素に指定します。例えばoverflow-y:scrollと指定すると縦方向にはみ出しそうになったときにスクロールバーが表示され、はみ出さなくなります。{options}の部分に書けるのは、およそhidden(はみ出した分は描画しない,WPFの挙動に近い) scroll(スクロールバーを出す) visible(はみ出す)の3通りです。完全な説明はドキュメント を参照してください。 上付き・下付き 高さがautoまたは固定長(150px,20vhなど)の要素と高さが比率(1frなど)の要素を混在させると上付き・下付きの要素(上下に固定して表示される要素)が作成できます。 WPFと同じ仕様ですが、実際のWebアプリケーション開発では結構役に立つと思います。 要素を重ねる 同じRowかつColumnの要素を複数用意すると重なります。基本的にWPFと同じです。 子要素にz-indexをcssで指定すると重ねる順番を変えられます。レイアウトの自由度が広がる便利な機能です。 その他 WPFのGridSplitterに相当するものはないようです。 実例 手前味噌ですが、私が開発中のBlazor WebAssemblyを使用したC#開発環境での例をご紹介します。 ロジック部分は省略しますのでお察しください。 現時点での内容と変わらないことは保証できませんがここに実物があります。 CodeEdit.razor @using System; @using System.Collections.Generic; @using System.Linq; @using UiLogics; @using WorkerConnection; @inject HttpClient Http; @inject IJSRuntime JS; @inject CompileQueueService CompileQueue; @page "/CodeEdit" <style> #primaryEditorContainer { display: grid; width: 100%; height: 100vh; max-height: 100vh; grid-template-columns: 1fr; grid-template-rows: 1fr repeat(3,auto) 20vh; margin: 0px; padding: 6px 12px; } .width-matchParent { width: 100% } .tab-active { background-color: white } .header-nonselected { color: GrayText } .tab-content { } .visible { visibility: visible } .hidden { visibility: hidden } </style> <div id="primaryEditorContainer"> <div style="grid-row:1;overflow-y:scroll;"> <WasmCsTest.Components.CodeEditor @bind-UserCode="userCode" @bind-UserCode:event="OnChanged" CodeEditorContext="CodeEditorContext" /> </div> <div style="grid-row:2;"> @if (IsCompiling) { <button class="width-matchParent btn btn-outline-primary">コンパイル取消(未実装)</button> } else if (IsRunning) { <button class="width-matchParent btn btn-outline-primary">プログラム強制終了(未実装)</button> } else { <button class="width-matchParent btn btn-primary" @onclick="OnRunButtonClicked">実行</button> } </div> <div style="grid-row:3;"> <hr /> </div> <div style="grid-row:4;"> <ul class="nav nav-tabs"> <li class="nav-item"> <button class=@tab0.HeaderClassString @onclick="tab0.OnHeaderClick">実行結果</button> </li> <li class="nav-item"> <button class=@tab1.HeaderClassString @onclick="tab1.OnHeaderClick">コンソール</button> </li> </ul> </div> <div class="@tab0.ContentClassString" style="grid-column:1; grid-row:5; padding:6px;"> <WasmCsTest.Components.CompileResult CodeEditorContext="CodeEditorContext" /> </div> <div class="@tab1.ContentClassString" style="grid-column:1; grid-row:5;"> <WasmCsTest.Components.VirtualConsole CodeEditorContext="CodeEditorContext" /> </div> </div> 行 内容 大きさ 1 コードエディタ 1fr(残り全部) 2 実行ボタン auto 3 境界線(hr) auto 4 タブ切り替えボタン auto 5 コンパイラメッセージ 20vh 5 コンソール 20vh と設計されています。5行目のタブ部分は、class属性の内容をC#コードとバインディングして、visibilityを書き換えることで擬似的にタブを実装しています。 同じrow と column を指定して重ねています。重ねる場合は両方明示的に指定しないと重なりません。 1行目が1fr指定なので他の要素は下付きになります。 さいごに WPFのグリッドが直線的なレイアウトならほぼ表現できるくらい強力なように、CSS Grid Layoutも非常に強力です。WPFと割と似ているのもC#開発者的には楽なポイントでしょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラム初心者勉強会 8回目 -C# Windowsフォーム入門

目次 1. 前回リンク 2. 次回リンク 3. やったこと 4. 配信時の録画 5. 今回学んだこと 6. メインメニューの作成方法 7. コンテキストメニューの作成方法 8. ツールバーの作成方法 9. 各ボタンのショートカットキーやアクセラレートキーについて 10. 振り返り 11. 次回予告 1. 前回リンク 2. 次回リンク まだ 3. やったこと 下のサイトの 11. メニュー (従来版)を学習 4. 配信時の録画 5. 今回学んだこと メニューの作成方法が分かってよかった。今回娘がぐずって早めに終わっちゃったので、次は勉強頑張らなきゃだね 6. メインメニューの作成方法 メインメニューもツールボックスを作成するためのコントロールが用意されています。 6.1 メインメニューの追加方法  1. 「MenuStrip」をデザイン画面にDrag&Dropする  2. MenuStripのプロパティ内「items」の編集画面を開く    3.  メニューのエディター窓を開いたら、「追加(A)」(画像の③)を押し、メニュー項目を増やす。    4.  各メニューの名前は、画像④のプロパティメニュー「Text」から編集する。メニュー名の変数名は「デザイン->(Name)」を編集する    5. 上の画像④のプロパティ「DropDownItems」から、各メニューにぶら下がるDropDownメニューの編集窓を開く。ここでメニューの追加も名前の編集も可能。   こんな感じでメニューの編集が可能となります。 6.2 メニュのイベント追加方法 6.1章で追加したメニューの内「閉じる(X)」ボタンの編集を行います。  1. 「閉じる(X)」をダブルクリックし、「閉じる(X)」ボタンのイベントハンドラ関数を作成する  2. イベントハンドラ関数内に「this.Close()」を書き込む  3. デバック起動し、「閉じる(X)」ボタンを押してアプリが終了することを確認する  「閉じる(X)」ボタンのイベントハンドラ関数 private void toolStripMenuItem2_Click(object sender, EventArgs e) { this.Close(); }   こんな感じで各メニューボタンのイベントを登録します。 7. コンテキストメニューの作成方法 7.1 コンテキストメニューの追加方法  1. 「ContextMenuStrip」をデザイン画面にDrag&Dropする。  2. 2以降はメインメニューと同じ手順で編集すればOK。  3. ここ重要! Windowsフォーム画面のプロパティ項目「ContextMenuStrip」に、作成した「ContextMenuStrip」を登録する。    これしないと右クリックしてもメニューが表示されない!   8. ツールバーの作成方法 8.1 ツールバーの追加方法  1. 「ToolStrip」をデザイン画面にDrag&Dropする。  2. 「Button」メニューを選択する。するとアイコンメニューが作成される  3. 作成されたアイコンメニューをダブルクリックすれば、クリックイベントハンドラが作成される。ほかのイベント作成したいならプロパティメニュの稲妻マークから作成可能  4. その後はメインメニュー等と同じ 8.2 ツールバーのメニューアイコンを編集 プロパティから変更可能。 「image」からローカルファイルを選択すれば画像変更可能。 サイズは「ImageScaling」で”None”にすれば画像のサイズそのまま、”SizeToFit”なら小さく表示されます。 9. 各ボタンのショートカットキーやアクセラレートキーについて 下のページに色々書いてた。 各メニューボタンの名前横に(&"アルファベット")と書くことで、ショートカットキーやアクセラレートキーを付与することが出来る。 10. 振り返り やっぱり後半になるにしたがって実業務に近いことが学べるようになったね! 11. 次回予告 次回はメニューバーやオーナードロー?ってのを学ぶよ。 第7回は11月28日(日)の夜8時から以下チャンネルで行います! ニコニコ配信先 Twitch配信先 Discordリンク
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ファイルの比較(C# )

概要 Windows環境でファイルのリリース作業をする際に最新ファイルのみ抽出する比較コマンドを作成したいと思います。 事前準備 比較する新しいファイルと既存ファイルをAとBのディレクトリにそれぞれコピーして置きます。 ソース 下記のソースをVSにコピーペーストしましょう。 using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; namespace filelist { class Program { static readonly HashAlgorithm hashProvider = new SHA1CryptoServiceProvider(); /// <summary> /// Returns the hash string for the file. /// </summary> /// <param name="filePath"></param> /// <returns></returns> public static string ComputeFileHash(string filePath) { var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); var bs = hashProvider.ComputeHash(fs); return BitConverter.ToString(bs).ToLower().Replace("-", ""); } static void Main(string[] args) { try { if(args.Length>0 && args[0].ToString().ToUpper() == "-D") { dcomp(args); } else { fList(args); } } catch (Exception e) { Console.WriteLine(e.Message); } } static void dcomp(string[] args) { var srcList = new List<fileObj>(); var dstList = new List<fileObj>(); var resultList = new List<fileObj>(); //source directory if (Directory.Exists(args[1].ToString())) { var srcfile = Directory.EnumerateFiles(args[1].ToString(), "*.*", System.IO.SearchOption.AllDirectories); StreamWriter sw1 = new StreamWriter(@".\srcfile.txt"); sw1.WriteLine("UpdateTime\tFileName"); foreach (string f in srcfile) { fileObj fobj = new fileObj() { filename=f, filehash=ComputeFileHash(f), updateTime=File.GetLastAccessTime(f) }; sw1.WriteLine(fobj.updateTime + "\t" + fobj.filename); srcList.Add(fobj); } sw1.Close(); } //target directory if (Directory.Exists(args[2].ToString())) { var dstfile = Directory.EnumerateFiles(args[2].ToString(), "*.*", System.IO.SearchOption.AllDirectories); StreamWriter sw2 = new StreamWriter(@".\dstfile.txt"); sw2.WriteLine("UpdateTime\tFileName"); foreach (string f in dstfile) { fileObj fobj = new fileObj() { filename = f, filehash = ComputeFileHash(f), updateTime = File.GetLastAccessTime(f) }; sw2.WriteLine(fobj.updateTime + "\t" + fobj.filename); dstList.Add(fobj); } sw2.Close(); } //compare foreach(fileObj srcobj in srcList) { string srcfname = Path.GetFileName(srcobj.filename); fileObj result = new fileObj(); foreach (fileObj dstobj in dstList) { string dstfname = Path.GetFileName(dstobj.filename); if (srcfname == dstfname && srcobj.updateTime > dstobj.updateTime) { Console.WriteLine("[new]" + "\t" + srcobj.filename); //SRCが最新 result.filename = "[new]" + srcobj.filename; result.filehash = srcobj.filehash; result.updateTime = srcobj.updateTime; resultList.Add(result); break; } if (srcfname == dstfname && srcobj.updateTime < dstobj.updateTime) { Console.WriteLine("[new]" + dstobj.filename); //SRCが最新 result.filename = "[new]" + dstobj.filename; result.filehash = dstobj.filehash; result.updateTime = dstobj.updateTime; resultList.Add(result); break; } } if(result.filename == null) { //DSTにないものを追加 Console.WriteLine("[add]" +"\t" + srcobj.filename); result.filename = "[add]"+srcobj.filename; result.filehash = srcobj.filehash; result.updateTime = srcobj.updateTime; resultList.Add(result); } } StreamWriter sw = new StreamWriter(@".\result.txt"); foreach (fileObj fobj in resultList) { sw.WriteLine(fobj.updateTime + "\t" + fobj.filename); } sw.Close(); } static void fList(string[] args) { string path1 = @".\"; var filename = Directory.EnumerateFiles(path1, "*.*", System.IO.SearchOption.AllDirectories); StreamWriter sw = new StreamWriter(@".\filelist.txt"); foreach (string f in filename) { if (args.Length == 1 && args[0].ToString().ToUpper() == "-H") { Console.WriteLine(ComputeFileHash(f) + " " + Path.GetFileName(f)); sw.WriteLine(ComputeFileHash(f) + " " + Path.GetFileName(f)); } else { Console.WriteLine(Path.GetFileName(f)); sw.WriteLine(Path.GetFileName(f)); } } sw.Close(); } } public class fileObj { public string filename; public string filehash; public DateTime updateTime; } } コマンドのオプション -h :現在ディレクトリの配下にあるファイル一覧とhash値を表示 -h(なし):現在ディレクトリの配下にあるファイル一覧を表示 -d:二つのディレクトリに存在するファイルを比較し、内容を表示 ※-dで表示される[new]が最新、[add]は追加されたファイルです。 コマンドの実行結果 実行するコマンドと比較するディレクトリを指定します。 SRC→DSTディレクトリを比較することになります。 コマンド例) filelist.exe -d ./a ./b ※aのディレクトリに入っているファイルとbのディレクトリに入っているファイルを比較し、結果をコンソールに表示およびresult.txtに保存しています。 #aのファイルをbのファイルと比較。bのファイルが最新でnewと表示されます。 C:\repos\filelist\filelist\bin\Debug>filelist.exe -d ./a ./b [add] ./a\App.config [new]./b\file.txt [add] ./a\filelist.csproj [new]./b\filelist.exe [new]./b\filelist.exe.config [new]./b\filelist.pdb [add] ./a\packages.config [add] ./a\Program.cs [add] ./a\obj\Debug\DesignTimeResolveAssemblyReferencesInput.cache [add] ./a\obj\Debug\filelist.csproj.CoreCompileInputs.cache [add] ./a\obj\Debug\filelist.csproj.FileListAbsolute.txt [new]./b\filelist.exe [new]./b\filelist.pdb [add] ./a\Properties\AssemblyInfo.cs #bのファイルをaのファイルと比較。bのファイルに追加されたファイルがあります。 C:\repos\filelist\filelist\bin\Debug>filelist.exe -d ./b ./a [add] ./b\ddst.txt [new] ./b\file.txt [new] ./b\filelist.exe [new] ./b\filelist.exe.config [new] ./b\filelist.pdb 実行結果フォルダに生成されたテキストファイル srcfile.txtファイルの内容 dstfile.txtファイルの内容 result.txtファイルの内容 実行結果(オプションなし) 実行ファイルをこのまま実行すると配下に存在するすべてのファイル一覧が表示されfileslist.txtに保存されます。 C:\repos\filelist\filelist\bin\Debug>filelist.exe dstfile.txt filelist.exe filelist.exe.config filelist.pdb filelist.txt result.txt srcfile.txt a\App.config a\file.txt a\filelist.csproj a\filelist.exe a\filelist.exe.config a\filelist.pdb a\packages.config a\Program.cs a\obj\Debug\DesignTimeResolveAssemblyReferencesInput.cache a\obj\Debug\filelist.csproj.CoreCompileInputs.cache a\obj\Debug\filelist.csproj.FileListAbsolute.txt a\obj\Debug\filelist.exe a\obj\Debug\filelist.pdb a\Properties\AssemblyInfo.cs b\ddst.txt b\file.txt b\filelist.exe b\filelist.exe.config b\filelist.pdb 保存されたfilelist.txtの内容です。 実行結果(-h) -hオプションを入れると現在ディレクトリの配下にファイルすべてのファイルのhash値も一緒に表示してfilelist.txtに保存されます。 C:\repos\filelist\filelist\bin\Debug>filelist.exe -h c9424476c1f21db28a78037991495f27542f6996 dstfile.txt 1d6917bb159a8209cbd00ce167b42c895c7af40c filelist.exe 2711de49785aba673df043f543686685e3b53e73 filelist.exe.config 3638b6a09b197a11d74d2ac75f022c9220a60cbd filelist.pdb da39a3ee5e6b4b0d3255bfef95601890afd80709 filelist.txt d51c8a74bc93a6bcf172a622577cbba94243f0e7 result.txt aa37458a0f5d24af2bd272357c05e84a4779a54f srcfile.txt 2711de49785aba673df043f543686685e3b53e73 a\App.config 620106eba2a07b032917b5f2576d48ffb2d98503 a\file.txt 438ec6715c8704daa65908e929808ecf945cc70e a\filelist.csproj 7337afd3b69934139e71f0470106c9aadbe595c6 a\filelist.exe 2711de49785aba673df043f543686685e3b53e73 a\filelist.exe.config 4c8c7058cf33e07891bf71329a98c9360ff6eb1a a\filelist.pdb 4dfa86ebeaad9070e2a981004942ba68090ce368 a\packages.config 139b02206cbd12ece9cd62aa50b4be4c18ef3697 a\Program.cs 5ff5492237512ea3dc836a309bcdb3182e97c702 a\obj\Debug\DesignTimeResolveAssemblyReferencesInput.cache 6d5a1db8b80683c8e82601af7b0679bb8338eb7e a\obj\Debug\filelist.csproj.CoreCompileInputs.cache 28afd52d819d8f9d9ab07c96580d659a582caa8e a\obj\Debug\filelist.csproj.FileListAbsolute.txt 7337afd3b69934139e71f0470106c9aadbe595c6 a\obj\Debug\filelist.exe 4c8c7058cf33e07891bf71329a98c9360ff6eb1a a\obj\Debug\filelist.pdb 41525e3927742a96f9f2edca06ecc0481a28f553 a\Properties\AssemblyInfo.cs da39a3ee5e6b4b0d3255bfef95601890afd80709 b\ddst.txt 620106eba2a07b032917b5f2576d48ffb2d98503 b\file.txt 4dc3645a1daf449cccf3c0dfa6a88d63cb0fd511 b\filelist.exe 2711de49785aba673df043f543686685e3b53e73 b\filelist.exe.config 8673638c7999ba1876adffd361c64cb6dcafbe6f b\filelist.pdb 保存されたファイルの内容です。 終わりに ソースの中ではhash値も入れてみましたが、結果的にファイルの更新日時で最新を確認ができるので利用はしていないです。 悪用で更新日時を更新するとhash値は変更されるんですかね。 これを利用してファイルのリリース作業に役に立つとよいですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データレコード形式のテストデータを生成するツールを公開しました。

このドキュメントの内容 データレコード形式のテストデータを生成するツール mxDataFountain を紹介します。 ツールの概要 先日公開したライブラリを使用しています。 生成するデータの定義をGUIで入力することができます。入力した定義を mxProject.Devs.DataGenerator が提供する構成ファイルの形式で出力することができます。 スクリーンショット 使用方法 フィールドの追加 Field, TupleField, AdditionalField, AdditionalTupleField ノードのコンテキストメニューを表示してください。右側に表示されるエディタにフィールドの定義を入力して Apply ボタンを押すと、フィールドが追加されます。 DirectProduct フィールドの場合、DirectProduct ノードのコンテキストメニューから子フィールドを追加してください。 フィールドの編集 ツリービューで選択されたフィールドの型に対応したエディタが表示されます。フィールドの定義を入力して Apply ボタンを押すと、フィールドが更新されます。 Field, TupleField の場合、Preview Field ボタンで入力中のフィールドから生成される値をプレビューすることができます。 構成ファイルの読込と保存 File メニューでは構成ファイルの読込と保存を行うことができます。 メニュー 説明 New Project 入力されている内容をクリアして新規プロジェクトの入力を開始します。 Load ProjectSettings mxDataFountain プロジェクトファイルを読み込みます。 Load DataGeneratorSettings mxProject.Devs.DataGenerator の DataGeneratorSettings に対応した JSON ファイルファイルを読み込みます。 Save as ProjectSettings 入力されている内容を mxDataFountain プロジェクトファイルとして保存します。 Save as DataGeneratorSettings 入力されている内容を mxProject.Devs.DataGenerator の DataGeneratorSettings に対応した JSON ファイルファイルとして保存します。 プレビュー Preview ボタンをクリックすると、プレビューダイアログが表示されます。100 件のデータが生成されます。任意のデータ件数を入力して生成することもできます。 CSVファイル出力 CSV ボタンをクリックすると、CSV出力ダイアログが表示されます。ファイルパスと生成するデータ件数を入力します。 CSV出力に関する動作設定を変更するには、Settings > CsvSettings メニューから設定画面を表示してください。 任意のライブラリの読込 任意のライブラリに定義された値型の値を生成したい場合、そのライブラリを参照アセンブリリストに追加してください。Settings > ExecutorSetup メニューから設定画面を表示してください。 この設定画面では、データ生成に使用されるコンテキストクラスをカスタマイズすることもできます。mxProject.Devs.DataGenerator の IDataGeneratorContextActivator インターフェースを実装した型を定義し、その型名をここに入力してください。 動作要件 フレームワーク .NET Framework 4.7.2 利用している主なパッケージ mxProject.Devs.DataGenerator (>= 0.8.2) Newtonsoft.Json (>= 12.0.3) JsonSubTypes (>= 1.7.0) Microsoft.CodeAnalysis.CSharp.Scripting (>= 3.7.0) CsvHelper (>= 27.1.1) MessagePipe (>= 1.6.1) Microsoft.Extensions.DependencyInjection (>= 5.0.2) インストール .NET Framework 4.7.2 ランタイムがインストールされていない場合は、マイクロソフトのWEBサイトなどから入手してインストールしてください。 このリポジトリのリリースページから圧縮ファイルをダウンロードし、任意のフォルダに展開してください。アプリケーションの実行ファイルは mxDataFountain.Net472.exe です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自作液晶コントローラで動画再生(階調表示編)

はじめに こちらは鈴鹿高専 Advent Calendar 2021の3日目の記事です. 注意事項としては,ここで記載されているコードを他のプロダクトでそのまま使うのは禁止とします. 何かバグがあっても責任を取れないためです. また,実際に何かを制作する際には必ず一次情報(データシートなど)を参考にしてください. 作ったもの 自作液晶コントローラを利用して3階調で動画を再生できるようにしました. 概念 このデジット液晶には(S)TN液晶が使用されています. みなさんのパソコンの画面は多くがTFT液晶だと思いますが,TN液晶は反応速度が遅く残像が残ります. 今回はこの残像を利用し,明るさに応じて点灯,消灯を制御し階調表示を実現しました. 下にイメージを示します. 出典:ELM(elm-chan.org) 改修 コントローラ 従来の独自フォーマット4bit通信では明らかに速度が不足(必要な速度の半分程度)しているので通信フォーマットをより乱暴で速い手法に変更しました. ここでもフォーマットの詳細は述べませんがコレにより単色表示時には約90fps相当の高速通信が可能になりました. また,以前より高いフレームレートで駆動(単純に3倍の速度)させる必要があるため,液晶の表示更新まわりも高速化しました. データシートの上限を遥かに上回っていますが,動作に問題がないことを確認しています. マージンを食い潰すのは本当は良くないのでやめましょう(汗 無論,代償が伴いました.90fps表示をさせた際に走査線のようなものがうっすら見えてしまうようになりましたが問題なく見えるレベルなので無視します. 若干動作を高速化させるためにクロック供給まわりも変更,下のようになりました. ホストソフトウェア 今回もC#でソフトウェアを作成しました. 大きな変更点としてはディザリング処理をやめ,階調表示用のデータを生成するようにしました. また,これまでは横着してシングルスレッドで処理していた変換をマルチスレッド対応にし単純にスレッド数倍の速度で処理が可能になりました. GUIは個人的に使いにくかったためコンソールで操作します. そして自分しか使わないのにUsageなんかもちゃんと書いています. 開発環境もWin7+VisualStudio 2015に変更しましたが特に理由はありません. 例によってmp4を入力するだけで全自動でデータ生成から再生までやってくれる優れものです. また,前回までは全てを表示したかったため縦方向に合わせてかなり小さい画面でしたが,見切れることを受け入れて全画面表示をしています. 以下に画像の変換処理を示します. static void createData() { separateMovie(); total = Directory.GetFiles(Path.GetFileNameWithoutExtension(inputFilePath), "*.png",SearchOption.TopDirectoryOnly).Length * 3; byte[] tmp = new byte[1920]; buffers.AddRange(Enumerable.Repeat(tmp, total)); object lockBmp = new object(); object lockBuffer = new object(); object lockCurrent = new object(); Parallel.For(0, total, frame => { Bitmap bitmap; int bitpos = 0; int bytepos = 0; byte data = 0x00; lock (lockBmp) { bitmap = new Bitmap(Path.GetFileNameWithoutExtension(inputFilePath) + "/image_" +((frame / 3)+1).ToString("D4") + ".png"); } byte[] pixels = new byte[1920]; for (int y = 0; y < 64; y++) { for (int x = 0; x < 240; x++) { data <<= 1; data |= (0.4 + 0.20 * (frame % 3) >= bitmap.GetPixel(x, y).GetBrightness()) ? (byte)1 : (byte)0; bitpos++; if(bitpos == 8) { pixels[bytepos++] = (data == 0x7F) ? (byte)0xFF : data; bitpos = 0; } } } bitmap.Dispose(); lock (lockBuffer) { buffers[frame] = pixels; } lock (lockCurrent) { current++; if(current % 500 == 0) { showPercentage(); } } }); System.IO.FileStream fs = new System.IO.FileStream(outputFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write); for (int index = 0; index < buffers.Count; index++) { fs.Write(buffers[index], 0,1920); } fs.Close(); } 単純にピクセルごとの明るさをもとにデータを作成しています. 並列処理に関してもC#のParalellを使用することにより容易に実現ができました. テスト うまく動いていますね. 終わりに ジャンク部品でここまで楽しめるとは思っていませんでした. データを生成するのは結局書いたプログラムなので,予想以上に綺麗に表示されて自分でも驚いています.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む