前にBCCSkeltonの習作として作成したライフゲームに、

 

(1)「寿命」、「性別」、(親子間の生殖を禁止する個体IDとしての)「名前」、(「世界」内の社会IDとしての)「種族」の属性を持つ「生命体(Livant-)」クラスを作り、

(2)メソッドに「有効 vs. 敵対」判断と「(有効なら)共生」「(敵対なら)攻撃」を加え、

(3)「世界(閉鎖領域)」をクラスで表現し、複数の種族(同一IDの生命体)を発生させ、単独別離の状態では従来のライフゲーム類似のルールで発展、衰退するが、発展の過程で他種族に遭遇すると攻撃(より複雑化するなら「避難」も)して自種族を存続させるようにする。

注:「生命体」は"Life"で十分なのですが、「生命エネルギー」をLifeと命名する可能性があったので「体(するもの)」の接尾語である"-ant"(例:mutant→Mutate+ant、variant→Vary+ant)を"Live"につけて造語しましたが、タイムリーにTBSの日曜劇場に乗っかっちゃいましたね。なお、生命エネルギーのプロパティは後に"vital"にしようと考えました。(ついでに言えば、Bサウンドの「別班」の話を引っ張ってきたくて、Vサウンドのフランス語、"VIVANT"に引っ掛けた努力はわかりますが、やや強引感を感じるのは私だけでしょうか?タイトルを日本語で「ビバン」とか「びばん」にしていればまだブリッジがあったかな?しかしそうなると"VIVANT"の語の存在感が薄まるのでしょうね。)

 

という要素を加味したLifeGame+をC#で作ってみようかと考えましたが、「皮」から作り始めると「最終形」が想像されて、

何故か萎えて

しまいました。

 

何故、萎えた

のか?それは私の歳の所為ではなく、LifeGame+に追加するファクターは増殖を制限(注)するものばかりであり、

単に種族の滅亡を促進するだけ

である為に、結果として「最終形」(PC上の見た目)は

単に早く終わるライフゲームでしかない

からです。

:「寿命」を設定したので何もしなくても死んでゆきます。「性別SOGIESexual Orientation、Gender Identity、Gender Expression」を設定したので、「種族」が同一の異性愛男性(50%)と異性愛女性(40%)のカップルでしか子供(次世代)が生まれませんし、また「名前」が同一の親子間での生殖は禁忌なので出生率が激減します。更に「種族」の相違により、「敵対」状態に基づく攻撃でLivantのvitalが減少して更に死亡率が高まります。

 

要すれば、冒頭の画面を元に想像するに、「花の集落がオレンジだけではなく、青や赤のものがあり、近接する集落間では(何処かと何処かの国のように)滅亡が促進されるし、単純なライフゲームよりも少子化で集落の発展が進まない」ゲームになります、というか、にしかなりません

 

要するに、

そういう国、状況の中に、私たちは居る

ということが強く実感されたので、

LifeGame+を作るまでもないな

という結論となりました。悪しからずご容赦下さい。

 

前回、思い付きであのように書いてみましたが、結構面白いテーマかもしれないな、と考えています。

 

私のプログラミング作法としては、

(1)先ず「最終形」をイメージ(視覚的に想像)してみる。

(2)実際にコードを書き始めて、そこ(+試行錯誤)から啓発を得る。

(3)上記(1)←→(2)を繰り返しながら煮詰める。

(4)プロトタイプを作ってみる。

(5)仕様を具体化(文章化)してみる。

なので、先ずは上記(1)、(2)をやってみて「モノになるか」試験を行いたいと、今日はスタートラインの具体化を始めてみました。

 

【LifeGamePlus.cs】

///////////////////////////////
// LifeGamePlus.cs (LifeGame+)
//   Copyright August 2023
//        by Y-Sama
///////////////////////////////

using System;

namespace LifeGamePlus
{
    ///////////////
    //生命体クラス
    ///////////////

    public class Livant
    {
    //プロパティ
        public bool Alive            //生存状態フラグ
        {
            get; set;
        }
        public int Age                //年齢(0→最高年齢(暫定的に64))
        {
            get; set;
        }
        //SOGIE:性的指向(Sexual Orientation)、性自認(Gender Identity)、性表現(Gender Expression)を組み合わせた用語
        public int Sogie            //異性愛女性 = 0(40%)、異性愛男性 = 1(50%)、同性愛 = -1(10%)に設定
        {
            get; set;
        }
        public string Name            //個体的ID(固有名で子が出来た場合のみ付与する)
        {                            //生殖による出生時に「世界」から与えられる
            get; set;
        }
        public string Tribe            //社会的ID(集団の固有名-同一であれば共生、異なれば敵対)
        {                            //出生時に「世界」から与えられる
            get; set;
        }
    //メソッド
        public Livant()                //コンストラクター
        {
            this.Alive = true;            //誕生したので生きている
            this.Age = 0;                //誕生したので0歳
            this.Sogie = GivenGender();    //乱数でジェンダーを決定する
        }
        
    /*    public ~Livant()        //デストラクター(https://ufcpp.net/study/csharp/resource/rm_destructor/)
        {
            //delete this; this = null;    //危険な処理
        } */


        private int GivenGender()    //誕生時の性別の決定
        {
            Random rnd = new System.Random();
            switch(rnd.Next(0, 9))
            {
                case 0:                //10%の同性愛グループ
                    return -1;
                case 1:                //1 - 4の40%は女性異性愛グループ
                case 2:
                case 3:
                case 4:
                    return 0;
                default:            //5 - 9の50%は男性異性愛グループ
                    return 1;
            }
        }
    }

    /////////////////////////////////////
    //世界クラス(生命体の存在する世界)
    /////////////////////////////////////

    public class World
    {
    //プロパティ
        public int Width            //世界の幅
        {
            get; set;
        }
        public int Height            //世界の高さ
        {
            get; set;
        }
    //メソッド
    }

    ////////////////
    // For Testing
    ////////////////

    public class Application
    {
        public static void Main()
        {
        }
    }

}
 

こんな感じ?すべてはドンガラ(Skelton)だけですが、ドンガラ(構造)ができると結構「見えて」きます。

 

取り敢えず、(どのようにプログラミングしているかのプロセス紹介を含み)現況をご報告まで。

 

昨晩、なんと7時に寝てしまい、今朝は1時に起きる始末。寝床の中でつらつら考えていると、古くから欧州、中東、アジア、中南米等場所を選ばず発生した侵略、覇権、示威、暴力等の人間の愚かさが思い起こされるも、宇宙の広大さを鑑みればそれは卑小さにしか過ぎず、まるでその様は「昔作ったBCCSkeltonのLifeGameのようだ」と感慨を抱きました。

ん?

それはネタとして使えるかも?と考え始めました。

 

末尾にあるのは、定年退職した直後にC++とBCCSkeltonの復習で書いた「ライフゲーム」プログラムで、

昔からあるライフゲームのアルゴリズムをクラス(注)でまとめようとしたものです。現在見直すと羞恥に堪えませんがお許しを。

注:このクラスは「世界」を表現していて、「生命体」を表現していないことに注意。

 

このライフゲームは、「世界(縦、横二次元の領域)」にランダムで1/3程度の「生命体(Cells)」を発生させ、集落(による共生)ができないと次世代生命が生まれなくなったり、死滅してしまいますが、一定数いれば繁殖し、過密状態になるとまた死滅するというものです。

 

このライフゲームが単純である理由は、

(1)種族が無い単一生命体(性別のない単一生殖)

(2)複数いると共助、共生する(というお気楽さ)

なのでしょうか?最初に作った時は世代交代で集落が変化してゆく様(それなりに)面白く見えますが、何度か見ると飽きてきますね。

 

では、どうすればより「人間に近いライフゲーム」が作れるのか?これは上記単純である理由を逆に降ればよいでしょう。

(1)クラスを「生命体ベースで作る。」

(2)生命体は「寿命」、「性別」、「種族」の属性を持ち、メソッドに「有効 vs. 敵対」判断と「(有効なら)共生」「(敵対なら)攻撃」を加えてやればよいのではないか?

