妄想プログラマのらくがき帳 : 5月 2015

2015年5月21日木曜日

[C#][Roslyn]SyntaxWalkerでメソッドコメントを取得する

リバースエンジニアリングをするときやコーディングが先でドキュメントを後に書くような開発のときとか、C#のソースコードからメソッドコメントをツールで抽出したいなあーと思うことがたまにあります。

既存のツールで抽出できるものがあるかもしれませんが、有料だったり、細かいところに手が届かなかったりで、結局手作業でやるはめになりがちです。そんなとき、Roslynを使えば簡単にメソッドコメントを抽出できますよーっていうのが今回の内容です。

XMLドキュメントコメントを格納するクラスを作る

まずメソッドコメントであるXMLを格納するクラスを作ります。
class DocumentComment
{
    public string Summary { get; set; }
    public List<ParamDocumentComment> Params { get; set; }

    public DocumentComment()
    {
        Params = new List<ParamDocumentComment>();
    }
}

class ParamDocumentComment
{
    public string Name { get; set; }
    public string Comment { get; set; }
}
今回はsummaryとparamsを取ってくるだけにするので、それぞれのプロパティのみの構成にしています。

メソッドコメントのXMLをパースするクラスを作る

次にメソッドコメントのXMLをパースするクラスを作ります。
class DocumentCommentParser
{
    public static DocumentComment Parse(string documentComment)
    {
        // XDocumentクラスを用いてsammary要素とparam要素を取得する
        XDocument doc = XDocument.Parse(documentComment);
        var summary = doc.Descendants("summary").FirstOrDefault();
        var paramList = doc.Descendants("param");

        var docComment = new DocumentComment();
        // summary要素が取得できた場合、不要な改行や空白を取り除く
        docComment.Summary = (summary != null) ? RemoveAnySpaceChar(summary.Value) : string.Empty;

        foreach (var param in paramList)
        {
            // param要素はname属性が取得できたもののみ有効な要素とする
            var name = param.Attribute("name");
            if (name != null)
            {
                var pdc = new ParamDocumentComment();
                pdc.Name = RemoveAnySpaceChar(name.Value);
                pdc.Comment = RemoveAnySpaceChar(param.Value);
                docComment.Params.Add(pdc);
            }
        }
        return docComment;
    }

    // 引数の文字列から正規表現のパターン「\s」に該当する文字を削除した文字列を返す
    private static string RemoveAnySpaceChar(string str)
    {
        return Regex.Replace(str, "\\s", "");

    }
}
引数のXML文字列をパースし、DocumentCommentクラスを返すParse()メソッドを定義します。XDocumentクラスを使えばXMLのパースも簡単です。

CSharpSyntaxWalkerを継承したメソッドコメント収集クラスを作る

次にCSharpSymtaxWalkerを継承してメソッドコメントを収集するクラスを作ります。
class MethodDocumentCommentWalker : CSharpSyntaxWalker
{
    private SemanticModel m_semanticModel; // ドキュメントコメントを取得するにはセマンティックモデルが必要
    private Dictionary<MethodDeclarationSyntax, DocumentComment> m_documentComments = new Dictionary<MethodDeclarationSyntax, DocumentComment>();

    // 取得したドキュメントコメントのリストを返すプロパティ
    public IReadOnlyDictionary<MethodDeclarationSyntax, DocumentComment> DocumentComments
    {
        get { return m_documentComments; }
    }

    public MethodDocumentCommentWalker(SyntaxTree tree)
    {
        // コンストラクタでセマンティックモデルを作成しておく
        var compilation = CSharpCompilation.Create("tmpcompilation", syntaxTrees: new[] { tree });
        m_semanticModel = compilation.GetSemanticModel(compilation.SyntaxTrees[0], true);
    }

    // VisitMethodDeclaration()をオーバーライドし、各メソッド宣言からドキュメントコメントを取得する
    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        base.VisitMethodDeclaration(node);

        // IMethodSymbol.GetDocumentationCommentXml()でメソッドコメントのXML文字列を取得し、
        // パーサークラスを用いてパースする
        IMethodSymbol symbol = m_semanticModel.GetDeclaredSymbol(node);
        m_documentComments[node] = DocumentCommentParser.Parse(symbol.GetDocumentationCommentXml());
    }
}

メソッドコメント収集クラスを用いてメソッドコメントを収集する

最後にメソッドコメント収集クラスでメソッドコメントを収集します。
class Program
{
    static void Main(string[] args)
    {
        SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText("sample.cs"));
        
        // MethodDocumentCommentWalker.Visit()を呼び出せば、
        // MethodDocumentCommentWalker.DocumentCommentsに収集した結果が格納される
        var walker = new MethodDocumentCommentWalker(syntaxTree);
        walker.Visit(syntaxTree.GetRoot());

