2018年5月11日金曜日

ひとまず完結のご挨拶

ご挨拶


2018年2月から始めたこのブログには、2018年1月中旬から約2週間、集中的に取り組んだプログラミング言語C#の学習の足跡を35回に渡って記してきました。

本来なら、こういった内容のものは、Qiitaあたりに書いて、現役プログラマに揉まれるべきなのかもしれませんが、そこまで臨戦体制的なものではなく、自分が覚えておく為の記録の意味合いで書いて来ました。

そもそも、気が弱いのであそこに何か書くのは無理です(笑)

C#学習のそもそもの発端は、最初のご挨拶にも記したとおり、お仕事としてC#でプログラム開発が出来ないか、という打診を頂いたことでした。

残念ながらお仕事にはなりませんでしたが、2週間という短い期間で、Windows環境で動くGUIアプリケーションを書くことが出来るようになったのは大きな成果と自分では思っています。

今回、当初の目標仕様を満足するプログラムの作成の完了まで来ましたので、記事の更新はこれで一段落ということにします。しかし、今後も色々なソフトウエア開発のお勉強や研究をすることはあると思いますので、その際にはこのブログに足跡を残していきたいと考えています。

お付き合いいただき、ありがとうございました。そして、今後ともよろしくお願いいたします。

筆者:冨樫伸也

今までの成果を全部入れて、いよいよプログラム完成です

前回の記事で、Form(Form1)上のPictureBox(pictureBox1)にマウスの動きに合わせて動く図形(丸)を描くことが出来ました。

ここまで来れば、PictureBoxの中に縦又は横の直線を描いて、マウスの動きに合わせて動かすのは難しいことではありません。出口はもうすぐです。

2018年5月10日木曜日

マウスに合わせて図形を動かす(2) 〜 使いやすさを求めて

前回の記事で、マウスの動きに合わせて黒丸を動かすことが出来るようになりました。しかしながら、これは単純なFormの上での話です。

次は、これを輪切り描画プログラムに移植します。

前回の記事では、Paintイベントのイベントハンドラとして自前のメソッドを準備して、その中に描画処理を記述し、メソッド黒丸を描く座標を指定した後に、Invalidateメソッドを呼び出してPaintイベントを発生させ、描画するという流れの処理を書きました。

最初は、これを単純に今まで作ってきた輪切り画像描画プログラムに持っていくことを考えました。

当初からあるMouseMoveイベントのイベントハンドラの中に、黒丸を描く座標を計算し、再描画のためのInvalidate()メソッドを呼び出すコードを入れます。

なお、リスト中のマウス座標を取得するメソッドgetMousePosition()は、以前のCursor.Positionプロパティを読んで座標変換する処理方法から、MouseMoveイベントの引数として与えられるウインドウ内座標系のマウス座標を、描画データの座標系に変換する処理に変更しました。

        
  リスト1 MouseMoveイベントのイベントハンドラ
        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            // イベントが発生したときのマウス座標を取得
            Point point = new Point( e.X, e.Y ); 
            // この処理で、ウィンドウ座標で入手したマウス座標を、データ座標系に変換
            getMousePosition( point );           
            //黒丸を描く座標を算出
            this.pt = new Point(e.X - (size.Width / 2), e.Y - (size.Height / 2));
           // 描画処理で使う座標をグローバル変数に設定、
           // 新たにインスタンスを作る必要がないと思うので、pointをそのまま代入
            this.pt2 = point;     

            this.Invalidate();       // 再描画
        }

さらに、前回のプログラムと同様にOnPaint()メソッドをオーバーライドして丸を描くコードを入れます。なお、pictureBox1は背景が黒なので、丸の色を白に変更しています。

リスト2 描画メソッド
        protected  override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            Brush brush = new SolidBrush(Color.White);

            e.Graphics.FillEllipse(brush, pt.X, pt.Y, size.Width, size.Height);
        }

ところが、この方法はうまくいきません。白い丸がForm1の上で動いているのを黒いpictureBoxが隠してしまっているように見えます。試しにthis.Invalidate();を、this.pictureBox1.Invalidate();に変更すると、丸は動かなくなります。どういうことでしょうか?

ここからは私の推理ですが、リスト1におけるthis.Invalidate();のthisはpictureBox1ではなくて、Form1を指しています。また、リスト2におけるOnPaint()の基底クラスはForm1.OnPaint()になるんだと思います。

つまり、再描画要求はあくまでもForm1に対する再描画要求であり、描画処理もForm1に対するOnPaint()メソッドのオーバーライドになります。つまりpictureBox1は蚊帳の外でなのです。

this.Invalidate();をthis.pictureBox1.Invalidate();に変更した場合は、再描画要求は出るものの、pictureBox1には何も描画されていない状況であるのに加えて、再描画処理は何も手を加えていないpictureBox1の再描画処理が走るだけ。

そしてForm1のOnPaintは起動しないので、Form1内に描画をするコードが動かず、何も起こらない、丸は動かない、ということになるのだと思います。

pictureBox1にも再描画の為のOnPaintメソッドはあるのですが、これはPictureBoxクラスの中のメソッドなので、Form1からオーバーライドして使ったりすることはできません。

色々調べ回って試行錯誤した結果、PaintイベントのイベントハンドラをOnPaintメソッドのオーバーライドとして記述するのではなく、Visual StudioのpictureBox1のプロパティから、明示的にイベントハンドラを定義すればよいのではないか、ということになりました。

これに加えて、再描画要求をthis.pictureBox1.Invalidate();と記述し、要求先をpictureBox1に、明示的に指示することにしました。

これによって、動作の流れは、MouseMoveイベント→描く丸の座標を計算→pictureBox1再描画要求→pictureBox1のPaintイベント発生→pictureBox1への描画処理、という流れに明確化します。

描画処理のコードは以下のようにしました。

リスト3 再描画メソッド
        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            Brush brush = new SolidBrush(Color.White);

            //ここに図形を描く処理を追加すると、マウスの動きに合わせてその図形が動いてくれるようになる。
            e.Graphics.FillEllipse(brush, pt.X, pt.Y, size.Width, size.Height);
        }

このような動作になります。
今日はここまでにします。

2018年5月9日水曜日

マウスに合わせて図形を動かす 〜 使いやすさを求めて

前回、輪切り画像作成に於いて、どこで切るのかを指示するときに、切る場所をわかりやすく見せるために、マウスカーソルの形状を変えてみました。

ここからさらに進化させて、マウスカーソルの動きに合わせてpictureBoxの中を縦に又は横に貫く直線を動かし、輪切りする場所をよりわかりやすく表示、指定できるようにしたいと思います。そのための準備として、マウスに合わせて図形を動かす方法を研究します。

マウスの動きに合わせて連続的に物体を動かす


世の中、インターネットを検索すると、マウスの動きに合わせて何かを動かす、というプログラムの例は沢山出て来ます。需要が多いのでしょう。

以下は、Formを表示させ、その中で黒丸をマウスの動きに合わせて動かす、というプログラムです。色々なWebで勉強させていただき、Wisdom Soft様で学ばせていただいたプログラムです。


リスト1 マウスの動きに合わせて黒丸がうごく
(名前空間参照の為のusingは省略)

namespace mousetest
{
    public partial class Form1 : Form
    {
        private Point pt;  // 黒丸の座標を保持
        private Size size = new Size(40, 40);  // 黒丸のサイズを保持

        private void Test_MouseMove( object sender, MouseEventArgs e )
        {
            // Point構造体に、これから黒丸を描く座標を設定
            this.pt = new Point(e.X - (size.Width / 2), e.Y - (size.Height / 2));
            // Form1のコントロール領域を無効にして、描画メッセージを送る
            this.Invalidate();
        }

       // OnPaintメソッドをオーバーライド
        protected override void OnPaint(PaintEventArgs e) 
        {
            base.OnPaint(e);  // 継承元クラスの再描画処理を実施
            Brush brush = new SolidBrush(ForeColor);  // これから黒丸を描くのに使うブラシを設定
            e.Graphics.FillEllipse(brush, pt.X, pt.Y, size.Width, size.Height);  // 黒丸を描画
        }

        
        public Form1()
        {
            InitializeComponent();
            // MouseMoveイベントのイベントハンドラとしてTest_MouseMoveを登録
            this.MouseMove += new MouseEventHandler(Test_MouseMove);
        }
    }
}

上記リストにおいて、MouseMoveイベントのイベントハンドラとしてTest_MouseMoveメソッドを記述しています。

この中では、これから描く黒丸の中心の座標を、MouseMoveイベントへの引数の中にあるマウスマーソルの座標と黒丸の大きさから計算し、Point構造体ptに設定した後、再描画要求の為にthis.Invalidate()メソッドを呼び出しています。これによってOnPaintイベントが発生します。

一方、通常のOnPaint()メソッドをオーバーライドして定義している、このプログラムのOnPaint()メソッドでは、通常のOnPaintイベントで実行されるはずの再描画処理をbase.OnPaint(e);で実行した後、黒丸を描くための描画ブラシの設定と描画処理を実施します。

base.OnPaint(e);の呼び出しで、黒丸を描く前のウインドウの中の絵柄が描画されるので、移動前の黒丸は消えて、移動後の黒丸が描かれる、という仕組みです。

この方式を活用して、PictureBoxの中でマウスの動きに合わせて直線を動かす処理を次回入れたいと思います。

今日はここまでにします。


2018年5月8日火曜日

マウスカーソルを変える 〜 使いやすさを求めて

前回で、輪切り画像の描画処理は一通り開発を終わりました。

ただ、輪切りをする場所を指定する画面の使いやすさを、もう少し向上したいと思ったんです。要するに、どこで切るのかを操作している人がわかりやすい仕掛けを入れたいと思った、ということです。

一番簡単な方法は、必要なときにマウスカーソルの形状を縦棒もしくは横棒に変えると言うものだと思います。マウスカーソルのライブラリーにはVSplit、HSplitという名前のカーソルがあるので、これを使います。

プログラムの中に、輪切り画像作成モードを設定する処理、checkBox1_CheckedChanged()、checkBox2_CheckedChanged()という2つのメソッドがあります。輪切り画像作成モードに入る為のボタンを押したときに呼び出されるイベントハンドラです。この中で、マウスカーソルの切替をします。


         /* X軸に垂直な平面で輪切りにする場合の処理 */
        private void checkBox1_CheckedChanged(object sender, EventArgs e)
        {
            if (checkBox2.Checked && !event_by_inner1)
            {
                event_by_inner1 = true;
                checkBox2.Checked = false;
            }

            event_by_inner1 = false;

            if (checkBox1.Checked)
            {
                checkBox1.Text = "Push Button to stop getting mouse position X-axis";
                button1_on = true;
                mouse_x = mouse_y = 0; // マウス座標データ初期化
                // マウスカーソルを、縦向きに分割するようなものに変更
                this.pictureBox1.Cursor = System.Windows.Forms.Cursors.VSplit;
                // マウスイベント有効
                ctrlEvent(true);
            }
            else
            {
                checkBox1.Text = "Push Button to start getting mouse position X-axis";
                button1_on = false;
                // マウスカーソルを元に戻す
                this.pictureBox1.Cursor = System.Windows.Forms.Cursors.Default;
                // マウスイベント削除
                ctrlEvent(false);
            }
        }

X軸に垂直な平面で輪切りにするモードに遷移したら、pictureBox1.CursorプロパティをSystem.Windows.Forms.Cursors.VSplitに書き換えて縦向きの直線に変更し、輪切りモードから通常モードに遷移したらSystem.Windows.Forms.Cursors.Defaultに書き換えてデフォルトのマウスカーソルに戻します。

         /* Y軸に垂直な平面で輪切りにする場合の処理 */
        private void checkBox2_CheckedChanged(object sender, EventArgs e)
        {
            if (checkBox1.Checked && !event_by_inner1)
            {
                event_by_inner1 = true;
                checkBox1.Checked = false;
            }

            event_by_inner1 = false;

            if (checkBox2.Checked)
            {
                checkBox2.Text = "Push Button to stop getting mouse position Y-axis";
                button2_on = true;
                mouse_x = mouse_y = 0; // マウス座標データ初期化
                // マウスイベント有効
                // マウスカーソルを水平の向きに分割するものに変更
                this.pictureBox1.Cursor = System.Windows.Forms.Cursors.HSplit; 
                ctrlEvent(true);
            }
            else
            {
                checkBox2.Text = "Push Button to start getting mouse position Y-axis";
                button2_on = false;
                // マウスカーソルを元に戻す
                this.pictureBox1.Cursor = System.Windows.Forms.Cursors.Default; 
                // マウスイベント削除
                ctrlEvent(false);
            }
        }
