20200722のC#に関する記事は12件です。

【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.Essentials

Zxingパッケージ

上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"の権限を取っておきます。

qiita qrコード.jpg



または、AndroidManifest.xmlに直接追加することもできます。以下を manifestタグの中に入れてやってください。

AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />




Appクラスの修正

NavigationPageをMainPageに設定します。
これを行うことで、ナビゲーションスタックに、ルートページとなる初期画面がプッシュされます。

App.xaml.cs
using 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.cs
using 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.cs
Device.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が違うのは勘弁してください。


6fhqa-cecfh.gif


さいごに

読み取りメッチャ速い(・∀・)

今Prismを勉強中なので、今回のPrism版もまた今度ね。

その前にiOSビルドのための実機が必要だよね。ipod touch 7とかでいいんでしょうか。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#のノート

はじめに

AzureLearnningで学んだ内容や参考文献などをまとめてノートにする。


更新日時

2020.07.20 Mon 新規作成 Yamada
2020.07.24 Fri 演算子、インクリメントとデクリメント追加
2020.07.24 Fri .Netクラスライブラリを追加


参考資料

C# の 最初のステップ(Azure Learnning)


最初のプログラム

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 initialized
var 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メソッドでは
下限値と上限値を指定しています。


おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のログが標準出力されます。

終わりに

もっと良い方法や間違いがあればコメントや修正リクエストいただけると幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UiPathで高度なデバックを実施する

UiPathで高度なデバックを実施する

これまでの記事で、DLLをUiPathから直接呼び出す手法をご紹介しました

本記事では、Visual Studioでブレイクポイントを設定して、高度なデバック方法をご紹介します。

■ 実施例
image.png

開発のきっかけ

最近では当たり前ですが、昔は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

実装(完成版)

image.png

デバッグの手法

1. ブレイクポイントの設定

DLLのソースをVisual Studioで開き、デバッグポイントにブレイクポイントを設定します。
image.png

2. ビルド

ビルドを行って、DLLを生成します
image.png

3. pdbファイル

DLLと同時にpdbファイルが作成されていることを確認します。
image.png

4. DLLとpdbファイルをUiPathプロジェクトファイルに格納

UiPathのプロジェクトフォルダにDLLと一緒に、pdbファイルも格納します
image.png

5. UiPathプロジェクトの実行

Assembly.LoadFileでDLLロードされるUiPathのプロジェクトを実行し、
MessageBoxなどで、ユーザの入力待ち状態にします。
image.png

6. プロセスのアタッチを選択

UiPathでMessageBoxが開かれている状態で、VisualStudioのデバッグから、「プロセスのアタッチ」を選択します。
image.png

7. 対象のプロセスにアタッチ

検索窓に、UiPathと入力し、プロジェクト名または、ロボット名と一致するプロセスを選択して、右下の「アタッチ」を選択します。
image.png

8. デバッグ開始

Visual Studioがデバッグ表示に切り替わります
image.png

9. ブレイクポイントで停止

image.png

10. 高度のデバッグを実施

ラムダ式を含んだ実装をイミディエイトウインドウで確認したり、
message引数の値や、その他の情報を自動パネルから確認したり

UiPath以上に有用なDebugが実施できます。
image.png

実践例

UiPathでExcelを操作する(活用編: 名前を付けて保存する[完成版])を利用して、実際にデバッグしてみます。

1. フローを作成する

image.png

2. プロセスアタッチ用のメッセージボックス追加

image.png

3. ロボ実行

メッセージボックスの表示を確認する
image.png

4. プロセスのアタッチ

Visual Studioでプロセスにアタッチする
image.png

5. デバッグ開始

ブレイクポイントで止まります
image.png

調査開始

a)アクティビティの引数調査

image.png

b) Comオブジェクトの調査

UiPathのデバッグでは調査できない、Comオブジェクトや動的ビューも、Visual Studioなら展開可能です。
image.png

終わりに

本投稿では、DLLと一緒にPDBファイルを配置し、VisualStudioからデバックメニューのプロセスアタッチから、UiPathのロボットを捕捉し、事前に設定したブレイクポイントで、処理を停止させる方法と実践的な調査例をご紹介しました。

もし、よければLGTMをお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web会議で手書きのアイデア伝えたくないですか?

はじめに

みなさん、こんにちは。
Web会議してますかー?(^o^)/

