20190827のC#に関する記事は11件です。

UnityでAndroidのKeystoreを自動でセーブ・ロードする

前提

  • unity 2018.4.7f1
  • Android

できること

  • 一度、KeystoreとAliasのパスワードを設定してビルドすると、以降、エディター起動時に自動的に設定されるようになります。

やりかた

  • 以下のコードを、適当なAssets/~/Editor/~へ入れてください。
SaveKeystore.cs
using UnityEditor;
using UnityEditor.Callbacks;

public class SaveKeystore {

    private static string keystorePrefsName => PlayerSettings.Android.keystoreName;
    private static string keyaliasPrefsName => $"{PlayerSettings.Android.keystoreName}/{PlayerSettings.Android.keyaliasName}";

    [InitializeOnLoadMethod]
    private static void OnLoad () {
        if (string.IsNullOrEmpty (PlayerSettings.Android.keystorePass) && !string.IsNullOrEmpty (PlayerSettings.Android.keystoreName)) {
            PlayerSettings.Android.keystorePass = EditorPrefs.GetString (keystorePrefsName);
        }
        if (string.IsNullOrEmpty (PlayerSettings.Android.keyaliasPass)) {
            PlayerSettings.Android.keyaliasPass = EditorPrefs.GetString (keyaliasPrefsName);
        }
    }

    [PostProcessBuild]
    private static void OnBuilded (BuildTarget target, string path) {
        if (target == BuildTarget.Android) {
            if (!string.IsNullOrEmpty (PlayerSettings.Android.keystoreName) && !string.IsNullOrEmpty (PlayerSettings.Android.keystorePass)) {
                EditorPrefs.SetString (keystorePrefsName, PlayerSettings.Android.keystorePass);
            }
            if (!string.IsNullOrEmpty (PlayerSettings.Android.keyaliasName) && !string.IsNullOrEmpty (PlayerSettings.Android.keyaliasPass)) {
                EditorPrefs.SetString (keyaliasPrefsName, PlayerSettings.Android.keyaliasPass);
            }
        }
    }

}

留意事項

  • KeystoreおよびAliasのパスワードが、平文でエディターの設定内(EditorPrefs)に保存されます。
  • Keystoreファイルの絶対パスに依存します。プロジェクトフォルダを移動するとオートロードできなくなります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでAndroidのKeystoreパスワードを自動セーブ・ロードする

前提

  • unity 2018.4.7f1
  • Android

できること

  • 一度、KeystoreとAliasのパスワードを設定してビルドすると、以降、エディター起動時に自動的に設定されるようになります。

やりかた

  • 以下のコードを、適当なAssets/~/Editor/~へ入れてください。
SaveKeystore.cs
using UnityEditor;
using UnityEditor.Callbacks;

public class SaveKeystore {

    private static string keystorePrefsName => PlayerSettings.Android.keystoreName;
    private static string keyaliasPrefsName => $"{PlayerSettings.Android.keystoreName}/{PlayerSettings.Android.keyaliasName}";

    [InitializeOnLoadMethod]
    private static void OnLoad () {
        if (string.IsNullOrEmpty (PlayerSettings.Android.keystorePass) && !string.IsNullOrEmpty (PlayerSettings.Android.keystoreName)) {
            PlayerSettings.Android.keystorePass = EditorPrefs.GetString (keystorePrefsName);
        }
        if (string.IsNullOrEmpty (PlayerSettings.Android.keyaliasPass)) {
            PlayerSettings.Android.keyaliasPass = EditorPrefs.GetString (keyaliasPrefsName);
        }
    }

    [PostProcessBuild]
    private static void OnBuilded (BuildTarget target, string path) {
        if (target == BuildTarget.Android) {
            if (!string.IsNullOrEmpty (PlayerSettings.Android.keystoreName) && !string.IsNullOrEmpty (PlayerSettings.Android.keystorePass)) {
                EditorPrefs.SetString (keystorePrefsName, PlayerSettings.Android.keystorePass);
            }
            if (!string.IsNullOrEmpty (PlayerSettings.Android.keyaliasName) && !string.IsNullOrEmpty (PlayerSettings.Android.keyaliasPass)) {
                EditorPrefs.SetString (keyaliasPrefsName, PlayerSettings.Android.keyaliasPass);
            }
        }
    }

}

留意事項

  • KeystoreおよびAliasのパスワードが、平文でエディターの設定内(EditorPrefs)に保存されます。
  • Keystoreファイルの絶対パスに依存します。プロジェクトフォルダを移動するとオートロードできなくなります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#でWindows Serviceを作る(5)

概要

前回の記事C#でWindows Serviceを作る(4)の続き。
イベントログの実装を行います。

環境

Windows 10
Visual Studio 2017 Community

手順

イベントログはWindowsで管理されるログです。
出力したログ情報は、イベントビューワーで見ることができます。
image.png

ServiceBaseクラスの派生クラス(デフォルトだとService1.csというファイル。私のプロジェクトではServiceTest.cs)をデザイナーで開きます。
⇒ソリューションエクスプローラで該当ファイルをダブルクリックしてください。

その後、ツールボックスから、コンポーネントの中にある「EventLog」を選択し、
デザイナーにドラッグ&ドロップします。
image.png

次にドロップしたEventLogコンポーネントをクリックして、プロパティタブを開きます。
nameを適宜変更します。
ここではServiceEventLogにしています。
image.png

ここまで(コンポーネントを追加すること)で、EventLogクラスのインスタンス(ServiceEventLog)が生成されます。
⇒ServiceTest.Designer.csに定義されていますね。

続いてServiceTest.csのコードを表示し、コンストラクタに処理を追加します。

ServiceTest.cs
    ServiceTest()
    {
        InitializeComponent();

        // ---------- 以下が追加した処理 ----------
        ServiceEventLog = new System.Diagnostics.EventLog();
        if (!System.Diagnostics.EventLog.SourceExists("ServiceTest"))
        {
            System.Diagnostics.EventLog.CreateEventSource(
                "ServiceTest", "Application");
        }
        ServiceEventLog.Source = "ServiceTest";
        ServiceEventLog.Log = "Application";
        // ----------------------------------------
    }

これでイベントログを出力する準備ができました。
試しに、前回Overrideして生成したOnPauseやOnContinueのイベントハンドラ内にログ出力処理を追加してみます。
ログ出力はWriteEntry関数を使用します。

ServiceTest.cs
    protected override void OnContinue()
    {
        ServiceEventLog.WriteEntry("OnContinue Called.");
        base.OnContinue();
    }

    protected override void OnPause()
    {
        ServiceEventLog.WriteEntry("OnPause Called.");
        base.OnPause();
    }

イベントビューワーを起動して、ログを確認してみます。
image.png

image.png

追加したイベントログがちゃんと保存されています。

次回は周期実行処理の実装について書きます。

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

.NETを使ったOfficeの自動化が面倒なはずがない―そう考えていた時期が俺にもありました。

前書き

.NETを使ったOfficeの自動化が面倒なはずがない

―そう考えていた時期が俺にもありました。

以下のPowerShellのコードを見てみましょう。

    $app = New-Object -ComObject Excel.Application
    $books = $app.Workbooks
    $book = $books.Open("test.xlsx")
    Write-Host $book.Sheets["Sheet1"].Cells[1,1].Text

    $book.Close()
    $app.Quit();

Excelを立ち上げて、シートのセルを表示し、Excelを終了するコードです。
本来であれば、なんの問題もありません。

しかしながら、起動したExcelのプロセスは終了せずタスクマネージャーに残り続けます。

今回はExcelで説明しましたが、これはWordでもOutlookでも同様です。また、PowerShellでなくC#で同様の実装をしても、この問題は発生します。

何が問題なのか?

Office オートメーションで割り当てたオブジェクトは、自分で解放する必要があります。
解放処理を適切に行わないことで、予期せぬ動作や、メモリの圧迫を引き起こします。
解放処理のベストプラクティスについてはマイクロソフトの中の人が以下のような記事をあげています。

Office オートメーションで割り当てたオブジェクトを解放する – Part1
Office オートメーションで割り当てたオブジェクトを解放する – Part2

.NETでOfficeの操作をする場合は必ず一読しておくことをお勧めします。
上記、記事のポイントとしては以下の通りです。

・作成されたRCWはReleaseComObjectで解放を行う
・Officeのアプリケーションの終了前後にガベージコレクトを実行する

作成されたReleaseComObjectで解放処理を行う。

New 演算子で Excel.Application クラス等を生成すると、RCW (ランタイム呼び出し可能ラッパー) も生成され COM オブジェクトのインスタンスを管理することになります。
ここで作成したRCWはReleaseComObjctを実行して参照カウントを減算します。これを行わないとオブジェクトが解放されずに予期せぬ動作を引き起こします。

    # ここでRCWが作成される。
    $app = New-Object -ComObject Excel.Application
    $app.Quit()

    # 作成したオブジェクトはReleaseComObjectを用いて解放する。
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($app)

COMオブジェクトは暗黙的に作成されることがあり、以下のケースがそれにあたります。

    # ここでRCWが作成される。
    $app = New-Object -ComObject Excel.Application

    # 暗黙的に「Microsoft.Office.Interop.Excel.Workbooks」オブジェクトが作成される。
    Write-Host $app.WorkBooks.Count

    $app.Quit()

    # 作成したオブジェクトはReleaseComObjectを用いて解放する。
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($app)

上記の暗黙的に作成されるCOMオブジェクトも解放処理を記載する必要があります。
つまり、以下のように実装する必要があります。

    # ここでRCWが作成される。
    $app = New-Object -ComObject Excel.Application

    $books = $app.WorkBooks
    Write-Host $books.Count

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($books)
    $app.Quit()

    # 作成したオブジェクトはReleaseComObjectを用いて解放する。
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($app)

How to properly release Excel COM objectsで述べているように「1 dot good, 2 dots bad」と覚えると楽だと思います。

Officeのアプリケーションの終了前後にガベージコレクトを実行する

Office 開発系サポートの提唱するいわゆる「ベストプラクティス」ではOfficeのアプリケーションの終了前後に明示的にガベージコレクトを実行することを薦めています。

アプリケーションの終了時には、Excel からの COM オブジェクトの参照解放処理が行われます。このタイミングまでに適切に COM オブジェクトが解放されていないと、ガベージコレクトのタイミングによっては予期せぬエラーが生じる場合があります。このため、アプリケーションの終了前にまずガベージコレクトを実行します。

さらに、Application インスタンスを解放する際なのですが、Marshal.ReleaseComObject メソッドを使用して参照カウンタをデクリメントしただけでは、プロセスが終了することを保証できませんので、GC.Collect メソッドでガベージコレクトを強制してオブジェクトを解放しています。

つまりいわゆる「ベストプラクティス」では以下のようになります。

    # ここでRCWが作成される。
    $app = New-Object -ComObject Excel.Application

    $books = $app.WorkBooks
    Write-Host $books.Count

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($books)

    # GCの対象になるようにNULLをいれて変数の参照を切る
    $books = $null
    Remove-Variable books -ErrorAction SilentlyContinue

    # アプリケーション終了前のGC
    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

    $app.Quit()

    # 作成したオブジェクトはReleaseComObjectを用いて解放する。
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($app)

    # GCの対象になるようにNULLをいれて変数の参照を切る
    $app = $null
    Remove-Variable app -ErrorAction SilentlyContinue

    # アプリケーション終了前のGC
    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

また、以下のようにReleaseComObjectなしのガベージコレクトだけでExcelのプロセスは終了して一見正しく動いているように見えます。

    # ここでRCWが作成される。
    $app = New-Object -ComObject Excel.Application

    $books = $app.WorkBooks
    Write-Host $books.Count

    # GCの対象になるようにNULLをいれて変数の参照を切る
    $books = $null
    Remove-Variable books -ErrorAction SilentlyContinue

    # アプリケーション終了前のGC
    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

    $app.Quit()

    # GCの対象になるようにNULLをいれて変数の参照を切る
    $app = $null
    Remove-Variable app -ErrorAction SilentlyContinue

    # アプリケーション終了前のGC
    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

Office オートメーションで割り当てたオブジェクトを解放する – Part2」のTipsではこの危険性を訴えています。

Null 代入の後、GC.Collect メソッドを実行するだけで良いように見える場合があります (例. EXCEL.EXE プロセスが終了するなど) が、これは非常に危険な方法です。
RCW への参照を残しているままの状態では、COM オブジェクト生成や参照等を繰り返した際に、例えば開発者が解放済みと考えている COM オブジェクトに対して接続しに行くなど、正しい COM オブジェクトに対する接続が実施できる保障ができなくなります。

