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でリソースの開放をしなくても済んでいる、ということですね。

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

2018年3月11日日曜日

念のために、PictureBoxのImageプロパティとBitmapで点を描けるか試す

前回のエントリーで、PictureBoxのImageプロパティとBitmapオブジェクトを使って、SetPixel()でピクセル単位での描画ができそう、と書きましたが、次の学習は本当にそれができるのかどうかの確認です。

このプログラムを書いていたときに、今までの流れから、まずはGraphicsオブジェクトを生成するということでGraphicsオブジェクト生成のコードを入れてあったのですが、GraphicsクラスにはSetPixelメソッドがありません。逆にGraphicsオブジェクトを使って点を打とうとすると、「C#でWindows Formのウインドウに点を描画する」のエントリーで書いたような、回りくどい書き方になりそうです。

SetPixelはBitmapオブジェクトに直接するもののようで、Graphicsオブジェクトのメソッドを使う必要がないのであれば、Graphicsオブジェクトを生成する必要はありませんね。

dobon.net様のこのページの「SetPixelメソッドで点を描く」という項に同じお話が出て来ます。

以下のリストは、最初にGraphicsオブジェクトの生成とDisposeするコードをコメントで残してありますが、不要です。

なお、いつもと同じようにMainメソッドは割愛しています。

/*
 * bitmapに点を打つ試み 
 * setPixelを使って、pictureBoxに画を描いてみたつもり。
 * Graphicsオブジェクトのメソッドは使わないので、Graphicsオブジェクトは生成しないで
 * SetPixel()する
 */


using System;
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 sample31
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

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

            Bitmap img = new Bitmap(600, 720);

            //Graphics g = Graphics.FromImage(img);

            for( int x = 0; x <; 600; x+=2)
            {
                for (int y = 0; y <; 720; y+=2 )
                {
                    img.SetPixel(x, y, Color.Red);
                }
            }

            //g.Dispose();

            pictureBox1.Image = img;
        }
    }
}

このプログラムを実行すると、下記のような結果となります。


描画方法は、どんな図形をどのような形で描くかによって、適切な方法が違ってくるのかもしれません。しかし、ピクセルベースで何かを描いていく今回の目標仕様に対しては、このPictureBoxのImageプロパティとBitmapオブジェクト、そしてSetPixelを使った方法がよさそうです。

今回はこれで終わりです。

2018年3月10日土曜日

PictureBoxのImageプロパティとBitmapオブジェクトで描画

このブログでは今まで何度かグラフィックの描画処理を取り上げました。

最初は、GraphicsオブジェクトのFillRectangle()メソッドとDrawEllipse()メソッドを使った描画処理でした。Paintイベントのイベントハンドラの引数、PaintEventArgs eが持っているGraphicオブジェクトを使って描画する、という方法でした。

次は、Paintイベントのイベントハンドラの中で描画処理を行うのは同じですが、CreateGraphics()メソッドで作ったGraphicsオブジェクトに、小さなBitmapオブジェクトをタイルのように貼り付けて点を描く方法でした。

今度は、PictureBoxとBitmapを使った描画処理をやります。これも、Bitmapから生成したGraphicsオブジェクトに対する描画処理、ということになりますが、今ひとつよくわかりません。

Graphicsオブジェクトは何種類か作り方があって、作り方によって描画速度が違う、という記事もあったりとかして、この辺からしてまずわかりません。

分からないなりにも学習は進めるということで、勉強させていただいたのは、dobon.net様です。

下記が今回のプログラムリストになります。いつものようにMainメソッドは割愛しています。

Visual Studioで新規プロジェクトからWindows Formアプリケーションを選択し、生成されたForm1のプロパティからSizeを400 X 300程度にします。


さらに、ツールボックスからPictureBoxを選び、Form1に配置し、Sizeを370 X 240程度にします。


/*
 * 3Dデータからウィンドウ上にデータをプロットするプログラムに挑む
 */

using System;
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 sample28
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

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

            Bitmap img = new Bitmap(200, 100);
            Graphics g = Graphics.FromImage(img);

            g.FillRectangle(Brushes.Black, g.VisibleClipBounds);
            g.DrawPie(Pens.Yellow, 60, 10, 80, 80, 30, 300);
            g.Dispose();
            pictureBox1.Image = img;
        }

    }
}

実行した結果は、以下のようになります。


ここで注目すべきは、描画処理がPaintイベントのイベントハンドラとして記述されていない点です。PictureBoxのImageプロパティを使って描画する場合、Paintイベントが発生しても描画内容は消えないようです。同じdobon.net様に記載されていますが、Paintイベント発生する直前に、DrawImageメソッドを使って内部的に描画されているのだそうです。

