前々回で日付入力、前回でバイオリズムグラフの描画をやりましたので、その機能をコントロールに実装してゆきます。

 

コントロールを作る場合、コードにして静的に実装する場合(例:BCCSkeltonのPICTUREBOX)と動的ライブラリ(DLL)にして実装する場合(例:Win32 SDKのDICE)がありますが、C#は簡単にDLLが作れ、また作ったDLLを簡単に読み込んで利用できるので、DLLによる実装をお勧めします。

 

次にコントロールを作る場合、「更地」のウィンドウから始める手もありますが、今回の場合、PictureBoxを表示に使っていたので、PictureBoxを承継した派生コントロール、"BrmBox"(クラス名)としてBiorythm(名前空間およびDLL名)に入れましょう。MSCompAssでDLLを作るには、"Option"の「出力ファイル」を"/target:library"するだけで、後はいつもと変わりありません。(参考までに私の環境でのoptファイルも載せます。)

 

なお、既に前回、前々回やった解説は省きます。(注)

注:ウィンドウベースではEaselクラスを使いましたが、文字列表示とLineしか使わないので、ここではC#のGraphicsクラスのメソッドだけを使って実装します。

 

【BrmBox in Biorythm】

//////////////////////////////
//        C# Biorythm.cs
// Biorythm Control Component
//////////////////////////////

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

namespace Biorythm
{
    public partial class BrmBox : PictureBox    //PictureBoxを承継したBrmBoxというクラスを作る
    {
        //バイオリズムグラフ表示関係
        private Bitmap Canvas = null;    //仮想画面ビットマップ
        private Graphics hGraph = null;    //仮想画面のグラフィック
        //バイオリズム計算関係
        DateTime DOB;                    //誕生日(Date of Birth)
        DateTime DOT;                    //指定日(Date of Traget)
        //コンテキストメニュー関係-これは新しい内容です。右クリックでメニューを出します。
        ContextMenu contextMenu;
        MenuItem mi1, mi2, mi3;

        //コンストラクター
        public BrmBox()
        {
            this.ClientSize = new Size(320, 320);
            this.BackColor = SystemColors.Control;
            this.SizeChanged += Control_SizeChanged;
            //同サイズのビットマップ仮想画面の作成-やり方はEaselと同じです。
            Canvas = new Bitmap(this.Width, this.Height);
            //Graphicsクラス(アンチエイリアス)-やり方はEaselと同じです。
            hGraph = Graphics.FromImage(Canvas);
            hGraph.SmoothingMode = SmoothingMode.AntiAlias;
            hGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
            //コンテキストメニュー関連-コントロール上で右クリックして出すポップアップメニューです。
            mi1 = new MenuItem();
            mi1.Index = 0;
            mi1.Text = "生年月日入力";
            mi2 = new MenuItem();
            mi2.Index = 0;
            mi2.Text = "指定日入力";
             mi3 = new MenuItem();
            mi3.Index = 0;
            mi3.Text = "グラフ表示";
             contextMenu = new ContextMenu();
            contextMenu.MenuItems.Add(mi1);
            contextMenu.MenuItems.Add(mi2);
            contextMenu.MenuItems.Add(mi3);
             this.ContextMenu = contextMenu;
            mi1.Click += new EventHandler(SetDOB);
            mi2.Click += new EventHandler(SetDOT);
            mi3.Click += new EventHandler(Display);
        }

        //デストラクター
        ~BrmBox()
        {
            if(hGraph != null)
                hGraph.Dispose();                    //グラフィックリソースの開放
            if(Canvas != null)
                Canvas.Dispose();                    //ビットマップリソースの開放
        }

        //コントロールがサイズ変更された場合
        private void Control_SizeChanged(object sender, EventArgs e)
        {
            //幅または高さが0の場合、「使用されたパラメーターが有効ではありません」エラーとなる為
            if(this.Width * this.Height == 0)
                return;
            Rectangle srcRect;
            Rectangle desRect;
            Image img = (Bitmap)Canvas.Clone();                //旧画像をBitmapで保存
            Canvas.Dispose();                                //Canvasを一旦開放
            Canvas = new Bitmap(this.Width, this.Height);    //新しいサイズで作成
            if(this.Width > img.Width)
            {
                if(this.Height > img.Height)
                {
                    srcRect = new Rectangle(0, 0, img.Width, img.Height);
                    desRect = new Rectangle(0, 0, img.Width, img.Height);
                }
                else
                {
                    srcRect = new Rectangle(0, 0, img.Width, this.Height);
                    desRect = new Rectangle(0, 0, img.Width, this.Height);
                }
            }
            else
            {
                if(this.Height > img.Height)
                {
                    srcRect = new Rectangle(0, 0, this.Width, img.Height);
                    desRect = new Rectangle(0, 0, this.Width, img.Height);
                }
                else
                {
                    srcRect = new Rectangle(0, 0, this.Width, this.Height);
                    desRect = new Rectangle(0, 0, this.Width, this.Height);
                }
            }
            //CanvasのGraphicsクラスの再生成
            hGraph.Dispose();                                                //旧いサイズのCanvasのものを廃棄
            hGraph = Graphics.FromImage(Canvas);                            //新しいサイズのCanvasのものを生成
            hGraph.SmoothingMode = SmoothingMode.AntiAlias;                    //アンチエイリアス
            hGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;            //高品質画面
            hGraph.DrawImage(img, desRect, srcRect, GraphicsUnit.Pixel);    //旧いCanvasイメージ(img)のコピー
            this.Image = Canvas;                                            //Canvasに張り付け
            img.Dispose();                                                    //使い終わったimgを開放
        }

        public void SetDOB(object sender = null, EventArgs e = null)
        {
            DateDialog dd = new DateDialog("生年月日を入力してください。");
            //OKボタンが押された場合
            if(dd.ShowDialog() == DialogResult.OK)
                DOB = dd.date;

            dd.Dispose();    //2023年06月30日追加
        }

        public void SetDOT(object sender = null, EventArgs e = null)
        {
            DateDialog dd = new DateDialog("バイオリズムを見る年月日を指定してください。");
            //OKボタンが押された場合
            if(dd.ShowDialog() == DialogResult.OK)
                DOT = dd.date;
            dd.Dispose();    //2023年06月30日追加
        }

        public void Display(object sender = null, EventArgs e = null)
        {
            //初期設定
            TimeSpan ts = DOT - DOB;        //誕生から指定日までの経過日数
            Color oldcol = this.ForeColor;    //現在の前景色を退避
            hGraph.Clear(this.BackColor);    //画面消去
            //使用するブラシの生成
            Brush RBrush = new SolidBrush(Color.Red);
            Brush GBrush = new SolidBrush(Color.Green);
            Brush BBrush = new SolidBrush(Color.Blue);
            Brush VBrush = new SolidBrush(Color.BlueViolet);
            //身体、感情、知性の色定義を表示する
            hGraph.DrawString("身体周期(Physical Cycle):", System.Drawing.SystemFonts.DefaultFont, RBrush, 10, 10, new StringFormat(StringFormatFlags.NoWrap));
            hGraph.DrawString("感情周期(Emotional Cycle):", System.Drawing.SystemFonts.DefaultFont, GBrush, 10, 30, new StringFormat(StringFormatFlags.NoWrap));
            hGraph.DrawString("知性周期(Intellectual Cycle):", System.Drawing.SystemFonts.DefaultFont, BBrush, 10, 50, new StringFormat(StringFormatFlags.NoWrap));
            //バイオリズムグラフ表示領域の画面設定
            //黒枠と基準線

            hGraph.DrawRectangle(Pens.Black, 10, 80, this.Width - 20, this.Height - 90);
            int VCenter = (this.Height - 70) / 2 + 70;    //バイオリズムグラフ描画領域の水平中央線のy座標
            hGraph.DrawLine(Pens.Black, 10, VCenter, this.Width - 10, VCenter);
            //日を表す縦線    Color.Silver or Color.DarkGray
            for(int i = 1; i < 15; i++)
                hGraph.DrawLine(Pens.Silver, 10 + (int)(i * (double)(this.Width - 20) / 30.0), 80, 10 + (int)(i * (double)(this.Width - 20) / 30.0), this.Height - 10);
            for(int i = 16; i < 30; i++)
                hGraph.DrawLine(Pens.Silver, 10 + (int)(i * (double)(this.Width - 20) / 30.0), 80, 10 + (int)(i * (double)(this.Width - 20) / 30.0), this.Height - 10);
            //指定日のみ青紫で表示
            hGraph.DrawLine(Pens.BlueViolet, this.Width / 2, 80, this.Width / 2, this.Height - 10);
            hGraph.DrawString(DOT.ToString("yyyy年MM月dd日"), System.Drawing.SystemFonts.DefaultFont, VBrush, this.Width / 2 - 40, 64, new StringFormat(StringFormatFlags.NoWrap));
            //正弦波グラフを横(10~this.Width - 10)、縦(80~this.Height - 10)に表示する
            int oldx = 0, oldPy = 0, oldEy = 0, oldIy = 0;
            string Pstr = "", Estr = "", Istr = "";    //評価表示用文字列
            for(double i = 0; i < 30; i += 0.1)        //指定日を中央に30日のデータを表示
            {    //グラフのx座標の範囲:10 ~ this.Width - 10
                //バイオリズム計算(グラフy座標):sin(2πt/T): tは経過日数、Tは23(P), 28(S), 33(I)日
                double t = (double)(ts.Days + i - 15);
                int x = 10 + (int)(i * (double)(this.Width - 20) / 30.0);
                int Py = (int)(-Math.Sin(Math.PI * 2 * t / 23.0) * (double)(this.Height - 100) / 2) + this.Height / 2 + 40;
                int Ey = (int)(-Math.Sin(Math.PI * 2 * t / 28.0) * (double)(this.Height - 100) / 2) + this.Height / 2 + 40;
                int Iy = (int)(-Math.Sin(Math.PI * 2 * t / 33.0) * (double)(this.Height - 100) / 2) + this.Height / 2 + 40;
                if((int)i *10 == 150)    //基準日処理-if(i == 15.0 または 15d)やif(i.Equal(15.0 または 15d))は失敗する。
                {
                    if(Py < VCenter)
                        Pstr = "高調期";
                    else if(Py == VCenter)
                        Pstr = "不安定日";
                    else
                        Pstr = "低調期";
                    if(Ey < VCenter)
                        Estr = "高調期";
                    else if(Ey == VCenter)
                        Estr = "不安定日";
                    else
                        Estr = "低調期";
                    if(Iy < VCenter)
                        Istr = "高調期";
                    else if(Iy == VCenter)
                        Istr = "不安定日";
                    else
                        Istr = "低調期";
                }
                if(i > 0)    //最初の始点は描画しない
                {
                    hGraph.DrawLine(Pens.Red, oldx, oldPy, x, Py);
                    hGraph.DrawLine(Pens.Green, oldx, oldEy, x, Ey);
                    hGraph.DrawLine(Pens.Blue, oldx, oldIy, x, Iy);
                }
                oldx = x;
                oldPy = Py;
                oldEy = Ey;
                oldIy = Iy;
            }
            //身体、感情、知性の評価を表示する
            hGraph.DrawString(Pstr, System.Drawing.SystemFonts.DefaultFont, RBrush, 160, 10, new StringFormat(StringFormatFlags.NoWrap));
            hGraph.DrawString(Estr, System.Drawing.SystemFonts.DefaultFont, GBrush, 160, 30, new StringFormat(StringFormatFlags.NoWrap));
            hGraph.DrawString(Istr, System.Drawing.SystemFonts.DefaultFont, BBrush, 160, 50, new StringFormat(StringFormatFlags.NoWrap));
            //バイオリズムグラフを表示する
            this.Image = Canvas;
            //ブラシの開放
            RBrush.Dispose();
            GBrush.Dispose();
            BBrush.Dispose();
            VBrush.Dispose();
            this.ForeColor = oldcol;    //前景色を元に戻す
        }
    }

