LINQ to Objectについて

LINQ to Objectはとても便利だけど、DataTableとかDataSetを使ったサンプルがあんまりないので、メモ。
DataTable自体があまり使われないのかもしれないけど、データベースからとってきた値をとりあえず入れて使う分には使いやすいのでよく使う。
プログラマーによってクラスの作り方がバラバラだったり、共通関数に渡すときに統一できたりとか、Webサービスからデータを渡すときに簡単に渡せたりするんで結構重宝している。

CSVとデータベースの値を結合したり、CSVとCSVを結合したりするときにもSQLみたいに書きたい要望があったりするのでそのサンプル。
※まれにデータが数百万行あったりして、SQLよりもLINQで処理したほうが早いケースがあったりするときに書き直したりする。

VB.NET ====

    ''' <summary>
    ''' Left Join
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub btnLeftJoin_Click(sender As Object, e As EventArgs) Handles btnLeftJoin.Click
        Dim dtUri As New DataTable
        Dim drUriSample As DataRow

        ' ▼ 売上データ
        dtUri.Columns.Add(New DataColumn("売上No", GetType(Integer)))
        dtUri.Columns.Add(New DataColumn("売上日", GetType(String)))
        dtUri.Columns.Add(New DataColumn("得意先CD", GetType(String)))
        dtUri.Columns.Add(New DataColumn("商品名", GetType(String)))
        dtUri.Columns.Add(New DataColumn("売上数量", GetType(Decimal)))
        dtUri.Columns.Add(New DataColumn("売上金額", GetType(Decimal)))

        '   テストデータ
        drUriSample = dtUri.NewRow
        drUriSample("売上No") = 1
        drUriSample("売上日") = "20171031"
        drUriSample("得意先CD") = "00001"
        drUriSample("商品名") = "パソコン"
        drUriSample("売上数量") = 1
        drUriSample("売上金額") = 150000
        dtUri.Rows.Add(drUriSample)

        drUriSample = dtUri.NewRow
        drUriSample("売上No") = 2
        drUriSample("売上日") = "20171031"
        drUriSample("得意先CD") = "00002"
        drUriSample("商品名") = "マウス"
        drUriSample("売上数量") = 1
        drUriSample("売上金額") = 3000
        dtUri.Rows.Add(drUriSample)

        drUriSample = dtUri.NewRow
        drUriSample("売上No") = 1
        drUriSample("売上日") = "20171031"
        drUriSample("得意先CD") = "00001"
        drUriSample("商品名") = "モニター"
        drUriSample("売上数量") = 2
        drUriSample("売上金額") = 30000
        dtUri.Rows.Add(drUriSample)


        ' ▼ 得意先データ
        Dim dtTok As New DataTable
        Dim drTokSample As DataRow

        dtTok.Columns.Add(New DataColumn("得意先CD", GetType(String)))
        dtTok.Columns.Add(New DataColumn("得意先名", GetType(String)))
        dtTok.Columns.Add(New DataColumn("備考", GetType(String)))

        '   テストデータ
        drTokSample = dtTok.NewRow
        drTokSample("得意先CD") = "00001"
        drTokSample("得意先名") = "株式会社 システム作ろう"
        drTokSample("備考") = ""
        dtTok.Rows.Add(drTokSample)

        drTokSample = dtTok.NewRow
        drTokSample("得意先CD") = "00002"
        drTokSample("得意先名") = "株式会社 いろいろ販売"
        drTokSample("備考") = ""
        dtTok.Rows.Add(drTokSample)

        Dim dtUriIchiran As New DataTable
        Dim drIchiran As DataRow

        ' ▼ 売上一覧
        dtUriIchiran.Columns.Add(New DataColumn("売上No", GetType(Integer)))
        dtUriIchiran.Columns.Add(New DataColumn("売上日", GetType(String)))
        dtUriIchiran.Columns.Add(New DataColumn("得意先CD", GetType(String)))
        dtUriIchiran.Columns.Add(New DataColumn("得意先名", GetType(String)))
        dtUriIchiran.Columns.Add(New DataColumn("売上数量", GetType(Decimal)))
        dtUriIchiran.Columns.Add(New DataColumn("売上金額", GetType(Decimal)))

        Dim lnqIchiran = From drUri In dtUri.AsEnumerable
                         Group Join drTok In dtTok
                         On drUri("得意先CD") Equals drTok("得意先CD")
                         Into grp = Group
                         From drTokEmpty In grp.DefaultIfEmpty
                         Select New With {
                             .売上No = drUri("売上No"),
                             .売上日 = drUri("売上日"),
                             .得意先CD = drUri("得意先CD"),
                             .得意先名 = If(drTokEmpty("得意先名") Is Nothing, "", drTokEmpty("得意先名")),
                             .売上数量 = drUri("売上数量"),
                             .売上金額 = drUri("売上金額")
                             }

        For Each lnqdr In lnqIchiran
            drIchiran = dtUriIchiran.NewRow
            drIchiran("売上No") = lnqdr.売上No
            drIchiran("売上日") = lnqdr.売上日
            drIchiran("得意先CD") = lnqdr.得意先CD
            drIchiran("得意先名") = lnqdr.得意先名
            drIchiran("売上数量") = lnqdr.売上数量
            drIchiran("売上金額") = lnqdr.売上金額
            dtUriIchiran.Rows.Add(drIchiran)
        Next

    End Sub