この方法、SetPixel()メソッドを使ったピクセル単位の描画にも使えそうです。PictureBoxを使った方が手軽に記述できて楽です。

本番プログラムでは、この方法を採用しようと思います。

なお、この方法で描画する場合は、描画処理の終わりでDispose()メソッドを使用してGraphicsオブジェクトのリソースを開放する必要があるようです。

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


2018年3月8日木曜日

マウスの座標を取得し続ける(その2)

前回の続きです。マウスの座標を取得し続ける方法の学習です。

前回、サブスレッドを使ってマウスの座標を取得し続ける方法を学びました。しかし、やりたいことを整理すると、


  • マウスカーソルがウインドウの上にあるときだけ座標を取得できれば良い
  • 座標を取得するのは、どこから輪切りにするかを決めるときだけ


ということになって、ひたすら律儀に座標を取得し続ける必要はなさそうですし、座標の取得をしている最中に、他の込み入った処理をしなければならないこともなさそうです。

それで色々と調べ回ったところ、マウスに関するイベントを利用してあげれば、それほど面倒な処理をしなくてもやりたいことが出来そう、ということが分かりました。

イベントとは、マイクロコンピューターの世界で言う割り込み処理です。私は、ハードウエアをいじる仕事をしていた経験から、割り込み処理と言った方がピンときます。

マウスに関係するイベントは全部で6種類あります。ここで言うマウスに関係するイベントとは、クリックやダブルクリックではなく、マウスがコントロールの上に来たときや、マウスがコントロールの上を動いたときに発生するイベントのことです。こちらに詳細説明がありますが、機械翻訳なのでわかりにくいです。

この中で、MouseHoverイベントと、MouseMoveイベントに着目しました。MouseHoverイベントはマウスカーソルがコントロールの上にあるときに発生するイベント、MouseMoveはマウスカーソルがコントロールの上を移動すると発生するイベントです。

Mainメソッドが書かれたファイルは前回と同じなので割愛します。

using System;
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 sample27
{
    public partial class Form1 : Form
    {
        public static int mouse_x = 0, mouse_y = 0; //マウスポインタの座標置き場 

        public Form1()
        {
            InitializeComponent();

            this.MaximumSize = this.Size; //ウィンドウサイズを変更できない用にする処理
            this.MinimumSize = this.Size; //ロードされたときのウィンドウサイズを取得して、Min/Maxサイズ両方に設定
        }

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

        // マウスがウインドウの中に入ったときに発生するイベント
        private void Form1_MouseHover(object sender, EventArgs e)
        {
            getMousePosition();
        }

        // Formが非選択になったときに発生
        private void Form1_Leave(object sender, EventArgs e)
        {
            if(!this.Focused)
            {
                label1.Text = "Lost Focus";
            }
        }

        // Formが無効化されたときに発生
        private void Form1_Deactivate(object sender, EventArgs e)
        {
            label1.Text = "Deactivated";
            mouse_x = mouse_y = (int)0xffff;
            this.Text = "x=" + mouse_x + ":y=" + mouse_y;
        }

        private void pictureBox1_MouseHover(object sender, EventArgs e)
        {
            getMousePosition();
        }

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

        // マウスの座標を取得、ウィンドウ内の座標(クライアント座標)に変換して出力
        private void getMousePosition( )
        {
            if (this.Focused)
            {
                System.Drawing.Point cp = this.PointToClient(Cursor.Position);

                mouse_x = cp.X;
                mouse_y = cp.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;
            }

        }
    }
}
これがうまく動いた初版プログラムです。

上記リストでは、2種類のMouseHoverイベント、MouseMoveイベント(Form1とpictureBox1)を捕まえて、getMousePosition()メソッドでマウスカーソルのシステム座標をウインドウ内の座標に変換して求めています。

Formの上にPictureBoxが乗っているようなウインドウの場合は、PictureBoxが置いてあるエリアではFormに関するマウスイベントは発生しません。したがって、このようにFormとPictureBoxの二つのマウスイベントのハンドラーを用意しなければなりません。右の図において、Form1の中の破線の四角がPictureBoxです。

なお、マウスイベントの処理をする以外に、フォームがアクティブになったときとフォームが選択されなくなったときに発生するイベントを使って、フォームが選択されているかどうかをフォームに表示させる機能も付けてあります。

プログラムを起動してウインドウを選択し、マウスをウインドウに乗せると、マウスカーソルの座標がウインドウの上に表示されます。