    /////////////////////
    //日付入力ダイアログ
    /////////////////////

    class DateDialog : Form
    {
        //日付出力用DateTImeクラスオブジェクト
        private DateTime _date;    
        public DateTime date
        {
            get
            {
                return _date;
            }
        }
        string message;                        //ダイアログ表示メッセージ
        ComboBox yyList, mmList, ddList;    //年月日入力用コンボボックス
        Button OkBtn, CancelBtn;            //ボタンコントロール

        public DateDialog(string str)
         {
            //メッセージを記録
            message = str;
            // ダイアログボックス用の設定
            this.Text = "日付入力";
            this.MaximizeBox = false;            //最大化ボタン
            this.MinimizeBox = false;            //最小化ボタン
            this.FormBorderStyle = FormBorderStyle.FixedDialog;        //境界のスタイル
            this.StartPosition = FormStartPosition.CenterParent;    // 親フォームの中央に配置
            this.Size = new Size(240, 200);
            this.Load += Dlg_Load;
            this.AcceptButton = OkBtn;        // Enter キーで選択できるボタン
            this.CancelButton = CancelBtn;    // Esc キーで選択できるボタン
        }

        private void Dlg_Load(object sender, EventArgs e)
        {
            //メッセージラベル
            Label label = new Label();
            label.Location = new Point(10, 10);
            label.AutoSize = true;
            label.Text = message;
            this.Controls.Add(label);
            //コンボボックス(yyList)
            Label yylabel = new Label();
            yylabel.Location = new Point(10, label.Height + 20);
            yylabel.AutoSize = true;
            yylabel.Text = "年(西暦)";
            this.Controls.Add(yylabel);
            yyList = new ComboBox();
            yyList.Location = new Point(yylabel.Width + 20, label.Height + 20);
            for(int i = DateTime.Today.Year; i >= 1923; i--)    //100年分リスト
            {
                yyList.Items.Add(i.ToString("D4"));
            }
            this.Controls.Add(yyList);
            //コンボボックス(mmList)
            Label mmlabel = new Label();
            mmlabel.Location = new Point(10, label.Height + yylabel.Height + 30);
            mmlabel.AutoSize = true;
            mmlabel.Text = "月";
            this.Controls.Add(mmlabel);
            mmList = new ComboBox();
            mmList.Location = new Point(yylabel.Width + 20, label.Height + yyList.Height + 30);
            for(int i = 1; i <= 12; i++)    //12月    分リスト
            {
                mmList.Items.Add(i.ToString("D2"));
            }
            this.Controls.Add(mmList);
            //コンボボックス(ddList)
            Label ddlabel = new Label();
            ddlabel.Location = new Point(10, label.Height + yylabel.Height + mmlabel.Height + 40);
            ddlabel.AutoSize = true;
            ddlabel.Text = "日";
            this.Controls.Add(ddlabel);
            ddList = new ComboBox();
            ddList.Location = new Point(yylabel.Width + 20, label.Height + yyList.Height + mmList.Height + 40);
            for(int i = 1; i <= 31; i++)    //31日分リスト
            {
                ddList.Items.Add(i.ToString("D2"));
            }
            this.Controls.Add(ddList);
            //Cancelボタン
            CancelBtn = new Button();
            CancelBtn.Location = new Point(10, ClientSize.Height - CancelBtn.Height - 10);
            CancelBtn.Text = "Cancel";
            CancelBtn.AutoSize = true;
            CancelBtn.Click += CancelButton_Click;
            this.Controls.Add(CancelBtn);
            //OKボタン
            OkBtn = new Button();
            OkBtn.Location = new Point(ClientSize.Width - OkBtn.Width - 10, ClientSize.Height - OkBtn.Height - 10);
            OkBtn.Text = "OK";
            OkBtn.AutoSize = true;
            OkBtn.Click += OkButton_Click;
            this.Controls.Add(OkBtn);
        }

        private void OkButton_Click(object sender, EventArgs e)
        {
            //OKボタン
            try
            {
                _date = DateTime.Parse(yyList.Text + "/" + mmList.Text + "/" + ddList.Text);
                //_date = DateTime.Parse(yyList.SelectedItem.ToString() + "/" + mmList.SelectedItem.ToString() + "/" + ddList.SelectedItem.ToString());
            }
            catch
            {
                MessageBox.Show("選択された日付が不正です", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                this.DialogResult = DialogResult.None;    // Meaning false
                this.Close();
                return;
            }
            this.DialogResult = DialogResult.OK;        // Meaning true
            this.Close();
        }

        private void CancelButton_Click(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.Cancel;    // Meaning true
            this.Close();
        }
    }
}

 

【Biorythm.opt】

[Compile Option]
Target=3
Resource=0
RscFile=
IconFile=
DbgOpt=0
WarnErr=5
Others=
 

前回、日付入力ダイアログをやりました(注)ので、本日はあの、定番の正弦波グラフを描画してみましょう。

注:このダイアログを呼び出すには次のようにします。

            //ボタンが押された場合
            DateDialog dd = new DateDialog("生年月日を入力してください。");
            //戻り値はDialogResult.OK(1)、DialogResult.Cancel(2)またはDialogResult.None(0)
            if(dd.ShowDialog() == DialogResult.OK)
            {
                MessageBox.Show(dd.date.ToString(), "選択された生年月日", MessageBoxButtons.OK, MessageBoxIcon.Information);
                DOB = dd.date;
        //ダイアログのDateTimeプロパティから親ウィンドウのDOB変数に代入

                dd.Dispose();           //2023年06月30日追記
            }

 

前提としてメインウィンドウ(Form)にはPictureBoxインスタンス(picBox)が張られており、Easelクラスインスタンス(easel)も作られています。また生年月日はDateTimeクラスオブジェクトのDOBに、指定日はDOTに入っています。

 

【バイオリズムグラフ表示】

            //初期設定
            TimeSpan ts = DOT - DOB;        //誕生から指定日までの経過日数
            Color oldcol = easel.gFtColor;    //現在の前景色を退避
            easel.Clear();                    //画面消去
            //身体、感情、知性の色定義を表示する
            easel.gFtColor = Color.Red;
            easel.PrintText("身体周期(Physical Cycle):", 10, 10, new StringFormat(StringFormatFlags.NoWrap));
            easel.gFtColor = Color.Green;
            easel.PrintText("感情周期(Emotional Cycle):", 10, 30, new StringFormat(StringFormatFlags.NoWrap));
            easel.gFtColor = Color.Blue;
            easel.PrintText("知性周期(Intellectual Cycle):", 10, 50, new StringFormat(StringFormatFlags.NoWrap));
            
//表示領域の画面設定
            //黒枠と基準線
            easel.gFtColor = Color.Black;
            easel.Box(10, 80, picBox.Width - 10, picBox.Height - 10);
            int VCenter = (picBox.Height - 70) / 2 + 70;    
//バイオリズムグラフ描画領域の水平中央線
            easel.Line(10, VCenter, picBox.Width - 10, VCenter);
          