なお、このベストプラクティスはあくまでOfficeアプリケーションのCOM解放処理のベストプラクティスであって、全てのベストプラクティスではありません。
サーバーサイドでガベージコレクトを明示的に実行することは、ベストではありません。

ベストプラクティスとその問題点

前書きに以下のようなコードを記載しました。

    $app = New-Object -ComObject Excel.Application
    $books = $app.Workbooks
    $book = $books.Open("test.xlsx")
    Write-Host $book.Sheets["Sheet1"].Cells[1,1].Text

    $book.Close()
    $app.Quit();

このベストプラクティスは以下のようになります。

powershell:powershellの例.ps1
    $app = New-Object -ComObject Excel.Application
    $books = $app.Workbooks
    $book = $books.Open("test.xlsx")
    $sheets = $book.Sheets
    $sheet = $sheets["Sheet1"]
    $cells = $sheet.Cells
    $cell = $cells[1,1]
    Write-Host $cell.Text

    $book.Close()

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($cell) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($cells) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheets) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($book) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($books) | Out-Null

    $cell = $null
    Remove-Variable cell -ErrorAction SilentlyContinue
    $cells = $null
    Remove-Variable cells -ErrorAction SilentlyContinue
    $sheet = $null
    Remove-Variable sheet -ErrorAction SilentlyContinue
    $sheets = $null
    Remove-Variable sheets -ErrorAction SilentlyContinue
    $book = $null
    Remove-Variable book -ErrorAction SilentlyContinue
    $books = $null
    Remove-Variable books -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

    $app.Quit();

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($app) | Out-Null
    $app = $null
    Remove-Variable app -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()
C#の例
using Excel = Microsoft.Office.Interop.Excel;

// 略
        static void test1()
        {
            {
                Excel.Application app = new Excel.Application();
                {
                    Excel.Workbooks books = app.Workbooks;
                    Excel.Workbook book = books.Open(@"test.xlsx");
                    Excel.Sheets sheets = book.Sheets;
                    Excel.Worksheet sheet = sheets["Sheet1"];
                    Excel.Range cells = sheet.Cells;
                    Excel.Range cell = cells[1, 1];
                    Console.WriteLine(cell.Value);

                    book.Close(Type.Missing, Type.Missing, Type.Missing);

                    Marshal.ReleaseComObject(cell);
                    Marshal.ReleaseComObject(cells);
                    Marshal.ReleaseComObject(sheet);
                    Marshal.ReleaseComObject(sheets);
                    Marshal.ReleaseComObject(book);
                    Marshal.ReleaseComObject(books);

                }
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();

                app.Quit();
                Marshal.ReleaseComObject(app);
            }

            // Application オブジェクトのガベージ コレクトを強制します。
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

いわゆるベストプラクティスではコードのステップ数が数倍になっていることがわかります。

また、ReleaseComObjectによるオブジェクトの解放漏れを検知することが非常に厄介です。
上記のコードで仮にReleaseComObjectをいくつか漏らしても、それを検知することは困難です。
先に説明したようにEXCELのプロセスが終了するなどの事象で解放漏れが起きているかどうかを検知することは難しいです。

また、暗黙的に作成されるCOMオブジェクトは、プログラマが意図せぬタイミングでおこなわれます。
たとえば以下のコードを10分ほど読んで見て解放漏れを考えてみてください。

                Excel.Application app = new Excel.Application();
                {
                    Excel.Workbooks books = app.Workbooks;
                    Excel.Workbook book = books.Open(@"test.xlsx");
                    Excel.Sheets sheets = book.Sheets;

                    foreach(Excel.Worksheet sheet in sheets)
                    {
                        Console.WriteLine(sheet.Name);
                        Marshal.ReleaseComObject(sheet);
                        Console.ReadLine();
                    }

                    Console.WriteLine("Excel起動済み");
                    Console.ReadLine();

                    book.Close(Type.Missing, Type.Missing, Type.Missing);

                    Marshal.ReleaseComObject(sheets);
                    Marshal.ReleaseComObject(book);
                    Marshal.ReleaseComObject(books);

                }
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();

                app.Quit();
                Marshal.ReleaseComObject(app);
                Console.WriteLine("ReleaseComObject");
                Console.ReadLine();
            }

            // Application オブジェクトのガベージ コレクトを強制します。
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            Console.WriteLine("ガベージコレクション済み");
            Console.ReadLine();

実は下記のコードのforeach句で「System.Runtime.InteropServices.CustomMarshalers.EnumeratorViewOfEnumVariant」からCOMオブジェクトが作成されてしまいます。

                    foreach(Excel.Worksheet sheet in sheets)
                    {
                        Console.WriteLine(sheet.Name);
                        Marshal.ReleaseComObject(sheet);
                        Console.ReadLine();
                    }

そのため、以下のようにforeachを使わずにCOMオブジェクトを操作した方が安全です。

                    for (int i = 1; i <= sheets.Count; ++i)
                    {
                        Excel.Worksheet sheet = book.Sheets[i];
                        Console.WriteLine(sheet.Name);
                        Marshal.ReleaseComObject(sheet);
                        Console.ReadLine();
                    }

このように、挙動を理解した上でソースコードを読み込まないとベストプラクティスのメモリ解放を実現することができないのです。

いわゆる「ベストプラクティス」の問題点の対応策

ソースコードを読まないと適切にメモリ解放をしているかわからない、かつ、タスクマネージャーだけではメモリが正しく解放されたかどうかを検知するのが困難であることがわかりました。
さすがに令和の時代にメモリ解放を気にして実装するのは辛いものがあるので、この状況を改善する方法について検討してみます。

NetOfficeを使用してCOMオブジェクトの解放を任せる

NetOfficeというMIT Licenseのオープンソースのライブラリが存在します。
https://github.com/NetOfficeFw/NetOffice

Officeの各オブジェクトをラッパーしてReleaseComObjectを行わなくてもすむようになっています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Excel = NetOffice.ExcelApi;

namespace NetOfficeSample
{
    class Program
    {
        static void Main(string[] args)
        {
            using (Excel.Application app = new Excel.Application()) {
                Excel.Workbook book = app.Workbooks.Open(@"test.xlsx");
                Excel.Worksheet workSheet = (Excel.Worksheet)book.Worksheets[1];
                Console.WriteLine(workSheet.Cells[1, 1].Value);

                Console.WriteLine("起動済み");
                Console.ReadLine();
                book.Close();
                app.Quit();

            }
            Console.WriteLine("Application終了");
            Console.ReadLine();
        }

    }
}

ただし、NetOfficeのコードをみるかぎりガベージコレクトを明示的にやっていないようにみえるので、いわゆるベストプラクティスとは違う部分があります。

RCWのオブジェクトの状況を監視する。

RCW (ランタイム呼び出し可能ラッパー) のオブジェクトの解放状況を監視しながら実装する方法もあります。
そのためには、マネージドヒープを監視するためのライブラリ、Microsoft.Diagnostics.Runtime.dll (nicknamed "CLR MD")を利用します。

https://github.com/microsoft/clrmd

なお、ウィルスバスターなどのウィルス対策ソフトは、このライブラリを使用して別プロセスにアタッチしてマネージドヒープを調べる操作を「怪しい操作」とみなすので、除外リストに入れて対応してください。

RCWのオブジェクトを列挙するサンプル

マネージドヒープ中のRCWのオブジェクトを列挙するサンプルを以下に記載します。

using Microsoft.Diagnostics.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EnumRcw
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (var process in System.Diagnostics.Process.GetProcessesByName(args[0]))
            {
                int pid = process.Id;
                Console.WriteLine("{0} {1} =======================================================", args[0], pid);
                using (var dataTarget = DataTarget.AttachToProcess(pid, 1000))
                {
                    Console.WriteLine(dataTarget.Architecture);
                    var clrVersion = dataTarget.ClrVersions.First();
                    var dacInfo = clrVersion.DacInfo;
                    ClrRuntime runtime = clrVersion.CreateRuntime();
                    foreach (var obj in runtime.Heap.EnumerateObjects())
                    {
                        ClrType type = obj.Type;
                        ulong size = obj.Size;
                        if (type.IsRCW(obj))
                        {
                            RcwData rcw = type.GetRCWData(obj);
                            if (rcw != null)
                            {
                                string ifname = "";
                                foreach (var i in rcw.Interfaces)
                                {
                                    ifname += i.Type.Name + ",";
                                }
                                Console.WriteLine("{0,16:X} {1,12:n0} {2} {3} {4} {5}", obj.Address, size, type.Name, rcw.RefCount, rcw.Disconnected, ifname);

                            }
                            else
                            {
                                Console.WriteLine("{0,16:X} {1,12:n0} {2} (GetRCWDataに失敗)", obj.Address, size, type.Name);

                            }
                        }
                    }
                }
            }
        }
    }
}


このプログラムは指定のプロセスにアタッチして、マネージドヒープ中のRCWオブジェクトを列挙します。
この際、アタッチ対象のプロセスと同じになるように、プラットフォームのターゲットをX86/X64を明示してください。

image.png

コマンドラインからプロセス名?を指定することで、現在のヒープ中のRCWのオブジェクトを列挙します。
以下のサンプルは「foreach(Excel.Worksheet sheet in sheets)」のブロック内におけるRCWのオブジェクトを列挙した例になります。

>EnumRcw プロセス名
memorycheck 18688 =======================================================
Amd64
     25601C57CE8           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     25601C57D08           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     25601C58348           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,
     25601C58368           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Sheets,
     25601C58C78           32 System.__ComObject 1 False
     25601C58DA8           32 System.__ComObject (GetRCWDataに失敗)

指定のオブジェクトを参照しているオブジェクトを探す

Microsoft.Diagnostics.Runtime.dll では指定のオブジェクトがGCのルートから、どのパスで参照されているか確認することもできます。
https://blog.maartenballiauw.be/post/2017/01/03/exploring-.net-managed-heap-with-clrmd.html

using Microsoft.Diagnostics.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WalkHeap
{
    class Program
    {
        static void Main(string[] args)
        {
            string name = args[0];
            foreach (var process in System.Diagnostics.Process.GetProcessesByName(args[0]))
            {
                int pid = process.Id;
                Console.WriteLine("{0} {1} =======================================================", args[0], pid);
                ulong taregetPtr = ulong.Parse(args[1], System.Globalization.NumberStyles.HexNumber);

                using (var dataTarget = DataTarget.AttachToProcess(pid, 1000))
                {
                    var clrVersion = dataTarget.ClrVersions.First();
                    var dacInfo = clrVersion.DacInfo;
                    ClrRuntime runtime = clrVersion.CreateRuntime();
                    var stack = new Stack<ulong>();

                    var heap = runtime.Heap;
                    if (heap.CanWalkHeap)
                    {
                        Console.WriteLine("-----");
                        foreach (var ptr in heap.EnumerateObjectAddresses())
                        {
                            var type = heap.GetObjectType(ptr);
                            if (type == null || taregetPtr != ptr)
                            {
                                continue;
                            }

                            Console.WriteLine("find");

                            // todo: retention path
                            Console.WriteLine("roots...");
                            foreach (var root in heap.EnumerateRoots())
                            {
                                stack.Clear();
                                stack.Push(root.Object);

                                if (GetPathToObject(heap, ptr, stack, new HashSet<ulong>()))
                                {
                                    // Print retention path
                                    var depth = 0;
                                    foreach (var address in stack)
                                    {
                                        var t = heap.GetObjectType(address);
                                        if (t == null)
                                        {
                                            Console.WriteLine("{0} {1,16:X} ", new string('+', depth++), address);
                                            continue;
                                        }

                                        Console.WriteLine("{0} {1,16:X} - {2} - {3} bytes", new string('+', depth++), address, t.Name, t.GetSize(address));
                                    }

                                    break;
                                }
                            }

                            break;
                        }
                    }

                }

            }
        }

        // https://blog.maartenballiauw.be/post/2017/01/03/exploring-.net-managed-heap-with-clrmd.html
        private static bool GetPathToObject(ClrHeap heap, ulong objectPointer, Stack<ulong> stack, HashSet<ulong> touchedObjects)
        {
            // Start of the journey - get address of the first objetc on our reference chain
            var currentObject = stack.Peek();

            // Have we checked this object before?
            if (!touchedObjects.Add(currentObject))
            {
                return false;
            }

            // Did we find our object? Then we have the path!
            if (currentObject == objectPointer)
            {
                return true;
            }


            // Enumerate internal references of the object
            var found = false;
            var type = heap.GetObjectType(currentObject);
            if (type != null)
            {
                type.EnumerateRefsOfObject(currentObject, (innerObject, fieldOffset) =>
                {
                    if (innerObject == 0 || touchedObjects.Contains(innerObject))
                    {
                        return;
                    }

                    // Push the object onto our stack
                    stack.Push(innerObject);
                    if (GetPathToObject(heap, objectPointer, stack, touchedObjects))
                    {
                        found = true;
                        return;
                    }

                    // If not found, pop the object from our stack as this is not the tree we're looking for
                    stack.Pop();
                });
            }

            return found;
        }
    }
}