C# ====


        private void btnLeftJoin_Click(object sender, EventArgs e)
        {
            DataTable dtUri = new System.Data.DataTable();
            DataRow drUriSample = null;

            // ▼ 売上データ

            dtUri.Columns.Add(new DataColumn("売上No", typeof(int)));
            dtUri.Columns.Add(new DataColumn("売上日", typeof(string)));
            dtUri.Columns.Add(new DataColumn("得意先CD", typeof(string)));
            dtUri.Columns.Add(new DataColumn("商品名", typeof(string)));
            dtUri.Columns.Add(new DataColumn("売上数量", typeof(decimal)));
            dtUri.Columns.Add(new DataColumn("売上金額", typeof(decimal)));

            //   テストデータ
            drUriSample = dtUri.NewRow();
            drUriSample["売上No"] = 1;
            drUriSample["売上日"] = "20171031";
            drUriSample["得意先CD"] = "00001";
            drUriSample["商品名"] = "パソコン";
            drUriSample["売上数量"] = 1;
            drUriSample["売上金額"] = 150000;
            dtUri.Rows.Add(drUriSample);

            drUriSample = dtUri.NewRow();
            drUriSample["売上No"] = 2;
            drUriSample["売上日"] = "20171031";
            drUriSample["得意先CD"] = "00002";
            drUriSample["商品名"] = "マウス"; ;
            drUriSample["売上数量"] = 1;
            drUriSample["売上金額"] = 3000;
            dtUri.Rows.Add(drUriSample);

            drUriSample = dtUri.NewRow();
            drUriSample["売上No"] = 1;
            drUriSample["売上日"] = "20171031";
            drUriSample["得意先CD"] = "00001";
            drUriSample["商品名"] = "モニター";
            drUriSample["売上数量"] = 2;
            drUriSample["売上金額"] = 30000;
            dtUri.Rows.Add(drUriSample);


            // ▼ 得意先データ
            DataTable dtTok = new DataTable();
            DataRow drTokSample = null;

            dtTok.Columns.Add(new DataColumn("得意先CD", typeof(string)));
            dtTok.Columns.Add(new DataColumn("得意先名", typeof(string)));
            dtTok.Columns.Add(new DataColumn("備考", typeof(string)));

            //   テストデータ
            drTokSample = dtTok.NewRow();
            drTokSample["得意先CD"] = "00001";
            drTokSample["得意先名"] = "株式会社 システム作ろう";
            drTokSample["備考"] = "";
            dtTok.Rows.Add(drTokSample);

            drTokSample = dtTok.NewRow();
            drTokSample["得意先CD"] = "00004";
            drTokSample["得意先名"] = "株式会社 いろいろ販売";
            drTokSample["備考"] = "";
            dtTok.Rows.Add(drTokSample);


            DataTable dtUriIchiran = new DataTable();
            DataRow drIchiran = null;

            // ▼ 売上一覧
            dtUriIchiran.Columns.Add(new DataColumn("売上No", typeof(int)));
            dtUriIchiran.Columns.Add(new DataColumn("売上日", typeof(string)));
            dtUriIchiran.Columns.Add(new DataColumn("得意先CD", typeof(string)));
            dtUriIchiran.Columns.Add(new DataColumn("得意先名", typeof(string)));
            dtUriIchiran.Columns.Add(new DataColumn("売上数量", typeof(decimal)));
            dtUriIchiran.Columns.Add(new DataColumn("売上金額", typeof(decimal)));

            var lnqIchiran = from drUri in dtUri.AsEnumerable()
                             join drTok in dtTok.AsEnumerable()
                             on drUri["得意先CD"] equals drTok["得意先CD"]
                             into tokGroup
                             from drTokEmpty in tokGroup.DefaultIfEmpty()
                             select new
                             {
                                 売上No = drUri["売上No"],
                                 売上日 = drUri["売上日"],
                                 得意先CD = drUri["得意先CD"],
                                 得意先名 = (drTokEmpty == null) ? "" : drTokEmpty["得意先名"],
                                 売上数量 = drUri["売上数量"],
                                 売上金額 = drUri["売上金額"]
                             };

            foreach (var lnqdr in lnqIchiran)
            {
                drIchiran = dtUriIchiran.NewRow();
                drIchiran["売上No"] = lnqdr.売上No;
                drIchiran["売上日"] = lnqdr.売上日;
                drIchiran["得意先CD"] = lnqdr.得意先CD;
                drIchiran["得意先名"] = lnqdr.得意先名;
                drIchiran["売上数量"] = lnqdr.売上数量;
                drIchiran["売上金額"] = lnqdr.売上金額;
                dtUriIchiran.Rows.Add(drIchiran);
            }

        }

===========

on句で結合するときに別の関数を間に挟んだりできるので結構便利

例)Private MaeZero(string Value,int Keta) As String   ->前ゼロ梅する関数


        Dim lnqIchiran = From drUri In dtUri.AsEnumerable
                         Group Join drTok In dtTok
                         On MaeZero(drUri("得意先CD"),5) Equals MaeZero(drTok("得意先CD"),5)
                         Into grp = Group
                         From drTokEmpty In grp.DefaultIfEmpty
                         Select New With {
                             .売上No = drUri("売上No"),
                             .売上日 = drUri("売上日"),
                             .得意先CD = drUri("得意先CD"),
                             .得意先名 = If(drTokEmpty("得意先名") Is Nothing, "", drTokEmpty("得意先名")),
                             .売上数量 = drUri("売上数量"),
                             .売上金額 = drUri("売上金額")
                             }

ただ、プロジェクト単位である程度書き方を統一しておかないとプログラマさんによっては書き方が全然違うので保守とか考えるときは注意したほうがいい。

みんなが知識ある人ばっかりだったらそんなに気にしなくてもいいかもしれないけど、開発の後半になって仕様変更とかで危なそうな個所を探すときにも書き方が一緒じゃないとGrepしても引っかからなかったりする。

パッと見でもわからないし・・・。

ガントチャート作成日記

今日はタスクの分割機能を実装してみた。

タスクを選んで右クリックでタスクを細分化することができる。
そのため、TODOリストの組み合わせという形ではなく、大きなタスクをざっと洗い出して、その中でさらに何をすればいいのかを細かく分けていけるようにしてみた。

タスクを選んで右クリック→タスクの分割を押下することで分割される。













タスクの分割設定ボタンを押下すると設定画面で分割数やタスク名を指定することができる。

不具合のある機能を修正し、タスクの分割機能などいくつかの機能を回収した。

公開URL:https://sites.google.com/site/mmbloguserfile/wai-bufairu/ProjectManager_Beta_0.0.0.1.zip

まだまだ作成中。

ガントチャート作成日記

今日もちょっとずつガントチャートを作成してみる。
なんかいろいろとバグがあるのでゆっくりつぶしながら、今日も機能を追加。

やっぱりテストとかよりも思いついた機能を実装しているほうが楽しい。

今日追加したのは、タスクの移動をしながら水平スクロールする機能。


右クリック+スクロールの場合は、水平スクロールするように機能を修正。
その時に合わせてタスクも横に移動するようにしてみた。

ほかにも、試しに使ってたらタスクを右クリックしたら「タスクの分割」を一気にできるようにする機能とかもほしいなーと思ったりしたので実装中。
  例)
    〇〇〇作業  ← 「タスクの分割」機能を実行
    ▲▲▲作業
     ↓
    〇〇〇作業
     ∟ 〇〇の準備
     ∟ 〇〇の資料作成
     ∟ 〇〇のデモプレゼン
    ▲▲▲作業

みたいな感じで、はじめは大きな枠でタスクを作って少しずつ細かいTODOに落とし込んでいくイメージ。

ちょっとずつバグもつぶれてきたので懲りずに近々ベータ版をアップしてみようかと思う。


ガントチャート作成日記

備考があるときだけ付箋っぽい画像を表示してカーソルが当たったら備考を表示するようにしたいなーと思ったので実装してみた。
慣れて着たらWPFはやりたいと思ったことをさくっと作れるので すごいと思う。




<ユーザーコントロール>
PopUpNote.xaml

<UserControl x:Class="ProjectManager.Controls.PopUpNote"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:ProjectManager.Controls"
             mc:Ignorable="d" >

    <!--フィルタ内容を選択するためのPopUpコントロール-->
    <Popup Name="popupInfomation" StaysOpen="False" Width="200" Height="180">
        <Border BorderBrush="Black" BorderThickness="0.5" Margin="0" Padding="0">
            <DockPanel  Margin="0">
                <TextBox BorderBrush="Black" Name="txtNotes" Background="LightYellow" x:FieldModifier="public" IsReadOnly="false" IsEnabled="True"></TextBox>
            </DockPanel>
        </Border>
    </Popup>

</UserControl>

    /// <summary>
    /// PopUpNote.xaml の相互作用ロジック
    /// </summary>
    public partial class PopUpNote : UserControl
    {
        public PopUpNote()
        {
            InitializeComponent();

        }
          
        private bool _isOpen = false;
        public bool IsOpen
        {
            get { return _isOpen; }
            set { _isOpen = value; popupInfomation.IsOpen = _isOpen; }
        }

        #region 添付プロパティ

        #region Notes

        /// <summary>
        /// ヘッダに表示する文字列
        /// DataGridFilterTextColumnHeaderクラスのHeaderTextプロパティに値を渡すための添付プロパティ
        /// </summary>
        //public string Notes
        //{
        //    get { return base.GetValue(NotesProperty) as string; }
        //    set { base.SetValue(NotesProperty, value); }
        //}
        public string Notes
        {
            get { return (string)this.GetValue(PopUpNote.NotesProperty); }
            set { this.SetValue(PopUpNote.NotesProperty, value); }
        }
        public static  DependencyProperty NotesProperty = DependencyProperty.RegisterAttached(
            "Notes", typeof(string),
            typeof(PopUpNote), new PropertyMetadata(string.Empty,
                                                                                new PropertyChangedCallback((sender, e) =>
                                                                                {
                                                                                    (sender as PopUpNote).OnNotesPropertyChanged(sender, e);
                                                                                })));
        private void OnNotesPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue == null) { return; }
            this.txtNotes.Text = e.NewValue.ToString();
        }

        #endregion

        #endregion


