ZoomAndPanControl は神

ひさしぶりにWPFを触っているのだけど、
大きい画像を表示するのに ScrollViewerが効いてくれない。おかしい、おかしいと思っていたけど、
ZoomAndPanControl を使ったらあっさり解決した。数時間を返せ。

直感的には、

   

こんなかんじ書いておけばいいだろ、と思うのだけど、大きすぎる画像に対しては機能しない。



 


こういう感じにすると期待通りの挙動をする。

http://www.codeproject.com/Articles/85603/A-WPF-custom-control-for-zooming-and-panning

とはいえ、これは大きいキャンバスの部分表示をしてくれたりして、かなり便利なものな感じだ。

AccessViolationException のバカ

Silverlightでプログラミングしていると、「AccessViolationException」と謎のエラーが頻発。
これはどうも、データバディングしているのだけど、描画をしなかったものに対して起こるエラーらしくて、バグ臭い。

ここ参照 http://stackoverflow.com/questions/8495242/silverlight-5-accessviolationexception

なので、このエラーが発生する直前で、対象のコントロールのVisibilityをCollapsedにして、データコントラクトに入れたあと、VisibilityをVisibleにすると収まったのかな???という感じ。
ソースコード的にはこんな感じ。

ContentControl1.Visibility = System.Windows.Visibility.Collapsed;
ContentControl1.DataContext = data;
ContentControl1.Visibility = System.Windows.Visibility.Visible;

アホかと。

C# Silverlight CSVファイルの読み込みクラス

SilverlightのOpenFileDialog経由で手持ちのCSVファイルを読み込んで処理をするというのを作っているのだけど、
いつも便利な、System.IO.File.ReadLines()でラクラクファイル読み込みをしようとしたら、OpenFileDialogは、Steamしか返さず(セキュリティのため)、しかし、System.IO.File.ReadLinesはファイルパスが必要なため、開発気力がガタ落ちだったので、簡易的にさささと、CSVファイルを読むものを作った。

ソース:https://gist.github.com/kiichi54321/5298464

使い方

CSVFile.ReadLinesをforeachで呼び出すだけ。
そこで、item.GetValue("列名");で対応する列名の値を取得できる。
列番号と列名の対応関係をチェックする作業から開放されます。

注意点
CSVファイルの文字コードはUTF8限定。
ソースを見るとわかるけど、正確なCSVファイルを読み込む仕組みにはなっていない。ただのカンマ区切りだ。
エラー処理が割と適当。データを信頼しきっている。


あと、自分の過去のGistを見ていたら、こんなのみつけた。
webからCSVファイルを読み込む便利クラス。
どんだけ、CSVを読むためのものを作っているのですかね。
これは、解説すると、WebClientを使って読むと一行ごとに読めないから、それ用につくったもの。
Dropboxとの連携用に作った。データを作る人とプログラムを作る人との分業を達成するために。

SilverLight WPF クロス表を作る。

別にクロス表を作ったというわけでもないけど、それっぽいことをやったので、メモ。
縦横に動的に伸びる表をどうやったら作れるか?ということ。

最終的にこういうのを作る。

結論から言うと、ポイントは、データ構造をちゃんと作る、StackPanelを使うこと、Borderを使って、横幅の固定化、ItemsControlの入れ子化だ。

綺麗な縦横の線で区切られているのだからはじめは、Gridを使おうかと思ったけど、どうやって、動的に行と列を増やすことができるの?というところで詰まった。
そこで、StackPanelとBorderで横幅の固定化を行うという戦略と、ItemsControlの入れ子かを使って縦横を生成しようということを試みた。

ソース。
https://gist.github.com/kiichi54321/5290813

まずは、表示するためのデータ構造づくり。
ClusterTableクラスを大元として、ヘッダに当たる列とデータの中身に当たるところを定義する。
そして、ヘッダは、オブジェクトにしているけど、事実上はテキストが並んでいるだけ。拡張性も考えると、オブジェクトにしたほうがいいという判断。
LayerGroupクラスは、一列を表現する。Nameパラメータは、見出しに使う用である。
本来のクロス表では、ここまででいいのだが、こっちの要件として、さらに中身がテキストの列である必要があったので、Comunityクラスのリストをもたせている。
そのため、ソースでは、ItemsControlの三段入れ子になっている。