コマンドラインから以下のようにプロセス名とオブジェクトのアドレスを指定して実行することで、指定のプロセスのオブジェクトが、どのように参照されているか表示します。

>WalkHeap.exe memorycheck 25601C58C78
memorycheck 18688 =======================================================
-----
find
roots...
      25601C58C78 - System.__ComObject - 32 bytes
+      25601C58CD0 - System.Runtime.InteropServices.CustomMarshalers.EnumeratorViewOfEnumVariant - 40 bytes

RCWの解放状況の検証

ヒープ上のRCWの状況を検査する方法が分かったので様々なケースで実験してみようと思います。

OS
 Windows10 64bit

Excel
 Office16 Excel32bit

PowerSehll
 5.1.17134.858(64bit)

操作側のアプリ:
 .NET Framework4.5.2
 64bit
 Releaseビルド
debugビルドだと挙動が変わる可能性があります。

実験1 アプリケーションの解放漏れがあるケース
        //using Excel = Microsoft.Office.Interop.Excel;
        static void t1()
        {
            Excel.Application app = new Excel.Application();
            app.Quit();
            Console.WriteLine("ここでヒープのチェックを行う");
            Console.ReadLine();
        }

結果:

memorycheck 20140 =======================================================
Amd64
     19880007BE0           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,

Microsoft.Office.Interop.Excel.ApplicationClassが解放されずに残っていることがわかります。

実験2 アプリケーションの解放をReleaseComObjectで行った場合
        //using Excel = Microsoft.Office.Interop.Excel;
        static void t1_2()
        {
            Excel.Application app = new Excel.Application();
            app.Quit();
            Marshal.ReleaseComObject(app);
            Console.WriteLine("ここでヒープのチェックを行う");
            Console.ReadLine();
        }


結果:

memorycheck 16108 =======================================================
-----

Microsoft.Office.Interop.Excel.ApplicationClassが解放されていることが確認できます。

実験3 アプリケーションの解放をガベージコレクションだけで行った場合
        //using Excel = Microsoft.Office.Interop.Excel;
        static void t1_3()
        {
            Excel.Application app = new Excel.Application();
            app.Quit();
            app = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            Console.WriteLine("ここでヒープのチェックを行う t1_3");
            Console.ReadLine();
        }

結果:

memorycheck 8664 =======================================================
Amd64

Microsoft.Office.Interop.Excel.ApplicationClassが解放されていることが確認できます。
なお、デバッグビルドの場合は以下のようになります。

memorycheck 19908 =======================================================
Amd64
     1B583A77BD8           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,

これはデバッグ実行の場合、オブジェクトの生存期間が異なるためです。

実験4 暗黙の参照が作成されるケース
        //using Excel = Microsoft.Office.Interop.Excel;
        static void t2()
        {
            Excel.Application app = new Excel.Application();
            // Workbooksの暗黙のオブジェクトの作成
            Excel.Workbook book = app.Workbooks.Open(@"test.xlsx");
            Console.WriteLine("ここでヒープのチェックを行う1");
            Console.ReadLine();
            book.Close();
            Marshal.ReleaseComObject(book);

            app.Quit();
            Marshal.ReleaseComObject(app);
            Console.WriteLine("ここでヒープのチェックを行う2");
            Console.ReadLine();
        }

結果:

1回目のメモリの内容
memorycheck 21916 =======================================================
Amd64
     1D04E377CE8           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     1D04E377D08           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     1D04E378348           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,


2回目のメモリの内容
memorycheck 21916 =======================================================
Amd64
     1D04E377D08           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     1D04E378348           32 System.__ComObject (GetRCWDataに失敗)

Microsoft.Office.Interop.Excel.Workbooksのオブジェクトが暗黙的に作成されていることがわかります。
この暗黙的なオブジェクトを解放しない場合、「Microsoft.Office.Interop.Excel._Workbook」のオブジェクトが中途半端に残っていることも確認できます。

実験5 暗黙の参照が作成しないように修正したケース
        static void t2_1()
        {
            Excel.Application app = new Excel.Application();
            Excel.Workbooks books = app.Workbooks;
            Excel.Workbook book = books.Open(@"test.xlsx");
            Console.WriteLine("ここでヒープのチェックを行う1 t_1");
            Console.ReadLine();
            book.Close();
            Marshal.ReleaseComObject(book);
            Marshal.ReleaseComObject(books);

            app.Quit();
            Marshal.ReleaseComObject(app);
            Console.WriteLine("ここでヒープのチェックを行う2");
            Console.ReadLine();
        }

結果:

1回目のメモリの状態:
memorycheck 22292 =======================================================
Amd64
     12E47377CF0           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     12E47377D10           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     12E47378350           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,


2回目のメモリの状態:
memorycheck 22292 =======================================================
Amd64

作成したオブジェクトが全て削除されていることが確認できます。

実験6 for eachを用いた繰り返しの場合
        static void t3()
        {
            Excel.Application app = new Excel.Application();
            Excel.Workbooks books = app.Workbooks;
            Excel.Workbook book = books.Open(@"test.xlsx");
            foreach (var sheet in book.Sheets)
            {
                Console.WriteLine("ここでヒープのチェックを行う1(ループ内) t3");
                Console.ReadLine();
                Marshal.ReleaseComObject(sheet);
            }
            Console.WriteLine("ここでヒープのチェックを行う2 ループ完了 t3");
            Console.ReadLine();
            book.Close();
            Marshal.ReleaseComObject(book);
            Marshal.ReleaseComObject(books);

            app.Quit();
            Marshal.ReleaseComObject(app);
            Console.WriteLine("ここでヒープのチェックを行う3");
            Console.ReadLine();
        }

結果:

1:ループ内のメモリの状態
memorycheck 17784 =======================================================
Amd64
     1C74A3B7D50           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     1C74A3B7D70           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     1C74A3B83B0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,
     1C74A3B83D0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Sheets,
     1C74A3B8CE0           32 System.__ComObject 1 False
     1C74A3B8E10           32 System.__ComObject 1 False

2: 1C74A3B8CE0の参照箇所を調べる
-----
find
roots...
      1C74A3B8CE0 - System.__ComObject - 32 bytes
+      1C74A3B8D38 - System.Runtime.InteropServices.CustomMarshalers.EnumeratorViewOfEnumVariant - 40 bytes

3: 1C74A3B8E10 の参照箇所を調べる
-----
find
roots...
      1C74A3B8E10 - System.__ComObject - 32 bytes

4: ループ外でのメモリの状態
memorycheck 17784 =======================================================
Amd64
     1C74A3B7D50           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     1C74A3B7D70           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     1C74A3B83B0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,
     1C74A3B83D0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Sheets,
     1C74A3B8CE0           32 System.__ComObject 1 False
     1C74A3B8E10           32 System.__ComObject (GetRCWDataに失敗)

5: 1C74A3B8CE0の参照箇所を調べる
-----
find
roots...

6: 1C74A3B8E10の参照箇所を調べる
-----
find
roots...

7: Officeアプリケーション終了後
memorycheck 17784 =======================================================
Amd64

foreachでループを行った場合、1C74A3B8CE0が暗黙的に作成されていることがわかります。このオブジェクトはSystem.Runtime.InteropServices.CustomMarshalers.EnumeratorViewOfEnumVariantから参照されていましたが、ループを抜けると、どこからも参照されずゴミとして残ってしまいます。
※Officeの終了で削除されるが、その間、予期せぬオブジェクトが残り続ける

実験7 for eachを排除した繰り返しの場合
        static void t3_1()
        {
            Excel.Application app = new Excel.Application();
            Excel.Workbooks books = app.Workbooks;
            Excel.Workbook book = books.Open(@"test.xlsx");
            Excel.Sheets sheets = book.Sheets;

            for (int i = 1; i <= sheets.Count; ++i)
            {
                var sheet = sheets[i];
                Console.WriteLine("ここでヒープのチェックを行う1(ループ内) t3-1");
                Console.ReadLine();
                Marshal.ReleaseComObject(sheet);

            }
            Console.WriteLine("ここでヒープのチェックを行う2 ループ完了 t3");
            Console.ReadLine();
            book.Close();

            Marshal.ReleaseComObject(sheets);
            Marshal.ReleaseComObject(book);
            Marshal.ReleaseComObject(books);

            app.Quit();
            Marshal.ReleaseComObject(app);
            Console.WriteLine("ここでヒープのチェックを行う3");
            Console.ReadLine();
        }
ループ内のメモリの状態:
memorycheck 17612 =======================================================
Amd64
     29107A77D90           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     29107A77DB0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     29107A783F0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,
     29107A78410           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Sheets,
     29107A78480           32 System.__ComObject 1 False

ループ外のメモリの状態:
memorycheck 17612 =======================================================
Amd64
     29107A77D90           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     29107A77DB0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     29107A783F0           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,
     29107A78410           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Sheets,
     29107A78480           32 System.__ComObject (GetRCWDataに失敗)

Officeアプリケーション終了後のメモリの状態
memorycheck 17612 =======================================================
-----

foreachで暗黙的に作成されたSystem.Runtime.InteropServices.CustomMarshalers.EnumeratorViewOfEnumVariantからの参照されているSystem.__ComObjectが存在しないことがわかります。

実験8 RCWのオブジェクトを別の変数に格納した場合
        //using Excel = Microsoft.Office.Interop.Excel;
        static void t4()
        {
            Excel.Application app = new Excel.Application();
            Excel.Workbooks books = app.Workbooks;
            Excel.Workbook book = books.Open(@"test.xlsx");
            var book2 = book;
            Console.WriteLine(book.Name);
            Console.WriteLine(book2.Name);

            Console.WriteLine("ここでヒープのチェックを行う1 t_1");
            Console.ReadLine();
            book.Close();
            Marshal.ReleaseComObject(book);
            Marshal.ReleaseComObject(books);

            app.Quit();
            Marshal.ReleaseComObject(app);
            Console.WriteLine("ここでヒープのチェックを行う2");
            Console.ReadLine();
        }

結果:

1回目:
MemoryCheck 15648 =======================================================
Amd64
     1FC94A97CF0           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False Microsoft.Office.Interop.Excel._Application,
     1FC94A97D10           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,Microsoft.Office.Interop.Excel.Workbooks,
     1FC94A98350           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel._Workbook,


2回目:
MemoryCheck 15648 =======================================================
Amd64

1回目のメモリの内容確認でExcel.Workbookをbookとbook2変数に格納していますが、RCWのオブジェクト自体が増加しておらず、かつ、参照カウンタが1のままであることが確認できます。
2回目のbook変数のみにたいしてReleaseComObjectを行うことでRCWオブジェクトが削除されることも確認できます。

実験9:PowerShellで実行した場合
test2.ps1
    Read-Host "初期状態"

    $app = New-Object -ComObject Excel.Application
    $books = $app.Workbooks
    $book = $books.Open("test.xlsx")
    $sheets = $book.Sheets
    $sheet = $sheets["Sheet1"]
    $cells = $sheet.Cells
    $cell = $cells[1,1]
    Write-Host $cell.Text
    #Write-Host $cell.Value # Variant Value (Variant)が表示される
    #$cell.Value = "TEST"
    #Write-Host $cell.Text

    $book.Close()
    Read-Host "①起動済み"

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($cell) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($cells) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheets) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($book) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($books) | Out-Null
    Read-Host "②ReleaseComObject"

    $cell = $null
    Remove-Variable cell -ErrorAction SilentlyContinue
    $cells = $null
    Remove-Variable cells -ErrorAction SilentlyContinue
    $sheet = $null
    Remove-Variable sheet -ErrorAction SilentlyContinue
    $sheets = $null
    Remove-Variable sheets -ErrorAction SilentlyContinue
    $book = $null
    Remove-Variable book -ErrorAction SilentlyContinue
    $books = $null
    Remove-Variable books -ErrorAction SilentlyContinue


    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

    $app.Quit();


    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($app) | Out-Null
    $app = $null
    Remove-Variable app -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()
    Read-Host "すべて終了"