Y軸に垂直な平面で輪切りにするモードに遷移したら、pictureBox1.CursorプロパティをSystem.Windows.Forms.Cursors.HSplitに書き換えて横向きの直線に変更し、輪切りモードから通常モードに遷移したらSystem.Windows.Forms.Cursors.Defaultに書き換えてデフォルトのマウスカーソルに戻します。

動作の様子をアニメーションgifにしてみました。下に貼り付けます。


今回はここまでです。

2018年5月7日月曜日

輪切り画像の描画処理はこれで完成

拡張メソッド、ラムダ式のエントリーを挟んだ前回のエントリーで、輪切り画像の描画のための基本的な部品は出来上がりました。

前回までの進捗を整理すると、


  • データファイルをOpen File Dialogを使って選択
  • Visual BasicのCSVパーサーを使ってCSVのデータをArrayListに格納
  • プロット
  • 輪切りする場所を指定する為のUIを実装(X座標を指定するかY座標を指定するかを排他的に選択
  • マウスでクリックした場所の座標を拾う(X座標のみ)
  • 拾った座標に最も近いデータをArrayListから探してくる(X座標のみ)


というところまで、実装済みです。この後は、


  • 奥から描画していくようにデータを並び替え(隠面処理)
  • マウスでクリックした場所の座標を拾う(前回はX軸だけだったのでY軸も含めて)
  • 拾った座標に最も近い座標をArrayListから探してくる(X座標、Y座標ともに)
  • ArrayListから見つけてきた一番近い座標を含む全てのデータを取得
  • プロット

という機能を実装すれば、輪切り画像の描画処理は完成となります。

奥から描画していくようにデータを並び替え


立体的な物体を画面に描くことは、物体の形状を平面に投影するということになります。その際に気をつけなければいけないのが、見える場所は見えるように、隠れて見えない場所は見えないように描くこと。

人間が鉛筆で絵を描く場合は、隠れて見えない場所は描かないのでしょうが、コンピューターに描かせる場合は、隠れて見えない場所は描かない、という処理をしてあげる必要があります。これを隠面処理と言います。

隠面処理には色々な方法があるようですが、今回のプログラムでは、見ている場所から遠い点から描画して、後ろを遮るものが前にある場合は、後ろの点を重ね書きして隠してしまう方法を採用します。一番単純な方法だからです。

プログラムが読むデータファイルは、データの順番が都合の良いように並んでいるとは限らない、と仮定すると、描画する前にデータを画面奥から手前に向かう座標軸、今回の場合はZ軸に関して並べ替える必要があります。

並べ替えの手法は、「業務開発で使えるプログラム」様にて勉強させていただきました。

描画する点のデータは、X座標、Y座標、Z座標及び点における測定値の大きさ、という4つのデータがひとかたまりになっています。これらをExcelのソートのように、1つのデータ、Excelのワークシートに例えるなら、1つのカラムをキーにして並べ替えたいわけです。

これは、バラバラのArrayListのままではなかなか難しいので、データテーブルというデータ形式を使って表形式のデータにまとめてから取り扱うのがよさそうです。

            //
            // 後でデータの並べ替えが必要になるので、プロットデータをデータテーブルに格納
            //
            table.Columns.Add("x", Type.GetType("System.Int32"));
            table.Columns.Add("y", Type.GetType("System.Int32"));
            table.Columns.Add("z", Type.GetType("System.Int32"));
            table.Columns.Add("dt", Type.GetType("System.Int32"));

            for (int i = 0; i < x_axis_dt.Count; ++i)
            {
                //
                // 実座標をウィンドウ座標に変換するためにオフセット加算
                //
                int x = (int)x_axis_dt[i] + Constants.offset_x;
                int y = -1 * ((int)y_axis_dt[i]) + Constants.offset_y;
                int z = (int)z_axis_dt[i] + Constants.offset_z;
                int d = (int)dt_dt[i];

                x_axis.Add(x);
                y_axis.Add(y);
                z_axis.Add(z);
                dt.Add(d);

                table.Rows.Add(x, y, z, d);  //データ1行分追加
            }
            
            // データテーブルのソートをするために、Dataviewを使う
            DataView dv = new DataView(table);
            dv.Sort = "z";        // 昇順 -> Z座標の数値が大きい方が手前。小さい方が奥
            //dv.Sort = "z DESC"; // 降順 -> Z座業の数値が小さい方が手前、大きい方が奥
            table = dv.ToTable();

データテーブルを作った後は、DataViewというクラスを使って、簡単にデータテーブルの並べ替えをすることができます。DataViewにデータテーブルを設定して、並べ替えをして、並べ替えたデータを元のデータテーブルに書き戻すまで、DataView dv = new DataView(table);の行からたった3行で記述することができます。

マウスでクリックした場所の座標を拾う


前回、X軸に垂直な平面に関して輪切りを行う場合について、クリックした場所の座標を拾う処理を実装しました。今回は、Y軸に垂直な平面に関して輪切りを行う場合の処理も合わせて組み込みます。

作成したプログラムの中では、Form1の上にあるpictureboxに対する描画処理が終了したあと、ユーザーが輪切り画像描画のボタンを押すことにより輪切り画像描画モードに遷移します。

このモードに遷移すると、Clickイベント、MouseMoveイベント、MouseHoverイベントが有効となり、マウスカーソルがpictureboxの上を動くたびにマウスカーソルの座標が、クラス内のグローバル変数であるmouse_x、mouse_yの中に記録され、更新され続けます。

このmouse_x、mouse_yのデータを拾ってくることで、マウスでクリックした場所の座標を知ることが出来ます。

拾った座標に最も近い座標をArrayListから探してくる

輪切り画像の描画を行うメソッドであるdrawSlice()の冒頭で、button1が有効かbutton2が有効かを調べて、X軸に関して輪切りをするか、Y軸に関して輪切りをするかを切り分け、それぞれでX軸の場合はmouse_x、Y軸の場合はmouse_yの値を取り出し、これに最も近い値をArrayList x_axis_dt_int(X軸に関して輪切りにする場合) y_axis_dt_int(Y軸に関して輪切りにする場合)から見つけてきます。

下記のプログラムリストにおいて、

// マウスでポイントした座標に最も近いデータ上の座標を拾ってくる
nearest_point = x_axis_dt_int.Nearest(mouse_x);  
nearest_point = y_axis_dt_int.Nearest(mouse_y);

の部分です。上はX軸に関して、下はY軸に関して輪切りにする場合の座標取得です。
ただし、ここで気をつけたいのは、まだ描画処理が走っていない状態で、ArrayListから最も近い値を探してくる処理を走らせてはいけないことです。これを防ぐ為に、描画データを格納するArrayListにデータが入っているかどうかを、逆に言えばArrayListが空でないかどうかをテストする処理を入れています。これは拡張メソッドIsEmpty()として定義しています。

実はあえて拡張メソッドにする必要もないのですが、使うときの書きやすさで、空かどうかをbool値で返す拡張メソッドにしてみました。

ArrayListから見つけてきた一番近い座標を含む全てのデータを取得


拡張メソッドNearest()を使ってArrayListから探してきた数値(X座標もしくはY座標)を含む座標データを、座標データが格納されているArrayListから取得します。

座標データは、データファイルのCSVデータを読み込んだ後、座標変換を行い、一番奥にある座標から順に並ぶように並べ替えたものが、x_axis_dt_int、y_axis_dt_int、z_axis_dt_int、dt_dt_intに格納されています。

この座標データに対して、X軸に対して垂直な面で輪切りにするのであればx_axis_dt_intからx_axis_dt_int.Nearest()で探してきた数値と等しい座標データを全て、Y軸に対して垂直な面で輪切りにするのであればy_axis_dt_intからy_axis_dt_int.Nearest()で探してきた数値と等しい座標データを全て取得します。

プロット


記事ではデータの取得とプロットを別項目にしましたが、実際にはNearest()で探した数値と同じX座標、もしくはY座標を持ったデータかどうかをif分で振り分けて、目的の座標であれば、そのままプロットの色を決めてプロットしてしまうような処理にしました。

以下、上記説明をコーディングしたプログラムリストを以下に示します。pictureBox1_Click()は、pictureBox1の上でマウスをクリックしたときに発生するイベントのイベントハンドラー、drawSlice()は実際に輪切り画像を描画するメソッド、IsEmpty()はArrayListの中身が空かどうかをテストするための拡張メソッドとなります。ArrayListの中身が空だった場合は、何もしないで呼び出し元に戻ります。

なお、拡張メソッドIsEmpty()は、コガネブログ様で教えていただきました。ありがとうございます。

        /*
         * pictureboxの上でマウスをクリックしたときに発生するイベント
         * 何も描かれていない状態でスライス描画処理を呼び出すと、データが入っているリストに
         * 中身がある前提の処理に
         * 支障を来すし、何も描かれていない状態でスライス描画処理を呼び出すことは意味が
         * 無いことなので、何も描かれて
         * いないときにはスライス描画処理が行われないようにする必要がある。
         * その判断をここで実施。
         */
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            // データを入れるリストが単独で空であることはあり得ないので、
            // X軸もしくはY軸の座標データがあるかどうか一つだけのテストで判断
           if (!x_axis.IsEmpty())
            {
                drawSlice();  // データのリストが空でなければスライスを描く処理を呼び出す
            }
            else
            {
                MessageBox.Show("何も描かれていません");
            }
        }
        /*
         * スライス画像を作成する
         */
         private void drawSlice()
        {
            int nearest_point;
            Color col = new Color();

            Form3 frm3 = new Form3();
            frm3.Show();

            Bitmap img = new Bitmap(Constants.size_slicebox_x, Constants.size_slicebox_y);

            if (button1_on)
            {

               // マウスでポイントした座標に最も近いデータ上の座標を拾ってくる
               nearest_point = x_axis_dt_int.Nearest(mouse_x);  
                frm3.Text = "Slice by X-axis : " + nearest_point;
               // ArrayListの全ての要素数でループを回し、一番近い数値に関してだけ描画処理を動かす
                for (int i = 0; i < x_axis_dt.Count; ++i) 
                {
                    if (x_axis_dt_int[i] == nearest_point) 
                    {
                        // プロットデータの内容から描画色を決定
                        if (dt[i] != 0)   // データが0の時は描画しない
                        {
                            switch (dt[i])
                            {
                                case 0:
                                    col = Color.Black;
                                    break;
                                case 1:
                                    col = Color.Blue;
                                    break;
                                case 2:
                                    col = Color.Red;
                                    break;
                                case 3:
                                    col = Color.Purple;
                                    break;
                                case 4:
                                    col = Color.Green;
                                    break;
                                case 5:
                                    col = Color.Yellow;
                                    break;
                                case 6:
                                    col = Color.Violet;
                                    break;
                                case 7:
                                    col = Color.White;
                                    break;
                                default:
                                    col = Color.Magenta;
                                    break;
                            }
                        }
                        img.SetPixel(z_axis[i], y_axis[i], col);
                    }
                }
                frm3.pictureBox1.Image = img;
            }

            //
            // Y軸でスライスする場合の処理
            //
            if(button2_on)
            {
                nearest_point = y_axis_dt_int.Nearest(mouse_y);
                frm3.Text = "Slice by Y-axis : " + nearest_point;

                for (int i = 0; i < y_axis_dt.Count; ++i)
                {
                    if (    y_axis_dt_int[i] == nearest_point)
                    {
                        // プロットデータの内容から描画色を決定
                        if (dt[i] != 0)   // データが0の時は描画しない
                        {
                            switch (dt[i])
                            {
                                case 0:
                                    col = Color.Black;
                                    break;
                                case 1:
                                    col = Color.Blue;
                                    break;
                                case 2:
                                    col = Color.Red;
                                    break;
                                case 3:
                                    col = Color.Purple;
                                    break;
                                case 4:
                                    col = Color.Green;
                                    break;
                                case 5:
                                    col = Color.Yellow;
                                    break;
                                case 6:
                                    col = Color.Violet;
                                    break;
                                case 7:
                                    col = Color.White;
                                    break;
                                default:
                                    col = Color.Magenta;
                                    break;
                            }
                        }
                        img.SetPixel(x_axis[i], z_axis[i], col);
                    }
                }
                frm3.pictureBox1.Image = img;
            }

/*
 * 配列やリストが空かどうかを返す拡張メソッドisEmpty.cs
 * http://baba-s.hatenablog.com/entry/2015/07/17/100000
 */
using System.Collections.Generic;

public static class IListExtentions
{
    public static bool IsEmpty<T>( this IList<T> self )
    {
        return self.Count == 0;
    }
}

下に、プログラムを動作させた様子をキャプチャした動画を示します。輪切り画像がきちんと表示できればよかったのですが、色々な都合で綺麗に出ていないことをお許し下さい。


ソースファイルはここにzipファイルにして置きます。ソースのみですので、Visual Studioでビルドして動かす場合には、色々な設定が必要になると思います。それに関しては、誠に勝手ながら、各自にてお願いいたします。また、プログラムソースは学習用の使用に留めて下さいますようお願いいたします。

本プログラムもWindows10上のVisual Studio 2017 Communityエディションで開発、動作確認しています。動作環境として.Net Frameworkを使用していますので、Mac OS版のVisual Studio 2017では動かすことはできません。GUIを全て書き換える必要があると思います。

なお、プログラムソースの内容については無保証です。ご質問は可能な範囲でお受けいたしますが、回答にお時間を頂く場合もございますことをご了承下さい。

2018年4月17日火曜日

さらに立ち止まって、拡張メソッドの研究

前々回のエントリーで出て来た下記のリスト、この中で記述されているのが、前回のエントリーで研究したラムダ式と、今日やる拡張メソッドです。

using System;
using System.Collections.Generic;
using System.Linq;

public static class IEnumerableExtensions
{
    public static int Nearest(this IEnumerable<int> self, int target)
    {
        var min = self.Min(c => Math.Abs(c - target));
        return self.First(c => Math.Abs(c - target) == min);
    }
}

拡張メソッドとは何か?という簡単な説明が、Qiitaのkuwapp様記事にありました。

メソッドを追加しようと思ったら、普通はソースを編集してメソッドを追加して再コンパイルするか、派生クラスを作るしかないのですが、この拡張メソッドはそれをせずにクラスの追加が出来るように見えるものです。

上記で引用させていただいた記事には具体的に記述方法の説明はありません。記述方法などに関する詳しい説明は、いつも勉強させていただいている++C++ ; //未確認飛行 C様の記事にあります。

静的クラスの中に静的メソッドを定義し、その中で第一引数にthis修飾語を付けた静的メソッドを定義する、ということのようです。

下記リストではこの拡張メソッドを、スライス画像を描くためのメソッドの中で呼び出していますが、それは下記のように記述しています。

        /*
         * スライス画像を作成する
         */
         private void drawSlice()
        {
            Bitmap img = new Bitmap(Constants.size_slicebox_x, Constants.size_slicebox_y);
            MessageBox.Show("Slice by "+x_axis.Nearest(mouse_x));  //描画は未実装、座標をメッセージボックスに
        }

x_axisというArrayListには、Nearestというメソッドは元々ありませんが、拡張メソッドを定義しているので、あたかもメソッドが拡張されたかのように記述できている、という訳です。

拡張メソッドについては、ここまでにします。

2018年4月13日金曜日

ここで、ちょっと立ち止まってラムダ式の研究

とにかく最後まで進める為に、輪切り画像の生成に集中して先に進もうと思っていました。しかし、前回の記事を書いていて、ラムダ式に引っかかりがあって、ある程度理解できないと気が済まなくなりました。

一旦立ち止まって、ラムダ式の研究をしてみたいと思います。

お付き合いください。

たまにお世話になっている(とは言っても、勝手に拝見しているだけなんですけど)Qitaのアカウントrawr様の記事から、以下の例を考えてみます。

Listに入っている文字列の後ろに、何か決まった文字列を追加したいとき、foreach文を使って書くと以下のようになると思います。foreachが2回出て来ますが、最初のforeachは後ろに文字列を追加する処理、2番目に出てくるforeachは、処理結果をコンソールに出力するためのものです。

リスト1

using System;
using System.Collections.Generic;
using System.Linq;

namespace lamda
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();  // Listクラスのオブジェクトとしてlistを生成
            list.Add("aaa");  // listの要素を追加していきます
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

          // 処理結果を格納する為のListクラスのオブジェクトresultを生成        
            var result = new List<string>();  

            foreach ( var s in list )
            {
                result.Add( s + ".hoge");
            }
            
            foreach ( var s in result )
            {
                Console.WriteLine( s );
            }
        }
    }
}
ここで出てくるListクラスは初めて出てくるものです。Listは以前、「CSVのデータを一旦変数に格納、型変換をして描画処理を行います」のエントリーで出て来たArrayListによく似た可変長配列を実現するためのクラスです。 
ArrayListとの違いはこちらのページに説明があります。できるだけListを使いましょう、とのこと。 
ArrayListでは、各要素の型はオブジェクト型で、どんな型のデータでも格納できるものの、使うときに必ずキャストする必要があるのに対して、Listではオブジェクトを生成するときに決まってしまうと書いてあります。使うときに必ずキャストしなければならないのは、ちょっと面倒ですし、各要素の型が要素ごとに違う、なんていう設計はしないとおもいますので、最初に適切に型が決まった方が使いやすそうです。
このブログで取り扱っている描画処理のプログラムに於いては、この辺の事情も良くわからずにコーディングしていたので、ArrayListを使用しており、それをそのまま載せています。

