もっと詳しく

今回は、CSVファイルの読み込みとグラフ化をテーマに取り上げたいと思います。

「CSVファイルの読み込み」⇒「DataGridで表示」⇒「グラフの表示」という一連の操作をC#とWPFを使ってプログラムしています。

それぞれの処理は関数化しているので、必要な個所をコピペして使って頂いたり、丸ごとコピーして必要部分を改修するのも比較的しやすいと思います。

興味のある方は、是非この記事をご一読ください。

サンプルプログラムの概要

今回のサンプルプログラムの動作は以下の様になっています。

任意のCSVファイルをDataGrid(一覧表示)部分にドラッグ&ドロップして頂くと、その内容がDataGridに表示されます。

あとは、任意のカラムをダブルクリックしていただければ、グラフが描画できるようになっています。

起動時は一覧の左端のデータがX軸として使われていますが、画面左上のドロップダウンリストで任意のカラムに変更することができます。

区切り文字はカンマかタブ、文字コードはSHIFT-JISかUTF-8が選択できるようにもなっています。

プログラムの構造

プログラムの構造は以下の通り5つのメソッドで構成されています。

コンストラクタでは EnableDragDropメソッドを、DataGridへのドラッグ&ドロップ処理はLoadCsvメソッドを呼ぶだけと非常にシンプルです。

DataGridのダブルクリック時に呼ばれる処理が一番複雑ですが、やっていることはクリックされたカラムを特定し、そのカラムのデータをDrawLineに渡しているだけです。

グラフを描画するためには、補助線の有無などレイアウトの体裁を整える必要があるので、その処理はChartInitというメソッドとして外出ししていて、DataLineからChartInitを呼び出すようにしています。

ソースコードをビルドする上での注意点

グラフ描画コントロールは手軽さを優先し、Visual Studio 標準で備わっている MSChartコントロールを使うことにしました。

そこで、以前、こちらの記事で解説したグラフ描画クラスのソースから、ChartInit と DrawLine の2つのメソッドを抜粋して使っています。

MSChartはWPFでそのまま使えないので、WindowsFromsHost 経由で MSCart を呼び出す必要があるのですが、そのためには次の3つのアセンブリを参照設定する必要があります。

  • System.Windows.Forms
  • System.Windows.Forms.DataVisualization
  • WindowsFormsIntegration

下図を参考にして、ソリューションエクスプローラーからアセンブリを参照してください。

ソースコード一式

今回は ChartFromCsv という名前でソリューションを作成しています。

MainWindowが1つだけの簡単なプログラムとなっています。

XAMLのソース

XAMLのソースを読み解くための前提知識として、どのようなコントロールがあるかについて押さえておきたいと思います。

吹き出しに、画面上の機能とコントロールに付けた名前を表記していますので、まずこれを理解してからソースを読むと、より分かりやすいかと思います。

以下がXAMLのソースコードです。

<Window x:Class="ChartFromCsv.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:wfc="clr-namespace:System.Windows.Forms.DataVisualization.Charting;assembly=System.Windows.Forms.DataVisualization"
        xmlns:local="clr-namespace:ChartFromCsv"
        mc:Ignorable="d"
 
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="29"/>
            <RowDefinition Height="223*"/>
            <RowDefinition Height="5"/>
            <RowDefinition Height="177*"/>
        </Grid.RowDefinitions>
        <WindowsFormsHost Grid.Row="1" >
            <wfc:Chart x:Name="uxChart"/>
        </WindowsFormsHost>
        <GridSplitter Grid.Row="2" HorizontalAlignment="Stretch"/>
        <DataGrid Grid.Row="3" x:Name="uxDataGrid" SelectionMode="Single" IsReadOnly="True" AlternatingRowBackground="Aqua" ItemsSource="{Binding}" MouseDoubleClick="uxDataGrid_MouseDoubleClick" />
        <StackPanel Orientation="Horizontal">
            <Label Content="X軸" HorizontalAlignment="Left" VerticalAlignment="Center" Height="26" Width="29"/>
            <ComboBox x:Name="uxColumnX" HorizontalAlignment="Left" VerticalAlignment="Center" Width="120" Height="22"/>
            <Label Content="区切り文字" HorizontalAlignment="Left" VerticalAlignment="Center" Height="26" Width="70"/>
            <ComboBox x:Name="uxSplitChar" HorizontalAlignment="Left"  SelectedIndex="0"  VerticalAlignment="Center" Width="120" Height="22">
                <ComboBoxItem>カンマ</ComboBoxItem>
                <ComboBoxItem>タブ</ComboBoxItem>
            </ComboBox>
            <Label Content="文字コード" HorizontalAlignment="Left" VerticalAlignment="Center" Height="26" Width="70"/>
            <ComboBox x:Name="uxCharCode" HorizontalAlignment="Left" SelectedIndex="0" VerticalAlignment="Center" Width="120" Height="22">
                <ComboBoxItem>shift-jis</ComboBoxItem>
                <ComboBoxItem>utf-8</ComboBoxItem>
            </ComboBox>
        </StackPanel>
    </Grid>