ウインドウを非選択にすると、下記のようにNot Focusedと表示され、座標値が65535(0xffff)、(0xffff)になります。

今回の目標仕様では、このマウスカーソルの座標取得方法で十分と判断し、この方法を採用することにしました。

プログラムを動かしてみたところを動画にしてみました。

なお、その後の検討で、MouseHoverイベントは、コントロール上にマウスがある一定時間止まっていないと発生しないイベントということがわかり、使わなくてもイベントを取りこぼすことはないだろうと判断しました。

したがって、最終的には上記プログラムからMouseHoverイベントの処理に関するコードを抜き取ることにしました。

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


マウスの座標を取得し続ける(その1)

ウインドウに点が打てるようになったので、グラフィック処理は一旦置いておいて、次はマウスの座標を取得する方法を学びました。


リスト1:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

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

リスト1は、いつも通りのApplication.Runです。Visual Studioが吐き出したコードそのままです。

次のリスト2がマウス座標を取得し続けるプログラムの本体です。Lassyの戯言様で勉強させていただきました。

リスト2:Form1.cs
using System;
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 System.Threading;

namespace sample25
{
    public partial class Form1 : Form
    {

        private Thread thread = null;

        public Form1()
        {
            InitializeComponent();

            thread = new Thread(new ThreadStart(GetMousePosition));
            thread.Start();
        }

        public void GetMousePosition()
        {
            while(true)
            {
                SetText();
                Thread.Sleep(100);
            }
        }

        public delegate void SetTextDelegate();

        public void SetText()
        {
            if(InvokeRequired)
            {
                Invoke(new SetTextDelegate(SetText));
                return;
            }
            this.Text = "x=" + Cursor.Position.X + " :y=" + Cursor.Position.Y;
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if( thread != null )
            {
                thread.Abort();
                //return();
            }
        }
    }
}

このプログラムでは、Form1のコンストラクタの中で、マウス座標を取得し続ける処理を別スレッド(サブスレッド)で起動する、ということをしています。100msごとにマウス座標を取得して、Form1のTextプロパティに座標を書き込んでいます。

Form1のTextプロパティに対して、Form1で生成したサブスレッド(つまり別のスレッド)からForm1のプロパティの操作を行う為に、難しい処理をしています。

具体的には、

            if(InvokeRequired)
            {
                Invoke(new SetTextDelegate(SetText));
                return;
            }


の部分です。呼び出し元が自スレッド(Form1)ではない時、このプログラムの場合はサブスレッドとして動いているGetMousePoisition()から呼び出されたとき、Invokeメソッドを使ってSetTextメソッドへの参照(関数ポインタのようなもの)を生成し、参照から間接的にSetTextメソッドを呼び出しています。Invoke経由の動作や自スレッドからの呼び出しの場合は、InvokeRequiredがfalseとなり、Invokeメソッドは実行されずにifブロックの直後の処理(this.Text〜)が動くのだと思います。

マルチスレッドで動作するプログラムの場合、自スレッド以外のスレッドからのアクセス場合、参照・ポインタによるアクセスでなければならないようで、間接呼び出しを行う為にこのような処理をするのだと思います。

他のサイトでも同様の処理が紹介されていたので、別スレッドからの呼び出しで使われる定番処理なのだと思います。

なお、参考にさせていただいているサイトでは、他にもフックというものを使用する、といった処理の紹介もされています。

そのプログラムも、入力して動かしてみましたが、この記事では割愛します。

さて、サブスレッドでマウス座標を取得し続けるのは、親スレッドが何をしているときでも座標の取得を止めどなく続ける、ということです。しかし、今回の目的に対して必要かどうかを考えると、もう少し簡便な方法でも良いと思います。

今回はここまでです。次回、マウスの話の続きをやります。


2018年3月7日水曜日

C#でWindows Formのウインドウに点を描画する

三次元データをXY平面に投影した図を描く、という目的を満足する為に必要な要素技術、その中で一番基本的なものは、ウインドウの任意の場所に点を描くことです。

点を描く方法をインターネットで調べると、結構たくさん出て来ます。

まずはこちら、C#ビギナー様がヒットしたので、このサイト様で勉強させていただきました。使用したリストを以下に示します。このコードは、このまま打ち込んでも動きません。下記の手順に従って、フォームを生成し、イベントハンドラーの登録を行った後、イベントハンドラーの中身として記述します。手順を以下に書きます。