<呼び出し側>
                    <!-- ***情報*** -->
                    <GridViewColumn Header="Info" Width="30" HeaderContainerStyle="{StaticResource GridViewColumnHeaderStyle}">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <!-- Marginで左右から-6pxとしないとHearder部のタイトルとラインの位置がずれる.GridViewの既知の不具合 -->
                                <Border BorderBrush="Black"  BorderThickness="0,0,0.5,0" Margin="-6,0,-6,0">
                                    <DockPanel  >
                                        <Image x:Name="popTriger" MouseEnter="popup_MouseEnter" Source="Images/postit.png" Visibility="{Binding Memo,Converter={StaticResource visibilityConverter}}"></Image>
                                        <csControls:PopUpNote x:Name="popup" Notes="{Binding Memo}" IsEnabled="True" >
                                        </csControls:PopUpNote>
                                    </DockPanel>
                                </Border>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

実装例



すごいなーと思ったけど、ポップアップの中のテキストボックスが触れない。
貼り付けはできるっぽいけど・・・。簡単に調べた限りじゃわからんかった。
まぁいいってことにして少し使ってみてやっぱり変更したくなったら修正するようにしようと思う。

ガントチャート作成日記

少しづつ作っていたガントチャートがとりあえず、データを保存できるレベルになったので一端公開。

最新バージョンをダウンロードするように常に通知されるとか、タスクのカラーを変更できそうに見えるとか、Excel出力とか、帳票出力っぽいものとかボタンはあるけど動かないし、そもそも実装するかも未定だけど・・・。

とりあえず普通のタスク管理はできるのと、表示タブからいくつかの単位でまとめ直して表示することができる。

こまめにCtrl+Sで上書き保存してもらって変な動きしたらもう一回開きなおしてもらえばそれなりに使えるかも。

インストール不要なんで試してもらう分にはそれなりにいいかも。

DownLoad URL(ZipFile、Portable)
 => https://sites.google.com/site/mmbloguserfile/wai-bufairu/ProjectManager_Beta_0.0.0.0.zip

解凍してProjectManager.exeを起動してもらえば試せます。

<サンプル>

 表示タブ⇒作業者一覧 を選択


 作業者ごとにグルーピング(日付を変更すると元データも変更される)


 メイン画面=>タスク一覧を選択するとExcelっぽい感じで使える。
 Excelからのコピーペーストもできる
 ※空白セルがあるとずれる不具合がある。Excel上で全角空白か半角空白を入れてから張り付けてもらえばうまくいく。




ガントチャート作成日記

イナズマ線の表示の不具合を解消した。


イナズマ線はCellごとにPointCollection型のプロパティに値を指定している。
開始点、中間点、終了点はそれぞれ高さや行の幅×進捗率で計算して保持している。

        /// <summary>
        /// イナズマ線を表示
        /// </summary>
        /// <param name="targetTask">イナズマ線を表示するタスク</param>
        /// <param name="showPairThunderLine">ペアタスクのイナズマ線を表示するかどうか</param>
        public void ShowThunderLine(GanttGridTask targetTask,bool showPairThunderLine)
        {
            if (GanttGridCells == null) { return; }
            foreach (var rowItem in this.GanttGridCells.Where(i => i.DataTemplateType == GanttCell.TemplateType.ThunderLineRow && i.Task.Id.ToString() == targetTask.Id.ToString()))
            {
                // 通常タスクのイナズマ線を描画
                if (this.ThunderLineVisible)
                {
                    if (targetTask.Name != "" || targetTask.Name != null) { rowItem.ThunderLine = _GetThunderLinePoint(targetTask); }
                    else { rowItem.ThunderLine = null; }
                }

            }
        }

        /// <summary>
        /// イナズマ線のPointCollecctionを取得
        /// </summary>
        /// <param name="targetTask">イナズマ線を作成するタスク</param>
        /// <returns>
        ///  
        ///  ・基準日付時点でタスクが完了していないものは進捗位置までの線を引く
        ///    ⇒ 進捗率が1(完了)以外で、終了日
        ///  ・基準日までに開始できていないものは線を引く
        ///    ⇒ 進捗率が1(完了)以外で、終了日
        /// 
        ///  ・タスクの進捗率が基準日より遅れている場合に線を引く
        ///    ⇒ タスクの進捗率が1(完了)以外で、基準日より前にタスクの終了予定日がある
        ///      タスクの進捗率が1(完了)以外で、基準日より開始予定日が小さい場合
        ///  ・タスクの進捗率が基準日より進んでいる場合に線を引く
        ///
        /// </returns>
        private PointCollection _GetThunderLinePoint(GanttGridTask targetTask)
        {
            TimeSpan ts = this.ThunderLineBaseDate - ProjectStartDay; ;
            PointCollection _ThunderLine = new PointCollection();

            if ((targetTask.ProgressRate != 1 && targetTask.EndDay < this.ThunderLineBaseDate)
                || ((targetTask.ProgressRate != 1) && this.ThunderLineBaseDate > targetTask.StartDay)
                || ((targetTask.ProgressRate != 0) && this.ThunderLineBaseDate < targetTask.EndDay))
            {
                double startPoint = targetTask.Cell.ColumnIndex * this.ColumnWidth;
                double durectionPoint = targetTask.Cell.ColumnSpan * this.ColumnWidth;
                if (targetTask.ProgressRate == 0) { durectionPoint = durectionPoint * 0.01; }
                else { durectionPoint = durectionPoint * targetTask.ProgressRate; }

                _ThunderLine.Add(new Point(ts.Days * this.ColumnWidth, 0));
                _ThunderLine.Add(new Point(startPoint + durectionPoint, this.RowHeight / 2));
                _ThunderLine.Add(new Point(ts.Days * this.ColumnWidth, this.RowHeight));
            }
            else
            {
                _ThunderLine.Add(new Point(ts.Days * this.ColumnWidth, 0));
                _ThunderLine.Add(new Point(ts.Days * this.ColumnWidth, this.RowHeight));
            }

            return _ThunderLine;

        }

表現するためには、XAMLにDataTemplateを宣言しておいて、DataTemplateSelecterで変換している。

        <!-- イナズマ線を描画するためのDataTemplate -->
        <DataTemplate x:Key="ThunderLineRowLineTypeDataTemplate" >
            <Grid>
                <Polyline Points="{Binding ThunderLine}"  Stroke="Red" StrokeThickness="1" />
            </Grid>
        </DataTemplate>

ガントチャートを表現するためにItemControlに独自に作成したGanttTaskCell型をバインドしている。どのようなDataTemplateを利用するかはTemplateSelecterで切り替えている。そうすることで曜日によって色を変えたり、色々することができる。

詳細は全部完成してからいつか書きたいとは思う。

ついでに、垂直スクロールだけではなく、左クリックを押しながらホイールすれば水平スクロールもするように修正してみた。結構便利な気がする。


ガントチャート作成日記

昨日悩んでいた交互色について、プロパティを追加してバインドすることで解決した。
別dllのコントロールに対して、バインディングするのは気が引けるが今更かと思い直してバインドして解決した。
(TreeListViewとGanntGridはそれぞれ別々のdll)