ここで、参考にさせていただいたQiitaの筆者様は、もう少し簡潔に書けないものかと考えられたわけですね。

そこで、Selectメソッドというメソッドに登場してもらいます。上のリストはもう少し簡潔に以下のように書くことが出来るようです。

リスト2

using System;
using System.Collections.Generic;
using System.Linq;

namespace lamda2
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();
            list.Add("aaa");
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

            // データの格納先を生成するとともに、データの加工メソッドをSelectメソッドに渡す
            var result = list.Select( append ); 

            foreach( string s in result )
            {
                Console.WriteLine( s );
            }
        }

        private  static string append( string s )
        {
            return s + "hoge";
        }
    }
}

ここで、見慣れない書法が出て来ました。var result = list.Select( append );です。

list.Select()はメソッドですが、メソッドの引数としてメソッドの名前を指定しています。更にスッキリしなくなったので、色々調べ回ったところ、これはDelegateの一種で、Func<T, TResult>によるDelegateである、ということのようです。マイクロソフトの説明がここにあります。

メソッドとは、クラスに属する関数、もしくは手続きなので、メソッドに渡す引数は本来数値もしくは文字、文字列といった「値」です。しかし、今回のようにメソッドにメソッドを渡したい、というニーズがあり、このような拡張がなされたものと思います。今回の例では、Selectメソッドの中で、Listオブジェクトのメンバー全てに対して、メンバーを加工する方法をSelectメソッドに与えたいのです。

C#は進化し続ける高級言語のようで、あとから色々な仕様追加がなされていますね。勉強する側の立場からすると、言語仕様的にスッキリしなくてわかりにくい、という印象を受けます。

さて、リスト2で定義されてSelectメソッドに渡されているappendメソッドは、たった1行の小さなメソッドです。このぐらいのメソッドであれば、わざわざ別の場所で定義して使うよりも、もっと簡潔な書き方ができないか?ということで考えられたのが以下の書き方です。

var result = list.Select( (string s) => { return s + ".txt"; } );

こうやって、インライン展開して書くと、わざわざ別のところでメソッドを定義しなくてもよくなります。これがラムダ式なのだそうです。見慣れたラムダ式とは若干違いますね。

ここで、


  • 引数の型が自明(型推論できる)の場合は省略可能
  • メソッドが1文しかない場合は、{}やreturnを省略可能
  • 引数が1つしかない場合は()を省略可能


といったルールがあるようで、上記リストは

var result = list.Select( s => s + ".txt" );

と書き換えることが出来る、ということになります。これで、割によく見るラムダ式になりました。

これから、最初のリストは以下のように書き換えることが出来ます。

リスト3

using System;
using System.Collections.Generic;
using System.Linq;

namespace lamda3
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();
            list.Add("aaa");
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

            var result = list.Select( s => s + "hoge" );

            foreach( string s in result )       
            {
                Console.WriteLine( s );
            }
        }
    }
}

リスト1に書いたforeach節は、この1行のラムダ式で書き表された、ということになります。

さて、ここからは余談になります。

ここで、var result = list.Select( s => s + "hoge" );なる式でresultに結果を受け取ると、resultはIEnumerable<T>型となります。これを、ToList()メソッドでList<T> 型に変換してあげると、foreach節をより短い記述にすることが出来ます。

リスト4

using System;
using System.Collections.Generic;
using System.Linq;

namespace lamda3
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();
            list.Add("aaa");
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

            var result = list.Select( s => s + "hoge" ).ToList();

            result.ForEach( Console.WriteLine );
        }
    }
}

結果出力のforeach節も、1行で書き表されてしまいました。

今までの流れからすると、ラムダ式を使うとメソッドに手続きを渡すことが出来る、ということになります。ただ、手続きを受け取る側もそれに応じた書き方をする必要があるようです。それはまた別の機会に書こうと思います。

今回はここまでにします。


2018年4月3日火曜日

いよいよ輪切り画像の生成に取り組む

いよいよ、輪切り画像の生成に取り組みます。

輪切りする場所の座標を取得する


今まで何度かマウスカーソルの座標を取得し続ける、という動作を取り上げました。取得し続けるのはリアルタイム処理で難しそうですが、輪切りする場所の座標を取得する、という処理はそれ程難しくありません。pictureBoxの上でマウスボタンがクリックされたとき、そのクリックされた座標を取得すれば良いのです。

        /*
         * pictureboxの上でマウスをクリックしたときに発生するイベント
         */
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            // クリックされたときのクライアント座標をsaveして使う。
            // mouse_x/mouse_yはマウスイベントが入ると更新されてしまうので、それを防ぐ
            int x_slice = mouse_x, y_slice = mouse_y;

            MessageBox.Show("Mouse clicked X= " + x_slice + " : Y= " + y_slice);
            // データの中にある座標で、クリックした座用に最も近いものを得る(Nearest拡張メソッド)
            MessageBox.Show("Slice by " + x_axis_dt_int.Nearest(x_slice));
           // drawSlice();

        }

この中で、mouse_xとmouse_yは、MouseMoveイベントが発生するたびにマウスカーソルの座標が代入されるグローバル変数です。

このメソッドをpictureBox_Clickイベントへのイベントハンドラとして登録すると、クリックされた時点でのマウスの座標をx_slice、y_sliceに取り込むことができます。

この時点では、輪切り画像を実際に描画するメソッドをまだ作成していなかったので、取得したマウスカーソルの座標をMessageBoxに表示するようにしました。

実はこれ、その後の学習で、イベントハンドラの引数のeの中に、イベントが発生したときのマウスカーソルの座標の情報が含まれることがわかりました。それを参照すれば、もっとシンプルで外の影響を受けないコードを書くことが出来ます。

今回のケースでは、学習したときそのままのコードを引用すると言うことで、最初に書いたコードをそのまま載せました。

最も近い数値を拾ってくる


マウスで輪切りするポイントの座標を拾うとき、拾った座標の数値と全く同じ数値がデータの中にあるとは限りません。データが全て整数でできていて、マウスポインターの座標も整数であれば、全く同じ数値がデータの中に存在する可能性はありますが、それは特殊なケースと考えておくのが後々無難です。

プログラムの詳細仕様を考えていて、この問題に突き当たりました。これを解決する為には、指定した数値に最も近い数値をデータの中から探す、という機能が必要になります。

色々と調べていたら、発見しました。コガネブログ様です。

下記が、最も近い値を拾ってくる、ということを実現するコードです。この中には、拡張メソッド、という技術と、ラムダ式という技術が入っています。


using System;
using System.Collections.Generic;
using System.Linq;

public static class IEnumerableExtensions
{
    public static int Nearest(this IEnumerable < int > self, int target)
    {
        var min = self.Min(c => Math.Abs(c - target));
        return self.First(c => Math.Abs(c - target) == min);
    }
}