結果:

1: スクリプト実行前
powershell 1656 =======================================================
Amd64
     2873E065E98           32 System.__ComObject 1 False Windows.Foundation.Diagnostics.IAsyncCausalityTracerStatics,

2: スクリプト実行して「初期状態」が表示された時点の状態
powershell 1656 =======================================================
Amd64
     2873E065E98           32 System.__ComObject 1 False Windows.Foundation.Diagnostics.IAsyncCausalityTracerStatics,
     2873E709130           32 System.__ComObject (GetRCWDataに失敗)

※2873E709130はルートから検索できない

3:「①起動済み:」が表示された時点の状態
powershell 1656 =======================================================
Amd64
     2873E065E98           32 System.__ComObject 1 False Windows.Foundation.Diagnostics.IAsyncCausalityTracerStatics,
     2873E7B2618           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False System.Management.Automation.IDispatch,Microsoft.Office.Interop.Excel._Application,
     2873E7BA850           32 System.__ComObject 1 False System.Runtime.InteropServices.ComTypes.ITypeInfo,
     2873E87F860           32 System.__ComObject 1 False Microsoft.Office.Interop.Excel.Workbooks,System.Management.Automation.ComInterop.IDispatch,
     2873E891FE0           32 System.__ComObject 1 False System.Runtime.InteropServices.ComTypes.ITypeInfo,
     2873E8DD090           32 System.__ComObject 1 False System.Management.Automation.ComInterop.IDispatch,
     2873E8EF398           32 System.__ComObject 1 False System.Runtime.InteropServices.ComTypes.ITypeInfo,
     2873E93BB10           32 System.__ComObject 1 False System.Management.Automation.ComInterop.IDispatch,
     2873E94AE08           32 System.__ComObject 1 False System.Runtime.InteropServices.ComTypes.ITypeInfo,
     2873E9A66A0           32 System.__ComObject 1 False System.Management.Automation.ComInterop.IDispatch,
     2873E9B7928           32 System.__ComObject 1 False System.Runtime.InteropServices.ComTypes.ITypeInfo,
     2873E9F5C90           32 System.__ComObject 1 False System.Management.Automation.ComInterop.IDispatch,
     2873EA05D48           32 System.__ComObject 2 False System.Runtime.InteropServices.ComTypes.ITypeInfo,
     2873EA6ED78           32 System.__ComObject 1 False System.Management.Automation.ComInterop.IDispatch,

※2873E709130は消えている

4:「②ReleaseComObject:」が表示された時点の状態
powershell 1656 =======================================================
Amd64
     2873E065E98           32 System.__ComObject 1 False Windows.Foundation.Diagnostics.IAsyncCausalityTracerStatics,
     2873E6B77C0           32 System.__ComObject (GetRCWDataに失敗)
     2873E6C5288           32 System.__ComObject (GetRCWDataに失敗)
     2873E6DBB58           32 System.__ComObject (GetRCWDataに失敗)
     2873E6E0058           32 System.__ComObject (GetRCWDataに失敗)
     2873E6F11D8           32 System.__ComObject (GetRCWDataに失敗)
     2873E715028           32 System.__ComObject (GetRCWDataに失敗)
     2873E7B2618           32 Microsoft.Office.Interop.Excel.ApplicationClass 1 False System.Management.Automation.IDispatch,Microsoft.Office.Interop.Excel._Application,
     2873E7BA850           32 System.__ComObject 1 False System.Runtime.InteropServices.ComTypes.ITypeInfo,

5:「すべて終了」が表示された時点の状態
powershell 1656 =======================================================
Amd64
     2873E065E98           32 System.__ComObject 1 False Windows.Foundation.Diagnostics.IAsyncCausalityTracerStatics,
     2873E7BA850           32 System.__ComObject 1 False System.Runtime.InteropServices.ComTypes.ITypeInfo,


2873E7BA850の検索結果は以下の通り
      2873E7BA850 - System.__ComObject - 32 bytes
+      2873E7BAEA8 - System.Management.Automation.ComProperty - 64 bytes
++      2873E7E1738 - System.Collections.Generic.Dictionary+Entry<System.String,System.Management.Automation.ComProperty>[] - 10368 bytes
+++      2873E7BA950 - System.Collections.Generic.Dictionary<System.String,System.Management.Automation.ComProperty> - 80 bytes
++++      2873E7BA918 - System.Management.Automation.ComTypeInfo - 56 bytes
+++++      2873E7EC398 - System.Management.Automation.DotNetAdapterWithComTypeName - 32 bytes
++++++      2873E7EC3B8 - System.Management.Automation.PSObject+AdapterSet - 32 bytes
+++++++      2873E7EC3D8 - System.Collections.Concurrent.ConcurrentDictionary+Node<System.Type,System.Management.Automation.PSObject+AdapterSet> - 48 bytes
++++++++      2873E3FD9B8 - System.Collections.Concurrent.ConcurrentDictionary+Node<System.Type,System.Management.Automation.PSObject+AdapterSet>[] - 272 bytes
+++++++++      2873E3FDAC8 - System.Collections.Concurrent.ConcurrentDictionary+Tables<System.Type,System.Management.Automation.PSObject+AdapterSet> - 48 bytes
++++++++++      2873E3FD8B8 - System.Collections.Concurrent.ConcurrentDictionary<System.Type,System.Management.Automation.PSObject+AdapterSet> - 64 bytes

PowerShellでは初期状態で「Windows.Foundation.Diagnostics.IAsyncCausalityTracerStatics」のCOMオブジェクトが存在しています。
いわゆるベストプラクティスを守ったスクリプトを実行しても、操作終了後に「System.Runtime.InteropServices.ComTypes.ITypeInfo」が残ります。このオブジェクトはSystem.Collections.Concurrent.ConcurrentDictionaryから参照されています。

※以下のs_adapterMappingのように見えるが詳細は不明。
https://github.com/PowerShell/PowerShell/blob/c684902fba5b5e63fa750e66270a25b6889b4ec6/src/System.Management.Automation/engine/MshObject.cs

まとめと意識の低い対応策

.NETでOfficeオートメーションを使用する場合、いわゆるベストベストプラクティスは以下のようになります。

・アプリケーションで作成したRCWオブジェクトはReleaseComObjectで解放します
・RCWオブジェクトは暗黙的に作成される場合があります。
 例:
  foreach句
  「app.WorkBooks.Count」で2ドット含む場合
・Officeオートメーションの解放だけを考えた場合、Officeの終了前後でガベージコレクトを実行する。

正直面倒…さらにいうとサーバー側のプログラムでガベージコレクトを実行するのは、よくないです。

ここでは、意識を低くして対応策を考えてみます。

Office オートメーションで割り当てたオブジェクトを解放する – Part1」によると以下のような記述があります。

なお、コンソール アプリケーション等では、処理が一通り実行された後、プロセスが終了する際に CLR ランタイムが終了するのに合わせて、Application オブジェクトの解放および EXCEL.EXE プロセスの終了が実施されます。そのため、解放漏れがあったとしても、プログラム終了時に解放されるため、ほとんどの場合大きな影響がない傾向があります。

すなわち、 Officeの操作をして、すぐプロセスを終了すると影響が少なくできると考えられます。
なので、別プロセスで起動して単一のOfficeの操作を行い、すぐ終了するという設計にすることで、Officeの解放漏れを抑えられると考えられます。

たとえばPowerShellを実行する場合も以下のように別プロセスで実行してしまいます。

# PowerShellのコマンドプロンプトを落とすまで影響がのこる
./test1.ps1

# 別プロセスで実行することで影響を抑える
powershell ./test1.ps1

あとはわりきって、完璧なリソース解放はあきらめて、操作中はOfficeを使用しないでくださいとか運用でカバーしたり、予期せぬOfficeのプロセスが起動してたら終了させるなどの意識の低い対処案が考えられます。

参考

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

C#でPowerShellを使ってCOMオブジェクトブラウザを作ってみた

COMオブジェクトのメンバー一覧を取得する (PowerShell)
を応用してブラウザつくってみた。
※oleview.exeを持ってればいらない説…(まだ使ってないのでわからない)

ちなみにCOMオブジェクトでない普通の.Netのクラス(System.IO.Fileとか)であれば、
C#リフレクションTIPS 55連発
が参考になります。

画面キャプチャ

image.png

ソースコード

using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using System.Management.Automation;


public class ProgIdInfo
{
    private PSObject _psObj; // ProgID レジストリキー情報のPSObject
    private string _ProgID;
    private string _CLSID;
    private string _ValueOfCLSID;

    public string ProgID       { get{return _ProgID;} }
    public string CLSID        { get{return _CLSID;} }
    public string ValueOfCLSID { get{return _ValueOfCLSID;} }

    // レジストリのキーを返すので取り扱い注意
    // (戻ってきた PSObject に対してSetやDelete等の書き込み系のメソッドを使わないように。)
    //   ProgIDの一覧取得については、PowerShell使わなくてもレジストリ検索を実装すればできなくはない。その方が処理速いかも
    private static Collection<PSObject> GetProgIdCollection()
    {
        using ( var invoker = new RunspaceInvoke() ) {
            return invoker.Invoke(@"dir REGISTRY::HKEY_CLASSES_ROOT\CLSID -Include PROGID -Recurse");
        }
    }

    public static List<ProgIdInfo> GetProgIDs()
    {
        var ret = new List<ProgIdInfo>();

        var psObjs = GetProgIdCollection();

        foreach ( var psObj in psObjs ) {
            ProgIdInfo t = NewProgIdInfo(psObj);
            ret.Add(t);
        }

        return ret;
    }

    private static ProgIdInfo NewProgIdInfo(PSObject psObj)
    {
        var ret = new ProgIdInfo();
        ret._psObj  = psObj;
        ret._ProgID = "";
        ret._CLSID  = "";
        ret._ValueOfCLSID = "";

        ret._ProgID  = PSObject_Invoke_GetValue(psObj); // ProgID の取得

        string[] regKeyPathOfProgID = (psObj.Members["Name"].Value??"").ToString().Split('\\');
        // ["Name"] は HKEY_CLASSES_ROOT\CLSID\{xxxx..-xx..}\ProgID が返る

        if ( regKeyPathOfProgID.Length >= 2 ) {
            ret._CLSID = regKeyPathOfProgID[regKeyPathOfProgID.Length-2]; // parent of last node
        }
        string parentKey = MyPartialJoin("\\", regKeyPathOfProgID, 1, 1); // 先頭の @"HKEY_CLASSES_ROOT\" と末尾の @"\ProgID" を捨てる
        ret._ValueOfCLSID = GetClassRootRegisterValue(parentKey, ""); // 既定の値を読み出す

        return ret;
    }


    // レジストリアクセス
    private static string GetClassRootRegisterValue(string regKeyPath, string regValueName)
    {
        if ( regKeyPath == null || regKeyPath == "" ) {
            return "";
        }

        Microsoft.Win32.RegistryKey regkey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(regKeyPath);
        if (regkey == null) {
            return "";
        }

        try{
            return (regkey.GetValue(regValueName)??"").ToString();
        }
        finally{
            regkey.Close();
        }
    }

    private static string MyPartialJoin(string separator, string[] ss, int startIndex, int numOfOmitLast)
    {
        // CopyメソッドなりLINQなり使った方が綺麗な気がするが、とりあえずこれで。
        int n = ss.Length - startIndex - numOfOmitLast;
        if ( n <= 0 ) { return ""; }
        else {
            string[] ss2 = new String[n];
            for ( int i = startIndex; i < ss.Length - numOfOmitLast; i++) {
                ss2[i-startIndex] = ss[i];
            }
            return String.Join(separator, ss2);
        }
    }

    private static string PSObject_Invoke_GetValue(PSObject psObj)
    {
        object t = psObj.Methods["GetValue"].Invoke(new object[]{""}); // 規定値を取得する。 // レジストリアクセスでやってもよい
        return (t??"").ToString(); // Microsoft PenInputPanel ControlがなぜかProgIdの値が空欄なので、処置しないと例外発生する
    }
}


public class ComObjectMember
{
    private PSObject _psObj;
    private string _MemberTypeText;
    private string _ReturnTypeText;
    private string _MemberName;
    private string _DefinitionText;

    private static Regex r = new Regex(@"^([^ ]+)");

    public string MemberTypeText { get{return _MemberTypeText;} }
    public string ReturnTypeText { get{return _ReturnTypeText;} }
    public string MemberName     { get{return _MemberName;}     }
    public string DefinitionText { get{return _DefinitionText;} }