(3)世界(閉鎖領域)をクラスで表現し、複数の種族(同一IDの生命体)を発生させ、単独別離の状態では従来のライフゲーム類似のルールで発展、衰退するが、発展の過程で多種族に遭遇すると攻撃(より複雑化するなら「避難」も)して自種族を存続させるようにすればよいのではないか?

 

と、ここまで考えて

「それって、うちらの世界、まんまじゃん!」

ということに気が付きました。

まぁ、気長にアイデアを熟成させましょうか?_

 

【LifeClass.h】

/*
    LifeClass.h
*/
// 定数定義


#define    MAX_WIDTH    50        // 世界の最大幅
#define    MAX_HEIGHT    50        // 世界の最大高さ
#define    INTERVAL    100        // Generation(世代)表示の長さ
#define    ALIVE        1
#define    DEAD        0
#define    TRUE        1
#define    FALSE        0

// Lifeクラス定義
class Life {
private:
    int        Width;
    int        Height;
    int*    Cells;
    int*    NewGen;
public:
    // サイズ無指定コンストラクター
    Life() {
        // 乱数の初期化
        srand((unsigned int)time(NULL));
        // 世界と次世代
        Cells = 0;
        NewGen = 0;
        // 世界の規模
        Width = rand() % (MAX_WIDTH - 2) + 3;    // Min 3が必要
        Height = rand() % (MAX_HEIGHT - 2) + 3;    // Min 3が必要
        // 生命体(Cells)の初期化
        if(!Cells)    Cells = new int[Width * Height];
        if(!NewGen)    NewGen = new int[Width * Height];
        // 世界の初期化(誕生する生命体数は1/3)
        for(int y = 0; y < Height; y++) {
            for(int x = 0; x < Width; x++) {
                if(rand() % 3 == 0)    // Cellを増やす場合、2(50%) → 3(66%)にする
                    Cells[x + Width * y] = ALIVE;
                else
                    Cells[x + Width * y] = DEAD;
            }
        }
    }
    // サイズ指定コンストラクター
    Life(int w, int h) {
        // 乱数の初期化
        srand((unsigned int)time(NULL));
        // 世界と次世代
        Cells = 0;
        NewGen = 0;
        // 世界の規模
        w = (w < 3 ? 3 : w);                // 幅はMin 3
        h = (h < 3 ? 3 : h);                // 高さもMin 3
        Width = (w > MAX_WIDTH ? MAX_WIDTH : w);    // 幅はMAX_WIDTH限度
        Height = (h > MAX_HEIGHT ? MAX_HEIGHT : h);    // 高さはMAX_HEIGHT限度
        // 生命体(Cells)の初期化
        if(!Cells)    Cells = new int[Width * Height];
        if(!NewGen)    NewGen = new int[Width * Height];
        // 世界の初期化(誕生する生命体数は1/3)
        for(int y = 0; y < Height; y++) {
            for(int x = 0; x < Width; x++) {
                if(rand() % 3  == 0)    // Cellを増やす場合、2(50%) → 3(66%)にする
                    Cells[x + Width * y] = ALIVE;
                else
                    Cells[x + Width * y] = DEAD;
            }
        }
    }
    // デストラクター
    ~Life() {
        delete [] Cells;
        delete [] NewGen;
    }
    // 次世代(NewGen)の世界の評価
    void Eval() {
        // 上下左右の状況確認
        for(int y = 0; y < Height; y++) {        // 高さ
            for(int x = 0; x < Width; x++) {    // 幅
                int count = 0;    // 周囲の生存(ALIVE)生命体(Cell)数
                if(x == 0) {                                            // 左は無い
                    if(y == 0) {                                            // 上は無い
                        if(Cells[(x + 1) + Width * y] == ALIVE)                    // 右
                            count++;
                        if(Cells[(x + 1) + Width * (y + 1)] == ALIVE)            // 右下
                            count++;
                        if(Cells[x + Width * (y + 1)] == ALIVE)                    // 下
                            count++;
                    }
                    else {
                        if(y == Height -1) {                                // 下は無い
                            if(Cells[x + Width * (y - 1)] == ALIVE)                // 上
                                count++;
                            if(Cells[(x + 1) + Width * (y - 1)] == ALIVE)        // 右上
                                count++;
                            if(Cells[(x + 1) + Width * y] == ALIVE)                // 右
                                count++;
                        }
                        else {    // 上下あり
                            if(Cells[x + Width * (y - 1)] == ALIVE)                // 上
                                count++;
                            if(Cells[(x + 1) + Width * (y - 1)] == ALIVE)        // 右上
                                count++;
                            if(Cells[(x + 1) + Width * y] == ALIVE)                // 右
                                count++;
                            if(Cells[(x + 1) + Width * (y + 1)] == ALIVE)        // 右下
                                count++;
                            if(Cells[x + Width * (y + 1)] == ALIVE)                // 下
                                count++;
                        }
                    }
                }
                else {
                    if(x == Width - 1) {                                // 右は無い
                        if(y == 0) {                                        // 上は無い
                            if(Cells[x + Width * (y + 1)] == ALIVE)                // 下
                                count++;
                            if(Cells[(x - 1) + Width * (y + 1)] == ALIVE)        // 左下
                                count++;
                            if(Cells[(x - 1) + Width * y] == ALIVE)                // 左
                                count++;
                        }
                        else {
                            if(y == Height -1) {                            // 下は無い
                                if(Cells[(x - 1) + Width * y] == ALIVE)            // 左
                                    count++;
                                if(Cells[(x - 1) + Width * (y - 1)] == ALIVE)    // 左上
                                    count++;
                                if(Cells[x + Width * (y - 1)] == ALIVE)            // 上
                                    count++;
                            }
                            else {                                            // 上下あり
                                if(Cells[x + Width * (y + 1)] == ALIVE)            // 下
                                    count++;
                                if(Cells[(x - 1) + Width * (y + 1)] == ALIVE)    // 左下
                                    count++;
                                if(Cells[(x - 1) + Width * y] == ALIVE)            // 左
                                    count++;
                                if(Cells[(x - 1) + Width * (y - 1)] == ALIVE)    // 左上
                                    count++;
                                if(Cells[x + Width * (y - 1)] == ALIVE)            // 上
                                    count++;
                            }
                        }
                    }
                    else {                                                // 左右あり
                        if(y == 0) {                                        // 上は無い
                            if(Cells[(x + 1) + Width * y] == ALIVE)                // 右
                                count++;
                            if(Cells[(x + 1) + Width * (y + 1)] == ALIVE)        // 右下
                                count++;
                            if(Cells[x + Width * (y + 1)] == ALIVE)                // 下
                                count++;
                            if(Cells[(x - 1) + Width * (y + 1)] == ALIVE)        // 左下
                                count++;
                            if(Cells[(x - 1) + Width * y] == ALIVE)                // 左
                                count++;
                        }
                        else {
                            if(y == Height - 1) {                            // 下は無い
                                if(Cells[(x - 1) + Width * y] == ALIVE)            // 左
                                    count++;
                                if(Cells[(x - 1) + Width * (y - 1)] == ALIVE)    // 左上
                                    count++;
                                if(Cells[x + Width * (y - 1)] == ALIVE)            // 上
                                    count++;
                                if(Cells[(x + 1) + Width * (y - 1)] == ALIVE)    // 右上
                                    count++;
                                if(Cells[(x + 1) + Width * y] == ALIVE)            // 右
                                    count++;
                            }
                            else {                                            // 上下あり
                                if(Cells[(x - 1) + Width * y] == ALIVE)            // 左
                                    count++;
                                if(Cells[(x - 1) + Width * (y - 1)] == ALIVE)    // 左上
                                    count++;
                                if(Cells[x + Width * (y - 1)] == ALIVE)            // 上
                                    count++;
                                if(Cells[(x + 1) + Width * (y - 1)] == ALIVE)    // 右上
                                    count++;
                                if(Cells[(x + 1) + Width * y] == ALIVE)            // 右
                                    count++;
                                if(Cells[(x + 1) + Width * (y + 1)] == ALIVE)    // 右下
                                    count++;
                                if(Cells[x + Width * (y + 1)] == ALIVE)            // 下
                                    count++;
                                if(Cells[(x - 1) + Width * (y + 1)] == ALIVE)    // 左下
                                    count++;
                            }
                        }
                    }
                }
                switch(count) {
                    case 0:    // 周囲の生命体数が1以下だと死滅
                    case 1: NewGen[x + Width * y] = DEAD;
                            break;
                    case 2:    // 周囲の生命体数が2 - 3の場合は現状維持
                            NewGen[x + Width * y] = Cells[x + Width * y];
                            break;
                    case 3:    // 周囲の生命体数が3の場合は、生命体が誕生する
                            NewGen[x + Width * y] = ALIVE;
                            break;
                    case 4:    // 周囲の生命体数が4以上だと過密で死滅
                    case 5:
                    case 6:
                    case 7:
                    case 8: NewGen[x + Width * y] = DEAD;
                            break;
                }
            }
        }
    }
    void Transition() {
        for(int y = 0; y < Height; y++) {        // 高さ
            for(int x = 0; x < Width; x++) {    // 幅
                Cells[x + Width * y] = NewGen[x + Width * y];
            }
        }
    }
    int GetW() {return Width;}
    int GetH() {return Height;}
    int* GetCells() {return Cells;}
};
 