呼び出す側では、こう言う書き方をします。

         private void drawSlice()
        {
            Bitmap img = new Bitmap(Constants.size_slicebox_x, Constants.size_slicebox_y);
            MessageBox.Show("Slice by "+x_axis.Nearest(mouse_x));
        }

呼び出す側では、まだスライス画像を実際に描くコードの実装ができていなかったので、指定したx座標に最も近いx座標をx_axisオブジェクトから探し出して、メッセージボックスに表示するようにしています。


拡張メソッドは、こちら(いつも助けていただいている「++C++ //未確認飛行 C様」に説明がありますが、何やら難しくてよくわかりません。

ラムダ式についても、同じ所に説明があります。これも難しいです。時間をかけて理解を深める必要があります。

もう一つこう言う説明もあります。Qiitaの「今更ですが、ラムダ式」という記事です。

特にラムダ式は記述の仕方が通常のC#のコードとあまりにも違って、理解が難しいので、今後研究して理解することにして、今はこういう物を入れておくと意図通りの動きをする、というレベルにとどめておきたいと思います。

今回の記事では、
  • 測定データからプロットを描画
  • 描画したプロットをクリックして輪切り画像を作成したい場所の座標を取得
  • 取得した座標に最も近いデータ(今回はX軸のデータのみ)をリストから拾う
  • 取得した座標をメッセージボックスに表示した後、輪切りする座標をメッセージボックスに表示する
ということを行っています。

前回の記事に対して、輪切り画像を作成したい場所の座標の取得と、取得した座標に最も近いデータをリストから拾う、という大きな2つの機能を追加しています。

プログラムリストを以下に示します。Form1のデザインは前回の記事と全く同じで、イベントの追加もないので、Form1.Designer.csは割愛します。


2018年3月24日土曜日

MouseMoveイベントのON/OFF制御を実装しました(続き)

前回のエントリーでは、MouseMoveイベントを必要なときだけ発生させる処理を実装しました。MouseMoveイベントを有効にしたり無効にしたりする操作は、輪切り画像の生成モードへの遷移、輪切り画像生成モードから通常モードへの遷移と同時に行うようにするのが操作的にわかりやすいと思い、そのような仕様としました。

2つのCheckBox


前回のプログラムでは、MouseMoveイベントのON/OFF切替の為にcheckboxコントロールを1個設けましたが、今回は2つとしました。

輪切り画像生成モードとして、X軸と垂直な平面で輪切りをするモードとY軸に垂直な平面で輪切りにするモードの2つのモードを設けて、それぞれを排他的に動作させる、という構造にするため、モード切替用のcheckboxが2つ必要となったのです。

このcheckboxコントロールの働きは以下のようなものです。

checkboxの状態が変化したとき、

  checkboxがONになったのなら、
    →もう片方のcheckboxがONかOFFかを調査し、
       ・ONの場合は強制的にOFFにする。
       ・OFFの場合は何もしない。
    →MouseMoveイベントを有効にして、イベントが発生するようにする
  checkboxがOFFになったのなら、
    →MouseMoveイベントを無効にする

という処理の流れになりますが、ここで問題があります。checkboxの状態変化で発生するcheckbox_CheckedChangedイベントは、ユーザーのcheckbox操作だけでなく、checkboxの状態をソフトウエアで書き換えた場合でも発生してしまいます。

ソフトウエアで書き換えるのは、自分以外のcheckboxを強制的に無効とするためなので、ソフトウエアで状態を書き換えたときには、イベントが発生しないようにするか、もしくはイベントが発生したとしても何もしないようにしたいのです。

どのように解決したかというと、ソフトウエアによる内部的なcheckboxの書き換えによるイベント発生でることを表すbool型の変数(event_by_inner1)を設けました。

発生したcheckbox_CheckedChangedが内部からのソフトウエア書き換えによるものの場合にtrueとなる想定、初期値はfalseです。

イベントが発生しても、内部からの発生である場合はマウスイベントを無効にする処理だけ行って戻る、という構造です。もしかすると、組み込み系のマイコンソフトでよくある、割り込み処理中は割り込みをマスクしてしまう、というのと同じようにイベントを一旦無効にする処理にした方が、わかりやすいのかもしれません。

checkboxの処理部分を抜き出したのが下記のリストです。checkboxに対するイベントハンドラーとなっています。つまりcheckboxの数の分だけ必要で、今回は2組あります。ただし、イベントが同時に発生することはないので、event_by_innerフラグは1つだけあれば十分です。

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
    if (checkBox2.Checked && !event_by_inner1) {
     /*
    ** CheckedChangedイベントが発生したとき、まず反対側のcheckboxの状態を調べる
    ** 反対側のcheckboxが有効、かつ、内部からの書き換えによるイベント発生「ではない」場合
    ** 内部からの発生であることを示すフラグをtrueにして反対側のcheckboxの状態を無効に
    ** 書き換える。
    */
        event_by_inner1 = true;
        checkBox2.Checked = false;  // この書き換えでcheckbox2_CheckedChangedイベントが発生するはず
        // checkbox2.Checkedを書き換えたことによるイベントでは、event_by_inner1が
        // trueとなっているので
        // このif節は実行されずに次の行以降が実行される
    }

     event_by_inner1 = false;  // event_by_inner1をfalse(初期値)に書き換え

    if (checkBox1.Checked)    // checkbox1が有効となっているときは、このif節を実行
    {
        checkBox1.Text = "Push Button to stop getting mouse position X-axis";
        button1_on = true;
        mouse_x = mouse_y = 0; // マウス座標データ初期化
        // マウスイベント有効
        pictureBox1.MouseMove += 
            new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
    }
    else
    {   // ソフトウエア書き換えによるイベント発生の場合は、checkbox1.Checkedが必ずfalseなので必ずこちらが
        // 実行される(マウスイベント削除側)
        checkBox1.Text = "Push Button to start getting mouse position X-axis";
        button1_on = false;
        // マウスイベント削除
         pictureBox1.MouseMove -= 
             new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
    }
}

このコード、実は最初のif節の中を実行して、上のリストの例だとcheckBox2.Checked=false;でイベントが発生して、if節の直後のevent_by_inner1 = false;の実行前にイベントハンドラーから戻ってくることを期待しています。イベントの発生タイミングはシステムの状態によっても変わると思うので、本当はこう言う書き方をしてはいけないのかもしれません。やるのなら、時間的な処理の順番を担保する為にcheckBox2.Checked = false;の後に、何かイベント待ちの処理を入れるのが安全かもしれません。

これを書いていたときには、気にはなっていましたが、具体的な方法が思いつかず、動作検証では正しく動いているように見えていたので、深入りしていません。

OpenFileDiagram


上記のイベント処理とは無関係ですが、今までパス名を決め打ちでプログラム内に書いていたデータファイルを、今回からその都度指定できるようにしました。

実際にどうするかというと、
  • Formのデザイン時にデザイナーのツールボックスからOpenFileDiagramを探す
  • Formに貼り付ける
  • コードの編集でOpenFileDiagram型の変数を定義し、オブジェクトをnewで生成


あまり詳しい情報を見つけることができず、難儀しましたが、ここでお勉強させていただきました。いつものdobon.net様です。

実際に書いたコードが下記になります。注意すべきは、このOpenFileDiagramというコントロールは、ファイル名をファイルシステムの中から入手すること「だけ」をしてくれるのであって、ファイルを実際にオープンするのは別の処理で記述する必要があることです。

なお、OSのバージョン等によって外観が違うようです。私の環境で動作させると、図のような外観となります。


今回のユーザーインタフェースでは、テキストボックスとボタンを設けて、テキストボックスには仮のファイル名を入れておき、ボタンを押すとダイアグラムが開く、というインタフェースをとりました。下の図がFormのデザインで作成したFormの絵柄です。


他に、描画開始ボタンと終了ボタンを設けましたが、それぞれボタンのクリックイベントを使って描画メソッドを呼び出したり、終了処理をしたりしています。

        /*
         * ファイル名操作のボタンを押したときに発生するイベント
         * 
         * textboxにある文字列をとりあえずのファイル名としたうえで、OpenFileDialogを起動
         * そのうえで、OpenFileDialogで得たファイル名を変数fnに入れると共にtextboxにも入れる
         */
        private void button2_Click(object sender, EventArgs e)
        {
            fn = textBox1.Text;

            // OpenFileDialogクラスのインスタンスを作成
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.FileName = textBox1.Text;
            ofd.Filter = "CSVファイル(*.csv)|*.csv|テキストファイル(*.txt)|*.txt|全てのファイル(*.*)|*.*";
            ofd.FilterIndex = 0;
            ofd.Title = "開くファイルを選択してください";
            ofd.RestoreDirectory = true;
            ofd.CheckFileExists = true;
            ofd.CheckPathExists = true;

            if (ofd.ShowDialog() == DialogResult.OK)
            {
                fn = ofd.FileName;
            }
            textBox1.Text = fn;  //ダイアログで得たファイル名を一旦textboxに入れる
        }

ボタンをクリックしたときのイベントハンドラーの中で、OpenFileDiagramをオブジェクトを生成し、各種プロパティを設定して、ShowDialog()し、最終的にofd.FileNameにファイル名のフルパスが返ってくることになります。

これ以外の部分は、今までと同じです。これで、輪切り画像を生成する1歩手前まで来たと思います。

今回はこれで終わりにします。以下に全リストを示します。