Visual StudioでWindows Formアプリケーションを選択し、Formを一つ用意します。
図のようなフォームが現れます。
FormのプロパティからSizeを適切な大きさに設定します。この例では300x300ピクセルにしています。
フォームの大きさが設定値通りに変わります。
BackColorをBlackに設定します。
プロパティ設定ウインドウのイベントボタンを押します。イベントボタンは、稲妻のような形をしています。
Paintイベントを探して、ダブルクリックします。
 Paintイベントのイベントハンドラー
private void From1_Paint( object sender, PaintEventArgs e)
のスケルトンが自動生成されます。

コード本体はこちらです。

using System;
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 sample22
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            Graphics g = this.CreateGraphics();
            Bitmap p = new Bitmap(1, 1);
            p.SetPixel(0, 0, Color.White);
            g.DrawImageUnscaled(p, 100, 100);
        }
    }
}

コードの中身を見ると、Paintイベントが発生したら、

  1. Form1に対してCreateGraphicsメソッドでGraphicsオブジェクトgを生成
  2. 大きさ1x1ピクセルのBitmapオブジェクトpを生成
  3. オブジェクトpに対して、SetPixelメソッドを使って、座標(0,0)に白い点を打つ
  4. オブジェクトpをGraphicsオブジェクトgの座標(100,100)に描画

という処理を行っています。オブジェクトpは、大きさ1x1ピクセルのBitmapオブジェクトなので、座標(0,0)に白い点を打つということは、オブジェクトpは大きさ1x1の白い点、ということになりますね。

このプログラムはMainメソッドがありませんが、F5キーを押してデバッグ動作をさせると動いてくれます。実行結果はは以下のようになります。


点をたくさん格子状に打つようにしたプログラムが下記です。イベントハンドラの中身を差し替えるだけで、実現出来ます。

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            int x1 = 20;
            int y1 = 20;
            int x = this.Width;
            int y = this.Height;
            Graphics g = this.CreateGraphics();
            Bitmap p = new Bitmap(1, 1);
            p.SetPixel(0, 0, Color.White);
            for( int i = x1; i< x; i += x1 )
            {
                for( int j=y1; j < y; j+= y1 )
                {
                    for( int k=1; k <1000; ++k) { }
                    g.DrawImageUnscaled(p, i, j);
                }
            }
        }

実行結果が下記です。


これらのプログラムはいずれも、Graphicsオブジェクトに、描画がなされた小さなBitmapオブジェクトを、タイルのように貼り付けて描画する、という考え方で書かれています。言ってみれば2段階の動作で描画している構造です。

もう少し直接的な描画動作ができれば、と考えます。それは、次回以降で研究したいと思います。

というわけで、 今回はこれで終わりにします。

2018年3月6日火曜日

目標仕様を見据えた学習 目標仕様の確認

学習開始から1週間ほど経って、色々なことをやって、言語にある程度慣れてきたかな、と思えるようになりました。これから先は、とりあえずの目標仕様を見据えた学習に切り替えました。

元々やりたいと思っていた目標仕様とは以下のものです。

概要

  • 測定点の3次元座標(X/Y/Z座標)及び測定値の大きさが入ったCSVファイルからデータを読み込み、XY平面への投影図をウインドウに描く。
  • ウインドウに描かれた投影図に対して、X軸と平行な平面(今回はY軸に垂直な平面のみを考える)、Y軸と平行な平面(今回はX軸に垂直な平面のみを考える)における断層画像を描く。

データの形式

  • データはCSVファイルで与えられ、X座標,Y座標,Z座標,測定データの形式。座標の最小解像度は1ピクセルとする。(つまり座標値は整数のみ)

ということで、これからのエントリーでは、この目標仕様を満足するプログラムを書くために必要な要素技術を中心にこれから学習を進めていったことを書いていきます。

上に書いた目標仕様を満たすプログラムを作るための要素技術として、当初以下のものを想定しました。

  • ウインドウに点が打てる
    Windowsのアプリケーションを作ったことがない人間からすると、Windowsのウインドウの中に点を1個打つだけでも、ハードルが高く感じるものです。
  • ファイルの読み出しができる
    Windowsでファイルを開く操作をすると出てくるダイヤログを使ってファイルを選択できるようにします
  • ウインドウ上でマウスのクリックが拾える
    マウスがクリックされたことをきっかけにして、何か動作を始める、というWindowsによくある動作を自分のプログラムで実現するにはどうしたらよいのかを学必要があります
  • ウインドウ上にあるマウスの座標が読み取れる(連続的に読み続ける)
    輪切りの絵を描く為に、主として操作性を良くする目的で、マウスカーソルの座標を連続的に読み続ける機能が必要です
  • ウインドウの座標系とデータの座標系の変換ができる
    データが持っている座標系とウインドウの中の座標系は当然違うはずです。描画する為には、データ座標系をウインドウ座標系に変換してあげる必要があります
  • 隠面処理のために、データの並べ替えができる
    隠面処理とは、奥にあるものが手前にあるものに遮られて見えない、ということを計算機上の処理で実現するものですが、今回は奥にある点から描き始めて、手前にある点が奥にある点を隠していくような描画方式にします。そのためには奥の方から描くようにデータの並べ替えが必要です

