【WPF】C#でファイルの容量をグラフで可視化する方法【ソースコード付き】

当ページのリンクには広告が含まれています。

以前の記事で、とあるサーバの使用量の増加推移を調べるため、「指定フォルダ配下に存在するファイルの情報(ファイル名、ファイルサイズ、作成日など)を全て表示するコマンド」を作成したという記事を書きました。

あの時は、結果をEXCELに読み込んでグラフ化していたのですが、今後定期的にこの作業を行う必要がありそうなので、ファイル情報取得~集計~グラフ作成までの一連の処理を行うツールを作ってみました。

皆様のプログラム作成において、何かの参考になればと思ったのでソースコード一式を公開したいと思います。

目次

プログラムの概要

今回作ったのは下記のようなツールです。

グラフのX軸ラベルが重なったり、処理中のプログレスリングが少々ぎこちない動きだったり、対象フォルダ指定が単純なテキスト入力だったりと、結構手を抜いている部分は多々ありますが、その辺はご了承下さい。

ちなみに対象フォルダのテキスト入力部分については、フォルダをドラッグ&ドロップで指定できるようになっています。

このツールは、指定したフォルダ配下のファイルをサブフォルダも含めて検索し、ファイル作成日を対象にファイルサイズを集計、グラフ化するものです。

また、次のような機能を持っています。

  • 対象フォルダをドラッグ&ドロップで指定可能
  • 集計する単位は、年ごと、月ごと、日ごとの3つが選択可能です。
  • 一覧部分は検索した全てのファイル情報を表示(ファイルサイズの単位はバイト)
  • グラフ部分は集計単位の棒グラフと、累計の折れ線グラフを表示(単位はメガバイト)
  • グラフ部分はマウス操作で拡大・縮小が可能
  • ESCキーで検索を中断し、その時点の集計結果を表示

使い方

対象フォルダの入力欄に、集計したいフォルダ名を入力して、「解析」ボタンをクリックするだけです。

ちなみに、ファイル名をワイルドカード付きで指定することが可能で、例えば

"D:\My Documents\Visual Studio 2019\Projects\*.c" と入力すると、拡張子が ".c" のものだけを集計対象にすることが可能です。

また、グラフ部はマウスホイールで拡大/縮小が行え、ドラッグ&ドロップで上下左右にグラフをスクロールさせることが可能です。

使用しているライブラリなど

グラフを描画する部分はフリーのグラフ描画ライブラリ「ScottPlot」の Ver4.1.68(2023/11/22時点で最新)を使用しています。

「ScottPlot」のインストール方法は、「【WPF】いちばんやさしいScottPlot の使い方(WindowsForm共通) | 初心者DIYプログラミング入門」の記事をご覧ください。

尚、ScottPlotは「【WPF】C#+ScottPlotで複合グラフのユーザーコントロールを作る! | 初心者DIYプログラミング入門」の記事で紹介したユーザーコントロール化したものを使っています。

また、処理中に表示するプログレスリングは、「【WPF】C#で処理中にぐるぐる回るGIFを表示したい! | 初心者DIYプログラミング入門」 で紹介した方法(WindowsFormsHost経由でPictureBoxにアニメーションGIFを登録して表示)を使っています。

ソースコードの解説

それでは、XAMLとC#のソースコードについて、簡単な解説を交えて紹介いたします。

XAMLのソースコード

単純にコントロールを張り付けているだけなので、特に難しい部分は無いかと思います。

<Window
        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:local="clr-namespace:FolderAnalyzer"
        xmlns:ChartUserControl="clr-namespace:ChartUserControl" x:Class="FolderAnalyzer.MainWindow"
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
    Title="FolderAnalyzer" Height="360" Width="600" MinHeight="360" MinWidth="600">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="64"/>
            <RowDefinition Height="13*"/>
            <RowDefinition Height="5"/>
            <RowDefinition Height="12*"/>
        </Grid.RowDefinitions>
        <Label Content="対象フォルダ" HorizontalAlignment="Left" Margin="10,7,0,0" VerticalAlignment="Top" Height="26" Width="70"/>
        <Button x:Name="uxAnalyze" Content="解析" Margin="0,10,10,0" VerticalAlignment="Top" Height="23" HorizontalAlignment="Right" Width="85" Click="uxAnalyze_Click"/>
        <GridSplitter Grid.Row="2" HorizontalAlignment="Stretch" />
        <TextBox x:Name="uxTarget" Height="23" Margin="85,10,100,0" TextWrapping="NoWrap" VerticalAlignment="Top"/>
        <DataGrid x:Name="uxView" ItemsSource="{Binding}" Margin="10" Grid.Row="3" AlternatingRowBackground="Aqua" IsReadOnly="True" Background="{x:Null}" />
        <ChartUserControl:ScottLineBarChartControl x:Name="uxChart" Grid.Row="1"/>
        <RadioButton x:Name="uxYear" Content="年単位" HorizontalAlignment="Left" Margin="85,38,0,8" Click="uxRadio_Click" Width="55"/>
        <RadioButton x:Name="uxMonth" Content="年月単位" HorizontalAlignment="Left" Margin="156,38,0,8" IsChecked="True" Click="uxRadio_Click" Width="67"/>
        <RadioButton x:Name="uxDay" Content="年月日単位" HorizontalAlignment="Left" Margin="240,38,0,8" Click="uxRadio_Click" Width="79"/>
        <WindowsFormsHost x:Name="uxHost" Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Center" Visibility="Hidden">
            <wf:PictureBox x:Name="uxProgress" SizeMode="StretchImage" />
        </WindowsFormsHost>
    </Grid>
