RSS

タグ別アーカイブ: C#

WPFで全ての子コントロールに対する処理

 Windows Formではコントロール自身が保有している子コントロールのコレクションをプロパティとして持っている。これを利用して、多数コントロールに対する処理をループや再帰処理で済ませることが出来る。
 例えば、
  ・画面状態A : 画面内の全てのTextBoxが入力不能
  ・画面状態B : 画面内の全てのTextBoxが入力可能
という状態遷移を実現するのに、画面上の全TextBoxのreadonlyプロパティをひとつひとつ手動で変更するのは骨が折れる。大体、こういう単純作業をしないためのPGではなかったか。要するに面倒くさいのは嫌なので、子要素のコレクションを利用して適当にフィルタリングしつつループを回してプロパティを変更…ということをしたりする(個人的には「全コントロールを舐める」処理とか呼んでる)。まぁ、可読性やパフォーマンスの面を考えると賛否両論あるかもしれないが。

 さて、WPFではどうだろう。ざっと調べた感じ汎用の子コントロールコレクションのようなプロパティは見当たらない(機能上、特定の子要素を持つようなコントロールは除く)。しかし、一度でも書いたことがある人はわかるだろうが、XAMLはどう見ても親-子の依存関係が明確なツリー構造をしており、何らかのアクセス方法があってもおかしくない。
 んで、いくつか記事やフォーラムを覗いてみるとLogicalTreeHelperVisualTreeHelperというヘルパークラスが用意されており、それがまさしくそうらしいことがわかった。なお、一番参考になったのは、かずきのBlogさんの記事。VisualTreeとLogicalTreeの違いはその記事を見ていただければわかると思うが、「全コントロールを舐める」にはLogicalTreeのほうが向いていると思う。勿論VisualTreeにアクセスする必要のある処理もあるだろうけど。以下、パクって参考にしてテストコードを書いてみた。

    public class WpfUtil
    {
        /// <summary>
        /// targetの論理ツリー上の子要素全てに対してactionを実行します。
        /// actionはtarget自身にも作用する。再帰処理なのでスタックフレームに注意。
        /// </summary>
        /// <param name="target">ルートとするオブジェクト</param>
        /// <param name="action">実行するメソッドのデリゲート</param>
        public static void OperateLogicalChildren(DependencyObject target, Action<DependencyObject> action)
        {
            action(target);
            foreach(var child in LogicalTreeHelper.GetChildren(target))
            {
                if (child is DependencyObject)
                {
                    OperateLogicalChildren((DependencyObject)child, action);
                }
            }
        }

        /// <summary>targetの論理ツリー内での階層を返す</summary>
        public static int GetDepthInLogicalTree(DependencyObject target)
        {
            DependencyObject parent = LogicalTreeHelper.GetParent(target);
            int depth = 0;
            while (parent != null)
            {
                depth++;
                parent = LogicalTreeHelper.GetParent(parent);
            }
            return depth;
        }
    }

 典型的な使い方としてはこんなもんだと思う。共通関数っぽく作ってみたので(悪い癖?)、具体的処理内容はActionデリゲートにしてある。ちなみにあんまり使いやすくない(前処理とか後処理がほしくなったり、そもそもデリゲートに引数追加したくなったり)。まぁこのへんはおいおい改良するとして、ラムダ式の練習がてら適当なサンプルWPFのGridコントロール以下の論理ツリーを出力するプログラムを書いてみた。

<Window x:Class="WpfTreeTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="176" Width="197">
    <Grid Name="Container" Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="100*" />
        </Grid.ColumnDefinitions>

        <Label  Grid.Row="0" Grid.Column="0">ラベル1</Label>
        <Border  Grid.Row="0" Grid.Column="1" BorderThickness="3" VerticalAlignment="Top" CornerRadius="5">
            <Border.BorderBrush>#F00F</Border.BorderBrush>
            <TextBlock Name="Title">
                <Bold>あいうえお</Bold>
            </TextBlock>
        </Border>

        <Label  Grid.Row="1" Grid.Column="0">ラベル2</Label>
        <Button Grid.Row="1" Grid.Column="1" Name="OutputButton" Click="OutputButton_Click">
            <Button.Content>出力</Button.Content>
        </Button>

        <Label  Grid.Row="2" Grid.Column="0">ラベル3</Label>
        <ComboBox Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" Grid.ColumnSpan="2">
            <ComboBoxItem>アイテム1</ComboBoxItem>
            <ComboBoxItem>アイテム2</ComboBoxItem>
            <ComboBoxItem>アイテム3</ComboBoxItem>
        </ComboBox>

        <Label  Grid.Row="3" Grid.Column="0">ラベル4</Label>
        <CheckBox Name="CheckA" Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">チェックA</CheckBox>

        <Label  Grid.Row="4" Grid.Column="0">ラベル5</Label>
        <CheckBox Name="CheckB" Grid.Row="4" Grid.Column="1" VerticalAlignment="Center">チェックB</CheckBox>
    </Grid>