    private static Collection<PSObject> _GetMembers_PS(string comObjectName)
    {
        Regex r = new Regex(@"^[ ._0-9A-Za-z]+$"); // MS Remote など、スペースが入っている輩がいるので注意

        if ( r.IsMatch(comObjectName) ) {  // 危険なパラメータが混入しないように簡易チェック
            string[] source = new string[] {
                "$t = New-Object -ComObject \"" + comObjectName + "\"",
                "Get-Member -InputObject $t"
            }; // memo: ReleaseComObjectしないとよろしくない気がする

            using ( var invoker = new RunspaceInvoke() ) {
                try {
                    Collection<PSObject> tmp = invoker.Invoke(source[0]);
                    Collection<PSObject> result = invoker.Invoke(source[1]);
                    return result;
                }
                catch (System.Management.Automation.CmdletInvocationException e) {
                    // memo:
                    //  VideoRenderCtl.VideoRenderCtl.1 選んだらCreateInstanceに失敗したっぽい例外を吐いたので
                    //  とりあえずcatchしておく
                    Console.WriteLine(e);
                    return new Collection<PSObject>();
                }
            }
        }
        else {
            return new Collection<PSObject>();// 要素数 0 の Collection を返しておく
        }
    }


    public static List<ComObjectMember> GetMembers(string comObjectName)
    {
        var psObjs = _GetMembers_PS(comObjectName);
        var ret = new List<ComObjectMember>();

        foreach ( var psObj in psObjs ) {
            ret.Add(NewComObjectMember(psObj));
        }

        return ret;
    }

    private static ComObjectMember NewComObjectMember(PSObject psObj)
    {
        var ret = new ComObjectMember();
        ret._psObj = psObj;

        // Regex r = new Regex(@"^([^ ]+)");
        // フォーマットが複雑すぎて分解することが困難なため、戻り値の型(っぽい)部分だけを取り出す。
        //   - overloadが1行に入ってたりする (例: System.Random)
        //   - 仮引数名があったりなかったり
        //   - ()がない場合もある
        //   - ()がネストしている場合もある
        //   - 後ろに{get} {set} がつく場合もある

        Match m = r.Match(psObj.Members["Definition"].Value.ToString());
        //Match m = r.Match(psObj.ToString());

        if ( m.Success ) {
            ret._MemberTypeText = (psObj.Members["MemberType"].Value).ToString(); // PSMemberTypes (Method / Property など)
            ret._ReturnTypeText = m.Groups[1].Value;
            ret._MemberName     = psObj.Members["Name"].Value.ToString();
            ret._DefinitionText = psObj.Members["Definition"].Value.ToString();
        }
        else {
            // 想定外の形式の場合は、とりあえずToString()して DefinitionText に入れる
            ret._MemberTypeText = "";
            ret._ReturnTypeText = "";
            ret._MemberName     = "";
            ret._DefinitionText = psObj.ToString();
        }

        return ret;
    }

}


class MainForm:Form
{
    Button btnGetProgID;
    Button btnGetMembers;
//    CheckBox chkFilter;
    TextBox  txtFilter;
    SplitContainer spl;
    TextBox txtProgID;
    ListView lsvProgID;
    ListView lsvMemb;
    MyListViewContextMenuInfo contextMenuInfo;

    List<ProgIdInfo> allProgIDs; // filter処理を入れたいので、ListViewに直接持たせるのはやめた。


    MainForm()
    {
        Text = "COM Object Browser";

        allProgIDs = new List<ProgIdInfo>();

        spl = new SplitContainer();
        spl.Location = new Point(0, 0);
        spl.Dock = DockStyle.Fill;
        spl.Orientation = Orientation.Vertical;
        Controls.Add(spl);

        ////// 左 Panel ここから
        btnGetProgID = new Button();
        btnGetProgID.Location = new Point(0, 0);
        btnGetProgID.Size = new System.Drawing.Size(80,25);
        btnGetProgID.Text = "Get ProgIDs";
        btnGetProgID.Click += (sender,e)=>{ReloadProgIDs();};
        spl.Panel1.Controls.Add(btnGetProgID);

        txtFilter = new TextBox();
        txtFilter.Location = new Point(btnGetProgID.Right+10, 0);
        txtFilter.Size = new System.Drawing.Size(90,25);
        txtFilter.Text = "";
        txtFilter.TextChanged += (sender,e)=>{UpdateLsvProgID();}; // 入力中も変化するので鬱陶しいが妥協する。
        spl.Panel1.Controls.Add(txtFilter);
        new ToolTip().SetToolTip(txtFilter, "Filter text for <ProgID>, <Value of CLSID>\r\nTo show all, clear the TextBox content.");

        lsvProgID = new ListView();
        lsvProgID.Location = new Point(0, 30);
        lsvProgID.View = View.Details;
        lsvProgID.FullRowSelect =true;
        lsvProgID.GridLines = true;
        lsvProgID.Columns.Add("ProgID", 210, HorizontalAlignment.Left);
        lsvProgID.Columns.Add("CLSID", 210, HorizontalAlignment.Left);
        lsvProgID.Columns.Add("Value of CLSID", 210, HorizontalAlignment.Left);
        lsvProgID.SelectedIndexChanged += LsvProgID_SelectedIndexChanged;
        lsvProgID.ColumnClick += LsvProgID_ColumnClick;
        lsvProgID.ListViewItemSorter = new ListViewItemComparer(0); // index 0 のcolumnでソート設定しておく
        spl.Panel1.Controls.Add(lsvProgID);
        ////// 左 Panel ここまで


        ////// 右 Panel ここから
        txtProgID = new TextBox();
        txtProgID.Location = new Point(0, 0);
        txtProgID.Width = 250;
        txtProgID.Text = "InternetExplorer.Application";
        spl.Panel2.Controls.Add(txtProgID);
        new ToolTip().SetToolTip(txtProgID, "ProgID");

        btnGetMembers = new Button();
        btnGetMembers.Location = new Point(txtProgID.Right+10, 0);
        btnGetMembers.Size = new System.Drawing.Size(100,25);
        btnGetMembers.Text = "Get Members";
        btnGetMembers.Click += (sender,e)=>{ReloadLsvMemb(txtProgID.Text);};
        spl.Panel2.Controls.Add(btnGetMembers);
        new ToolTip().SetToolTip(btnGetMembers, "Get Members from ProgID in the TextBox");

        lsvMemb = new ListView();
        lsvMemb.Location = new Point(0, 30);
        lsvMemb.View = View.Details;
        lsvMemb.FullRowSelect =true;
        lsvMemb.GridLines = true;
        lsvMemb.Columns.Add("MemberType", 70, HorizontalAlignment.Left);
        lsvMemb.Columns.Add("ReturnType", 100, HorizontalAlignment.Left);
        lsvMemb.Columns.Add("MemberName", 130, HorizontalAlignment.Left);
        lsvMemb.Columns.Add("Definition", 350, HorizontalAlignment.Left);
        spl.Panel2.Controls.Add(lsvMemb);

        {
            var a = new ContextMenuStrip();
            a.Items.Add(new ToolStripMenuItem("Copy ReturnType", null, GetSelectedItems, "CopyReturnType"));
            a.Items.Add(new ToolStripMenuItem("Copy MemberName", null, GetSelectedItems, "CopyMemberName"));
            a.Items.Add(new ToolStripMenuItem("Copy Definition", null, GetSelectedItems, "CopyDefinition"));

            a.Opening += LsvMemb_ContextMenuStrip_Opening;
            lsvMemb.ContextMenuStrip = a;
        }
        ////// 右 Panel ここまで

        ClientSize = new System.Drawing.Size(800,530);

        Load      += Form_Resize;
        Resize    += Form_Resize;
        ResizeEnd += Form_Resize;
        spl.SplitterMoving += Form_Resize;
        spl.SplitterMoved  += Form_Resize;
    }

    void ClipboardSetText(string s)
    {
        if ( s==null || s=="" ){return;}

        try {
            Clipboard.SetText(s);
        }
        catch ( ExternalException ) { // クリアに失敗
            // 1回だけリトライする
            try {
                Clipboard.SetText(s);
            }
            catch ( ExternalException e2 ) {
                Console.WriteLine(e2);
            }
        }
    }


    void LsvMemb_ContextMenuStrip_Opening(object sender, CancelEventArgs e)
    {
        // https://qiita.com/Toraja/items/51ffc5cbfa0b9f8e154f
        Point p = lsvMemb.PointToClient(Cursor.Position);
        ListViewItem item = lsvMemb.HitTest(p).Item;

        contextMenuInfo = null;

        if (item == null) {
            e.Cancel = true;
        }
        else if ( item.Bounds.Contains(p) ) {
            contextMenuInfo = new MyListViewContextMenuInfo(lsvMemb, item);
        }
        else {
            e.Cancel = true;
        }
    }

    // ToolStripMenuItem.Click イベント
    void GetSelectedItems(object sender, EventArgs e)
    {
        var mi = (ToolStripMenuItem)sender;

        if ( contextMenuInfo != null ) {
            var memb = (ComObjectMember)contextMenuInfo.SelectedItem.Tag;

            string text = "";
            if ( mi.Name == "CopyDefinition" ) {
                text = memb.DefinitionText;
            }
            else if ( mi.Name == "CopyReturnType" ) {
                text = memb.ReturnTypeText;
            }
            else if ( mi.Name == "CopyMemberName" ) {
                text = memb.MemberName;
            }
            ClipboardSetText(text);
        }
    }

    void Form_Resize(object sender, EventArgs e)
    {
        lsvProgID.Size = new System.Drawing.Size(spl.Panel1.ClientSize.Width, spl.Panel1.ClientSize.Height - lsvProgID.Top);
        lsvMemb.Size   = new System.Drawing.Size(spl.Panel2.ClientSize.Width, spl.Panel2.ClientSize.Height - lsvMemb.Top);
    }

    // 再取得処理
    void ReloadProgIDs()
    {
        btnGetProgID.Enabled = false; // 処理に時間がかかるので操作を禁止しておく。※結局押したら意味ないかも
        try {
            allProgIDs = ProgIdInfo.GetProgIDs();
            UpdateLsvProgID();
        }
        finally {
            btnGetProgID.Enabled = true;
        }
    }

    // 再取得 or フィルタ変更時の lsvProgID 更新
    void UpdateLsvProgID()
    {
        lsvProgID.Items.Clear();
        lsvProgID.BeginUpdate();
        try {
            foreach ( var t in allProgIDs ) {
                string filterText = txtFilter.Text.Trim().ToLowerInvariant();

                if ( filterText == "" ||
                     t.ProgID.ToLowerInvariant().Contains(filterText) ||
                     t.ValueOfCLSID.ToLowerInvariant().Contains(filterText) ) {
                    lsvProgID.Items.Add(MyNewListItem_LsvProgID(t));
                }
            }
        }
        finally {
            lsvProgID.EndUpdate();
        }
    }

    void ReloadLsvMemb(string comName)
    {
        List<ComObjectMember> comObjMembers;

        btnGetMembers.Enabled = false;
        try {
            comObjMembers = ComObjectMember.GetMembers(comName);

            lsvMemb.Items.Clear();
            lsvMemb.BeginUpdate();
            try {
                foreach ( var t in comObjMembers ) {
                    lsvMemb.Items.Add(MyNewListItem_LsvMemb(t));
                }
            }
            finally {
                lsvMemb.EndUpdate();
            }
        }
        finally {
            btnGetMembers.Enabled = true;
        }
    }

    ListViewItem MyNewListItem_LsvProgID(ProgIdInfo progId)
    {
        var itm = new ListViewItem(new string[]{progId.ProgID, progId.CLSID, progId.ValueOfCLSID});
        itm.Tag = progId;
        return itm;
    }


    ListViewItem MyNewListItem_LsvMemb(ComObjectMember memb)
    {
        var itm = new ListViewItem(new string[]{memb.MemberTypeText, memb.ReturnTypeText, memb.MemberName, memb.DefinitionText});
        itm.Tag = memb;
        return itm;
    }


    void LsvProgID_SelectedIndexChanged(object sender, EventArgs e)
    {
        if (lsvProgID.SelectedIndices.Count != 1) {return;}
        int index = lsvProgID.SelectedIndices[0];
        ProgIdInfo progIdInfo = (ProgIdInfo)(lsvProgID.Items[index].Tag);
        txtProgID.Text = progIdInfo.ProgID;
    }