Program.cs
/*
 * checkboxコントロールを使ってマウス座標の取り込みオンオフを切り替えていたものを
 * bottunコントロールに変えてみる試み
 * checkboxはボタンの大きさが自由にならないので使いにくい
 * bottunコントロールはウインドウデザインの時にサイズを好きに変えられる
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace EventOnOff
{
    /*
     * 定数をクラスとして定義する
     */
    static class Constants
    {
        public const string FileName = "";
        // public const string FileName = "c:\\users\\ぐすたふ\\検討用テストデータ.csv";
        public const int size_pic_box_x = 400;
        public const int size_pic_box_y = 400;
        public const int offset_x = (Constants.size_pic_box_x) / 2;
        public const int offset_y = Constants.size_pic_box_y - 100;
        public const int offset_z = 0;

    }


    static class Program
    {
        /// 
        /// アプリケーションのメイン エントリ ポイントです。
        /// 
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

Form1.cs
/*
 * checkboxコントロールを使ってマウス座標の取り込みオンオフを切り替えていたものを
 * bottunコントロールに変えてみる試み
 * checkboxはボタンの大きさが自由にならないので使いにくい
 * bottunコントロールはウインドウデザインの時にサイズを好きに変えられる
 * 
 * しかし、buttonコントロールを使う方法はうまくいかず、
 * checkboxで行くしかないことがわかった。ただし、checkboxのプロパティ
 * で、ボタンの大きさを自由に変えられるよう設定ができることがわかった。
 * たしか、プロパティのAutoSizeをfalseにしたはず。
 * 
 * pictureBox1のサイズがビルド時に勝手に変わって悩んだが、
 * Form1.AutoScaleModeにNoneを設定すると勝手に変わらなくなった。
 * 解像度が変わっても自動的にスケーリングしなくなるので、注意
 * 
 * なお、Formの大きさは変えられないように
 *          this.MaximumSize = this.Size;
 *          this.MinimumSize = this.Size;
 * の処理を入れておくのが楽かと。
 * 
 * 今まで、アプリケーションの終了にEnvironment.Exit()を使っていたが、これは色々な事情から
 * 使わない方がいいとのこと。
 * 参考:https://dobon.net/vb/dotnet/programing/applicationexit.html
 * 
 * ファイル名をTextBoxからも入力出来るようにしているので、TextBoxの中身が不正で
 * ファイルが見つからないときの処理を、プロットする関数の頭に入れた。
 * System.IO.FileExists(file)でテストし、見つからないときはMessageBoxでメッセージを出し、
 * プロット処理を行わずにreturnする。
 * 
 * イベントの有効、無効の切り替え処理を追加。ボタンを押したときのイベントの中で、マウスイベントの追加と
 * 削除をすることにより、切り替え。ただ、このような書き方が良いのかどうかはわからない。
 * 最終的には、X軸とY軸で排他的にスライスするので、ボタン2つでイベントの有効無効の切り替え処理にX/Yの区別を
 * 盛り込んで処理を分けるようにする
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.VisualBasic.FileIO;

namespace EventOnOff
{
    public partial class Form1 : Form
    {
        /*
         * 各種変数定義
         * なお、定数はmain部で定義
         */
        public static int mouse_x = 0, mouse_y = 0;       // マウスの座標
        public static ArrayList x_axis = new ArrayList(); // X座標の可変長配列
        public static ArrayList y_axis = new ArrayList(); // Y座標の可変長配列
        public static ArrayList z_axis = new ArrayList(); // Z座標の可変長配列
        public static ArrayList dt = new ArrayList();     // プロットデータ
        public static bool button1_on = false;             // ボタン1が押されているかどうか
        public static bool button2_on = false;             // ボタン2が押されているかどうか
        public static bool event_by_inner1 = false;         // ボタンの状態変化が捜査によるものか内部からの書き換えか
        //public static bool event_by_inner2 = false;         // ボタンの状態変化が操作によるものか内部からの書き換えか
        public string fn = Constants.FileName;             // データファイル名入れ
           
        public Form1()
        {
            InitializeComponent();

            this.MaximumSize = this.Size;
            this.MinimumSize = this.Size;
        }

        /*
         * Form1がロードされたときの処理(ロードされるときに発生するイベント)
         */
        private void Form1_Load(object sender, EventArgs e)
        {
            // マウス座標を取り込むモードかどうかをチェックして、その旨表示
            // 本来不要な処理だが、Form1がロードされるのがアプリケーション開始の時だけか
            // どうかあやふやなので入れておく
            if (button1_on)
            {
                this.checkBox1.Text = "Push Button to stop getting mouse position X-axis";
            }
            else
            {
                this.checkBox1.Text = "Push Button to start getting mouse position X-axis";
            }

            if( button2_on )
            {
                this.checkBox2.Text = "Push Button to stop getting mouse position Y-axis";
            }
            else
            {
                this.checkBox2.Text = "Push Button to start getting mouse position Y-axis"
;            }

            // データファイルのファイル名初期値の設定
            textBox1.Text = fn;

            // checkBox1にフォーカスを当てる
            this.ActiveControl = this.checkBox1;
        }

        /*
         * pictureBox1の上にマウスカーソルが乗った時に発生するイベント
         */
        private void pictureBox1_MouseHover(object sender, EventArgs e)
        {
            getMousePosition();
        }

        /*
         * pictureBox1の上のマウスが動いたときに発生するイベント
         */
        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            getMousePosition();
        }

        /*
         * Button1(終了ボタン)がクリックされたときに発生するイベント
         */
        private void button1_MouseClick(object sender, MouseEventArgs e)
        {
            Application.Exit();
            //Environment.Exit((int)0); // アプリケーション終了
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Application.Exit();
            //Environment.Exit((int)0); // アプリケーション終了
        }

        /*
         * checkbox1(X軸でスライスの指示)の状態が変化したときに発生するイベント
         * 
         * X軸でのスライス、Y軸でのスライスは、それぞれ排他的に動かなければいけないので、X軸実行中に
         * Y軸のボタンを押したらX軸を止めてY軸を動かすようにする。
         * そのためにボタンの状態をプログラムから変更すると、状態変化によるイベントが発生する。
         * ボタンの状態変化によるイベントの中で、自分以外のボタンの状態をクリアすると、
         * そのことによってまたイベントが発生し、自分の状態をクリアしてしまうことになるので、
         * ボタン操作によって発生したイベントか、プログラムによる状態変更のよって発生したイベントか
         * 区別するためのフラグを設けた。それがevent_by_inner1。bool型の変数。
         * プログラムによる状態変化のときはtrueとなる前提。
         *
         */
        private void checkBox1_CheckedChanged(object sender, EventArgs e)
        {
            if (checkBox2.Checked && !event_by_inner1)
            {
                event_by_inner1 = true;
                checkBox2.Checked = false;
            }

            event_by_inner1 = false;

            if (checkBox1.Checked)
            {
                checkBox1.Text = "Push Button to stop getting mouse position X-axis";
                button1_on = true;
                mouse_x = mouse_y = 0; // マウス座標データ初期化
                // マウスイベント有効
                pictureBox1.MouseHover += new System.EventHandler(pictureBox1_MouseHover);
                pictureBox1.MouseMove += new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
            }
            else
            {
                checkBox1.Text = "Push Button to start getting mouse position X-axis";
                button1_on = false;
                // マウスイベント削除
                pictureBox1.MouseHover -= new System.EventHandler(pictureBox1_MouseHover);
                pictureBox1.MouseMove -= new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
            }
        }

        /*
         * checkbox2(Y軸でスライスの指示)の状態が変化したときに発生するイベント
         * 
         * X軸でのスライス、Y軸でのスライスは、それぞれ排他的に動かなければいけないので、X軸実行中に
         * Y軸のボタンを押したらX軸を止めてY軸を動かすようにする。
         * そのためにボタンの状態をプログラムから変更すると、状態変化によるイベントが発生する。
         * ボタンの状態変化によるイベントの中で、自分以外のボタンの状態をクリアすると、
         * そのことによってまたイベントが発生し、自分の状態をクリアしてしまうことになるので、
         * ボタン操作によって発生したイベントか、プログラムによる状態変更のよって発生したイベントか
         * 区別するためのフラグを設けた。それがevent_by_inner1。bool型の変数。
         * プログラムによる状態変化の時はtureとなる前提
         */
        private void checkBox2_CheckedChanged(object sender, EventArgs e)
        {

            if (checkBox1.Checked && !event_by_inner1)
            {
                event_by_inner1 = true;
                checkBox1.Checked = false;
            }

            event_by_inner1 = false;

            if (checkBox2.Checked)
            {
                checkBox2.Text = "Push Button to stop getting mouse position Y-axis";
                button2_on = true;
                mouse_x = mouse_y = 0; // マウス座標データ初期化
                // マウスイベント有効
                pictureBox1.MouseHover += new System.EventHandler(pictureBox1_MouseHover);
                pictureBox1.MouseMove += new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
            }
            else
            {
                checkBox2.Text = "Push Button to start getting mouse position Y-axis";
                button2_on = false;
                // マウスイベント削除
                pictureBox1.MouseHover -= new System.EventHandler(pictureBox1_MouseHover);
                pictureBox1.MouseMove -= new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
            }
        }

        /*
         * ファイル名操作のボタンを押したときに発生するイベント
         * 
         * textboxにある文字列をとりあえずのファイル名としたうえで、OpenFileDialogを起動
         * そのうえで、OpenFileDialogで得たファイル名を変数fnに入れると共にtextboxにも入れる
         */
        private void button2_Click(object sender, EventArgs e)
        {
            fn = textBox1.Text;

            // OpenFileDialogクラスのインスタンスを作成
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.FileName = textBox1.Text;
            ofd.Filter = "CSVファイル(*.csv)|*.csv|テキストファイル(*.txt)|*.txt|全てのファイル(*.*)|*.*";
            ofd.FilterIndex = 0;
            ofd.Title = "開くファイルを選択してください";
            ofd.RestoreDirectory = true;
            ofd.CheckFileExists = true;
            ofd.CheckPathExists = true;

            if (ofd.ShowDialog() == DialogResult.OK)
            {
                fn = ofd.FileName;
            }
            textBox1.Text = fn;  //ダイアログで得たファイル名を一旦textboxに入れる
        }

        /*
         * 描画ボタンが押されたときに発生するイベント
         */
        private void button3_Click(object sender, EventArgs e)
        {
            Plot_Data();
        }

        /*
         * データをプロットする
         */
        private void Plot_Data()
        {
            Color col = new Color();
            Bitmap img = new Bitmap(Constants.size_pic_box_x, Constants.size_pic_box_y);

            fn = textBox1.Text;  // プロット指示が出た時点のtextboxの値をファイル名とする

            if (!System.IO.File.Exists(fn))
            {
                MessageBox.Show("ファイルが見つかりません : " + fn);
                return;
            }

            try
            {
                using (TextFieldParser parser = new TextFieldParser(fn, System.Text.Encoding.GetEncoding("Shift_JIS")))
                {
                    parser.TextFieldType = FieldType.Delimited;
                    parser.SetDelimiters(","); // 区切り文字はコンマ
                    parser.CommentTokens = new string[1] { "#" };
                    int line = 0;

                    while (!parser.EndOfData)
                    {
                        //++line;
                        //col = 0;

                        string[] row = parser.ReadFields(); // 1行読んで、要素に分解、配列rowに格納

                        x_axis.Add(row[0]);  // ArrayListにX軸座標を追加
                        y_axis.Add(row[1]);  // ArrayListにY座標を追加
                        z_axis.Add(row[2]);  // ArrayListにZ座標を追加
                        dt.Add(row[3]);      // ArrayListにデータ本体を追加
                        ++line;
                    }
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.Message + "\n終了します");
                Application.Exit();
            }

            for (int i = 0; i < x_axis.Count; ++i)
            {
                //
                // 実座標をウィンドウ座標に変換するためにオフセット加算
                //
                int x = (int)Convert.ToDouble(x_axis[i]) + Constants.offset_x;
                int y = -1 * ((int)Convert.ToDouble(y_axis[i])) + Constants.offset_y;
                int z = (int)Convert.ToDouble(z_axis[i]) + Constants.offset_z;
                int d = (int)Convert.ToDouble(dt[i]);

                // プロットデータの内容から描画色を決定
                switch (d)
                {
                    case 0:
                        col = Color.Black;
                        break;
                    case 1:
                        col = Color.Blue;
                        break;
                    case 2:
                        col = Color.Red;
                        break;
                    case 3:
                        col = Color.Purple;
                        break;
                    case 4:
                        col = Color.Green;
                        break;
                    case 5:
                        col = Color.Yellow;
                        break;
                    case 6:
                        col = Color.Violet;
                        break;
                    case 7:
                        col = Color.White;
                        break;
                    default:
                        col = Color.Magenta;
                        break;
                }

                img.SetPixel(x, y, col);
            }

            pictureBox1.Image = img;

            MessageBox.Show("Plot Completed.");
        }



        /*
         * マウスの画面座標をcp.X、cp.Yから拾って、pictureBoxのクライアント座標
         * に変換して、ウィンドウの上のテキストエリアに表示
         */
        private void getMousePosition()
        {

            System.Drawing.Point cp = pictureBox1.PointToClient(Cursor.Position);

            mouse_x = cp.X - Constants.offset_x;
            mouse_y = -1 * cp.Y + Constants.offset_y;

            if (button1_on)
            {
                mouse_y = 0;
            }
            else
            {
                mouse_x = 0;
            }
            label1.Text = "x-size=" + pictureBox1.Size.Width;
            this.Text = "x=" + mouse_x + ":y=" + mouse_y + "(x=" + cp.X + ":y=" + cp.Y + ")";

        }
    }

}

Form1.Designer.cs
namespace EventOnOff
{
    partial class Form1
    {
        /// 
        /// 必要なデザイナー変数です。
        /// 
        private System.ComponentModel.IContainer components = null;

        /// 
        /// 使用中のリソースをすべてクリーンアップします。
        /// 
        /// マネージ リソースを破棄する場合は true を指定し、その他の場合は false を指定します。
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows フォーム デザイナーで生成されたコード