</Window>

 この適当なXAMLコードで記述されたGridに対して

        /// <summary>
        /// target以下の論理ツリー上の子要素をコンソール出力。階層分インデントする。
        /// 要素がControlでNameを持っていた場合は併せて出力。
        /// </summary>
        public static void PrintLogicalChildren(DependencyObject target)
        {
            WpfUtil.OperateLogicalChildren(target, t =>
            {
                String contName = "";
                Control cont = t as Control;
                if (cont != null && cont.Name.Length > 0)
                {
                    contName = " [" + cont.Name + "]";
                }
                int depth = WpfUtil.GetDepthInLogicalTree(t);
                //デバッグの都合を考えて一つずつ出力する
                Console.WriteLine(new string(' ', depth*2) + t.GetType().ToString() + contName);
            }
            );
        }

このメソッドをかますと以下の出力が得られる。

  System.Windows.Controls.Grid
    System.Windows.Controls.ColumnDefinition
    System.Windows.Controls.ColumnDefinition
    System.Windows.Controls.RowDefinition
    System.Windows.Controls.RowDefinition
    System.Windows.Controls.RowDefinition
    System.Windows.Controls.RowDefinition
    System.Windows.Controls.RowDefinition
    System.Windows.Controls.Label
    System.Windows.Controls.Border
      System.Windows.Controls.TextBlock
        System.Windows.Documents.Bold
          System.Windows.Documents.Run
    System.Windows.Controls.Label
    System.Windows.Controls.Button [OutputButton]
    System.Windows.Controls.Label
    System.Windows.Controls.ComboBox
      System.Windows.Controls.ComboBoxItem
      System.Windows.Controls.ComboBoxItem
      System.Windows.Controls.ComboBoxItem
    System.Windows.Controls.Label
    System.Windows.Controls.CheckBox [CheckA]
    System.Windows.Controls.Label
    System.Windows.Controls.CheckBox [CheckB]

 まぁ、こんなもんだろう。コンソール出力じゃなくて各要素に対する処理を書けばいい感じ。
 なお、VisualTreeのほうも書いてはみたもののカオスになるので記事にはしないでおく。メソッドの書き方としてはほとんど変わらない(VisualTreeHelper.GetChildrenメソッドが存在しないくらい程度)。

 
コメントする

投稿者: : 2011/05/22 投稿先 プログラム, C#, WPF

 

タグ: , , ,

C#でヒアドキュメント

 ちょっと調べて思ったこと。

 ヒアドキュメントっていうのは元々何の用語なのかはっきり知らない(来歴を知っている人がいれば教えていただきたい)が、シェルスクリプトを書くときや、言語的にはPerl、PHPの系統で使われる用語だ。意味としては「エスケープなどを必要とせず改行などを含むことが出来る文字列の記述法」くらいのものと思って良いだろう。
 C#の文字列リテラルでもヒアドキュメントと似たようなことが出来るのだが、呼び名が若干違う。MSDNでは「逐語的文字列リテラル」と表記されているが、他にも、「ヒアストリング」「@-quoted string」のような呼ばれ方もされるようで、どうにも統一性がない。確かに逐語的文字列リテラル…なんて変にややこしいだけの気もする。個人的には「ヒアストリング」がすっきりしてていいかな。

 具体的な記述法はこう、文字列リテラルの頭に@を付ける。

            string filepath = @"c:\hoge\fuga\piyo.xml";

            string strLines = @"エスケープ無しでも
                                改行付き文字列が作れるんだ。";

            string incQuote = @"ちなみに二重引用符は""こんな風に書く""と使える。";

 参考:http://msdn.microsoft.com/ja-jp/library/ms228362.aspx
 
 上記filepathの区切り文字のようにエスケープが必要とされる文字列を記述するのに便利ではある(バックスラッシュは本来なら\\と書かなければならない)。また同様にエスケープが必要な改行も、strLinesのように書くことが出来る(本来は\nなどと書く)。
 ただし「プレーンテキストをそのまま出力したい」という時にはあまり向かないと思う。なぜなら、当然のことだが、インデント用の空白も文字列として含まれるからだ。strLinesをこのまま出力した場合、インデント分だけ二行目の頭に空白が出力されてしまうことになる。どうしても空白が邪魔であれば、左詰めにして書くしかない。