最近はこれを使ってC#のプログラミングばっかり行っている、BCCSkeltonで書かれたMSCompAss(Microsoft Compiler Assistant

(懐かしい Ver 1.0の頃)

 

ですが、Ver 1.5でDLLを(取り敢えず16個迄)複数参照できるようにしましたが、

(Ver 1.5のダイアログ)

 

不図オプションファイル(MSCompAssが出力する"*.opt"ファイル)をのぞいてみたら、一旦は参照したがその後削除したDLLへのファイルパスが残っていることに気が付きました。

 

ということで、今日はそのメンテを兼ねての修正を行いました。

 

【MSCompAss.h】

オプションダイアログクラス宣言のメンバー変数に関して、C++11 以降は単なる変数宣言でゼロ初期化してくれる、ということですが、オプションダイアログのメンバー変数を明示的にゼロ初期化するために次の通りにしています。

<従来>

///////////////////////////////////////////
// CDLGクラスからOPTIONDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class OPTIONDLG : public CDLG {
public:
    char m_RscFile[MAX_PATH];            //メンバー変数(オプションファイル名)
    char m_IconFile[MAX_PATH];            //メンバー変数(アイコンファイル名)
    char m_RefFile[DLLMAX][MAX_PATH];    //メンバー変数(参照DLLファイル名)
    char m_Others[MAX_PATH];            //その他オプション文字列記録用
    bool OnInit(WPARAM, LPARAM);
    bool OnRes(WPARAM);
    bool OnRscFile();
    bool OnIconFile();
    bool OnRefFile();
    bool OnDelFile();
    bool OnIdok();
    bool OnCancel();
    //ユーザー定義関数
    void SetHScroll();
};
 

<修正後>

///////////////////////////////////////////
// CDLGクラスからOPTIONDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class OPTIONDLG : public CDLG {
public:
    char m_RscFile[MAX_PATH] = {0};            //メンバー変数(オプションファイル名)
    char m_IconFile[MAX_PATH] = {0};        //メンバー変数(アイコンファイル名)
    char m_RefFile[DLLMAX][MAX_PATH] = {0};    //メンバー変数(参照DLLファイル名)
    char m_Others[MAX_PATH] = {0};            //その他オプション文字列記録用
    bool OnInit(WPARAM, LPARAM);
    bool OnRes(WPARAM);
    bool OnRscFile();
    bool OnIconFile();
    bool OnRefFile();
    bool OnDelFile();
    bool OnIdok();
    bool OnCancel();
    //ユーザー定義関数
    void SetHScroll();
};
 

【MSCompAssProc.h】

Ver 1.5まではオプションダイアログの元データ(メンバー変数のoptiondlg.m_RefFile[i])にデータが入っているか否かをチェックし、あればオプションファイルに書き込みますが、「無ければ、(前に設定していた参照ファイルのデータを消去して)掃除する」ことはしていませんでした。

<従来>

void CMyWnd::WriteOpt() {

    char fn[MAX_PATH];
    lstrcpy(fn, g_FileName.ToChar());
    switch(g_FileType) {
    case 1:    //C#
        lstrcpy(strstr(fn, ".cs"), ".opt");
        break;
    case 2:    //VB
        lstrcpy(strstr(fn, ".vb"), ".opt");
        break;
    default:
        return;
    }
    CINI optout(fn);
    optout.WriteInt("Compile Option", "Target", g_Target);
    optout.WriteInt("Compile Option", "Resource", g_Resource);
    optout.WriteStr("Compile Option", "RscFile", optiondlg.m_RscFile); 
    optout.WriteStr("Compile Option", "IconFile", optiondlg.m_IconFile); 
    optout.WriteInt("Compile Option", "DbgOpt", g_DbgOpt);
    optout.WriteInt("Compile Option", "WarnErr", g_WarnErr);
    if(*optiondlg.m_RefFile[0]) {    //解説:最初の参照DLLファイルパスデータがあるか否か
        optout.WriteStr("Compile Option", "RefFile", optiondlg.m_RefFile[0]);

        //解説:従来との互換性の為に最初()optiondlg.m_RefFile[0]は"RefFile"と書く
        for(int i = 1; *optiondlg.m_RefFile[i]; i++) {    //解説:複数DLL参照への拡張後"RefFile01(02 ... 15)"と書く
            char str[10];
            wsprintf(str, "RefFile%02d", i);
            optout.WriteStr("Compile Option", str, optiondlg.m_RefFile[i]);
        }
    }

    //解説:データが無かった場合(*optiondlg.m_RefFile[i] == 0)何もしていないので、ゴミが残存する
    optout.WriteStr("Compile Option", "Others", optiondlg.m_Others);
}

<修正後>

void CMyWnd::WriteOpt() {

    char fn[MAX_PATH];
    lstrcpy(fn, g_FileName.ToChar());
    switch(g_FileType) {
    case 1:    //C#
        lstrcpy(strstr(fn, ".cs"), ".opt");
        break;
    case 2:    //VB
        lstrcpy(strstr(fn, ".vb"), ".opt");
        break;
    default:
        return;
    }
    CINI optout(fn);
    optout.WriteInt("Compile Option", "Target", g_Target);
    optout.WriteInt("Compile Option", "Resource", g_Resource);
    optout.WriteStr("Compile Option", "RscFile", optiondlg.m_RscFile); 
    optout.WriteStr("Compile Option", "IconFile", optiondlg.m_IconFile); 
    optout.WriteInt("Compile Option", "DbgOpt", g_DbgOpt);
    optout.WriteInt("Compile Option", "WarnErr", g_WarnErr);
    for(int i = 0; i < DLLMAX; i++) {    //解説:委細構わず総ての参照DLLデータをチェックします
        char str[10];
        if(i == 0)
    //解説:参照DLLデータの最初のみ互換性の為に番号をつけていません。
            lstrcpy(str, "RefFile");
        else       
     //解説:複数DLLデータの参照の為に、二番目以降は末尾に番号を振っています。
            wsprintf(str, "RefFile%02d", i);
        if(*optiondlg.m_RefFile[i])
    //解説:データがあれば書き込みます。
            optout.WriteStr("Compile Option", str, optiondlg.m_RefFile[i]);
        else                                
    //解説:データが無ければ削除(NULLで書き込む)します。
            optout.WriteStr("Compile Option", str, NULL);
    }

    optout.WriteStr("Compile Option", "Others", optiondlg.m_Others);
}

 

動作確認を行い、本日新しい版woVectorにアップしておきました。(上がるのは1W後くらいか?)なお、過去のoptファイルでも再度オプションを確認してからコンパイルすれば、正しい形で書き直されますのでご安心を。

 

実は現在神さんが横浜で入院していて、自宅で一人暮らしをしています。

昨日の猛暑で、5時になっても水シャワーを浴びて一杯始めようかと思っていた所、