        /// 
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// 
        private void InitializeComponent()
        {
            this.button1 = new System.Windows.Forms.Button();
            this.label1 = new System.Windows.Forms.Label();
            this.pictureBox1 = new System.Windows.Forms.PictureBox();
            this.checkBox1 = new System.Windows.Forms.CheckBox();
            this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog();
            this.button2 = new System.Windows.Forms.Button();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.groupBox1 = new System.Windows.Forms.GroupBox();
            this.button3 = new System.Windows.Forms.Button();
            this.checkBox2 = new System.Windows.Forms.CheckBox();
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
            this.groupBox1.SuspendLayout();
            this.SuspendLayout();
            // 
            // button1
            // 
            this.button1.Location = new System.Drawing.Point(10, 570);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(400, 29);
            this.button1.TabIndex = 0;
            this.button1.Text = "プログラムの終了";
            this.button1.UseVisualStyleBackColor = true;
            this.button1.Click += new System.EventHandler(this.button1_Click);
            this.button1.MouseClick += new System.Windows.Forms.MouseEventHandler(this.button1_MouseClick);
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(188, 413);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(35, 12);
            this.label1.TabIndex = 2;
            this.label1.Text = "label1";
            // 
            // pictureBox1
            // 
            this.pictureBox1.BackColor = System.Drawing.SystemColors.ControlLightLight;
            this.pictureBox1.Location = new System.Drawing.Point(10, 10);
            this.pictureBox1.Name = "pictureBox1";
            this.pictureBox1.Size = new System.Drawing.Size(400, 400);
            this.pictureBox1.TabIndex = 3;
            this.pictureBox1.TabStop = false;
            // 
            // checkBox1
            // 
            this.checkBox1.Appearance = System.Windows.Forms.Appearance.Button;
            this.checkBox1.Location = new System.Drawing.Point(10, 532);
            this.checkBox1.Name = "checkBox1";
            this.checkBox1.Size = new System.Drawing.Size(195, 32);
            this.checkBox1.TabIndex = 4;
            this.checkBox1.Text = "checkBox1";
            this.checkBox1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
            this.checkBox1.UseVisualStyleBackColor = true;
            this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged);
            // 
            // openFileDialog1
            // 
            this.openFileDialog1.FileName = "openFileDialog1";
            // 
            // button2
            // 
            this.button2.Location = new System.Drawing.Point(361, 21);
            this.button2.Name = "button2";
            this.button2.Size = new System.Drawing.Size(33, 23);
            this.button2.TabIndex = 5;
            this.button2.Text = "...";
            this.button2.UseVisualStyleBackColor = true;
            this.button2.Click += new System.EventHandler(this.button2_Click);
            // 
            // textBox1
            // 
            this.textBox1.Location = new System.Drawing.Point(6, 21);
            this.textBox1.Name = "textBox1";
            this.textBox1.Size = new System.Drawing.Size(349, 19);
            this.textBox1.TabIndex = 6;
            // 
            // groupBox1
            // 
            this.groupBox1.Controls.Add(this.textBox1);
            this.groupBox1.Controls.Add(this.button2);
            this.groupBox1.Location = new System.Drawing.Point(10, 439);
            this.groupBox1.Name = "groupBox1";
            this.groupBox1.Size = new System.Drawing.Size(400, 54);
            this.groupBox1.TabIndex = 7;
            this.groupBox1.TabStop = false;
            this.groupBox1.Text = "データファイル";
            // 
            // button3
            // 
            this.button3.Location = new System.Drawing.Point(10, 499);
            this.button3.Name = "button3";
            this.button3.Size = new System.Drawing.Size(400, 27);
            this.button3.TabIndex = 8;
            this.button3.Text = "描画";
            this.button3.UseVisualStyleBackColor = true;
            this.button3.Click += new System.EventHandler(this.button3_Click);
            // 
            // checkBox2
            // 
            this.checkBox2.Appearance = System.Windows.Forms.Appearance.Button;
            this.checkBox2.Location = new System.Drawing.Point(215, 532);
            this.checkBox2.Name = "checkBox2";
            this.checkBox2.Size = new System.Drawing.Size(195, 32);
            this.checkBox2.TabIndex = 9;
            this.checkBox2.Text = "checkBox2";
            this.checkBox2.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
            this.checkBox2.UseVisualStyleBackColor = true;
            this.checkBox2.CheckedChanged += new System.EventHandler(this.checkBox2_CheckedChanged);
            // 
            // Form1
            // 
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit;
            this.ClientSize = new System.Drawing.Size(429, 611);
            this.Controls.Add(this.checkBox2);
            this.Controls.Add(this.button3);
            this.Controls.Add(this.groupBox1);
            this.Controls.Add(this.checkBox1);
            this.Controls.Add(this.pictureBox1);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.button1);
            this.MaximizeBox = false;
            this.MinimizeBox = false;
            this.Name = "Form1";
            this.Text = "Form1";
            this.Load += new System.EventHandler(this.Form1_Load);
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
            this.groupBox1.ResumeLayout(false);
            this.groupBox1.PerformLayout();
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.Button button1;
        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.PictureBox pictureBox1;
        private System.Windows.Forms.CheckBox checkBox1;
        private System.Windows.Forms.OpenFileDialog openFileDialog1;
        private System.Windows.Forms.Button button2;
        private System.Windows.Forms.TextBox textBox1;
        private System.Windows.Forms.GroupBox groupBox1;
        private System.Windows.Forms.Button button3;
        private System.Windows.Forms.CheckBox checkBox2;
    }
}

2018年3月16日金曜日

MouseMoveイベントのON/OFF制御を実装しました

前回のエントリーで、CSVファイルからデータを取り込んでウインドウに描画する処理ができるようになりました。また、マウスイベントを使って、マウスカーソルの座標を取得し続けることも出来ています。

しかし、マウスカーソルの座標を取得し続ける必要があるのは、ある特定の条件の時だけ、今回のプログラムでは、輪切り画像を作るときだけです。それ以外の時には、イベントが発生しないでいてほしいのです。

ということで、色々調べ回って試行錯誤して、出来上がったのか下記リストです。前回のエントリーでやったことに対して、マウスカーソル座標の取得をcheckboxを使って有効にしたり無効にしたりできるようにしてあります。

checkBox1_CheckedChangedメソッドの中にその処理が書いてありますが、Delegateの登録と削除を普通にすればいいようです。感覚的には、一度登録しておいて、不要なときはマスク、ということにしたかったのですが、マスクする機能はどうやらないようで、マスクするためには、イベントハンドラーの中でマスク条件を吟味して不要なときは処理をしない、というコードを書かなければならず、無駄な処理になります。

例によって、Mainメソッドは省略です。
using System;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.VisualBasic.FileIO;

namespace CSVtoData2
{
    public partial class Form1 : Form
    {
        public static int mouse_x = 0, mouse_y = 0;

        public static ArrayList x_axis = new ArrayList();
        public static ArrayList y_axis = new ArrayList();
        public static ArrayList z_axis = new ArrayList();
        public static ArrayList dt = new ArrayList();

        public static bool button_on = false;

        public Form1()
        {
            InitializeComponent();
            Bitmap img = new Bitmap(Constants.size_pic_box_x, Constants.size_pic_box_y);

            Color col = new Color();

            try
            {
                using(TextFieldParser parser =
                    new TextFieldParser(Constants.FileName, 
                    System.Text.Encoding.GetEncoding("Shift_JIS")))
                {
                    parser.TextFieldType = FieldType.Delimited;
                    parser.SetDelimiters(","); // 区切り文字はコンマ
                    parser.CommentTokens = new string[1] { "#" };
                    int line = 0;

                    while (!parser.EndOfData)
                    {
                        string[] row = parser.ReadFields(); // 1行読んで、要素に分解、配列rowに格納

                        x_axis.Add(row[0]);  // ArrayListにX軸座標を追加
                        y_axis.Add(row[1]);  // ArrayListにY座標を追加
                        z_axis.Add(row[2]);  // ArrayListにZ座標を追加
                        dt.Add(row[3]);      // ArrayListにデータ本体を追加
                        ++line;
                    }
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.Message);
                Environment.Exit(0); // エラーが出たら強制終了
            }

            for( int i=0; i<x_axis.Count; ++i )
            {
                //
                // 実座標をウィンドウ座標に変換するためにオフセット加算
                //
                int x = (int)Convert.ToDouble(x_axis[i]) + Constants.offset_x;
                int y = -1 * ((int)Convert.ToDouble(y_axis[i])) + Constants.offset_y;
                int z = (int)Convert.ToDouble(z_axis[i]) + Constants.offset_z; 
                int d = (int)Convert.ToDouble(dt[i]);
          
                switch( d )
                {
                    case 0:
                        col = Color.Black;
                        break;
                    case 1:
                        col = Color.Blue;
                        break;
                    case 2:
                        col = Color.Red;
                        break;
                    case 3:
                        col = Color.Purple;
                        break;
                    case 4:
                        col = Color.Green;
                        break;
                    case 5:
                        col = Color.Yellow;
                        break;
                    case 6:
                        col = Color.Violet;
                        break;
                    case 7:
                        col = Color.White;
                        break;
                    default:
                        col = Color.Magenta;
                        break;
                }

                img.SetPixel(x, y, col);    
            }

            pictureBox1.Image = img;
            this.checkBox1.Focus();


        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            getMousePosition();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Environment.Exit((int)0);
        }

        private void checkBox1_CheckedChanged(object sender, EventArgs e)  //checkBox1イベント
        {
            if (checkBox1.Checked)
            {   //checkBox1がチェック状態であれば割り込みハンドラー登録
                this.pictureBox1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.pictureBox1_MouseMove);
                checkBox1.Text = "push to event off";
                button_on = true;
            }
            else
            {   //checkBox1がチェックされていない状態であれば割り込みハンドラー登録削除
                this.pictureBox1.MouseMove -=  new System.Windows.Forms.MouseEventHandler(this.pictureBox1_MouseMove);
                checkBox1.Text = "push to event on";
                button_on = false;
            }
        }

        /*
         * Form1が起動したときに、チェックボックスにフォーカスが当たるようにする
         */
        private void Form1_Load(object sender, EventArgs e)
        {
            this.ActiveControl = this.checkBox1;
        }

        private void getMousePosition()
        {
            if (checkBox1.Checked )
            {
                System.Drawing.Point cp = pictureBox1.PointToClient(Cursor.Position);

                mouse_x = cp.X - Constants.offset_x;
                mouse_y = -1 * cp.Y + Constants.offset_y;

                label1.Text = "x-size=" + pictureBox1.Size.Width;
                this.Text = "x=" + mouse_x + ":y=" + mouse_y + "(x=" + cp.X + ":y=" + cp.Y + ")";
            }
            else
            {
                mouse_x = mouse_y = (int)0xffff;
                label1.Text = "Not Focused";
                this.Text = "x=" + mouse_x + ":y=" + mouse_y;
            }

        }
    }
}
Formのデザインは上記のような感じで、Formの大きさ等は本質ではないので任意に。また、checkBoxは、プロパティの操作で外見をボタンのようにすることができるので、それを使ってボタンのような外見にしています(Appearanceプロパティです)。このフォームからできるVisual Studioの出力(Form1.Designer.cs)は、以下です。


namespace CSVtoData2
{
    partial class Form1
    {
        /// 
        /// 必要なデザイナー変数です。
        /// 
        private System.ComponentModel.IContainer components = null;

        /// 
        /// 使用中のリソースをすべてクリーンアップします。
        /// 
        /// マネージ リソースを破棄する場合は true を指定し、その他の場合は false を指定します。
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows フォーム デザイナーで生成されたコード

        /// 
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// 
        private void InitializeComponent()
        {
            this.pictureBox1 = new System.Windows.Forms.PictureBox();
            this.label1 = new System.Windows.Forms.Label();
            this.button2 = new System.Windows.Forms.Button();
            this.checkBox1 = new System.Windows.Forms.CheckBox();
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
            this.SuspendLayout();
            // 
            // pictureBox1
            // 
            this.pictureBox1.BackColor = System.Drawing.SystemColors.Window;
            this.pictureBox1.Location = new System.Drawing.Point(38, 13);
            this.pictureBox1.Margin = new System.Windows.Forms.Padding(4);
            this.pictureBox1.Name = "pictureBox1";
            this.pictureBox1.Size = new System.Drawing.Size(400, 400);
            this.pictureBox1.TabIndex = 0;
            this.pictureBox1.TabStop = false;
            //this.pictureBox1.MouseHover += new System.EventHandler(this.pictureBox1_MouseHover);
            //this.pictureBox1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.pictureBox1_MouseMove);
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Font = new System.Drawing.Font("MS UI Gothic", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(128)));
            this.label1.Location = new System.Drawing.Point(197, 417);
            this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(55, 19);
            this.label1.TabIndex = 1;
            this.label1.Text = "label1";
            // 
            // button2
            // 
            this.button2.Location = new System.Drawing.Point(38, 507);
            this.button2.Name = "button2";
            this.button2.Size = new System.Drawing.Size(400, 28);
            this.button2.TabIndex = 3;
            this.button2.Text = "プログラム終了";
            this.button2.UseVisualStyleBackColor = true;
            this.button2.Click += new System.EventHandler(this.button2_Click);
            // 
            // checkBox1
            // 
            this.checkBox1.Appearance = System.Windows.Forms.Appearance.Button;
            this.checkBox1.AutoSize = true;
            this.checkBox1.Location = new System.Drawing.Point(38, 476);
            this.checkBox1.Name = "checkBox1";
            this.checkBox1.Size = new System.Drawing.Size(89, 25);
            this.checkBox1.TabIndex = 4;
            this.checkBox1.Text = "checkBox1";
            this.checkBox1.UseVisualStyleBackColor = true;
            this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(482, 547);
            this.Controls.Add(this.checkBox1);
            this.Controls.Add(this.button2);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.pictureBox1);
            this.Margin = new System.Windows.Forms.Padding(4);
            this.Name = "Form1";
            this.Text = "Form1";
            this.Load += new System.EventHandler(this.Form1_Load);
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.PictureBox pictureBox1;
        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.Button button2;
        private System.Windows.Forms.CheckBox checkBox1;
    }
}