Web会議してるときに、どうしても「書いたもの」で伝えたくなることってないですか?

僕はオフライン会議でも、アイデアを共有するときは「手書き」で伝えたい派なんですよねー。(そんな派閥があるのか?(笑))

急にテレワーク、Web会議が広まって、今まで手書きで共有してきた情報がかなりやりづらくなった気がします。。。

調べてみると

やはり、すでにすごいアプリがありますね!

Microsoft社製のWhiteboard
https://www.microsoft.com/ja-JP/p/microsoft-whiteboard/9mspc6mp8fm4?activetab=pivot:overviewtab

すごいですよねー!
オンラインで共有しながら使えるみたいですよねー!!!

裏側の仕組み、どんななってるんだろーって興味深々です。

ただ、僕はもっとシンプルなものが欲しかったんです。。。

で、調べるのも面倒なので

自分で作ろう!

と思いました☆彡

ホワイトボードアプリ、その名も

HAKUBAN
001.png

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

ではでは!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 { } }
コード補完.cs
prop + tab
public int MyProperty { get; set; }

propg + tab
public int MyProperty { get; private set; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPF の DataGrid で動的にカラムを追加する

WPF の DataGrid で実行時にカラムの数や名前が確定するような動的なデータを扱いたいとおもったとき、DataTable を使うと実現することができます。

つくるもの

データとカラムを追加できる DataGrid をつくります。
https://github.com/ishida722/DataTableSample

datatable.gif

View をつくる

まず DataGird を配置して、ItemsSource をバインドします。今回は DataTableView というプロパティにバインドしました。

MainWindow.xaml
<DataGrid ItemsSource="{Binding DataTableView}" />

DataTable をつくる

ViewModel をつくります。まずテーブルデータを保持する DataTable オブジェクトをつくります。これは直接外部に公開しないので private にします。

DataTableViewModel.cs
private readonly DataTable dataTable = new DataTable();

この dataTable オブジェクトにデータを追加していきます。カラムを追加する場合は以下のようにします。

DataTableViewModel.cs
dataTable.Columns.Clear();
dataTable.Rows.Clear();
dataTable.Columns.Add("ID");
dataTable.Columns.Add("Name");

これでIDとNameというカラムが追加されました。次にデータを追加します。

DataTableViewModel.cs
var row = dataTable.NewRow();
row[0] = 0;
row[1] = "Jhon";
dataTable.Rows.Add(row);

IDが0のJhonというデータが追加できました。

View に通知する

作成した DataTable を DataGrid に表示するためには DataView を使います。具体的には以下のようなプロパティを用意します。

DataTableViewModel.cs
public DataView DataTableView => new DataView(dataTable);

DataTableView は View の DataGrid にバインドされています。このプロパティを読みだす度に、dataTable から DataView が作成されます。

DataTable は ObservableCollection ではないのでコレクションの変更を通知してくれません。なので自分で手動で変更を通知する必要があります。

DataTableViewModel.cs
private void NotifyTableUpdate()
{
    OnPropertyChanged(nameof(DataTableView));
}

テーブルを変更するたびに、NotifyTableUpdate() を実行する必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.1

AddTransient

有効期間が一時的なサービス (AddTransient) は、サービス コンテナーから要求されるたびに作成されます。 この有効期間は、軽量でステートレスのサービスに最適です。
要求を処理するアプリでは、一時的なサービスが要求の最後に破棄されます。

AddScoped

有効期間がスコープのサービス (AddScoped) は、クライアント要求 (接続) ごとに 1 回作成されます。
要求を処理するアプリでは、スコープ付きサービスは要求の最後で破棄されます。

AddSingleton

有効期間がシングルトンのサービス (AddSingleton) は、最初に要求されたときに作成されます (または、Startup.ConfigureServices が実行されて、サービス登録でインスタンスが指定された場合)。 以降の要求は、すべて同じインスタンスを使用します。 アプリをシングルトンで動作させる必要がある場合は、サービス コンテナーによるサービスの有効期間の管理を許可することをお勧めします。 クラス内のオブジェクトの有効期間を管理するために、シングルトンの設計パターンを実装してユーザー コードを提供しないでください。
要求を処理するアプリでは、ServiceProvider がアプリのシャットダウン時に破棄されたとき、シングルトン サービスが破棄されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NuGetのキャッシュをすべてクリアする

NuGetを利用していると、たまにキャッシュが「悪さ」をすることがあります。
手動(CLI)で消すこともできますが、キャッシュは複数あるのですべてを消すのはちょっと面倒です。

そこで、Visual StudioのGUI上からすべてのキャッシュを一括で削除する方法を紹介します。

まずは「ツール」→「オプション」を開きます。

SnapCrab_【NIT-03】帳票識別 - Microsoft Visual Studio_2020-7-22_8-40-43_No-00.png

つづいて「NuGetパッケージマネージャー」→「全般」を選択し、「すべてのNuGetキャッシュをクリア」をクリックしてください。

SnapCrab_【NIT-03】帳票識別 - Microsoft Visual Studio_2020-7-22_8-41-0_No-00.png

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 アプリケーション」を選択

image.png

任意の「プロジェクト名」及び「ソリューション名」を入力

image.png

テンプレートの選択

今回は「ASP.NET Core 3.1」「」のプロジェクトを選択します。

image.png

プロジェクトの作成完了

VisualStudioで以下のような画面が立ち上がります。

image.png

ソリューションエクスプローラーを眺める

」のテンプレートのソリューションエクスプローラーを眺めてみましょう。

こちらのテンプレートには、MVCテンプレートとは異なり、ControllersViewsといったMVC用のフォルダ等は一切含まれておりません。これから作成していきます。

image.png

MVCを有効にする

Startup.csConfigureServicesメソッドを以下のように変更してください。
ConfigureServicesメソッドでは、アプリケーションで使用するサービスを追加(登録)します。

AddControllersWithViewsは、Viewを使ったControllerを追加するという意味です。
これによって、ViewControllerを用いた(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フォルダを追加してください。

image.png

Controllerを追加

Controllersフォルダに空のControllerを追加してください。
ここではChatControllerとしておりますが、もちろん、ご自身が作成するアプリケーションに含まれる機能と対応したController名をつけてください。

image.png

作成した空のController

    public class ChatController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }

Viewを追加

アクションメソッド(Indexメソッド)のメソッド名を右クリック > ビューの追加

と進んでください。

image.png

「Razor ビュー」を選択します。
なお「スキャフォールディング」というのは、要は雛形を自動生成する機能のことを示します。ここではViewの雛形を自動生成してもらうために、この機能を使います。

image.png

続いてビュー名ですが、アクションメソッド名とします。
アクションメソッドで呼び出されるViewメソッドが
明示的に指定しない限り、/Views/コントローラー名/アクションメソッド名.cshtmlを読み込むためです。
テンプレートは「Empty(モデルなし)」を選択します。

image.png

Routeの設定

Startup.csConfigureメソッドを以下のように変更し、Routeも合わせて登録しておきます。
Configureメソッドでは、Routeの定義に代表されるように、アプリケーションがHTTPリクエストに対してどのように応答するかを定義します。
Routeについてはこちらの記事を参照してください。

変更前

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });

変更後

デフォルトのRouteは先ほど作成したControllerIndexアクションメソッドに向けておきます。

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Chat}/{action=Index}/{id?}"
        );
    });

動作確認

F5で実行します。
作成したControllerIndexアクションメソッドが呼び出されていることが確認できればOKです。

image.png

レイアウトページを追加

レイアウトページについてはこちらの記事を参照してください。

Views > Sharedフォルダを作成します。

image.png

Sharedフォルダを右クリック > 追加 > 新しい項目

と進んでください。

image.png

Razorレイアウトを選択します。
ファイル名は最初から_Layout.cshtmlとなっているので、そのままでOKです。

image.png

_Layout.cshtmlが追加できました。

image.png

スタートページ(_ViewStart.cshtml)を追加

スタートページ(_ViewStart.cshtml)についてはこちらの記事を参照してください。

Viewsフォルダを右クリック > 追加 > 新しい項目

と進んでください。

image.png

Razorビューの開始を選択します。
ファイル名は最初から_ViewStart.cshtmlとなっているので、そのままでOKです。

image.png

_ViewStart.cshtmlが追加できました。

image.png

動作確認

F5で実行します。
レイアウトページが適用されていることが確認できればOKです。
分かりづらいですが、タブの表示名に<title>が効いてIndexとなっています。

image.png

_ViewImports.cshtmlを追加

Views_ViewImports.cshtml を追加します。
このファイルでは、複数のViewで利用する名前空間のインポートをおこないます。