string indentIyayo = @"インデントの空白が嫌なので
左詰めして
書きますよ";

 ただし見た目(=可読性)の問題がある。今はサンプルコードなのでこの程度だが、実際にはもっと深いインデントで書かれるだろう。namespace -> class -> method だけでも3段階程と考えると、1インデント4whitespaceだとして12個も空白があるわけで、そんな中で左詰め記法をしたらソースの可読性は著しく落ちてしまう。まぁこれはHTMLの<pre>タグでも似たような悩みと言えよう。
 ということで、自由に改行するためにヒアストリングを使う場合は「インデントの空白を無視してよい」ような状況、例えば、「通常は出力はしない」とか「あんまりメモリの制約を気にしなくて良い」という状況で使いましょう、ということになる。…まぁ好みの問題かもしれないけど。

 さてこんなヒアストリングだが使いたくなる局面といえば、先ほどのファイルパスの他、SQL文みたいなものをハードコーディングする場合がある。まぁ細かい議論は省略するが、SQL文をヒアストリングで書きたい場合とは要するに「面倒くさい記述をしたくない&可読性も確保したい」時だろう。可読性を考えると左詰めという技法はまず採用出来ない、と、なるとインデント空白を許容しなくてはいけない。
 SQL文ハードコーディングにおいてインデント空白が邪魔になる状況はあるだろうか?パッと思いつく限りではログ出力だ。デバッグ時や、例外発生時にSQL文をログ出力したいことは良くある。そういう時、あまり深いインデントでヒアドキュメント記述していると、ログでSQL文がやたら右の方に出力されてしまうし、記述場所がバラバラだとSQL文によって出力位置が変化しまって読みづらい。
 ということで、もしもSQL文をヒアストリングでハードコーディングする場合は、まとめて定数フィールドとして定義しておくのが良いだろう。

    public class DataBaseAccess
    {
        const string SQL_HOGE = @"
            select *
            from
                HOGE_TABLE hoge,
                FUGA_TABLE fuga
            where
                hoge.ID = fuga.ID
        ";

        private void selectHogeFuga()
        {
            // db はなんかSqlDataSourceみたいなオブジェクト。超適当。定義もしてないw
            db.selectCommand(SQL_HOGE);
            db.select();
        }

    }

 こんな感じ?適当すぎてゴメンナサイ。
 フィールド定義であれば左インデントも最小限に抑えられるし、同じインデント階層で定義することで出力位置もバラバラにならない。まぁ、ヒアストリング使わなくても普通はそうやって書くと思うけどね…(StringBuilder派はReadOnly化してstaticブロックで書いたり?とかかもだけど)。
 SQLの記述法としては何が綺麗なやり方なのか模索中だったりする。外部リソースとしてXMLファイルかテキストファイルで書いておいて別途作った読み込みモジュールで読み込む…というのが好みだったりするけど、毎回それが必要とも限らない。まぁ、それは別のお話なので掘り下げないけど。

 ぶっちゃけこんな記法が出来たらいいのに!と思ったりもする。

string newHereStr = @" こんな書き方あったらいいな。
                    $ 特定の記号より左側の空白は消される。
                    $ 可読性も確保され、記述の面倒さもあんまりない.
                    ";

 この場合、$が特殊な文字になっていて、その左の空白が無視される記法という意味。まぁ、これを実現する文字列操作関数を自分で作ることは可能だが、コスト考えると微妙だし、ちょっと強引すぎる気もする。実際こんな実装を持つ言語もあるようだけど、便利に使われているのだろうか?

 以上。予定の3倍くらいの長さのエントリになってしまった…。

 
1件のコメント

投稿者: : 2011/04/24 投稿先 プログラム, C#

 

タグ: , , ,

Visual StudioにおけるUTF-8の取り扱い

 たまには技術的な話題も書いていかないとね。
 といっても、ちょっと引っかかった程度のこと。

VisualStudioのテキストエディタではデフォルトで文字エンコードUTF-8で保存されるが、こいつはBOM付きなので注意すること。特にテキストファイルをプログラム中やWebページで取り扱うような場合。

という話。

 UTF-8エンコードでのテキスト記述が一般的となってきている昨今、そのファイルがBOM付きか否か、という注意は常識とも言える。多くのプログラマが一度は引っかかったことがあるのではないだろうか。この問題が根深いのは、何も考えずにIDE(統合開発環境)で保存するとデフォでUTF-8(BOM付)でエンコードされることが多いから、なのだろう。今回はVisualStudioだったが、Eclipseもそうだったような気がする(未調査)。BOM付きで保存されることを知っていたり、きちんと意識するようにしている時は問題ないが、気を抜いてると原因不明の空白に悩まされることになる。
 んで今回すっかりVisualStudioのことを信頼して気を抜いていた私も、危うくはまりかけたわけで、以下、一応経緯の記録。コード挿入タグを使ってやろうじゃなイカ、という目的もある。

 なお、BOMって何?問題は?という話はGoogle先生に丸投げします。 参考: バイトオーダーマーク – Wikipedia