ピンポーン

と来客です。「って、なんだよ、今時分?」と思ってインターフォーンに出ると、若い女性の声で「あのー、すみません。」

 

又なんかの勧誘?と身構えて話を聞くと、どうも中国の方のようで(たどたどしい日本語で)

私は隣人です。あなたにプレゼント。

というので、

ひぇ~、(女房の留守に)やばくない?

と思い、ちょっとキツメに(更に英語で)不要の旨、伝えると、英語はよくわからないようで、日本語でも伝え、切りました。

 

体をふいて、自室にかえると窓から隣のアパートに帰る女性の姿が。彼女も何故か、振り返り目が合い、戻ってきます。

「しょーがないなー。」

と玄関まで下りて話します。すると....

 

実は一週間前位にウーバーのお兄さんが1Fに入ってきたので「うちじゃねーよ」と追い返した事件があり、どうも隣だったようですが、彼女の話を総合するとどうもその依頼主である彼女が、謝罪に手土産(大きな柑橘類、ザボン?)を持って見えた、ということのようです。(なので「私は隣人です。あなたに(ゴメンナサイの)プレゼント。」なのか...

 

「今は一人しかいないので、僕がもらっても食べきれないから、お友達と食べて。」と伝えます。よくよく見ると(今どき珍しい)黒髪の、目鼻立ちの整ったお嬢さんで、「そういわないで受け取ってください」的オーラに包まれていますが、矢張り食べ物を無駄にすることが出来ない世代なので、

分かりましたので、気にしないで。私は中国語(“中文” Zhōngwén)は、小姐、没有、対不起くらいしか知らないけど、お気持ちはいただいたので謝謝。

等と、訳の分からないことを言ってご理解していただけました。

 

で、

 

何が言いたいかというと、中国から勉学の為に来られた若いお嬢さんが(それも私自身が若い時に経験したことがある、言葉が通じない外国生活の苦労の中で)自分が描けたかもしれない迷惑(それも、たかがウーバーの誤配で実害もないのに)を気にして、(たどたどしい日本語で買っただろう)手土産までもって、迷惑をかけたことに対する謝罪で直接来訪するなんて、将に日本が嘗て誇った、しかし既に日本が失った

誠実、真摯の美徳

そのものであることに心を打たれ、「あー、中国にもこのような素晴らしい方がいらっしゃるんだなぁ」という気持ちをシェアしたくて長々と書かせていただきました。また、よくよく話も聞かずに勧誘かと思い込んでとった私の塩対応に猛省、Shame on me!する為にも書かせていただきました。

 

老人の繰り言、ご容赦召されよ。

 

本日のブログの追補です。

 

ChatGPTが提供してくれたMENACEのC言語プログラムが正しく動いていないようなので、ちょっと真面目に中身を整理しながら分析してみました。その結果、おかしな動作をしていた部分(盤が〇✖で満たされてもゲームを続けようとすること)は簡単に修正できましたが、毎回の勝負の結果を経験DB()に登録していないことに気が付きました。(プログラムでは、過去の手を検索して同じ手があればそれを使い、その結果勝利すれば評価を+1、敗北すればー1修正するコードしかありません。)

:↓ではメモリーパターンと言っていますが、対戦経験(盤面、手、評価-以下でいう「重み」)を記録する構造体データの集合(配列)です。

 

疑問に思ったので、

あなたのくれたMENACEのCプログラムで質問します。

コンピューターの手の決定で、304のメモリパターン(Pattern memory[PATTERN_COUNT];)をmemcmpで検索しますが、毎回の勝負の際にこのメモリーパターンに勝負のデータを登録してはいません。その結果、このプログラムでは、PCは毎回乱数による手を打つだけのように見えますが、正しいでしょうか?

と質問をすると、即座に

申し訳ありませんが、私の先ほどの回答に誤りがありました。提供したプログラムは、メモリパターンのデータを正しく更新していないため、MENACEが毎回ランダムな手を選ぶだけとなっています。すべてのゲームの結果をメモリに反映することが重要です。

正しい実装では、MENACEが手を選ぶたびに、そのボードの状態に対応するメモリパターンを探し、そのパターンに基づいて最適な手を選択します。そして、ゲームの結果に応じてメモリの重みを更新する必要があります。このようにしてMENACEは経験を積み、より良い戦略を学習していきます。

提供したプログラムは、MENACEの学習機能が欠落しているため、ランダムな手を選ぶだけとなってしまいます。MENACEの学習機能を実装するには、以下の手順が必要です:

  1. MENACEの手の選択時に、現在のボード状態に対応するメモリパターンを探します。
  2. メモリパターンが存在しない場合は、新しいパターンを作成し、メモリに追加します。
  3. パターンに基づいて最適な手を選択します。
  4. ゲームの結果に応じて、メモリパターンの重みを更新します。

このような学習機能を持つMENACEの実装には、さらなるプログラムの改善が必要です。私が提供したプログラムは、MENACEの学習機能を完全に実装していないため、ランダムな手を選ぶだけとなっています。お詫び申し上げます。

と回答が来ました。当然、

目が点!

となったので、続けて

お詫びは要りませんが、勝負の盤面の登録を追加したプログラムサンプルがありますか?またPattern構造体のmoveメンバーが何故0-8に初期化されているのかも、説明ができるのであれば説明してください。

と質問を続けると、

申し訳ありませんが、私の提供するデータベースには、MENACEの完全なプログラムサンプルが含まれていないため、勝負の盤面を登録するための具体的なプログラムサンプルを提供することはできません。MENACEの学習機能を実装するには、以下の手順を考慮する必要があります。

  1. ゲームの開始時に、空のメモリパターンデータベースを作成します。初期化時に、moveメンバーを0から8までの値で初期化することにより、それぞれのメモリパターンが井字ゲームのボード上の特定の位置を表している)ことを示します。

  2. MENACEが手を選択する際に、現在の盤面状態に対応するメモリパターンをデータベースから探します。

  3. メモリパターンが存在しない場合は、新しいパターンを作成し、メモリパターンデータベースに追加します。この新しいパターンは、現在の盤面状態と選択した手の情報を保持します。

  4. 学習のために、メモリパターンデータベースにおいて、それぞれの盤面と手の組み合わせに対する重みを設定します。この重みは、勝利時に増加させたり、敗北時に減少させたりすることによって調整します。

具体的なプログラムの実装方法は、MENACEの学習アルゴリズムに依存します。メモリパターンの更新と重みの調整は、ゲームの結果に基づいて行われる必要があります。

また、Pattern構造体のmoveメンバーが0から8に初期化される理由は、井字ゲームのボード上の特定の位置(0から8までのインデックス)を表すためです。MENACEがメモリパターンデータベース内で適切なパターンを探すために、それぞれのメモリパターンには対応する手の位置を指定する必要があります。

:説明はないが、おそらくコンピューターが打つ手(盤面のどの位置に打ったのか)と考えられるが、毎回の勝負で最大4回手を打つのでその最終回を記録するものと考えられる。

 

ということで「誤りがあ」る「メモリパターンのデータを正しく更新していないため、MENACEが毎回ランダムな手を選ぶだけ」の「MENACEの学習機能が欠落している」コードしかなく、役に立たないことが分かりました。

 

因みに私の訊いたメモリパターンはオリジナルのC言語のプログラムでは次の構造体の配列になっています。

 

 

// メモリパターンの情報

typedef struct {

    Board board;    //解説:〇✖ゲームの盤面

    int move;         //解説:0-8の「盤の位置」で、盤面データの最終手だと考えられる-初期値0

    int weight;       //解説:評価値でプログラムでは-9999と比較される-初期値0

} Pattern;

// メモリパターン

Pattern memory[PATTERN_COUNT];    //PATTERN_COUNT(304)の配列

 

なお、メモリーパターンの検索は「0 - 8 の char 配列の、memcmp()関数を使った単純比較」しか行っていませんが、

 