次にXAMLファイル。
縦横の大きさが未定のため、とりあえず、ScrollViewerで囲った。
ContentControlで、ClusterTableクラスをぶち込み先を明示化させる。
まずは、ヘッダのところをの処理。StackPanelの入れ子を作って、Borderで一番隅の空白のところを作る。
ItemsControlで、横を埋める。必ずBorderで枠の大きさを指定する。ItemsControl.ItemsPanelでStackPanelの指定もポイント。
次、行。はじめの行の名前のところは別でつくる。そして、入れ子のItemsControl データ構造としてちゃんと入れ子のリストにしておけば、こういう風に作れる。
さらに、その中でも、ItemsControlの入れ子はやれて、やっている。

なんか解説は飽きたのでこのくらいで。

とりあえず、ItemsControlの入れ子を使えば、結構色々できることがわかった。

C# 2GBを超えたテキストファイルを読む

よくわかんないのだけど、通常のSystem.IO.File.ReadLines(path)では、なぜか2GBを超えたでかいテキストファイルが読めなかった。エラーも特に出さないのが酷い。ファイルサイズをみたら気づいた。たぶんバグ。そのうち修正されるのだろう。内部がINT32で管理されているとかそういうオチなんだろう。
しかし、普通のファイルストリームを使えば、2GB越えは普通に読めるみたいなので、ちょこっと改造して読めるようにした。


戦略は、「読む位置をずらしながら読む」です。
ずーとストリームとストリームリーダーが分かれている理由がよくわかんなかったのだけど、
さまざま種類のストリームを扱えるというのは当然として、こういう使い方をするためでもあるのですかねぇ。


あと、前提として改行が\r\nに固定なので、使うときは気を付けてください。
おまけとして、ファイル分割用のつけました。

    public class BigText
    {
        public static int MaxLine = 1000000;
        public static IEnumerable<string> ReadLines(string file, Encoding enc)
        {
            if (MaxLine == 0) MaxLine = 100000;
            long postion = 0;
            int count = 0;

            bool flag = true;
            while (flag)
            {
                using (System.IO.FileStream fs = new System.IO.FileStream(file, System.IO.FileMode.Open))
                {
                    fs.Seek(postion, System.IO.SeekOrigin.Begin);
                    using (var stream = new System.IO.StreamReader(fs, enc))
                    {
                        while (flag && count < MaxLine)
                        {
                            string line = stream.ReadLine();
                            if (line == null) flag = false;
                            else
                            {
                                count++;
                                postion += enc.GetByteCount(line + "\r\n");
                                yield return line;
                            }
                        }
                    }
                    count = 0;
                }
            }
        }


        public static void SplitTextFile(string file,Encoding enc,string folder,int maxLine)
        {
            long postion = 0;
            int count = 0;
            int part = 0;
            bool flag = true;
            while (flag)
            {
                using (System.IO.FileStream fs = new System.IO.FileStream(file, System.IO.FileMode.Open))
                {
                    fs.Seek(postion, System.IO.SeekOrigin.Begin);
                    string wFile = folder + "/" + Path.GetFileNameWithoutExtension(file) + "_" + part.ToString() + Path.GetExtension(file);
                    using (var stream = new System.IO.StreamReader(fs, enc))
                    using(var write = new StreamWriter(wFile,false,enc))
                    {
                        while (flag && count < maxLine)
                        {
                            string line = stream.ReadLine();
                            if (line == null) flag = false;
                            else
                            {
                                count++;
                                postion += enc.GetByteCount(line + "\r\n");
                                write.WriteLine(line);
                            }
                        }
                    }
                    count = 0;
                    part++;
                }
            }
        }
    }

リソースを使ってのUIの共通化(データテンプレートをバインディングする)

SilverLight,WPFの話。WPFでは試していないけど、たぶん出来るだろ。
本当にこういうテクニックでいいのかは謎だけど、とりあえず動いたので。

同じようなもの(たとえばグラフなど)を複数配置するしたいという要望があるのだけど、XAMLのコピーでも済ますのは、数が少ないならまぁいいけれど、多くなってくるとメンテナンスが大変だ。そのため、共通化させたい。