やはり交互色があるほうがわかりやすい。
次はイナズマ線の不具合を直しつつ、ガントチャートの初期位置の表示を当日にするように修正してみる。

ガントチャート作成日記

TreeListViewにずっと交互色を付けたいと思っていた。調べたところAlternationCountプロパティを設定すればいいらしい・・・。

<dll呼び出し側>
<tree:TreeListView Name="treeView" AllowsColumnReorder="True" ItemsSource="{Binding GanttGridViewModel.Tasks}"
                               ScrollViewer.HorizontalScrollBarVisibility="Visible"  Background="White" ScrollViewer.VerticalScrollBarVisibility="Visible"
                               VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling" AlternationCount="2">


<dllのGeneric.xamlを抜粋>
        <!-- コントロールテンプレートのトリガーを定義 -->
        <ControlTemplate.Triggers>
            <!-- 交互色を指定 -->
            <!-- AlternationCountで交互色のパターンを指定。色についてはTreeListViewのGeneric.xamlのItemsControl.AlternationIndexで指定 -->
            <!-- 奇数行 -->
            <Trigger Property="ItemsControl.AlternationIndex" Value="0">
                <Setter TargetName="Border" Property="Background" Value="Transparent"/>
            </Trigger>
            <!-- 偶数行-->
            <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                <Setter TargetName="Border" Property="Background" Value="#FFF0F0F0" />
            </Trigger>
         
        </ControlTemplate.Triggers>

    </ControlTemplate>


できた!!!

と思いきや、広げると・・・・


(゜-゜)・・・・。
やはりNo列で判定して書いたほうが確実かなぁ・・・。
ValueConverterを実装するほうが簡単かもしれないので次はそっちでやってみる。

ゴールは遠い・・・。


ガントチャート作成日記

スケジュール管理用のガントチャートのツールを作成しているときに独自に作成したTreeListViewで選択されている行に「▶」を出力したいと思ったのでいろいろいじってみた。

<修正前>
 <修正後>

思ったより簡単にできた。さすがWPF!!

Xaml抜粋

<TextBlock Text=" ▶" HorizontalAlignment="Left" VerticalAlignment="Center"  Margin="0,0,3,0" Visibility="{Binding IsSelected,Converter={StaticResource visibilityConverter},  RelativeSource={RelativeSource Mode=FindAncestor,AncestorType =TreeViewItem}}"/>

VisibilityConverter .cs
    /// <summary>
    /// ResizeRectangle.xamlで利用している
    /// リサイズ用のThumbコントロールの表示非表示を制御している
    /// </summary>
    public class VisibilityConverter : IValueConverter
    {
        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            bool b = (bool)value;
            return b ? Visibility.Visible : Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }

やっていることはTextBlockに「▶」の文字を入れておく。表示非表示の制御をバインディングとValueConverterインターフェイスを実装。

バインディングは、親のTreeViewItemのIsSelectedプロパティを取得し、取得したプロパティの値(bool型)をValueConveterでVisibilityの値(列挙体のVisibility)に変換している。

試しに使っているといろいろ気になることがあるので修正中。果たして完成はいつになるのやら。

Gridコントロールへのバインドについて

GridコントロールのRow、Column、RowSpan、ColumnSpanに対してバインディングを行いたいと思って色々調べていると下記のサイトを見つけた。

https://rachel53461.wordpress.com/2011/09/17/wpf-grids-rowcolumn-count-properties/

GridコントロールのRow、Column、RowSpan、ColumnSpanに対してバインディングを行えるので、Canvasコントロールほど自由度は高くなく、方眼紙を塗りつぶすようなイメージでコントロールの配置を動的に変更できる。
ピクロス的な感じ。

Rowプロパティにバインドすることでバインドされている側のプロパティの値を変えればコントロールの位置を自分で変えることができる。
設定画面を作ればユーザーに自由にコントロールを配置してもらうことができ、単票形式の帳票であれば結構簡単に作れそうな気がする。