// MENACEの手を選ぶ

    int bestMove = -1;

    int bestWeight = -9999;

    for (int i = 0; i < PATTERN_COUNT; i++) {    //解説:memoryというPattern構造体配列総てをリニアサーチ

        if(memcmp(&board, &memory[i].board, sizeof(Board)) == 0) {    //解説:memcmp関数での比較

            if(memory[i].weight > bestWeight) {    //解説:暫定最大評価変数と検索データの評価を比較

                bestWeight = memory[i].weight;    //解説:より高い評価の手が見つかれば、暫定最大評価を変更し、

                bestMove = memory[i].move;    //解説:その高い評価の手を記録する

            }

        }

    }

井桁状の盤面は90度づつ回転したもの(rotate)、上下、左右で反転したもの(mirror)でプログラム上は「等価」になるので、それの処理プロセス(BCCSkeltonのMENACE開発日記参照-この回)も欠落しています。

 

結果として、ChatGPTを敬愛していた余り、こういう結果になりました。(考えてみれば、MENACEがこんなに簡単にできるわけがないんだよね。)

ということで、

ということで、現在ChatGPTがくれたC言語版のMENACEをもとにC++へ移植し、それを更にC#に移植して最後ウィンドウ(Form)化してみようかな

とは書きましたが、アルゴリズムからやり直すなら既にBCCSkeltonでやったので表題の気持ち(See the title!)になっています。

 

CardboardBoxを終了し、次のネタを考えていたら、前にBCCSkeltonで作ったMENACEを思い出しました。

この時のブログで書かれているように、これは確か1986年のASCII(ふっ、旧い!)に印刷されたC言語のプログラムが載っており、それを37年前にBDS-Cコンパイラー用に修正し、更に20年前以上にBCCSkeltonにウィンドウプログラムとして移植しました。(しかし、2017年に経験した新PCデータ移行の事故の際に永久に失われてしまい、それからreal thingを心に秘めておりました。)

 