[事例]

 ASP.NETで開発するWebページにて、外部テキストファイルに定義したHTML構造を挿入したいとする。これはページのヘッダやメニューを表示する際に良く行われる手法だ(ASP.NETならマスターページを使うという手もあるが、実はASP.NETは今回初めての触り始めなので、あのコントロールが裏で何をしてるか良く知らない)。例えば以下のASP.NETページを作成し実行する。

WebForm1.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="HtmlIncludeTest.WebForm1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>部分HTML挿入テスト</title>
    <meta http-epuiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
    <form id="form1" runat="server">
    <h1>部分HTML挿入テスト</h1>
<div style="width:200px;border:2px solid #00F">
<% Response.WriteFile("InsertPart.html"); %>
</div>
    </form>
</body>
</html>

InsertPart.html

<div style="border:2px solid #F00;">
<h2>今日の格言</h2>
<p>「訳が分からないよ。どうして人間はそんなに、魂の在処にこだわるんだい?」  </p>
</div>

 上記コードで表示されるページは以下のような画像になる。

HTML挿入プレビュー画像

 おかしな空白が入っていることがわかる。ソースを表示すると(表示に使うテキストエディタにも依るが)、上記のハイライト部分にあたる挿入テキストに半角空白が入っていることが確認できる。どうしてこういうHTML構造で表示上空白が入るんだったかという方は忘れたが…なんでだっけ。
 なお実際に直面したときは、埋め込みコードの書き方や、HttpRespose#WriteFile()メソッドを疑った(なにせASP.NETに不慣れだったので)。しかしそちらに問題はないわけで、ではでは、とファイル自体を疑ってバイナリエディタで表示してみると明らかにBOMなバイト列が入っていることがわかる(下の画像)。

[解決策]

 兎も角これじゃ困るので、BOMを削除する。しかしなぜかVisual StudioはBOMなしUTF-8が指定出来ないようで、バイナリエディタで無理矢理削除するか、別のテキストエディタで文字エンコードを指定しなおして保存する。
 で、ファイルを差し替えることでちゃんと空白が無くなる。


 よくある話といえばそれまでだが、IDEのデフォルトの指定がそうである、というところが罠っぽくなっていると思う。おそらく、IDEがUTF-8ファイルを判別する都合上、BOM付きで取り扱うことにしているのだろうが、それにしたってBOM無し指定にするメニューがあっても良いと思う。もしかしたらあるのかもしれない。もしくは、BOM無しにすると何か不都合があるのだろうか?今回の事例ではあまり問題になるようなことはなかったけれど。

 以上。トラブルシューティングというか、よくあるサンプルコード記事というのを目指して書いてみたけども、結構大変だな…画像を用意したり、サンプルコードを書いたり。世の中のコードサンプルブログの筆者はよく根気が続くなぁとも思う。
 まぁ今回は面白い話ではなかったけれども、今後何かしらもうちょっと役に立ちそうな、オリジナリティのあるコーディング記事でも公開していきたいところだ。

 
コメントする

投稿者: : 2011/04/16 投稿先 プログラム, C#, Visual Studio, Web

 

タグ: , , , , , ,

ソースコード用タグ

どうも<code>タグでは微妙すぎるので検索して

http://en.support.wordpress.com/code/posting-source-code/

というものを見つけたので試してみる。


int hoge = hoge + 1;

(プレビューしてみて)
おお?これは素晴らしい!
ということでちょっとC#のコードをだらっと貼ってみる


        /// <summary>
        /// うんたらかんたら
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void jmkControlTabContextMenu_Click(object sender, RoutedEventArgs e)
        {
            //コンテキストメニューの起動元はタブアイテムである
            ContextMenu contextMenu = sender as ContextMenu;
            MenuItem clickedItem = e.Source as MenuItem;
            if (contextMenu == null || clickedItem == null)
            {
                //想定外の型から呼び出された場合は落とす
                Environment.Exit(CODE_ERROR);
            }

            switch (clickedItem.Name)
            {
                case "addJmkControlMenuItem":
                    addJmkController();
                    break;
                case "removeJmkControlMenuItem":
                    TabItem tabItem = contextMenu.PlacementTarget as TabItem;
                    if (tabItem == null)
                    {
                        //呼び出し元がおかしい場合
                        Environment.Exit(CODE_ERROR);
                    }
                    removeJmkController(tabItem);
                    break;
            }
        }

おおー
すばらしい。

これつかっていけばいいんですね。

 
コメントする

投稿者: : 2011/04/03 投稿先 プログラム

 

タグ: , ,