 //日を表す縦線
            easel.gFtColor = Color.Silver;    //Color.Silver or Color.DarkGray
            for(int i = 1; i < 15; i++)
                easel.Line(10 + (int)(i * (double)(picBox.Width - 20) / 30.0), 80, 10 + (int)(i * (double)(picBox.Width - 20) / 30.0), picBox.Height - 10);
            for(int i = 16; i < 30; i++)
                easel.Line(10 + (int)(i * (double)(picBox.Width - 20) / 30.0), 80, 10 + (int)(i * (double)(picBox.Width - 20) / 30.0), picBox.Height - 10);
            
//指定日のみ青紫で表示
            easel.gFtColor = Color.BlueViolet;
            easel.Line(picBox.Width / 2, 80, picBox.Width / 2, picBox.Height - 10);
            easel.PrintText(DOT.ToString("yyyy年MM月dd日"), picBox.Width / 2 - 40, 64, new StringFormat(StringFormatFlags.NoWrap));
            
//正弦波グラフを横(10~picBox.Width - 10)、縦(80~picBox.Height - 10)に表示する
            int oldx = 0, oldPy = 0, oldEy = 0, oldIy = 0;
            string Pstr = "", Estr = "", Istr = "";    
//評価表示用
            for(double i = 0; i < 30; i += 0.1)        //指定日を中央に30日のデータを表示
            {    //グラフのx座標の範囲:10 ~ picBox.Width - 10
                //バイオリズム計算(グラフy座標):sin(2πt/T): tは経過日数、Tは23(P), 28(S), 33(I)日
                double t = (double)(ts.Days + i - 15);
                int x = 10 + (int)(i * (double)(picBox.Width - 20) / 30.0);    
//x座標
                //y座標は↑が0で↓が数が多くなるので、Math.Sinを負数にすることで反転している。
                int Py = (int)(-Math.Sin(Math.PI * 2 * t / 23.0) * (double)(picBox.Height - 100) / 2) + picBox.Height / 2 + 40;
                int Ey = (int)(-Math.Sin(Math.PI * 2 * t / 28.0) * (double)(picBox.Height - 100) / 2) + picBox.Height / 2 + 40;
                int Iy = (int)(-Math.Sin(Math.PI * 2 * t / 33.0) * (double)(picBox.Height - 100) / 2) + picBox.Height / 2 + 40;
                
/* ChatGPTの回答参照 */

                if((int)(i * 10) == 150)    //基準日処理-if(i == 15.0 または 15d)やif(i.Equal(15.0 または 15d))は失敗する。Why?
                {
                    if(Py < VCenter)
                        Pstr = "高調期";
                    else if(Py == VCenter)
                        Pstr = "不安定日";
                    else
                        Pstr = "低調期";
                    if(Ey < VCenter)
                        Estr = "高調期";
                    else if(Ey == VCenter)
                        Estr = "不安定日";
                    else
                        Estr = "低調期";
                    if(Iy < VCenter)
                        Istr = "高調期";
                    else if(Iy == VCenter)
                        Istr = "不安定日";
                    else
                        Istr = "低調期";
                }
                if(i > 0)    
//最初の始点は描画しない
                {
                    easel.gFtColor = Color.Red;
                    easel.Line(oldx, oldPy, x, Py);
                    easel.gFtColor = Color.Green;
                    easel.Line(oldx, oldEy, x, Ey);
                    easel.gFtColor = Color.Blue;
                    easel.Line(oldx, oldIy, x, Iy);
                }
                oldx = x;
                oldPy = Py;
                oldEy = Ey;
                oldIy = Iy;
            }
          
 //身体、感情、知性の評価を表示する
            easel.gFtColor = Color.Red;
            easel.PrintText(Pstr, 160, 10, new StringFormat(StringFormatFlags.NoWrap));
            easel.gFtColor = Color.Green;
            easel.PrintText(Estr, 160, 30, new StringFormat(StringFormatFlags.NoWrap));
            easel.gFtColor = Color.Blue;
            easel.PrintText(Istr, 160, 50, new StringFormat(StringFormatFlags.NoWrap));
            
//バイオリズムグラフを表示する
            picBox.Image = easel.Canvas;
            easel.gFtColor = oldcol;
    //前景色を元に戻す
 

以上でこんな感じに描画します。(私は絵心がないので、自分でもつまらない構図だなと思いますが。)

 

さて、いよいよこのバイオリズム関連の機能をコントロールに詰め込んでゆきます。

 

前回書いたように、コントロールは、

 

(1)何らかの目的と機能を持ったウィンドウを作り、

(2)それを他のウィンドウに所有される子ウィンドウになって、

(3)(必要に応じ)親と通信する

 

と考えてよいと思いますが、今回のバイオリズムコントロールは

 

(1)ユーザーが生年月日と指定日を入力し、それをPictureBoxに表示し、

(2)それをメインウィンドウに所有されるコントロール(子ウィンドウ)にしますが、

(3)(バイオリズムグラフを描くだけなので、特に)親と通信しない

 

こととし、取り敢えずEaselを使ったウィンドウベースのものを作ります。

 

バイオリズムは生誕から身体、感情、知性がそれぞれ23、28、33日周期で上下するという考え方を採っており、指定日までの経過日数に応じた正弦波(Sin curve)でそれを表現するのが一般的です。従って、

 

(1)誕生日、指定日を特定するための日付入力ダイアログが必要

(2)日数計算が必要(これはもうカバーしていますね。)

 

となります。

 

ということで、

 

今日は入力指示メッセージ付きの「日付入力ダイアログ」のコードを参考までに紹介します。

【日付入力ダイアログ】

    /////////////////////
    //日付入力ダイアログ
    /////////////////////
    class DateDialog : Form
    {
        //日付出力用DateTimeクラスオブジェクト
        private DateTime _date;    
        //_dateオブジェクトを使ったdateプロパティ

        public DateTime date
        {
            get
            {
                return _date;
            }
        }
        string message;                        //ダイアログ表示メッセージ(↑の例では「生年月日を入力してください。」)
        ComboBox yyList, mmList, ddList;    //年月日入力用コンボボックス
        Button OkBtn, CancelBtn;            //ボタンコントロール

        //コンストラクター
        public DateDialog(string str)
         {
            //メッセージを記録
            message = str;
            //モーダルダイアログボックス用の設定
            this.Text = "日付入力";
            this.MaximizeBox = false;            //最大化ボタン
            this.MinimizeBox = false;            //最小化ボタン
            this.FormBorderStyle = FormBorderStyle.FixedDialog;        //境界のスタイル
            this.StartPosition = FormStartPosition.CenterParent;    // 親フォームの中央に配置
            this.Size = new Size(240, 200);        //ダイアログのサイズ
            this.Load += Dlg_Load;        //WM_INITDIALOG処理
            this.AcceptButton = OkBtn;        // Enter キーで選択できるボタン
            this.CancelButton = CancelBtn;    // Esc キーで選択できるボタン
        }

        //WM_INITDIALOG処理
        private void Dlg_Load(object sender, EventArgs e)
        {
            //メッセージラベル
            Label label = new Label();
            label.Location = new Point(10, 10);
            label.AutoSize = true;
            label.Text = message;
            this.Controls.Add(label);
            //コンボボックス(yyList)
            Label yylabel = new Label();
            yylabel.Location = new Point(10, label.Height + 20);
            yylabel.AutoSize = true;
            yylabel.Text = "年(西暦)";
            this.Controls.Add(yylabel);
            yyList = new ComboBox();
            yyList.Location = new Point(yylabel.Width + 20, label.Height + 20);
            for(int i = DateTime.Today.Year; i >= 1923; i--)    //100年分リスト
            {
                yyList.Items.Add(i.ToString("D4"));        //4桁の十進数文字列をリストに追加
            }
            this.Controls.Add(yyList);
            //コンボボックス(mmList)
            Label mmlabel = new Label();
            mmlabel.Location = new Point(10, label.Height + yylabel.Height + 30);
            mmlabel.AutoSize = true;
            mmlabel.Text = "月";
            this.Controls.Add(mmlabel);
            mmList = new ComboBox();
            mmList.Location = new Point(yylabel.Width + 20, label.Height + yyList.Height + 30);
            for(int i = 1; i <= 12; i++)    //12ケ月分リスト
            {
                mmList.Items.Add(i.ToString("D2"));        //2桁の十進数文字列をリストに追加
            }
            this.Controls.Add(mmList);
            //コンボボックス(ddList)
            Label ddlabel = new Label();
            ddlabel.Location = new Point(10, label.Height + yylabel.Height + mmlabel.Height + 40);
            ddlabel.AutoSize = true;
            ddlabel.Text = "日";
            this.Controls.Add(ddlabel);
            ddList = new ComboBox();
            ddList.Location = new Point(yylabel.Width + 20, label.Height + yyList.Height + mmList.Height + 40);
            for(int i = 1; i <= 31; i++)    //31日分リスト
            {
                ddList.Items.Add(i.ToString("D2"));        //2桁の十進数文字列をリストに追加
            }
            this.Controls.Add(ddList);
            //Cancelボタン
            CancelBtn = new Button();
            CancelBtn.Location = new Point(10, ClientSize.Height - CancelBtn.Height - 10);
            CancelBtn.Text = "Cancel";
            CancelBtn.AutoSize = true;
            CancelBtn.Click += CancelButton_Click;        //クリック時の処理
            this.Controls.Add(CancelBtn);
            //OKボタン
            OkBtn = new Button();
            OkBtn.Location = new Point(ClientSize.Width - OkBtn.Width - 10, ClientSize.Height - OkBtn.Height - 10);
            OkBtn.Text = "OK";
            OkBtn.AutoSize = true;
            OkBtn.Click += OkButton_Click;        //クリック時の処理
            this.Controls.Add(OkBtn);
        }

