- 投稿日:2019-08-29T21:28:48+09:00
【UiPath】カスタムアクティビティを作成してみた_祝日判定①
概要
uipathのカスタムアクティビティを作成してみました。
uipathの画面で入力した日付を祝日判定し、祝日だったら
祝日名を返すだけのアクティビティ
※カスタムアクティビティ:uipathで使用できる拡張機能アクティビティ:祝日判定
思った以上に簡単にアクティビティの作成ができました。uipath特有の
処理が画面から入力された値の受け取りと値をuipathに返すだけだったので...①"2019/05/03"を入力して実行すると...
②入力された日付に対応する祝日名を返します。
3年分(去年・当年・来年)の祝日一覧を取得するapiがありそれを使用
Holidays JP API
apiで取得した祝日一覧をjson形式に変換しており、ライブラリをVSに
いれる必要があります。
C#でJSONを扱うライブラリ「Json.NET」を使ってみました//3年間分の祝日一覧を取得(当年・去年・来年) string url = "https://holidays-jp.github.io/api/v1/date.json"; var req = WebRequest.Create(url); req.Headers.Add("Accept-Language:ja,en-us;q=0.7,en;q=0.3"); var res = req.GetResponse(); string responseFromServer = ""; using (Stream dataStream = res.GetResponseStream()) { StreamReader reader = new StreamReader(dataStream); responseFromServer = reader.ReadToEnd(); } //取得した祝日一覧をjson形式に変換 var jsonValues = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseFromServer); ArrayList dayList = new ArrayList(); ArrayList dayListName = new ArrayList();一致する場合:祝日名を返す。
一致しない場合:平日、土日を判定して返す。int flg = 1; string outputDay = ""; string outputDayName = ""; for (int i = 0; i <= (dayList.Count - 1); i++) { flg = date.CompareTo(DateTime.Parse(dayList[i].ToString())); if (0 == flg) { outputDay = DateTime.Parse(dayList[i].ToString()).ToString("yyyy/MM/dd"); outputDayName = dayListName[i].ToString(); break; } } if (0 != flg) { int number = ((int)date.DayOfWeek); string[] week = { "Su", "M", "Tu", "W", "Th", "F", "Sa", }; string dayOfWeek = week[(int)number]; string result = date.ToString("yyyy/MM/dd"); outputDay = result; if (dayOfWeek == "Su" || dayOfWeek == "Sa") { outputDayName = "土日"; } else { outputDayName = "平日"; } } return outputDayName;参考
カスタムアクティビティ作成環境
結構簡単に環境作成ができました。
【UiPath】カスタムアクティビティの作成(事前準備と実践初級)ソースコード
Class1.csusing System; using System.Collections.Generic; using Newtonsoft.Json; using System.Collections; using System.IO; using System.Net; using System.Activities; using System.ComponentModel; namespace ClassLibrary1 { public class Class1 : CodeActivity { [Category("Input")] [RequiredArgument] public InArgument<String> inputDay { get; set; } [Category("Output")] public OutArgument<String> outputDay { get; set; } protected override void Execute(CodeActivityContext context) { //画面入力された日付取得 var Arg = inputDay.Get(context); DateTime date = DateTime.Parse(Arg.ToString()); var result = outputMethod(date); //祝日判定結果をuipathに渡す outputDay.Set(context, result); } public string outputMethod(DateTime date) { //3年間分の祝日一覧を取得(当年・去年・来年) string url = "https://holidays-jp.github.io/api/v1/date.json"; var req = WebRequest.Create(url); req.Headers.Add("Accept-Language:ja,en-us;q=0.7,en;q=0.3"); var res = req.GetResponse(); string responseFromServer = ""; using (Stream dataStream = res.GetResponseStream()) { StreamReader reader = new StreamReader(dataStream); responseFromServer = reader.ReadToEnd(); } //取得した祝日一覧をjson形式に変換 var jsonValues = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseFromServer); ArrayList dayList = new ArrayList(); ArrayList dayListName = new ArrayList(); //祝日一覧を配列に設定 foreach (var key in jsonValues.Keys) { dayList.Add(key); dayListName.Add(jsonValues[key]); } /*入力された日付が祝日一覧に存在するか確認 一致する場合は祝日名を呼び出し元に渡す 一致しない場合は入力された日付が平日、土日を 渡す */ int flg = 1; string outputDay = ""; string outputDayName = ""; for (int i = 0; i <= (dayList.Count - 1); i++) { flg = date.CompareTo(DateTime.Parse(dayList[i].ToString())); if (0 == flg) { outputDay = DateTime.Parse(dayList[i].ToString()).ToString("yyyy/MM/dd"); outputDayName = dayListName[i].ToString(); break; } } if (0 != flg) { int number = ((int)date.DayOfWeek); string[] week = { "Su", "M", "Tu", "W", "Th", "F", "Sa", }; string dayOfWeek = week[(int)number]; string result = date.ToString("yyyy/MM/dd"); outputDay = result; if (dayOfWeek == "Su" || dayOfWeek == "Sa") { outputDayName = "土日"; } else { outputDayName = "平日"; } } return outputDayName; } } }
- 投稿日:2019-08-29T17:16:32+09:00
【C#】リトライ共通関数を用意した時のメモ
特定のクラス内の関数をリトライする必要があった際に
作ったのでメモ。共通関数部/// <summary> /// リトライ共通処理 /// </summary> /// <param name="caller">処理を実行するインスタンス</param> /// <param name="funcName">リトライ対象関数名</param> /// <param name="parameters">引数群</param> /// <param name="successResult">対象関数の成功時の戻り値</param> /// <returns></returns> private T invokeRetry<T>(object caller, string funcName, object[] parameters, T successResult) { // リトライ最大回数(引数で指定してもいいかも) int retryMax = 3; // 戻り値を初期化しておく T result = default( T ); var method = typeof( BaseClass ).GetMethod( funcName ); for(int retryCount = 0; retryCount <= retryMax ; retryCount++) { result = (T)method.Invoke( caller, parameters ); // 処理に成功していれば抜ける if(successResult.Equals( result )) { break; } // リトライ間隔を適当にあけておく Thread.Sleep( 500 ); } return result; }BaseClassが特定クラスの共通の基底クラスとなります。
実際はリトライ回数をモジュールのコンフィグから読み込んでいましたが
たぶん引数で与えてやるか、共通処理を実装するクラスの初期化時にでも渡してあげれば良い気がしてます。使用する場合は以下のような感じ。
実行部BaseClass comm = new DerivedClass(); bool result = invokeRetry( comm, "MethodHoge", new object[] { arg1, arg2}, true );実際のコードでは実行する為の派生クラスのインスタンスも
別メソッドで取得したりするので色々とやっていますが省略。
#省略したせいでおかしな点があるかもしれませんが…
- 投稿日:2019-08-29T17:07:55+09:00
.NET FrameworkからのExcel操作
本記事は2012年末頃に、社内向けに作成した説明資料をQiita向けに編集したものです。
最新の仕様とは異なる場合があるので注意してください。はじめに
.NET Frameworkには「COM相互運用1」と呼ばれる機能があり、COMコンポーネント2を手軽に呼び出すことができます。一方、ExcelをはじめとするOffice製品は、その機能をマクロ(VBA)などからも活用できるようにCOMコンポーネントとして実装されています。このため、COM相互運用を使えば.NETアプリケーションから容易にExcelやWordのファイルを開き、それをさまざまに操作することが可能です。
本記事ではC#で作成されたサンプルアプリケーションのソースコードを基に、COM相互運用機能を用いてExcelファイルを操作する方法を説明します。
使用しているVisual Studioのバージョンは” Microsoft Visual Studio Express 2012 for Windows Desktop”です。ただし、Visual Studioの旧バージョンとの差は見た目ぐらいですので、特に問題はないと思います。
.Net Frameworkのバージョン、およびC#のバージョンに依存している記述などについては、その都度言及して、補足を行なっていきます。以降の章では、サンプルアプリケーションのうち、Excelファイルを操作している部分に着目して説明を行なっていきます。ファイルの存在チェックや出力のフォーマットに関しては、本書の目的とそれるので割愛します。サンプルプロジェクトのソースファイルを見てください。
事前バインディング
本章では、C#からExcelを操作する上で一般的に使用される「事前バインディング」方式を用いて、Excelファイルを操作する手順を説明します。
「事前バインディング」方式とは、文字通り事前に必要なDLLの参照設定を行なっておき、そのDLL使用する方式です。参照を追加するため、コードの補完機能などが使用出来るので、Excelを操作するコーディングを行う上でも一番やりやすい方法となります。
以下に、「事前バインディング」方式によるExcelファイルを操作する手順を示していきます。プロジェクトへ参照を追加
まずは、事前バインディングの名の通り、Excelファイルを操作するためのDLL参照を設定します。
[プロジェクト]メニュー – [参照の追加]をクリックし、参照マネージャーを起動してください。ソリューションエクスプローラーから見える参照設定の右クリックメニューからでも同様の操作が行えます。
参照マネージャーの[COM]タブをクリックし、表示されるライブラリーの一覧から「Microsoft Excel XX Object Library」を選択します。インストールされているExcelのバージョンによって、XXの部分に表示されるライブラリーのバージョン番号が異なります。
Officeのバージョン ライブラリのバージョン3 Microsoft Office 2010 14.0 Microsoft Office 2007 12.0 Microsoft Office 2003 11.0 Microsoft Excel 2002 10.0 Microsoft Excel 2000 9.0 Microsoft Excel 97 8.0 参照の追加が行われたら、ソリューションエクスプローラーの参照設定に「Micorosoft.Office.Core」と「Microsoft.Office.Interop.Excel」が追加されるのが見て取れます。
最後に、バインドした参照のうち、使用するクラスが含まれている名前空間を追加します。追加する名前空間は「Microsoft.Office.Interop.Excel」になります。
名前空間の追加が正しく行われると、コーディング時にコードサジェストが行われます。
ソースコード解説
ここからは、サンプルコードを元に、C#からどのようにExcelを操作するかを説明します。
操作は主に5ステップ存在します。
1. ファイルオープンなどの前処理
2. シート、セルの領域指定
3. 選択したセル領域から値の取得
4. 取得した値の詰め替え
5. ファイルクローズなどの後処理
以下、ステップごとに行う処理について説明します。
サンプルコードの全文はAppendix. 「事前バインディング」を参照してください。前処理
// ファイルオープン Application xlApp = new Application(); Workbooks xlBooks = xlApp.Workbooks; Workbook xlBook = xlBooks.Open( ExcelFileName, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing );
行 解説 2 Applicationインスタンスの生成。
xlAppにExcelを操作するMicrosoft.Office.Interop.Excel.Application COMオブジェクトクラスのインスタンスが格納されます。3 xlAppが持つWorkbooksプロパティ(MDIの親玉)を取得。 4〜10 xlBooksに、ExcelFileNameで指定したファイルをオープンし、そのインスタンスをxlBookに格納します。
第2引数以降で指定している”Type.Missing”で、パラメーターの既定値(所謂デフォルトパラメータ)を使用するようメソッドに通知しています。
Workbooks.Openメソッドの引数の詳細については、こちらを参照してください。なお、C# 4.0 (Visual Studio 2010)からはデフォルトパラメータを省略できるようになったので、4行目のExcelファイルのオープン処理は、以下のように記述することが可能です。
Workbook xlBook = xlBooks.Open( ExcelFileName );ExcelのCOMオブジェクト構成は、
Application └ Workbooks[] ├ Workbook │ ├ Sheets[] │ │ ├ Sheet1 │ │ │ ├ Cells[] │ │ ├ Sheet2 │ │ ├ Sheet3 │ │ ├ : ├ Workbook ├ Workbook ├ :となっており、操作対象となるセルオブジェクトに到達するまで、上位のオブジェクトから順次取得して行く必要があります。上述の前処理で行ったのは、この内上位3階層分のオブジェクトの取得処理に当たります。
シート、セルの領域指定
// シートを選択 Worksheet sheet = xlBook.Sheets["メンバー一覧"]; // セルの領域を選択 Range TableRange = sheet.Range["A1", "B15"];
行 解説 2 OpenしたExcelファイルから、”メンバー一覧”シートを指定して取得します。
Sheets[]コレクションは、要素番号とシート名どちらでもアクセスできます。今回操作対象シートが明確なのでシート名でアクセスしています。5 ”メンバー一覧”シートの領域$A$1:$B$15を示すオブジェクトを取得します。
領域取得には他に、
Range TableRange = sheet.Range["A1:B15"];
というふうに、1次元目だけを使用した記述方法も可能です。領域オブジェクトを取得する操作は、上記の他にget_Range()メソッドが存在します。
しかし、get_Range()には後方互換性の不備があり、.NET Frameworkのある特定の更新に限り正常に動作しないというバグが存在します。
領域オブジェクトを取得する際には、Range[]アクセサーからの取得を推奨します。選択したセル領域から値の取得
// 選択した領域の値をメモリー上に格納 object[,] values = TableRange.Value;
行 解説 2 選択した領域の値を配列に格納しています。
領域オブジェクトのValueまたはValue2プロパティは、まとめて取得されメモリー上に配置されます。
今回、セルには文字が書き込まれているので、セルの書式設定が日付/通貨の時のみ意味があるValue2ではなく、Valueで取得しています。領域オブジェクトから、値を二次元配列に格納するという手法は、Excel VBAでもよく使われる高速化テクニックの一つです。領域内のセルをループで取得するのに比べ、格段に早くすることができます。
なお、たとえ一列しか選択していなくても二次元目の深さが1である二次元配列になります。
また、配列の添字は1が最初です。要素0へアクセスすると例外が発生するので、注意してください。取得した値の詰め替え
// 配列アクセスができるので、それぞれをDictionaryに追加 for (int i = 1; i <= values.GetLength(0); i++) { dic.Add((string)values[i, 1], (string)values[i, 2]); }
行 解説 2〜5 値を格納した二次元配列から値を取り出し、それぞれkeyとvalueとしてDictionaryに格納しています。配列の添字は1が最初。 後処理
// 使用したCOMオブジェクトを解放 System.Runtime.InteropServices.Marshal.ReleaseComObject(TableRange); System.Runtime.InteropServices.Marshal.ReleaseComObject(sheet); // Excelのクローズ xlBook.Close(); xlApp.Quit(); // 使用したCOMオブジェクトを解放その2 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBook); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBooks); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlApp);
行 解説 2〜3 使用したRangeやSheetのCOMオブジェクトを解放します。 6 Close()メソッドにより、ファイルをクローズします。 7 Quit()メソッドにより、アプリケーションを終了します。 10〜12 使用したApplicationやWorkbooksなどのCOMオブジェクトを解放します。後ほど詳しく説明します。 ReleaseComObject()/FinalReleaseComObject()メソッドは、COMオブジェクトの参照カウントを1/すべて減らすメソッドです。
参照カウント絡みの話は割りと深く面倒くさいものですので、今はあまり時間がないけどとにかく使えるようになりたい方は、「COMは、メモリーのalloc()/free()と同様に、使用者が管理するもの」という程度に理解していただければまず十分です。詳しく知りたい方はAppendix. 「COMの仕組み」をお読みください。
なお、COMオブジェクトが正しく解放されなかった場合、Excelのプロセスが残り続けてしまいます。オブジェクトの解放忘れには十分に注意してください。COMオブジェクトを解放し忘れる典型的パターン
以下に、解放を忘れるパターンを示します。この記述を行うとCOMオブジェクトの解放し忘れにつながるため、使用してはいけません。この記述を見つけた場合、速やかに指摘してあげてください。
- 一気にOpen
一般的にC#でファイルをオープンするときは、以下の様な記述をすることがよくあります。
FileStream fs = System.IO.File.Open(filename, FileMode.Open);
これと同じ感覚でExcelファイルのオープンを記述すると
Workbook book = new Application().Workbooks.Open(ExcelFileName);
となりますが、ApplicationオブジェクトとWorkbooksオブジェクトをどの変数にも格納していないため、後々ReleaseComObject()で解放することができず、COMオブジェクトが残存します。COMオブジェクトを返すメソッドやプロパティでは、面倒でもひとつひとつ変数に格納して行きましょう。- Cellsプロパティ
セルの領域を取得する方法として、上ではRange[]アクセサーを使用する方法を紹介しましたが、Cellsプロパティを使用して取得することもできます。
Range TableRange = sheet.Cells["A1", "B15"];
しかし、CellsプロパティはCOMオブジェクトを返すので、上記の記述ではCellsを受け取る変数がおらず、COMオブジェクトを開放することができません。
(実際はこうなっている)
Range TableRange = sheet.Cells.Range["A1", "B15"];
Cellsプロパティを使用して各セルにアクセスする場合は、Cellsのオブジェクトを変数に格納し、そこから各セルのRangeオブジェクトをアクセサーで取得する必要があります。
Range cells = sheet.Cells; // cellsはあとでReleaseComObject()で解放する
Range TableRange = cells["A1", "B15"];
その他、COMオブジェクトを返すプロパティを、変数へ格納せずに使用している箇所はすべて解放忘れにつながります。発見次第、修正してください。COMオブジェクトを返すプロパティについては、Appendix.「COMオブジェクトを返すプロパティ」を参照してください。
遅延バインディング
「遅延バインディング」方式とは、DLLやCOM内のクラス・関数を、必要になったときに、必要な分だけ実行時に読み込む機能です。実行時に読み込まれるということは、プログラマーはそのDLLの存在やバージョンについて、実装時に意識する必要がなくなるということになります4。
バージョンを意識する必要がなくなるため、事前バインディングの時のように、Excelのバージョンに応じてライブラリーを読み込む必要がなく、将来新しいバージョンのExcelが出てきたとしても、プログラムに改変を加えることなくサポートできる可能性が高いということになります。
以下に、「遅延バインディング」方式によるExcelファイルを操作する手順を示していきます。ソースコード解説
サンプルコードを元に、遅延バインディングを使用してC#からExcelを操作する方法を説明します。
操作自体は事前バインディングと同じ5ステップで、ステップごとに行っている処理も同様のため省略し、ここでは、遅延バインディング独特の構文について説明します。
サンプルコードの全文はAppendix.「遅延バインディング」を参照してください。
以下に例として、前処理で行うExcelファイルオープン処理を、遅延バインディング形式で記述したものを示します。// ファイルオープン object xlApp = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application")); object xlBooks = xlApp.GetType().InvokeMember("Workbooks", System.Reflection.BindingFlags.GetProperty, null, xlApp, null); object xlBook = xlBooks.GetType().InvokeMember("Open", System.Reflection.BindingFlags.InvokeMethod, null, xlBooks, new object[] { ExcelFileName, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing });突然摩訶不思議な記述になりましたが、処理の内容自体は事前バインディングの時と何らかわりがありません。Applicationオブジェクトを作成し、Workbooksプロパティを取得し、Workbookをオープンしているだけです。ここでは、その摩訶不思議な部分であるActivator.CreateInstance()、Type.GetTypeFromProgID()、Type.InvokeMember()メソッドの解説を行います。
- Type.GetTypeFromProgID()
指定したプログラムID(ProgID)に関連付けられている型を取得します。ここで指定している引数は”Excel.Application”なので、取得できる型はMicrosoft.Office.Interop.Excel.Applicationクラスとなります。- Activator.CreateInstance()
CreateInstanceメソッドは、指定した引数に最も一致するコンストラクターを呼び出して、アセンブリで定義された型のインスタンスを作成します。つまり、Applicationクラスのインスタンスを作成してくれます。- Type.InvokeMember()
InvokeMemberは、引数の文字列と一致するメソッドやメンバーの操作を行うメソッドです。コンストラクターのメンバーまたはメソッドのメンバーの呼び出し、プロパティのメンバーの取得または設定、データフィールドのメンバーの取得または設定、または配列のメンバーの要素の取得または設定を行います。
身も蓋もなく日本語に直すなら「”第4引数”から、”第1引数”のプロパティまたはメソッドを、[”第5引数”のobject配列を”第3引数”に変換したものを引数として] ”第2引数”します」という感じ。
引数インデックス 説明 第1引数 メンバー名、メソッド名の文字列 第2引数 クラスへのアクセス用フラグ 第3引数 引数のコンバータ用クラス 第4引数 制御対象インスタンス 第5引数 メソッド引数 上述の例のように、基本的にはCOMオブジェクトを操作する処理をInvokeMember()メソッドに書き換えることで、遅延バインディング方式の記述となります。
遅延バインディングのデメリット
Excelのバージョンに寄らず記述できる遅延バインディングですが、以下の様なデメリットが存在します。
- Excel操作クラスの形を自分で知る必要がある
DLL参照を追加しておけばIntellisenseによる補完機能が働きますが、遅延バインディングでは参照を追加しないためその機能は働いてくれません。自分が操作したいCOMオブジェクトのメソッドやプロパティ名が正しいかどうかは、自分の目で確かめる必要があります。- 実行時にしか正当性が判別できない
(1)と同様に、Intellisenseによる誤り訂正が働かないので、実際に実行してみるまでメソッド名を正しく記述できたかどうかはわかりません。そして、間違っていた場合は例外が発生することもあるので、例外処理をおこなっていないとCOMオブジェクトが解放されずに……- 記述がとても長くなる
例を見ればわかるように、あからさまに横に伸びます。ただでさえCOMの解放処理で縦に伸びているのに、これで横にまで伸びてしまうのは明らかに可読性が下がっていきます。ただでさえInvokeMethod()は可読性が悪いのに……[C# 4.0] dynamicキーワードを用いた遅延バインディング
C# 4.0で導入された、動的言語との連携の仕組みの1つが動的型付け変数(dynamicキーワード)です。動的型付け変数を使うことで、動的な(コンパイル時にメンバー情報がわからない型の)メンバーアクセスが可能になります。このdynamicキーワードを用いると、先ほど指摘した遅延バインディングのデメリットがほぼ解消されます。早速、前処理で行うExcelファイルオープン処理を、dynamicキーワードを用いた記述で見てみましょう。
// ファイルオープン dynamic xlApp = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application")); dynamic xlBooks = xlApp.Workbooks; dynamic xlBook = xlBooks.Open(ExcelFileName);2行目だけ遅延バインディング形式と同じで、あとは事前バインディング方式の時とほぼ同じです。違うのは、変数の型がdynamicになっているだけです。このことから、
1. 事前バインディング形式でExcel操作クラスを実装し、動作確認を行う。
2. COMオブジェクトを格納する変数の型をdynamicへ変更する。
3. DLLの参照を削除する。
という手順を踏めば、Excelのバージョンに依存する事前バインディングの記述を、バージョンに依存しない遅延バインディングに変換できます。Appendix.
COMの仕組み
.NET から COM オブジェクトを扱うということは、マネージドからアンマネージにアクセスするということであり、すなわちマーシャリングが必要となります。これは、ランタイム呼び出し可能ラッパー(RCW: Runtime Callable Wrapper)なるしくみによって行われています。
通常、.NET クライアント(アプリケーション)が直接的に扱うのは、生身の COM オブジェクトではなく、そのラッパーたる RCW です。ランタイム(.NET Framework)によってサポートされるため意識しづらいですが、RCW というプロキシを挟んで COM オブジェクトにアクセスしているのです。これは重要な事実であり、認識しておかなければなりません。
COMの規定によれば、COMオブジェクトたるもの「自分のメモリーは自分で解放する責任がある」とのこと。そして、それを実現するためには、参照カウントというテクニックが用いられます。参照カウントとは、その COMオブジェクトが他からどれだけ参照されているかを示す数値です。COMオブジェクトは、自分が使ったメモリーを自分で解放するために、自身への参照数を自分で管理していて、参照数が"0"になったら他からの参照がなくなった(=用済み)と判断して、自分自身のメモリーを解放してくれます。だけどもそれ故に、いま誰が必要とし、必要としなくなったのかは、使う側から通達しなくてはなりません。
.NETでメモリー管理といえばガベージコレクション(GC)です。COMオブジェクトを内包するRCWは、マネージドであり、もちろんGCの管理対象です。しかし、COMオブジェクトは、自分のメモリーは自分で解放する責務を負っています。ここに、メモリー管理のアプローチの違いからくるミスマッチが生まれています。RCWはGCの管理下にありますが、内包するCOMオブジェクトがGCの対象ではないので、結局、アプリケーション作成者が RCW を通じてCOMオブジェクトが使用するメモリーを、参照カウントというしくみで管理しなければならない、ということになっています。
管理するとは、つまり、RCWをすべて変数に保持しておくということであり、そしてその参照カウントをデクリメントするのがReleaseComObject()メソッドです。VBScriptSub ShowRangeA1() Dim excel Dim book Set excel = CreateObject("Excel.Application") Set book = excel.Workbooks.Open("hoge.xls") Dim a1val a1val = book.Sheets(1).Range("A1").Value book.Close False excel.Quit WScript.Echo a1val End SubVBScriptだとこれだけですむのに…
public void ShowRangeA1() { Application xlApp = new Application(); Workbooks xlBooks = xlApp.Workbooks; Workbook xlBook = xlBooks.Open(@"hoge.xls"); Sheets xlSheets = xlBook.Worksheets; Worksheet xlSheet = (Worksheet)xlSheets[1]; Range xlCells = xlSheet.Cells; Range xlRangeA1 = (Range)xlCells[1, 1]; string a1val = (string)xlRangeA1.Value2; Marshal.ReleaseComObject(xlRangeA1); Marshal.ReleaseComObject(xlCells); Marshal.ReleaseComObject(xlSheet); Marshal.ReleaseComObject(xlSheets); xlBook.Close(false, Type.Missing, Type.Missing); Marshal.ReleaseComObject(xlBook); Marshal.ReleaseComObject(xlBooks); xlApp.Quit(); Marshal.ReleaseComObject(xlApp); MessageBox.Show(a1val); }C#だとごらんの有様だよ!
上記例を見ればわかるように、処理の本質ではないコード、タイプ量が激増しています。これだけでも十分面倒くさいのに、実は例外発生時にも確実にデクリメントを行うためには、RCWを生成するごとにtry~finallyをしてやらなければいけません。詳しくはこちらの記事を参考にして頂きますが、こうなると、超絶ネスト構造の一丁上がりです。意味がわかりません。しかし、COM相互参照を使用するということは、本来はこういうことを行う必要があると理解してください。
COMオブジェクトを返すプロパティ一覧
Application Areas Borders Cells Characters Columns Comment CurrentArray CurrentRegion Dependents DirectDependents DirectPrecedents End EntireColumn EntireRow Font FormatConditions Hyperlinks Interior Item ListObject MDX MergeArea Next Offset Parent Phonetic Phonetics PivotCell PivotField PivotItem PivotTable Precedents Previous QueryTable Range Resize Rows ServerActions SmartTags Validation Worksheet XPath サンプルアプリケーションのソースコード
事前バインディング
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; //インターフェースの名前空間 using ExtractExcelDataInterface; //Excelを操作するために必要なクラスの名前空間を追加 using Microsoft.Office.Interop.Excel; namespace ExtractExcelData { public class ExtractExcelUseExcelDLL : IExtractExcelData { public Dictionary<string, string> ExtractExcelData(string ExcelFileName) { //returnするDicionaryインスタンス Dictionary<string, string> dic = new Dictionary<string, string>(); // ファイルオープン Application xlApp = new Application(); Workbooks xlBooks = xlApp.Workbooks; Workbook xlBook = xlBooks.Open( ExcelFileName, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing ); // シートを選択 Worksheet sheet = xlBook.Sheets["メンバー一覧"]; // セルの領域を選択 Range TableRange = sheet.Range["A1", "B15"]; // 選択した領域の値をメモリー上に格納 // (1セルずつ見ていくよりも早い) object[,] values = TableRange.Value; // 配列アクセスができるので、それぞれをDictionaryに追加 // [WARNING] 配列の開始インデックスは1から for (int i = 1; i <= values.GetLength(0); i++) { dic.Add((string)values[i, 1], (string)values[i, 2]); } // 使用したCOMオブジェクトを解放 System.Runtime.InteropServices.Marshal.ReleaseComObject(TableRange); System.Runtime.InteropServices.Marshal.ReleaseComObject(sheet); // Excelのクローズ xlBook.Close(); xlApp.Quit(); // 使用したCOMオブジェクトを解放その2 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBook); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBooks); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlApp); return dic; } } }遅延バインディング
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ExtractExcelDataInterface; namespace ExtractExcelData { public class ExtractDelayBinding : IExtractExcelData { public Dictionary<string, string> ExtractExcelData(string ExcelFileName) { Dictionary<string, string> dic = new Dictionary<string, string>(); object xlApp = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application")); object xlBooks = xlApp.GetType().InvokeMember("Workbooks", System.Reflection.BindingFlags.GetProperty, null, xlApp, null); object xlBook = xlBooks.GetType().InvokeMember("Open", System.Reflection.BindingFlags.InvokeMethod, null, xlBooks, object[] { ExcelFileName, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing,Type.Missing, Type.Missing, Type.Missing,Type.Missing, Type.Missing, Type.Missing }); // シートを選択 object sheet = xlBook.GetType().InvokeMember("Sheets", System.Reflection.BindingFlags.GetProperty, null, xlBook, new object[] { "メンバー一覧" }); // セルの領域を選択 object TableRange = sheet.GetType().InvokeMember("Range", System.Reflection.BindingFlags.GetProperty, null, sheet, new object[] { "A1", "B15" }); // 選択した領域の値をメモリー上に格納 // (1セルずつ見ていくよりも早い) object[,] values = TableRange.GetType().InvokeMember("Value", System.Reflection.BindingFlags.GetProperty, null, TableRange, null) as object[,]; // 配列アクセスができるので、それぞれをDictionaryに追加 // [WARNING] 配列の開始インデックスは1から for (int i = 1; i <= values.GetLength(0); i++) { dic.Add((string)values[i, 1], (string)values[i, 2]); } // 使用したCOMオブジェクトを解放 System.Runtime.InteropServices.Marshal.ReleaseComObject(TableRange); System.Runtime.InteropServices.Marshal.ReleaseComObject(sheet); // Excelのクローズ xlBook.GetType().InvokeMember("Close", System.Reflection.BindingFlags.InvokeMethod, null, xlBook, null); xlApp.GetType().InvokeMember("Quit", System.Reflection.BindingFlags.InvokeMethod, null, xlApp, null); // 使用したCOMオブジェクトを解放その2 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBook); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBooks); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlApp); return dic; } } }dynamicキーワードを使用した遅延バインディング
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ExtractExcelDataInterface; namespace ExtractExcelData { public class ExtractDelayBindUseDynamic : IExtractExcelData { public Dictionary<string, string> ExtractExcelData(string ExcelFileName) { Dictionary<string, string> dic = new Dictionary<string, string>(); // ファイルオープン dynamic xlApp = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application")); dynamic xlBooks = xlApp.Workbooks; dynamic xlBook = xlBooks.Open(ExcelFileName); // シートを選択 dynamic sheet = xlBook.Sheets["メンバー一覧"]; // セルの領域を選択 dynamic TableRange = sheet.Range["A1", "B15"]; // 選択した領域の値をメモリー上に格納 // (1セルずつ見ていくよりも早い) object[,] values = TableRange.Value; // 配列アクセスができるので、それぞれをDictionaryに追加 // [WARNING] 配列の開始インデックスは1から for (int i = 1; i <= values.GetLength(0); i++) { dic.Add((string)values[i, 1], (string)values[i, 2]); } // 使用したCOMオブジェクトを解放 System.Runtime.InteropServices.Marshal.ReleaseComObject(TableRange); System.Runtime.InteropServices.Marshal.ReleaseComObject(sheet); // Excelのクローズ xlBook.Close(); xlApp.Quit(); // 使用したCOMオブジェクトを解放その2 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBook); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlBooks); System.Runtime.InteropServices.Marshal.FinalReleaseComObject(xlApp); return dic; } } }
- 投稿日:2019-08-29T14:45:03+09:00
C#6 から $"{hoge}" みたいな感じで文字列に変数が埋め込めるようになったけど、フィールド変数で string a = $"{hoge}"; ってやるとエラーで怒られるのなんで??ねぇなんで??
初学者やりがち。
public class Test{ private string logFormat = $"{now}>{log}"; public void Func(){ var now = DateTime.Now; var log = "なんやかんやはなんやかんやですよ!"; Console.WriteLine(logFormat); //"2019/08/29 12:52:00 > なんやかんやはなんやかんやですよ!" って出てほしい } }現在のコンテキストに 'now' という名前は存在しません。
現在のコンテキストに 'log' という名前は存在しません。はい。エラー。なぜなら、
$"a={変数名1},b={変数名2}"
はstring.Format("a={0},b={1}",変数名1,変数名2);
にシンタックスシュガーとしてコンパイル前に変換されているから「string.Format で、 {17} とか書いて、引数の17番目に目的の変数置くとか大変でしょ? もう何が何番目かわからないでしょ? ほらほら。僕が変換しといてあげるから~」
という親切機能でしかありません。
そのため、↑の例も内部的には//private string logFormat = $"{now}>{log}"; ↓に変換されている private string logFormat = string.Format("{0}>{1}",now,log);と、なってるんですね。 すると、このスコープではnow,logなんてものは無いわけで。そりゃエラーにもなりますわな。
すなわち
実行時解決参照タイミングで展開されてその時のスコープの変数を展開するような仕組みではない わけです。1それでは
public class Test{ private string logFormat = "{now}>{log}"; //$ を外した public void Func(){ var now = DateTime.Now; var log = "なんやかんやはなんやかんやですよ!"; Console.WriteLine(logFormat); } }こうすればエラーは出なくなりますけどー。
{now}>{log}
当然まんま出てきちゃいますね。
じゃぁ、がんばって自分でReplaceする?
ということで、変更
public class Test{ private string logFormat = "{now}>{log}"; //$ を外した public void Func(){ var now = DateTime.Now; var log = "なんやかんやはなんやかんやですよ!"; Console.WriteLine(logFormat.Replace("{now}",now.ToString()).Replace("{log}",log)); //変数の数だけReplaceが数珠つなぎ? } }2019/08/29 13:07:45>なんやかんやはなんやかんやですよ!
でるけど・・・。 出るけれどー。
もうちょっとなんとか・・・。というわけで作ってみた
StringFormatExtension.csusing System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Util { public static class StringFormatExtension { public static string Format(this string templateText, object args) { var Matches = new Regex(@"\{(.+?)\}").Matches(templateText);//{~} の間を抽出 var index = 0; var objectList = new List<object>(); foreach (Match match in Matches) { //指定されている変数名を取得する , や : などの書式指定されている場合もあるので、考慮する var paramName = match.Value.Substring(1, match.Value.Length - 2).Trim(); //{~}の中の中身だけ(Trim済み) var variableName = paramName.Split(new[] {":", ","}, StringSplitOptions.RemoveEmptyEntries)[0]; //書式文字列を抜いた、純粋な変数名 var formatParam = paramName.Substring(variableName.Length); //変数名を抜いて書式文字列のみを取得する //引数 args から Reflectionを使って プロパティ or フィールド の中身を取得 var replaceValue = args.GetType().GetProperties().Where(info => info.CanRead).FirstOrDefault(info => info.Name == variableName)?.GetValue(args) ?? args.GetType().GetFields().FirstOrDefault(info => info.Name == variableName)?.GetValue(args); if (replaceValue != null) { templateText = templateText.Replace(match.Value, "{"+ index++ + formatParam + "}"); // {0}..{n}のFormat置換文字に変換 objectList.Add(replaceValue); //実際の値はListに入れておいて最後にstring.Formatで返却する } else { templateText = templateText.Replace(match.Value, string.Empty); } } return string.Format(templateText,objectList.ToArray()); } } }やっていることは、先ほども書いたプリコンパイラがやってくれている$付きの文字列のシンタックスシュガー
$"{hoge}" → string.Format("{0}",hoge)を、実行時に疑似的に解決しているだけです。
使い方
拡張メソッドなので、
.Format
メソッドがstringに対して生えます。(よしなにusingしてね)
引数に渡すのは object です。 今回のようにローカル変数の場合は匿名クラスにしてしまうと、プロパティ名がローカル変数名に設定されるので、らくちんです。public class Test{ private string logFormat = "{now}>{log}"; public void Func(){ var now = DateTime.Now; var log = "なんやかんやはなんやかんやですよ!"; Console.WriteLine(logFormat.Format(new{now,log})); } }匿名クラスじゃなくて、既存インスタンスを渡してもいいですが、Field/Property に外からアクセスすることになるので、publicである必要があるので要注意です。
public class Test{ private string logFormat = "{now}>{log}"; public DateTime now => DateTime.Now; //外からアクセスされるのでpublicじゃないとだめ! public string log; //外からアクセスされるのでpublicじゃないとだめ! public void Func(){ log = "なんやかんやはなんやかんやですよ!"; Console.WriteLine(logFormat.Format(this)); //thisを渡す! } }便利! かな・・・?
追記(2019/08/30)
コメントなどで「なんでそんな面倒臭いことを!?、普通に指定すればいいのでは!?(要約)」等々
- いったい、こいつは何の役に立つの?
という反応がありました。 確かにこの記事だけ見ると「で?」となるのもイタシカタガナイ。
言葉足らずで申し訳ない。
この拡張メソッドの利用ケースは「外部設定データ(など)で画面などに表示する文言を変更したい」を想定していました。たとえば、エラーログのある1行
2019/09/01 10:10:00 > エラーが発生しました (errorCode:4101 ,pId:1000)これには
- TimeStamp
- エラーメッセージ
- エラーコード
- プロセスID
が出ています。
それぞれ
- TimeStamp
timeStamp
- エラーメッセージ
errorMessage
- エラーコード
errorCode
- プロセスID
pId
と変数で定義されているとして、
LogOutput($"{timeStamp} > {errorMessage} (errorCode:{errorCode} ,pId:{pId})");こうすれば、目的のLogがでます。よしよし。
そんな中、しばらくそのアプリが動いた後、ふとログを見た偉い人がこう言います。
「pIDってなに? あ、プロセスID? じゃぁ、プロセスIDって書いておいてよ」
あっはいー。しょうがないなぁ。なんでそんなどうでもいいことを・・・ブツブツ・・・。 修正してビルドビルドっと。LogOutput($"{timeStamp} > {errorMessage} (errorCode:{errorCode} ,プロセスID:{pId})");そしてまたある日
「Excelにコピペしたいからさ、各項目Tab(orカンマ)で区切ってくれない?」
ぐぬぬ・・・。
みなさん、こんなやり取りをあと10回ほど繰り返しますよね?(ブラック
でも、ちょっとまって。辞表を投げつけるのはちょっと早い。
こんな時のために、 フォーマットはソースコードに書かずに、設定ファイルなどに置いておきたい そして
「あ、それ設定ファイルで変更できるので、お好きにどうぞ。」
と言いたい。(ことがしばしばあります。(よね?)(まぁ、大体言えない))なので、設定ファイル(別にDBのマスタデータでもよいです)にこんな感じに書きたい。
設定ファイル<logFormat>{timeStamp} > {errorMessage} (errorCode:{errorCode} ,プロセスID:{pId})</logFormat>var template = config.logFormat;//まぁ、こんな感じで↑の</logFormat>~</logFormat>が取れるとしよう。 LogOutput(template); // TODO あれ、ここどうしよう。こうやって書いても変数展開されないのは、前述したとおり。 あ、Replace4連発だ。これ。 と気付きます。
ここで、ここで、ようやく作った拡張メソッドが役に立ちます。
var template = config.logFormat;//まぁ、こんな感じで↑の</logFormat>~</logFormat>が取れるとしよう。 LogOutput(template.Format(new{errorMessage,timeStamp,errorCode,pId});やったね!
そもそも、よく見る解決方法としては
- TimeStamp
{0}
- エラーメッセージ
{1}
- エラーコード
{2}
- プロセスID
{3}
として、 「
{n}
の箇所は置換されるよ!」 と決め打ちするパターン
設定ファイルはこんな感じに設定ファイル<logFormat>{0} > {1} (errorCode:{2} ,プロセスID:{3})</logFormat>プログラムはこんな感じに
var template = config.logFormat;//まぁ、こんな感じで↑の</logFormat>~</logFormat>が取れるとしよう。 LogOutput(string.Format(template,timeStamp,errorMessage,errorCode,pId));(書いてて、「まぁ、これでもいいよね」という気にもなってきてしまっているが、負けない!)
この、{0}とか{1}とか、超マジックナンバーが設定ファイルに横行するのを嫌がっているわけです。 どうでしょうか、そう考えるとこの拡張メソッドの価値も見えてきたりしませんでしょうか?
え。 「プログラム中にどんな変数名で宣言しているか知らないと指定できないじゃないか」って? そうですね。でも、{0}や{1}みたいにプログラム中のstring.Formatへの引数の順番を知らないと指定できないのと同じですし、だったらただの数字よりは人間にやさしくないですか?
あ、はい。 「リファクタリングにめちゃ弱いじゃないか?」と。 ん・・はい・・・そうですね。変数名で探しに行き・・・ますもんね・・・。たしかに・。
っ、は・はひっ・・。 「Reflection使ってまですることか。」と。 えぇ、はい。そうですね。・・・すみませんでした・・。
そんなにいじめないで。辞表投げつけたくなっちゃう。
追記終了
最後に
- こっそり、書式指定も対応しています。なので
"{now:yyyyMMdd}"
なんて風にDateTimeの書式指定なんかもできます。- 式木使えばもっとなんと便利になるんじゃないかなーとも思いましたが、とりあえずやめておきました。
参考
http://neue.cc/2013/01/05_392.html
neuec神に感謝!
「実行時解決はしている」とコメントを頂きました、確かに間違った用語を使ってしまっていたので修正させていただきました。ご指摘ありがとうございます。 ↩
- 投稿日:2019-08-29T14:26:48+09:00
ASP.NET MVCでCookieが保存されなくて困った話
あらまし
ajaxでActionMethodを呼び出して値をCookieに保存
↓
完了後にページ遷移させて、上記Cookieの値を参照
↓
変更が反映されてない!原因
AJAXはレスポンスを参照せず、正常終了したか否かしか見てなかった。
なので成功時のActionResultは以下のように返していた。return new HttpStatusCodeResult(HttpStatusCode.OK);対処方法
ActionResultを以下のように書き換えた。
return Json("OK");考察
検証はしていないが、おそらくクライアントに返却するレスポンスヘッダがnew HttpStatusCodeResult(HttpStatusCode.OK)では正しく生成されておらず、Cookieの書き換えが正常に行われていなかった。
Json("OK")ではレスポンスヘッダがきちんと生成されていてCookieの書き換えが正常に行なわれていたものと思われる。結論
気軽にActionResultにnew HttpStatusCodeResult(HttpStatusCode.OK)を返してはいけない
- 投稿日:2019-08-29T13:47:14+09:00
ASP.NET MVC5 でAction名とView名は統一すべき
タイトルから何が言いたいのかわからん
こういう作り方、すごく気持ち悪いです。
Index2のActionからはIndex2のViewが表示されるべき。SampleController.cs[HttpGet] public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index() { /* なんやかんや処理 */ return View(nameof(Index2)); // ←ActionがIndexなのにIndex2のViewが表示される } [HttpPost] public ActionResult Index2() { /* なんやかんや処理 */ return View(nameof(Index3)); // ←ActionがIndex2なのにIndex3のViewが表示される }Action名とView名の基本
Actionメソッドの作り方としては、以下のようにView名を省略したものが基本となる。
SampleController.cs[HttpGet] public ActionResult Index() { return View(); }ここから例外処理や処理分岐などで表示するViewが変化することはあっても、基本的には正常時に表示されるのはIndex.cshtmlであるのが綺麗な姿である。
また、画面修正時時にurlから修正対象のViewが一発でわかるためメンテナンス性もよい。課題
入力エラー発生時に前の画面に戻したい場合、urlとViewに差異が発生するため気持ち悪い。
SampleController.cs[HttpGet] public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index2(SampleViewModel vm) { if (ModelState.IsValid){ return View(nameof(Index)); // ←urlがIndex2なのにIndexのViewが表示される } new SampleModel().Register(vm); return View(vm); }RedirectToActionを利用すればある程度回避できるが、前のActionがPostだったりすると使えなかったり、GetでもModelStateをTempData経由で持ち回る必要があったりして無理矢理感が拭えない。
SampleController.cs[HttpGet] public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index2(SampleViewModel vm) { if (ModelState.IsValid){ return RedirectToAction(nameof(Index)); } new SampleModel().Register(vm); return View(vm); } [HttpPost] public ActionResult Index3(SampleViewModel vm) { if (ModelState.IsValid){ TempData["ModelState"] = ModelState; return RedirectToAction(nameof(Index2)); // Getで定義されたActionがないのでエラー } new SampleModel().Register2(vm); return View(vm); }
- 投稿日:2019-08-29T03:26:37+09:00
IEnumerableをforeachする際の速度検証
なぜこんなことをするのか
IEnumerableをforeachしたらオブジェクト化が起きるのでは...?と思ったから。
今回はIEnumerable,配列,リストで検証を行う。計測環境
Core i7-7700HQ 16GB DDR4 2400MHz NVIDIA GeForce GTX 1060 6GB 256GB NVMe SSD + 1TB HDD検証コード
var enumerable = Enumerable.Range(0, 10); var array = enumerable.ToArray(); var list = enumerable.ToList(); var sw = new Stopwatch(); sw.Start(); foreach (var i in enumerable){} sw.Stop(); Console.WriteLine(sw.Elapsed.ToString()); sw.Restart(); foreach (var i in list){} sw.Stop(); Console.WriteLine(sw.Elapsed.ToString()); sw.Restart(); foreach (var i in array){} sw.Stop(); Console.WriteLine(sw.Elapsed.ToString());結果
試行回数 IEnumerable List Array n = 10 0.0631ms 0.085ms 0.001ms n = 100 0.0689ms 0.0865ms 0.0015ms n = 1000 0.073ms 0.0922ms 0.0022ms n = 10000 0.1473ms 0.1360ms 0.0088ms 予想どうり!配列が一番速かった!
内部動作
展開されているコード
ヒープの状態
メモリの状態
上記の3つを見ていただければわかるのですが、配列が他2つに比べてかなりわかりやすいのではないかと。
IEnumerableはRangeIteratorに-1から9までの値を保存しているようで、配列やリストとはかなり違うアプローチをとっています。
ListはItemsにT[]を持っており、実質的に配列のようなものです。IEnumerableと異なる点は_size(リストのサイズ)があることくらいです。
配列はかなりシンプルで、0から10までの値をヒープに保存しています。感想
どうしても速度を求めたいのならば配列を使うといいと思います。そこまで遅い訳ではないので、バンバンListやEnumerableを使っていけば良いんじゃないかなと思いました!
- 投稿日:2019-08-29T00:20:57+09:00
【Unity(C#)】VRカメラを任意のポジションに移動する方法
サマーアドベントカレンダー
Unityゆるふわサマーアドベントカレンダー 2019 #ゆるふわアドカレの枠が空いたそうなので
急遽代打で参加させていただきました!!Unity関連の記事ということで本記事はスレスレですが、ゆるふわなのでセーフセーフ!
VRのカメラ
SteamVRやOculusIntegrationでのVR開発で独特なカメラの階層構造に行き詰まりました。
ご覧の通り、CameraRigというゲームオブジェクトの子階層にカメラが存在しています。
このような階層構造になっている理由は、
VRのカメラがHMDを追従して動くからです。(他にも理由があるかもしれませんが)つまり、直接カメラを動かすことはできず、親階層のCameraRigを動かすことで移動を再現します。
子階層のカメラは自由に動く
しかし、親階層のCameraRigを動かすことで移動を再現した場合、問題が発生します。
それは、子階層のカメラの位置を考慮しなければ任意の位置には移動できないことです。カメラはユーザーの移動に伴って位置が変わるので、特定の位置に誘導が難しい場合は
強制ワープさせた先で壁にめり込んでしまう
といった現象が引き起こされる恐れがあります。図解
説明をわかりやすくするために図を用いて説明します。
赤い点Aの場所にプレイヤーをワープさせたいとします。
薄緑の枠は部屋、青い四角はCameraRig、黄色い点はCameraRigの中心、黒く塗りつぶされた四角はカメラ(プレーヤー)です。カメラ(プレイヤー)のポジションを直接変更することはできないので、
CameraRigのポジションを赤い点Aに移動させることで
カメラ(プレイヤー)移動を再現するのですが、そのまま移動させるとこうなります↓赤い四角が移動後のCameraRigです。
ご覧の通り、カメラが部屋から はみ出してしまいました。
カメラの位置を考慮して移動させる必要がある理由は以上です。デモ
投げたキューブの位置にプレイヤーが移動するデモです。
少しわかりにくいですが、
キューブを持った状態で歩いてカメラの位置を初期位置からずらした後に投げてます。
キューブと同じ座標にプレイヤーが移動していることがわかるかと思います。
コード
今回のデモに利用したコードです。
適当なオブジェクトにアタッチusing UnityEngine; public class WarpCenterCamera : MonoBehaviour { [SerializeField] GameObject ovr_Rig; [SerializeField] GameObject centerCamera; [SerializeField] GameObject warpPointCube; void Update() { Vector3 ovr_Rig_Pos = ovr_Rig.transform.position; Vector3 centerCamera_Pos = centerCamera.transform.position; if (OVRInput.GetDown(OVRInput.RawButton.RIndexTrigger)) { ovr_Rig.transform.position = warpPointCube.transform.position; ovr_Rig.transform.position += new Vector3(ovr_Rig_Pos.x - centerCamera_Pos.x, 0, ovr_Rig_Pos.z - centerCamera_Pos.z); } } }下記の箇所でカメラの座標を考慮したCameraRigの座標の計算を行っています。
ovr_Rig.transform.position += new Vector3(ovr_Rig_Pos.x - centerCamera_Pos.x, 0, ovr_Rig_Pos.z - centerCamera_Pos.z);特定の向きを指定してなおかつ移動も行いたい場合は
ovr_Rig.transform.Rotate(new Vector3(0, 90, 0)); ovr_Rig.transform.position = warpPointCube.transform.position; ovr_Rig.transform.position += new Vector3(ovr_Rig_Pos.x - centerCamera_Pos.x, 0, ovr_Rig_Pos.z - centerCamera_Pos.z);のように回転させてから移動することで実装可能です。
実はもっと簡単な方法がありそうで怖いのですが、思いついたのでメモしときます。