</Window>

C#のソースコード

以下はC#のソースコードになります。

ソースコードの量は少し多いかもしれませんが、関数化されていますので、1つ1つは単純です。

主な処理はDataGridのダブルクリック処理なので、uxDataGrid_MouseDoubleClick を見ていただくと、どのような処理を行っているかが分かるかと思います。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Forms.DataVisualization.Charting;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;

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

            //DataGridをドラッグ&ドロップ対応にする
            EnableDragDrop(uxDataGrid);
        }

        /// <summary>
        /// DataGridのマウスダブルクリックのイベントハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void uxDataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            //クリックされたセル位置を取得
            var (row, col) = ClickCellIndex(uxDataGrid, e.GetPosition(uxDataGrid));

            //カラム位置が0以上なら、グラフを描画
            if (col > 0 && uxDataGrid.DataContext != null)
            {
                //DataGrid からDataTableを取り出す
                var dt = (DataTable)uxDataGrid.DataContext;

                //選択されているX軸のカラム番号を取得
                int column_x = (uxColumnX.SelectedIndex < 0) ? 0 : uxColumnX.SelectedIndex;

                //Xの値を取得(左端のカラムがXのデータとして扱う)
                double[] xs = dt.AsEnumerable().Select(i => double.TryParse(i[column_x].ToString(), out double val) ? val : double.NaN).ToArray();
                //Yの値を取得(クリックしたカラムの値)
                double[] ys = dt.AsEnumerable().Select(i => double.TryParse(i[col].ToString(), out double val) ? val : double.NaN).ToArray();

                //グラフ描画
                DrawLine(uxChart, "折線", xs, ys);
            }
        }

        /// <summary>
        /// 折れ線グラフ(単独)
        /// </summary>
        /// <param name="chart"></param>
        /// <param name="title"></param>
        /// <param name="xs"></param>
        /// <param name="ys"></param>
        private void DrawLine(Chart chart, string title, double[] xs, double[] ys)
        {
            ChartInit(chart, title);

            Series seri = new Series() { ChartType = SeriesChartType.Line, IsVisibleInLegend = false };
            Enumerable.Range(0, ys.Length).Select(i => seri.Points.AddXY(xs[i], ys[i])).ToArray();

            chart.Series.Add(seri);
        }


        /// <summary>
        /// チャートの初期化
        /// </summary>
        /// <param name="chart"></param>
        private void ChartInit(Chart chart, string title, bool Zoom = true)
        {
            //タイトル/エリア/シリーズのクリア
            chart.Titles.Clear();
            chart.ChartAreas.Clear();
            chart.Series.Clear();
            chart.Legends.Clear();

            //タイトルの設定
            chart.Titles.Add(title);

            //凡例表示エリアの登録
            chart.Legends.Add("");

            //チャートエリアの生成と登録
            var area = new ChartArea();
            chart.ChartAreas.Add(area);

            //X軸とY軸のオブジェクトを取得
            var axis_x = area.AxisX;
            var axis_y = area.AxisY;

            //X軸の補助線を設定
            axis_x.MajorGrid.LineColor = System.Drawing.Color.LightGray;
            axis_x.MinorGrid.LineColor = System.Drawing.Color.LightGray;
            axis_x.MinorGrid.LineDashStyle = ChartDashStyle.Dash;

            //Y軸の補助線を設定
            axis_y.MajorGrid.LineColor = System.Drawing.Color.LightGray;
            axis_y.MinorGrid.LineColor = System.Drawing.Color.LightGray;
            axis_y.MinorGrid.LineDashStyle = ChartDashStyle.Dash;

            //ズーム機能を有効化
            axis_x.ScaleView.Zoomable = Zoom;
            axis_y.ScaleView.Zoomable = Zoom;

            //ズーム機能を実現するためのイベントハンドラ定義
            if (Zoom)
            {
                //マウスホイールボタンのクリックによるズーム解除
                chart.MouseClick += (s, e) =>
                {
                    if (e.Button == System.Windows.Forms.MouseButtons.Middle)
                    {
                        axis_x.ScaleView.ZoomReset();
                        axis_y.ScaleView.ZoomReset();
                    }
                };

                //マウスホィールによるズーム処理
                chart.MouseWheel += (s, e) =>
                {
                    try
                    {
                        double xmin = axis_x.ScaleView.ViewMinimum;
                        double xmax = axis_x.ScaleView.ViewMaximum;
                        double xpos = axis_x.PixelPositionToValue(e.Location.X);
                        double xsize = (xmax - xmin) * ((e.Delta > 0) ? 0.25 : 1);
                        axis_x.ScaleView.Zoom(Math.Round(xpos - xsize, 0, MidpointRounding.AwayFromZero), Math.Round(xpos + xsize, 0, MidpointRounding.AwayFromZero));

                        double ymin = axis_y.ScaleView.ViewMinimum;
                        double ymax = axis_y.ScaleView.ViewMaximum;
                        double ypos = axis_y.PixelPositionToValue(e.Location.Y);
                        double ysize = (ymax - ymin) * ((e.Delta > 0) ? 0.25 : 1);
                        axis_y.ScaleView.Zoom(Math.Round(ypos - ysize, 0, MidpointRounding.AwayFromZero), Math.Round(ypos + ysize, 0, MidpointRounding.AwayFromZero));
                    }
                    catch { }
                };
            }
        }

        /// <summary>
        /// CSVの読み込み
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="delimiter"></param>
        /// <param name="encodeName"></param>
        /// <returns></returns>
        private DataTable LoadCsv(string fileName, char delimiter = ',', string encodeName = "shift-jis")
        {
            //結果を格納するリスト
            DataTable dt = new DataTable();

            //カンマで分割した1行分を格納するリスト
            List<string> line = new List<string>();

            //1カラム分の値を格納する変数
            StringBuilder value = new StringBuilder();

            //ダブルクォーテーションの中であることを現わすフラグ
            bool dq_flg = false;

            //ファイルをオープンする
            using (StreamReader sr = new StreamReader(fileName, Encoding.GetEncoding(encodeName)))
            {
                //ファイルの最後になるまでループする
                while (!sr.EndOfStream)
                {
                    //1文字読み込む
                    var ch = (char)sr.Read();

                    //ダブルクオーテーションが見つかるとフラグを反転する
                    dq_flg = (ch == '\"') ? !dq_flg : dq_flg;

                    //ダブルクォーテーション中ではないキャリッジリターンは破棄する
                    if (ch == '\r' && dq_flg == false)
                    {
                        continue;
                    }

                    //ダブルクォーテーション中ではない時にカンマが見つかったら、
                    //それまでに読み取った文字列を1つのかたまりとしてline に追加する
                    if (ch == delimiter && dq_flg == false)
                    {
                        line.Add(to_str(value));
                        value.Clear();
                        continue;
                    }

                    //ダブルクォーテーション中ではない時にラインフィードが見つかったら
                    //line(1行分) を result に追加する
                    if (ch == '\n' && dq_flg == false)
                    {
                        line.Add(to_str(value));
                        //カラム数が0なら、読み込んだ1行分の内容でカラムを作成
                        if (dt.Columns.Count == 0)
                        {
                            line.Select(i => dt.Columns.Add(i)).ToArray();
                        }
                        else
                        { 
                            dt.Rows.Add(line.ToArray());
                        }
                        line.Clear();
                        value.Clear();
                        continue;
                    }
                    value.Append(ch);
                }
            }

            //ファイル末尾が改行コードでない場合、ループを抜けてしまうので、
            //未処理の項目がある場合は、ここでline に追加
            if (value.Length > 0)
            {
                line.Add(to_str(value));
                dt.Rows.Add(line.ToArray());
            }

            return dt;

            //前後のダブルクォーテーションを削除し、2個連続するダブルクォーテーションを1個に置換する
            string to_str(StringBuilder p_str)
            {
                string l_val = p_str.ToString().Replace("\"\"", "\"");
                int l_start = (l_val.StartsWith("\"")) ? 1 : 0;
                int l_end = l_val.EndsWith("\"") ? 1 : 0;
                return l_val.Substring(l_start, l_val.Length - l_start - l_end);
            }
        }

        /// <summary>
        /// ドラッグ&ドロップ処理
        /// </summary>
        /// <param name="control"></param>
        private void EnableDragDrop(Control control)
        {
            //ドラッグ&ドロップを受け付けられるようにする
            control.AllowDrop = true;

            //ドラッグが開始された時のイベント処理(マウスカーソルをドラッグ中のアイコンに変更)
            control.PreviewDragOver += (s, e) =>
            {
                //ファイルがドラッグされたとき、カーソルをドラッグ中のアイコンに変更し、そうでない場合は何もしない。
                e.Effects = (e.Data.GetDataPresent(DataFormats.FileDrop)) ? DragDropEffects.Copy : e.Effects = DragDropEffects.None;
                e.Handled = true;
            };

            //ドラッグ&ドロップが完了した時の処理(ファイル名を取得し、ファイルの中身をTextプロパティに代入)
            control.PreviewDrop += (s, e) =>
            {
                if (e.Data.GetDataPresent(DataFormats.FileDrop)) // ドロップされたものがファイルかどうか確認する。
                {
                    //ドラッグされたファイル名の取得
                    string[] paths = ((string[])e.Data.GetData(DataFormats.FileDrop));

                    //区切り文字の判定
                    var delimiter = (uxSplitChar.Text == "カンマ") ? ',' : '\t';

                    //CSVファイルとして読み込む
                    var dt = LoadCsv(paths[0], delimiter, uxCharCode.Text);

                    //DataGridにデータをセット
                    uxDataGrid.DataContext = dt;

                    //CSVのカラム名をドロップダウンにセットする。
                    uxColumnX.Items.Clear();
                    dt.Columns.Cast<DataColumn>().Select(i => uxColumnX.Items.Add(i.ColumnName)).ToArray();
                    
                    //X軸カラム名ドロップダウンの先頭項目を表示
                    uxColumnX.SelectedIndex = 0;
                }
            };

        }

        /// <summary>
        /// クリックされたセルの行番号と列番号の取得
        /// </summary>
        /// <param name="dataGrid"></param>
        /// <param name="pos"></param>
        /// <returns></returns>
        public (int rowIndex, int columnIndex) ClickCellIndex(DataGrid dataGrid, Point pos)
        {
            DependencyObject dep = VisualTreeHelper.HitTest(dataGrid, pos)?.VisualHit;
            int rowIndex = -1;
            int columnIndex = -1;

            while (dep != null)
            {
                dep = VisualTreeHelper.GetParent(dep);

                while (dep != null)
                {
                    if (dep is DataGridCell)
                    {
                        columnIndex = (dep == null) ? -1 : (dep as DataGridCell).Column.DisplayIndex;
                    }
                    if (dep is DataGridRow)
                    {
                        rowIndex = (dep == null) ? -1 : (int)(dep as DataGridRow).GetIndex();
                    }

                    if (rowIndex >= 0 && columnIndex >= 0)
                    {
                        break;
                    }

                    dep = VisualTreeHelper.GetParent(dep);
                }
            }
            return (rowIndex, columnIndex);
        }

    }
}

まとめ

今回は、ドラッグ&ドロップしたCSVファイルを一覧表示(DataGrid)し、各カラムをダブルクリックすることで、グラフを描画するサンプルプログラムを紹介しました。

簡易的に作成しているため、フォルダからファイルを選択するような機能や、例外処理は入れてはいませんが、WPFにおけるドラッグ&ドロップの方法、CSVファイルの読み込み方法、DataGridにおいてクリックされたセルの取得方法、グラフの描画方法など、基本的な機能は備わっていますので、参考にして頂ける部分も多いのではないかと思います。

もっといろいろなグラフを描画したい場合はこちらの記事から必要なグラフのソースをコピペしてお使い下さい。

また、MSChart以外のグラフ描画コントロールを使いたい場合は、こちらの記事に詳しく紹介していますので、ご一読ください。

今回の記事が皆様のお役に立てれば光栄です。