- 投稿日:2021-07-15T23:19:31+09:00
.NET MVCのViewのRazor構文でif文を使い、返す値を単に文字列にする方法
Viewのファイルを操作するにあたり、困ったことがあった。 @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> @Html.DisplayFor(modelItem => item.Password) </td> <td> @Html.DisplayFor(modelItem => item.done) </td> <td> @Html.ActionLink("Edit", "Edit", new { id=item.Id }) | @Html.ActionLink("Details", "Details", new { id=item.Id }) | @Html.ActionLink("Delete", "Delete", new { id=item.Id }) </td> </tr> } という構文があったときに、item.doneはbool型の値なので、 trueの場合は、済 falseの場合は、未 を表示したい場合、<td>タグの中を<div>などのhtmlタグで囲まずに使う方法が分からなかったので調べてみた。 方法は2つ。 文字列を<text>タグで囲む <td> @if(item.done){ <text>済</text> } else { <text>未</text> } </td> 文字の前に「@:」をつける <td> @if(item.done){ @:済 } else { @:未 } </td> 参考記事
- 投稿日:2021-07-15T20:38:12+09:00
WPFで既定のアプリでファイルを開く方法
はじめに 既定のアプリでファイルを開くというのは、Windowsを使っていると当たり前のような機能なのですが、WPFで実現しようとするとどうすればいいの?と思ってしまいます。 調べていくと、ファイルに応じた既定のアプリの起動というMicrosoftのドキュメントがありました。 ところが、これ、UWPのAPIを使用しています。つまりWPFではないわけです。 ということはWPFでUWPのAPIを使用できるようにすればいいということです。 UWPのAPIをWPFで使用できるようにする 実装方法ですが、.NetFrameworkを使用している場合と、.NET5を使用している場合とでは異なるようです。 .NetFrameworkの場合 Microsoft.Windows.SDK.ContractsというNuGetパッケージをインストールします .NET5の場合 プロジェクトファイルを以下のように設定します <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Library</OutputType> <TargetFramework>net5.0-windows10.0.17763.0</TargetFramework> <UseWPF>true</UseWPF> </PropertyGroup> </Project> TargetFrameworkのところがポイントで、 net5.0-windows10.0.17763.0の場合は Windows 10 Version 1809 net5.0-windows10.0.18362.0の場合は Windows 10 Version 1903 net5.0-windows10.0.19041.0の場合は Windows 10 Version 2004 となります この設定の留意事項としては、プロジェクトのプロパティの対象フレームワークがグレー表示になるところです。 既定のアプリでファイルを開く これでWPFでも利用できる環境が整ったので実装ですが、これはシンプルで、以下のコードを使うだけ IStorageFile file = await StorageFile.GetFileFromPathAsync(filePath) await Windows.System.Launcher.LaunchFileAsync(file) この引数のfilePathで開きたいファイルのパスを指定します 以上で完了です。 実装はとても簡単なのですが、ここまでたどりつくのにいろいろと調べたりと、時間がかかったので、同じような方がいればその助けになればと思います。
- 投稿日:2021-07-15T18:02:57+09:00
C#でテキストファイルを読み込む方法
本記事は以下のURLをもとに、C#(VisualStudio2019)でのテキストファイルの入出力を説明してみる試みです。 ・StreamWriter クラス https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter?view=net-5.0 ・StringReader クラス (https://docs.microsoft.com/ja-jp/dotnet/api/system.io.stringreader?view=net-5.0 →StreamWriterと対になっているのは、このURLだと思いますが、こちらは意図した通りに動かなかったので、以下を参照。) https://docs.microsoft.com/ja-jp/dotnet/standard/io/how-to-read-text-from-a-file まず、最初にプログラム冒頭で以下を定義 using System.IO; StreamWriterのサンプルコードは以下。Cドライブの中身を.txtファイルに吐き出させます。 qiita.C# namespace StreamReadWrite { class Program { static void Main(string[] args) { // Get the directories currently on the C drive. DirectoryInfo[] cDirs = new DirectoryInfo(@"c:\").GetDirectories(); // Write each directory name to a file. using (StreamWriter sw = new StreamWriter("CDriveDirs.txt")) { foreach (DirectoryInfo dir in cDirs) { sw.WriteLine(dir.Name); } } // Read and show each line from the file. string line = ""; using (StreamReader sr = new StreamReader("CDriveDirs.txt")) { while ((line = sr.ReadLine()) != null) { Console.WriteLine(line); } } } } これを実行することによって、Cドライブ直下のファイルのリストを 以下のフォルダに.txt形式で取得できます。 C:\Users\xxxx\source\repos\xxxx\xxxx\bin\Debug (xxxx:各PCの設定に依存) 引き続き、このファイルを読み込むために以下のサンプルプログラムを使用します。 qiita.C# namespace StreamReadWrite { class Program { public static void Main() { try { // Open the text file using a stream reader. using (var sr = new StreamReader("CDriveDirs.txt")) { // Read the stream as a string, and write the string to the console. Console.WriteLine(sr.ReadToEnd()); } } catch (IOException e) { Console.WriteLine("The file could not be read:"); Console.WriteLine(e.Message); } } } new StreamReader部分でテキストファイルを吐き出させたファイル名を指定すれば、読み込むことができます。 また、try-catch関数を使うことによって、 ファイルがない場合、ファイルが指定のフォルダ直下に無い旨を、コマンドプロンプト上のエラー文で明言できます。
- 投稿日:2021-07-15T17:28:29+09:00
実際のブラウザーのプロフィールをWebDriverで使う
普段使っているブラウザーのプロフィールをWebDriverで使いたい。 Chromeの場合 var options = new ChromeOptions(); options.AddArguments(@"--user-data-dir=C:\Users\Taro\AppData\Local\Google\Chrome\User Data"); options.AddArguments(@"--profile-directory=プロファイル名"); ChromeDriver driver = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), options); try { // Navigate to Url driver.Navigate().GoToUrl("https://www.google.co.jp/"); }finally { Console.ReadKey(); driver.Quit(); } Firefoxの場合 使いにくい。 FirefoxOptions options = new FirefoxOptions(); FirefoxProfile profile = new FirefoxProfile(@"Firefoxのプロファイル"); options.Profile = profile; FirefoxDriver driver = new FirefoxDriver(options); try { // Navigate to Url driver.Navigate().GoToUrl("https://www.google.co.jp/"); }finally { Console.ReadKey(); driver.Quit(); } 注意 ChromeもFirefoxも普段使っているプロファイルを想定している。 Chromeはコンソールでエラー表示が出るが、一応 動いてるっぽい。 Firefoxはブラウザーをいつものように使っていると、そのプロセスがロックファイルを使って、C#プログラムを起動できないセキュリティの高い仕様のようだ。プロセスを使っていない場合(ブラウザーを普段使いしていない場合)、プログラムは起動するがプロファイルが一時ファルダにコピーされるうえ、オーバーヘッドが大きく、やたら起動に時間がかかる。 解決方法は今の時点で見つからず。
- 投稿日:2021-07-15T17:28:29+09:00
実際のブラウザーのプロファイルをWebDriverで使う
普段使っているブラウザーのプロファイルをWebDriverで使いたい。 Chromeの場合 var options = new ChromeOptions(); options.AddArguments(@"--user-data-dir=C:\Users\Taro\AppData\Local\Google\Chrome\User Data"); options.AddArguments(@"--profile-directory=プロファイル名"); ChromeDriver driver = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), options); try { // Navigate to Url driver.Navigate().GoToUrl("https://www.google.co.jp/"); }finally { Console.ReadKey(); driver.Quit(); } Firefoxの場合 使いにくい。 FirefoxOptions options = new FirefoxOptions(); FirefoxProfile profile = new FirefoxProfile(@"Firefoxのプロファイル"); options.Profile = profile; FirefoxDriver driver = new FirefoxDriver(options); try { // Navigate to Url driver.Navigate().GoToUrl("https://www.google.co.jp/"); }finally { Console.ReadKey(); driver.Quit(); } 注意 ChromeもFirefoxも普段使っているプロファイルを想定している。 Chromeはコンソールでエラー表示が出るが、一応 動いてるっぽい。 Firefoxはブラウザーをいつものように使っていると、そのプロセスがロックファイルを使って、C#プログラムを起動できないセキュリティの高い仕様のようだ。プロセスを使っていない場合(ブラウザーを普段使いしていない場合)、プログラムは起動するがプロファイルが一時ファルダにコピーされるうえ、オーバーヘッドが大きく、やたら起動に時間がかかる。 解決方法は今の時点で見つからず。
- 投稿日:2021-07-15T15:48:40+09:00
iframe>シャドウDOMの先にある要素の取得、操作 --メモ--
MDNのHTMLのリファレンスで https://developer.mozilla.org/ja/docs/Web/HTML/Element/select のselectタグにアクセスしたかったが、Seleniumでエラーが出て取得できなかった。 調べたら、取得したいタグは iframeの中にあり、さらにShadow DOMがあってその中にあった。 通常のシンプルなロケータではうまくいかなかったので、一工夫してみたら上手くいった。 ChromeDriver driver = new ChromeDriver(); try { driver.Manage().Window.Maximize(); driver.Navigate().GoToUrl("https://developer.mozilla.org/ja/docs/Web/HTML/Element/select"); var iframe = driver.FindElement(By.CssSelector(".interactive")); driver.SwitchTo().Frame(iframe); IWebElement shadow_output = driver.FindElement(By.TagName("shadow-output")); IWebElement shadow_root = (IWebElement)driver.ExecuteScript("return arguments[0].shadowRoot", shadow_output); IWebElement select_pet = shadow_root.FindElement(By.Id("pet-select")); var selectObject = new SelectElement(select_pet); selectObject.SelectByIndex(2); } finally { Console.ReadKey(); driver.Quit(); }
- 投稿日:2021-07-15T14:38:30+09:00
Unity 2D オブジェクトをタッチしてイベントを発生させる方法
0.0 はじめに オブジェクトをタッチまたはクリックしてイベントを発生させる方法をまとめました。 ゲーム中にキャラクタやオブジェクトをタッチして操作する際にとても便利です。 準備として何でもよいので、まずは可視出来るオブジェクトを1つ作ってください。 何もなければ、下の手順でオブジェクトが作れます。 1. 下記の青い四角を右クリック、"名前を付けて画像を保存"してください。名前は何でもよいです(例:Square.png) 2. 保存した画像をUnityのProjectウインドウ内のAssetフォルダに入れてください(ドラッグ&ドロップ)。 3. そしてこの画像をそのままHierarchyへ入れてください(ドラッグ&ドロップ)。 1.0 Event Systemオブジェクトを追加する Hierarchyの中にEvent System(イベントシステム)を追加します。忘れがちなので最初にやりましょう。 Event Systemはタッチやクリックなどのユーザー入力を監視しイベントを伝える役割を果たします。 ?ポイント Event SystemはUIを作成した際に自動的にできます。1つあれば十分なので、すでにEvent SystemがHierarchyの中にあるのであれば追加する必要はありません。 HierarchyのCreateから UI -> Event Systemの順で選んで追加します。 2.0 Physics 2D Raycasterコンポーネントをカメラオブジェクトに追加する カメラにPhysics 2D Raycasterコンポーネントを追加します。こちらも忘れがちなので最初にやりましょう。 たいていの場合、シーンにMain Cameraがあると思います(シーンの初期設定でHierarchyの中にMain Cameraが作られています)。このカメラにRaycastコンポーネントを追加します。 ?ポイント Raycast(レイキャスト)とは、ある地点から特定方向へ発射された光線(Ray)のようなもので、そのRayと衝突したオブジェクトの情報を入手することができる機能です。今回はこのRaycastの機能を使ってタッチしたところにあるオブジェクトの情報を入手します。 HierarchyでMain Cameraを選択します。そしてAdd Componentをクリックして、検索窓にてPhysics 2D Raycasterを検索して選びます。ray と入力すると予測が出てきます。 Physics Raycasterも出ますが2Dの場合はPhysics 2D Raycasterの方を選んでください。 3.0 Event Triggerコンポーネントの追加 タッチするオブジェクトを選択して、Add ComponentでEvent Triggerを追加します。Add ComponentボタンはInspector(インスペクター)の一番下にあります。 検索の窓でEvent Triggerを探します。(Eと入力すると入力予測でEvent Triggerがすぐ下に出てきます。) これでEvent Triggerコンポーネントが追加できました。 Add New Event Typeをクリックするとイベントが選べます。 ↓は Pointer Down を選んだ場合 ここへ呼び出す関数を登録します。詳しくは6.0 呼び出し関数の登録を参考にしてください。 4.0 Colliderの追加 タッチさせたいオブジェクトにコライダー追加にコライダーを追加しましょう。下の図はBox Colliderを追加したところ。 ?ポイント コライダーはBox ColliderやCircle Colliderなど数種類あります。オブジェクトの形やタッチをしたい領域に合わせて選びましょう。 2Dの場合はBox Collider 2Dのように2Dと名前が付いたものを選びましょう。 5.0 スクリプトを作成 タッチしたときの動作が書かれたスクリプトを作成します。 今回は下のようなSquare.csというスクリプトを作成してオブジェクト(ここではSquareオブジェクト)にアタッチ(貼り付け)しましょう。 Square.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Square : MonoBehaviour { // タッチされた時に呼ばれる関数 public void OnTouched() { Debug.Log("オブジェクトがタッチされました!!"); } } 6.0 呼び出し関数の登録 右下の+を押して呼び出し関数の登録画面を作ります。 スクリプトがアタッチされたオブジェクトを"None(Object)"となっている枠に入れます(ドラッグ&ドロップします)。 "No Function"と書かれたバーをクリックしてスクリプト名、関数名を選びます。 ?ポイント 目的の関数が見つからないときは、 スクリプトが正しくアタッチされているか? 関数のアクセス修飾子がpublicになっているか? を確認してみてください。 7.0 おわりに コンソール画面に正しく表示されたら成功です。
- 投稿日:2021-07-15T13:03:02+09:00
インターフェースじゃなければ出来ない事
はじめに その昔DelphiやJavaを使っている時に、インターフェースの理解がモヤモヤしている時期がありました。 ググっていても、ふと気付くと何故かCOMの話になっていたりと、説明を読んでも良くわからない、メリットがいまいちピンと来ないという状態でした。 そして実際にインターフェースがなくても特に困っていなかったので、その頃の私には必要ないものだったのでしょう(※)。 ※今振り返ると「あそこは使えたな」という事もあったりしますが(^^; 当時、一番しっくりこなかったのが、 <Delphi> var x: IInterface; //インターフェースを代入できる??? begin ... ...のように「変数xにインターフェースが代入できる」という表現でした。 実装のないインターフェースを代入できるって、どういう事!? ...と混乱していました。 前提 多重継承NGでインターフェースが使える言語なら、大体当てはまると思います。 最近はC#の人なので以降はC#で記述しますが、Delphi、Java等も考え方は同じかと。 インターフェースじゃなければ出来ない事...? そんな訳で、自分設計のクラスを使っている分には別にインターフェースがなくても全然困ってなかった私ですが... ある時、他人設計のクラスを魔改造する必要が生じました。 具体的には.NETのWinFormsのコントロール(MS製やGrapeCity製だったり)に、カーソルキー(←↑↓→)移動の機能を追加する事になったのです(※)。 ※縦横に並んだ入力コントロールから、Tab移動以外で思った方向に進めるための機能です。 Windowsアプリの流儀から外れるのは百も承知ですので、その点へのご批判はナシでお願い致します。 TextBox、ComboBox、CheckBox等を継承して、カーソルキーの移動先のコントロールを設定して移動できるようにしました。 <C#>継承クラス public class XXXTextBox : TextBox { [Browsable(true)] [Description("左カーソルを押した時の移動先を設定します。")] [Category("移動先")] public Control ToLeft { get; set; } ... //こんな感じで各コントロールを継承して //上下左右の移動先を記憶、押された方向に進める //C#なのでプロパティ使ってますが、 //他言語なら各setter & getter を書けば良いです 因みに移動先の"Control"には、今回の魔改造コントロール以外も設定可能にしています。 視覚的に表現されるコンポーネントであるコントロールの基本クラスを定義します。 (MSのControlクラスの説明より引用) ここまでなら、別にインターフェースがなくても困りませんでした。 夜明け前... 各継承コントロールに移動する機能を実装してヤレヤレと思っていた所、今度は無効な項目をスキップして進めたいという仕様追加が発生... 項目Bで↓を押した時、項目Eが無効なら項目Gに進めたい! そこで、インターフェースに開眼していない私は、 「移動先が無効な場合、条件分岐で移動先が魔改造コントロールなら、更にその先を追って...」 ...とか考えていたのですが。 「あ、これって魔改造コントロールの種類が増える度にメンテしないとダメなヤツだ...」 <C#>魔改造コントロールを個別に判定... Control toDown = null; if (ctrl is XXXTextBox textBox) //魔改造コントロール個別に判断、 { toDown = textBox.ToDown; //個別に取得 } else if (ctrl is XXXComboBox comboBox) //魔改造コントロール個別に判断、 { toDown = comboBox.ToDown; //個別に取得 } ... //新しい魔改造コントロールが増える度に要対応... //※C#の場合、リフレクションで判定&取得って手もあるけど(^^; ...と思った瞬間に、ここがインターフェースの出番なのかっ!とビビッと開眼! 開眼!インターフェースじゃなければ出来ない事! 開眼した私は下記のインターフェースを作成、 <C#>インターフェース public interface IXXXControl { Control ToLeft { get; set; } //こんな感じで上下左右を記憶するインターフェースを作成 //C#なのでプロパティ使ってますが、 //他言語なら各setter & getter を書けば良いです 各継承コントロールにインターフェースを追加。 <C#>継承クラス public class XXXTextBox : TextBox, IXXXControl { [Browsable(true)] [Description("左カーソルを押した時の移動先を設定します。")] [Category("移動先")] public Control ToLeft { get; set; } //クラス宣言にIXXXControlを書き足しただけ。 //インターフェースの内容は元々実装してあるので無問題。 こうしておくと先の判定も、 <C#>魔改造コントロール用インターフェースの実装で判定 Control toDown = null; if (ctrl is IXXXControl xxxCtrl) //新しい魔改造コントロールが増えても無問題! { toDown = xxxCtrl.ToDown; //IXXXControlの実装が保証されている! } ...で魔改造コントロールの判定が出来てメンテナンスフリーになるやん! まとめ という事で大分遅まきながらインターフェースにしか出来ない事が腹落ちした瞬間でした。 自分が直接手出しできないクラスツリーに介入して、インターフェース単位でグルーピングできる! 昔しっくりこなかった下記コードも、今はすっかり馴染むようになりました! <Delphi> var x: IInterface; //×インターフェースを代入できる??? //〇インターフェースを実装したインスタンスを代入できる // &インターフェースの機能が使える // (インターフェースにない機能は使えない) begin ~ ※近年Delphiを触る機会はないですが...(^^; 個人的にオブジェクト指向の中で、インターフェースの理解が一番引っかかった部分でした。 同じようなモヤモヤを抱えている方の参考になれば幸いです! 更新 2021.07.15 PlantUMLのイメージを、画像からMarkdown記述に変更
- 投稿日:2021-07-15T12:45:46+09:00
Docker & dotnet repl で簡単にLinux環境での動作を確認する。
はじめに Linux環境でC#を動かしたときの動作を確認する場合、Windowsで開発している場合はDockerで動かすのがお手軽です。VSCodeのリモートでバック機能を使えば、普段と同じノリでコードを書いて確認もできるし便利ですね。WSL環境を作っておくというのもよいですね。 ただ、特定のメソッドの結果などをお手軽に確認したい場合に毎回コンパイルして、というのはちょっとめんどくさいので、以前紹介した.NETのREPL拡張を使うと簡単に確認することができます。 現在日時の確認 ここでは、Alpineでローカル時間をJSTにしている場合のDateTimeやDateTimeOffsetのNowが何を返すのかを確認していきます。 まずは.NETのAlpineイメージをインタラクションモードで起動して、.NET REPLをインストールします。 ❯ docker run --rm -it mcr.microsoft.com/dotnet/sdk:5.0-alpine # dotnet tool install -g dotnet-repl # export PATH="$PATH:/root/.dotnet/tools" 現時点でのdateコマンドの結果と、DateTime, DateTimeOffset で現在日付を表示するとUTCで表示されることが分かります。 # date Thu Jul 15 02:09:55 UTC 2021 # dotnet repl _ _ _____ _____ ____ _____ ____ _ ____ _ _ | \ | | | ____| |_ _| | _ \ | ____| | _ \ | | _ / ___| _| || |_ | \| | | _| | | | |_) | | _| | |_) | | | (_) | | |_ .. _| _ | |\ | | |___ | | | _ < | |___ | __/ | |___ _ | |___ |_ _| (_) |_| \_| |_____| |_| |_| \_\ |_____| |_| |_____| (_) \____| |_||_| .NET Interactive ? Spectre.Console C# > TimeZone.CurrentTimeZone ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ { System.CurrentSystemTimeZone: StandardName: Coordinated Universal Time, DaylightName: Coordinated Universal │ │ Time } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTime.Now ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 02:10:57Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTime.UtcNow ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 02:10:59Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTimeOffset.UtcNow ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 02:11:02Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTimeOffset.UtcNow.LocalDateTime ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 02:11:11Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ タイムゾーンをJSTにして確認 まずはapkで必要なモジュールをインストールして、dateコマンドで現在日付を確認するとJSTで表示されますね。 # apk add --no-cache jq tzdata fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/main/x86_64/APKINDEX.tar.gz fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/community/x86_64/APKINDEX.tar.gz (1/3) Installing oniguruma (6.9.6-r0) (2/3) Installing jq (1.6-r1) (3/3) Installing tzdata (2021a-r0) Executing busybox-1.32.1-r6.trigger OK: 59 MiB in 35 packages # cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime # echo "Asia/Tokyo" > /etc/timezone # TZ=Asia/Tokyo # date Thu Jul 15 11:30:43 JST 2021 次にもう一度.NET REPLに入って確認するとTimeZoneがJSTになってローカル時間で表示されましたね。 # dotnet repl _ _ _____ _____ ____ _____ ____ _ ____ _ _ | \ | | | ____| |_ _| | _ \ | ____| | _ \ | | _ / ___| _| || |_ | \| | | _| | | | |_) | | _| | |_) | | | (_) | | |_ .. _| _ | |\ | | |___ | | | _ < | |___ | __/ | |___ _ | |___ |_ _| (_) |_| \_| |_____| |_| |_| \_\ |_____| |_| |_____| (_) \____| |_||_| .NET Interactive ? Spectre.Console C# > TimeZone.CurrentTimeZone ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ { System.CurrentSystemTimeZone: StandardName: Japan Standard Time, DaylightName: Japan Daylight Time } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTime.Now ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 11:32:45Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTime.UtcNow ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 02:32:48Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTimeOffset.UtcNow ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 02:32:51Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ C# > DateTimeOffset.UtcNow.LocalDateTime ╭─✔───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 2021-07-15 11:32:57Z │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
- 投稿日:2021-07-15T11:18:51+09:00
C# PowerPointで文書内の文字を置換
今日はSpire.Presentationという無料のライブラリを使って、パワーポイントで文書内の文字を置換する方法を紹介します。 下準備 1.E-iceblueの公式サイトからFree Spire.Presentation無料版をダウンロードしてください。 2.Visual Studioを起動して新規プロジェクトを作成してから、インストールされたファイルにあった相応しいSpire. Presentation.dllを参照に追加してください。 (Net 4.0を例としたら、デフォルトパスは“Bin→NET4.0→Presentation.dll”というようになります。) 元のファイル public ReplaceText() { { //Dictionary オブジェクトを作成し、itemを追加します。 Dictionary TagValues = new Dictionary(); TagValues.Add("日本語", "韓国語"); //PowerPointファイルをロードします。 Presentation presentation = new Presentation(); presentation.LoadFromFile("Sample.pptx", FileFormat.Pptx2010); //ReplaceTagsメソッドで文字を置換します。 ReplaceTags(presentation.Slides[0], TagValues); //保存します。 presentation.SaveToFile("Result.pptx", FileFormat.Pptx2010); System.Diagnostics.Process.Start("Result.pptx"); } } public void ReplaceTags(Spire.Presentation.ISlide pSlide, Dictionary TagValues) { foreach (IShape curShape in pSlide.Shapes) { if (curShape is IAutoShape) { foreach (TextParagraph tp in (curShape as IAutoShape).TextFrame.Paragraphs) { foreach (var curKey in TagValues.Keys) { if (tp.Text.Contains(curKey)) { tp.Text = tp.Text.Replace(curKey, TagValues[curKey]); } } } } } } 実行結果
- 投稿日:2021-07-15T11:05:58+09:00
C# ExcelをSVGに変換
今回はC#ではSpire.XLSという無料のライブラリを活用して、Excel文書をSVGに変換する方法を紹介してさしあげましょう。 下準備 1.E-iceblueの公式サイトからFree Spire.XLS無料版をダウンロードしてください。 2.Visual Studioを起動して新規プロジェクトを作成してから、インストールされたファイルにあった相応しいSpire.XLS.dllを参照に追加してください。 (Net 4.0を例としたら、デフォルトパスは“Bin→NET4.0→XLS.dll”というようです。) using System.Drawing.Imaging; using System.IO; namespace Convert { class Program { static void Main(string[] args) { //ファイルをロードします。 Workbook workbook = new Workbook(); workbook.LoadFromFile("Sample.xlsx"); //ToSVGStream(Stream stream)メソッドでシートを別々にSVGで保存 for (int i = 0; i < workbook.Chartsheets.Count; i++) { FileStream fs = new FileStream(string.Format("chartsheet-{0}.svg", i), FileMode.Create); workbook.Chartsheets[i].ToSVGStream(fs); fs.Flush(); fs.Close(); } } } } 実行結果
- 投稿日:2021-07-15T02:43:50+09:00
WindowsフォームのForm_Load()メソッドをWPFのMVVMで置き換える
はじめに WindowsフォームアプリからWPFアプリに移行すると MVVMの呪縛にかかり、コードビハインドに何も書けなくなる (※ 個人差があります) たとえば、フォーム(WPFではウィンドウ)を読み込んだ直後に何かの初期化処理をしたい場合など・・ フォームアプリでは、フォームをダブルクリックすると自動的にForm_Load()メソッドが作られるので、そこに処理を書けばよい 一方、WPFアプリでは・・・Windowをいくら高速クリックしてもメソッドは生成されない? これは困ったことなので、どうしたらいいかを書く 目次のようなもの << ゴール >> WPFアプリにおいて、ウィンドウが読み込まれた直後になんらかの初期化メソッドを呼ぶ (ただし、コードビハインドには何も追記しない) 流れとしては、まずフォームアプリで所望の動作をさせてみる。 そのあと、WPFアプリで同じことをする フォームアプリ おなじみのWindowsフォームアプリ プロジェクトを立ち上げ直後は、まっさらなフォームが表示される フォームの上でマウスを左ダブルクリックすると、メソッドが生成される // フォームをダブルクリックすると、このメソッドが生成される private void Form1_Load(object sender, EventArgs e) { // フォームをロードし終えたときの動作 } あとはこの関数の中に、初期化などの処理を書けばいい フォームアプリについては以上 WPFアプリ つづいてWPFアプリの2パターン コードビハインドを使う場合と、使わない場合 Case1: MVVMの呪縛に囚われていない場合 WindowタグにLoaded="Window_Loaded"を追加する XAMLファイル <Window x:Class="MyWpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MyWpfApp" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" + Loaded="Window_Loaded"> <Grid> </Grid> </Window> コードビハインドに同名のメソッドを追加し、中身を書けば終わり コードビハインド public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } + private void Window_Loaded(object sender, RoutedEventArgs e) + { + // ここに処理を書く + } } Tips) IDEの補助機能を有効につかう ViewでLoaded="まで入力すると、「新しいイベントハンドラー」ポップアップが表示される このポップアップをクリックすると、コードビハインドにメソッドが自動で追加される おまけ)ほかのイベントについて ウィンドウのプロパティを開いて右上の⚡を押せば、使えるであろうたくさんのイベントが表示される Case2: MVVMの呪縛でコードビハインドに何も書けない場合(本題) MVVMに従う場合、何はともあれViewModelのファイルを作成する 今回はMainWindowViewModel.csをプロジェクトに追加するものとする NuGetで必要なライブラリをインストールする Xaml.Behaviors.Wpfをインストールする これはViewのイベントとViewModelに定義したコマンドを紐づけるためにつかう 詳しくは・・・System.Windows.Interactivity.dll から Xaml.Behaviors.Wpf へ ReactivePropertyをインストールする これはコマンドを簡単に書くためのライブラリである View (XAML) を編集する さきほどのLoaded=...イベントは削除する。 代わりにiに関わるものを追加する。 新たに追加した行を見ると イベントトリガーという名前からして、なにかのイベントに連動して処理が行われそうだとわかる Loadedと書かれており、きっとウィンドウがロードされたイベントを指定していそうだとわかる Command="{Binding ...}"でViewModelのコマンドに連動しそうだとわかる MainWindow.xaml <Window x:Class="MyWpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MyWpfApp" + xmlns:i="http://schemas.microsoft.com/xaml/behaviors" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" - Loaded="Window_Loaded"> + <i:Interaction.Triggers> + <i:EventTrigger EventName="Loaded"> + <i:InvokeCommandAction Command="{Binding Loaded_Command}"/> + </i:EventTrigger> + </i:Interaction.Triggers> <Grid> </Grid> </Window> ViewModelを編集する ViewModelはINotifyPropertyChangedを実装する なぜ必要か? --->>> Q. あれ、ReactivePropertyだとINotifyPropertyChanged要らないんじゃなかったっけ? ReactiveCommandクラスのLoaded_CommandがViewにバインドされる .Subscribeの引数(ラムダ式)に書かれた処理が実行される MainWindowViewModel.cs using Reactive.Bindings; using System.ComponentModel; namespace MyWpfApp { public class MainWindowViewModel : INotifyPropertyChanged { // Viewにバインドするコマンド public ReactiveCommand Loaded_Command { get; } = new(); public MainWindowViewModel() // コンストラクター内で { // ボタンが押された時の動作を定義する Loaded_Command.Subscribe(()=> System.Diagnostics.Debug.WriteLine("Loaded !!")); } // これはひとまず気にしないでいい public event PropertyChangedEventHandler PropertyChanged; } } このまま実行しても"Loaded !!"とは表示されない 最後にもう1度、View (XAML) を編集する ViewとViewModelをつなぐために、Viewを編集する DataContextにMainWindowViewModelを指定する MainWindow.xaml <Window x:Class="MyWpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MyWpfApp" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> + <Window.DataContext> + <local:MainWindowViewModel/> + </Window.DataContext> <i:Interaction.Triggers> <i:EventTrigger EventName="Loaded"> <i:InvokeCommandAction Command="{Binding Loaded_Command}"/> </i:EventTrigger> </i:Interaction.Triggers> <Grid> </Grid> </Window> なお、MVVMの呪縛に囚われていない場合は コードビハインドでthis.DataContext = new MainViewModel();としてもよい。 まとめ ことLoadedイベントだけに関してみれば、WinFormのほうが何も考えずにできてよいかもしれない しかし、ひるがえってみれば、LoadedのためだけにNuGetを通してわざわざ2つのライブラリをインストールするWPFに対しては、なにかこの先大きなことをしてくれるのではないかと期待してしまう というまとめ。
- 投稿日:2021-07-15T00:50:49+09:00
【Unity(C#)】UIを画面に映さずにスクリーンショットし、ios・Androidそれぞれの写真アプリに保存
はじめに ARなどでUIを非表示にしてスクショを取りたい場面があるかと思います。 今回はいろいろ調べて下記アセットに辿り着きました。 ■ Unity Native Gallery Plugin 自前で用意することがほとんどなくなる神ライブラリだったので使い方をメモします。 バージョン情報 諸々名前 バージョン Unity 2019.4.8f1(LTS) UniTask 2.2.4 ARFoundation 4.1.1 ARCore XR Plugin 4.1.1 ARCore Kit Plugin 4.1.1 AR Subsystems 4.1.1 XR Plugin Management 3.2.16 Unity Native Gallery Plugin 1.6.4 下準備 ■ Android 「Player Settings」で「Write Permission」を「External (SDCard)」に変更します。 ■ ios どうやら、Permissionなどの記述は自動的に行ってくれるようです。 下記がProject Settingsに追加されていました。 サンプルコード using System; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UI; /// <summary> /// スクショ /// 適当なオブジェクトにアタッチ /// </summary> public class SaveScreenShot : MonoBehaviour { [SerializeField] private Button _ssButton; [SerializeField] private Camera _camera; void Start() { _ssButton.onClick.AddListener(SaveScreenShotToGallery); } private void OnDestroy() { _ssButton.onClick.RemoveListener(SaveScreenShotToGallery); } /// <summary> /// ボタンに登録するスクショ処理 /// </summary> private void SaveScreenShotToGallery() { var ct = this.GetCancellationTokenOnDestroy(); SaveScreenShotToGalleryAsync(ct).Forget(); } /// <summary> /// スクショを作成して保存する /// </summary> private async UniTask SaveScreenShotToGalleryAsync(CancellationToken ct) { //任意のフレームの描画処理が終わるまで待つ await UniTask.WaitForEndOfFrame(ct); //Cameraの描画領域をRenderTextureとして取り出す var rt = new RenderTexture(_camera.pixelWidth, _camera.pixelHeight, 24); var prev = _camera.targetTexture; _camera.targetTexture = rt; _camera.Render(); _camera.targetTexture = prev; RenderTexture.active = rt; var screenShot = new Texture2D( _camera.pixelWidth, _camera.pixelHeight, TextureFormat.RGB24, false); screenShot.ReadPixels(new Rect(0, 0, screenShot.width, screenShot.height), 0, 0); screenShot.Apply(); var date = DateTime.Now.ToString("yyyyMMdd"); //CameraのRenderTextureを元に画像を作成して保存 NativeGallery.SaveImageToGallery(screenShot, "GalleryTest", $"{date}.png" ); } } 保存先には指定した名前のアルバムが作成されます。 アプリをアンインストールしてもアルバムと写真は残ります。 任意のカメラのRenderTextureをスクショに利用しています。 これにより、Screen Space OverlayのUIはスクリーンショットに映りません。 なぜ映らないのか、それっぽい言及があったので載せときます。 The 'overlay' mode doesn't go through the normal render pipeline so it's not injected into a normal camera render. 【引用元】:https://forum.unity.com/threads/render-a-canvas-to-rendertexture.272754/ デモ 赤がWorld SpaceのUI、白がCubeです。 スクリーンショットと書かれたボタンはScreen Space OverlayのUIです。 スクショのタイミングで若干画面が止まりますが、まあ許容範囲内でしょう。 下記が実際に保存された画像です。 Screen Space OverlayのUI以外が画面に映ります。 ネイティブのUIなども映らないので通知などに邪魔されることもなく便利ですね。 おわりに 動画もUnity Native Gallery Pluginで保存できるらしいですが、 そもそも動画を書き出す処理が面倒なのでそこは自前かアセットの力を借りる必要がありそうです。 参考リンク 【Unity】iOS の写真や Andoid のギャラリーに画像や動画を保存できる「Unity Native Gallery Plugin」紹介 【Unity】スクリーンショットを保存する
- 投稿日:2021-07-15T00:04:45+09:00
オリジナル文字コード(UTF-16, JIS8混在)での文字コード変換プログラムを作ってみました
はじめに 同じような悩みを持つ方がどれだけいるかは全く謎ですが,オリジナル文字コードを変換して,別の環境に移植する必要があったため,その方法を記載します。取り上げる内容は以下のとおりです。 対象は文字コードですが,何らしかのコードを変換するという仕組みとして共通に使用できます 文字コード初心者ならではの躓きを取り上げます サロゲートペアという unicode(ucs2) の範囲を超えてしまった文字を扱います C#(初心者)でスマートに実装しようとしたのですが,途中ライブラリの使用をあきらめています 今後の課題を取り上げます 環境 Windows 10 VS Code .Net Core SDK 3.1 工数 約1人日(以下,内訳) 実装・テスト:1/4人日 qiitaへの投稿記載:1/4人日 サロゲートペア変換API(UNICODE符号位置からサロゲートペアへの変換)の捜索:2/4人日 3については結局既存のライブラリを使用して実装することを断念して,計算することにしました。 背景 以下の理由でシステムの若返りを図りたい。 技術者が足りないため,保守ができない 開発環境の選択が少ない データの利用が難しい(必ず難解な文字コード変換を挟むことになる)。 今回作成したもの 下の赤枠の文字コード変換するためのツール 変換ツールの材料 1バイトデータ(JIS8)を2バイトデータ(BMP)に置換する変換テーブル .\1ByteT\1ByteToBMP.csv : 23,0023 24,0024 25,0025 : 2バイトデータを2バイトデータ(BMP)又は面02(U+2XXXX)に置換する変換テーブル .\2ByteT\2ByteToBMPandSIP.csv : 2127,2127 212B,212B : A0C3,2373F A0C4,23763 : 項目リスト(固定長データの区切り単位:レコードサイズ254バイト) 引数3 1B ,1,10 1B ,11,2 1B ,13,2 1B ,15,2 2B ,17,100 2B ,117,60 2B ,177,40 1B ,217,30 1B ,247,7 1B ,254,1 変換プログラム 変換プログラムのインターフェースは以下のとおり コマンド入力 module名 【引数1:入力ファイル名(固定長)】 【引数2:出力ファイル名(csv)】 【引数3:項目リスト】 Program.cs using System; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Collections.Generic; using System.Diagnostics; class Program { static void Main(string[] args) { // CSVファイルの読み込み string filePath = @".\JUFx_KMJ\JUF1_KMJ.csv"; string filePath2 = @".\JUFx_KMJ\JUF2_KMJ.csv"; string filePath3 = args[2]; string filePath4 = args[0]; string filePath5 = args[1]; var myTable = new Dictionary<string, string>(); var myTable2 = new Dictionary<string, string>(); using (StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("UTF-8"))) { while (reader.Peek() >= 0) { // 読み込んだ文字列をカンマ区切りで配列に格納 string[] cols = reader.ReadLine().Split(','); // コメント行を飛ばす if(Regex.IsMatch(cols[0], "^#")){continue;} for (int n = 0; n < cols.Length; n++) { //For Debug //Console.Write(cols[n] + ","); } // Dictionary生成 myTable.Add(cols[0], cols[1]); } } using (StreamReader reader2 = new StreamReader(filePath2, Encoding.GetEncoding("UTF-8"))) { while (reader2.Peek() >= 0) { // 読み込んだ文字列をカンマ区切りで配列に格納 string[] cols = reader2.ReadLine().Split(','); // コメント行を飛ばす if(Regex.IsMatch(cols[0], "^#")){continue;} for (int n = 0; n < cols.Length; n++) { //For Debug //Console.Write(cols[n] + ","); } // Dictionary生成 myTable2.Add(cols[0], cols[1]); } } // 最後まで読み込む List<string[]> readCsvList = new List<string[]>(); using (StreamReader readCsvObject = new StreamReader(filePath3, Encoding.GetEncoding("utf-8"))) { while (!readCsvObject.EndOfStream) { var readCsvLine = readCsvObject.ReadLine(); readCsvList.Add(readCsvLine.Split(',')); } } var recSize = Convert.ToInt32(readCsvList[readCsvList.Count - 1][1]) + Convert.ToInt32(readCsvList[readCsvList.Count - 1][2]) - 1; //Console.Write("recSize: " + recSize + "\n"); //Console.Write(readCsvList.Count + "\n"); using (FileStream fs = new FileStream(filePath4, FileMode.Open, FileAccess.Read)) { using (FileStream fsout = new FileStream(filePath5, FileMode.Create, FileAccess.Write)) { using (var writer = new BinaryWriter(fsout)) { // バイナリファイル読み出し int fileSize = (int)fs.Length; // ファイルのサイズ byte[] buf = new byte[fileSize]; // データ格納用配列 int readSize; // Readメソッドで読み込んだバイト数 int remain = fileSize; // 読み込むべき残りのバイト数 int bufPos = 0; // データ格納用配列内の追加位置 //マルチレイアウトは許可しない Debug.Assert(fileSize%recSize == 0); while (remain > 0) { readSize = fs.Read(buf, bufPos, Math.Min(recSize, remain)); bufPos += readSize; remain -= readSize; } for(int i = 0; i < fileSize; i++) { for(int j = 0; j < readCsvList.Count; j++) { if((i != 0) && ((i%recSize) == 0)) { writer.Write(Convert.ToUInt16("000A", 16)); } if((j != 0) && ((i%recSize) == (Convert.ToInt32(readCsvList[j][1])-1))) { writer.Write(Convert.ToUInt16("002C", 16)); } // 1バイトテーブル if(Regex.IsMatch(readCsvList[j][0], "EBC") && ((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) && ((i%recSize) < (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1))) { //Console.Write(myTable[buf[i].ToString("X2")] + "\n"); writer.Write(Convert.ToUInt16(myTable[buf[i].ToString("X2")], 16)); break; } // 2バイトテーブル if(Regex.IsMatch(readCsvList[j][0], "EKJ") && ((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) && ((i%recSize) < (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1))) { Encoding utf8 = Encoding.GetEncoding("UTF-8"); int num = utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]); if(utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]) <= 4) { //Console.Write(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")] + "\n"); writer.Write(Convert.ToUInt16(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")], 16)); } else { //サロゲートペア計算(ライブラリが見つからなかった) var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16); //Console.Write("codePoint: " + codePoint + "\n"); var codePoint2 = (codePoint & ~0x10000) >> 10; //Console.Write("plane: " + plane + "\n"); var lead = (codePoint2 & 0x3F) | 0xD800; //Console.Write("lead: " + lead + "\n"); var trail = (codePoint & 0x3FF) | 0xDC00; //Console.Write("trail: " + trail + "\n"); writer.Write(Convert.ToUInt16(lead)); writer.Write(Convert.ToUInt16(trail)); } i++; break; } } } } } } } } 実装のポイント 1 テーブルのキー読み出しでエンディアン変換 以下のコードでbuf(元データ:ビッグエンディアン)のデータを読み出し,そのままビッグエンディアン順でキーの指定を行っている(リトルエンディアンであれば,buf[i]とbuf[i+1]の指定が逆になる。)。 ビッグエンディアンデータの読みだしとキー指定 myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")] 2 サロゲートペアの計算ロジックを作成した 2バイトデータが変換テーブルを使用して,面02(U+2XXXX)に変換された場合,UTF-16ではサロゲートペアに更に変換が必要となる。その際,C#で変換ライブラリを見つけることができなかったため,以下の変換を実装した。 サロゲートペア計算とバイナリ書き込み var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16); //Console.Write("codePoint: " + codePoint + "\n"); var codePoint2 = (codePoint & ~0x10000) >> 10; //Console.Write("plane: " + plane + "\n"); var lead = (codePoint2 & 0x3F) | 0xD800; //Console.Write("lead: " + lead + "\n"); var trail = (codePoint2 & 0x3FF) | 0xDC00; //Console.Write("trail: " + trail + "\n"); writer.Write(Convert.ToUInt16(lead)); writer.Write(Convert.ToUInt16(trail)); ネット上にはいくつか実装方法があったが,乗算・除算器を使用するものだったため,論理演算及びシフト演算のみのコードに書き直した。また,サロゲートペアは32bitデータになるが,UTF-16なので16ビットごとにリトルエンディアンによる書き込みが必要。 今後 計算時間が結構掛かっている割に,以下のようにCPU,DISKIOは全力を出し切っていないので,HyperThread OFF にして,ファイルを4分割し,Threadを4本立てて同時実行する実装を現在検討している。
- 投稿日:2021-07-15T00:04:45+09:00
オリジナル文字コード(UTF-16, JIS8混在)からユニコード(UTF-16)へ変換する文字コード変換プログラムを作ってみました
はじめに 同じような悩みを持つ方がどれだけいるかは全く謎ですが,オリジナル文字コードを変換して,別の環境に移植する必要があったため,その方法を記載します。取り上げる内容は以下のとおりです。 対象は文字コードですが,何らしかのコードを変換するという仕組みとして共通に使用できます 文字コード初心者ならではの躓きを取り上げます サロゲートペアという unicode(ucs2) の範囲を超えてしまった文字を扱います C#(初心者)でスマートに実装しようとしたのですが,途中ライブラリの使用をあきらめています 今後の課題を取り上げます 環境 Windows 10 VS Code .Net Core SDK 3.1 工数 約1人日(以下,内訳) 実装・テスト:1/4人日 qiitaへの投稿記載:1/4人日 サロゲートペア変換API(UNICODE符号位置からサロゲートペアへの変換)の捜索:2/4人日 3については結局既存のライブラリを使用して実装することを断念して,計算することにしました。 背景 以下の理由でシステムの若返りを図りたいと考えています。 技術者が足りないため,保守ができない 開発環境の選択が少ない データの利用が難しい(必ず難解な文字コード変換を挟むことになる)。 今回作成したもの 下の赤枠の文字コード変換するためのツール 変換ツールの材料 1バイトデータ(JIS8)を2バイトデータ(BMP)に置換する変換テーブル .\1ByteT\1ByteToBMP.csv : 23,0023 24,0024 25,0025 : 2バイトデータを2バイトデータ(BMP)又は面02(U+2XXXX)に置換する変換テーブル 【特徴】変換元の環境では,BMP面の一部をサロゲートペア領域として使用しています(固定長データ処理系のため)。 .\2ByteT\2ByteToBMPandSIP.csv : 2127,2127 212B,212B : A0C3,2373F A0C4,23763 : 項目リスト(固定長データの区切り単位:レコードサイズ254バイト) 引数3 1B ,1,10 1B ,11,2 1B ,13,2 1B ,15,2 2B ,17,100 2B ,117,60 2B ,177,40 1B ,217,30 1B ,247,7 1B ,254,1 変換プログラム 1 変換プログラムのインターフェースは以下のとおりです。 コマンド入力 module名 【引数1:入力ファイル名(固定長)】 【引数2:出力ファイル名(csv)】 【引数3:項目リスト】 2 変換プログラムは以下のとおりです。 Program.cs using System; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Collections.Generic; using System.Diagnostics; class Program { static void Main(string[] args) { // CSVファイルの読み込み string filePath = @".\JUFx_KMJ\JUF1_KMJ.csv"; string filePath2 = @".\JUFx_KMJ\JUF2_KMJ.csv"; string filePath3 = args[2]; string filePath4 = args[0]; string filePath5 = args[1]; var myTable = new Dictionary<string, string>(); var myTable2 = new Dictionary<string, string>(); using (StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("UTF-8"))) { while (reader.Peek() >= 0) { // 読み込んだ文字列をカンマ区切りで配列に格納 string[] cols = reader.ReadLine().Split(','); // コメント行を飛ばす if(Regex.IsMatch(cols[0], "^#")){continue;} for (int n = 0; n < cols.Length; n++) { //For Debug //Console.Write(cols[n] + ","); } // Dictionary生成 myTable.Add(cols[0], cols[1]); } } using (StreamReader reader2 = new StreamReader(filePath2, Encoding.GetEncoding("UTF-8"))) { while (reader2.Peek() >= 0) { // 読み込んだ文字列をカンマ区切りで配列に格納 string[] cols = reader2.ReadLine().Split(','); // コメント行を飛ばす if(Regex.IsMatch(cols[0], "^#")){continue;} for (int n = 0; n < cols.Length; n++) { //For Debug //Console.Write(cols[n] + ","); } // Dictionary生成 myTable2.Add(cols[0], cols[1]); } } // 最後まで読み込む List<string[]> readCsvList = new List<string[]>(); using (StreamReader readCsvObject = new StreamReader(filePath3, Encoding.GetEncoding("utf-8"))) { while (!readCsvObject.EndOfStream) { var readCsvLine = readCsvObject.ReadLine(); readCsvList.Add(readCsvLine.Split(',')); } } var recSize = Convert.ToInt32(readCsvList[readCsvList.Count - 1][1]) + Convert.ToInt32(readCsvList[readCsvList.Count - 1][2]) - 1; //Console.Write("recSize: " + recSize + "\n"); //Console.Write(readCsvList.Count + "\n"); using (FileStream fs = new FileStream(filePath4, FileMode.Open, FileAccess.Read)) { using (FileStream fsout = new FileStream(filePath5, FileMode.Create, FileAccess.Write)) { using (var writer = new BinaryWriter(fsout)) { // バイナリファイル読み出し int fileSize = (int)fs.Length; // ファイルのサイズ byte[] buf = new byte[fileSize]; // データ格納用配列 int readSize; // Readメソッドで読み込んだバイト数 int remain = fileSize; // 読み込むべき残りのバイト数 int bufPos = 0; // データ格納用配列内の追加位置 //マルチレイアウトは許可しない Debug.Assert(fileSize%recSize == 0); while (remain > 0) { readSize = fs.Read(buf, bufPos, Math.Min(recSize, remain)); bufPos += readSize; remain -= readSize; } for(int i = 0; i < fileSize; i++) { for(int j = 0; j < readCsvList.Count; j++) { if((i != 0) && ((i%recSize) == 0)) { writer.Write(Convert.ToUInt16("000A", 16)); } if((j != 0) && ((i%recSize) == (Convert.ToInt32(readCsvList[j][1])-1))) { writer.Write(Convert.ToUInt16("002C", 16)); } // 1バイトテーブル if(Regex.IsMatch(readCsvList[j][0], "EBC") && ((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) && ((i%recSize) < (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1))) { //Console.Write(myTable[buf[i].ToString("X2")] + "\n"); writer.Write(Convert.ToUInt16(myTable[buf[i].ToString("X2")], 16)); break; } // 2バイトテーブル if(Regex.IsMatch(readCsvList[j][0], "EKJ") && ((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) && ((i%recSize) < (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1))) { Encoding utf8 = Encoding.GetEncoding("UTF-8"); int num = utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]); if(utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]) <= 4) { //Console.Write(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")] + "\n"); writer.Write(Convert.ToUInt16(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")], 16)); } else { //サロゲートペア計算(ライブラリが見つからなかった) var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16); //Console.Write("codePoint: " + codePoint + "\n"); var codePoint2 = (codePoint - 0x10000) >> 10; //Console.Write("plane: " + plane + "\n"); var lead = (codePoint2 & 0x3F) | 0xD800; //Console.Write("lead: " + lead + "\n"); var trail = (codePoint & 0x3FF) | 0xDC00; //Console.Write("trail: " + trail + "\n"); writer.Write(Convert.ToUInt16(lead)); writer.Write(Convert.ToUInt16(trail)); } i++; break; } } } } } } } } 実装のポイント(妥協を含む) 1 テーブルのキー読み出しでエンディアン変換 以下のコードでbuf(元データ:ビッグエンディアン)のデータを読み出し,そのままビッグエンディアン順でキーの指定を行っています(リトルエンディアンであれば,buf[i]とbuf[i+1]の指定が逆になる。)。 ビッグエンディアンデータの読みだしとキー指定 myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")] 2 サロゲートペアの計算ロジックを作成した 2バイトデータが変換テーブルを使用して,面02(U+2XXXX)に変換された場合,UTF-16ではサロゲートペアに更に変換が必要となる。その際,C#で変換ライブラリを見つけることができなかったため,以下の変換を実装しています。 サロゲートペア計算とバイナリ書き込み var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16); //Console.Write("codePoint: " + codePoint + "\n"); var codePoint2 = (codePoint - 0x10000) >> 10; //Console.Write("plane: " + plane + "\n"); var lead = (codePoint2 & 0x3F) | 0xD800; //Console.Write("lead: " + lead + "\n"); var trail = (codePoint & 0x3FF) | 0xDC00; //Console.Write("trail: " + trail + "\n"); writer.Write(Convert.ToUInt16(lead)); writer.Write(Convert.ToUInt16(trail)); ネット上にはいくつか実装方法があったが,乗算・除算器を使用するものだったため,加減算及びシフト演算のみのコードに書き直しました。また,サロゲートペアは32bitデータになりますが,UTF-16なので16ビットごとにリトルエンディアンによる書き込みが必要です。 今後 計算時間が結構掛かっている割に,以下のようにプロセスエクスプローラーで確認するとCPU,DISKIOは全力を出し切っていません。実際の移行時には,HyperThread OFF にして,ファイルを4分割し,Threadを4本立てて同時実行する実装を現在検討しています。 追記1 @jzkeyさんのコメントより,サロゲートペアの計算を以下のライブラリで計算できることがわかった。 Char.ConvertFromUtf32での実装 var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16); string sg = Char.ConvertFromUtf32(codePoint); writer.Write(Convert.ToUInt16(sg[0])); writer.Write(Convert.ToUInt16(sg[1]));