    void LsvProgID_ColumnClick(object sender, ColumnClickEventArgs e)
    {
        if ( e.Column >= 0 && e.Column < lsvProgID.Columns.Count ) {
            lsvProgID.ListViewItemSorter = new ListViewItemComparer(e.Column);
        }
    }

    public class ListViewItemComparer : IComparer
    {
        private int _column;
        public ListViewItemComparer(int col) { _column = col; }

        public int Compare(object obj1, object obj2)
        {
            string s1 = ((ListViewItem)obj1).SubItems[_column].Text;
            string s2 = ((ListViewItem)obj2).SubItems[_column].Text;
            return string.Compare(s1, s2, true); // 第3引数の true は、大文字小文字の差異を無視する指定(ignore case)
        }
    }


    class MyListViewContextMenuInfo
    {
        private ListViewItem _SelectedItem;
        private ListView     _Sender;

        public ListViewItem SelectedItem{get{return _SelectedItem;}}
        public ListView Sender{get{return _Sender;}}

        public MyListViewContextMenuInfo(ListView sender, ListViewItem selectedItem)
        {
            _Sender = sender;
            _SelectedItem = selectedItem;
        }
    }


    [STAThread]
    static void Main(){
        Application.Run(new MainForm());
    }
}

機能追加

・テキストコピーできるようにする
→ 済み(右側のみ) 右クリックで選択項目コピー可能
・ListView上の検索(filtering)
→ 済み

コンパイル方法

csc ^
/r:C:\Windows\assembly\GAC_MSIL\System.Management.Automation\1.0.0.0__31bf3856ad364e35\System.Management.Automation.dll ^
xxxx.cs

参考サイト

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

Unity:UI Elements でマウスのクリックイベントを取得する

はじめに

こんにちは、のんびりエンジニアのたっつーです。
ブログを運営しているのでよろしければ見てください。