        private void OkButton_Click(object sender, EventArgs e)
        {
            //OKボタン
            try    //Parseメソッドで日付に変換できなかった場合のエラーを処理する
            {
                _date = DateTime.Parse(yyList.Text + "/" + mmList.Text + "/" + ddList.Text);
                //_date = DateTime.Parse(yyList.SelectedItem.ToString() + "/" + mmList.SelectedItem.ToString() + "/" + ddList.SelectedItem.ToString());
            }
            catch    //エラー処理
            {
                MessageBox.Show("選択された日付が不正です", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                this.DialogResult = DialogResult.None;    // Meaning false(値は0)
                this.Close();
                return;    //voidだが、↓の処理をしないで戻る為
            }
            this.DialogResult = DialogResult.OK;        // Meaning true(値は1)
            this.Close();
        }

        private void CancelButton_Click(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.Cancel;    // Meaning true(値は2)
            this.Close();
        }
    }
}

 

 

前回、C#でカスタムコントロールを作ると書きましたが、さて、どう進めるべきか迷っています。まぁ、手始めにコントロールとは何たるか、の復習から。

 

前にも書きましたが、ダイアログ等に貼るボタンやテキストボックス等のコントロールは、基本「一種のウィンドウ」です。(例えばメニューもメニューバーにのっけたボタン上のウィンドウだし、ポップアップメニューも小さな灰色の四角いウィンドウの組み合わせに過ぎず、デスクトップの中で見られる総てのGUIオブジェクトってみんなウィンドウですよね。)

 

コントロールは、しかし、アプリケーションプログラムという特定の仕事を行うメインウィンドウ(親)の「所有」下で、親と連絡を取りつつ、仕事の実行のための作業を行う子、孫等子孫と言えます。また子ウィンドウとなる為にはWS_CHILDスタイルで作る必要があります。「子ウィンドウ」と「親との関係」は次の引用をご覧ください。

 

子ウィンドウ

子ウィンドウにはWS_CHILD スタイルがあり、親ウィンドウのクライアント領域に限定されます。 通常、アプリケーションは子ウィンドウを使用して、親ウィンドウのクライアント領域を機能領域に分割します。 子ウィンドウを作成するには、CreateWindowEx 関数でWS_CHILDスタイルを指定します。

子ウィンドウには親ウィンドウが必要です。 親ウィンドウには、重なるウィンドウ、ポップアップ ウィンドウ、または別の子ウィンドウを指定できます。 CreateWindowEx を呼び出すときに親ウィンドウを指定します。 CreateWindowEx でWS_CHILD スタイルを指定しても親ウィンドウを指定しない場合、ウィンドウは作成されません。

子ウィンドウにはクライアント領域がありますが、明示的に要求されない限り、他の機能はありません。 アプリケーションでは、タイトル バー、ウィンドウ メニュー、最小化と最大化ボタン、境界線、および子ウィンドウのスクロール バーを要求できますが、子ウィンドウにメニューを設定することはできません。 アプリケーションが子のウィンドウ クラスを登録するとき、または子ウィンドウを作成するときにメニュー ハンドルを指定した場合、メニュー ハンドルは無視されます。 罫線スタイルが指定されていない場合は、罫線のないウィンドウが作成されます。 アプリケーションでは、境界のない子ウィンドウを使用して、親ウィンドウのクライアント領域を分割しながら、ユーザーに対して分割を非表示にすることができます。」(出典

とか、

親ウィンドウとの関係

アプリケーションは 、SetParent 関数を呼び出すことによって、既存の子ウィンドウの親ウィンドウを変更できます。 この場合、システムは古い親ウィンドウのクライアント領域から子ウィンドウを削除し、新しい親ウィンドウのクライアント領域に移動します。 SetParent で NULL ハンドルが指定されている場合、デスクトップ ウィンドウは新しい親ウィンドウになります。 この場合、子ウィンドウはデスクトップ上に描画され、他のウィンドウの境界線の外側に描画されます。 GetParent 関数は、子ウィンドウの親ウィンドウへのハンドルを取得します。

親ウィンドウは、クライアント領域の一部を子ウィンドウに放棄し、子ウィンドウはこの領域からすべての入力を受け取ります。 ウィンドウ クラスは、親ウィンドウの子ウィンドウごとに同じである必要はありません。 つまり、アプリケーションは、異なる外観の子ウィンドウで親ウィンドウに入力し、さまざまなタスクを実行できます。 たとえば、ダイアログ ボックスにはさまざまな種類のコントロールを含めることができます。各コントロールには、ユーザーのさまざまな種類のデータを受け入れる子ウィンドウがあります。

子ウィンドウには親ウィンドウが 1 つだけありますが、親ウィンドウには任意の数の子ウィンドウを含めることができます。 各子ウィンドウには、子ウィンドウを含めることができます。 このウィンドウチェーンでは、各子ウィンドウは元の親ウィンドウの子孫ウィンドウと呼ばれます。 アプリケーションは IsChild 関数を使用して、特定のウィンドウが子ウィンドウか、特定の親ウィンドウの子孫ウィンドウかを検出します。

EnumChildWindows 関数は、親ウィンドウの子ウィンドウを列挙します。 次 に、EnumChildWindows は 、各子ウィンドウにハンドルをアプリケーション定義のコールバック関数に渡します。 指定された親ウィンドウの子孫ウィンドウも列挙されます。」(出典

 

注意しなければならないのは、「ウィンドウ」は常に「」に「所有(Own)」されていますが、ウィンドウでなくても(WS_CHILDスタイルがなくても)CreateWIndow(Ex)関数でhwndParentに"NULL"を指定(注)せずに、他のウィンドウを指定すると指定されたhwndParentの「所有」下に入ります。

注:この記事によれば、"NULL"にすると「Top-Level Windowになる」、というようです。また、末尾に載せたサンプルプログラムの紫の行青の行を交互に実行することで、ToolWindowを子ウィンドウにしてメインウィンドウのクライアントエリアに閉じ込めるか、子ウィンドウにしないかで外に出すことを許すか、という差がよく分かります。

 

ということで、コントロールはWS_CHILDスタイル付で作られ、メインウィンドウ(親)の「所有」下で親と連絡を取りつつ、作業をするウィンドウといえます。

 

次に親との「連絡を取る」については、↓の引用を参照してください。私の理解ではWin16の当時は総てWM_COMMANDでの処理、Win32となり、コモンコントロールが発達してからはWM_NOTIFYに移行したと理解しています。(C#のイベント処理に出てくる「object sender, EventArgs e」は、EventArgsと、PaintEventArgs等(特定イベントの)EventArgsの関係を見ていると、様々なコントロールの通信用構造体に必ず含まれるNMHDR構造体と深い関連がありそうですね。

        typedef struct tagNMHDR {

            HWND hwndFrom;    //通知の有ったコントロールのウィンドウハンドル

            UINT_PTR idFrom;    //通知の有ったコントロールのコントロールID

            UINT code;              //コントロールからのメッセージ

        } NMHDR;

コントロールからの通知

コントロールは、通常、ユーザーからの入力によってトリガーされるイベントがコントロール内で発生したときに、親ウィンドウに通知メッセージを送信する子ウィンドウです。 アプリケーションは、これらの通知メッセージに基づいて、ユーザーがアプリケーションにどのようなアクションを望んでいるかを判断します。 WM_HSCROLLメッセージとWM_VSCROLL メッセージを使用して親に変更を通知するトラックバーを除き、共通コントロールは通知のリファレンス トピックで指定されているように、WM_COMMANDメッセージまたはWM_NOTIFY メッセージとして通知を送信します。 通常、古い通知 (長い間 API に含まれている通知) では 、WM_COMMANDが使用されます。

WM_NOTIFYの lParam パラメーターは、NMHDR 構造体のアドレスか、最初のメンバーとして NMHDR を含むより大きな構造体のアドレスです。 構造体には通知コードが含まれており、通知メッセージを送信した共通コントロールを識別します。 残りの構造体メンバー (存在する場合) の意味は、通知コードによって異なります。

共通コントロールの各種類には、対応する通知コードのセットがあります。 共通コントロール ライブラリには、複数の種類の共通コントロールから送信できる通知コードも用意されています。 送信する通知コードと使用する形式を決定するには、対象の制御に関するドキュメントを参照してください。

WM_NOTIFY メッセージの処理方法を示すコード例については、そのメッセージのリファレンス トピックを参照してください。」

 

と、いうことで、

 

(1)何らかの目的と機能を持ったウィンドウを作り、

(2)それを他のウィンドウに所有される子ウィンドウになって、

(3)(必要に応じ)親と通信する

 

ようなプログラムを作ればよい、ということになります。なお、BCCSkeltonを使ってC++でカスタムコントロールを作る例としては過去ログの"PICTUREBOX"や"DICE"を参照してください。

 

【参考プログラム】

ToolWindowプログラムは、プレーンの親ウィンドウにボタンを貼り付けて、それを押すとToolWindowを作成して表示します。
この際、ToolWindowのスタイルにWM_CHILDを入れるか、入れないかで子ウィンドウの表示が親ウィンドウのクライアント領域に限定されるか否かの違いが出ます。
いずれにしても、「親」を指定すると「親の死に目」には「子も道連れ」になることは相違ありません。

/////////////////////////////////////

// Sample ToolWindow Program

/////////////////////////////////////

#include <windows.h>
#define ID_BUTTON 100
// Prototype Declaration
HWND CreateToolWindow(HWND hParent, HINSTANCE hInst);
LRESULT CALLBACK ToolWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR lpszCmdLine, int nCmdShow) {

    TCHAR      szAppName[] = TEXT("Sample");
    HWND       hWnd;
    MSG        msg;
    WNDCLASSEX wc;
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = WindowProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInst;
    wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
    wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = szAppName;
    wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
    if(!RegisterClassEx(&wc))
        return 0;
    hWnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst, NULL);
    if(!hWnd)
        return 0;
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    
    while(GetMessage(&msg, NULL, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    static HWND hWndTool = NULL;

    switch(uMsg) {
    case WM_CREATE:
        CreateWindowEx(0, TEXT("BUTTON"), TEXT("ツールウインドウ作成"), WS_CHILD | WS_VISIBLE, 30, 30, 190, 30, hWnd, (HMENU)ID_BUTTON, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
        return 0;
    case WM_COMMAND:
        if(LOWORD(wParam) == ID_BUTTON) {
            if(!IsWindow(hWndTool))
                hWndTool = CreateToolWindow(hWnd, (HINSTANCE)GetWindowLongPtr(hWnd, GWLP_HINSTANCE));
            else
                SetActiveWindow(hWndTool);
        }
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        break;
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

HWND CreateToolWindow(HWND hParent, HINSTANCE hInst) {
    TCHAR      szAppName[] = TEXT("ToolWindow");
    HWND       hWnd;
    WNDCLASSEX wc;

    if (!GetClassInfoEx(hInst, szAppName, &wc)) {
        wc.cbSize        = sizeof(WNDCLASSEX);
        wc.style         = 0;
        wc.lpfnWndProc   = ToolWindowProc;
        wc.cbClsExtra    = 0;
        wc.cbWndExtra    = 0;
        wc.hInstance     = hInst;
        wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
        wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
        wc.hbrBackground = (HBRUSH)(COLOR_MENU + 1);
        wc.lpszMenuName  = NULL;
        wc.lpszClassName = szAppName;
        wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
        
        if (!RegisterClassEx(&wc))
            return NULL;
    }
    hWnd = CreateWindowEx(WS_EX_TOOLWINDOW, szAppName, szAppName, WS_SYSMENU | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 300, 200, (HWND)hParent, NULL, hInst, NULL);  //外に出す
//    hWnd = CreateWindowEx(WS_EX_TOOLWINDOW, szAppName, szAppName, WS_SYSMENU | WS_CHILD | WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 300, 200, (HWND)hParent, NULL, hInst, NULL);    //クライアントエリア内に留まる

    if (hWnd == NULL)
        return NULL;
    return hWnd;
}

LRESULT CALLBACK ToolWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    switch (uMsg) {
    case WM_CREATE:
        return 0;
    default:
        break;
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

 

前回、C#でウィンドウ(Form)ベースでEaselクラスを使ってバイオリズムグラフの表示を行う試作品を作りました。

 

その時にこれだと余り面白くないので「ひねり」が欲しい、という思いがしてまだ公開を延期しました。そしてその「ひねり」の内容として「C#でカスタムコントロールを作る」というテーマを考えてみました。

 

「カスタムコントロール」とは、ボタンやテキストボックスのような既成のコントロールではなく、ユーザーが作成するコントロールのことです。BCCSkeltonでは過去に、

 

 

 

 

を作ったことがありました。これをC#で「バイオリズム」ネタで作ろうという試みです。(換言すれば↑のプログラムと全く同じ(かそれ以上の)ものを、独立した「バイオリズムコントロール」を使って作る試みです。)

 

次回からはそれをテーマにやりましょう。

 

【Biorythm.dllを使い、BrmBoxコントロールを使ったプログラムイメージ】

(※コントロールにポップアップメニューを追加)

 

何か、(実効性がないが)バイオリズムが正弦式を使ったグラフを描くので、Easel()にはよいかな、と少し嵌りました。

と言っても、式は↓の通りで、難しくはありません。

注:この記事で、ボケてローカルGraphicsオブジェクトを開放し忘れていましたので修正しています。

 

sin(2π x (指定日から誕生日までの経過日数)/T)

但し、T=23(身体)、28(感情)、33(知性)

 

実際にダイアログ風(サイズ可変ですが...)のFormにPictureBoxを貼って作ってみると結構それなりの感があります。

 

しかし、実際に描画命令を書くのは画面消去、文字描画、BoxとLineだけなので別にEaselを使わなくてもよい感じですね。

 

所で、次のようなコーディングをして実行するとコメントにある通り、条件が成立しないので、???となりました。

 

            for(double i = 0; i < 30; i += 0.1)        //指定日を中央に30日のデータを表示
            {
            ... 略 ...

                if(i  == 15.0)    //基準日処理-if(i == 15.0 または 15d)やif(i.Equal(15.0 または 15d))は失敗する。Why?
               {

                ... 略 ...

 

webでも調べたのですが、今一つストライクゾーンに入る回答は無し。こういう時は最後の頼みの「ChatGPT様」。矢張り、ど真ん中の回答でした。

 

/* <<<< ChatGPTの回答 >>>>
提供されたコードは、for ループを使用して変数 i を0から30まで0.1ずつ増加させながら処理を行っています。
ループ内の条件式は、変数 i の整数部分が15と等しい場合に条件を満たします。
問題が発生する理由は、浮動小数点数の計算における精度の問題にあります。浮動小数点数は2進数で表現されるため、
十進数の値を正確に表現することができない場合があります。したがって、 double 型の変数 i の値が厳密に15となる
ことはありません。
具体的には、ループ内で i に0.1を加えていくプロセスにおいて、小数点以下の値を2進数で正確に表現することができ
ません。その結果、 i の値が理想的な値とは微妙に異なることがあります。
このため、(int)i == 15 の条件式が厳密に成立しない場合があります。代わりに、浮動小数点数の比較を行う場合は、
許容誤差を考慮する必要があります。
*/

 

なーるほど、ということで条件式を次のように変更。

    if((int)(i * 10) == 150)

正常動作し始めました。

 

さて、このままコードを公表してもよいのですが、このままでは余りに「ひねり」がないので、何か面白いテーマをつけようと今考え中。

 

Stay tuned!!!

さて、昔のBasicプログラムの移植の為に描画ツール、Easelを作りましたが、まだネタを決めきっておりません。

が、

昔々のROM Basic時代のユーザーサンプルプログラムの定番に「バイオリズム(Biorythm)」というものがありました。しかし参照にある通り、その科学的根拠は未定であり、疑似科学(というより似非科学(junk science)?)と言われているようです。

その原理は、人の誕生と共に始まる、23日の身体周期(Physical cycle)、28日の感情周期(Emotional or sensitivity cycle)および33日の知性周期(Intellectual cycle)という正弦波の上下により、特定日の高調、低調、不安定を判断するものです。(注)

注:既にWEBベースで診断できるサイトが、これとか、あれとか、それとか、いっぱいあります。正弦波の計算は次の通り。

sin(2pi x (指定日から誕生日までの経過日数)/T) ※T=23(身体)、28(感情)、33(知性)

 

こいつを習作としてなんかできないかな?という思い付きから少し調べていると、誕生日、指定日に関して西暦、うるう年、経過日数計算に興味が移りました。まずはこれからか?

 

1.西暦について

前にBCCSkeltonでCalenderというMCコントロールの習作を作った時の「解説」を引用します。

西暦はローマ歴から始まり、シーザーがエジプト歴による修正を命じた後、何度かの修正を経て、現在のグレゴリオ歴に至りました。1752年9月に修正を行った英国では過去の誤差修正(閏年調整)の為に9月2日の後が14日となりました。残念ながら、MCコントロールは1753年から9998年までしか表示できません。」なお、グレゴリオ暦は1582年10月15日の採用だそうです。

 

2.うるう年(Leap year)

うるう年の解説はこちら。↓は2020年に定年退職後、再度Cを復習したときに作った関数をC#に焼き直したものです。

    ///////////////////
    //うるう年判別関数
    ///////////////////
    bool CheckLeap(int y)

    {
        //(1) 西暦年号が4で割り切れ、且つ (2) 西暦年号が100で割り切れてない、または400で割り切れる
        return ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0) ;
    }

 

3.経過日数計算

先ずは誕生日から指定日をC#で愚直に計算するとこうなる、というプログラムを示します。

    /////////////////
    //愚直な日数計算
    /////////////////
    public int NODays(int bYear, int bMonth, int bDay, int tYear, int tMonth, int tDay)
    {
        //12か月の日数を定数とする
        int[] month = new int[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        //経過日数の初期化
        int days = 0;
        //生年月日初月の日数
        days = month[bMonth - 1] - bDay;
        //翌月から年末までの日数
        for(int i = bMonth; i < 12; i++)
            days += month[i];
        //初年のうるう年修正
        if(bMonth < 3 && CheckLeap(bYear))
            days++;
        //翌年から指定年前年までの日数
        for(int i = bYear + 1; i < tYear; i++)
        {
            days += 365;
            if(CheckLeap(i))
                days++;
        }
        //指定年の日数
        for(int i = 0; i < tMonth - 1; i++)
            days += month[i];
        //初年のうるう年修正
        if(CheckLeap(tYear) && (tMonth > 2))    //3月以降の場合
            days++;
        //指定月の日数(2月29日の指定の場合はtDayでうるう年調整される)
        days += tDay;
        return days;
    }

 

次にwebにあった旧いVB6プログラムを矢張りC#で組みなおしたものを以下に示します。

    //////////////////////////////////
    //VB6のプログラムにあった計算方法
    //////////////////////////////////
    public int NumOfDays(int bYear, int bMonth, int bDay, int tYear, int tMonth, int tDay)
    {
        if(bMonth == 1 || bMonth == 2)
        {
            bMonth = bMonth + 12;
            bYear = bYear - 1;
        }
        if(tMonth == 1 || tMonth == 2)
        {
            tMonth = tMonth + 12;
            tYear = tYear - 1;
        }
        int days = ((int)(365.25 * (double)tYear) + (int)(tYear / 400) - (int)(tYear / 100) + (int)(30.59 * (double)(tMonth - 2)) + tDay)
                     - ((int)(365.25 * (double)bYear) + (int)(bYear / 400) - (int)(bYear / 100) + (int)(30.59 * (double)(bMonth - 2)) + bDay);
        return days;
    }

 

こういう「先ずは愚直にコーディングし、次にアルゴリズムを色々と考えて短縮化、高速化を図る」のがプログラミングの楽しみなんですが、C#はそういう楽しみを奪います。なんと次の一行が終わり。

    TimeSpan ts = DOT - DOB;    //DOTとDOBはDateTime構造体で、減算結果が入るtsもTimeSpan構造体

 

C#やっていると、あまりに簡単なので、段々アルゴリズムとか、考えるとかしなくなりそうで怖いですね。

 

私は絵心がないので、余り描画ソフトには関心がありませんでした。とはいえ、PCでプログラミングするなら描画は昔から必須でした。(注)

注:私のPC遍歴と画素、色数一覧

1983年 MSX(SONY HitBitー8 Bit):256x192、16色(参考:MSX2:512×212、256色)

1986年 MZ2500(Sharpー8 Bit):640x400、16色(320x200では256色)

1991年 米国IBM互換PC:VGA(640x480、15色)、SVGA(800x600、16 - 256色)、XVGA(1024×768、16 - 256色)

以降は90年代末に1280x800、21世紀から1600×900、1920×1080(現在)で16,777,215(RGB - FF FF FF)色にアルファ値(0が透明で255(FF)が不透明)が付きます。

 

8 bit時代から関数を用いたワイアーメッシュ曲線図形が好きで、(何の役にもたちませんが)表示して喜んでいました。(当時はBasic、その後はCで3つの「3D地形」図形をRGBの色分けをして表示していましたね。)その所為か、その手のサンプルがあるとC++に移植して遊んでいました。(注)

注:Visual Basic 6.0のサンプルコードがあったので、最初はBorland C++ Builderで、次にそれをさらにBCCSkeltonに(CANVASから派生させたCPICBOXを使って)移植しました。

 

そんなことをC#でもしたかったのでCANVAS類似のEaselクラスを作ったのですが、移植すべきネタが余りありません。

 

(1)QBasic64のサイトに色々とあったので「これにしよう!」と思ったのですが、色数が多く、複雑な図形のサンプルはコード量が大きく、その中身は浮動小数点データばかりで、流石に移植するのがためらわれます。(ってか、無理っ!!!)その他の簡単そうなところで、唯一採用したのは羊歯の葉のような図形です。(他は2つ移植してみたのですが、面白くない図形で没にしました。)

(2)Webを探し回ったのですが、昔あったようなワイヤーメッシュ図形などは見当たらず、そういう3D系はOpenGL(OpenTK)等のサンプルになってしまうので、あきらめることにしました。

(3)逆に(GDI+に依拠する)C#のGraphicsクラスはメソッドが多様多彩であり、こちらからサンプルを選んでは?と考え直しました。Graphicsクラスのメソッドは、Bitmap等(DrawImage)やIcon(DrawIcon)等の表示に加え、線描画の'Draw'系と塗潰しの'Fill'系に分かれ、線はDrawLine、四角(多角)形はDraw|FillRectanglar/Polygon、楕円(含真円)はDraw|FillEllipse/Arc/Pie等のメソッドが多様な引数でオーバーロードされ、覚えきれない量となっています。しかしその中で昔のGDIにはなかったDrawCurve/FillClosedCurve、DrawBezierというメソッドもあります。

(4)ということで、取り急ぎそいつらを見てみようとサンプルを見つけてEasel用に焼き直してみました。

 

まぁまぁのシュール度で満足です。今後はもう少し描画系のサンプルを追加してPictureBoxのサンプルにしたいと思っています。

 

ごめんなさい:Canvasのサイズ変更時に使うローカルオブジェクトを開放していませんでした。以下赤太字部分を追加しています。(修正日:2023/05/28)

 

前回「ChatGPT様にお伺いを立てたらPaintの悩みが突然終焉を迎えました」と書きました。

具体的には「C#で、連続する領域色を描画色で塗潰すにはどうしたらよいのでしょう?」というような質問をしたら、後掲のEaselのPaintメソッド(紫字部分)を教えてもらったからです。

 

このコード、よく見るとアルゴリズムは私の最初の思い付きに似ています(注1)が、更によく練れています。私は上下左右「斜め」迄入れていたのですが、上下左右だけで斜めもカバーされることはこのコードを見て「あぁ」と分かりました。(注2)また拡大させる方法は「始点(対象点)を描画色にかえた後、その周囲のピクセルをチェックして同じ領域色であれば『スタックに積む』」というとことが(キューではないですが)前に紹介したC++のサンプル注3)に似ています。しかし、このChatGPTのコードの最大の相違は(描画したピクセルを何度も再チェックする非効率があったり、ドットごとに描画するSetPixelを使っているにもかかわらず)「結構速い、十分に速い」ことです。

 

注1:「前々回、ベッドの中で思いついた『Easel.Canvas(Bitmap)の一点(x, y)のピクセルの色を調べ(Canvas.GetPixel())、↑~→~↓~←(~には「斜め」入れて0~7の方向を設定)の順に隣接するピクセルの色を調べ、同じ色のピクセルがあれば(x, y)に描画色の点を打って、o-7の方向を定め、次に同じ色のピクセルで同様の処理を繰り返す(再帰)』」

注2:つまり、始点(●)の周囲のピクセル色を調べてスタックに入れて拡大してゆく、という考えは同一であるものの、「上下左右」だけで「斜め」もカバーされること(=既に描画済のピクセルのチェック処理が減少するのでより効率的)、またシステムスタックを使う「再帰」ではなく、より大きなスタックサイズを採れる(C++のSTLみたいな)Stackクラスオブジェクトを使うことを、当時の私は思いつきませんでした。

【始点()、次世代次々世代および次々々世代候補の3代までの例(〇は未処理ピクセル)】

〇〇〇〇〇〇

〇〇〇〇

〇〇〇〇

〇〇〇〇〇〇

注3:C++のサンプルのメイン描画処理の構造概略

  //描画ループ
    do {
        //キューからデータを取り出し

        //領域色が描画色スキップする
        //左右の領域色境界を探してその線分を描画
        //一段上のラインを走査してキューに登録
        //一段下のラインを走査してキューに登録

    } while(); //キューに未処理データががあるか

 

このコード(オリジナルは"FillArea"メソッドだった)をほぼそのまま取り入れて(この場合、CopyrightはOpenAIにあるのだろうか?-注4)、ほぼほぼBCCSkeltonのCANVASクラスみたいになったので、取り敢えずこれ(↓末尾のコード)をVersion1.0にしたいと思います。(少々疲れたし...)

注4:これも調べるのが面倒なので本人(ChatGPT)に「ChatGPTの作成する文章等成果物の著作権は誰にありますか?」聞いてみた。その答えは「ChatGPTによって生成された文章や成果物の著作権は、OpenAIに帰属します。OpenAIはChatGPTの開発およびトレーニングを行った団体であり、その知的財産権を保有しています。」と来たので、「ChatGPTの作成する文章等成果物をユーザーは自由に使ってよいのですか?」と追加質問したら「OpenAIは、ChatGPTによって生成された文章や成果物に対して、使用制限を課していません。ユーザーはChatGPTが生成した文章を自由に使用することができます。ただし、ユーザーが生成物を利用する際には、他の法的要件や倫理的な考慮事項に従う必要があります。例えば、著作権やプライバシーに関連する法律に違反しないように注意する必要があります。また、生成された文章が意見や情報を提供するものであるため、その正確性や信頼性については検証が必要です。」という答えでした。

 

Easel(画架)クラスは名前空間Atelier(アトリエ)というライブラリー(DLL)にして、アプリケーションで"using Atelier; //Atelier.dllを使う"を入れ、コンパイルの際に「参照DLL」を指定するだけで使えるようにしました。(今後Atelierに他のクラスを追加したり、Easelをバージョンアップしてもよいように。)以下にコードとオプションファイルの内容を書いておきます。

 

ps. しっかし、よくできているなぁ、ChatGPT。そして間違いなくこいつのおかげで人間は好奇心を失い、分からないことを調べなくなったり、勉強しなくなって、単にAIに質問してその通り行動する「家畜化」が進行してゆくよなぁ、今回のこの俺のように。くわばら、くわばら。

 

【Easelクラス(Atelier.cs)】

/////////////////
// C# Atelier.cs
/////////////////
using System;
using System.Collections.Generic;    //Stackを使用する為
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

namespace Atelier                    //Atelier.dllを使用する為
{
    /*    "using System.Collections.Generic;"
        "using System.Drawing;"
        "using System.Drawing.Drawing2D;"
        "using System.Drawing.Imaging;"が必要 */
    public partial class Easel
    {
        //変数
        private Pen _gpen;                            //描画用ペン
        private Brush gBrush;                        //描画用代表ブラシ(以下のブラシの代入用)
        private SolidBrush _gsbrush;                //描画用ソリッドブラシ
        private HatchBrush _ghbrush;                //描画用ハッチブラシ
        private LinearGradientBrush _glgbrush;        //描画用リニアグラディエントブラシ
        private TextureBrush _gtbrush;                //描画用テクスチュァブラシ
        private Font _gfont;                        //描画用フォント
        private Image _gimage;                        //画像入出力用イメージ
        //プロパティ
        public Bitmap Canvas {set; get;}            //仮想画面ビットマップ
        public Graphics gHandle {set; get;}            //仮想画面のグラフィック
        public Color gFtColor {set; get;}            //仮想画面の前景色
        public Color gBkColor {set; get;}            //仮想画面の背景色
        public Pen gPen                                //描画用ペン
        {
            set
            {
                if(_gpen != null)
                    _gpen.Dispose();
                _gpen = value;
            }
            get
            {
                return _gpen;
            }
        }
        public SolidBrush gsBrush                    //描画用ソリッドブラシ
        {
            set
            {
                if(_gsbrush != null)
                    _gsbrush.Dispose();
                _gsbrush = value;
            }
            get
            {
                return _gsbrush;
            }
        }
        public HatchBrush ghBrush                    //描画用ハッチブラシ
        {
            set
            {
                if(_ghbrush != null)
                    _ghbrush.Dispose();
                _ghbrush = value;
            }
            get
            {
                return _ghbrush;
            }
        }
        public LinearGradientBrush glgBrush            //描画用リニアグラディエントブラシ
        {
            set
            {
                if(_glgbrush != null)
                    _glgbrush.Dispose();
                _glgbrush = value;
            }
            get
            {
                return _glgbrush;
            }
        }
        public TextureBrush gtBrush                    //描画用テクスチュァブラシ
        {
            set
            {
                if(_gtbrush != null)
                    _gtbrush.Dispose();
                _gtbrush = value;
            }
            get
            {
                return _gtbrush;
            }
        }
        public Font gFont                            //描画用フォント
        {
            set
            {
                if(_gfont != null)
                    _gfont.Dispose();
                _gfont = value;
            }
            get
            {
                return _gfont;
            }
        }
        public Image gImage                            //画像入出力用イメージ
        {
            set
            {
                if(_gimage != null)
                    _gimage.Dispose();
                _gimage = value;
            }
            get
            {
                return _gimage;
            }
        }
        //コンストラクター1
        public Easel()
        {
            //デスクトップサイズのビットマップ仮想画面の作成
            InitCanvas(    System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
                        System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height
            );
        }
        //コンストラクター2
        public Easel(int w, int h)
        {
            //指定サイズのビットマップ仮想画面の作成
            InitCanvas(w, h);
        }
        //デストラクター
        ~Easel()
        {
            if(gImage != null)
                gImage.Dispose();                    //イメージリソースの開放
            if(gFont != null)
                gFont.Dispose();                    //フォントリソースの開放
            if(gtBrush != null)
                gtBrush.Dispose();                    //テクスチュァブラシリソースの開放
            if(glgBrush != null)
                glgBrush.Dispose();                    //リニアグラディエントブラシリソースの開放
            if(ghBrush != null)
                ghBrush.Dispose();                    //ハッチブラシリソースの開放
            if(gsBrush != null)
                gsBrush.Dispose();                    //ソリッドブラシリソースの開放
            if(gBrush != null)
                gBrush.Dispose();                    //ブラシリソースの開放
            if(gPen != null)
                gPen.Dispose();                        //ペンリソースの開放
            if(gHandle != null)
                gHandle.Dispose();                    //ビットマップリソースの開放
            if(Canvas != null)
                Canvas.Dispose();                    //ビットマップリソースの開放
        }
        //Easelクラスオブジェクトの初期化
        public bool InitCanvas(int w, int h)
        {
            //wまたはhが0の場合、「使用されたパラメーターが有効ではありません」エラーとなる為
            if(w * h == 0)
                return false;
            //デスクトップサイズのビットマップ仮想画面の作成
            if(Canvas == null)    //最初のインスタンス生成時
            {
                Canvas = new Bitmap(w, h);
                //前景色(ウィンドウ文字色)
                gFtColor = SystemColors.WindowText;
                //背景色(コントロール背景色)
                gBkColor = SystemColors.Control;
                //描画ペン
                gPen = new Pen(gFtColor, 1);            //初期値は細黒字Pen
                //描画ブラシ
                //gtBrush = new TextureBrush(Image);    //テクスチュァブラシ(https://learn.microsoft.com/ja-jp/dotnet/api/system.drawing.texturebrush?view=windowsdesktop-8.0)
                //glgBrush = new LinearGradientBrush(Point, Point, Color, Color);    //リニアグラディエントブラシ(https://learn.microsoft.com/ja-jp/dotnet/api/system.drawing.drawing2d.lineargradientbrush.-ctor?view=windowsdesktop-8.0#system-drawing-drawing2d-lineargradientbrush-ctor(system-drawing-point-system-drawing-point-system-drawing-color-system-drawing-color))
                //ghBrush = new HatchBrush(HatchStyle.Cross, gFtColor, gBkColor);        //ハッチブラシ(https://learn.microsoft.com/ja-jp/dotnet/api/system.drawing.drawing2d.hatchbrush?view=windowsdesktop-8.0)
                gsBrush = new SolidBrush(gFtColor);        //ソリッドブラシ
                gBrush = gsBrush;                        //初期値はソリッドブラシ
            }
            else
            {
                Rectangle srcRect;
                Rectangle desRect;
                Image img = (Bitmap)Canvas.Clone();        //旧画像をBitmapで保存
                Canvas.Dispose();                        //Canvasを一旦開放
                Canvas = new Bitmap(w, h);                //新しいサイズで作成
                Graphics g = Graphics.FromImage(Canvas);
                if(w > img.Width)
                {
                    if(h > img.Height)
                    {
                        srcRect = new Rectangle(0, 0, img.Width, img.Height);
                        desRect = new Rectangle(0, 0, img.Width, img.Height);
                    }
                    else
                    {
                        srcRect = new Rectangle(0, 0, img.Width, h);
                        desRect = new Rectangle(0, 0, img.Width, h);
                    }
                }
                else
                {
                    if(h > img.Height)
                    {
                        srcRect = new Rectangle(0, 0, w, img.Height);
                        desRect = new Rectangle(0, 0, w, img.Height);
                    }
                    else
                    {
                        srcRect = new Rectangle(0, 0, w, h);
                        desRect = new Rectangle(0, 0, w, h);
                    }
                }
                g.DrawImage(img, desRect, srcRect, GraphicsUnit.Pixel);    //コピー

                g.Dispose();    //ローカルGraphicsオブジェクトの開放を追加(2023/05/28
            }
            //Graphicsクラス(アンチエイリアス)
            if(gHandle != null)        //既存のCanvasを変更する場合、一旦Graphicsを開放する
                gHandle.Dispose();
            gHandle = Graphics.FromImage(Canvas);
            gHandle.SmoothingMode = SmoothingMode.AntiAlias;
            gHandle.PixelOffsetMode = PixelOffsetMode.HighQuality;
            return true;
        }
        //前景色に合わせてペン、ブラシの色を変える
        private void UpdateColor()
        {
            gPen.Color = gFtColor;
            gsBrush.Color = gFtColor;
        }
        //描画ブラシを選択する(インスタンスが無ければエラー)
        public bool SelectBrush(int n)
        {
            switch(n)
            {
            case 0:    //ソリッドブラシ
                gBrush = gsBrush;
                break;
            case 1:    //ハッチブラシ
                if(ghBrush != null)
                    gBrush = ghBrush;
                else
                    return false;
                break;
            case 2:    //リニアグラディエントブラシ
                if(glgBrush != null)
                    gBrush = glgBrush;
                else
                    return false;
                break;
            case 3:    //テキスチュァブラシ
                if(gtBrush != null)
                    gBrush = gtBrush;
                else
                    return false;
                break;
            default:
                return false;
            }
            return true;
        }
        //サイズ変更
        public bool Resize(int w, int h)
        {
            return InitCanvas(w, h);
        }
        //画面消去1
        public void Clear()
        {
            gHandle.Clear(gBkColor);
        }
        //画面消去2
        public void Clear(Color c)
        {
            gHandle.Clear(c);
        }
        //点を描画
        public void Dot(int x, int y)
        {
            Canvas.SetPixel(x, y, gFtColor);
        }
        //線を描画
        public void Line(int x1, int y1, int x2, int y2)
        {
            UpdateColor();
            gHandle.DrawLine(gPen, x1, y1, x2, y2);
        }
        //矩形を描画(フラグf-塗りつぶし有無)
        public void Box(int x1, int y1, int x2, int y2, bool f = false)
        {
            UpdateColor();
            if(f)
                gHandle.FillRectangle(gBrush, x1, y1, x2 - x1 + 1, y2 - y1 + 1);
            else
                gHandle.DrawRectangle(gPen, x1, y1, x2 - x1 + 1, y2 - y1 + 1);
        }
        //円を描画(フラグf-塗りつぶし有無)
        public void Circle(int x, int y, int r, bool f = false)
        {
            UpdateColor();
            if(f)
                gHandle.FillEllipse(gBrush, x - r, y - r, 2 * r, 2 * r);
            else
                gHandle.DrawEllipse(gPen, x - r, y - r, 2 * r, 2 * r);
        }
        //矩形内接楕円を描画(フラグf-塗りつぶし有無)
        public void Ellipse(int x1, int y1, int x2, int y2, bool f = false)
        {
            UpdateColor();
            if(f)
                gHandle.FillEllipse(gBrush, x1, y1, x2 - x1 + 1, y2 - y1 + 1);
            else
                gHandle.DrawEllipse(gPen, x1, y1, x2 - x1 + 1, y2 - y1 + 1);
        }
        //円弧(線無)を描画
        public void Arced(int x1, int y1, int x2, int y2, int dg1, int dg2)
        {
            UpdateColor();
            gHandle.DrawArc(gPen, x1, y1, x2 - x1 + 1, y2 - y1 + 1, dg1, dg2);
        }
        //円弧(線有)を描画
        public void Chorded(int x1, int y1, int x2, int y2, int dg1, int dg2, bool f = false)
        {
            UpdateColor();
            if(f)
                gHandle.FillPie(gBrush, x1, y1, x2 - x1, y2 - y1, dg1, dg2);
            else
                gHandle.DrawPie(gPen, x1, y1, x2 - x1, y2 - y1, dg1, dg2);
        }
        //指定色で塗潰す
        public void Paint(int x, int y, Color col)
        {
            Color AreaCol = Canvas.GetPixel(x, y);        //塗潰す領域色を取得
            Stack<Point> stack = new Stack<Point>();    //領域塗潰し用スタック
            stack.Push(new Point(x, y));                //始点の保存
            while (stack.Count > 0)
            {
                Point currentPoint = stack.Pop();        //塗潰しスタックから取り出し
                int currentX = currentPoint.X;
                int currentY = currentPoint.Y;
                if(currentX >= 0 && currentX < Canvas.Width && currentY >= 0 && currentY < Canvas.Height)
                {
                    if(Canvas.GetPixel(currentX, currentY) == AreaCol)
                    {
                        //領域色であれば塗潰す
                        Canvas.SetPixel(currentX, currentY, col);
                        //現在座標の上下左右のピクセルをスタックに追加
                        stack.Push(new Point(currentX - 1, currentY));
                        stack.Push(new Point(currentX + 1, currentY));
                        stack.Push(new Point(currentX, currentY - 1));
                        stack.Push(new Point(currentX, currentY + 1));
                    }
                }
            }
        }

        //ビットマップ画像取込み
        public void GetBMP(PictureBox pb, int x1, int y1, int x2, int y2)
        {
            Bitmap bmp = new Bitmap(x2 - x1 + 1, y2 - y1 + 1, PixelFormat.Format32bppArgb);
            Graphics g = Graphics.FromImage(bmp);
            Point pt = pb.PointToScreen(new Point(x1, y1));
            g.CopyFromScreen(pt, new Point(0, 0), bmp.Size, CopyPixelOperation.SourceCopy);
            g.Dispose();
            bmp.Save("Temp.bmp", ImageFormat.Bmp);
            gImage = bmp;
        }
        //内部ビットマップ画像張付け
        public void PutBMP(int x, int y)
        {
            gHandle.DrawImage(gImage, x, y);    //今のところAND、OR、XORで表示できない
        }
        //外部画像張付け
        public void PutBMP(Image img, int x, int y)
        {
            gHandle.DrawImage(img, x, y);        //今のところAND、OR、XORで表示できない
        }
        //外部画像を指定サイズに変えて張付け
        public void PutBMP(Image img, int x1, int y1, int x2, int y2)
        {
            Rectangle rec = new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
            gHandle.DrawImage(img, rec);
        }
        //文字を描画
        public void PrintText(String str, Single x, Single y, StringFormat format)
        {
            UpdateColor();
            gFont = System.Drawing.SystemFonts.DefaultFont;
            gHandle.DrawString(str, gFont, gBrush, x, y, format);
        }
    }
}

【オプション(Atelier.opt)-Target=3はライブラリー(DLL)を表します

[Compile Option]
Target=3
Resource=0
RscFile=
IconFile=
DbgOpt=0
WarnErr=5
Others=
 

前々回、ベッドの中で思いついた「Easel.Canvas(Bitmap)の一点(x, y)のピクセルの色を調べ(Canvas.GetPixel())、↑~→~↓~←(~には「斜め」入れて0~7の方向を設定)の順に隣接するピクセルの色を調べ、同じ色のピクセルがあれば(x, y)に描画色の点を打って、o-7の方向を定め、次に同じ色のピクセルで同様の処理を繰り返す(再帰)」注1アイデアを思いつきましたが、システムスタックが通常1MB程度なので、直ぐにスタックオーバーフローでやられました。

次に前回昔つくったツールバービットマップ編集ソフトのC++で書かれた塗潰しルーチン(注2)を紹介し、テストプログラムで確認しましたが、速度の点で今一つだったので取り敢えず放置しています。

注1始点の領域色を採り、描画色で点を打ち、次に隣接する周辺ピクセルの色を調べて同様に描画領域色を拡大してゆくアルゴリズム。
注2始点の左右走査で「連続した領域色の横線」を調べながら横線を描画し、上下に範囲拡大してゆくて塗潰すアルゴリズム。

 

ということで今度はC#でWin APIのExtFloodFillが上手く機能しない原因を探ってみました。まずはオリジナルのコードです。

 

using System.Runtime.InteropServices;    //ExtFloodFill関数を使用する為
 

        [DllImport("gdi32.dll")]
        static extern uint GetPixel(IntPtr hdc, int nXPos, int nYPos);
        [DllImport("gdi32.dll")]
        static extern bool ExtFloodFill(IntPtr hdc, int nXStart, int nYStart, uint crColor, uint fuFillType);
        [DllImport("gdi32.dll")]
        static extern IntPtr CreateSolidBrush(uint crColor);
        [DllImport("gdi32.dll")]
        static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr hgdiobj);
        [DllImport("gdi32.dll")]
        static extern bool DeleteObject([In] IntPtr hObject);

 

        //指定色で塗潰す
        public bool Paint(int x, int y, Color col)
        {
            bool result = false;
            IntPtr hDC = gHandle.GetHdc();
            IntPtr hBrush = CreateSolidBrush((uint)ColorTranslator.ToWin32(col));
            IntPtr hOldBrush = SelectObject(hDC, hBrush);
            result = ExtFloodFill(hDC, x, y, (uint)GetPixel(hDC, x, y), 1);    //"#define  FLOODFILLSURFACE  1" in wingdi.h
            SelectObject(hDC, hOldBrush);
            DeleteObject(hBrush);
            gHandle.ReleaseHdc(hDC);
            return result;
        }

 

このコードではExtFloodFillによる塗潰しは行われていないように見えます。その為、Graphicsから取得したhDCの値を調べましたが、PictureBoxのImageと同じでした。そんな状況で不図「ChatGPTに相談してみよう」と思いつき、症状やコードを与えると、正しく理解してアドバイスをくれます。(解決には至りませんでしたが...)

しかし、一番有用であったアドバイスは「Bitmap.Saveメソッドを使って、ビットマップの描画状況を確認してはどうか?」というものでした。そういうことで、実験したいコード(以下の赤部分)を追加(注)して、コンパイル実行してみました。

注:コードの中にウィンドウ画面と保存されたビットマップの画像を表示します。

 

        //指定色で塗潰す
        public bool Paint(int x, int y, Color col)
        {
            IntPtr hDC = g.GetHdc();

 

//C#で描画する
g.DrawRectangle(Pens.Blue, new Rectangle(0, 0, 200, 200));
g.FillRectangle(Brushes.Red, new Rectangle(1, 1, 50, 50));
g.FillRectangle(Brushes.Blue, new Rectangle(70, 70, 50, 50));
g.DrawRectangle(Pens.Blue, new Rectangle(80, 80, 200, 200));
picBox.Image = canvas;
MessageBox.Show("矩形の重なり合った部分を塗りつぶします。");

            bool result = false;
            IntPtr hDC = gHandle.GetHdc();

 

//Win32 GDIで矩形を描く
Rectangle(hDC, 10, 10, 50, 50);
Rectangle(hDC, 30, 30, 70, 70);
Rectangle(hDC, 50, 50, 90, 90);
Ellipse(hDC, 0, 0, 100, 100);


            IntPtr hBrush = CreateSolidBrush((uint)ColorTranslator.ToWin32(col));

Rectangle(hDC, 70, 70, 110, 110);
Ellipse(hDC, 100, 0, 200, 100);
// ビットマップをファイルに保存
Canvas.Save("Canvas_before_fill.bmp", System.Drawing.Imaging.ImageFormat.Bmp);

//Win32 GDIで描画した内容が反映していない。

MessageBox.Show("ExtFloodFillを実行します。");

            IntPtr hOldBrush = SelectObject(hDC, hBrush);
            result = ExtFloodFill(hDC, x, y, (uint)GetPixel(hDC, x, y), 1);    //"#define  FLOODFILLSURFACE  1" in wingdi.h
            ExtFloodFill(hDC, 250, 250, (uint)ColorTranslator.ToWin32(canvas.GetPixel(250, 250)), FLOODFILLSURFACE);
            ExtFloodFill(hDC, 150, 50, (uint)ColorTranslator.ToWin32(canvas.GetPixel(150, 50)), FLOODFILLSURFACE);

            SelectObject(hDC, hOldBrush);
            DeleteObject(hBrush);
            gHandle.ReleaseHdc(hDC);

// ビットマップをファイルに保存
Canvas.Save("Canvas_after_fill.bmp", System.Drawing.Imaging.ImageFormat.Bmp);

//Win32 GDIで描画した内容が反映された。(何処でUpdateしたの?)

            return result;
        }

 

これがどういうことかお判りでしょうか?以下は私の推測です。

(1)Win APIでhDCに描画書き込みを行った後に取得したビットマップ画面には描画内容が表示されていません。C#のBitmapクラスは既存の描画内容のビットマップに新たにGraphicsで描画が加わったビットマップを貼り付け合成して描画内容を更新しているのではないでしょうか?(背景イメージに全面イメージを貼り付ける方法はこれを見てください。)

(2)その為、PictureBoxコントロールの規定値の背景の場合、Imageメンバーは背景が黒(COLORREF 0)になっているのではないでしょうか?

(3)この推定は依然「hDCを採ると同一なのに」「Win APIのGetPixcel関数とC#のBitmap.GetPixelメソッドで取得した色が異なる」という事実にも合致します。

(4)この状態のビットマップでも矩形や楕円を描画することはできますが、「領域色」を「描画色」にするExtFllodFill関数を実行する場合、GetPixel関数やメソッドで取得した色がこのビットマップの色と一致しない場合、ExtFllodFill関数は失敗することになり、塗潰しは行われないことになり、これも経験と符合します。

 

従って、残された道はただ一つ。C#で独自に塗りつぶしルーチンを組む必要がある(その方がC#プログラミングにとって作法にのっとった良い方法である)ということです。

 

じゃ、どうしようか?と考えたときに、

 

そうだ、またChatGPT様に聞いてみよう!

 

と考え、実際聞いたのですが...

 

悩みは突然の終焉を迎えました。

(詳しくは次回)