動作させると下の動画のようになります。注目いただきたいのは、 小さな押しボタンの状態でウインドウ左上の数値の表示が動いたり止まったりすること。動いているときはMouseMoveイベントを受け付けているとき、止まっているときはMouseMoveイベントが受け付けられていないときです。
   
今回はこれで終わりにします。

2018年3月14日水曜日

CSVのデータを変数に一旦格納、型変換して描画処理を行います

CSVファイルを読めるようになったら、次は各フィールドのデータを、後で使いやすいようにどこかにしまっておく必要があります。

普通に考えるとしまっておくのに最適な場所は配列ではないかと考えると思います。しかし、今回のプログラムはデータの大きさが既知ではないことを想定しています。通常の配列は宣言時にデータの大きさを宣言しなければならず、通常の配列は使えないと当初から思っていました。

データの大きさが決まっていないような場合に、動的にサイズを変える仕組みがある入れ物はないだろうか?と考えて、探してみたら、C#にはArrayListというものがあります。前のエントリーでちょっと出て来ましたね。

前回のエントリーに出てきたプログラムを改造して、TextFieldParserで分割したフィールドデータをArrayListに格納していくコードを作成しました。動かした結果は前回と同じですが、内部的にはArrayListの行を追加しながら画面にも表示する、という動作になっています。詳細は、プログラム中のコメントをご覧下さい。

なお、CSVファイルの中身ですが、1行の作りが

測定点のX座標,測定点のY座標,測定点のZ座標,測定点のデータ本体

という作りになっています。1行に測定点1点分のデータが入っており、測定点の数だけ行がある、という構造です。今回のプログラム評価用のデータは、全て整数です。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualBasic.FileIO;

namespace getDataFromCSV
{
    class Program
    {
        //string line = "";
        //ArrayList al     = new ArrayList();
        public static ArrayList x_axis = new ArrayList(); // X軸データ用ArrayList
        public static ArrayList y_axis = new ArrayList(); // Y軸データ用ArrayList
        public static ArrayList z_axis = new ArrayList(); // Z軸データ用ArrayList
        public static ArrayList dt = new ArrayList(); // データ本体用ArrayList

        static void Main(string[] args)
        {
            getData("c:\\users\\gustav\\検討用テストデータ.csv");

            Console.ReadKey();
        }