Viewsフォルダを右クリック > 追加 > 新しい項目

と進んでください。

image.png

Razorビューのインポートを選択します。
ファイル名は最初から_ViewImports.cshtmlとなっているので、そのままでOKです。

image.png

_ViewImports.cshtmlが追加できました。
以後、複数のViewで利用する名前空間が発生した場合には、このファイルでusingすると効率的です。

image.png

タグヘルパーの有効化

@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.TagHelpers

Configuration

Configurationクラスが appsettings.jsonに設定した値の取得機能を提供してくれます。
まだ設定値が無いので、ここではConfigurationを定義して、取得できる状態までとしておきます。

    public IConfiguration Configuration { get; set; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

wwwroot の追加

ルートにwwwrootフォルダを作成します。
スタイルシートや画像ファイル、JavaScriptファイルといった静的ファイルを格納するフォルダで、Webルートと呼びます。

image.png

静的ファイルを利用可能にする

スタイルシートや画像ファイル、JavaScriptファイルといった静的ファイルを利用可能にします。
Startup.csConfigureメソッドを以下のように変更し、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フォルダを作成します。

image.png

cssフォルダを右クリック > 追加 > 新しい項目

と進んでください。

image.png

スタイルシートを選択します。
ファイル名はsite.cssとしておきます。

image.png

スタイルシートを適用

レイアウトページで参照します。

<!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フォルダを作成します。

image.png

jsフォルダを右クリック > 追加 > 新しい項目

と進んでください。

image.png

JavaScriptファイルを選択します。
ファイル名はsite.jsとしておきます。

image.png

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>

以後はアプリケーション固有の機能を追加していきます。

以上となります。
ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#のみを使って、変更前後の差分表示のできる、ファイル名変更ソフトの作り方

概要

2020年にもなってファイル名変更ソフトを新しく作りました。名前はFileRenamerDiffです。
この記事では、ソフトウェアの記述解説をします。

ソフトウェアの内容やスクリーンショットは前回の記事を参照ください。

screenshot_ja.png

ざっくりいうと、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で書きました。

FileRenamerDiff.png

各層ごとに解説します。

Model

Model層ではファイルの探索、正規表現を使ったリネーム、リネーム保存、設定ファイル読み書きといったことを行っています。

ファイル探索

指定されたパス以下のファイルパスを取得します。このとき設定によって、隠しファイルやフォルダ・(狭義の)ファイルの両方か片方だけリストにするか切り替えます。
探索されたファイルの情報をFileElementModelにまとめて、そこにリネーム後のファイル名もあとで加えます。

リネーム処理(プレビュー)

設定はSettingAppModelにまとめてあり、このクラス全体をJSONにシリアライズして設定ファイルとして保存しています。設定には探索対象ディレクトリや複数の削除パターン・置換パターンが含まれています。

リネームの実行は.NETのRegexに置換後の文字列を加えたReplaceRegexクラスで行います。複数のファイル名に対して行うのを想定しているので、RegexOptionCompiledにしておきます。
ソフトウェアの設定では削除パターンと置換パターンで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()を呼ぶだけでよいですが、行フィルタを使用する場合はICollectionViewerFilterプロパティに表示判定用コールバックを指定して、行フィルタ基準が変わるたびに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 を使用しています。

image.png

ウインドウの周りがちょっと青いのがわかりますでしょうか。
それ以外のコントロールは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.cs
private 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を配置しておきます。
image.png

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にして、クリックされたらエクスプローラーで開くようにします。特定のファイルだけ手修正したい場合などに使用します。
image.png

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色空間への変換も)

差分表示

screenshot2.png
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.cs
public 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を使用しました。
各言語を横に並べて入力できるので便利です。
image.png

デフォルトの言語は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.1

FileRenamerDiff.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詳しい人いたら教えてほしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の値を”小さく”した。
処理を軽くする為には、値を大きくすべきだそうですが、大きくすると思ってもない動きをしたり、若干ワープしたような動きをするので、逆に小さくしてみると、スムーズに動くようになったので、小さくしてしまいました。

結果、目的がブレたような気もしますが、見た目のスムーズさは向上したのと、固まったかのような現象はほとんど起こらなくなったので、とりあえずはこのまま開発を進めようと思います。
新情報等見つけたら、再度試してこちらに追記します。

ここまで読んでいただき、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む