今回は「いくつかサンプル書いたよ」の記事の1つ「 画像をパラパラ漫画風に動かす 」を解説したいと思います。
ぜひ、お付き合いいただければと思います(#^^#)

UI Elementsとは?

こちらの記事をご参照いただければと思います。

マウスイベントの取得方法

通常ではマウスイベントは、Button.Clicked とかのボタンがデフォルト定義してくれているイベントを使いますが、
今回ご紹介するのはもっと汎用的な低レベルな感じの実装になりますので様々なコントロールでマウスの移動・クリックなどのイベントが実装できます。

それでは、マウスイベントの実装をしてみたいと思います。

Editor フォルダを作成し、右クリックのメニューから「UIElements Editor Window」を選択してください。

image-38.png

「Sample3」を入力してください。

image-44.png

次に、Sample3.cs を以下のように変更してください。

Sample2.cs
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;


public class Sample3 : EditorWindow
{
    [MenuItem("UIElementsSamples/Sample3")]
    public static void ShowExample()
    {
        Sample3 wnd = GetWindow<Sample3>();
        wnd.titleContent = new GUIContent("Sample3");
    }

    public void OnEnable()
    {
        VisualElement root = rootVisualElement;

        root.AddManipulator(new MouseEventLogger());
        root.Add(new Label() { style = { backgroundColor = Color.yellow }, text = "output console log!" });
    }

    class MouseEventLogger : Manipulator
    {
        protected override void RegisterCallbacksOnTarget()
        {
            target.RegisterCallback<MouseUpEvent>(OnMouseUpEvent);
            target.RegisterCallback<MouseDownEvent>(OnMouseDownEvent);
        }

        protected override void UnregisterCallbacksFromTarget()
        {
            target.UnregisterCallback<MouseUpEvent>(OnMouseUpEvent);
            target.UnregisterCallback<MouseDownEvent>(OnMouseDownEvent);
        }

        void OnMouseUpEvent(MouseEventBase<MouseUpEvent> evt)
        {
            Debug.Log("Mouse Up " + evt + " in " + evt.propagationPhase + " for target " + evt.target);
        }

        void OnMouseDownEvent(MouseEventBase<MouseDownEvent> evt)
        {
            Debug.Log("Mouse Down " + evt + " in " + evt.propagationPhase + " for target " + evt.target);
        }
    }
}

動作結果

どうでしたか簡単に実装が行えたと思います。
Unityのメニューから、「UIElementsSamples -> Sample3」を選択して実行を確認してみてください。

image-45.png

sample3.gif

終わりに

よければ ブログ「Unity+AssetStoreおすすめ情報」の方にも色々記載しているのでぜひご参照いただければと思います。

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

Unity:UI Elements でツールバーを実装する

はじめに

こんにちは、のんびりエンジニアのたっつーです。
ブログを運営しているのでよろしければ見てください。

今回は「いくつかサンプル書いたよ」の記事の1つの共通コントロールの作成方法と使い方を解説したいと思います。
ぜひ、お付き合いいただければと思います(#^^#)

UI Elementsとは?

こちらの記事をご参照いただければと思います。

ツールバーの実装

さっそくですが、ツールバーの実装をしてみたいと思います。

Editor フォルダを作成し、右クリックのメニューから「UIElements Editor Window」を選択してください。
image-38.png

「Sample2」を入力してください。
image-42.png

以下の3ファイルが生成されました、次にこの3ファイルを編集したいとおもいます。
image-43.png

Sample2.uss は使わないので削除してください!

次に、Sample2.uxml を以下のように変更してください。

Sample2.uxml
<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
    xsi:schemaLocation="UnityEngine.UIElements ../../UIElementsSchema/UnityEngine.UIElements.xsd">

  <engine:Label text="Sample2 UXML" />
</engine:UXML>

次に、Sample2.cs を以下のように変更してください。

Sample.cs
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;


public class Sample2 : EditorWindow
{
    [MenuItem("UIElementsSamples/Sample2")]
    public static void ShowExample()
    {
        Sample2 wnd = GetWindow<Sample2>();
        wnd.titleContent = new GUIContent("Sample2");
    }

    public void OnEnable()
    {
        VisualElement root = rootVisualElement;

        // Create Toolbar
        {
            var toolbar = new Toolbar();
            root.Add(toolbar);

            var btn1 = new ToolbarButton { text = "Button" };
            toolbar.Add(btn1);

            var spc = new ToolbarSpacer();
            toolbar.Add(spc);

            var tgl = new ToolbarToggle { text = "Toggle" };
            toolbar.Add(tgl);

            var spc2 = new ToolbarSpacer() { name = "flexSpacer1", flex = true };
            toolbar.Add(spc2);

            var menu = new ToolbarMenu { text = "Menu" };
            menu.menu.AppendAction("Default is never shown", a => { }, a => DropdownMenuAction.Status.None);
            menu.menu.AppendAction("Normal menu", a => { }, a => DropdownMenuAction.Status.Normal);
            menu.menu.AppendAction("Hidden is never shown", a => { }, a => DropdownMenuAction.Status.Hidden);
            menu.menu.AppendAction("Checked menu", a => { }, a => DropdownMenuAction.Status.Checked);
            menu.menu.AppendAction("Disabled menu", a => { }, a => DropdownMenuAction.Status.Disabled);
            menu.menu.AppendAction("Disabled and checked menu", a => { }, a => DropdownMenuAction.Status.Disabled | DropdownMenuAction.Status.Checked);
            toolbar.Add(menu);

            var spc3 = new ToolbarSpacer() { name = "flexSpacer2", flex = true };
            toolbar.Add(spc3);

            var popup = new ToolbarMenu { text = "Popup", variant = ToolbarMenu.Variant.Popup };
            popup.menu.AppendAction("Popup", a => { }, a => DropdownMenuAction.Status.Normal);
            toolbar.Add(popup);
        }

        // Import UXML
        {
            var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample2.uxml");
            // var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Sample2.uss");
            VisualElement labelFromUXML = visualTree.CloneTree();
            // labelFromUXML.styleSheets.Add(styleSheet);
            root.Add(labelFromUXML);
        }
    }
}

動作結果

どうでしたか簡単に実装が行えたと思います。
sample2.gif

終わりに

よければ ブログ「Unity+AssetStoreおすすめ情報」の方にも色々記載しているのでぜひご参照いただければと思います。

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

Unity:UI Elements でパラパラ漫画風のGUIを作ってみる。

はじめに

こんにちは、のんびりエンジニアのたっつーです。
ブログを運営しているのでよろしければ見てください。

今回は「いくつかサンプル書いたよ」の記事の1つの共通コントロールの作成方法と使い方を解説したいと思います。
ぜひ、お付き合いいただければと思います(#^^#)

UI Elementsとは?

こちらの記事をご参照いただければと思います。

パラパラ漫画風のGUIを作ってみよう

さっそくですが、ツールバーの実装をしてみたいと思います。

GUIの実装

Editor フォルダを作成し、右クリックのメニューから「UIElements Editor Window」を選択してください。
image-38.png

「Sample4」を入力してください。
image-46.png

次に、Sample4.cs を以下のように変更してください。
```C#:Sample4.cs
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

public class Sample4 : EditorWindow
{
[MenuItem("UIElementsSamples/Sample4")]
public static void ShowExample()
{
Sample4 wnd = GetWindow();
wnd.titleContent = new GUIContent("Sample4");
}

private Label label;
private Image image;
private SliderInt slider;

public void OnEnable()
{
    VisualElement root = rootVisualElement;

    // Import UXML
    var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample4.uxml");
    var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Sample4.uss");
    VisualElement labelFromUXML = visualTree.CloneTree();
    labelFromUXML.styleSheets.Add(styleSheet);
    root.Add(labelFromUXML);

    this.label = root.Q<Label>("label");
    this.image = root.Q<Image>("image");
    this.slider = root.Q<SliderInt>("slider");

    slider.RegisterValueChangedCallback<int>(Slider_ValueChanged);
}

private void Slider_ValueChanged(ChangeEvent<int> evt)
{
    this.label.text = $"page {evt.newValue}/{slider.highValue}";
    var backgroundTexture = Resources.Load<Texture2D>($"img_{evt.newValue}");
    this.image.style.backgroundImage = backgroundTexture;
}

}
```

次に、Sample4.uss を以下のように変更してください。

Sample4.uss
Label {
    font-size: 20px;
    -unity-font-style: bold;
    -unity-text-align: middle-center;
    color: rgb(68, 138, 255);
}

Image {
    padding-left: 20;
    padding-top: 20;
    height: 150;
    background-image: url("/Assets/Resources/img_0.png");
}

次に、Sample4.uxml を以下のように変更してください。

Sample4.uxml
<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
    xsi:schemaLocation="UnityEngine.UIElements ../../UIElementsSchema/UnityEngine.UIElements.xsd
                        UnityEditor.UIElements ../../UIElementsSchema/UnityEditor.UIElements.xsd
                        UnityEditor.PackageManager.UI ../../UIElementsSchema/UnityEditor.PackageManager.UI.xsd">

  <engine:Label text="start" name="label" />
  <engine:Image name="image" />
  <engine:SliderInt low-value="0" high-value="92" value="1" name="slider" />
</engine:UXML>

画像リソースの準備

次に、「Resources」フォルダを作成して、その下に 「img_0.png」 「img_1.png」 「img_2.png」・・・ 「img_92.png」までの画像を格納してください。
image-37.png

動作結果

どうでしたか簡単に実装が行えたと思います。
Unityのメニューから、「UIElementsSamples -> Sample4」を選択して実行を確認してみてください。
image-48.png
sample4.gif

終わりに

よければ ブログ「Unity+AssetStoreおすすめ情報」の方にも色々記載しているのでぜひご参照いただければと思います。

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

Unity:UI Elements で共通コントロールを作成する

はじめに

こんにちは、のんびりエンジニアのたっつーです。
ブログを運営しているのでよろしければ見てください。

今回は「いくつかサンプル書いたよ」の記事の1つの共通コントロールの作成方法と使い方を解説したいと思います。
ぜひ、お付き合いいただければと思います(#^^#)

UI Elementsとは?

こちらの記事をご参照いただければと思います。

共通コントロールの実装

さっそくですが、共通コントロールの実装をしてみたいと思います。

共通コントロール

Editor フォルダを作成し、右クリックのメニューから「UIElements Editor Window」を選択してください。
image-38.png

「RedButton」を入力してください。
 ※つけたいコントロールの名前に変更してしまって、OKです。
image-39.png

以下の3ファイルが生成されました、次にこの3ファイルを編集したいとおもいます。
image-40.png

まずは、RedButton.uxml は使わないので削除してください!
※使っても実装できますが今回はスクリプトですべて生成していきます。

次に、RedButton.uss を以下のように変更してください。

RedButton.uss
Button {
    font-size: 15px;
    -unity-font-style: bold;
    color: rgb(255, 0, 0);
}

次に、RedButton.cs を以下のように変更してください。

また、独自イベントとして clicked を定義しており、ボタンを押下イベントに合わせて clicked が呼ばれるようにしております。
また、MyControls のネームスペースに変更するのをお忘れずに!

RedButton.cs
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.UIElements;


namespace MyControls
{
    internal class RedButton : VisualElement
    {
        public new class UxmlFactory : UxmlFactory<RedButton, UxmlTraits> { }

        public new class UxmlTraits : VisualElement.UxmlTraits
        {
            UxmlStringAttributeDescription m_Text =
                new UxmlStringAttributeDescription { name = "text", defaultValue = "none label" };

            public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
            {
                get { yield break; }
            }

            public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
            {
                base.Init(ve, bag, cc);
                var text = m_Text.GetValueFromBag(bag, cc);
                ((RedButton)ve).Init(text);
            }
        }

        private Button m_button;

        public string text
        {
            get { return m_button.text; }
            set { m_button.text = value; }
        }

        public event Action<EventBase> clicked;

        public RedButton()
        {
            var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/RedButton.uss");
            styleSheets.Add(styleSheet);

            m_button = new Button();
            m_button.text = "noen label";
            m_button.clickable.clickedWithEventInfo += Button_Clicked;
            hierarchy.Add(m_button);
        }

        public RedButton(string text) : this()
        {
            Init(text);
        }

        public void Init(string text)
        {
            this.text = text;
        }

        private void Button_Clicked(EventBase eb)
        {
            eb.target = this;
            clicked?.Invoke(eb);
        }
    }
}

呼び出し側

次に呼び出し側の実装になります。
先ほどと同じよう入力ですが今回は「Sample1」で追加してください。

image-41.png

同じように3ファイルとも以下のように編集してください。

共通コントロールとして、以下の赤い文字のボタン(RedButton)を用意しました、これを使いまわせるパーツとして、ボタン1~3の計3つを作成しています。

ポイントとしては、「xmlns:my=”MyControls”」 で my:RedButton が使えるようになります。

Sample1.uxml
<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xmlns:my="MyControls"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
    xsi:schemaLocation="UnityEngine.UIElements ../../UIElementsSchema/UnityEngine.UIElements.xsd">

  <engine:Label text="Sample1 UXML" />

  <my:RedButton text="button1" name="btn1" />
  <my:RedButton text="button2" name="btn2" />
  <my:RedButton text="button3" name="btn3" />

  <engine:Label text="Sample1 UXML end" />
</engine:UXML>
Sample1.uss
Label {
    font-size: 20px;
    -unity-font-style: bold;
    color: rgb(68, 138, 255);
}

ポイントとしては、各ボタンイベント(Clicked)にイベントを紐づけるために「root.Q(“btn1”).clicked += ButtonClicked;」で実行しています。

Sample1.cs
using MyControls;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class Sample1 : EditorWindow
{
    [MenuItem("UIElementsSamples/Sample1")]
    public static void ShowExample()
    {
        Sample1 wnd = GetWindow<Sample1>();
        wnd.titleContent = new GUIContent("Sample1");
    }

    public void OnEnable()
    {
        VisualElement root = rootVisualElement;

        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample1.uxml");
        VisualElement labelFromUXML = visualTree.CloneTree();
        root.Add(labelFromUXML);

        root.Q<RedButton>("btn1").clicked += ButtonClicked;
        root.Q<RedButton>("btn2").clicked += ButtonClicked;
        root.Q<RedButton>("btn3").clicked += ButtonClicked;
    }

    void ButtonClicked(EventBase eventBase)
    {
        var redButton = (RedButton)eventBase.target;
        Debug.Log($"Clicked {redButton.text} RedButton!! ");
    }
}

動作結果

どうでしたか簡単に実装が行えたと思います。
今回は、RedButton.uxml は使わない実装でしたが使って実装する場合もそんなに難しくないと思いますのでぜひチャレンジしてみてください!

sample1.gif

終わりに

よければ ブログ「Unity+AssetStoreおすすめ情報」の方にも色々記載しているのでぜひご参照いただければと思います。

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

C#で汎用的なアプリケーション設定の編集画面を作成する

概要

C#のアプリケーション設定は便利ですよね。
取得・変更・保存が簡単にできます。
ところが、編集画面やインポート/エクスポートを作ろうとしたら結構ハマってしまいました。
解決策がなかなか見つからなかったので、書き残しておきます。

サンプルコード

以下に実際に動作するコードを置いてます。
https://github.com/minoru-nagasawa/SampleApplicationSettingsDialog

以下のような画面でアプリケーション設定を変更できます。
image.png

PropertyGridにオブジェクトのコピーを設定

PropertyGridにアプリケーション設定の実体(Settings.Default)をセットすると、保存しなくても実体が変更されてしまいます。
それを防ぐため、コピーを作成して設定します。

ただし、StringCollection型は注意が必要です。
これをコピーしただけでは、その中の個々のstringは同じ実体を参照してしまいます。
それにより、コピーしたテキストを変更したつもりが、本物の設定が変更されてしまいます。
それを防ぐためにディープコピーしたオブジェクトを設定します。

SettingsEditForm.cs
        /// <summary>
        /// オブジェクトのディープコピーを作成する
        /// </summary>
        private static T deepCopy<T>(T src)
        {
            using (var memoryStream = new System.IO.MemoryStream())
            {
                var binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
                binaryFormatter.Serialize(memoryStream, src);
                memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
                return (T)binaryFormatter.Deserialize(memoryStream);
            }
        }

        /// <summary>
        /// 設定のコピーを保管する
        /// </summary>
        private Settings copiedSettings;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SettingsEditForm()
        {
            InitializeComponent();

            // StringCollectionの初期の編集画面では追加ができない。
            // これを実行することで編集画面が変わり、追加できるようになる。
            TypeDescriptor.AddAttributes(typeof(System.Collections.Specialized.StringCollection),
                                         new EditorAttribute("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
                                         typeof(System.Drawing.Design.UITypeEditor)));

            // 設定のコピーを作成する
            copiedSettings = new Settings();
            foreach (SettingsProperty property in Settings.Default.Properties)
            {
                // StringCollection型のために必要
                copiedSettings[property.Name] = deepCopy(Settings.Default[property.Name]);
            }

            // コピーしたオブジェクトを表示させる
            propertyGrid1.SelectedObject = copiedSettings;
        }

保存ボタンを押したときにデータを保存する

以下のように、Settings.Defaultに変更を反映してSvae()すれば保存できます。
ただし、この時もStringCollection型の問題があるため、ディープコピーしたオブジェクトを設定します。

SettingsEditForm.cs
        private void buttonSave_Click(object sender, EventArgs e)
        {
            foreach (SettingsProperty property in Settings.Default.Properties)
            {
                // StringCollection型のために必要
                Settings.Default[property.Name] = deepCopy(copiedSettings[property.Name]);
            }
            Settings.Default.Save();
            MessageBox.Show("保存しました");
        }

CategoryとHelpのテキストを変更する

PropertyGridにはプロパティのCategoryとHelpを表示する機能があります。
image.png

CategoryにはプロパティのCategory属性、HelpにはDescription属性が表示されます。

        [System.ComponentModel.Category("ここがCategoryに表示される")]
        [System.ComponentModel.Description("ここがHelpに表示される")]
        public bool BoolSetting {
            get {
                return ((bool)(this["BoolSetting"]));
            }
            set {
                this["BoolSetting"] = value;
            }
        }

ですが、Settingsのプロパティが書かれているコードは自動生成のため、この方法では指定できません。
その対処として、以下のようにSettingsのコンストラクタで動的にCategoryAttributeとDescriptionAttributeを追加します。

Settings.cs
        public Settings()
        {
            // PropertyGridのCategoryを設定する
            var categoryTable = new Dictionary<string, Attribute>()
            {
                { nameof(Settings.BoolSetting),             new CategoryAttribute("組み込みのデータ型") },
                { nameof(Settings.StringSetting),           new CategoryAttribute("組み込みのデータ型") },
                { nameof(Settings.StringCollectionSetting), new CategoryAttribute("複合データ型") },
                { nameof(Settings.DateTimeSetting),         new CategoryAttribute("複合データ型") },
                { nameof(Settings.IntSetting),              new CategoryAttribute("組み込みのデータ型") },
            };
            addAttribute(categoryTable);

            // PropertyGridのHelpテキストを設定する
            var descriptionTable = new Dictionary<string, Attribute>()
            {
                { nameof(Settings.BoolSetting),             new DescriptionAttribute("bool型の設定") },
                { nameof(Settings.StringSetting),           new DescriptionAttribute("string型の設定") },
                { nameof(Settings.StringCollectionSetting), new DescriptionAttribute("複数のstring型の設定") },
                { nameof(Settings.DateTimeSetting),         new DescriptionAttribute("DateTime型の設定") },
                { nameof(Settings.IntSetting),              new DescriptionAttribute("int型の設定") },
            };
            addAttribute(descriptionTable);
        }

        /// <summary>
        /// プロパティに属性を追加する
        /// </summary>
        /// <param name="attributeTable">Key:プロパティ名、Value:追加する属性</param>
        private void addAttribute(Dictionary<string, Attribute> attributeTable)
        {
            if (attributeTable == null)
            {
                return;
            }

            var properties = TypeDescriptor.GetProperties(this);
            foreach (PropertyDescriptor p in properties)
            {
                Attribute attribute;
                if (attributeTable.TryGetValue(p.Name, out attribute))
                {
                    // 属性にDescriptionAttributeを追加する。
                    // 本当はMemberDescriptor.Attributes.Addのようにしたいのだが、Attributes属性はgetだけ定義されている。
                    // そのためリフレクションを使って属性を追加する
                    var fi = p.Attributes.GetType().GetField("_attributes", BindingFlags.NonPublic | BindingFlags.Instance);
                    var attrs = fi.GetValue(p.Attributes) as Attribute[];
                    var listAttr = new List<Attribute>();
                    if (attrs != null)
                    {
                        listAttr.AddRange(attrs);
                    }
                    listAttr.Add(attribute);
                    fi.SetValue(p.Attributes, listAttr.ToArray());
                }
            }
        }

StringCollection型のプロパティを変更できるようにする

StringCollection型のプロパティを変更しようとした場合、以下のダイアログが表示されます。
ですが、このダイアログでは「追加」を押すとエラーになってしまいます。
image.png

その対処として以下を実行します。

SettingsEditForm.cs
            // これを追加しないと、StringCollectionの追加ができない
            TypeDescriptor.AddAttributes(typeof(System.Collections.Specialized.StringCollection),
                                         new EditorAttribute("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
                                         typeof(System.Drawing.Design.UITypeEditor)));

これにより表示されるダイアログも以下のように変わります。
image.png

エクスポート機能を追加する

設定のエクスポートがあった方がいいので作ります。

まず、ユーザスコープの設定ファイルのパスを取得できるようにするため、
[プロパティを右クリック] → [追加] → [参照]としてダイアログを開きます。
image.png

その後、System.Configurationを選択してOKします。
image.png

そして、ユーザスコープの設定ファイルを、SaveFileDialogで選んだパスにコピーすれば完了です。
ただし、保存を1度も実行してない場合はファイルが存在しないため、その場合は保存を実行します。

SettingsEditForm.cs
        private void buttonExport_Click(object sender, EventArgs e)
        {
            // ファイルを選択
            string fullPath;
            using (var sfd = new SaveFileDialog())
            {
                sfd.FileName = "user.config";
                sfd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
                sfd.Filter = "設定ファイル(*.config)|*.config";
                sfd.FilterIndex = 1;
                sfd.Title = "エクスポート先のファイルを選択してください";
                sfd.RestoreDirectory = true;
                if (sfd.ShowDialog() != DialogResult.OK)
                {
                    return;
                }

                fullPath = sfd.FileName;
            }

            // ファイルをコピー
            try
            {
                // user.configのパスを取得
                string userConfigPath = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;

                // ファイルが無ければSave()して生成する
                if (!File.Exists(userConfigPath))
                {
                    Settings.Default.Save();
                }

                // エクスポートはファイルをコピーするだけ
                File.Copy(userConfigPath, fullPath, true);
                MessageBox.Show("エクスポートしました");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "エクスポート失敗", MessageBoxButtons.OK);
            }
        }

インポート機能を追加する

エクスポートを作ったのでインポートも作ります。
インポートは、設定を読み込んで反映させるだけですが、なかなか癖があります。

まず、設定の読み込みは以下のコードになります。
読み込みはConfigurationManager.OpenMappedExeConfigurationで行います。
その引数としてファイルパスを指定するのですが、アプリの設定+現在の設定+読み込んだ設定、の3つを読み込ますと自然な動作になります。
読み込んだ後は、GetSectionでアプリケーション設定が取得できます。

SettingsEditForm.cs
            ClientSettingsSection section = null;
            try
            {
                // ExeConfigFilenameにインポートするファイルだけ指定しても、そのファイルにはセクション情報が書かれていないためGetSectionで正しく読めない。
                // さらに、ExeConfigFilenameにアプリケーション設定、RoamingUserConfigFilenameにインポートするファイルを指定しても、正しく動かない場合がある。
                // 例えばインポートするファイルに吐かれていない新規設定がある場合、本来は現在値を保持してほしいが、デフォルト値で上書きしてしまう。
                // ということで、ExeConfigFilename/RoamingUserConfigFilenam/LocalUserConfigFilenameの3つを指定して読み込む。
                var tmpAppConfig  = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
                var tmpUserCOnfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
                var exeFileMap = new ExeConfigurationFileMap
                {
                    ExeConfigFilename         = tmpAppConfig.FilePath,
                    RoamingUserConfigFilename = tmpUserCOnfig.FilePath,
                    LocalUserConfigFilename   = fullPath
                };
                var config = ConfigurationManager.OpenMappedExeConfiguration(exeFileMap, ConfigurationUserLevel.PerUserRoamingAndLocal);
                section = (ClientSettingsSection)config.GetSection($"userSettings/{typeof(Settings).FullName}");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "インポート失敗", MessageBoxButtons.OK);
                return;
            }

続いて、データの更新は以下のコードになります。
読み込んだ設定の値はSettingElementのValue.ValueXml.InnerXmlに入ってます。
それを、画面に表示している変数(SettingsPropertyValue型)のSerializedValueに設定すればいいです。

ただし、SettingsPropertyValueを一度も参照していないと、値を更新しても元の値に戻ってしまいます。
そのため、_ChangedSinceLastSerializedを無理やりfalseに変更しています。
以下の実装を見るとわかると思います。
https://referencesource.microsoft.com/#System/sys/system/configuration/SettingsPropertyValue.cs,69

さらに、Deserializedにfalseを設定しておきます。
これにより、PropertyValueにアクセスしたときにDeserializeされます。
https://referencesource.microsoft.com/#System/sys/system/configuration/SettingsPropertyValue.cs,40

SettingsEditForm.cs
            try
            {
                // Key:プロパティ名、Value:読み込んだファイルの該当プロパティのSettingElement、のDictionaryを作成する
                var dict = new Dictionary<string, SettingElement>();
                foreach (SettingElement v in section.Settings)
                {
                    dict.Add(v.Name, v);
                }

                // 現在の設定を更新する
                foreach (SettingsPropertyValue value in copiedSettings.PropertyValues)
                {
                    SettingElement element;
                    if (dict.TryGetValue(value.Name, out element))
                    {
                        // SerializedValueを1度も参照していないと、参照したときの元の値に戻ってしまうという仕様になっている。
                        // https://referencesource.microsoft.com/#System/sys/system/configuration/SettingsPropertyValue.cs,69
                        // その対策として、リフレクションで無理やり内部のメンバをfalseに変更する。
                        // リフレクションを使わなくても、var dummy = value.SerializedValueを実行して1度参照する方法でもよい。
                        var _ChangedSinceLastSerialized = typeof(SettingsPropertyValue).GetField("_ChangedSinceLastSerialized", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Instance);
                        _ChangedSinceLastSerialized.SetValue(value, false);

                        // 値の設定
                        value.SerializedValue = element.Value.ValueXml.InnerXml;

                        // value.Deserializedをfalseにすると、value.PropertyValueにアクセスしたときにDeserializeされる
                        value.Deserialized = false;
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "インポート失敗", MessageBoxButtons.OK);
                return;
            }

これらをつなげて、結局インポートは以下のコードとなります。

SettingsEditForm.cs
        private void buttonImport_Click(object sender, EventArgs e)
        {
            // ファイル選択
            string fullPath = "";
            using (var ofd = new OpenFileDialog())
            {
                ofd.FileName = "user.config";
                ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
                ofd.Filter = "設定ファイル(*.config)|*.config";
                ofd.FilterIndex = 1;
                ofd.Title = "インポートするファイルを選択してください";
                ofd.RestoreDirectory = true;
                if (ofd.ShowDialog() != DialogResult.OK)
                {
                    return;
                }

                fullPath = ofd.FileName;
            }

            // 読み込み
            ClientSettingsSection section = null;
            try
            {
                // ExeConfigFilenameにインポートするファイルだけ指定しても、そのファイルにはセクション情報が書かれていないためGetSectionで正しく読めない。
                // さらに、ExeConfigFilenameにアプリケーション設定、RoamingUserConfigFilenameにインポートするファイルを指定しても、正しく動かない場合がある。
                // 例えばインポートするファイルに吐かれていない新規設定がある場合、本来は現在値を保持してほしいが、デフォルト値で上書きしてしまう。
                // ということで、ExeConfigFilename/RoamingUserConfigFilenam/LocalUserConfigFilenameの3つを指定して読み込む。
                var tmpAppConfig  = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
                var tmpUserCOnfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
                var exeFileMap = new ExeConfigurationFileMap
                {
                    ExeConfigFilename         = tmpAppConfig.FilePath,
                    RoamingUserConfigFilename = tmpUserCOnfig.FilePath,
                    LocalUserConfigFilename   = fullPath
                };
                var config = ConfigurationManager.OpenMappedExeConfiguration(exeFileMap, ConfigurationUserLevel.PerUserRoamingAndLocal);
                section = (ClientSettingsSection)config.GetSection($"userSettings/{typeof(Settings).FullName}");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "インポート失敗", MessageBoxButtons.OK);
                return;
            }

            // データの更新
            try
            {
                // Key:プロパティ名、Value:読み込んだファイルの該当プロパティのSettingElement、のDictionaryを作成する
                var dict = new Dictionary<string, SettingElement>();
                foreach (SettingElement v in section.Settings)
                {
                    dict.Add(v.Name, v);
                }

                // 現在の設定を更新する
                foreach (SettingsPropertyValue value in copiedSettings.PropertyValues)
                {
                    SettingElement element;
                    if (dict.TryGetValue(value.Name, out element))
                    {
                        // SerializedValueを1度も参照していないと、参照したときの元の値に戻ってしまうという仕様になっている。
                        // https://referencesource.microsoft.com/#System/sys/system/configuration/SettingsPropertyValue.cs,69
                        // その対策として、リフレクションで無理やり内部のメンバをfalseに変更する。
                        // リフレクションを使わなくても、var dummy = value.SerializedValueを実行して1度参照する方法でもよい。
                        var _ChangedSinceLastSerialized = typeof(SettingsPropertyValue).GetField("_ChangedSinceLastSerialized", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Instance);
                        _ChangedSinceLastSerialized.SetValue(value, false);

                        // 値の設定
                        value.SerializedValue = element.Value.ValueXml.InnerXml;

                        // value.Deserializedをfalseにすると、value.PropertyValueにアクセスしたときにDeserializeされる.
                        // https://referencesource.microsoft.com/#System/sys/system/configuration/SettingsPropertyValue.cs,40
                        value.Deserialized = false;
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "インポート失敗", MessageBoxButtons.OK);
                return;
            }

            // 画面を更新
            propertyGrid1.SelectedObject = copiedSettings;

            // メッセージ
            MessageBox.Show("インポートした設定を反映するには保存を押してください");
        }

最後に

これで汎用的なアプリケーション設定の編集画面ができました。
今まで直接XMLを変更させてたアプリに組み込んでみてください。

参考

Helpのテキストを変更する方法は、以下を参考にしました。
https://www.codeproject.com/Articles/415070/Dynamic-Type-Description-Framework-for-PropertyGri

StringCollection型のプロパティを変更できるようにする方法は、以下を参考にしました
https://stackoverflow.com/questions/2043579/adding-editor-editorattribute-at-run-time-dynamically-to-an-objects-propert

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

【C#】3分でわかる!型を簡単に変換する拡張メソッドのつくりかた

(Qiita初めてそれなりに経つんですけど。)
(そういえばWPFやElectronばかりでC#単体の記事って書いたことないなって思ったので)
(今日から雑に小ネタ挟んでいきたいと思います。雑に。)

今日は拡張メソッドについて紹介したいと思います!

型変換するときどれ使う?

前職のあるプログラムで 文字列型 → 数値型 の変換が散見されていたのですが、
皆さんC#で 文字列型 → 数値型 を行う場合って、どの方法が思いつきますかね?

こんなんだったり、

こんなん
public int ToInt(string value) => Convert.ToInt32(value);

あんなんだったり、

あんなん
public int ToInt(string value) => int.Parse(value);

そんなんだったり、

そんなん
public int ToInt(string value) => (int)value;

C#だといろいろな変換方法があって嬉しいですね!すごい!さすがC#!
ただ、これだとコーダーによって同じプログラムの中で多数の変換方法が混在してしまいます。。。

そこで、やむをえず変換を繰り返すことが想定されているなら、拡張メソッドを使ってみよう!
というのが今回の本題です。
表題に3分と書いてしまったのでサクサクいきます。

拡張メソッドとは?

拡張メソッド (C# プログラミング ガイド) | Microsoft Docsより

拡張メソッドを使用すると、新規の派生型の作成、再コンパイル、または元の型の変更を行うことなく既存の型にメソッドを "追加" できます。
拡張メソッドは特別な種類の静的メソッドですが、拡張された型のインスタンス メソッドのように呼び出します。

はい、つまり ToString() のように、「既存の型にメソッドを追加して利用できるもの」ですね。

具体例(文字列型 → 数値型)

ではさっそく拡張メソッドを使用した例を見ていきましょう。

拡張クラス
// 静的クラス・静的メソッドで定義することに注意。
public static partial class StringExtend
{
    // 引数には this + 拡張対象の型 + 変数名 を定義する。
    public static int ToInt32(this string value) => Convert.ToInt32(value);
}
実装例
// ex) 文字列型"123"が入力されると、数値型123が返ってくる。
public int ToInt(string value) => value.ToInt32(); 

だいぶさっぱりしましたね!

具体例(列挙体 → 数値型)

今度は少し発展した例を紹介します。以下の例を見てみましょう。
これはインデックスが付与されたデータベースタイプ列挙体「Engine」です。

Engine.cs
public enum Engine
{
    SQLServer = 0,
    MySQL = 1,
    PostgreSQL = 2,
    MariaDB = 3,
    SQLite = 4,
    OracleDatabase = 5
}

この列挙体を使用する際に、数値に変換したくなる時ってありますよね。
インデックスついてるんだもん。
単純に数値に変換しようとすると以下のようになります。

たんじゅんなへんかん
public int ToInt(Engine engine) => (int)engine; 

この変換を、拡張メソッドを使用して記述してみます。

拡張クラス
public static partial class EngineExtend
{
    public static int ToIndex(this Engine engine) => (int)engine;
}
拡張メソッドを使用した変換
// ex) 列挙体の要素 Engine.MySQL が入力されると、対応する数値 1 が返ってくる。
public int ToInt(Engine engine) => engine.ToIndex();

だいぶさっぱりしましたね!(二回目)

具体例(文字列型 or 数値型 → 列挙体)

拡張メソッドを使えば、逆方向の変換もお手軽に実装可能です。

拡張クラス
public static partial class EngineExtend
{
    // ex) 数値 1 が入力されると、列挙体の要素 Engine.MySQL が返ってくる。
    public static Engine ToEngine(this int index) => (Engine)Enum.ToObject(typeof(Engine), index);

    // ex) 文字列 "MySQL" が入力されると、列挙体の要素 Engine.MySQL が返ってくる。
    public static Engine ToEngine(this string value) => (Engine)Enum.Parse(typeof(Engine), value, true);
}

まとめ

いかがでしょうか?

だいぶ駆け足になりましたが、拡張メソッドを使用すると頻出する処理をお手軽に共通化することが可能になります。今回は型変換に絞って紹介しましたが、メソッドなので当然他の処理を挟むことも可能です。カプセル化の概念にも通じるので、コード全体の可読性だけでなく保守性も向上させることができますね!

ぜひご活用ください~。

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