- 投稿日:2020-07-22T23:12:04+09:00
【Xamarin.Forms】C#でQRコードを読み取り、ブラウザを起動させる
はじめに
Xamarin.FormsでQRコード読み取り機能を実装してみたので、メモしておく。
環境・前提条件
Visual Studio 2019 V16.6.3(C#)
Xamarin.Forms V4.5.0.617
準備するもの
ZXing(Zebra Crossing ゼブラクロッシングと読むらしい) というバーコード読み取りのライブラリがあるみたいなので、これを使っていきます。
事前準備として、Nugetで下記の3つをインストール。
- ZXing.Net.Mobile
- ZXing.Net.Mobile.Forms
- Xamarin.EssentialsZxingパッケージ
上2つは先述したバーコード読み取り。片方だけだと動いてくれないので、両方必要です。
名前の似たようなものがたくさんあるので注意してください。Xamarin.Essentials
3つ目はAndroid、iOS、UWP各プラットフォームの固有APIを利用できるライブラリになります。
プラットフォーム依存のAPI(位置情報とか各種センサー等)を呼び出すコードを共通化してくれる優れものです。
この中に、アプリケーション内からWebリンクを開けるBrowserクラスが入ってます。サポートは下記参照
Android 4.4 (API 19) 以上
iOS 10.0 以上
UWP 10.0.16299.0 以上Visual Studio 2019からはプロジェクト作成時にインストールされているみたいなのですが、
僕のところには何故か入ってなかったので一応。
作り方
以下の手順で作成していきます。
1. Manifest設定
2. Appクラスの修正
3. 初期画面を作る
4. コードビハインドに、ZXingのイベントハンドラを作成
5. QRコードからの取得データをBrowserクラスで開く
Manifest設定
プロジェクトのプロパティの[Android Manifest]から [Required permissions]で
"CAMERA" と "FLASHLIGHT"の権限を取っておきます。
または、AndroidManifest.xmlに直接追加することもできます。以下を manifestタグの中に入れてやってください。AndroidManifest.xml<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.FLASHLIGHT" />
Appクラスの修正
NavigationPageをMainPageに設定します。
これを行うことで、ナビゲーションスタックに、ルートページとなる初期画面がプッシュされます。
App.xaml.csusing System; using Xamarin.Forms; using Xamarin.Forms.Xaml; namespace XamarinApp { public partial class App : Application { public App() { InitializeComponent(); MainPage = new NavigationPage(new MainPage(){}); } } }
iOSの初期設定
Xamarin.Formsの初期化処理であるglobal::Xamarin.Forms.Forms.Init();の後ろに、
global::ZXing.Net.Mobile.Forms.iOS.Platform.Init();
という、ZXingの初期化処理を入れてあげます。AppDelegate.csusing System; using System.Collections.Generic; using System.Linq; using Foundation; using UIKit; namespace XamarinApp.iOS { [Register("AppDelegate")] public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate { public override bool FinishedLaunching(UIApplication app, NSDictionary options) { global::Xamarin.Forms.Forms.Init(); global::ZXing.Net.Mobile.Forms.iOS.Platform.Init(); LoadApplication(new App()); return base.FinishedLaunching(app, options); } } }
初期画面を作る
アプリが起動して、最初に表示される画面を作成していきます。
今回は、あくまでQRコードの動作確認用、サンプルアプリですので、簡単に作っていきます。以下のコードをMainPage.xamlに入力。
MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="XamarinApp.MainPage"> <ContentPage.Padding> <OnPlatform x:TypeArguments="Thickness"> <On Platform="iOS">0,20,0,0</On> </OnPlatform> </ContentPage.Padding> <ContentPage.ToolbarItems> <ToolbarItem x:Name="menuDate" Order="Primary"/> <ToolbarItem x:Name="cameraItem" Text="カメラを起動" Order="Secondary" Clicked="Camera_Button"/> </ContentPage.ToolbarItems>
今回は、Toolbarに「カメラを起動」という名前のアイテムを設置しました。
もちろん、ButtonにしてもOKです。Clickedイベントに、Camera_Buttonを割り当てておきます。
途中のContantPage.Paddingタグですが、iOSのみ上部に20pxの余白を作っています。
これをしておかないと、iOSでは設置したコントロールが上部に隠れてしまうので、Xamarin.Formsでのアプリ開発ではこのPaddingの定義が必要です。
コードビハインドに、ZXingのイベントハンドラを作成
先ほど作成したToolbarItemに処理を書いていきます。
MainPage.xaml.cs/// <summary> /// カメラを起動 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private async void Camera_Button(object sender, EventArgs e) { var scanner = new ZXingScannerPage() { DefaultOverlayTopText = "バーコードを読み取ります", }; // スキャナページを表示 await Navigation.PushAsync(scanner); // スキャンできた場合 scanner.OnScanResult += (result) => { // スキャン停止 scanner.IsScanning = false; // PopAsyncで元のページに戻る Device.BeginInvokeOnMainThread(async () => { await Navigation.PopAsync(); }); }; }
実装方法はこんな感じ。
ZXingScannerPageというスキャナーページを生成し、OnScanResulでスキャン完了のイベントハンドラを記述しています。
そして、スキャンページ遷移後のバックグラウンドスレッドから。UIスレッドを操作するために
Device.BeginInvokeOnMainThreadで、UIを操作する処理を囲んであげます。ここでは、初期画面に戻るためのポップ処理ですね。プラットフォームごとに、様々な機能をカスタマイズできるDeviceクラスのメソッドです。
QRコードからの取得データをBrowserクラスで開く
前項で、スキャンが出来るようになったのはいいけど、...
この状態では、スキャン完了したら、初期画面に戻るだけです。ここで、戻る前にブラウザを起動させる処理を追加します。
MainPage.xaml.csDevice.BeginInvokeOnMainThread(async () => { await Browser.OpenAsync(result.Text, new BrowserLaunchOptions { LaunchMode = BrowserLaunchMode.SystemPreferred, TitleMode = BrowserTitleMode.Default }); await Navigation.PopAsync(); });
Device.BeginInvokeOnMainThreadの中に、BrowserクラスのOpenAsyncメソッドを追加します。
OpenAsyncメソッドにスキャン結果(URLが入ったresult)とLaunchOptionsを設定することで、アプリケーション内からWEBリンクを開くことができます。
LaunchMode = BrowserLaunchMode.SystemPreferred LaunchMode = BrowserLaunchMode.External TitleMode = BrowserTitleMode.Default TitleMode = BrowserTitleMode.Hide TitleMode = BrowserTitleMode.Show両者とも、起動方法に関するプロパティです。
LaunchModeですが、前者がブラウザを選択してアプリ内からリンクを開くのに対し、
後者がブラウザの選択後、アプリ外でリンクを開くという違いがあります。TitleModeは、リンクを開いている間、Webページのタイトルを上部に表示するかどうか設定できます。Defaultにしておくことで、表示になります。
ここらへんは是非とも、動きを見て確かめてほしいところです。
動作確認
Androidで動かします。iOSは実機が使えないので出来ましぇん。UWPもカメラが手元にないので割愛。
何でXamarin.Formsにしたしw
Android
サンプルソースと、ちょっとUIが違うのは勘弁してください。
さいごに
読み取りメッチャ速い(・∀・)
今Prismを勉強中なので、今回のPrism版もまた今度ね。
その前にiOSビルドのための実機が必要だよね。ipod touch 7とかでいいんでしょうか。
参考
- 投稿日:2020-07-22T21:21:27+09:00
C#のノート
はじめに
AzureLearnningで学んだ内容や参考文献などをまとめてノートにする。
更新日時
2020.07.20 Mon 新規作成 Yamada
2020.07.24 Fri 演算子、インクリメントとデクリメント追加
2020.07.24 Fri .Netクラスライブラリを追加
参考資料
最初のプログラム
Console.Write("Hello,World "); Console.Write('b'); // Hello,World b
基本ルール
変数の宣言
- ハッシュ記号は変数名に利用できない
- ドルマークは変数名に利用できない
- 数字で始まる変数名は宣言できない
- C#のキーワードに使われている文字列は変数に利用できない
- 変数の名前には短縮形を使わないこと
- 変数名の大文字と小文字は区別される(Value と valueは同じ)
- 変数名にはキャメルケースを利用すること
- 文字列リテラルはダブルクォーテーションを扱う
- 文字リテラルはシングルクォーテーションを扱う
データ型
- 文字列型
- string 変数名 と書く
string HelloStr = "Hello,World "; Console.Write(HelloStr); Console.Write('b'); // Hello,World b整数型は種類がある。
- 整数型
- int 変数名 と書く
- decimal 変数名 と書く
//integer 型の使用例 int firstNumber = 0; Console.Write("firstNumber = " + firstNumber); //firstNumber = 0
型変換
- 暗黙的型変換
- 変数初期化時に代入演算された値に基づいてデータ型を変換すること
- 宣言時は var 変数名 = 値 と書くこと
- 宣言時には必ず変数を初期化すること
宣言時に初期化しなかった場合のエラー
error CS0818: Implicitly-typed variables must be initializedvar HelloStr = "Hello,World "; Console.Write(HelloStr); Console.Write('b'); // Hello,World b
文字のエスケープ
表示文字 エスケープ文字 改行 \n タブ \t " \" バックスラッシュ \\
逐語的文字列リテラル
- エスケープしなくてもすべての空白と文字が維持される文字列
- 「@」 をつかう。
Console.WriteLine(@" c:\source\repos (this is where your code goes)");
Unicode エスケープ文字
- \u エスケープ シーケンスに Unicode (UTF-16) の文字を表す 4 文字コードを続ける方法
- エンコードした文字をリテラル文字列に追加することもできます。
// Kon'nichiwa World Console.WriteLine("\u3053\u3093\u306B\u3061\u306F World!");
文字列の連結
- 文字列の連結には「+」を使います。
使い方
string firstName = "Yamada"; string message = "Hello " + firstName; Console.WriteLine(message); // Hello Yamada!中間に変数を挟まない方法
string firstName = "Yamada"; string greeting = "Hello "; Console.WriteLine(greeting + " " + firstName + "!"); // Hello Yamada!
文字列補間
- 文字列内で変数の中身を展開する方法
string firstName = "Yamada"; string greeting = "Hello "; string message = $"{firstName} {greeting}!"; Console.WriteLine(message); // Yamada Hello !中間に変数を挟まない方法
string firstName = "Yamada"; string greeting = "Hello "; Console.WriteLine($"{firstName} {greeting}!"); // Yamada Hello !
文字列補間と逐語的リテラル
エスケープ文字を入れながら変数を展開する書き方
めっちゃオススメstring projectName = "First-Project"; Console.WriteLine($@"C:\Output\{projectName}\Data");
整数値の演算
- 加算には +
- 減算には -
- 掛け算には *
- 割り算には /
- 剰余には %
int sum = 7 + 5; int difference = 7 - 5; int product = 7 * 5; int quotient = 7 / 5; int mod = 7 % 5; Console.WriteLine("Sum: " + sum); Console.WriteLine("Difference: " + difference); Console.WriteLine("Product: " + product); Console.WriteLine("Quotient: " + quotient); Console.WriteLine("Mod: " + mod); /* Sum: 12 Difference: 2 Product: 35 Quotient: 1 Mod: 2 */decimal 型で計算する場合は数値の後ろに「m」をつける
decimal decimalQuotient = 7 / 5.0m; decimal decimalQuotient = 7.0m / 5.0m;
小数値の計算
integer型で計算すると小数点以下が切捨てられてしまうのでdecimalにキャストする。
int first = 7; int second = 5; decimal quotient = (decimal)first / (decimal)second; Console.WriteLine(quotient); // 1.4
演算の順序
int value1 = 3 + 4 * 5; int value2 = (3 + 4) * 5; Console.WriteLine(value1); Console.WriteLine(value2); // 23 // 35
インクリメント
+演算子を2回続けると値を1増やすことができる。
int value = 0; value = value + 5; // 初期値 5 value += 5; Console.WriteLine(value); // 10 value++; Console.WriteLine(value); // 11
デクリメント
-演算子を2回続けると値を1減らすことができる。
int value = 0; value = value + 11; // 初期値 11 value -= 5; Console.WriteLine(value); // 6 value--; Console.WriteLine(value); // 5
前置と後置
インクリメントとデクリメントはつける位置によって結果が変わる。
演算が前か後かの違い
int value = 1; value++; Console.WriteLine("First: " + value); Console.WriteLine("Second: " + value++); Console.WriteLine("Third: " + value); Console.WriteLine("Fourth: " + (++value)); /* First: 2 Second: 2 Third: 3 Fourth: 4 */
演習
華氏から摂氏に変換するコード
int fahrenheit = 94; fahrenheit -= 32; Console.WriteLine("The temperature is " + (decimal)(fahrenheit) * 5/9 + " Celsius."); // The temperature is 34.444444444444444444444444447 Celsius.別の書き方
int fahrenheit = 94; decimal celsius = (fahrenheit - 32m) * (5m / 9m); Console.WriteLine("The temperature is " + celsius + " Celsius."); // The temperature is 34.444444444444444444444444447 Celsius.
.Netクラスライブラリ
クラスライブラリを活用することで
開発に使う機能をすぐに用意できる。
データ型はクラスライブラリの一つでもある。
名前空間
データ型における苗字みたいなモノ
クラス名やデータ型が重複しないようにおける仕組み
クラスライブラリの探し方
Microsoft Docsのドキュメントを参照するのが一番
クラスライブラリのメソッドを呼び出す
Randomクラスを使ったサイコロのシュミレート
//クラス名(クラスのオブジェクト名).メソッド名 Random dice = new Random(); int roll = dice.Next(1, 7); Console.WriteLine(roll);
利用しているクラス
Random,Console
名前空間はどちらもSystem
メソッドの種類
- メソッド
- ステートレス
- アプリケーションの状態(メモリの状態)に依存しない
- Console.WriteLine
- ステートフル
- アプリケーションの状態(メモリの状態)に依存する
- Random.Next
- 乱数生成には時刻を利用している
※ステートフルメソッドはインスタンスメソッド
※ステートレスメソッドは静的メソッド
インスタンス
new 演算子を使うことでクラスのインスタンスを作成できる。
クラスのインスタンスすなわちオブジェクトRandom dice = new Random();
何も返さないメソッド
voidメソッドと言います。
メソッドに渡すモノ
引数(ひきすう)と言います。
※「いんすう」ではないです。RandomクラスのNextメソッドでは
下限値と上限値を指定しています。
おわり
- 投稿日:2020-07-22T18:40:08+09:00
NLogで環境変数を使って○○以上のログを出す(例:Info以上)
初めに
環境変数でログレベルの変更をしたいと思ったけれど、情報がなかったので備忘録的に。。
設定方法
Nlog.configを下記のように設定しましょう。
※layout(ログ出力形式)
は適当なのでお好きに変更してください。
※標準出力にろぐ出力しています。ファイルに出力したい時はwriteTo(ログ出力先)
などを良しなに。。Nlog.config<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <targets> <target name="logconsole" layout="${longdate} ${message}" xsi:type="Console" encoding="UTF-8" /> </targets> <rules> <logger name="*" minlevel="${environment:LOGLEVEL:cachedSeconds=5}" writeTo="logconsole"> </logger> </rules> </nlog>使い方
環境変数
LOGLEVEL
に公式のLog levelを参考に値を設定してください。例1:
LOGLEVEL=Info
-->Fatal, Error, Warn, Info
のログが標準出力されます。例2:
LOGLEVEL=Trace
-->Fatal, Error, Warn, Info, Debug, Trace
のログが標準出力されます。終わりに
もっと良い方法や間違いがあればコメントや修正リクエストいただけると幸いです!
- 投稿日:2020-07-22T18:38:47+09:00
UiPathで高度なデバックを実施する
UiPathで高度なデバックを実施する
これまでの記事で、DLLをUiPathから直接呼び出す手法をご紹介しました
本記事では、Visual Studioでブレイクポイントを設定して、高度なデバック方法をご紹介します。
開発のきっかけ
最近では当たり前ですが、昔はUiPathでカスタムライブラリを作成するには、
Visual StudioでDLLを作成し、Nuget ExplorerなどでDLLをnuget化して利用しかありませんでした。UiPathでデバッグするにも、Immediate Panelがなかったり、現時点でもComやラムダ式をデバッグしたり
解析することができませんでした。お約束事項(免責事項)
この記事は2020年7月時点の情報を基に作成しております。
記事の内容は私個人の見解であり、所属する組織の公式見解ではありません。カスタムライブラリ
前回の記事で使用したサンプルのカスタムライブラリを使用します
ソース内容
前回のソースと同じものです。
今回は、Test2のメソッドを使用します。using System.Windows; namespace UiPathCustomLibrary { public static class TestClass { public static void Test1() { MessageBox.Show("Hello World"); } public static void Test2(string message) { MessageBox.Show(message); } public static int Test3(int a, int b) { return a + b; } } }UiPathの実装
こちらの記事でご紹介した記事がデバッグを捕捉しやすいので、こちらの実装を流用します
https://qiita.com/takusonix/items/0ac407301570ca433556実装(完成版)
デバッグの手法
1. ブレイクポイントの設定
DLLのソースをVisual Studioで開き、デバッグポイントにブレイクポイントを設定します。
2. ビルド
3. pdbファイル
DLLと同時にpdbファイルが作成されていることを確認します。
4. DLLとpdbファイルをUiPathプロジェクトファイルに格納
UiPathのプロジェクトフォルダにDLLと一緒に、pdbファイルも格納します
5. UiPathプロジェクトの実行
Assembly.LoadFileでDLLロードされるUiPathのプロジェクトを実行し、
MessageBoxなどで、ユーザの入力待ち状態にします。
6. プロセスのアタッチを選択
UiPathでMessageBoxが開かれている状態で、VisualStudioのデバッグから、「プロセスのアタッチ」を選択します。
7. 対象のプロセスにアタッチ
検索窓に、
UiPath
と入力し、プロジェクト名または、ロボット名と一致するプロセスを選択して、右下の「アタッチ」を選択します。
8. デバッグ開始
9. ブレイクポイントで停止
10. 高度のデバッグを実施
ラムダ式を含んだ実装をイミディエイトウインドウで確認したり、
message引数の値や、その他の情報を自動パネルから確認したり実践例
UiPathでExcelを操作する(活用編: 名前を付けて保存する[完成版])を利用して、実際にデバッグしてみます。
1. フローを作成する
2. プロセスアタッチ用のメッセージボックス追加
3. ロボ実行
4. プロセスのアタッチ
5. デバッグ開始
調査開始
a)アクティビティの引数調査
b) Comオブジェクトの調査
UiPathのデバッグでは調査できない、Comオブジェクトや動的ビューも、Visual Studioなら展開可能です。
終わりに
本投稿では、DLLと一緒にPDBファイルを配置し、VisualStudioからデバックメニューのプロセスアタッチから、UiPathのロボットを捕捉し、事前に設定したブレイクポイントで、処理を停止させる方法と実践的な調査例をご紹介しました。
もし、よければLGTMをお願いします。
- 投稿日:2020-07-22T15:05:22+09:00
Web会議で手書きのアイデア伝えたくないですか?
はじめに
みなさん、こんにちは。
Web会議してますかー?(^o^)/Web会議してるときに、どうしても「書いたもの」で伝えたくなることってないですか?
僕はオフライン会議でも、アイデアを共有するときは「手書き」で伝えたい派なんですよねー。(そんな派閥があるのか?(笑))
急にテレワーク、Web会議が広まって、今まで手書きで共有してきた情報がかなりやりづらくなった気がします。。。
調べてみると
やはり、すでにすごいアプリがありますね!
Microsoft社製のWhiteboard
https://www.microsoft.com/ja-JP/p/microsoft-whiteboard/9mspc6mp8fm4?activetab=pivot:overviewtabすごいですよねー!
オンラインで共有しながら使えるみたいですよねー!!!裏側の仕組み、どんななってるんだろーって興味深々です。
ただ、僕はもっとシンプルなものが欲しかったんです。。。
で、調べるのも面倒なので
自分で作ろう!
と思いました☆彡
ホワイトボードアプリ、その名も
https://www.microsoft.com/ja-jp/p/hakuban/9nmp2xjbshsg#activetab=pivot:overviewtab
ポイント
- 起動したら速攻書ける!
- 「Save」で画像として保存できる!
- 「Load」で画像を読み込める!
- 「Clear」で一気に全部消える!
ボタンにはそれぞれショートカットが割り当てられています。
保存するときのファイル名は、[hakuban_2020-07-22_14-49-27.gif]のように毎回、秒までの名前が初期表示として出ます。
なので、
1. 書く!
2. Ctrl+sからのエンター連打!
3. Ctrl+cでクリア!
4. また書く!
5. 以降ループ
とすることにより、アイデアをどんどん画像化できます(^.^)このアプリは、別途のアカウントは不要ですし、クラウドに保存することもありません。
シンプルにサクサク使えることを目指しました。
ペンタブやタッチ対応画面があると良いかもしれません。
(マウスでも書けますが、厳しいです。。。。(^^;)もしよければ試してみてください(^o^)
みなさんのWeb会議が、もっともっとスムーズになることの一助になれば幸いです。
最後に
今回初めてUWPで作ってみました。
Androidアプリを作ったことがある人なら、UWPで作ろうとしたら「あー、なるほどなー」ってなると思います。アプリを作るよりも、開発者登録やアプリ登録が大変でした。。。
アプリ登録して審査されたときに、2日くらいかかったんですよ。
で、公開できたってメールきたー!と思ったらFailとのこと。。。
内容は、あなたのアプリはMixed Realityに対応しているって申請しているのに、対応してなくね?みたいなやつでしたw英語の画面だったんでよくわからなかったんですが、サポートチームに連絡したら丁寧に教えてくれました!!(英語でのメールできるようにがんばろう、とも思いましたw
ではでは!
- 投稿日:2020-07-22T13:54:09+09:00
C#のプロパティ初期化とGetter,Setterの書き方
samples.cs//初期化 public int Sum{ get; set; } = int.MinValue; //Getter定義 public int Sum2 => 10 + int.MinValue; public string Str { get { return $"===={Sum2}===="; } set { } }コード補完.csprop + tab public int MyProperty { get; set; } propg + tab public int MyProperty { get; private set; }
- 投稿日:2020-07-22T11:46:19+09:00
WPF の DataGrid で動的にカラムを追加する
WPF の DataGrid で実行時にカラムの数や名前が確定するような動的なデータを扱いたいとおもったとき、DataTable を使うと実現することができます。
つくるもの
データとカラムを追加できる DataGrid をつくります。
https://github.com/ishida722/DataTableSampleView をつくる
まず DataGird を配置して、ItemsSource をバインドします。今回は DataTableView というプロパティにバインドしました。
MainWindow.xaml<DataGrid ItemsSource="{Binding DataTableView}" />DataTable をつくる
ViewModel をつくります。まずテーブルデータを保持する DataTable オブジェクトをつくります。これは直接外部に公開しないので private にします。
DataTableViewModel.csprivate readonly DataTable dataTable = new DataTable();この dataTable オブジェクトにデータを追加していきます。カラムを追加する場合は以下のようにします。
DataTableViewModel.csdataTable.Columns.Clear(); dataTable.Rows.Clear(); dataTable.Columns.Add("ID"); dataTable.Columns.Add("Name");これでIDとNameというカラムが追加されました。次にデータを追加します。
DataTableViewModel.csvar row = dataTable.NewRow(); row[0] = 0; row[1] = "Jhon"; dataTable.Rows.Add(row);IDが0のJhonというデータが追加できました。
View に通知する
作成した DataTable を DataGrid に表示するためには DataView を使います。具体的には以下のようなプロパティを用意します。
DataTableViewModel.cspublic DataView DataTableView => new DataView(dataTable);DataTableView は View の DataGrid にバインドされています。このプロパティを読みだす度に、dataTable から DataView が作成されます。
DataTable は ObservableCollection ではないのでコレクションの変更を通知してくれません。なので自分で手動で変更を通知する必要があります。
DataTableViewModel.csprivate void NotifyTableUpdate() { OnPropertyChanged(nameof(DataTableView)); }テーブルを変更するたびに、NotifyTableUpdate() を実行する必要があります。
- 投稿日:2020-07-22T10:18:23+09:00
[C#] Injectionのライフサイクル
開発メモ
下記は、Startup.csにて外のクラスをインジェクトする際のコードになります。
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddScoped<IMyDependency, MyDependency>(); services.AddTransient<IOperationTransient, Operation>(); services.AddScoped<IOperationScoped, Operation>(); services.AddSingleton<IOperationSingleton, Operation>(); services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty)); // OperationService depends on each of the other Operation types. services.AddTransient<OperationService, OperationService>(); }ここで、 AddScoped, AddTransient, AddSingletonの違いをメモ
こちらを参照しました
https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1AddTransient
有効期間が一時的なサービス (AddTransient) は、サービス コンテナーから要求されるたびに作成されます。 この有効期間は、軽量でステートレスのサービスに最適です。
要求を処理するアプリでは、一時的なサービスが要求の最後に破棄されます。AddScoped
有効期間がスコープのサービス (AddScoped) は、クライアント要求 (接続) ごとに 1 回作成されます。
要求を処理するアプリでは、スコープ付きサービスは要求の最後で破棄されます。AddSingleton
有効期間がシングルトンのサービス (AddSingleton) は、最初に要求されたときに作成されます (または、Startup.ConfigureServices が実行されて、サービス登録でインスタンスが指定された場合)。 以降の要求は、すべて同じインスタンスを使用します。 アプリをシングルトンで動作させる必要がある場合は、サービス コンテナーによるサービスの有効期間の管理を許可することをお勧めします。 クラス内のオブジェクトの有効期間を管理するために、シングルトンの設計パターンを実装してユーザー コードを提供しないでください。
要求を処理するアプリでは、ServiceProvider がアプリのシャットダウン時に破棄されたとき、シングルトン サービスが破棄されます。
- 投稿日:2020-07-22T08:45:53+09:00
NuGetのキャッシュをすべてクリアする
NuGetを利用していると、たまにキャッシュが「悪さ」をすることがあります。
手動(CLI)で消すこともできますが、キャッシュは複数あるのですべてを消すのはちょっと面倒です。そこで、Visual StudioのGUI上からすべてのキャッシュを一括で削除する方法を紹介します。
まずは「ツール」→「オプション」を開きます。
つづいて「NuGetパッケージマネージャー」→「全般」を選択し、「すべてのNuGetキャッシュをクリア」をクリックしてください。
以上です。
- 投稿日:2020-07-22T08:37:04+09:00
ASP.NET Core MVC 3.1 「空のテンプレートからサクサクセットアップ」
はじめに
今回は「空」のテンプレートからASP.NET Core MVCアプリケーションをセットアップする際の手順を記載します。
「Web アプリケーション(モデル ビュー コントローラー)」テンプレートは、最初からMVCプロジェクトに必要な最低限の構成が用意されているので便利です。
ただし、最初からjQueryやBootstrapが組み込まれていたりと、場合によっては不要になるものも組み込まれています。
そのため、著者はまっさらな「空」のテンプレートから作成するようにしています。前回の記事
ASP.NET Core MVC 3.1 入門 その5 「View」
今回のゴール
- MVCを有効にする
- Controllerを追加する
- Viewを追加する
- Routeを設定する
- 特殊なViewテンプレートを追加する
- タグヘルパーを有効にする
- 静的ファイルを利用可能にする
- スタイルシートを適用する
- JavaScriptファイルを適用する
環境
- IDE
- Visual Studio 2019
- 言語
- C#
セットアップ
「ASP.NET Core Web アプリケーション」を選択
任意の「プロジェクト名」及び「ソリューション名」を入力
テンプレートの選択
今回は「ASP.NET Core 3.1」「空」のプロジェクトを選択します。
プロジェクトの作成完了
VisualStudioで以下のような画面が立ち上がります。
ソリューションエクスプローラーを眺める
「空」のテンプレートのソリューションエクスプローラーを眺めてみましょう。
こちらのテンプレートには、MVCテンプレートとは異なり、
Controllers
やViews
といったMVC用のフォルダ等は一切含まれておりません。これから作成していきます。MVCを有効にする
Startup.cs
のConfigureServices
メソッドを以下のように変更してください。
ConfigureServices
メソッドでは、アプリケーションで使用するサービスを追加(登録)します。
AddControllersWithViews
は、View
を使ったController
を追加するという意味です。
これによって、View
とController
を用いた(MVCの基本的な)機能が組み込まれます。// This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); }Controllersフォルダを追加
MVCパターンに沿った形で名前空間を構成します。
ルートにControllers
フォルダを追加してください。Controllerを追加
Controllers
フォルダに空のController
を追加してください。
ここではChatController
としておりますが、もちろん、ご自身が作成するアプリケーションに含まれる機能と対応したController名をつけてください。作成した空の
Controller
public class ChatController : Controller { public IActionResult Index() { return View(); } }Viewを追加
アクションメソッド(
Index
メソッド)のメソッド名を右クリック > ビューの追加と進んでください。
「Razor ビュー」を選択します。
なお「スキャフォールディング」というのは、要は雛形を自動生成する機能のことを示します。ここではViewの雛形を自動生成してもらうために、この機能を使います。続いてビュー名ですが、アクションメソッド名とします。
アクションメソッドで呼び出されるView
メソッドが
明示的に指定しない限り、/Views/コントローラー名/アクションメソッド名.cshtml
を読み込むためです。
テンプレートは「Empty(モデルなし)」を選択します。Routeの設定
Startup.cs
のConfigure
メソッドを以下のように変更し、Routeも合わせて登録しておきます。
Configure
メソッドでは、Routeの定義に代表されるように、アプリケーションがHTTPリクエストに対してどのように応答するかを定義します。
Routeについてはこちらの記事を参照してください。変更前
app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); });変更後
デフォルトのRouteは先ほど作成した
Controller
のIndex
アクションメソッドに向けておきます。app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Chat}/{action=Index}/{id?}" ); });動作確認
F5で実行します。
作成したController
のIndex
アクションメソッドが呼び出されていることが確認できればOKです。レイアウトページを追加
レイアウトページについてはこちらの記事を参照してください。
Views
>Shared
フォルダを作成します。
Shared
フォルダを右クリック > 追加 > 新しい項目と進んでください。
Razorレイアウトを選択します。
ファイル名は最初から_Layout.cshtml
となっているので、そのままでOKです。
_Layout.cshtml
が追加できました。スタートページ(_ViewStart.cshtml)を追加
スタートページ(_ViewStart.cshtml)についてはこちらの記事を参照してください。
Views
フォルダを右クリック > 追加 > 新しい項目と進んでください。
Razorビューの開始を選択します。
ファイル名は最初から_ViewStart.cshtml
となっているので、そのままでOKです。
_ViewStart.cshtml
が追加できました。動作確認
F5で実行します。
レイアウトページが適用されていることが確認できればOKです。
分かりづらいですが、タブの表示名に<title>
が効いてIndex
となっています。_ViewImports.cshtmlを追加
Views
に_ViewImports.cshtml
を追加します。
このファイルでは、複数のViewで利用する名前空間のインポートをおこないます。
Views
フォルダを右クリック > 追加 > 新しい項目と進んでください。
Razorビューのインポートを選択します。
ファイル名は最初から_ViewImports.cshtml
となっているので、そのままでOKです。
_ViewImports.cshtml
が追加できました。
以後、複数のViewで利用する名前空間が発生した場合には、このファイルでusingすると効率的です。タグヘルパーの有効化
@addTagHelper
ディレクティブにより、Viewでタグヘルパーが使用可能になります。最初のパラメーターでは、読み込むタグヘルパーを指定します。
すべてのタグヘルパーを指定する場合は*
を使用します。2 番目のパラメーターでは、タグヘルパーを含む名前空間を指定します。
Microsoft.AspNetCore.Mvc.TagHelpers
=> ASP.NET Core に組み込まれているタグヘルパーが存在する名前空間下記のコードを
Views/_ViewImports.cshtml
に追加すると、
Microsoft.AspNetCore.Mvc.TagHelpers
に含まれるすべてのタグヘルパーを、
=> ワイルドカード*
での指定によるもの
Views
フォルダまたはサブフォルダ内のすべてのViewで使用可能にする
=> Views/_ViewImports.cshtmlに定義したことによるものという意味になります。
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpersConfiguration
Configuration
クラスがappsettings.json
に設定した値の取得機能を提供してくれます。
まだ設定値が無いので、ここではConfiguration
を定義して、取得できる状態までとしておきます。public IConfiguration Configuration { get; set; } public Startup(IConfiguration configuration) { Configuration = configuration; }wwwroot の追加
ルートに
wwwroot
フォルダを作成します。
スタイルシートや画像ファイル、JavaScriptファイルといった静的ファイルを格納するフォルダで、Webルートと呼びます。静的ファイルを利用可能にする
スタイルシートや画像ファイル、JavaScriptファイルといった静的ファイルを利用可能にします。
Startup.cs
のConfigure
メソッドを以下のように変更し、UseStaticFiles
メソッドを呼び出します。
これでwwwroot
に格納されているファイルにアクセスできるようになります。public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); //追加 app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Chat}/{action=Index}/{id?}" ); }); }スタイルシートを追加
wwwroot
にスタイルシートを追加します。
wwwroot
>css
フォルダを作成します。
css
フォルダを右クリック > 追加 > 新しい項目と進んでください。
スタイルシートを選択します。
ファイル名はsite.css
としておきます。スタイルシートを適用
レイアウトページで参照します。
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link rel="stylesheet" type="text/css" href="~/css/site.css" asp-append-version="true"/> @*追加*@ </head> <body> <div> @RenderBody() </div> </body> </html>JavaScriptファイルを追加
wwwroot
>js
フォルダを作成します。
js
フォルダを右クリック > 追加 > 新しい項目と進んでください。
JavaScriptファイルを選択します。
ファイル名はsite.js
としておきます。Javascriptファイルを適用
レイアウトページで参照します。
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link rel="stylesheet" type="text/css" href="~/css/site.css" asp-append-version="true"/> </head> <body> <div> @RenderBody() </div> <script type="text/javascript" href="~/js/site.js" asp-append-version="true"></script> @*追加*@ </body> </html>以後はアプリケーション固有の機能を追加していきます。
以上となります。
ありがとうございました。
- 投稿日:2020-07-22T07:50:57+09:00
C#のみを使って、変更前後の差分表示のできる、ファイル名変更ソフトの作り方
概要
2020年にもなってファイル名変更ソフトを新しく作りました。名前はFileRenamerDiffです。
この記事では、ソフトウェアの記述解説をします。ソフトウェアの内容やスクリーンショットは前回の記事を参照ください。
ざっくりいうと、WPF on .NET Core by C#です。
MVVMライブラリはLivetとReactivePropertyを使用しています。
ソースコード全体はGitHubにあります。使用している主なライブラリ
ライブラリ名 使用目的 ReactiveProperty MVVMライブラリとして、主にViewModelで使用 Livet MVVMライブラリとして、ViewとViewModelの連携やINotifyPropertyChanged実装基底クラスなど DiffPlex 差分表示、ViewModelで使用。ViewではDataGrid内のRichTextBoxにBindingされます。 MahApps Viewのデザインライブラリ MaterialDesignThemes Viewのデザインライブラリ Markdig Viewのライセンス表記などのMarkdownの表示 Utf8Json JSON設定ファイルの読込/保存 Serilog ロギング 全体構成
MVVMパターンですので、Model-ViewModel-Viewの3層に分割して、ViewとViewModelの間はBindingによってつながっています。
メインとなるクラスだけ抜き出したクラス図は以下のようになっています。PlantUMLで書きました。
各層ごとに解説します。
Model
Model層ではファイルの探索、正規表現を使ったリネーム、リネーム保存、設定ファイル読み書きといったことを行っています。
ファイル探索
指定されたパス以下のファイルパスを取得します。このとき設定によって、隠しファイルやフォルダ・(狭義の)ファイルの両方か片方だけリストにするか切り替えます。
探索されたファイルの情報をFileElementModel
にまとめて、そこにリネーム後のファイル名もあとで加えます。リネーム処理(プレビュー)
設定は
SettingAppModel
にまとめてあり、このクラス全体をJSONにシリアライズして設定ファイルとして保存しています。設定には探索対象ディレクトリや複数の削除パターン・置換パターンが含まれています。リネームの実行は.NETの
Regex
に置換後の文字列を加えたReplaceRegex
クラスで行います。複数のファイル名に対して行うのを想定しているので、RegexOption
はCompiled
にしておきます。
ソフトウェアの設定では削除パターンと置換パターンで2種類がありますが、Regexを生成する際には、この削除パターンは全て|
で結合した1つのRegexにし、置換後の文字列は""
にしておきます。例)"a"、"b"、"c"、を削除Regex.Replace("a1b2c3", "(a|b|c)", "") //結果"123"重複判定
リネーム処理後に同じファイルパスが含まれていた場合、ファイルパスが重複しているとみなし、リネーム保存を禁止しています。なお、Windowsは大文字・小文字は同じとみなします。
リネーム後のファイルパスだけでなく、リネーム前のファイルパスとも重複していないかを判定しています。これは保存の順番によっては失敗する場合があるためです。
例)"A.txt", "B.txt"というファイル名が同じディレクトリにあり、"A"を"b"に、"B"を"c"に置き換えるリネームをした場合、先に"A.txt"→"b.txt"が保存されると既存の"B.txt"と重複して失敗してしまいます。リネーム保存
リネーム保存時は深い階層のファイルから保存します。フォルダもリネームする場合、保存前のファイルが移動したことになり失敗してしまいます。
例)"/Main/Sub/"という階層のフォルダがあり、"/Main"と"/Sub"をリネームする場合、"/Main"を先にリネーム保存してしまうと、"/Main/Sub"が見つからなくなります。また、リネーム処理自体もファイルかフォルダかで処理が微妙に異なります。これは過去記事を参照ください。
C#で確実にファイル名を変更するReactiveProperty(Model)
シリアライズされる
SettingAppModel
クラス以外はViewModelから参照される可変プロパティはだいたいReactivePropertyにしてあります。
これにより、Modelでの変更がRxなストリームとしてViewModel・Viewに伝わります。ViewModel
基本的にはModelの上記機能を呼び出すためのReactiveCommandとModelの状態を反映するReactivePropertyがメインです。
ViewModelはなるべく薄く作るべきなのですが、差分表示(DiffPlex)やDataGrid用のCollectionViewなどで、ふくらんでいます。ReactiveProperty(ViewModel)
リネーム保存などの時間のかかる処理は非同期で行うべきですが、ViewにBindingされるプロパティはUIスレッド以外から変更すると例外が発生してしまいます。
こういった事情もReactivePropertyのSchedulerやRxのObserveOnUIDispatcher()
を使うと解決できます。例えば、Modelでの非同期処理の結果、リネームファイル数プロパティ
CountReplaced
が変更になり、それをViewにも表示するためViewModelで同名のプロパティを作る場合はこうします。FileElementsGridViewModel.cs//プロパティ宣言 public IReadOnlyReactiveProperty<int> CountConflicted { get; } //コンストラクタ内で this.CountReplaced = model.CountReplaced.ObserveOnUIDispatcher().ToReadOnlyReactivePropertySlim();これにより、Modelのプロパティ変更時のスレッドに関係なく、ViewModelのプロパティ変更はUIスレッドから行われます。
差分表示
DiffPlexというライブラリを使用して、リネーム前後のファイル名の差分情報を生成します。
DiffPlexの詳しい解説は過去記事を参照ください。
WPFでGitのDiffっぽい差分表示をするDiffPlexライブラリ。~あるいは、あるジェダイの変遷ファイル名の性質上、1行全体のハイライトをしても意味がないので、ワード境界を細かく区切りました。
デフォルトの差分表示時のワード境界が全角文字などに対応していなかったので、追加しました。public static SideBySideDiffModel CreateDiff(string inputText, string outText) { char[] wordSeparaters = { ' ', '\t', '.', '(', ')', '{', '}', ',', '!', '?', ';', //MarkDiffデフォルトからコピー '_','-','[',']','~','+','=','^', //半角系 ' ','、','。','「','」','(',')','{','}','・','!','?',';',':','_','ー','-','~','‐','+','*','/','=','^', //全角系 }; var diff = new SideBySideDiffBuilder(new Differ(), wordSeparaters); return diff.BuildDiffModel(inputText, outText); }これにより、どこが変更されたかが分かりやすくなります。
$\style{background-color:pink;}{報告書「最終版」}$.txt
→ $\style{background-color:LightGreen;}{報告書「最終手前版」}$.txt
?
報告書「$\style{background-color:pink;}{最終版}$」.txt
→ 報告書「$\style{background-color:LightGreen;}{最終手前版}$」.txtファイル情報DataGrid用ViewModel
Modelでのファイル情報クラス
FileElementModel
に対応したFileElementViewModel
を作成し、上記差分情報もその中に含めています。
ただのDataGridにBindingするなら、これをObservableCollection
などに入れればよいのですが、並び替えや行フィルタ処理に対応するためにはICollectionView
に変換する必要があります。
変換自体はCollectionViewSource.GetDefaultView()
を呼ぶだけでよいですが、行フィルタを使用する場合はICollectionViewer
のFilter
プロパティに表示判定用コールバックを指定して、行フィルタ基準が変わるたびにRefresh()
メソッドを呼ぶ必要があります。
実際のコードのうち関係する部分を抜き出すと以下のようになります。FileElementsGridViewModel.cs(部分)/// <summary> /// ファイル情報コレクションのDataGrid用のICollectionView /// </summary> public ReadOnlyReactivePropertySlim<ICollectionView> CViewFileElementVMs { get; } /// <summary> /// 置換前後で差があったファイルのみ表示するか /// </summary> public ReactivePropertySlim<bool> IsVisibleReplacedOnly { get; } = new ReactivePropertySlim<bool>(false); public FileElementsGridViewModel() { this.CViewFileElementVMs = model .ObserveProperty(x => x.FileElementModels) .Select(x => CreateFilePathVMs(x)) .ObserveOnUIDispatcher() .Select(x => CreateCollectionViewFilePathVMs(x)) .ToReadOnlyReactivePropertySlim(); //表示基準に変更があったら、表示判定対象に変更があったら、CollectionViewの表示を更新する new[] { this.IsVisibleReplacedOnly, ... } .CombineLatest() .Throttle(TimeSpan.FromMilliseconds(100)) .ObserveOnUIDispatcher() .Subscribe(_ => RefleshCollectionViewSafe()); } private ICollectionView CreateCollectionViewFilePathVMs(ObservableCollection<FileElementViewModel> fVMs) { var cView = CollectionViewSource.GetDefaultView(fVMs); cView.Filter = (x => GetVisibleRow(x)); return cView; } /// <summary> /// 2つの表示切り替えプロパティと、各行の値に応じて、その行の表示状態を決定する /// </summary> /// <param name="row">行VM</param> /// <returns>表示状態</returns> private bool GetVisibleRow(object row) { if (!(row is FileElementViewModel pathVM)) return true; var replacedVisible = !IsVisibleReplacedOnly.Value || pathVM.IsReplaced.Value; var conflictedVisible = !IsVisibleConflictedOnly.Value || pathVM.IsConflicted.Value; return replacedVisible && conflictedVisible; } private void RefleshCollectionViewSafe() { if (!(CViewFileElementVMs?.Value is ListCollectionView currentView)) return; //なぜかCollectionViewが追加中・編集中のことがある。 if (currentView.IsAddingNew) { LogTo.Warning("CollectionView is Adding"); currentView.CancelNew(); } if (currentView.IsEditingItem) { LogTo.Warning("CollectionView is Editing"); currentView.CommitEdit(); } currentView.Refresh(); }View
MahAppsとMaterialDesignThemes
MahAppsとMaterialDesignThemesという2つのライブラリを使用して、見た目をカッチョイイ感じにします。
MahAppsはウインドウのタイトル部分とウインドウの周りにボヤッとした色をつける機能GlowBrush
を使用しています。ウインドウの周りがちょっと青いのがわかりますでしょうか。
それ以外のコントロールはMaterialDesignThemesのテーマが適用されています。今回はプリセット色は使わず、すべてカラーコードを指定したかったので、プリセットの色に上書きします。
App.xaml<Color x:Key="Primary900">#1A537C</Color> <Color x:Key="Primary800">#286591</Color> ... <Color x:Key="Primary50">#E8EFF4</Color>MaterialDesignThemesの詳しい内容は # 参考 を参照してください。
テーマ変更
Ligth/Darkテーマ切り替えをやってみたかったので、作ってみました。
MaterialDesignThemesのPaletteHelper
を使ってデフォルトのテーマをGet、変更後にSetします。
メイン色と前景色のペアで設定します。両者はある程度の明暗差がないと見づらいです。App.xaml.csprivate static void ChangeTheme() { var paletteHelper = new PaletteHelper(); var theme = paletteHelper.GetTheme(); bool isDark = Model.Instance.Setting.IsAppDarkTheme; theme.SetBaseTheme( isDark ? Theme.Dark : Theme.Light); theme.PrimaryDark = new ColorPair((Color)Current.Resources["Primary700"], Colors.White); theme.PrimaryMid = new ColorPair((Color)Current.Resources["Primary500"], Colors.White); theme.PrimaryLight = (Color)Current.Resources["Primary300"]; theme.Paper = AppExtention.ToColorOrDefault(isDark ? "#272E33" : "#E6EDF2"); //ベース色とのコントラストが Current.Resources["HighContrastBrush"] = (isDark ? theme.PrimaryLight : theme.PrimaryDark) .Color.ToSolidColorBrush(true); paletteHelper.SetTheme(theme); }ただし、各種色やBrushはStaticResourceで指定されているので、再起動後に反映されます。
ファイル情報DataGrid(差分以外)
ViewModelのファイル情報クラス
FileElementViewModel
を含んだICollectionViewをDataGridにBindingしています。重複判定とリネーム有無の表示列のヘッダにはカウント数と行フィルタを切り替えるToggleSwitchを配置しておきます。
FileElementGrid.xaml<DataGridTemplateColumn.Header> <materialDesign:Badged BadgeColorZoneMode="PrimaryMid" DataContext="{Binding DataContext, ElementName=rootObj}"> <materialDesign:Badged.Badge> <TextBlock Text="{Binding DataContext.CountReplaced.Value, ElementName=rootObj}" /> </materialDesign:Badged.Badge> <ToggleButton IsChecked="{Binding IsVisibleReplacedOnly.Value}"> <materialDesign:PackIcon Kind="CheckBold" /> </ToggleButton> </materialDesign:Badged> </DataGridTemplateColumn.Header>ディレクトリ表示列は中身をButtonにして、クリックされたらエクスプローラーで開くようにします。特定のファイルだけ手修正したい場合などに使用します。
FileElementGrid.xaml<DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <Button Command="{Binding OpenInExploreCommand, Mode=OneTime}" Style="{StaticResource MaterialDesignFlatButton}" ToolTip="{Binding DirectoryPath, Mode=OneTime}"> <StackPanel Orientation="Horizontal"> <materialDesign:PackIcon Kind="FolderEditOutline" /> <TextBlock Text="{Binding DirectoryPath, Mode=OneTime}" /> </StackPanel> </Button> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn>DataGridで使用している細かい技術は以前の記事を参照ください。
DataGridやListBox内でクリックされたら自身の行を上下に移動するButton
DataGridやListBox内でクリックされたら自身の行を削除するButton
C#のColor関連の便利拡張メソッド+α 24選(HSV色空間への変換も)差分表示
ViewModel内のDiffPlexで作成した差分情報をDataGrid内のRichTextBoxにBindingしています。FileElementGrid.xaml<DataGridTemplateColumn Header="{x:Static properties:Resources.Grid_OldText}"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <RichTextBox v:RichTextBoxHelper.Document="{Binding Diff.Value.OldText, Converter={StaticResource DiffPaneModelToFlowDocumentConverter}}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn>独自のConverterを使用して、DiffPlexの差分情報からRichTextBoxの中身のFlowDocumentに変換しています。
差分情報内のワード境界ごとに無変更・削除(Deleted)・挿入(Inserted)・挿入前(Imaginary)・修正(Modified)のどれかに判定されます。ただし1行単位で比較していないため、挿入前・修正は実際には使われないようです。
削除の場合はピンク、挿入の場合は黄緑色を指定しています。DiffPaneModelToFlowDocumentConverter.cspublic class DiffPaneModelToFlowDocumentConverter : IValueConverter { private static readonly Brush unchangeBrush = Colors.Transparent.ToSolidColorBrush(true); private static readonly Brush deletedBrush = AppExtention.ToColorOrDefault($"#FFAFD1").ToSolidColorBrush(true); private static readonly Brush insertedBrush = AppExtention.ToColorOrDefault($"#88E6A7").ToSolidColorBrush(true); private static readonly Brush imaginaryBrush = Colors.SkyBlue.ToSolidColorBrush(true); private static readonly Brush modifiedBrush = Colors.Orange.ToSolidColorBrush(true); private static readonly Brush changedTextBrush = Colors.Black.ToSolidColorBrush(true); private static readonly Brush normalTextBrush = (SolidColorBrush)App.Current.Resources["MaterialDesignBody"]; public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (!(value is DiffPaneModel diffVM)) return Binding.DoNothing; if (!diffVM.Lines.Any()) return Binding.DoNothing; if (diffVM.Lines.Count > 1) LogTo.Warning("Lines Count is over. {@LinesCount}", diffVM.Lines.Count); List<Run> lineView = ConvertLinveVmToRuns(diffVM.Lines.First()); var paragraph = new Paragraph(); paragraph.Inlines.AddRange(lineView); return new FlowDocument(paragraph); } private static List<Run> ConvertLinveVmToRuns(DiffPiece lineVM) => lineVM.Type switch { //ChangeType.Modifiedだったら変更された部分だけハイライトしたいのでSubPieceからいろいろやる ChangeType.Modified => lineVM .SubPieces .Select(x => ConvertPieceVmToRun(x)) .ToList(), //ChangeType.Modified以外は行全体で同じ書式 _ => new List<Run> { ConvertPieceVmToRun(lineVM) }, }; private static Run ConvertPieceVmToRun(DiffPiece pieceVM) => new Run { Text = pieceVM.Text, Foreground = (pieceVM.Type == ChangeType.Unchanged) ? normalTextBrush : changedTextBrush, //差分タイプによって、背景色を決定 Background = (pieceVM.Type switch { ChangeType.Deleted => deletedBrush, ChangeType.Inserted => insertedBrush, ChangeType.Imaginary => imaginaryBrush, ChangeType.Modified => modifiedBrush, _ => unchangeBrush }), }; public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { LogTo.Error("Not Implemented"); return Binding.DoNothing; } }RichTextBoxにFlowDocumentをBindingする方法は過去記事を参照ください。
WPFのRichTextBoxにBindingするファイルダイアログ
Livet.Extensionsを使用しています。
ButtonのTriggerに設定することで、Viewだけでダイアログ処理を完結することができます。
ViewModelにはユーザーが指定したファイルパス等の必要な情報だけが伝わります。MainWindow.xaml<Button Style="{StaticResource MaterialDesignRaisedButton}"> <behaviors:Interaction.Triggers> <behaviors:EventTrigger EventName="Click"> <l:FolderBrowserDialogInteractionMessageAction> <l:DirectInteractionMessage CallbackCommand="{Binding LoadFilesFromDialogCommand, Mode=OneTime}"> <l:FolderSelectionMessage Description="Select Target Folder" DialogPreference="None" SelectedPath="{Binding SettingVM.Value.SearchFilePath.Value}" /> </l:DirectInteractionMessage> </l:FolderBrowserDialogInteractionMessageAction> </behaviors:EventTrigger> </behaviors:Interaction.Triggers> <materialDesign:PackIcon Kind="FolderOpen" /> </Button>多言語対応
ResX Resource Managerを使用しました。
各言語を横に並べて入力できるので便利です。
デフォルトの言語はOSに合わせて変わります。ただし、日本語OS以外は持っていないので検証できていません。
設定メニューから変更することもできます。ただし、リソース文字列はx:Static
で取得しているので再起動後でないと反映されないです。Markdown表示
アプリケーション情報ページではMarkdownを表示しています。
サードパーティライセンスのMarkdownファイルをリソースとして追加した上で、MarkDigライブラリで表示しています。
MarkDigの詳しい解説は以前の記事を参照ください。
C#でMarkdownを表示するライブラリMarkDigの紹介ログ
ロギングはSerilogを使用しています。
メッセージテンプレートを使用して、以下の内容を保存しています。
現在時刻、ログレベル、スレッドID・名称、メッセージ本文、呼び出し元名前空間+クラス名、呼び出し元メソッドシグネチャ、行番号、使用メモリ量、(あれば例外)より詳しくは過去記事を参照ください。
C#でSerilogとFody.Anotarを使って、全部盛りのログをとる
エラー時のログなどをMicrosoftStoreに送ったりも出来るらしいですが、やり方がわかっていません。アイコン
パワーポイントでがんばりました。
デプロイ
Release
単一ファイルかつ、自己完結型で生成しました。
これにより、エンドユーザーは.NET Coreを事前にインストールする必要がありません。Store配布
Microsoft Storeにアプリケーションを登録しました。
意外と簡単にできて、コストも2000円弱を初回に払うだけです。
有料アプリの場合は売上から割合で取るようです。詳しい内容は過去記事、、、ではなくてそのうち書く記事を参考にしてください(もはや登録方法を忘れつつある)。
環境
VisualStudio 2019
C# 8
.NET Core 3.1FileRenamerDiff.csproj<ItemGroup> <PackageReference Include="DiffPlex" Version="1.6.3" /> <PackageReference Include="LivetCask" Version="3.2.3.1" /> <PackageReference Include="LivetExtensions" Version="3.2.3.1" /> <PackageReference Include="MahApps.Metro" Version="2.1.1" /> <PackageReference Include="Markdig" Version="0.18.0" /> <PackageReference Include="Markdig.Wpf" Version="0.3.1" /> <PackageReference Include="MaterialDesignColors" Version="1.2.6" /> <PackageReference Include="MaterialDesignThemes" Version="3.1.3" /> <PackageReference Include="MaterialDesignThemes.MahApps" Version="0.1.4" /> <PackageReference Include="ReactiveProperty" Version="6.2.0" /> <PackageReference Include="System.Interactive" Version="4.1.1" /> <PackageReference Include="System.Reactive.Compatibility" Version="4.4.1" /> <PackageReference Include="Anotar.Serilog.Fody" Version="5.1.3" /> <PackageReference Include="Serilog" Version="2.9.0" /> <PackageReference Include="Serilog.Enrichers.Memory" Version="1.0.4" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Exceptions" Version="5.6.0" /> <PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" /> <PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Utf8Json" Version="1.3.7" /> </ItemGroup>参考
MVVMとリアクティブプログラミングを支援するライブラリ「ReactiveProperty v2.0」オーバービュー
Livet/README.md
方法: DataGrid コントロールでデータをグループ化、並べ替え、およびフィルター処理する - WPF | Microsoft Docs
自己完結型アプリケーションのトリミング - .NET Core | Microsoft Docs
MaterialDesignInXamlToolkit/README.md余談
クラス図のBindingの書き方はこれで良いか、あまり自信ありません。クラス図には書かないほうがよいのかな?でも書かないとViewとViewModelの対応関係わからんしな。
UML詳しい人いたら教えてほしいです。
- 投稿日:2020-07-22T07:11:19+09:00
Unityでのゲーム制作時、オブジェクトの移動処理が重い場合に行った事とその効果
筆者の環境:OS:Windows10, Unity 2019.3.5f1 使用言語:C#
利用Asset:DOTween,Destructible2D
筆者のプログラミング歴:2020年1月よりUnity上でC#の勉強を開始。現在開発中のゲーム(2D)のメインが『落下してくるオブジェクトを切断する』といった内容なのですが、切断後に異常に動きが遅くなる現象が多発し、ゲーム性を著しく損なう為、対策を調べつつ対処した結果を、ざっくりではありますが、備忘録として残そうと思います。
まず、UnityのProfilerを起動し、どこの処理が重いのかを調べました。
結果、『Physics2D.FindNewContacts』という部分に多大な時間を取られていました(数百~千数百ms)。
調べてみると、どうやらRigidbody2Dが良くないようです。
しかし、色々と試しましたが、Rigidbody2Dを外したり、Kinematicにすると思った動きが出来なかった為、以下の事を試しました。
※筆者の環境で、効果が高かったと思われる順に表記。●切断されるオブジェクトのSpriteのサイズを小さくした。
Spriteを選択した時のInspectorから、MaxSizeを小さく、Pixels Per Unitの値を大きくして調整しました。●オブジェクトのGravity Scaleの値を小さくした。
これは、単純に動きが早すぎて切断し辛かったのでスピードを遅くする目的でやったのですが、カクカク感が軽減されました。●Update関数をFixedUpdateに変更。
数値としては反映されているか分かりませんでしたが、見た目のカクカクを軽減する効果があったように感じました。●Project SettingsのTime Fixed Timestepの値を”小さく”した。
処理を軽くする為には、値を大きくすべきだそうですが、大きくすると思ってもない動きをしたり、若干ワープしたような動きをするので、逆に小さくしてみると、スムーズに動くようになったので、小さくしてしまいました。結果、目的がブレたような気もしますが、見た目のスムーズさは向上したのと、固まったかのような現象はほとんど起こらなくなったので、とりあえずはこのまま開発を進めようと思います。
新情報等見つけたら、再度試してこちらに追記します。ここまで読んでいただき、ありがとうございました。