それに加えて、ウインドウのデザインが必要ですし、色々なエラーに対する対策をする必要がありますね。特に、エラーが出たらプログラムが止まってしまう、というのは困ります。

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

2018年3月5日月曜日

続グラフィック・描画とPaintイベントの関係について

前回のエントリーで、Graphicメソッドを使った描画処理は、Paintイベントのイベントハンドラーの中で実施するのが、一般的な方法、ということを書きました。

インターネットで色々なコードの記述例を見ると、ほとんどの場合そのような記述になっています。

なぜそうなのか、ということを少し調べてみました。

前回のエントリーで絵が出たプログラムを書き換えて、MyFormのコンストラクタで描画処理を行うようにしてみたリストが下記リストです。Graphicsオブジェクトは、MyFormオブジェクトのCreateGraphicsメソッドを使って取得しています。

注意)
このリストは、MyFormクラスを呼び出すMainメソッドを割愛しています。実際に動かす場合にはApplication.Run( new MyForm() );が入ったMainメソッドを別ファイルで追加する必要があります。

/*
 * ようやくグラフィックの描画に入る
 */

using System;
using System.Drawing;
using System.Windows.Forms;

namespace MyFrmApp
{
    public class MyForm : Form
    {
        public MyForm()
          {
            this.Width = 300;
            this.Height = 200;
            Graphics g = this.CreateGraphics();
            Pen p = new Pen(Color.Red);
            Brush b = new SolidBrush(Color.Blue);
            g.FillRectangle(b, 50, 50, 50, 50);
            g.DrawEllipse(p, 75, 75, 50, 50);
            g.Dispose();
          }
    }
}

このプログラムを実行すると、下記のようなウインドウが現れます。


どうでしょう?中身がありません。これは、おそらくウインドウが表示された後にPaintイベントが発生し、そのイベントによってウインドウの中が消去されてしまっていると考えられます

ここで、MyFormのコンストラクタに1行コードを追加します。

        public MyForm()
        {
            this.Width = 300;
            this.Height = 200;
            this.Show();  // 追加
            Graphics g = this.CreateGraphics();
            Pen p = new Pen(Color.Red);
            Brush b = new SolidBrush(Color.Blue);
            g.FillRectangle(b, 50, 50, 50, 50);
            g.DrawEllipse(p, 75, 75, 50, 50);
            g.Dispose();
        }

追加したthis.Show()メソッドは、自分自身を明示的に表示するメソッドです。すると、


図形が現れます。ところが、このウインドウを一度最小化(ウインドウの上にあるーマークを押す)してから再び表示させると、ウインドウの中の図形は消えてしまいます。

最小化してから再表示させると、Paintイベントが発生して、ウインドウの中を消してしまうからです。

これらの結果をまとめると、

  • 描画処理のコードが実行された後、ウインドウが表示される前にPaintイベントが発生してウインドウ内の図形が消えてしまう。
  • ウインドウを明示的に表示させるメソッドを実行した後に描画処理をすると、ウインドウ内の図形は消えないことから、上記現象は、ウインドウが表示される前に描画処理がなされ、ウインドウが表示される瞬間にPaintイベントが発生している(らしい)
  • ウインドウの最小化、最大化を実施すると、ウインドウ内の図形は消えてしまう。

このことから考えると、Graphicsオブジェクトを使ってウインドウに何かを描く処理をする場合は、Paintイベントが発生するたびにプログラムから再描画するような構造にしないといけないことになります。

そして、PaintEventArgsから取得したGraphicsオブジェクトを使って描画を行うよう描画処理を書くと、それはそのままPaintイベント発生時に再描画するプログラムになるのです。

これは、OSの仕組みと.NET Frameworkのライブラリの仕組みから、こうやって書くべき、というところでしょうね。特に、Paintイベントは自分以外の、システム全体の動作の都合で発生することもあるようですから、これ以外に方法はない、というところ。

Visual Basic用の記事ではありますが、このページのPaintイベントの項に詳しい説明があります。

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

参考文献:
Visual Basic 中学校 初級講座 第2回 絵を描く