<ちょっと手を加えた>
    public class GridHelpers
    {
        #region RowCount Property

        /// <summary>
        /// Adds the specified number of Rows to RowDefinitions.
        /// Default Height is Auto
        /// </summary>
        public static readonly DependencyProperty RowCountProperty =
            DependencyProperty.RegisterAttached(
                "RowCount", typeof(int), typeof(GridHelpers),
                new PropertyMetadata(-1, RowCountChanged));

        // Get
        public static int GetRowCount(DependencyObject obj)
        {
            return (int)obj.GetValue(RowCountProperty);
        }

        // Set
        public static void SetRowCount(DependencyObject obj, int value)
        {
            obj.SetValue(RowCountProperty, value);
        }

        // Change Event - Adds the Rows
        public static void RowCountChanged(
            DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (!(obj is Grid) || (int)e.NewValue < 0)
                return;

            Grid grid = (Grid)obj;
            grid.RowDefinitions.Clear();

            for (int i = 0; i < (int)e.NewValue; i++)
                grid.RowDefinitions.Add(
                    new RowDefinition() { Height = GridLength.Auto });

            SetStarRows(grid);

            // 行幅再設定
            SetRowHeight(grid);

        }

        #endregion

        #region ColumnCount Property

        /// <summary>
        /// Adds the specified number of Columns to ColumnDefinitions.
        /// Default Width is Auto
        /// </summary>
        public static readonly DependencyProperty ColumnCountProperty =
            DependencyProperty.RegisterAttached(
                "ColumnCount", typeof(int), typeof(GridHelpers),
                new PropertyMetadata(-1, ColumnCountChanged));

        // Get
        public static int GetColumnCount(DependencyObject obj)
        {
            return (int)obj.GetValue(ColumnCountProperty);
        }

        // Set
        public static void SetColumnCount(DependencyObject obj, int value)
        {
            obj.SetValue(ColumnCountProperty, value);
        }

        // Change Event - Add the Columns
        public static void ColumnCountChanged(
            DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (!(obj is Grid) || (int)e.NewValue < 0)
                return;

            Grid grid = (Grid)obj;
            grid.ColumnDefinitions.Clear();

            for (int i = 0; i < (int)e.NewValue; i++)
                grid.ColumnDefinitions.Add(
                    new ColumnDefinition() { Width = GridLength.Auto });

            SetStarColumns(grid);

            // カラム幅再設定
            SetColumnWidth(grid);

        }

        #endregion

        #region StarRows Property

        /// <summary>
        /// Makes the specified Row's Height equal to Star.
        /// Can set on multiple Rows
        /// </summary>
        public static readonly DependencyProperty StarRowsProperty =
            DependencyProperty.RegisterAttached(
                "StarRows", typeof(string), typeof(GridHelpers),
                new PropertyMetadata(string.Empty, StarRowsChanged));

        // Get
        public static string GetStarRows(DependencyObject obj)
        {
            return (string)obj.GetValue(StarRowsProperty);
        }

        // Set
        public static void SetStarRows(DependencyObject obj, string value)
        {
            obj.SetValue(StarRowsProperty, value);
        }

        // Change Event - Makes specified Row's Height equal to Star
        public static void StarRowsChanged(
            DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
                return;

            SetStarRows((Grid)obj);
        }

        #endregion

        #region StarColumns Property

        /// <summary>
        /// Makes the specified Column's Width equal to Star.
        /// Can set on multiple Columns
        /// </summary>
        public static readonly DependencyProperty StarColumnsProperty =
            DependencyProperty.RegisterAttached(
                "StarColumns", typeof(string), typeof(GridHelpers),
                new PropertyMetadata(string.Empty, StarColumnsChanged));

        // Get
        public static string GetStarColumns(DependencyObject obj)
        {
            return (string)obj.GetValue(StarColumnsProperty);
        }

        // Set
        public static void SetStarColumns(DependencyObject obj, string value)
        {
            obj.SetValue(StarColumnsProperty, value);
        }

        // Change Event - Makes specified Column's Width equal to Star
        public static void StarColumnsChanged(
            DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
                return;

            SetStarColumns((Grid)obj);
        }

        #endregion

        #region ColumnWidth Property

        /// <summary>
        /// カラム幅設定用独自プロパティ
        /// </summary>
        public static readonly DependencyProperty ColumnWidthProperty =
            DependencyProperty.RegisterAttached(
                "ColumnWidth", typeof(int), typeof(GridHelpers),
                new PropertyMetadata(0, ColumnWidthChanged));

        // 取得処理
        public static int GetColumnWidth(DependencyObject obj)
        {
            return (int)obj.GetValue(ColumnWidthProperty);
        }

        // 設定処理
        public static void SetColumnWidth(DependencyObject obj, string value)
        {
            obj.SetValue(ColumnWidthProperty, value);
        }

        // 値変更時処理
        public static void ColumnWidthChanged(
            DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
                return;

            SetColumnWidth((Grid)obj);
        }

        #endregion

        #region RowHeight Property
        /// <summary>
        /// カラム幅設定用独自プロパティ
        /// </summary>
        public static readonly DependencyProperty RowHeightProperty =
            DependencyProperty.RegisterAttached(
                "RowHeight", typeof(int), typeof(GridHelpers),
                new PropertyMetadata(0, RowHeightChanged));

        // 取得処理
        public static int GetRowHeight(DependencyObject obj)
        {
            return (int)obj.GetValue(RowHeightProperty);
        }

        // 設定処理
        public static void SetRowHeight(DependencyObject obj, string value)
        {
            obj.SetValue(RowHeightProperty, value);
        }

        // 値変更時処理
        public static void RowHeightChanged(
            DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (!(obj is Grid) || string.IsNullOrEmpty(e.NewValue.ToString()))
                return;
            SetRowHeight((Grid)obj);
        }

        #endregion


        private static void SetStarColumns(Grid grid)
        {
            string[] starColumns =
                GetStarColumns(grid).Split(',');

            for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
            {
                if (starColumns.Contains(i.ToString()))
                    grid.ColumnDefinitions[i].Width =
                        new GridLength(1, GridUnitType.Star);
            }
        }

        private static void SetStarRows(Grid grid)
        {
            string[] starRows =
                GetStarRows(grid).Split(',');

            for (int i = 0; i < grid.RowDefinitions.Count; i++)
            {
                if (starRows.Contains(i.ToString()))
                    grid.RowDefinitions[i].Height =
                        new GridLength(1, GridUnitType.Star);
            }
        }

        // カラム幅設定の独自プロパティ設定用
        private static void SetColumnWidth(Grid grid)
        {
            for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
            {
                grid.ColumnDefinitions[i].Width = new GridLength(GetColumnWidth(grid));
            }
        }

        // 行幅設定の独自プロパティ設定用
        private static void SetRowHeight(Grid grid)
        {
            for (int i = 0; i < grid.RowDefinitions.Count; i++)
            {
                grid.RowDefinitions[i].Height = new GridLength(GetRowHeight(grid));
            }
        }


<使い方>
<ScrollViewer Name="scrollDetail" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Row="1" ScrollViewer.IsDeferredScrollingEnabled="True"
  ScrollViewer.PanningMode="VerticalOnly" >
<ItemsControl Name="ic" ItemsSource="{Binding Source={StaticResource GanttGridCellsSource}}" VirtualizingPanel.IsVirtualizing="True" 
  VirtualizingPanel.VirtualizationMode="Recycling" ItemTemplateSelector="{StaticResource ItemViewModelTemplateSelector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid Name="gridCalender" helper:GridHelpers.RowCount="{Binding RowCount}" helper:GridHelpers.ColumnCount="{Binding ColumnCount}"
  helper:GridHelpers.ColumnWidth="{Binding ColumnWidth}" helper:GridHelpers.RowHeight="{Binding RowHeight}"
  PreviewMouseDown="gridCalender_PreviewMouseDown" Margin="0,0,0,25">
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Column" Value="{Binding Path=ColumnIndex}"/>
<Setter Property="Grid.Row" Value="{Binding Path=RowIndex}"/>
<Setter Property="Grid.ColumnSpan" Value="{Binding Path=ColumnSpan}"/>
<Setter Property="Grid.RowSpan" Value="{Binding Path=RowSpan}"/>
<Setter Property="Grid.ZIndex" Value="{Binding Path=ZIndex}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</ScrollViewer>

TreeListViewについて

TreeViewは1つの列しか表示できないので、複数の列を表示できるようなコントロールを探していたところ、下記のサイトにサンプルを見つけた。
(URL:https://www.codeproject.com/Articles/24973/TreeListView)

このサンプルをもとにdllを作成して、下記のような表現ができるようになった。


ガントチャートを表現するための独自コントロールと合わせることで、スケジュール管理用のソフトを作成できそう・・・!!

というわけで下記のようなものを作ってみた。

それっぽい感じにはなってきたが、使いにくいのでもうちょっと手を入れたい・・・。
つーか、タスク数が多くなると動きが遅くなる・・・。
500タスク程度は軽く扱えるように作りたい。できれば1000タスク。
作業者ごとの画面とかも表示したりしたいし・・・。

うーん、夢が広がってきて果たしていついったん完成とすることができるかなぁ。



FilterDataGrid作成(5)ソース:FilterDataGrid

今日はプロジェクトの最後のソースであるFilterDataGridについて
FilterDataGridはDataGridを継承したクラスで、フィルタ処理に必要ないくつかのクラスやプロパティを追加したもの。
複数のフィルタ条件を格納するためのGroupFilterなどを定義している。
これまで公開したソースを合わせるとExcelのようなフィルタ処理が可能なDataGridができる。
※Excelのフィルタ機能に近づけるにはもっと機能を追加する必要はあるが・・・。

ちなみにフィルタだけではなくDataGridTextColumnが標準で実装しているソート機能もそのまま利用することができる。

FilterDataGrid.cs
    public class FilterDataGrid : DataGrid
    {
        // 最後にフィルタをかけた列のフィルタ対象のチェックボックスリストを作成するときはViewを利用しないようにする
        public DataGridFilterTextColumn LastFilterColumn = null;
        public List<DataGridFilterTextColumn> FilteredColumns = new List<DataGridFilterTextColumn>();

        public GroupFilter _filters = new GroupFilter();
        public GroupFilter Filters { get { return _filters; } set { _filters = value; } }

        /// <summary>
        /// 複数のフィルタを適用するためのクラス
        /// Filterを適用するときに変数に格納しておく
        /// </summary>
        public class GroupFilter
        {
            private List<Predicate<object>> _filters;
            public Predicate<object> Filter { get; private set; }
            public GroupFilter()
            {
                _filters = new List<Predicate<object>>();
                Filter = InternalFilter;
            }
            private Dictionary<DataGridFilterTextColumn, Predicate<object>> ColumnPairPredicateList = new Dictionary<DataGridFilterTextColumn, Predicate<object>>();

            private bool InternalFilter(object o)
            {
                foreach (var filter in _filters)
                {
                    if (!filter(o))
                    {
                        return false;
                    }
                }

                return true;
            }

            public void AddFilter(Predicate<object> filter, DataGridFilterTextColumn column)
            {
                _filters.Add(filter);
                ColumnPairPredicateList.Add(column, filter);
            }

            public void RemoveFilterByColumn(DataGridFilterTextColumn column)
            {
                if (ColumnPairPredicateList.ContainsKey(column))
                {
                    RemoveFilter(ColumnPairPredicateList[column]);
                    ColumnPairPredicateList.Remove(column);
                }
            }

            private void RemoveFilter(Predicate<object> filter)
            {
                if (_filters.Contains(filter))
                {
                    _filters.Remove(filter);

                }
            }
        }

今日はここまで。

FilterDataGrid作成(4)ソース:CheckedListItem

DataGridFilterTextColumnHeaderで利用しているチェックボックスリストを作成

DataGridFilterTextColumnHeaderのPopupコントロールがクリックされたらDataGridFilterTextColumnにバインドされているクラスの情報を取得し、バインドされている値をObservableCollectionに格納し、表示する。

次回はFilterDataGrid.csを公開し、全体の説明を行う予定。

    public class CheckedListItem<T> : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private bool isChecked;
        private T item;

        public CheckedListItem()
        { }

        public CheckedListItem(T item, bool isChecked = false)
        {
            this.item = item;
            this.isChecked = isChecked;
        }

        public T Item
        {
            get { return item; }
            set
            {
                item = value;
                if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
            }
        }


        public bool IsChecked
        {
            get { return isChecked; }
            set
            {
                isChecked = value;
                if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
            }
        }
    }

今日はここまで。

FilterDataGrid作成(3)ソース:DataGridFilterTextColumn

前回作成したDataGridFilterTextColumnを作成していく。
DataGridTextColumnのHeaderプロパティはオブジェクト型で中身を様々にカスタマイズできる。今回は、Headerプロパティに前回作成したDataGridFilterTextColumnHeaderを設定するようにする。
ヘッダ部に表示する列のタイトルについてはDataGridFilterTextColumnHeaderで添付プロパティを公開し、その値を表示するようにする。(HeaderTextプロパティ)

DataGridTextColumn.xaml

<DataGridTextColumn x:Class="FilterDataGridSample.DataGridFilterTextColumn"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:FilterDataGridSample"
             mc:Ignorable="d" >

</DataGridTextColumn>

DataGridTextColumn.xaml.cs
    /// <summary>
    /// DataGridFilterTextColumn.xaml の相互作用ロジック
    /// </summary>
    public partial class DataGridFilterTextColumn : DataGridTextColumn
    {
        DataGridFilterTextColumnHeader filterHeader;

        public DataGridFilterTextColumn()
        {
            InitializeComponent();

            filterHeader= new DataGridFilterTextColumnHeader();
            filterHeader.OwnerColumn = this;
            this.Header = filterHeader;
        }

        #region 添付プロパティ
     
        #region HeaderText

        /// <summary>
        /// ヘッダに表示する文字列
        /// DataGridFilterTextColumnHeaderクラスのHeaderTextプロパティに値を渡すための添付プロパティ
        /// </summary>
        public string HeaderText
        {
            get { return base.GetValue(HeaderTextProperty) as string; }
            set { base.SetValue(HeaderTextProperty, value); }
        }
        public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.RegisterAttached(
            "HeaderText", typeof(string),
            typeof(DataGridFilterTextColumn), new PropertyMetadata(string.Empty,
                                                                                new PropertyChangedCallback((sender, e) =>
                                                                                {
                                                                                    (sender as DataGridFilterTextColumn).OnHeaderTextPropertyChanged(sender, e);
                                                                                })));
        private void OnHeaderTextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            this.filterHeader.HeaderText = e.NewValue.ToString();
        }

        #endregion

        #endregion

        #region メソッド

        /// <summary>
        /// DataGridFilterTextColumnHeaderのチェックボックスリストを表示するためのリストを取得
        /// </summary>
        /// <returns>
        /// どのようなクラスがバインドされるかわからないので、リフレクションを利用して文字列からクラス名や
        /// PropertyInfoを取得して、値を取得するようにする
        /// </returns>
        public ObservableCollection<CheckedListItem<string>> GetCheckBoxItem()
        {
            FilterDataGrid dgOwner = (FilterDataGrid)this.DataGridOwner;

            // CurrentItemからバインドされているクラス名を取得するためにセルにフォーカスを設定
            _SetCurrentItemIfIsNullCurrentItem();

            // 文字列から型を取得し、型からバインドされているプロパティ名を取得
            if (dgOwner.CurrentItem == null) { return null; };
            string BindingName = _GetBindingPath(this);
            Type BindingType = Type.GetType(dgOwner.CurrentItem.GetType().FullName.ToString());
            if (BindingType == null) { return null; }
            PropertyInfo BindingPropertyInfo = BindingType.GetProperty(BindingName);

            // 戻り値を作成
            ObservableCollection<CheckedListItem<string>> checkedList = new ObservableCollection<CheckedListItem<string>>();
            var dgItemsSource = dgOwner.ItemsSource;
            List<string> lstDistinct = new List<string>();

            // dgOwner.ItemsSourceでは、フィルタした後の結果をさらに絞り込むときにフィルタする前の結果と変わらなくなるためViewの値を取得してループする
            // Excelの動きと合わせるため、最後にフィルタした列はすべての選択肢を表示し、追加でフィルタした列はフィルタした後の結果に対して行う
            IEnumerable enumerableList;
            if (dgOwner.FilteredColumns.Contains(this)){enumerableList = (IEnumerable)dgOwner.ItemsSource;}
            else{ enumerableList = (IEnumerable)CollectionViewSource.GetDefaultView(dgOwner.ItemsSource);}
            foreach (var bindingItem in enumerableList)
            {
                // 対象とする型が一致しているかチェックする
                if (BindingType.FullName != bindingItem.GetType().FullName) { break; }
                var value = BindingPropertyInfo.GetValue(bindingItem);
                if (value == null) { break; }
                if (lstDistinct.Contains(value.ToString()) == false)
                {
                    checkedList.Add(new CheckedListItem<string> { Item = value.ToString(), IsChecked = true });
                    lstDistinct.Add(value.ToString());
                }
            }

            // 後処理
            dgItemsSource = null;
            dgOwner = null;

            return checkedList;
        }

        /// <summary>
        /// CurrentItemが存在しない場合は、CurrentItemを設定する
        /// </summary>
        private void _SetCurrentItemIfIsNullCurrentItem()
        {
            FilterDataGrid dgOwner = (FilterDataGrid)this.DataGridOwner;
            // NewPreceholderがCurrentItemになっていることがある
            //if (dgOwner.CurrentItem == null)
            //{
            int mincount = 0;
            if (dgOwner.CanUserAddRows == true) { mincount = 1; }
            if (dgOwner.Items.Count == mincount) { return; }
            DataGridCellInfo cellInfo = new DataGridCellInfo(dgOwner.Items[0], dgOwner.Columns[0]);
            dgOwner.Focus();
            dgOwner.SelectedIndex = 0;
            dgOwner.CurrentCell = cellInfo;
            //}
            dgOwner = null;
        }

        /// <summary>
        /// フィルタ処理を実行
        /// DataGridFilterTextColumnHeaderのチェックボックスリストのチェック状態が変更された時に呼び出される
        /// </summary>
        /// <returns>
        /// DataGridのItemsSourceにバインドされている値のDefaultViewを取得し、Viewのリアルたむフィルタを実行
        /// どのようなクラスがバインドされるかわからないので、リフレクションを利用して文字列からクラス名や
        /// PropertyInfoを取得して、値を取得するようにする
        /// </returns>
        public void ApplayFilter(List<string> lstFilterValues)
        {
            // CurrentItemからバインドされているクラス名を取得するためにセルにフォーカスを設定
            _SetCurrentItemIfIsNullCurrentItem();

            // 文字列から型を取得し、型からバインドされているプロパティ名を取得
            string BindingName = _GetBindingPath(this);
            FilterDataGrid dgOwner = (FilterDataGrid)this.DataGridOwner;
            Type BindingType = Type.GetType(dgOwner.CurrentItem.GetType().FullName.ToString());
            if (BindingType == null) { return; }
            PropertyInfo BindingPropertyInfo = BindingType.GetProperty(BindingName);
            if (BindingPropertyInfo == null) { return; }

            // 引数として渡された値を利用してフィルタ条件を作成し、DataGridのItemsSourceにバインドされているリストのViewのフィルタ条件と指定する
            var CustomFilter = new Predicate<object>(item => lstFilterValues.Contains(BindingPropertyInfo.GetValue(item).ToString()));
            var ItemSourceView = CollectionViewSource.GetDefaultView(dgOwner.ItemsSource);
            // 複数のフィルタに適用するためFilterDataGridにフィルタ条件を追加
            //ItemSourceView.Filter = CustomFilter;
            dgOwner.Filters.RemoveFilterByColumn(this);
            dgOwner.FilteredColumns.Remove(this);
            dgOwner.Filters.AddFilter(CustomFilter, this);
            dgOwner.FilteredColumns.Add(this);
            dgOwner.LastFilterColumn = this;
            ItemSourceView.Filter = dgOwner.Filters.Filter;

            // 後処理
            dgOwner = null;
            CustomFilter = null;
            ItemSourceView = null;
        }

        public FilterDataGrid GetDataGridOwner()
        {
            return (FilterDataGrid)this.DataGridOwner;
        }

        public void RemoveFilter()
        {
            FilterDataGrid dgOwner = (FilterDataGrid)this.DataGridOwner;
            dgOwner.FilteredColumns.Remove(this);
            dgOwner.Filters.RemoveFilterByColumn(this);
        }

        /// <summary>
        /// 指定されたDataGridColumnのプロパティパスを取得
        /// ソートメンバーパスが設定されていなければ、バインドされているプロパティ名を取得
        /// </summary>
        /// <param name="column">DataGridColumnオブジェクト</param>
        /// <returns>バインディングされているプロパティのパス</returns>
        private string _GetBindingPath(DataGridColumn column)
        {
            var path = column.SortMemberPath;

            if (!string.IsNullOrWhiteSpace(path)) { return path; }

            if (column is DataGridBoundColumn)
            {
                var binding = (column as DataGridBoundColumn).Binding as Binding;
                if (binding != null)
                {

                    if (!string.IsNullOrEmpty(binding.XPath)) { return binding.XPath; }
                    else if (binding.Path != null) { return binding.Path.Path; }
                }
            }
            else if (column is DataGridComboBoxColumn)
            {
                return ((DataGridComboBoxColumn)column).SelectedValuePath;
            }

            column = null;
            return path;
        }
        #endregion
     
    }

どのようなクラスがバインドされるかわからないのでリフレクションで文字列からクラスやプロパティを取得している。
dllとして公開するときには「Type.GetType」を利用してもアセンブリが異なるのでリフレクションで値を取得できない。
この場合は、下記のようにアセンブリを指定してリフレクションを行う必要があるので注意する。

        /// <summary>
        /// Type.GetTypeはmscorlib.dll、実行中のassembly内にあるものでしか動かないため、
        /// dllを別プロジェクトで参照しているときには実行できない
        /// そのため、GetAssembliesメソッドで参照しているすべてのassemblyを取得し、
        /// そのassemblyに対してGetTypeメソッドを実行する
        /// </summary>
        /// <param name="typeName"></param>
        /// <returns></returns>
        public static Type GetType(string typeName)
        {
            var type = Type.GetType(typeName);
            if (type != null) return type;
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                type = assembly.GetType(typeName);
                if (type != null)
                    return type;
            }
            return null;
        }