</Window>

C#のソースコード

グラフ表示で使っているユーザーコントロールについては、こちら の記事にソースコードを丸ごと掲載していますので、ここでは省略します。

気を付ける点としては、WindowsFormのPictureBoxを使用しているため、次の3つのアセンブリを参照設定に追加しておくくらいです。

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

namespace FolderAnalyzer
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        //解析結果(ファイル一覧)を保持するDataTable
        private DataTable _dt;

        //コンストラクタ
        public MainWindow()
        {
            InitializeComponent();

            //DataGridに行番号を表示
            EnableRowNum();

            //プログレスリングの画像を登録
            uxProgress.Image = Bitmap.FromFile(@".\Resources\progress.gif");

            //テキストボックスをドラッグ&ドロップ対応にする
            SetDragDrop(uxTarget);
        }

        /// <summary>
        /// 解析ボタンクリック処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void uxAnalyze_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                //プログレスリングの表示
                uxHost.Visibility = Visibility.Visible;

                //表示内容を消去
                uxView.DataContext = null;

                //ファイル情報の取得
                _dt = ScanFolder(uxTarget.Text);
                    
                //ファイル情報をDataGridに表示
                uxView.DataContext = _dt;

                //グラフの表示
                DrawChart(_dt);

                //プログレスリングの非表示
                uxHost.Visibility = Visibility.Hidden;
            }
            catch(Exception exp)
            {
                //プログレスリングの非表示
                uxHost.Visibility = Visibility.Hidden;

                MessageBox.Show(exp.Message, "エラー", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        /// <summary>
        /// 指定したパス配下のファイルを全て取得し、DataTableで返す
        /// </summary>
        /// <param name="folder"></param>
        /// <returns></returns>
        private DataTable ScanFolder(string path)
        {
            DataTable dt = new DataTable();
            dt.Columns.Add("ファイル名");
            dt.Columns.Add("サイズ", typeof(long));
            dt.Columns.Add("作成日", typeof(string));
            dt.Columns.Add("最終更新日", typeof(string));
            dt.Columns.Add("最終アクセス日", typeof(string));

            foreach (var file in Files(path, SearchOption.AllDirectories))
            {
                //ファイル情報をDataRowに格納
                DataRow dr = dt.NewRow();
                dr[0] = file.FullName;
                dr[1] = file.Length;
                dr[2] = file.CreationTime.ToString("yyyy/MM/dd HH:mm:ss");
                dr[3] = file.LastWriteTime.ToString("yyyy/MM/dd HH:mm:ss");
                dr[4] = file.LastAccessTime.ToString("yyyy/MM/dd HH:mm:ss");
                dt.Rows.Add(dr);

                //溜まっているメッセージ キューを処理
                DoEvents();

                //ESCキーが押されたら処理を中断
                if(Keyboard.IsKeyDown(Key.Escape))
                {
                    break;
                }
            }

            return dt;
        }

        private void DrawChart(DataTable dt)
        {
            if (dt != null)
            {
                var len = ((bool)uxYear.IsChecked) ? 4 : ((bool)uxMonth.IsChecked) ? 7 : ((bool)uxDay.IsChecked) ? 10 : 7;

                var datas = Summary(dt, len);
                uxChart.Labels = datas.Select(i => i.key).ToArray();
                uxChart.YValues = new List<double[]>()
                {
                    datas.Select(i => (double)i.len / 1000000).ToArray(),
                    datas.Select(i => (double)i.add / 1000000).ToArray()
                };
                uxChart.BarStyleDataNo = 0;
                uxChart.Draw("");
            }
        }

        /// <summary>
        /// 作成日の先頭から指定した文字数でグルーピング集計を行う
        /// </summary>
        /// <param name="dt"></param>
        /// <param name="len"></param>
        /// <returns></returns>
        private (string key, long len,long add)[] Summary(DataTable dt,int len)
        {
            //日付けでグルーピング集計を行う
            var res =  dt.AsEnumerable().GroupBy(i => i["作成日"].ToString().Substring(0, len)).
                    Select(i => (i.Key, i.Sum(j => long.Parse(j["サイズ"].ToString())))).ToArray();

            //各要素の累積値を求める
            long add = 0;
            return res.Select(i => (i.Key, i.Item2, add += i.Item2)).ToArray();
        }

        /// <summary>
        /// 指定したパス配下のファイルを全て抽出する
        /// </summary>
        /// <param name="path"></param>
        /// <param name="searchOption"></param>
        /// <returns></returns>
        public IEnumerable<FileInfo> Files(string path, SearchOption searchOption = SearchOption.AllDirectories)
        {
            string drive = System.IO.Path.GetPathRoot(path);          //ドライブ名を取得
            string directory = System.IO.Path.GetDirectoryName(path); //ディレクトリ名を取得
            string filter = System.IO.Path.GetFileName(path);         //ファイル名を取得

            //ディレクトリが未指定ならドライブ名をディレクトリ名とする
            directory = directory ?? drive;
            //ファイル名が指定されているとファイル名を、指定されていなければワイルドカードをフィルターに設定
            filter = (filter == "") ? "*.*" : filter;

            //フォルダ名が指定された場合
            if (!filter.Contains("?") && Directory.Exists(path) == true && drive != path)
            {
                directory = path;
                filter = "*.*";
            }

            return Files(directory, filter, searchOption);
        }
        /// <summary>
        /// ファイル情報の一覧を取得する。
        /// アクセス権限が無いディレクトリは無視する(例外を発生させない)。
        /// </summary>
        /// <param name="directory"></param>
        /// <param name="filter"></param>
        /// <param name="searchOption"></param>
        /// <returns></returns>
        static public IEnumerable<FileInfo> Files(string directory, string filter, SearchOption searchOption = SearchOption.AllDirectories)
        {
            List<FileInfo> infos = new List<FileInfo>();

            //ディレクトリが未指定ならカレントディレクトリを対象とする。
            directory = (directory.Trim() == "") ? System.IO.Directory.GetCurrentDirectory() : directory;

            //指定されたディレクトリの情報を取得
            DirectoryInfo dir_top = new DirectoryInfo(directory);
            try
            {
                //指定されたディレクトリ直下に存在するファイル情報を取得
                foreach (var info in dir_top.EnumerateFiles(filter))
                {
                    infos.Add(info);
                }
            }
            catch { }

            //取得したファイル情報を返す
            foreach (var info in infos)
            {
                yield return info;
            }

            //サーチオプションが配下のディレクトリを検索対象にしている場合
            if (searchOption == SearchOption.AllDirectories)
            {
                //指定されたディレクトリ直下にあるディレクトリを全て取得
                foreach (var dir_info in dir_top.EnumerateDirectories("*"))
                {
                    infos.Clear();

                    try
                    {
                        //取得したディレクトリ直下から末端までの階層をたぐって全てのファイル情報を取得
                        foreach (var info in dir_info.EnumerateFiles(filter, SearchOption.AllDirectories))
                        {
                            infos.Add(info);
                        }
                    }
                    catch { }

                    //取得したファイル情報を返す
                    foreach (var info in infos)
                    {
                        yield return info;
                    }
                }
            }
        }

        /// <summary>
        /// ラジオボタンが押された時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void uxRadio_Click(object sender, RoutedEventArgs e)
        {
            DrawChart(_dt);
        }

        /// <summary>
        /// 行Noを表示するためのイベント処理を登録
        /// </summary>
        private void EnableRowNum()
        {
            uxView.LoadingRow += (sender, e) =>
            {
                e.Row.Header = (e.Row.GetIndex() + 1).ToString().PadLeft(10);
            };

        }

        /// <summary>
        /// メッセージ キューに現在ある Windows メッセージをすべて処理する。
        /// </summary>
        private void DoEvents()
        {
            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(f => { ((DispatcherFrame)f).Continue = false; return null; }), frame);
            Dispatcher.PushFrame(frame);
        }

        /// <summary>
        /// プログレスリングの表示/非表示
        /// </summary>
        /// <param name="sw"></param>
        private void ProgreeRing(bool sw)
        {
            if (sw)
            {
                uxHost.Visibility = Visibility.Visible;
            }
            {
                uxHost.Visibility = Visibility.Hidden;
            }
        }

        /// <summary>
        /// ドラッグ&ドロップの設定
        /// </summary>
        /// <param name="control"></param>
        private void SetDragDrop(Control control)
        {
            //ドラッグ&ドロップの処理
            control.PreviewDragOver += (sender, args) =>
            {
                args.Effects = (args.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None);
                //コントロールの種類によって、下記の記述が必要(PreviewDropが効かない場合がある) なので、念のため
                args.Handled = true;
            };

            control.PreviewDrop += (sender, args) =>
            {
                if (args.Data.GetDataPresent(DataFormats.FileDrop))
                {
                    string[] files = (string[])args.Data.GetData(DataFormats.FileDrop);
                    uxTarget.Text = files[0];
                }
            };
        }

    }
}

プロジェクト一式のダウンロード

下記リンクからプロジェクト一式のダウンロードが可能です。

NuGetでインストールしたScottPlotも一緒に入っています。

まとめ

今回は指定したフォルダ配下のファイルからファイル情報を取得し、作成日ごとにファイル容量を集計、グラフ化するツールについて、使い方とソースコードを紹介しました。

このサイトで今までに紹介した内容(ScottPlot、複合グラフ作成ユーザーコントロール、PictureBoxを使ったプログレスリングの表示、フォルダ配下のファイル情報取得)を使っています。

個々の記事を見て使い方がピンとこなかった方においても、今回の記事で具体的な利用方法が掴めたのではないかと思います。

皆さんの何かのお役に立てれば幸いです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次