そしてこのブログを始めてから、ネタとしてMENACEを取り上げようと思い、当時のC言語プログラムをWEBで一生懸命探したのですが、とうとう発見できませんでした。(

注:わが敬愛するCharGPTも、この問題に対する2023年4月19日の回答は以下の通りでした。

「ASCIIの1986年6月号に掲載された『マッチ箱で学ぶ“マシン・ラーニング” MENACE』という記事をお探しですね。インターネット上で入手可能な情報を調べましたが、この記事のオンライン版は見つかりませんでした。しかし、この記事はASCIIのバックナンバーとして、一部の大学図書館や専門図書館などで入手可能な場合がありますので、そちらで検索してみてください。また、古書店などでも入手可能な場合があります。」
 

そしてBCCSkeltonで作ったMENACEでは、アルゴリズムをもとに一(イチ)から自作したのですが、つい最近ChatGPTに対して同じ内容のつもりで「MENACEのプログラムを知らないか?」という聞き方をしたところ、なんと、(私には当時のプログラムに酷似していると考えられる)サンプルを書いてくれました!

 

ということで、早速そのコードをECC(Embarcadero C++ Compiler-"*.c"ファイルのコンパイルは面倒なので、"*.cpp"のC++プログラム)用に若干手直ししてコンパイルし、一応動くのを確かめました。(尤も、未だにボードが〇✖で埋まってもまだ手を指すように求めてくるので、未だ完動してはいませんが... 。)

 

ということで、現在ChatGPTがくれたC言語版のMENACEをもとにC++へ移植し、それを更にC#に移植して最後ウィンドウ(Form)化してみようかな、というアイデアで色々と試行錯誤しています。

 

が、

 

最近「64bitプログラミングだから」を言い訳に、安きに流れてC#でばかりプログラミングしていたので、どうもC++の方を忘れかけてきていることに危機感を感じました。また、「似ているだけに引っ掛かりやすい、C++とC#の相違による落とし穴」に嵌り、C#できれいにコンパイルしても、実行すると

 

「オブジェクト参照がオブジェクトインスタンスに設定されていません」というSystemNullReferenceExceptionエラー

 

が発生してしまいます。ctor()()なる不可思議なメソッドとMain()がエラー発生場所のようです。

注:コンストラクターなんですね。

 

結局、原因は私の(C++の知識が邪魔をして)C#の「オブジェクト配列の宣言とインスタンス生成」に関わる理解が十分ではなかったことで、「配列を宣言してインスタンスを生成させたつもりで、そのインスタンスが生成されておらず、null参照をしたこと」が問題でした。

 

C++プログラマーの方は矢張り陥りやすい罠だと思いますので、サンプルを作成して↓に示します。(C++、C#のコード共に実際に動きます。)

 

【C#とC++の相違-サンプル】

/////////////////////
// TestArrayCPP.cpp
// 配列の生成
/////////////////////
/*
#include    <stdio.h>
#include    <conio.h>    //getch()使用の為
#include    <iostream>    //cout、cin使用の為

using namespace std;    //std::cout、std::cin使用の為

class Sample
{
public:
    int* a;
    char** b;

    Sample()    //コンストラクターで配列のインスタンスを生成
    {
        a = new int[5] {1, 2, 3, 4, 5};
        b = new char*[3];
        b[0] = "ABC";
        b[1] = "DEF";
        b[2] = "GHI";
        //新しいコンパイラーでは↓の同時初期化も可能
        //b = new char*[3] {"ABC", "DEF", "GHI"};
    }

    ~Sample()    //デストラクターで配列のインスタンスを開放
    {
        delete [] a;                //int配列aの開放
        for(int i = 0; i < 3; i++)    //文字列配列bの各要素のさす文字列を開放
            delete b[i];
        delete [] b;                //文字列配列bの開放
        }
    }
;

int main(int argc, char** argv)
{
    //クラスオブジェクト単体の場合
    Sample sample;
    for(int i = 0; i < 5; i++)
        cout << sample.a[i] << " ";
    cout << "\r\n";
    for(int i = 0; i < 3; i++)
        if(i < 2)
            cout << sample.b[i] << ", ";
        else
            cout << sample.b[i];
    cout << "\r\n";
    //クラスオブジェクト配列の場合
    Sample *sampleArr;
    sampleArr = new Sample[5];
    for(int j = 0; j < 5; j++)
    {
        for(int i = 0; i < 5; i++)
            cout << sampleArr[j].a[i] << " ";
        cout << "\r\n";
        for(int i = 0; i < 3; i++)
            if(i < 2)
                cout << sampleArr[j].b[i] << ", ";
            else
                cout << sampleArr[j].b[i];
        cout << "\r\n";
    }
    getch();    //コンソールウィンドウを直ぐに閉めない為
    return 0L;    //関数は値を返さなければならない。
}
*/
/////////////////
// TestArray.cs
// 配列の生成
/////////////////

using System;    //解説:"#include"の対象であるヘッダーファイルを体系的にし、数も制限されてすっきりした。

namespace TestArray    //解説基本的にアッセンブリー(実行ファイル名)の名前空間を使う。なお、ECCでは
//名前空間内にエントリ^ポイント(main関数)を入れると「ソースファイルが見当たらない」エラーになる。

{
    public class Sample        //C++ではclassキーワードにアクセス指定子をつけない。
    {
        public int[] a;        //C++では配列を宣言する場合、配列へのポインターを宣言する。
        public string[] b;    //C++では文字列配列を宣言する場合、char配列の配列へのポインターを宣言する。

        public Sample()        //C++ではコンストラクターにアクセス指定子をつけない。
        {
            a = new int[5] {1, 2, 3, 4, 5};
            b = new string[3] {"ABC", "DEF", "GHI"};
        }
    }    //C++ではクラス定義の最後に";"が必要。
    //デストラクターで明示的にメモリーを開放しなくても、C#はGarbage Collectorがやってくれる。

    public class App        //C++ではclassキーワードにアクセス指定子をつけない
    {
        static public void Main()
        {
            //クラスオブジェクト単体の場合
            Sample sample = new Sample();    //C++では宣言しただけでインスタンスが生成される。("=new..."は不要)
            for(int i = 0; i < sample.a.Length; i++)    //C++では配列にLengthメンバーなんてない。
                Console.Write(sample.a[i].ToString() + " ");
            Console.Write("\r\n");
            for(int i = 0; i < sample.b.Length; i++)
                if(i < sample.b.Length - 1)
                    Console.Write(sample.b[i] + ", ");
                else
                    Console.Write(sample.b[i]);
            Console.Write("\r\n");
            //クラスオブジェクト配列の場合
            Sample[] sampleArr = new Sample[5];    //C++ではポインター宣言と共にインスタンスが生成される。(個別のSampleに関わるnewは不要)
            for(int j = 0; j < 5; j++)
            {
                //解説:↓の行がないと私が嵌った「オブジェクト参照がオブジェクトインスタンスに設定されていません」というSystemNullReferenceExceptionエラーとなる。(sampleArrはnullの配列を指しているだけ。)
                //
sampleArr[j] = new Sample();    //まずSampleのインスタンスを個別に生成してやる必要がある。(超面倒!
                for(int i = 0; i < sampleArr[j].a.Length; i++)
                    Console.Write(sampleArr[j].a[i].ToString() + " ");
                Console.Write("\r\n");
                for(int i = 0; i < sampleArr[j].b.Length; i++)
                    if(i < sampleArr[j].b.Length - 1)
                        Console.Write(sampleArr[j].b[i] + ", ");
                    else
                        Console.Write(sampleArr[j].b[i]);
                Console.Write("\r\n");
            }
            Console.Read();    //C++のgetch()と同じ目的。
        }
    }
}

 

要すれば、C#に腹を立てて読者と腹立ちを分かち合いたかっただけです。悪しからず。

 

前回のCardboardBoxでFormから派生させたFileListダイアログ(csListFile.dll)をShowDialogで表示させる際に、後処理としてDispose()が必要(注)、としつこく書きました。というのは、私、浅学にしてそれを不知であったからです。

注:「ダイアログ ボックスとして表示されるフォームは閉じるのではなく非表示になるため、フォームがアプリケーションで不要になった場合は、フォームのメソッドを呼び出す Dispose 必要があります。」(Microsoft Docs)

 

そしてしたり顔でこんなことを書きました。

/* 解説:FileList.Dispose()について

前回のコードを見ていただければわかりますが、このcsFileList.csはエントリーポイントをコメントアウトしてダイナミックライブラリーにしてあります。(コメントアウトしているところを復活させるとスタンドアローンプログラムになりますが。)

従って、CardboardBoxプログラムに"using csFileList;"とし、csFileList,dllを参照してこのライブラリーが使えるようになります。

実際に使う場合は、"Show()"メソッドで呼び出すモードレスダイアログではメッセージボックス表示中にプログラムが進行してしまい塩梅が悪いので、"ShowDialog()"を使ってモーダルダイアログで呼び出す必要があります。

C#でモーダルダイアログを使う場合、↑で見てきた通り、必ずユーザーがリソースを開放する必要があります。(注)その為、本csFileListもユーザー定義でDispose()メソッドを規定通り追加することとなりました。

 

所が、実はFormクラスに既にDispose()は定義されていました。

ゴメンナサイッ!

 

Microsoft Docsで(呼び出し側で)ShowDialog()を使うならDisposeが必要、ということで、Dispose()の標準型を調べてFileListクラスのメソッドに追加した所、

/*  既にFormクラスで定義されているので、再定義エラー
    「
csFileList.FileList.Dispose()' は継承メンバー 'System.ComponentModel.Component.Dispose()' を隠します。
    意図的に隠す場合はキーワード new を使用してください。

    が出るので、
ダミー引数を入れ(オーバーロードさせ)てごまかしていた。*/
        //FileListクラスインスタンスのリソース開放用Dispose()メソッド(Microsoft Docs)
→不要
        public void Dispose(int i = 0)
        {
            
//アンマネージドリソースの開放
            Dispose(true);
            
//ガーベージコレクション(終了抑制)
            GC.SuppressFinalize(this);
        }

現在csFileList.csからこのDisposeメソッドを削除した修正版に変更しました。

 

所が、何かまだ引っ掛かります。

前に書いたコードでDLLのフォームをダイアログで使ってDispose()していない事例があるんじゃないか?

それで自分のC#プログラミングフォールダーをリビューします。そして、Biorythmのこの回で日付け入力ダイアログを使っていたことを思い出しました。その為、

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

を二つ入れておきました。

 

更に何か気に掛かります。「そうだっ!DLLでフォームを呼び出さなくても、Form派生クラスを"ShowDialog"で呼び出したならば、必ずDisposeしなければならないんだっ!」ということで、

C#を学習した時から作成したプログラムを総てリビュー(エディターで開いて"ShowDialog"を検索して廃棄処理をしているか否か確認する)してみました。

 

その結果、

 

Replacer.cs

ResReader.cs

FormMDISkelton.cs

FormSDISkelton.cs

の4ファイルで、バージョン表示ダイアログの廃棄処理がなされていませんでしたので追加しました。

    //「バージョン」処理
    private void OnVersion_Click(object sender, EventArgs e)
    {
        VersionDlg verDlg = new VersionDlg(this.Icon);    //バージョンダイアログの表示
        verDlg.ShowDialog();
        verDlg.Dispose();
    }
 

更に、ResReader.csではImgShowとStrShowクラスのダイアログ、ResWriter.csではInputDlgとEditDlgクラスのダイアログの廃棄処理がなされていませんでしたのでそれぞれDispose()を追加しました。

 

序ですが、

 

わが敬愛するCharGPT君に、

C#のプログラミングで、Formから派生させたクラスのウィンドウをShowDialogで表示した後、Dispose()しないとどうなりますか?

と質問した答えを紹介します。(珍しく回答に時間がかかりました。特に目新しいことは言っていませんが、Microsoft Docsの翻訳ページよりもましですね。)

Dispose() メソッドは、オブジェクトが使用しているリソースを解放するために呼び出されるメソッドです。C#の Form クラスには IDisposable インターフェイスが実装されており、リソースの解放に関連するメソッドとして Dispose() メソッドが提供されています。

ShowDialog() メソッドを使用して表示されたフォームを Dispose() しない場合、次のような問題が発生する可能性があります。

  1. メモリーリーク: Form クラスは、ウィンドウやコントロールなどのネイティブリソースを使用します。Dispose() を呼び出さない場合、これらのリソースが正しく解放されず、メモリーリークが発生する可能性があります。特に長期間にわたって多数のフォームを開いて閉じた場合、メモリ使用量が増加し続ける可能性があります。

  2. リソースの正しい解放: フォームが Dispose() メソッドを呼び出すことで、リソースが正しく解放されます。たとえば、フォームがファイルハンドルを使用している場合、Dispose() を呼び出さないと、そのファイルが他のプロセスでロックされたままになる可能性があります。(解説:突っ込みですが、これも(フォーム自体のリソースではなく、フォームが使用するリソースですが、畢竟)メモリーリークの問題です。)

したがって、Form クラスを ShowDialog() メソッドで表示した後は、適切に Dispose() メソッドを呼び出すことをお勧めします。通常、フォームが閉じられるタイミング(解説:C#のShowDialogは(C++などとは異なり)戻り値が列挙型のDialogResultしかないので、処理後の値を取得するメソッドを使うことがあります。従って戻り値を取得してからDispose()してください。)Dispose() を呼び出すようにすると、リソースの解放が確実に行われます。

 

何れにしても。

 

私が悪くありましたぁっ!(浅田次郎の小説風に)

ps. 過去の回に遡って必要な"〇〇.Dispose();    //2023年06月30日追記"の追加を完了しました。

一回無駄話を挟みましたが、最後に本体のCardboardBox(段ボール箱)のコードを解説します。

 

前にも書きましたが、本体のする仕事は単に「起動時、起動後のドラッグアンドドロップを受け付け、ファイル処理は右ポップアップメニューでcsFileListに任す」だけです。しかし、プログラミングで特徴的な点として「ビットマップ画像以外を透過させる」「タスクトレイに収納する」があり、その点を解説します。

 

【CardboardBox.cs】

///////////////////
// CardboardBox.cs
///////////////////

using System;
using System.Collections;
using System.Collections.Generic;    //List使用の為(解説:本体でのデータ管理はListを使います。)
using System.Windows.Forms;
using System.Drawing;
using System.Resources;
using System.IO;                    //Fileクラス使用の為
using csFileList;                    //FileLIstクラス使用の為(解説:これが作成したcsFileList.dllの読み込みです。)

namespace CardboardBox
{
    ///////////////////////////
    //エントリーポイントクラス
    ///////////////////////////

    class MainApp
    {
        [STAThread]
        public static void Main()
        {
            //解説:起動時のコマンドラインを取得します。

            string[] cmds = System.Environment.GetCommandLineArgs();    //先頭は実行ファイル名
            if(cmds.Length > 1)
            {

                //解説:実行ファイル名を飛ばしたコマンドラインをargにコピーします。
                string[] args = new string[cmds.Length - 1];
                Array.Copy(cmds, 1, args, 0, cmds.Length - 1);
                Application.Run(new CbBox(args));    //解説:CbBoxのコンストラクターに渡します。
            }
            else
                Application.Run(new CbBox());
        }
    }

    ///////////////////////
    //メインフォームクラス
    ///////////////////////

    public partial class CbBox : Form
    {
        //ウィンドウのマウス移動フラグ
        private bool cbMovable = false;
        //thisの移動開始座標
        private Point cbPoint;
        //ウィンドウ画像(段ボール箱-1 = Empty、2 = Full)
        private Bitmap cbbmp1, cbbmp2;
        //ファイル名リスト用(解説:取得したファイルパス、名のコンテナーです。)
        List<string> file_list = new List<string>();
        //コンテキストメニュー関係
        ContextMenu contextMenu;
        MenuItem mi1, mi2, mi3;
        //タスクトレイアイコン(解説:このタスクトレイ内に置くNotifyIconオブジェクトがポイントです。)
        NotifyIcon notifyIcon;
        ContextMenuStrip NIMenuStrip;    //タスクトレイアイコンのポップアップメニュー

        //コンストラクタ(解説:コマンドライン引数がない場合はnull初期化が行われます。)
        public CbBox(string[] args = null)
        {
            //このプログラムのアッセンブリーを取得する
            System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
            //プログラムアイコンをフォームにつける
            this.Icon = Icon.ExtractAssociatedIcon(asm.Location);
            //ResourceManagerインスタンスの作成
            ResourceManager rm = new ResourceManager("CardboardBox", asm);
            //data name="CBBox1"(空)、"CBBox2"(詰まった)というBitmapを読み込む
            cbbmp1 = (Bitmap)rm.GetObject("CBBox1");
            cbbmp2 = (Bitmap)rm.GetObject("CBBox2");
            cbbmp1.MakeTransparent(Color.Black);    //解説:ビットマップ背景色(黒)を透過させます。
            cbbmp2.MakeTransparent(Color.Black);    //解説:同上。
            //ウィンドウ位置の設定
            this.StartPosition = FormStartPosition.CenterScreen;
            //ウィンドウ枠の設定
            this.FormBorderStyle = FormBorderStyle.None;    //解説:プレーンのウィンドウです。(↓参照)
            /*    Fixed3D            2    固定された 3D 境界線。
            FixedDialog            3    太い固定されたダイアログ スタイルの境界線。
            FixedSingle            1    固定された一重線の境界線。
            FixedToolWindow        5    サイズ変更できないツール ウィンドウの境界線。 ツール ウィンドウは、タスクバー、またはユーザーが Alt + Tab キーを押したときに表示されるウィンドウには表示されません。 FixedToolWindow を指定するフォームはタスクバーに表示されませんが、ShowInTaskbar プロパティが false に設定されていることを確認する必要があります。既定値は true です。
            
None                0    境界線なし
            Sizable                4    サイズ変更できる境界線。
            SizableToolWindow    6    サイズ変更できるツール ウィンドウの境界線。 ツール ウィンドウは、タスクバー、またはユーザーが Alt + Tab キーを押したときに表示されるウィンドウには表示されません。
            */

            //サイズの設定
            this.Size = new Size(128, 128);
            //常に最前面に設定(解説:最初は普通のZオーダー任せでしたが、この方が使いやすいです。)
            this.TopMost = true;
            // タスクバーに表示しない(解説:タスクトレイに入れる場合はウィンドウ下部のバーに表示させません。)
            this.ShowInTaskbar = false;
            //フォームのロードイベントメソッドを設定
            this.Load += CbBox_Load;
            //フォームのキーダウンイベントメソッドを設定(解説:キー移動用)
            this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.CbBox_KeyDown);
            //フォームのマウスイベントメソッドを設定(解説:マウス移動用)
            this.MouseDown += new MouseEventHandler(this.CbBox_MouseDown);
            this.MouseUp += new MouseEventHandler(this.CbBox_MouseUp);
            this.MouseMove += new MouseEventHandler(this.CbBox_MouseMove);
            //コンテキストメニュー関連(解説:「ファイルリストの表示」、「タスクトレイへ格納」と「終了」のみです。
            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("-");        //セパレーター
            contextMenu.MenuItems.Add(mi2);
            contextMenu.MenuItems.Add("-");        //セパレーター
            contextMenu.MenuItems.Add(mi3);
             this.ContextMenu = contextMenu;
            mi1.Click += new EventHandler(FileList);
            mi2.Click += new EventHandler(TaskTray_In);
            mi3.Click += new EventHandler(Exit);
            //NotifyIconの設定(解説:これがタスクトレイへしまうポイントになります。)
            notifyIcon = new NotifyIcon();
            notifyIcon.Icon = this.Icon;    //解説:ウィンドウのシステムアイコンを使用しました。
            notifyIcon.Visible = true;
            notifyIcon.Text = "CardboardBox - クリックして表示";    //アイコンのテキスト
            notifyIcon.Click += TaskTray_Out;    //NotifyIconのクリックイベント(解説:クリックすると段ボール箱を表示します。)
            //NotifyIconのコンテキストメニューの設定
            NIMenuStrip = new ContextMenuStrip();
            ToolStripMenuItem tsMenuItem = new ToolStripMenuItem();
            tsMenuItem.Text = "&終了";    //解説:タスクトレイアイコンの「メニュー」は終了だけにしました。
            tsMenuItem.Click += Exit;    //解説:ポップアップメニューのメソッドをそのまま使っています。
            NIMenuStrip.Items.Add(tsMenuItem);
            notifyIcon.ContextMenuStrip = NIMenuStrip;    //解説:これでタスクトレイアイコンに紐づけします。
            //Drag and Dropを受け付ける
            this.AllowDrop = true;    //解説:ウィンドウでもコントロールでもこれが第一歩です。
            //コマンドライン引数がある場合(解説:最後に起動時の引数がある場合の処理です。)
            if(args != null)
            {
                foreach(string fn in args)
                {
                    //ファイルが存在するか、フォールダーではないか、をチェックする
                    if(!File.Exists(fn))
                    {
                        MessageBox.Show("このファイルは存在しません。\r\n" + fn, "", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        continue;
                    }
                    //Listに引数を追加する
                    file_list.Add(fn);
                }
            }
        }

        //フォームのロードイベントメソッド
        private void CbBox_Load(object sender, EventArgs e)
        {
            //背景画像の設定(解説:これで背景にビットマップを貼り付けます。)
            this.BackgroundImageLayout = ImageLayout.Zoom;    //Center, None, Stretch, Tile, Zoom(解説:貼り付け方)
            if(file_list.Count > 0)
                this.BackgroundImage = cbbmp2;    //解説:「詰まった段ボール箱」ファイルがあります。
            else
                this.BackgroundImage = cbbmp1;    //解説:「空の段ボール箱」ファイイルがありません。
            //背景色を透過色に設定
            this.TransparencyKey = this.BackColor;    //解説:【ポイント】貼り付けたビットマップの背景色を透過させます。
        }

        //ファイルをドラッグされた時(解説:以下は「定番」コードです。)
        protected override void OnDragEnter(DragEventArgs e)
        {
            base.OnDragEnter(e);
            if(e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;    //解説:マウスアイコンは「ドラッグ アンド ドロップ操作の Copy プラス記号、ドラッグ アンド ドロップ操作の Move 矢印記号、ドラッグ アンド ドロップ操作の場合は赤い円、線を通した赤い円を None 表示できます。」(Microsoft Docs)
            else
                e.Effect = DragDropEffects.None;
        }

        //ファイルをドロップされたとき
        protected override void OnDragDrop(DragEventArgs e)
        {
            base.OnDragDrop(e);
            //ドロップされたファイルパスを取得
            string[] files =  (string[])e.Data.GetData(DataFormats.FileDrop, false);
            //ドロップされたデータを追加する
            foreach(string fn in files)
            {
                //ファイルが存在するか、フォールダーではないか、をチェックする
                if(!File.Exists(fn))
                {
                    MessageBox.Show("このファイルは存在しません。\r\n" + fn, "", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    continue;
                }
                //Listに引数を追加する
                file_list.Add(fn);
            }
            this.BackgroundImage = cbbmp2;
        }

        //キーダウンイベントメソッド
        private void CbBox_KeyDown(object sender,KeyEventArgs e)
        {
            //フォームを移動させるピクセル数
            int movePixel = 2;
            if(e.Shift)    //解説:シフトキーを押しながらだと移動が速いです。
                movePixel *= 8;
            switch(e.KeyCode)
            {
                //上移動
                case Keys.Up:
                    this.Top -= movePixel;
                    break;
                //下移動
                case Keys.Down:
                    this.Top += movePixel;
                    break;
                //左移動
                case Keys.Left:
                    this.Left -= movePixel;
                    break;
                //右移動
                case Keys.Right:
                    this.Left += movePixel;
                    break;
                //シフトキー(終了させない)
                case Keys.ShiftKey:
                    break;
                //その他のキー(解説:シフトキーや矢印キー以外だと終了します。)
                default:
                    Close();
                    break;
            }
        }

        //マウスダウンイベントメソッド
        private void CbBox_MouseDown(object sender, MouseEventArgs e)
        {
            if(!cbMovable && e.Button == MouseButtons.Left)
            {
                //フラグを真にする(解説:ボタンを押し下げている間、ドラッグフラグは真となります。)
                cbMovable = true;
                //現在のマウス座標取得(解説:移動開始位置を取得します。)
                cbPoint = new Point(e.X, e.Y);
            }
        }

        //マウスアップイベントメソッド
        private void CbBox_MouseUp(object sender, MouseEventArgs e)
        {
            if(cbMovable && e.Button == MouseButtons.Left)
                //フラグを戻す(解説:ドラッグを終了させます。)
                cbMovable = false;
        }

        //マウスムーヴイベントメソッド
        private void CbBox_MouseMove(object sender, MouseEventArgs e)
        {
            //マウス押下中はドラッグ可
            if (cbMovable)
            {
                //フォームを移動させる
                this.Left += e.X - cbPoint.X;    //解説:移動開始位置とのx座標の差分です。
                this.Top += e.Y - cbPoint.Y;     //解説:移動開始位置とのy座標の差分です。
            }
        }

        public void FileList(object sender, EventArgs e)
        {
            /* ShowDialog()メソッドの注釈(from Microsoft Docs)
            このメソッドを使用すると、アプリケーションにモーダルダイアログボックスを表示できます。
            このメソッドが呼び出されると、ダイアログ ボックスが閉じられるまで次のコードは実行されません。
            ダイアログボックスは、フォームのDialogResultプロパティに、DialogResultの一つを割り当てるか、
            コードでDialogResultの列挙値の1つをButtonに設定することができます。
            この値は、このメソッドによって返されます。この戻り値を使用して、ダイアログボックスで発生した
            アクションを処理する方法を決定できます。たとえば、ダイアログボックスが閉じられ、このメソッドを
            使用して値DialogResult.Cancelが返された場合、呼び出しShowDialogに続くコードが実行されないように
            することができます。
            フォームがモーダルダイアログボックスとして表示されている場合、[閉じる]ボタン(フォームの右上隅にある
            Xボタン) をクリックすると、フォームが非表示になり、DialogResultプロパティがDialogResult.Cancelに設定
            されます。非モーダルフォームとは異なり、ユーザーがダイアログボックスの閉じる(Close)フォームボタン
            をクリックしたり、プロパティの値DialogResultを設定したりしても、メソッドは.NET Frameworkによって
            呼び出されません。代わりに、フォームは非表示になり、ダイアログ ボックスの新しいインスタンスを作成
            せずに再度表示できます。
ダイアログ ボックスとして表示されるフォームは閉じるのではなく非表示になる
            ため、フォームがアプリケーションで不要になった場合は、フォームのメソッドを呼び出す Dispose 必要が
            あります。

            */
            //ファイルリストを文字列配列に変換
            string[] strArray = file_list.ToArray();
            //FileListクラスダイアログを作成、表示(上記参照-csFileList.dllから)
            FileList fl = new FileList(strArray);
            fl.ShowDialog();
            //flに残っているリストを承継してから
            file_list.Clear();    //解説:いったん現在のリストを消去します。
            file_list.AddRange(fl.GetFileList());    //解説:csFileListのリストを取得し、Listに加えます。
            if(file_list.Count > 0)
            {
                //初期化確認(処理をしたので、csFileListで初期化された筈ですが、エラーで残ったファイルもあり得るので。)
                DialogResult dr = MessageBox.Show("FileLIst処理が終了したのでファイルデータを初期化します。\r\n宜しいですか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                if(dr == DialogResult.Yes)
                {
                    file_list.Clear();
                    this.BackgroundImage = cbbmp1;    //解説:空の段ボール。
                }
                else
                    this.BackgroundImage = cbbmp2;    //解説:詰まった段ボール。
            }
            else
                this.BackgroundImage = cbbmp1;    //解説:csFileListが処理を行い、受け継ぐリストが無ければ「空」です。
            //FileListクラスダイアログインスタンスの開放(//解説:↑の為にこのメソッドを作りました。)
            fl.Dispose();
        }

        //「タスクトレイへ収納」
        public void TaskTray_In(object sender, EventArgs e)
        {
            this.Visible = false;    //実は見えなくしただけなんです。
        }

        //(タスクトレイから)「表示」
        public void TaskTray_Out(object sender, EventArgs e)
        {
            this.Visible = true;    //「段ボール箱」の再表示(「安心してください、穿いてます。」)
        }

        public void Exit(object sender, EventArgs e)
        {
            //終了処理
            DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.Yes)
                Close();
        }
    }
}

 

ということで、(今まで使っていますが、未だ不具合、改良点等は見当たりませんので)

「これにて一件落着ぅ!」

CardboardBoxの完了です。

 

(昨日まで娘と孫が遊びに来ていたのでお休みしていました。スミマセン。)

今クールのネタであるCardboardBoxがクランクアップして、現在実際に稼働しています。

 

振り返ると、「ネタがない、ネタがない」とネタ探しをしている時に、「PCの異なるフォールダーの任意のファイルをポイポイ放り込んでまとめてファイル処理するツールはない」ということに気が付いて考え始めたことがきっかけでした。

 

先ずは「(任意のファイルをポイポイ放り込む)完成形のイメージ作り」から入り、あーでもない、こーでもないとリソースを最終決定。

  

 

次に実際に「ファイル処理を行う」(従って奇を衒わない、単なる)ダイアログの「機能確認」と「表示確認」を実施。

 

この段階で「削除」「コピー」「移動」「圧縮」の各動作を確認します。そしてcsFileList.dllを使ってCardboardBox.exeを動かします。

 

実際に使ってみて、矢張り「異なるフォールダーに入った」「(当初想定していなかった)共通項のあるファイルを」Explorerで投げ込んでいって「一括処理する」のに重宝します。(例:異なるフォールダーに入っている友人との旅行写真をまとめてあげるとか、孫の成長写真を一つのアルバムにする等)

 

ただ、毎日使うものではないので「いざという時に重宝するツール」として使ってみてください。

 

さて、次は何にするかな?