今日はここまで。

FilterDataGrid作成(2)ソース:DataGridFilterTextColumnHeader

まずは単純にDataGridに利用できるフィルタ機能の付いたDataGridColumnを作成する。

<やりたいこと>
 Excelみたいなフィルタ機能を持ったDataGridColumnを作りたい

<どうするか>
 Excelのように「▼」マークを押してフィルタ処理を行うためにはDataGridColumnのHeaderプロパティがクリックされたらポップアップで選択肢を表示してフィルタする

DataGridColumnはObject型のHeaderプロパティを持つので、Headerプロパティに独自で作成した「DataGridFilterTextColumnHeader」クラスを指定する。

DataGridFilterTextColumnHeaderクラスの役割はクリックされたらチェックボックスのあるリストを表示する。表示する内容は、DataGridFilterTextColumnにバインドされている値を候補とする。

 ・DataGridTextColumnを継承したフィルタ機能の付いたDataGridFilterTextColumnクラスを作成。
    1.UserControlを追加

    2.「UserControl」を「DataGridTextColumn」に変更し、エラーの発生する<Grid>タグや「d:DesignHeight」「d:DesignWidth」を削除する

    3.完成

    
    4.「DataGridFilterTextColumn.xaml.cs」を開いて、継承元のクラス名を修正

    /// <summary>
    /// DataGridFilterTextColumn.xaml の相互作用ロジック
    /// </summary>
    public partial class DataGridFilterTextColumn : UserControl
    {
      ↓
    /// <summary>
    /// DataGridFilterTextColumn.xaml の相互作用ロジック
    /// </summary>
    public partial class DataGridFilterTextColumn : DataGridTextColumn
    {

 ・DataGridTextColumnのHeaderプロパティに指定するためのUserControlを作成する

    1.UserControlを追加
    2.「Grid」「d:DesignHeight」「d:DesignWidth」を削除する
    3.コンテンツ部にHeaderに表示したい内容を作成する。

  ソース:DataGridFilterTextColumnHeader.xaml
<UserControl x:Class="FilterDataGridSample.DataGridFilterTextColumnHeader"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:FilterDataGridSample"
             mc:Ignorable="d" >
    <StackPanel Orientation="Horizontal">
        <TextBlock x:Name="tblHeaderText" Text="{Binding HeaderText}"/>
        <Button Name="btnFilter" Margin="3,0,0,0" >
            <Button.Template>
                <ControlTemplate>
                    <Image Source="{Binding ImagePath}" Width="10" Height="10" />
                </ControlTemplate>
            </Button.Template>
        </Button>
        
        <!--フィルタ内容を選択するためのPopUpコントロール-->
        <Popup Name="popupFilter" Placement="Bottom" PlacementTarget="{Binding ElementName=btnFilter}" StaysOpen="False" Width="200" >
            <Border Background="White" BorderBrush="Gray" BorderThickness="1,1,1,1">
                <StackPanel Margin="2,2,2,2">
                    <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                        <Button Margin="0,0,0,0" Name="btnSelectAll" >
                            全選択
                        </Button>
                        <Button Margin="5,0,0,0" Name="btnUnselectAll">
                            全解除
                        </Button>
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                        <TextBox Name="txtFilter" Width="150" Text="{Binding FilterText,UpdateSourceTrigger=PropertyChanged}"></TextBox>
                    </StackPanel>
                    <DockPanel  Margin="0" Height="75">
                        <ListBox x:Name="lstCountries" BorderThickness="1" ItemsSource="{Binding FilterList}">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <CheckBox IsChecked="{Binding IsChecked}" Content="{Binding Item}" Checked="ApplyFilters" Unchecked="ApplyFilters" />
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </DockPanel>

                </StackPanel>
            </Border>
        </Popup>
    </StackPanel>
</UserControl>

  ソース:DataGridFilterTextColumnHeader.xaml.cs
    /// <summary>
    /// DataGridFilterTextColumnHeader.xaml の相互作用ロジック
    /// </summary>
    public partial class DataGridFilterTextColumnHeader : UserControl
    {
        public ObservableCollection<CheckedListItem<string>> customerFilters = new ObservableCollection<CheckedListItem<string>>();
        public DataGridFilterTextColumn OwnerColumn;
        public MainViewModel ViewModel = new MainViewModel();
        
        public DataGridFilterTextColumnHeader()
        {
            InitializeComponent();

            btnFilter.Click += btnFilter_Click;
            btnSelectAll.Click += btnSelectAll_Click;
            btnUnselectAll.Click += btnUnselectAll_Click;
            popupFilter.Closed += popupFilter_Closed;
            ViewModel.OwnerColumn = OwnerColumn;
            this.DataContext = ViewModel;
            
        }
        
        #region 添付プロパティ

        #region HeaderText

        /// <summary>
        /// ヘッダに表示する文字列
        /// </summary>
        public string HeaderText
        {
            get { return base.GetValue(HeaderTextProperty) as string; }
            set { base.SetValue(HeaderTextProperty, value); }
        }

        // RegisterAttachedメソッドを使って添付プロパティを作成する
        public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.RegisterAttached(
            "HeaderText", typeof(string),
            typeof(DataGridFilterTextColumnHeader), new PropertyMetadata(string.Empty,
                                                                                new PropertyChangedCallback((sender, e) =>
                                                                                {
                                                                                    (sender as DataGridFilterTextColumnHeader).OnHeaderTextPropertyChanged(sender, e);
                                                                                })));
        private void OnHeaderTextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
             this.tblHeaderText.Text = e.NewValue.ToString();
        }
        
        #endregion

        #endregion

        /// <summary>
        /// フィルタ条件の画面を表示
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnFilter_Click(object sender, RoutedEventArgs e)
        {
            popupFilter.IsOpen = true;

            // ▽ フィルタ対象の追加・削除があるので現在の設定値を引き継ぐ形にする
            ObservableCollection<CheckedListItem<string>> _filterList = OwnerColumn.GetCheckBoxItem();
            if (_filterList == null) { return; }
            foreach (CheckedListItem<string> filter in _filterList)
            {
                CheckedListItem<string> _i = customerFilters.Where(c => c.Item == filter.Item).FirstOrDefault();
                if (_i != null) { filter.IsChecked = _i.IsChecked; };
            }
            customerFilters = _filterList;
            this.ViewModel.FilterList = customerFilters;
        }

        /// <summary>
        /// フィルタ処理を適用
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ApplyFilters(object sender, RoutedEventArgs e)
        {

            ViewModel.OwnerColumn = OwnerColumn;
            List<string> filter = customerFilters.Where(c => c.IsChecked == true).Select(c => c.Item).ToList();
            // フィルタ処理を呼び出す
            OwnerColumn.ApplayFilter(filter);
            if (filter.Count== customerFilters.Count) {
                // フィルタ処理対象文字列とチェックボックスの項目の数が同じ場合はフィルタからなくす
                ViewModel.OwnerColumn.RemoveFilter();
            }
            ViewModel.OnPropertyChanged_ImagePath();

        }

        /// <summary>
        /// 全選択処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSelectAll_Click(object sender, RoutedEventArgs e)
        {
            this.ViewModel.FilterText = "";
            foreach (CheckedListItem<string> item in customerFilters)
            {
                item.IsChecked = true;
            }
        }

        /// <summary>
        /// 選択解除処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnUnselectAll_Click(object sender, RoutedEventArgs e)
        {
            this.ViewModel.FilterText = "";
            foreach (CheckedListItem<string> item in customerFilters)
            {
                item.IsChecked = false;
            }
        }

        /// <summary>
        /// popupコントロールが閉じた時にFilterTextを空白にする
        /// FilterTextを指定して自動でチェックボックスリストのチェックが入った後に
        /// さらにチェックボックスリストでフィルタ条件を指定し、その後PopUpコントロールを開くと、手動で外したものがチェックされた状態で
        /// 表示されてしまう
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void popupFilter_Closed(object sender, EventArgs e)
        {
            this.ViewModel.FilterText = "";
        }

        #region ViewModel

        /// <summary>
        /// ViewModel
        /// </summary>
        public class MainViewModel : INotifyPropertyChanged
        {
                public DataGridFilterTextColumn OwnerColumn;
         
            #region INotifyPropertyChangedインターフェイスイベント実装
            public event PropertyChangedEventHandler PropertyChanged;
            private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            #endregion

            #region プロパティ
            private string _headerText = "";
            /// <summary>
            /// ヘッダに表示する文字列
            /// </summary>
            public string HeaderText
            {
                get { return _headerText; }
                set
                {
                    _headerText = value;
                    NotifyPropertyChanged();
                }
            }

            private string _filterText = "";
            /// <summary>
            /// CheckBoxリストボックスのフィルタ条件
            /// </summary>
            public string FilterText
            {
                get { return _filterText; }
                set
                {
                    _filterText = value;
                    NotifyPropertyChanged();
                    if (_filterText != "") { OnPropertyChanged_FilterList(); }
                }
            }

            /// <summary>
            /// 現在適用されているフィルタ状態をわかりやすくするためのイメージパス
            /// フィルタ処理前とフィルタ処理適用中とでイメージを分けることで見やすくしている
            /// </summary>
            public string ImagePath
            {
                get {
                    string path = @".\Images\filter.png";
                    if (OwnerColumn != null)
                    {
                        if (OwnerColumn.GetDataGridOwner().FilteredColumns.Contains(OwnerColumn)) { path = @".\Images\filtered.png"; }
                    }
                    return path;
                }
            }


            private ObservableCollection<CheckedListItem<string>> _filterList = new ObservableCollection<CheckedListItem<string>>();
            /// <summary>
            /// チェックリストボックスにバインドするプロパティ
            /// コントロールのDataTemplateにバインドするのでチェックボックスが自動生成される
            /// FilterTextプロパティが指定されている場合は一致するものをIsCheckedプロパティをTrueに指定
            /// </summary>
            public ObservableCollection<CheckedListItem<string>> FilterList
            {
                get
                {
                    if (this.FilterText.Trim() != "")
                    {
                        // FilterTextプロパティに指定されている文字列を含むかどうかの結果をチェックボックスの初期値にする
                        foreach (var filterItem in _filterList) { filterItem.IsChecked = filterItem.Item.Contains(this.FilterText); }
                        return _filterList;
                    }
                    else { return _filterList; }
                }
                set
                {
                    _filterList = value;
                    NotifyPropertyChanged();
                }
            }

            #endregion

            #region メソッド
            /// <summary>
            /// 外部からImagePathプロパティの変更通知を発生
            /// </summary>
            public void OnPropertyChanged_ImagePath()
            {
                NotifyPropertyChanged(nameof(MainViewModel.ImagePath));
            }

            /// <summary>
            /// 外部からFilterListプロパティの変更通知を発生
            /// </summary>
            public void OnPropertyChanged_FilterList()
            {
                NotifyPropertyChanged(nameof(MainViewModel.FilterList));
            }
            #endregion
        }
        #endregion

    }

今日はここまで。

PowerShellでEdgeを自動化(インストール不要。参考:郵便追跡サービス自動操作)

1.経緯について  RPAのソフトをインストールできないので、これまでVBSでCreateObjectでブラウザの自動操作をすることがたまにあった。 ※いざというときの手札として持っているだけで安心感が段違い  見た目上IEがインストールされていなくても、CreateObject...