真っ当な方法としては、UserContorolを作るというのがあるのだけど、何となく、リソース使えば何とかなるんじゃね?とか思ったら、面倒だけど、何とかなった。その方法を書く。

そもそも、データテンプレートをバインディングするためのクラスが存在しなかった。絶対あるだろ・・とか思ったけど、ないっぽい。
そういうわけで作った。ユーザコントロールを作って、依存プロパティでDataTemplateを追加、そして、コントロールロード時にデータテンプレートを追加するようにする。

    public partial class DataTemplateControl : UserControl
    {
        public DataTmpControl()
        {
            InitializeComponent();
        }

        public DataTemplate DataTemplate
        {               
            get { return (DataTemplate)GetValue(DataTemplateProperty); }
            set { SetValue(DataTemplateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DataTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataTemplateProperty =
            DependencyProperty.Register("DataTemplate", typeof(DataTemplate), typeof(DataTemplateControl), new PropertyMetadata(null));

        bool loadedFlag = false;
        private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
        {
            if (loadedFlag == false)
            {
                if (DataTemplate != null)
                {
                    var d = DataTemplate.LoadContent() as FrameworkElement;
                    LayoutRoot.Children.Add(d);
                }
                loadedFlag = true;
            }   
        }
    }

これはXAML側でこんな感じに呼び出して使う。リソースはDataTemplateで作っていること。

試していないけど、動的な要素を表示するのに、データテンプレートのプロパティを持たせてやれば、たぶん動的な要素の表示とか出来そう。

Rawler Framework C#用、webスクレイピングとテキスト処理のためのフレームワークを作ったよ。その2

その1からの続き。

PreTreeを使っての前処理の記述とツリーの折りたたみ

このフレームワークでいろいろ作っていくうちに気付いたのだけど、タグで囲むのはわかりやすいのだけど、その階層が深くなると、XAMLの見通しがすごく悪くなる。なるべくなら、概念のレベルで扱いたい。例えば、処理の前のテキストの加工(置換、trim、タグの除去など)は本質的な処理とはいえない、これらは必ず必要であるが、階層を深くし見通しを悪くさせてまで必要なものでもない。そのため、<クラス名.PreTree>で囲まれたところにそれらの処理を入れることによって、そのオブジェクトが実行する前の処理をすることができる。XAMLエディタの補助として、PreTreeの中身を折りたためるので見通しはよくなる。

例。

    <TagClear>
        <Trim>
            <DataWrite Attribute="date"></DataWrite>
        </Trim>
    </TagClear>

    <DataWrite Attribute="date">
        <DataWrite.PreTree>
            <TagClear>
                <Trim></Trim>
            </TagClear>
        </DataWrite.PreTree>
    </DataWrite>

は、同じ意味。DataWriteがツリーの上位に来たため、データを書き込むという意味が明確化される。行数的には後者は2行増えるが、PreTreを折りたためるので、3行となり2行分節約される。当然、前処理の数が増えれば恩恵は大きくなる。PreTreeでの注意点は、PreTreeでは複数を扱えない。エラーは出すことはないが、予期しないテキストが出てしまうことになるだろう。複数を扱うときは、今まで通りのやり方でやらないといけない。

ちなみに、このPreTreeはすべてのクラスにあるが、所々のクラスには***Treeというのがある。これは同様にTreeを格納することができるため、一つのオブジェクトで親からのテキストの変数だけではなく、複数の変数を扱うための仕組みである。Tree構造故に、親は一つしか持てず、本来は一つの変数しか扱えなかったことの拡張である。それぞれ、ツリーの一番最後のオブジェクトのテキストが採用される。

ファイル入出力機能

初めは、このツール、結果をメモリーに貯めて、最後の書きだすのを基本として、コードビハインドでの処理を追加によって、逐次的にDBなどに書き込めるような仕様にしていた。プログラマのためのフレームワークということであったが、XAMLのテキストファイルから、起動できるようにしたことから、それだけで完結できるような仕組みにした方がいいと思うようになった。
そこで追加したのがファイル入出力機能だ。クラス名的には、FileSave,FileReadLines,GetCurrentFileReadLine にあたる。
FileSaveはDataを継承しているので、Dataと同じようにDataWriteで内容を書き込み、NextDataRowで次のデータ行になる。FileSaveでは、NextDataRow発生時にファイルへの書き込みが発生する。そのため、途中で仮にソフトが落ちたとしても、そのデータは保存されている。FileSaveは書き込みモードとして、新規書き込みと追加書き込みの二つのモードをもっている。状況に応じて使い分けてくれ。FileNameに値を入れるとそれがファイル名になる。ツールを起動したところにそのファイルが生成されるはずだ。FileNameの値がない時、ダイアログが立ち上がり、そこで選択できるようになっている。ちなみに書き込むファイルのエンコードはUTF8である。
一方、FileReadLinesでは指定したファイルを一行分づつ読むことができる。FileSaveと同様にFileNameでの指定がない時、ダイアログがひらいて指定できる。テキストでの一般的なデータ形式である、タブ区切り、カンマ区切りを扱うためのSplitというクラスを用意した。指定したものでの区切りでテキストをリストにする。Num(0番から始まる)を指定することで、何番目の要素を採用するかを指定できる。これによりテキスト一行分から複数のデータを取得できる。このフレームワークの使用上、階層を進んでいく中でテキストはどんどん変わってしまうため、もともとのテキストが跡形もなくなってしまう。それを回避するために、GetCurrentFileReadLineは上流にあるFileReadLinesのテキストを取得できるクラスだ。Splitと組み合わせることで、DataWriteで読み込んだテキストの内容を書き込むことができる。

例 読み込むファイルの形式は、タブ区切りで初めの列にはURLが入っている。起動すると保存ファイル名と読み込むファイル名の指定をしないといけない。ExtendFilterでtsvと指定しているため、ダイアログではフィルタされた状態になる。

    <FileSave FileSaveMode="Create"  ExtendFilter="tsv"
      xmlns="clr-namespace:Rawler.Tool;assembly=Rawler"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <FileReadLines ExtendFilter="tsv">
            <Split Num="0" SeparatorType="Tab">
                <Page>
                    <Tags Tag="title">
                        <DataWrite Attribute="title"></DataWrite>
                    </Tags>
                    <GetCurrentFileReadLine>
                        <Split Num="1" SeparatorType="Tab">
                            <DataWrite Attribute="key"></DataWrite>
                        </Split>
                    </GetCurrentFileReadLine>
                </Page>
                <NextDataRow></NextDataRow>
            </Split>
        </FileReadLines>
    </FileSave>

このフレームワークの出力として、タブとカンマの混合での出力がなされる。一つの属性に複数の要素が入るためだ。これは他のツールでは扱いにくい。そのための変換も作ることができる。

タブ区切りのデータで、初めの列がKeyとなるもの 次の列がカンマ区切りのデータを想定している。

    <FileSave FileSaveMode="Create"
      xmlns="clr-namespace:Rawler.Tool;assembly=Rawler"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <FileReadLines ExtendFilter="tsv">
            <Split Num="1" SeparatorType="Tab">
                <Split SeparatorType="Comma">
                    <DataWrite Attribute="tag"></DataWrite>
                    <GetCurrentFileReadLine>
                        <Split SeparatorType="Tab" Num="0">
                            <DataWrite Attribute="key"></DataWrite>
                        </Split>
                    </GetCurrentFileReadLine>
                    <NextDataRow></NextDataRow>
                </Split>
            </Split>
        </FileReadLines>
    </FileSave>

出力はKeyとtagのペアが続くものとなる。

条件分岐

構造化プログラミングに置いて、抽象化すると、順次、反復、分岐の三つの要素があるという。このフレームワークにおいて、順次は親子関係と子の並びで達成されており、反復は、RawlerMultiBaseを継承した複数のテキストをもつクラスとその子要素で達成されている。あとは、分岐を組み込めばその3要素をすべてを達成したことになる。
単純な分岐としてEqualとContainsがある。プログラミング言語でいう、IF文に相当する。
Equalは親テキストとの完全一致、Containsは部分一致したとき、処理する内容を子に記述する。ResultをFalseとした時、一致しなかったときとなる。

    <Contains ContainsText="ビジー状態です">
   親テキストが「ビジー状態です」という文字を含んでいる時の処理
    </Contains>
    <Equal EqualCSV="た,です,し,な,だ,ん,で,い,う,さ,れ,て,せ,ます" Result="False">
     親テキストがEqualCSVに含まなかった文字列だった時の処理・・・
    </Equal>

しかし、これでは、if{}else{}は表現できても(ResultをTrue,False両方を用意すればいい)、iF{}else if{}else{} といった条件を指定することができない(if{}else{if{}else{}}とう感じに階層化すれば可能ではあるが、可読性は落ちる)。
そのための、複雑な分岐としてSwitchがある。これはそのあとに、必ず、CaseTextなどのCaseで始まるクラスがそのあとに来ないといけない。指定条件に当てはまるものだけが実行される。また、Switch.SwitchValueTreeを指定することで、Caseで一致の対象となる文字列の取得条件を指定できる。

例、テーブルタグ中で、一行文ずつ読んでいき、その指定されたところに入っている文字列で分岐を行う。

    <TagExtraction Tag="table"  IsSingle="True">
        <TagExtraction Tag="tr" >
            <Switch>
                <Switch.SwitchValueTree>
                    <TagExtraction Tag="td" ParameterFilter="nowrap">
                    </TagExtraction>
                </Switch.SwitchValueTree>
                <CaseText CheckText="レベル">
                  「レベル」だった時の処理
                </CaseText>
                <CaseText CheckText="参加メンバー">
                  「参加メンバー」だった時の処理
                </CaseText>
                <CaseText CheckText="登録タグ" >
                  「登録タグ」だった時の処理
                </CaseText>
                <Switch.OutsideTree>
                    いずれにも当てはまらなかった時の処理。
                </Switch.OutsideTree>
            </Switch>
        </TagExtraction>
    </TagExtraction>

CaseIntでは、数値を扱え、数値の範囲を扱える。テキストでは苦手な連続値を指定の文字列に変更することができる。
例は、タブ区切りのデータのある列を読み、その数値によってテキストを替える例。

     <Split Num="1" SeparatorType="Tab">
        <Switch>
            <CaseInt Start="0" End="9">
                <DataWrite Attribute="Age" Value="10才未満"></DataWrite>
            </CaseInt>
            <CaseInt Start="10" End="19">
                <DataWrite Attribute="Age" Value="10代"></DataWrite>
            </CaseInt>
            <CaseInt Start="20" End="29">
                <DataWrite Attribute="Age" Value="20代"></DataWrite>
            </CaseInt>
        </Switch>
    </Split>

CaseDateを使えば、日付データに対して同様のことができる。

クエリ機能

重複削除機能を実装しようと思ったが、子にその機能を持つクラスを追加する形ではどうにもうまくいかない。そこで、複数のテキストを出すクラスに対して簡単なクエリを行えるようにした。クエリといっても実装はLINQである。まだLINQにあるすべての機能は実装していない。

ページの中のリンクを取得し、重複を削除したものを表示させる。

    <Page>
        <Links>
            <Links.Query>
                <QueryDistinct></QueryDistinct>
            </Links.Query>
            <Report></Report>
        </Links>
    </Page>

また、クエリはQueryで始まるQueryBaseを継承したものなら、いくらでもつなげることができる。ただし複数の子はもつことができない。(意味ないので)

例:1から100までのリストをシャッフルして、上から10個取ってきて、それをソートしたものを出力する。(テキストとして扱っているため綺麗なソートはできない。)

    <Range Start="1" End="100">
        <Range.Query>
            <QueryShuffle>
                <QueryTake Count="10">
                    <QueryOrderBy></QueryOrderBy>
                </QueryTake>
            </QueryShuffle>
        </Range.Query>
        <Report></Report>
    </Range>

他にも、QueryFirst、QueryLast、QueryElementsAt はそれぞれリストの初めのもの、最後のもの、指定した番号のものを返します。これは対象の構造がわかっていれば便利に扱えます。
まぁ、クエリに関しては未実装クエリは多いです。必ず必須なはずのWhereもないですしwww先の条件分岐の命令を使えばできるからこそですけどね。


これで仕様の大半の説明は終わりました。僕的には生産性の向上が高いのですが、難しすぎなんでしょうかねぇ・・・。もっとも作りやすいエディタがVS.NETであるという点が難点といったら、難点ですが…。