        foreach (var docComment in walker.DocumentComments)
        {
            MethodDeclarationSyntax method = docComment.Key;
            DocumentComment comment = docComment.Value;

            // ここで取得したメソッドコメントを整形してファイルに出力したりする
            Console.WriteLine("#" + method.Identifier);
            Console.WriteLine("##Summary");
            Console.WriteLine(comment.Summary);
            Console.WriteLine("##parameters");
            foreach (var param in comment.Params)
            {
                Console.WriteLine(param.Name + "\t" + param.Comment);
            }
            Console.WriteLine();
        }
    }
}

上記処理では、以下のサンプルクラスに対してメソッドコメント収集を行って、メソッド名とsummary、paramsのセットでコンソールに出力しています。
class sample
{
    /// <summary>
    /// サンプルクラスのコンストラクタコメント。
    /// </summary>
    public sample()
    {
    }

    /// <summary>
    /// Method1のメソッドコメント。
    /// </summary>
    /// <param name="arg">Method1の引数1。</param>
    /// <returns>Method1の戻り値。</returns>
    public int Method1(int arg)
    {
        return arg * 2;
    }

    /// <summary>
    /// Method2のメソッドコメント。
    /// </summary>
    /// <param name="arg1">Method2の引数1。</param>
    /// <param name="arg2">Method2の引数2。</param>
    /// <returns>Method2の戻り値。</returns>
    public int Method2(int arg1, int arg2)
    {
        return arg1 * arg2;
    }
}
出力結果はこんな↓感じになります。

#Method1
##Summary
Method1のメソッドコメント。
##parameters
arg Method1の引数1。

#Method2
##Summary
Method2のメソッドコメント。
##parameters
arg1 Method2の引数1。
arg2 Method2の引数2。

収集する部分と出力する部分が独立しているので、出力形式や出力先の変更も簡単です。

2015年5月20日水曜日

[Android]ボタンの背景を変える(設定する)

  • ボタンの背景を静的に設定する
ボタンの背景を静的に設定するには、xmlファイルのボタン要素にbackground属性を追加します。
以下がボタンに背景色を設定する場合のxmlファイル例です。
<Button
    android:id="@+id/button1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"
    android:layout_marginTop="64dp"
    android:text="Button" 
    android:background="#ff0000" /> <!-- 背景色をRGBで指定 -->
ボタンに背景画像を設定する場合は、[res]-[drawable]フォルダに画像を置き、
次のようにbackground属性を設定します(以下の例は、drawableフォルダにbutton_background.pngを置いた場合の例です)。
<Button
    android:id="@+id/button1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"
    android:layout_marginTop="64dp"
    android:text="Button" 
    android:background="@drawable/button_background" /> <!-- 背景画像を設定 -->

  • ボタンの背景を動的に設定する
ボタンの背景色を動的に設定するには、Button.setBackgroundColor()を使います。
Button button = (Button) findViewById(R.id.button1);
button.setBackgroundColor(Color.rgb(0, 100, 200));

背景画像を動的に設定するには、Button.setBackgroundResource()を使います。
Button button = (Button) findViewById(R.id.button1);
// drawableフォルダに置いたbutton_background.pngを背景に設定
button.setBackgroundResource(R.drawable.button_background);

  • ボタンの状態に応じて背景が変わるようにする
ボタンがフォーカスされたときや押されたときに背景が変化するようにするには、
State List Drawableを作成し、xmlでbackground属性に指定します。

1. State List Drawableを作成する。
まず、以下のようなxmlファイルを作成し、[res]-[drawable]フォルダに配置します。
xmlファイル名は何でも良いんですが、とりあえず"custom_button.xml"としました。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/button_pressed"
          android:state_pressed="true" />
    <item android:drawable="@drawable/button_focused"
          android:state_focused="true" />
    <item android:drawable="@drawable/button_default" />
</selector>
item要素でボタンに設定する画像をリストアップします。
android:drawableに設定する値は、drawableフォルダ内の画像ファイル名です
(button_default.pngの場合、"@drawable/button_default"を設定します)。

android:state_pressed="true"の要素には、ボタンが押下されたときに設定する画像、
android:state_focused="true"の要素には、ボタンがフォーカスされたときに設定する画像を指定します。

2. ボタン要素のbackground属性に、作成したxmlファイルを設定する。
xmlファイルのボタン要素のbackground属性に、以下のように先ほど作成したxmlファイルの名前を指定します。
<Button
    android:id="@+id/btnCustom"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true"
    android:layout_marginBottom="172dp"
    android:background="@drawable/custom_button"
    android:text="Custom button" />
以上で、ボタンの状態に応じてボタン背景が設定した画像に変わるようになりました。

@通常時


@フォーカス時


@ボタン押下時