        static void getData(string file)
        {
            try
            {
                using (
                    TextFieldParser parser =
                    new TextFieldParser(file, System.Text.Encoding.GetEncoding("Shift_JIS"))
                    )
                {
                    parser.TextFieldType = FieldType.Delimited;
                    parser.SetDelimiters(","); // 区切り文字はコンマ
                    parser.CommentTokens = new string[1] { "#" };
                    int line = 0, col = 0;

                    while (!parser.EndOfData)
                    {
                        string[] row = parser.ReadFields();
                        x_axis.Add(row[0]);  // ここでArreyListの行を追加
                        y_axis.Add(row[1]);
                        z_axis.Add(row[2]);
                        dt.Add(row[3]);

                        Console.Write(line + " : ");  //画面出力
                        Console.Write("X=" + (string)x_axis[line]);  // メンバーの参照はこのように行う
                        Console.Write(" | Y=" + (string)y_axis[line]);
                        Console.Write(" | Z=" + (string)z_axis[line]);
                        Console.Write(" | DATA=" + (string)dt[line]);
                        Console.WriteLine(" ");

                        ++line;
                    }
                    Console.WriteLine("Line 3236 " + (string)dt[3235]);
                    // 行番号とデータが合っているかどうかの確認用
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

これでCSVから各フィールドのデータをstringデータながら拾い上げて配列におさめることができるようになりました。

今回は、さらにこの後、データを適切に型変換して、実際に使って見るところまで進めたいと思います。

以下のリストは、以下の処理をしています。

  • CSVファイルからデータを読み出して、配列に格納
  • 配列データを順に読んで、型変換を行う。(TextFieldParserはstring型のデータを返す仕様)
  • 型変換したデータに対して、座標データはデータが持つ座標系とウインドウの座標系の変換を行う(ウインドウの座標で描画しなければならない)
  • データ本体は、データの中身に応じて描画するピクセルの色を割り当てる
  • 上記で得られたデータを使って、X座標とY座標とデータの中身でBitmapオブジェクトに描画
  • PictureBoxのImageプロパティにBitmapを設定
  • マウスカーソルの座標を取得し続け、ウインドウの上部に座標を表示
座標系の変換ですが、特に難しい変換は不要で、単なる足し算です。Form上のPictureBoxの持つ座標系は左上の頂点が原点となります。これに対して、データの持つ座標系はでは、PictureBoxの都合の良い場所に、例えば図のような場所に置くことになると思います。

データは、あくまでも図のデータ座標系座標原点の場所が原点となっているデータなので、それをPictureBoxの座標系に変換するには、データ座標系の原点のPictureBox内に於ける座標を加えてあげれば変換できます。

このプログラムでは、Program.cs内のstatic class Constantsで計算の為のオフセット値を定数として定義しています。

また、マウスカーソルの座標を取得し続ける処理では、PictureBox座標をデータ座標に変換してから、ウインドウに表示しています。この処理も考え方は同じで、PictureBox座標で取得されるマウスカーソル座標からオフセット分を引き算して、データ座標を求めています。

ファイルProgram.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;


namespace CSVtoData
{
    static class Constants  // Mainメソッドが入っているファイルで、定数の定義をしています。
    {
        public const string FileName = "c:\\users\\gustav\\検討用テストデータ.csv";
        public const int offset_x = 640 / 2;
        public const int offset_y = 500;
        public const int offset_z = 0;

    }

    static class Program
    {
        /// 
        /// アプリケーションのメイン エントリ ポイントです。
        /// 
        [STAThread]

        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

ファイルForm1.cs
using System;

using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.VisualBasic.FileIO;

namespace CSVtoData
{
    public partial class Form1 : Form
    {
        public static int mouse_x = 0, mouse_y = 0;  // このForm1クラスの中で、広域的に参照できるようにこの定義方法です。

        public static ArrayList x_axis = new ArrayList();
        public static ArrayList y_axis = new ArrayList();
        public static ArrayList z_axis = new ArrayList();
        public static ArrayList dt = new ArrayList();

        public Form1()
        {
            InitializeComponent();

            this.MaximumSize = this.Size;
            this.MinimumSize = this.Size;

            Bitmap img = new Bitmap(600, 720);

            Color col = new Color();

            try
            {
                using (TextFieldParser parser =
                    new TextFieldParser(Constants.FileName, System.Text.Encoding.GetEncoding("Shift_JIS")))
                {
                    parser.TextFieldType = FieldType.Delimited;
                    parser.SetDelimiters(","); // 区切り文字はコンマ
                    parser.CommentTokens = new string[1] { "#" };
                    int line = 0;

                    while (!parser.EndOfData)
                    {
                        //++line;
                        //col = 0;

                        string[] row = parser.ReadFields(); // 1行読んで、要素に分解、配列rowに格納

                        x_axis.Add(row[0]);  // ArrayListにX軸座標を追加
                        y_axis.Add(row[1]);  // ArrayListにY座標を追加
                        z_axis.Add(row[2]);  // ArrayListにZ座標を追加
                        dt.Add(row[3]);      // ArrayListにデータ本体を追加
                        ++line;
                    }
                }
            }
            catch (Exception e)  // TextFieldParser処理中にエラーが出たら、エラーメッセージを出してプログラム終了
            {
                MessageBox.Show(e.Message);
                Environment.Exit(0); // 強制終了
            }

            for (int i = 0; i < x_axis.Count; ++i)
            {
                int x = (int)Convert.ToDouble(x_axis[i]) + Constants.offset_x;  // 座標データの型変換とオフセット加算
                int y = -1 * ((int)Convert.ToDouble(y_axis[i])) + Constants.offset_y;
                int z = (int)Convert.ToDouble(z_axis[i]) + Constants.offset_z;
                int d = (int)Convert.ToDouble(dt[i]);

                switch (d)  // ここでデータ本体と色の結びつけをしています。
                {
                    case 0:
                        col = Color.Black;
                        break;
                    case 1:
                        col = Color.Blue;
                        break;
                    case 2:
                        col = Color.Red;
                        break;
                    case 3:
                        col = Color.Purple;
                        break;
                    case 4:
                        col = Color.Green;
                        break;
                    case 5:
                        col = Color.Yellow;
                        break;
                    case 6:
                        col = Color.Violet;
                        break;
                    case 7:
                        col = Color.White;
                        break;
                    default:
                        col = Color.Magenta;
                        break;
                }

                img.SetPixel(x, y, col);  // データがCSVに入っている順番で、プロットします。
            }
            pictureBox1.Image = img;
        }
        // MouseMoveイベントハンドラー
        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            getMousePosition();
        }

        private void getMousePosition()  // このメソッドは、マウスの座標を取得し続けるエントリーで出てきたものそのまま。
        {
            if (this.Focused)
            {
                System.Drawing.Point cp = pictureBox1.PointToClient(Cursor.Position);

                mouse_x = cp.X - Constants.offset_x;
                mouse_y = -1 * cp.Y + Constants.offset_y;

                label1.Text = "Focused";
                this.Text = "x=" + mouse_x + ":y=" + mouse_y;
            }
            else
            {
                mouse_x = mouse_y = (int)0xffff;
                label1.Text = "Not Focused";
                this.Text = "x=" + mouse_x + ":y=" + mouse_y;
            }

        }
    }
}


ファイルForm1.Designer.cs
このリストは、Visual Studioが自動的に吐き出すもので、使用されるイベントや各コントロールのプロパティが記述されています。このファイルがないと、ビルドができません。内容は特に説明しませんが、PictureBox1の最後の行が、PictureBox1.MouseMoveイベント関連の記述です。

namespace CSVtoData
{
    partial class Form1
    {
        /// 
        /// 必要なデザイナー変数です。
        /// 
        private System.ComponentModel.IContainer components = null;

        /// 
        /// 使用中のリソースをすべてクリーンアップします。
        /// 
        /// マネージ リソースを破棄する場合は true を指定し、その他の場合は false を指定します。
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows フォーム デザイナーで生成されたコード

        /// 
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// 
        private void InitializeComponent()
        {
            this.pictureBox1 = new System.Windows.Forms.PictureBox();
            this.label1 = new System.Windows.Forms.Label();
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
            this.SuspendLayout();
            // 
            // pictureBox1
            // 
            this.pictureBox1.Location = new System.Drawing.Point(12, 9);
            this.pictureBox1.Name = "pictureBox1";
            this.pictureBox1.Size = new System.Drawing.Size(600, 703);
            this.pictureBox1.TabIndex = 0;
            this.pictureBox1.TabStop = false;
            //this.pictureBox1.MouseHover += new System.EventHandler(this.pictureBox1_MouseHover);
            this.pictureBox1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.pictureBox1_MouseMove);
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Font = new System.Drawing.Font
                    ("MS UI Gothic", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(128)));
            this.label1.Location = new System.Drawing.Point(271, 728);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(43, 15);
            this.label1.TabIndex = 1;
            this.label1.Text = "label1";
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(624, 761);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.pictureBox1);
            this.Name = "Form1";
            this.Text = "Form1";
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.PictureBox pictureBox1;
        private System.Windows.Forms.Label label1;
    }
}

上記プログラムを実行すると、このようなウインドウが出ます。見にくいですが、ウインドウの中に、小さな点が沢山描画されていて、それで四角形を形成しているのがわかります。

そして、ウインドウ左上に、マウスカーソルの座標が表示され続けます。

これで、目標仕様を実現するための要素技術として、


  1. ウインドウに点が打てる
  2. ファイルの読み出しが出来る
  3. ウインドウ上にあるマウスの座標が読み取れる
  4. ウインドウの座標系とデータの座標系の変換ができる

と、6項目中4項目が実現出来ています。

今回はここまでにします。


2018年3月13日火曜日

CSVファイルの処理方法について

目標としているプログラムでは、データはCSV形式のファイルで渡されることを前提にしているので、CSVファイルを読み込んで、CSVデータの解釈をして、分解したデータを適切な変数に格納する、という動作が必要となります。

CSVとは、comma-separated valuesの略で、いくつかのフィールド(項目)を区切り文字であるカンマで区切ったテキストデータおよびテキストファイルのことです。(Wikipediaより)
、読んだテキストを分解する、ということができれば、あとはデータ変換をして変数に格納、とう流れに持って行けると思います。

このような処理は、たとえばawkだとsplitを使って文字列分割をするとか、perlだと正規表現を使って数値を拾うといった処理を使うと簡単です。たとえば,
0,200,-100,8
0,195,-100,8
0,190,-100,8
0,185,-100,8
0,180,-100,8
0,175,-100,8
0,170,-100,8
0,165,-100,8
0,160,-100,8
0,155,-100,8
といったCSVファイルがある場合、perlの場合だと

/^(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*)/

で括弧内の正規表現を入力の数値にマッチさせてあげると、変数$1、$2、$3、$4にそれぞれ順番に各フィールドの数値が入ります。

#/usr/local/bin/perl
$cnt = 0;
while(<>) {
    if( /^(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*)/ ) {
        printf( "%04d : %f : %f : %f : %f\n", $cnt++, $1, $2, $3, $4 );

    }

}

このスクリプトは、CSVファイルの各フィールドを拾い上げて、画面に出力してくれるものです。私が書いているもう一つのブログ、「半導体やってました」で紹介しています。

実は、後で調べたらC#にもsplitや正規表現によるMatchesなどというものが用意されているみたいです。これは後日研究してみることにしたいと思います。

さて、正規表現やsplitメソッドがあることを知らなかった私は、インターネットを検索して、面白いものを見つけました。それはTextFieldParserというもの。これは、Visual Basicが持っているCSVのパーサーを呼び出して、CSVを解釈しようというものです。

上に書いた正規表現によるフィールドの分割は、各フィールドの文字列がしっかりと正規表現にマッチしないとうまく動いてくれません。たとえば、上の正規表現の例では、コンマの前後にスペースがあったらマッチしません。

それに対して、CSVファイルというものは、意外に表記方法にバラツキがあって、そのバラツキが正規表現でカバーできる範囲を超えてしまうとうまく動かなくなってしまいます。それに対して、このTextFieldParserは、かなり広範囲にわたってCSVファイルを適切に解釈してくれるもののようです。フィールドの数に動作が左右されることもありません。

下記は、TextFieldParserを使ってCSVファイルの各行をフィールド分割し、

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Collections;
using Microsoft.VisualBasic.FileIO;

namespace sample
{
    class Program
    {
        static void Main(string[] args)
        {
            string line = "";
            getDataFromCSV("c:\\users\\ぐすたふ\\検討用テストデータ.csv");

            Console.ReadKey();
        }

        static void getDataFromCSV(string file)
        {
            try
            {
                TextFieldParser parser =
                     new TextFieldParser(file, System.Text.Encoding.GetEncoding("Shift_JIS"));
                parser.TextFieldType = FieldType.Delimited;
                parser.SetDelimiters(","); // 区切り文字はコンマ
                parser.CommentTokens = new string[1] { "#" };
                int col = 0;

                while (!parser.EndOfData)
                {
                    col = 0;

                    string[] row = parser.ReadFields();
                    Console.Write("{0} : ", line);
                    // 配列rowの要素は読み込んだ行の各フィールドの値
                    foreach (string field in row)
                    {
                        ++col;
                        Console.Write("{0}:{1} | ", col, field);
                    }
                    ++line;
                    Console.WriteLine(" ");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

これは、区切り文字をコンマ、行頭が#の行はコメントであることをパーサーに指示して、CSVフィアルを解釈し、

行番号:フィールド番号:フィールドの中身 | フィールド番号:フィールドの中身 | ・・・

の形でデータをコンソールに出力します。

今回の主役であるTextFieldParserですが、解説のページを見るとわかるようにコンストラクタが8種類あります。引数の型や数に応じて異なるコンストラクタが動作するようになっています。

上記の例では、

TextFieldParser( string, Encoding )

を使用しています。stringは、入力ファイルの完全なパス、Encodingは文字エンコードで、上の例ではShift_JISを指定しています。

オブジェクトを生成したのち、

  • プロパティTextFieldTypeにFieldType.Delimitedを指定(可変長レコード)
  • プロパティSetDelimitersでフィールドの区切りを","に指定
  • プロパティCommentTalkenでコメント指定文字を"#"に指定

しています。FieldType.Delimitedは、名前空間Microsoft.VisualBasic.FileIOで定義された列挙体です。

そのあと、ReadField()メソッドでファイルから1行読み込んで解析を行い、配列rowにそれぞれのフィールドを代入して戻ってくる、という仕組みになっています。

なお、このTextFieldParserにはDisposeメソッドがあります。したがって、本来は、解析が終了したら、不要になったリソースを開放する必要があります。finallyでファイルをクローズしてDispose()を呼び出すか、usingを使うのが良いと思います。このことは、原稿を書いているときに気が付いたので、上記プログラムはusingを適用しないままのものを出しています。

usingで対応するとすれば、以下のようにすればよいのでしょうか。エラーなしでビルドできて、動作も問題なくできています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Collections;
using Microsoft.VisualBasic.FileIO;

namespace sample
{
    class Program
    {
        static void Main(string[] args)
        {
            string line = "";

            getDataFromCSV("c:\\users\\gustav\\検討用テストデータ.csv");

            Console.ReadKey();
        }

        static void getDataFromCSV(string file)
        {
            try
            {
                using (TextFieldParser parser = 
                    new TextFieldParser(file, System.Text.Encoding.GetEncoding("Shift_JIS")))
                {
                    parser.TextFieldType = FieldType.Delimited;
                    parser.SetDelimiters(","); // 区切り文字はコンマ
                    parser.CommentTokens = new string[1] { "#" };
                    int line = 0, col = 0;

                    while (!parser.EndOfData)
                    {
                        //++line;
                        col = 0;

                        string[] row = parser.ReadFields();
                        Console.Write("{0} : ", line);
                        // 配列rowの要素は読み込んだ行の各フィールdの値
                        foreach (string field in row)
                        {
                            ++col;
                            Console.Write("{0}:{1} | ", col, field);
                        }
                        ++line;
                        Console.WriteLine(" ");
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

usingは書き方にクセがあって、あまり美しくないのが気に入りませんが、仕方ないでしょう。

今回はここまでにします。

2018年3月12日月曜日

テキストファイルの読み込み

画面表示とマウスカーソルが一段落付いたので、データを読み込む為の要素技術である、ファイルの読み込みの学習を始めました。

昔のCであれば、テキストファイルを読み込んで画面表示しようとしたら、
#include <stdio.h>
#define MAXLINE 256

FILE *fp;
char *fname = "file.txt";
char readline[MAXLINE];

if( (fp = fopen( fname, "r" ) ) == NULL ) {
 fprintf( stderr, "file %s cannot open.\n", fname );
 exit(-1);
}

whiel( fgets( readline, MAXLINE, fp ) != NULL ) {
 fputs( readline );
}

fclose( fp );

等と言うことになるんだと思います。上記についてはあえて詳しく説明はしませんが、file.txtというファイルをオープンして、1行ずつ読み込み、標準出力に1行ずつ出力するというものです。unixのキャラクタベースの世界だと、あえてファイルオープンをせずに、標準入力から入力を読む、というコードにしておいて、入力をファイルにリダイレクトする、というような思想で書くことが多かったと思います。

C#でテキストファイルをオープンして読み込む方法を探したら、以下のようなものが見つかりました。

下記リストは、とあるCSVファイルをオープンして、ファイルを1行ずつ読んで配列に格納し、格納し終わったらコンソールに出力しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Collections;


namespace read1line
{
    class Program
    {
        static void Main(string[] args)
        {
            string line = "";
            ArrayList al = new ArrayList();

            try {
                using (StreamReader sr = new StreamReader("c:/users/ぐすたふ/検討用テストデータ.csv",
                    Encoding.GetEncoding("Shift_JIS")))
                {
                    while((line = sr.ReadLine()) != null)  // ファイルを1行ずつ読んで配列alに格納
                    {
                        al.Add(line);
                    }
                }
            }
            catch (Exception e )
            {
                Console.WriteLine(e.Message);  // 例外が発生したら、コンソールにメッセージを出力
            }

            for( int i = 0; i < al.Count; i++ )  // alを1行ずつコンソールに出力
            {
                Console.WriteLine(al[i]);
            }
            Console.WriteLine("readed " + al.Count + "lines");
            Console.ReadKey();
        }
    }
}

ここで、 StreamReaderはSystem.IO名前空間に属するクラスです。文字コードを指定して、テキストファイルを読み込むためのクラスです。マイクロソフトの解説ページはこちらです。

いつものように機械翻訳で意味が分かりにくいので、もっとわかりやすく解説したページはこちらです。いつも勉強させていただいているdobon.net様です。

                using (StreamReader sr = new StreamReader("c:/users/ぐすたふ/検討用テストデータ.csv",
                    Encoding.GetEncoding("Shift_JIS")))

のコードで、ファイルを読み込む為のオブジェクトを生成しています。この中でファイルをオープンして文字コードの指定をしています。

                    while((line = sr.ReadLine()) != null)  // ファイルを1行ずつ読んで配列alに格納

の、StreamReader.ReadLine()(リスト中ではsr.ReadLine())は、テキストを1行読み込むメソッドです。

なお、リスト中に出てくるArrayListは、動的に大きさを拡大することが可能な配列を提供するクラスです。リストの冒頭に、テキストを読み込んで配列に格納する、と書きましたが、サイズが未知のテキストファイルを1行ずつ配列に読み込む為に、このArrayListを使っています。

ArrayListの説明はこちらにあります。

今度は、ファイルを最後まで一気にstring型の変数に読み込んで、一気にコンソールに出力しています。

/*
 * テキストファイルを一気に読み込む
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string text = "";
            try
            {
                using (StreamReader sr = new StreamReader("c:/users/ぐすたふ/検討用テストデータ.csv", 
                            Encoding.GetEncoding("shift-jis")))
                {
                    text = sr.ReadToEnd();  // ファイルを一気に最後まで読んで、textに格納
                }
                Console.Write(text);  // textをコンソールに一気に出力
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);  // 例外が発生したら、コンソールにメッセージを出力
            }

            Console.ReadKey();
        }
    }
}



こちらで出てくる

                    text = sr.ReadToEnd();  // ファイルを一気に最後まで読んで、textに格納

の中のStreamReader.ReadToEnd()(リスト中ではsr.ReadToEnd())は、ファイルの現在の場所からファイルの最後までを一気に読み込むメソッドです。


上記2つのリストでは、いずれの例も、usingステートメントと、try 〜 catchステートメントを使っています。

usingステートメントは、マイクロソフトのこのページに説明がありますが、機械翻訳なので全くわかりませんね。このページ(C# Tips様)このページ(プログラムマーズ雑記帳様)を読むと、もう少しわかりやすいと思います。

C#のクラスには、使い終わった後にDisposeメソッドを使ってリソースを開放しなければならないものがあります。

しかし、例外の発生によってDisposeの呼び出しが適切なタイミングでできないような場合、動作に矛盾が生じてしまう場合があるようです。それを防ぐ為、適切なDisposeを確実に行ってくれるようにするのがusingである、と書いてあります。

ファイルを扱う場合は、usingを使うのが安全だそうです。

また、try 〜 catchは例外処理を行う為のステートメントです。こちら(++C++; //未確認飛行C様)に説明があります。正しくは、try 〜 catch 〜 finallyで、

try {
  例外を発生する可能性があるコード
}
catch( 例外の種類 ) {
  例外処理をするコード
}
finally {
  例外の有無にかかわらず実行させたいコード
  リソースの開放などに使う
}

という書き方をします。

それで、上記のプログラムの例だと、usingを使ってファイル操作をしているので、finallyでリソースの開放をしなくても済んでいる、ということですね。

今回はこれで終わりにします。