前回に引き続き、最近とみに【食い物話】をアップしてますけど、単なる偶然ですので...

 

今回のテーマは、

 

海鮮餡かけ焼きそば

 

麺喰いの私が好む(町中華料理系の)焼きそば(とタイを張る汁そば)の順位は、

 

海鮮餡かけ焼きそば || 野菜タンメン

    ↑

(五目)餡かけ堅焼 ||(五目)広東麺

きそば(または長崎

皿うどん)

    ↑

ソース焼きそば   || ラーメン(志那竹大好き)

 

こんなイメージでしょうか?(勿論、本格中華や東南アジアのミーゴレン、ラクサ、ビーフン等も好物ですが除いてあります。)

 

そんな私が「日本じゃなぁ」とガッカリするのは、「餡かけ焼きそば」というと直ぐに「堅焼きそば」「揚げ焼きそば」になってしまうところです。私が好きなのは「色が独特の薄茶色で、細く、柔らかいが腰のある

 

香港麺(ホンコンミー)

 

ですが、これは結構日本では見かけません。そんな中で「あっ、あった!」と思ったのは「Maruha Nichiroの冷凍五目餡かけ焼きそば」。こいつを加熱して後、キチンと油を多めに張ったパンで表面のみフライすると、パリパリとしっとりと腰のある噛み応えが香港麺に近い満足感を与えてくれます。

 

ただ問題は、

 

「五目」と謳ってはいますが、矢張りそれをそのまま鵜呑みにするには可也キツイと感じる五目餡にあります。これではフラストレーションを感じてしまいます。

 

ということで、「無ければ足すしかないっ!」

 

普通は肉野菜炒めなどをのっけますが、今回は冷凍シーフードミクス、キャベツ、モヤシ、人参、玉葱をニンニクのみじん切り油で炒めて作った海鮮餡(海鮮八宝菜)を追加したものです。

 

 

大ぶりの海老や烏賊と浅利が良い感じで、大変美味しくいただきました。

 

前回「ユーザーが取り敢えず遊べる」ウィンドウプログラムのドンガラだけはできましたが、

 

 

初期設定が単純な乱数による分割画像の配置なので、理論上9の階乗(9!)だけある盤面の半分は解けない初期設定になることから、今後の進め方は「現在考え中」であるとお伝えしました。

 

その後、一旦↑のウィンドウプログラムを離れ、コンソールベースで8Puzzleの解法(アルゴリズム)について参考()を眺めながら学習し、「さて、どのように進むか?」と思案しました。

置換の基礎

  8パズル,15パズルの不可能な配置と判定法

  スライドパズルの数理

  幅優先探索・深さ優先探索・A*アルゴリズムの説明

 

この時思いついたのは、今を去る半世紀弱の昔、私がC言語(C++ではないですよ!)を学び始めた1986年に購入して勉強した「実戦Cプログラミング」(東京大学情報科学科助手<当時> 小野芳彦著 工学社刊 ... 当時定価は600円でしたが。)にあったC言語による8パズルの解法プログラムでした。(何故かって?それなら昔入力したのでソースデータがあるからです。→基本 Lazy なもんで...)

 

しかし、

 

ソースコードがあるといっても、当時()のプログラムは表記が現在とはやや異なり、メモリー対策からコメントも限定されているために、正直内容を理解せずに本からインプットしただけものであることから、改めてその内容を理解することから始めました。

:当時、著者の小野さんは東大のUNIXマシーンでSmall-Cを使っていたようです。私は出たての8bitパソコン、Sharp MZ-2500CP/M(Control Program for Microcomputers)を走らせ、整数しか使えないBDS-Cを使っていました。(MZ-2500には浮動小数点演算を行えるFNCコールがあったので痛痒を感じませんでしたが。)

 

そして、

 

現在

 

オリジナルのCプログラムをC#に移植するに際してこの「一見親類かと思うけど、実は素性は結構違う」プログラミング言語2者に翻弄され、途中からChat-GPT様にも相談し、しかし時々注意書きの「ChatGPT の回答は必ずしも正しいとは限りません。重要な情報は確認するようにしてください。」という事態も発生()して、

 

あーでもない、こーでもない

 

と試行錯誤を繰り返している今日この頃です。

:例えば「(Chat-GPT)私が以前述べた「C言語版は正しく動くが、C#版は末尾チェックが抜ける」という主張は 間違っていました。これは私の観察ミスでした。」等。

 

ということで、

 

一旦は「1986年のCプログラムのC#移植版を使う」という方針を投げ出そうか、とも思いましたが、ここ迄時間を使ったので、

 

虚仮の一念

:愚か者でも一途に信念を持って物事に臨めば、困難を克服し、大きな成果を上げることができるという意味の慣用句。

 

で、このプロジェクトを完成させるべく頑張ろうと思っております。従いまして、

 

Keep on staying tuned!

 

ps. 次回からは、その問題となったCプログラムについてお話ししましょうか?

 

前回の「冷やし中華」が結構受け入れられたことに気を良くして、もう一本入れます。それは...

 

カツ。(やっぱ、黄と茶でしょう。)

 

私、若い頃は結構「蕎麦屋のカツ丼」が好きで、また多くの場合当たりはずれが少ないので()よく食べました。矢張りロースがおいしいのですが、今回は喜寿ということもあり、小ぶりのフィレで...

:生涯で一度だけ、例外に遭遇しました。あれは28歳の仕事納め近くのある日、突然の仕事で車で山間部へ調査に向かいましたが、過疎部で食べるところが無く、やっと見つけたのがお蕎麦屋さん。喜び勇んで「カツ丼」を頼みましたが...この悪食で昭和の「残しちゃいけない」文化に染まった私ですが、残して箸をおきました...

 

(プログラミングと同じく)先ずは「完成形を視覚的にイメージ」します。今回はご飯少な目(100g)、冷凍ロースかつ(小さかったので2枚使っちゃいました)、他は(潔く)玉葱のみ。これでは栄養が足らないので、モヤシ、キャベツ、長ネギの入ったお味噌汁(これもベースはインスタント生味噌汁)を連れてゆきましょう。

 

(リソースが手に入ったら)次に開発の段取りと所要時間を検討し、何から着手し、何を併行してやるか、確認します。今回は味噌汁の具の野菜を軽く炒めて、薬味の長ネギ、七味唐辛子と共にお湯を入れればよい状態にして、お湯もT-falに入れてスタンバイします。次にフィレのとんかつを電子レンジで解凍し、パンに油を入れて加熱し「パリッと揚げたて感」を出します。一方、小鍋に油と玉葱を入れて矢張り軽く炒め、水、和だし、蕎麦つゆを入れて弱火で煮ます。(汁はしっかりと多目がポイントです。又煮詰まりますので薄め、を心がけます。)

 

ここら辺でT-falにスイッチを入れ、冷凍ご飯をレンジで温め、トンカツをカットして小鍋に並べ上から卵液を全体にかけて蓋をし、弱火にします。

 

先ずT-falが終了し、味噌汁をサーブします。次にご飯が温まったので箸でほぐしてカツ煮の受け入れ準備を行い、後は「こんなもんかな」という時間で蓋を開けて卵液の凝固具合を確認します。(1~2割ほど半熟っ気があるところが宜しいでしょう。)

 

そして、出来上がりが、

 

この通り。我ながらよく出来上がり、美味しくいただきました。

 

ps. 尚、前々回の「"150g x 1/2 = 75g" のステーキ」のお皿、今回のカツ丼の器は神様が買ってくれたARABIAのMOOMINシリーズで、今回のお味噌汁を入れたスヌーピーの器と併せ、「三銃士」として愛用しています。

 

私は元々「寝るのが下手」(神様のお言葉)な質(タチ)で、寝ている際に考えが浮かぶと、そのことを考え続け、アドレナリンが放出され、興奮して目がギンギンになってしまいます。昔はこのような特に「翌朝が早い」ような場合、「後、何時間しか眠れないっ!」と焦って「力いっぱい、気合全開で寝よう」としてしまい、益々眠れなくなるという悪循環を繰り返しました。(

:睡眠に良いようなことは全くしていなかった「24時間闘えますの昭和時代」ですから、仕方ないです... 睡眠時間34時間は当たり前で、よく「電車の中で5分熟睡」、なんてありましたっけ。

 

ティーンエイジャーの頃、特に高校時代(1970年初頭)は完璧に「夜型」で、当時定番の深夜番組を聞きながら勉強したりして、(まぁ、夕方少し寝るのですが)3時から寝る、何てしょっちゅうでしたが、歳を重ねると起床、就寝パターンが変わってきて「眠らないから早く寝る」という風に極端な「朝型」になりました。特に(早朝ウォーキングを始めた)560代になってからこの傾向は強まり、65歳で完全定年となってからは

 

それ、異常ですやんっ!

 

という位、早まっています。どのくらい早まっているかというと...

 

就寝-大体8時(20時)。というのも、5時から晩酌を始めるので6-7時には夕食も終わり、最近の「ドつまらないテレビ」の為に、早々にリビングから自分の部屋に引き上げるためかと思います。(8時前、なんてことも。神様に叱られます。)

起床-というか、ここ20年ほど34時間以上連続して眠った記憶があまりないです。早いと1011時に一回、ふつうは12時頃、その後1時間半~2時間等毎に目が覚め、朝の4時までぐっすり眠れるなんて病気し(て体力が衰え)た時ぐらいじゃないかな?

 

人の睡眠って、よく言われるように、REM睡眠とNon-REM睡眠の周期があり、前者は脳の活動が覚醒時に近く「休息」程度ですが、後者は生理的な代謝や修復jが行われるようです。実際、4時間ほど熟睡して夢も見なかった時は、疲労感が解消され活性感を覚えますが、何だか運動した後のような身体負荷で疲れた感じがして、体重も減少します。一方、余り眠れなかったときは、頭も身体もぼーっとした疲労感が感じられて、活性感が消失します。また、体重が増えたりします。

 

まぁ、

 

若い頃は新陳代謝が活発で身体が入替わる迄早いですが、老人はもう余り新陳代謝(細胞の入れ替わり)が無く、従って睡眠も必要がないのでしょう。

 

てか、

 

ここにある「健康な人の一晩の睡眠経過図」を見ると、(スタートを20時前後にすれば経過時間は↑通りで)

 

 

俺って、全然オッケー!

 

じゃん?!?!

 

ということで、

 

「まっ、いいか」、"Let it be!”、「メイウェンティ(没問題)」、「オットケドゥンチャイ(어떻게든차이)」、「なんくるないさー」という気持ちで(事実、現役の時と違い、完全隠居となると仕事も約束もなくななるので)、日々を安寧に過ごしてゆきましょう。

 

ps. 最初タイトルは勝手に命名したつもりでしたが、これってちゃんとした症候群になっているんですね。

 

神様の北欧・ドイツ旅行も後3日。私のチョンガー生活も後3日。

 

ということで、

 

何故か

 

「所謂一つの町中華の冷やし中華」

(長嶋茂雄様のご冥福をお祈りいたします。)

 

に制作意欲が湧いてしまいました。

 

元々私は「麺喰い」で「冷やし」系も結構作ります。昔の作品を適当に見繕うと、

 

「2019年05月22日に作った韓国風冷やし中華。何が韓国風かというと、具は冷やし中華ですが、薄めのそばつゆに日本そばとキムチで冷麺らしさを出しているからです。」

 

「2021年04月27日は日本そばをベースにした冷麺です。これが大好きです。」

 

「2021年06月22日は暑くなったので、お昼に冷やし中華。レタス、きゅうり、もやし、錦糸卵、ソーセージ2本を入れてみました。」

 

「これは綺麗ですね。2021年07月13日のお昼はソーメンベースでキムチ冷麺です。」(解説:これはイタリアンをイメージして、カッペリーニの代わりに素麺を使いました。)

 

「お昼に茹でもやし、オニオンスライス、レタス、サニーレタス、茹で豚、茹で油揚げ、キムチの載ったうどんを作ってみました。」

 

ご覧の通り、私が日本蕎麦や冷麺が好きなので、色々と作りますが結局キムチを入れたりして、

 

「町中華の冷やし中華」

 

が無いことに気が付かれるでしょう。ということで、チャレンジ!

 

「所謂町中華の『冷やし中華』を作ってみました。麺は生の細麺、具材は錦糸卵、ハム、茹でもやし、レタス+オニオンスライス、胡瓜と紅生姜です。(具材が多すぎて麺が見えませんね。。解説:レタス+オニオンスライスは余計なのですが、野菜が好きなもので...)たれはミツカンのレシピに従いましたが、私には甘すぎたので食べ始めた後、ポン酢で調整しました
自分で全部作るのは楽しいし、味は外すリスクがある反面、嵌った時には本当にうれしいものです。」

 

が、

 

普通に生めんの冷やし中華を買った方がリスク管理的には優れているでしょう。

 

に「(これについては、別途【無駄話】で顛末を書く予定です。) 」と書いたお話です。

 

8Puzzleを開発中、分割画像の初期化用「0~8までの配列に08迄の数を重複せずにランダムに代入する」必要が生じました。

 

もっとも簡単なアルゴリズムとして、配列要素08まで順に08までの乱数を代入するという方法です。しかし、これでは値の重複の可能性があるので、「配列要素08まで順に、08までの乱数を発生させ、既に代入した数と同じでないならば、代入する」というストレートな物を考えました。

 

        //初期化

        for(int i = 0; i < Num; i++)

                arr[i] = -1;    //要素数Numの配列を-10-8でなければ何でもよい)で初期化

        //乱数生成方式

        public void GenRandArr()

        {

                //乱数の初期化

                Random rand = new Random((int) DateTime.Now.Ticks & 0x0000FFFF);

                for(int i = 0; i < Num; i++)

                {

                        while(true)

                        {

                                int val = rand.Next(Num);

                                if(Array.IndexOf(arr, val) == -1)

                                {

                                        arr[i] = val;

                                        break;

                                }

                        }

                }

        }

 

しかし、これはいかにもストレートで、被らないように検索を行っているのであか抜けません。

 

ということで、「では、最初から配列要素0→8まで順に08の数を入れておき、この配列要素自体を変えた方が良いのでは?」ということで、「最初に配列要素0→8まで順に08の数を入れておき、9番目(arr[8])の要素に18番目(arr[0-7])からランダムに選択してスワップさせ、、3番目迄続ける」というアルゴリズムにしてみました。

 

 

        //初期化

        for(int i = 0; i < Num; i++)

        arr[i] = -1; //要素数Numの配列を配列番号で初期化

        //シャッフル方式

        public void ShuffleArr()

        {

                //乱数の初期化

                Random rand = new Random((int) DateTime.Now.Ticks & 0x0000FFFF);

                for(int i = Num - 1; i > 1; i--)

                {

                        int n = rand.Next(i + 1);

                        int temp = arr[i];

                        arr[i] = arr[n];

                        arr[n] = temp;

                }

        }

 

こちらの方がスマートだし、垢抜けて洒落ていませんか?

 

しかしっ

 

なーんとその実行結果は次のようになりました。

 

<要素数 9 の場合

 

<要素数 999 の場合>

 

シャッフル方式の方が大分早いと思っていたのですが、「実際にやってみなければ、結果は分からん」の典型のような話です。

 

ps. しかし、パフォーマンスは乱数の出目によって異なるようで、半分ほどは両方ともタイムは同じ、ということが分かってきましたが、一度もシャッフル方式が乱数生成方式を負かすことはありませんでした...

 

前回、「ただ画像を表示するだけのドンガラ」を作りましたが、今回はそれに最低限の機能を追加して、「取り敢えず人が遊べるだけのもの」にします。末尾に追加部分の該当部分(「8Puzzle-一人遊び迄(追加部分)」)を(一応解説:でも追加部分を示します)で示します。

 

追加部分は、

 

(1)0~8迄そろっていたPiecesのID番号配列を乱数を使ってバラバラにする処理(末尾の追加コードの「 //乱数の初期化」と「//Pieces配列をシャッフルする」参照)

「0~8迄揃えた配列を9番目から0~8(= 9 - 1)番目の配列要素をランダムに選んで9番目の要素と交換し、それを3番目まで続ける」(「シャッフル方式」)処理です。これは最初「配列要素を総て-1にし、「1~9番目まで、(既に使った0~8の数ではないことを確認の上)0~8の乱数を入れる」(「乱数生成方式」)にしたのですが、あまりスマートではないな、ということで変更しました。(これについては、別途【無駄話】で顛末を書く予定です。)

 

(2)ユーザーの遊び方に間違いがないように、ボタンの有効無効をゲームの進行状態に合わせてコントロールしました。(「//ボタン状態変更」)具体的には、最初は「画像選択」しかできないようにし、画像を選択すると「開始」ボタンが有効になります。(画像の変更を許しているので、画像選択は有効なままです。)この段階でも上下左右ボタンは無効状態ですが、「開始」を押すと「画像選択」と「開始」は無効となり、上下左右ボタンだけが有効になります。(末尾画像参照)

 

(3)「開始」ボタンと上下左右ボタンの処理は、InitCanvasで作成した分割画像とその並びを表すPieces配列を使って、①配列の要素を交換し、②ピクチャーボックスをクリアして分割画像をPieces配列に基づいて表示しなおす、という単純なものです。

 

(4)分割画像の表示(ShowPieces())は特に難しい話ではなく、また完了判定処理(IsDone())も単に0~8の順になっているかチェックするだけです。

 

さて、今回の「取り敢えず人が遊べるだけのものへ」と書きましたが、これには

 

若干の偽り

 

があります。というのは、8Puzzleの盤面のパターン(0~8までの9つのピースの並び)は、9つの枠に一つずつ0~8までの自数を埋めてゆくことになるので、9の階乗(9! = 362880 )通りありますが、この内半分(9! / 2 = 181440 )は

 

解くことが出来ない盤面パターン

 

になります。(ご参考。具体的には「8パズルが完成するか否かは、0~8までの配列を完成させるまでの置換のパリティ(偶奇)と「空き」の最短のマンハッタン距離の偶奇が等しい」ということのようです。)

 

では、今後このプログラムをどうするのか?

 

That is the question!

 

現在考え中なのは、

 

(1)↓の「開始」ボタンのシャッフル処理を「解けるパターンだけ」に限定する。(現在と見栄えは同じ。)

(2)解けないパターンがあることも理解していただくために、シャッフル処理はそのままにして、「ヒント」で解けるか否かをチェックさせる。

(3)↑の(2)の「ヒント」を実際にPCに解かせてみて可不可を表示する。

 

上記(1)~(3)までを参考()に基づいて作成するか、または完成形(0~8の配列)を乱数で崩してバラバラにし、その過程(即ち、逆に見れば「解」)を使う、というチート的なアプローチもあるかな?なんて考えています。

置換の基礎

  8パズル,15パズルの不可能な配置と判定法

  スライドパズルの数理

  幅優先探索・深さ優先探索・A*アルゴリズムの説明

 

Stay tuned!(いつになるかわかんないけど...)

 

【8Puzzle-一人遊び迄(追加部分)】

        private void EtPuzzle_Load(object sender, EventArgs e)
        {
            //画像選択ボタン
            btnFile = new Button();
            btnFile.Location = new Point(ClientSize.Width - btnFile.Width - 10, 10);
            btnFile.Text = "画像を選ぶ";
            btnFile.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnFile.Click += OnFileOpen_Click;
            this.Controls.Add(btnFile);

            //開始ボタン
            btnStart = new Button();
            btnStart.Location = new Point(ClientSize.Width - btnStart.Width - 10, btnFile.Height + 20);
            btnStart.Text = "開始";
            btnStart.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnStart.Click += OnStart_Click;
            this.Controls.Add(btnStart);

            //↑ボタン
            btnUp = new Button();
            btnUp.Size = new Size(24, 24);
            btnUp.Location = new Point(ClientSize.Width - 58, btnFile.Height + btnStart.Height + 30);
            btnUp.Text = "↑";
            btnUp.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnUp.Click += OnUp_Click;
            this.Controls.Add(btnUp);

            //↓ボタン
            btnDown = new Button();
            btnDown.Size = new Size(24, 24);
            btnDown.Location = new Point(ClientSize.Width - 58, btnFile.Height + btnStart.Height + 78);
            btnDown.Text = "↓";
            btnDown.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnDown.Click += OnDown_Click;
            this.Controls.Add(btnDown);

            //←ボタン
            btnLeft = new Button();
            btnLeft.Size = new Size(24, 24);
            btnLeft.Location = new Point(ClientSize.Width - 82, btnFile.Height + btnStart.Height + 54);
            btnLeft.Text = "←";
            btnLeft.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnLeft.Click += OnLeft_Click;
            this.Controls.Add(btnLeft);

            //→ボタン
            btnRight = new Button();
            btnRight.Size = new Size(24, 24);
            btnRight.Location = new Point(ClientSize.Width - 34, btnFile.Height + btnStart.Height + 54);
            btnRight.Text = "→";
            btnRight.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnRight.Click += OnRight_Click;
            this.Controls.Add(btnRight);

            //終了ボタン
            btnExit = new Button();
            btnExit.Location = new Point(ClientSize.Width - btnExit.Width - 6, ClientSize.Height - btnExit.Height - 12);
            btnExit.Text = "終了";
            btnExit.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
            btnExit.Click += OnExit_Click;
            this.Controls.Add(btnExit);

            //ボタン状態の初期設定(解説:追加部分です。
            BtnStatus(0);


            //ピクチャーボックス
            picBox = new PictureBox();
            //位置
            picBox.Location = new Point(10, 10);
            picBox.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
            //サイズ
            picBox.Width = ClientSize.Width - btnFile.Width - 30;
            picBox.Height = ClientSize.Height - 20;
            //オリジナルサイズを保存
            picOrgWidth = picBox.Width;
            picOrgHeight = picBox.Height;
            //その他プロパティ
            picBox.BorderStyle = BorderStyle.Fixed3D;
            //Drag and Dropイベントハンドラの追加
            picBox.DragEnter += new DragEventHandler(PB_DragEnter);
            picBox.DragDrop += new DragEventHandler(PB_DragDrop);
            picBox.AllowDrop = true;
            //FormにpicBoxを追加
            this.Controls.Add(picBox);
        }

        private void EtPuzzle_Shown(object sender, EventArgs e)
        {
            //起動時の引数をチェックし、引数にファイル名があればそれを読み込む
            string[] arg = System.Environment.GetCommandLineArgs();
            if(arg.Length > 1)
            {
                //画像ファイルチェック
                if(Filter.IndexOf((System.IO.Path.GetExtension(arg[arg.Length - 1])).ToLower()) == -1)
                {
                    MessageBox.Show(arg[arg.Length - 1] + "は画像ファイルではありません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                //フォーム描画が完了した後に呼び出す(Chat-GPT様と相談してこの結果となった)
                this.BeginInvoke(new Action(() =>
                {
                    if(!InitCanvas(arg[arg.Length - 1]))
                        MessageBox.Show("初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    else
                        //ボタン状態の変更(解説:追加部分です。
                        BtnStatus(1);

                }));
            }
            else
                picBox.Image = null;
        }

        //画像選択ボタン
        private void OnFileOpen_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofDlg = new OpenFileDialog();
            //ファイルフィルターの指定
            ofDlg.Filter = Filter;
            ofDlg.RestoreDirectory = true;    //初期ディレクトリへ復帰
            ofDlg.CheckPathExists = true;    //ファイルパスの存在チェック
            ofDlg.InitialDirectory = ".";    // デフォルトのフォルダーの指定
            ofDlg.Title = "ファイルを開く";    //ダイアログのタイトルを指定する
            if(ofDlg.ShowDialog() == DialogResult.Cancel)    //ダイアログを表示する
            {
                MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
            else
            {
                //仮想画面処理
                if(!InitCanvas(ofDlg.FileName))
                    MessageBox.Show("初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                else
                    //ボタン状態の変更(解説:追加部分です。
                    BtnStatus(1);

            }
            //オブジェクトを破棄する
            ofDlg.Dispose();
        }

        //開始ボタン
        private void OnStart_Click(object sender, EventArgs e)
        {
            //乱数の初期化
            Random rand = new Random((int) DateTime.Now.Ticks & 0x0000FFFF);
          
 //Pieces配列をシャッフルする
            for(int i = 8; i > 1; i--)
            {
                int num = rand.Next(i + 1);
                int temp = Pieces[i];
                Pieces[i] = Pieces[num];
                Pieces[num] = temp;

            }

            //ピース画像の表示
            ShowPieces();
            //ドラッグアンドドロップの禁止
            picBox.AllowDrop = false;
            //ボタン状態の変更
            BtnStatus(2);

        }

        //↑ボタン
        private void OnUp_Click(object sender, EventArgs e)
        {
            int pos = Array.IndexOf(Pieces, 8);    //Pieces[8](空ピース)の位置
            int y = pos / 3;    //空ピースのy位置
            if(y < 1)
                Console.Beep();
            else                //Piecesの値をスワップする
            {
                int temp = Pieces[pos];
                Pieces[pos] = Pieces[pos - 3];
                Pieces[pos - 3] = temp;
            }
            //ピース画像の表示
            ShowPieces();
            //完成判定
            IsDone();

        }

        //↓ボタン
        private void OnDown_Click(object sender, EventArgs e)
        {
            int pos = Array.IndexOf(Pieces, 8);    //Pieces[8](空ピース)の位置
            int y = pos / 3;    //空ピースのy位置
            if(y > 1)
                Console.Beep();
            else                //Piecesの値をスワップする
            {
                int temp = Pieces[pos];
                Pieces[pos] = Pieces[pos + 3];
                Pieces[pos + 3] = temp;
            }
            //ピース画像の表示
            ShowPieces();
            //完成判定
            IsDone();
    
    }

        //←ボタン
        private void OnLeft_Click(object sender, EventArgs e)
        {
            int pos = Array.IndexOf(Pieces, 8);    //Pieces[8](空ピース)の位置
            int x = pos % 3;    //空ピースのx位置
            if(x == 0)
                Console.Beep();
            else                //Piecesの値をスワップする
            {
                int temp = Pieces[pos];
                Pieces[pos] = Pieces[pos - 1];
                Pieces[pos - 1] = temp;
            }
            //ピース画像の表示
            ShowPieces();
            //完成判定
            IsDone();

        }

        //→ボタン
        private void OnRight_Click(object sender, EventArgs e)
        {
            int pos = Array.IndexOf(Pieces, 8);    //Pieces[8](空ピース)の位置
            int x = pos % 3;    //空ピースのx位置
            if(x == 2)
                Console.Beep();
            else                //Piecesの値をスワップする
            {
                int temp = Pieces[pos];
                Pieces[pos] = Pieces[pos + 1];
                Pieces[pos + 1] = temp;
            }
            //ピース画像の表示
            ShowPieces();
            //完成判定
            IsDone();

        }

        ///////////////////////////
        //コントロール関連メソッド
        ///////////////////////////
    ・

    ・

    ・

        //picBoxにドロップされたとき
        private void PB_DragDrop(object sender, DragEventArgs e)
        {
            //ドロップされたファイルパスを取得
            string[] ddlist =  (string[])e.Data.GetData(DataFormats.FileDrop, false);
            //画像ファイルチェック
            if(Filter.IndexOf((System.IO.Path.GetExtension(ddlist[ddlist.Length - 1])).ToLower()) == -1)
            {
                MessageBox.Show(ddlist[ddlist.Length - 1] + "は画像ファイルではありません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            //仮想画面処理
            if(!InitCanvas(ddlist[ddlist.Length - 1]))
                MessageBox.Show("初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            else
                //ボタン状態の変更(解説:追加部分です。
                BtnStatus(1);

        }

        ///////////////////
        //ユーザーメソッド(解説:追加部分です。
        ///////////////////



        //PieceImgの表示
        private void ShowPieces()
        {    //一旦キャンバスをpicBoxの背景色でクリアする
            cvsHandle.Clear(picBox.BackColor);
            //左上から右へ0-2、3-5、6-8の順に、Pieces[i]配列の値(0-8)を引数とするPieceImg[]を表示する
            for(int i = 0; i < 9; i++)
            {
                if(Pieces[i] != 8)
                {
                    cvsHandle.DrawImage(PieceImg[Pieces[i]], (int)PieceW * (i % 3), (int)PieceH * (i / 3));
                    cvsHandle.DrawRectangle(Pens.Black, (int)PieceW * (i % 3), (int)PieceH * (i / 3), (int)PieceW, (int)PieceH);
                }
                else
                    cvsHandle.DrawRectangle(Pens.Red, (int)PieceW * (i % 3), (int)PieceH * (i / 3), (int)PieceW, (int)PieceH);
            }
            picBox.Image = Canvas;
        }

        
//ボタン状態変更
        private void BtnStatus(int pat = 0)
        {
            //各ボタンのEnabled属性初期値
            btnFile.Enabled = true;
            btnStart.Enabled = false;
            btnUp.Enabled = false;
            btnDown.Enabled = false;
            btnLeft.Enabled = false;
            btnRight.Enabled = false;
            switch(pat)
            {
                case 1:        //引数1は「開始」ボタンを有効化する
                    //btnFile.Enabled = false;    不能にすべきか?悩む
                    btnStart.Enabled = true;
                    break;
                case 2:        //引数2は上下左右ボタンを有効化し、他を無効化する
                    btnFile.Enabled = false;
                    btnUp.Enabled = true;
                    btnDown.Enabled = true;
                    btnLeft.Enabled = true;
                    btnRight.Enabled = true;
                    break;
                default:    //初期値0はこの処理
                    break;
            }
        }

        //完了判定
        private bool IsDone()
        {
            for(int i = 0; i < 9; i++)
                if(Pieces[i] != i)
                    return false;
            MessageBox.Show("Your made it!\r\n8 Puzzleが完成しました!", "Congratulations!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            InitImage();
            BtnStatus(0);    //ボタン状態の初期設定
            return true;
        }

    }
}
 

因みに、開始ボタンを押すとこうなります。

 

現在神様はポーランドとドイツへ旅行中。後8日は返ってこないなぁ。

 

そんなんで、先週の木曜日(5/29)飲みかけの赤ワインもあるので、

 

ビーフスティク

 

なぞ作ってみようかと...

 

まぁ、イメージとしてはOutback(私は余り刺しだらけ、油まみれの和牛は好きではなく、しっかりとした柔らかい噛み応え)のサーロインかリブアイ(が好みです)。付け合わせは矢張りベィクドポテトとブロッコリー。ウ~ン、後はコールスローかな?

 

早速、買い出しに出たのですが、いつも豪州産ステーキ肉(オーストラリア牛の種は和牛だそうですが)を置いてあるピーコックには刺しだらけの和牛肉。「これはダメだ」とほかの店に廻るのですが、ストライクゾーンがありません。そして、最後の店で...

 

「神戸牛 150g 398円」(本日限り50%off)

 

見た目も刺しが多いようには見えず、とっさに買ったのですが、家に帰ってよくよく見るとその肉は

 

「すね肉(カレー・シチュー用)」!

 

良い加減で、きれいに焼けましたが、そっくり反っておわん型になりました。

 

 

半分は筋っぽかったですが、半分は柔らかく食べられました。なので↑のタイトルとなりました。

 

前回の躓きを踏まえて、取り敢えずファイルを読み込んで表示するだけのプログラムです。()これから8Puzzleの仕様を決めて機能を実装してゆきます。

:ファイルをプログラムアイコンにドロップしたり、画像選択ボタンでファイルを開くダイアログを使って読み込んだり、ピクチャーボックスにドラッグアンドドロップして画像を表示します。

 

コメント解説:を書いておきましたのでご参考にしてください。

 

【8Puzzle.cs】

//////////////////////
//    EightPuzzle.cs
// Copyright (c) 2025
//     By Y-Sama
//////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;    //PixelFormatを使う為
using System.Reflection;        //Assemblyを使う為

namespace EightPuzzle
{
    public partial class EtPuzzle : Form
    {
        //メンバーグラフィックス
        Bitmap BaseImg;                        //ピクチャーボックスサイズ化した基本画像
        Bitmap Canvas;                        //Bitmapインスタンスによる仮想画面
        Graphics cvsHandle;                    //Canvasのグラフィックハンドル
        Image[] PieceImg = new Image[8];    //分割画像([0]-[7])
        //メンバーコントロール
        PictureBox picBox;
        Button btnFile, btnStart, btnUp, btnDown, btnLeft, btnRight, btnExit;
        //メンバーフィールド
        int picOrgWidth;    //ピクチャーボックス幅の初期値
        int picOrgHeight;    //ピクチャーボックス高さの初期値
        Single PieceW;        //分割画像幅
        Single PieceH;        //分割画像高さ
        int[] Pieces = new int[9] {0, 1, 2, 3, 4, 5, 6, 7, 8};    //8パズルのピース配列(8はブランク)
        const string Filter = "イメージファイル|*.bmp;*.jpeg;*.jpg;*.gif;*.tiff;*.png;*.wmf;*.emf;*.ico";

        [STAThread]
        public static void Main()
        {
            Application.Run(new EtPuzzle());
        }

        public EtPuzzle()
        {
            Assembly myOwn = Assembly.GetEntryAssembly();
            this.Icon = Icon.ExtractAssociatedIcon(myOwn.Location);    //プログラムアイコンをフォームにつける
            this.Size = new Size(640, 480);
            this.MinimumSize = new Size(320, 190);
            this.FormBorderStyle = FormBorderStyle.FixedDialog;        //ダイアログ枠
            this.MaximizeBox = false;
            this.Text = "8 Puzzle";
            this.Load += EtPuzzle_Load;
            this.Shown += EtPuzzle_Shown;    //解説:これが噂のForm.Shownイベントです。
        }

        private void EtPuzzle_Load(object sender, EventArgs e)
        {
            //画像選択ボタン
            btnFile = new Button();
            btnFile.Location = new Point(ClientSize.Width - btnFile.Width - 10, 10);
            btnFile.Text = "画像を選ぶ";
            btnFile.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnFile.Click += OnFileOpen_Click;
            this.Controls.Add(btnFile);

            //開始ボタン
            btnStart = new Button();
            btnStart.Location = new Point(ClientSize.Width - btnStart.Width - 10, btnFile.Height + 20);
            btnStart.Text = "開始";
            btnStart.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnStart.Click += OnStart_Click;
            this.Controls.Add(btnStart);

            //↑ボタン (解説:上下左右ボタンはMaze.csのものを再利用しました。)
            btnUp = new Button();
            btnUp.Size = new Size(24, 24);
            btnUp.Location = new Point(ClientSize.Width - 58, btnFile.Height + btnStart.Height + 30);
            btnUp.Text = "↑";
            btnUp.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnUp.Click += OnUp_Click;
            this.Controls.Add(btnUp);

            //↓ボタン
            btnDown = new Button();
            btnDown.Size = new Size(24, 24);
            btnDown.Location = new Point(ClientSize.Width - 58, btnFile.Height + btnStart.Height + 78);
            btnDown.Text = "↓";
            btnDown.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnDown.Click += OnDown_Click;
            this.Controls.Add(btnDown);

            //←ボタン
            btnLeft = new Button();
            btnLeft.Size = new Size(24, 24);
            btnLeft.Location = new Point(ClientSize.Width - 82, btnFile.Height + btnStart.Height + 54);
            btnLeft.Text = "←";
            btnLeft.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnLeft.Click += OnLeft_Click;
            this.Controls.Add(btnLeft);

            //→ボタン
            btnRight = new Button();
            btnRight.Size = new Size(24, 24);
            btnRight.Location = new Point(ClientSize.Width - 34, btnFile.Height + btnStart.Height + 54);
            btnRight.Text = "→";
            btnRight.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnRight.Click += OnRight_Click;
            this.Controls.Add(btnRight);

            //終了ボタン
            btnExit = new Button();
            btnExit.Location = new Point(ClientSize.Width - btnExit.Width - 6, ClientSize.Height - btnExit.Height - 12);
            btnExit.Text = "終了";
            btnExit.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
            btnExit.Click += OnExit_Click;
            this.Controls.Add(btnExit);

            //ピクチャーボックス
            picBox = new PictureBox();
            //位置
            picBox.Location = new Point(10, 10);
            picBox.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
            //サイズ
            picBox.Width = ClientSize.Width - btnFile.Width - 30;
            picBox.Height = ClientSize.Height - 20;
            //オリジナルサイズを保存(解説:これに画像の縦横比を乗じて調整します。)
            picOrgWidth = picBox.Width;
            picOrgHeight = picBox.Height;
            //その他プロパティ
            picBox.BorderStyle = BorderStyle.Fixed3D;
            //picBox.SizeMode = PictureBoxSizeMode.StretchImage;    //クライアントサイズに合わせる
            //picBox.SizeMode = PictureBoxSizeMode.Zoom;            //縦横比が変更されない

            //解説:↑はまだ考え中です。
            //Drag and Dropイベントハンドラの追加

            picBox.DragEnter += new DragEventHandler(PB_DragEnter);
            picBox.DragDrop += new DragEventHandler(PB_DragDrop);
            picBox.AllowDrop = true;
            //FormにpicBoxを追加
            this.Controls.Add(picBox);
        }

        private void EtPuzzle_Shown(object sender, EventArgs e)    //解説:Form.Shownイベント処理です。
        {
            //起動時の引数をチェックし、引数にファイル名があればそれを読み込む
            string[] arg = System.Environment.GetCommandLineArgs();
            if(arg.Length > 1)
            {
                //画像ファイルチェック(解説:引数が二つ以上あっても1画像しか表示できないので最後のものを表示します。)
                if(Filter.IndexOf((System.IO.Path.GetExtension(arg[arg.Length - 1])).ToLower()) == -1)
                {    //解説:↑でファイルパスから「拡張子のみ取得し、小文字化して、Filterに該当する文字列があるか」調べます。
                    MessageBox.Show(arg[arg.Length - 1] + "は画像ファイルではありません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
/*
(1) C#でBeginInvokeとは、デリゲート(メソッドを指す変数)を非同期に呼び出すためのメソッドです。非同期呼び出しを開始し、完了を待たずに制御を呼び出し元に戻します。非同期呼び出しの完了を待つ場合は、EndInvokeメソッドを使用します。
(2) 「非同期に」とは、逐次処理を行うのではなく、実行できる段階で実行されることを意味します。
(3) Actionはデリゲートの一種であり、戻り値を持たないvoid型で、Genericで引数の型を指定でき、パラメータは最大16個まで設定できます。
 (4) Actionの使用例
    A.デリゲートを使用したAction
        Action actionA = new Action(delegate () { Console.WriteLine("ActionA"); });
    B.デリゲートを使用したAction
        Action actionB = delegate () { Console.WriteLine("ActionB"); };
    C.ラムダ式を使用したAction
        Action actionC = new Action(() => Console.WriteLine("ActionC"));
    D.ラムダ式を使用したAction
        Action actionD = () => Console.WriteLine("ActionD");

    //解説:のコードは{}ブロックで書かれた処理をメソッドとしてラムダ式でAction化して非同期で実行します。

*/
                //フォーム描画が完了した後に呼び出す(Chat-GPT様と相談してこの結果となった)
                this.BeginInvoke(new Action(() =>
                {
                    if(!InitCanvas(arg[arg.Length - 1]))
                        MessageBox.Show("初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }));

            }
            else
                picBox.Image = null;
        }

        //終了処理
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.No)
                e.Cancel = true;
            else    //終了前にグラフィック関係のリソースを開放する
            {
                InitImage();        //グラフィック関係の初期化
                picBox.Dispose();    //PictureBoxリソースを解放
            }
        }

        //画像選択ボタン
        private void OnFileOpen_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofDlg = new OpenFileDialog();
            //ファイルフィルターの指定
            ofDlg.Filter = Filter;    //解説:ここでもFIlterを使っています。
            ofDlg.RestoreDirectory = true;    //初期ディレクトリへ復帰
            ofDlg.CheckPathExists = true;    //ファイルパスの存在チェック
            ofDlg.InitialDirectory = ".";    // デフォルトのフォルダーの指定
            ofDlg.Title = "ファイルを開く";    //ダイアログのタイトルを指定する
            if(ofDlg.ShowDialog() == DialogResult.Cancel)    //ダイアログを表示する
            {
                MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
            else
            {
                //仮想画面処理
                if(!InitCanvas(ofDlg.FileName))
                    MessageBox.Show("初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            //オブジェクトを破棄する
            ofDlg.Dispose();
        }
 

//解説:以下「ドンガラ」なので処理が描かれていません。
 

        //開始ボタン
        private void OnStart_Click(object sender, EventArgs e)
        {
            
        }

        //↑ボタン
        private void OnUp_Click(object sender, EventArgs e)
        {
            
        }

        //↓ボタン
        private void OnDown_Click(object sender, EventArgs e)
        {
            
        }

        //←ボタン
        private void OnLeft_Click(object sender, EventArgs e)
        {
            
        }

        //→ボタン
        private void OnRight_Click(object sender, EventArgs e)
        {
            
        }

        private void OnExit_Click(object sender, EventArgs e)
        {
            //終了処理
            Close();
        }

        ///////////////////////////
        //コントロール関連メソッド
        ///////////////////////////
        //picBoxにドラッグされた時

        private void PB_DragEnter(object sender, DragEventArgs e)
        {
            if(e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;
            else
                e.Effect = DragDropEffects.None;
        }

        //picBoxにドロップされたとき
        private void PB_DragDrop(object sender, DragEventArgs e)
        {
            //ドロップされたファイルパスを取得
            string[] ddlist =  (string[])e.Data.GetData(DataFormats.FileDrop, false);
            //画像ファイルチェック(解説:1画像しか表示できないので最後のものを表示します。)
            if(Filter.IndexOf((System.IO.Path.GetExtension(ddlist[ddlist.Length - 1])).ToLower()) == -1)
            {
                MessageBox.Show(ddlist[ddlist.Length - 1] + "は画像ファイルではありません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            //仮想画面処理
            if(!InitCanvas(ddlist[ddlist.Length - 1]))
                MessageBox.Show("初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }

        ///////////////////
        //ユーザーメソッド
        ///////////////////
        //画像ファイルパスを受け取った際の処理

        private bool InitCanvas(string filepath)
        {
            //グラフィック関係の初期化
            InitImage();
            //filepathで指定された画像ファイルを一旦orgに読み込み、サイズを調整してBaseImgを作成
            Image org = Image.FromFile(filepath);
            //画像ファイルの縦横比に合わせてpicBoxのサイズを再設定
            if(org.Width * org.Height == 0)
                return false;
            else
            {
                if(org.Width > org.Height)
                {
                    picBox.Width = picOrgWidth;
                    picBox.Height = (int)((double)picOrgWidth * (double)org.Height / (double)org.Width);
                }
                else
                {
                    picBox.Height = picOrgHeight;
                    picBox.Width = (int)((double)picOrgHeight * (double)org.Width / (double)org.Height);
                }
            }
            //修正されたpicBoxのサイズに合わせてBaseImgを生成
            BaseImg = new Bitmap(picBox.Width, picBox.Height);
            Graphics biHandle = Graphics.FromImage(BaseImg);
            //画像ファイルを変更したpicBoxのサイズでBaseImgに描画
            biHandle.DrawImage(org, 0, 0, picBox.Width, picBox.Height);
            //グラフィックハンドルを廃棄(開放)する
            biHandle.Dispose();
            //オリジナル画像を廃棄(開放)する
            org.Dispose();

            //BaseImgでCanvasを生成
            Canvas = new Bitmap(BaseImg);
            //cvsHandleを生成する
            cvsHandle = Graphics.FromImage(Canvas);
            //picBoxに画像ファイルを表示
            picBox.Image = Canvas;

            //分割画像サイズ設定
            PieceW = (Single)picBox.Width / 3;
            PieceH = (Single)picBox.Height / 3;
            //BasdeImgで分割画像を作成
            RectangleF Rect;
            Rect = new RectangleF(0, 0, PieceW, PieceH);
            PixelFormat Format = BaseImg.PixelFormat;
            for(int i = 0; i < 8; i++)
            {
                Single x = PieceW * (i % 3);
                Single y = PieceH * (i / 3);
                Rect = new RectangleF(x, y, PieceW, PieceH);
                PieceImg[i] = BaseImg.Clone(Rect, Format);
            }
            return true;
        }

        //グラフィック関係の初期化
        private void InitImage()
        {
            if(BaseImg != null)
                BaseImg.Dispose();        //基本画像
            if(cvsHandle != null)
                cvsHandle.Dispose();    //仮想画面のグラフィック
            if(Canvas != null)
                Canvas.Dispose();        //仮想画面
            if(PieceImg != null)
            {
                foreach(Image img in PieceImg)
                    if(img != null)
                        img.Dispose();    //分割画像
            }
        }
    }
}

 

コードが長くなるので、今後は追加・変更部分のみを書いてゆくつもりです。(が、分かりにくくて全部載せることもあるかと思います... 要すれば、自由にやらせていただきます。)

 

とうとう始めたこの【8Puzzle】シリーズ、プログラムはまだ全然できていないのですが、ブログネタとして一緒に走ります。

第一回のブログで書いた最初のトラブル

 

(1)ダイアログにボタンとピクチャーボックスを貼り付けるのは良いとして、どのように読み込んだ原画像をピクチャーボックスに合わせるのか?(サイズと縦横比)

(2)メンバーグラフィックオブジェクトの仕様問題・・・ピクチャーボックスはImageプロパティがあるので、それに仮想画面を貼り付けるが、これはImageクラスにすべきか、Bitmapクラスにすべきか?現画像は保持すべきか、分割画像はImageか、Bitmapか?(ここでトラブル発生

(3)分割画像をどう表示()するのか?8Puzzleの観掛けを出すために、分割画像をそれとわかるようにしたいが、どうすべきか?(ここでトラブル発生

:一旦仮想画面に描画して、それをピクチャーボックスに貼り付けるが、描画は変更部分のみか、全体か?変更部分のみであれば「空ピース」画像をどうする(取得?作成?)するか?

(4)画像選択ボタン(ファイル選択ダイアログ)、ドラッグアンドドロップは正常に画像が表示されるが、起動時にファイルをドロップしても画像が表示されない?(ここでトラブル発生

 

について触れます。

 

(1)サイズと縦横比

↑の(1)についても、本当は変動サイズ型のウィンドウにするか、固定サイズ型のダイアログにするか結構迷っていましたが、サイズが変更できてもユーザーにメリットがないことから、固定サイズとし、画像とそれを表示するピクチャーボックスの大きさを固定することにしました。そして、選択した画像の大きさに関わらず、その縦横比を求め、それに応じて表示画像とピクチャーボックスを調整する形にしました。

 

(2)Imageか、Bitmapか?

問題の原因は私の勉強不足にあったのですが、何故かImageをWin32 GDIベースのビットマップがGDI+対応に発展したものと理解していて()、インスタンスを生成すれば、それがデータを持つと思っていました。別の言葉でいえば、ImageクラスにBitmap

を代入し(Image = Bitmap)、それをピクチャボックスのImageプロパティに代入すれば基本Imageオブジェクトに画像データのコピーが生成されると考えていました。

:ImageはGDI+でカバーできるファイル形式を総て含むラスター、ベクター画像のラッパー(「抽象基本」)クラスで、これからBitmapクラスが派生しています。又Image()というコンストラクターは無く、画像データは全て".FromXXX"というメソッドで受け入れます。

 

現実には正反対で、、メソッド内のローカルオブジェクトで画像ファイルを読み込み、それをImageオブジェクトにコピーし、そのメソッド内でローカルオブジェクトを廃棄(Dispose())すると、コピーしたImageオブジェクトはnull参照エラーとなるようです。(「使用されたパラメーターが有効ではありません。」エラー)

その想定を確認すべく、Chat-GPT様と会話を重ねた最終結果が以下です。(

まとめ:今回の問題の本質

項目 説明
Image は抽象クラス 実際には BitmapMetafile のインスタンス
Canvas = img; は参照コピー 新しい画像を作ったわけではない
Dispose() で内部は破棄される 残っている参照も無効になる
解決方法 Clone() または DrawImage() を使ってコピーを作成すること

:しっかし、Chat-GPT様はどこで習ってくるのか、「人をヨイショする」ことも抜かりないですね。このやり取りの中でこんなことを言いました。「非常に鋭いご質問です。おっしゃっている内容の理解は 概ね正しく、深い考察が見られます。以下で、「Image と Bitmap の関係」「参照とデータの実体」「コピーの仕方」などについて、わかりやすく整理して解説します。」でも、読み方によっては「上から目線」で、「(無知な)人を馬鹿にしている」ようにも響きますね。

 

この為、最終の仕様としてグラフィック廻りのメンバーを以下の通りとしています。(最新の8Puzzle.csコードの抜粋)

 

        //メンバーグラフィックス
        Bitmap BaseImg;                        //ピクチャーボックスサイズ化した基本画像
        Bitmap Canvas;                        //Bitmapインスタンスによる仮想画面
        Graphics cvsHandle;                    //Canvasのグラフィックハンドル
        Image[] PieceImg = new Image[8];    //分割画像([0]-[7])
 

BaseImgは原画像をサイズ調整したもので、分割画像取得用です。その為、(Imageではできない)部分クローン処理ができるようBitmapにしました。一方部分クローン画像データを与えるので分割画面はImageにしています。(空ピースはピクチャーボックスの背景色を使うCanvasの背景のままです。)CanvasはImageでもよかったのですが、BaseImgと同じにしてBitmapにしました。

 

(3)分割画像をどう表示するか?

これはどういうことかというと、「選択した画像を8パズル用に分割し(「分割画像ピース」)、それが背景画像(「空ピース」)と人間の操作で上下左右にスワップさせる」のですが、それを「どう表示するか」という問題です。最初は、

 

【当初案】

ファイルから読み込んだ画像をピクチャーボックスに表示する前に、ピクチャーボックスと同じサイズに調整することは↑で述べました。その調整後の基本画像(Bitmap BaseImg)を縦横に三等分した9つの分割画像(Image PieceImg配列)を作成し、先ず0~7の8つの分割画像ピースと一つの空ピースを仮想画面(Image Canvas)に書き込みます。以降は空ピースの移動時に、その空ピースとスワップされる分割画像ピースのみ仮想画面に書き込み、ピクチャーボックスのImageプロパティに代入して表示します。(従って、原画像や基本画像等は不要なので廃棄しますが、これが結局Imageのエラーにつながりました。また、8パズルが完成した時のみ、空ピースの位置に9番目の分割画像ピースを表示する予定でした。)

 

を考えていましたが、結局↑の問題解決等に合わせて、

 

【修正案】

サイズに調整後の基本画像(Bitmap BaseImg)縦横に三等分した0~7の8つの分割画像(Image PieceImg配列)を作成し、以降は仮想画面(Bitmap Canvas)を一旦クリアして、分割画像ピース8つ書き込んで一括表示(ピクチャーボックスのImageプロパティに代入)します。(空ピースは使用しません。)8パズルが完成した時は、空ピース位置に画像を表示する代わりに基本画像全体を表示させるようにしました。(

:「8つの分割画像」と書いていますが、これを指定するインデックス配列は勿論9つにしています。("int[] Pieces = new int[9] {0, 1, 2, 3, 4, 5, 6, 7, 8};    //8パズルのピース配列(8はブランク)"としていますが、これは解法メソッドとの関連で、将来別のクラスオブジェクトにするかもしれません。)

 

また、最初分割画像のみを表示させましたが、「分割感」が乏しいので、分割画像ピースを黒、空ピースの矩形描画により目立たせています。

 

(4)起動時にファイルをドロップしても画像が表示されない

これは私も初めて経験したので一寸面喰いました。

前回作ったMSBuilderと同じく、ファイル選択を

 

(1)起動時に8Puzzleプログラムアイコンに画像ファイルをドロップする

(2)「画像を選ぶ」ボタンでファイル選択ダイアログを開く

(3)ピクチャーボックスに画像ファイルをドロップする

 

のいずれも可能になるようにプログラムした所、(2)、(3)は正常に動くのですが、(1)は(Form.Loadイベントで)ファイルパスをきちんと受け取っているのですが、画像が表示されません。(因みにMSBuilder等TextBoxの場合は同じコードでもきちんと文字列データが表示されます。)

 

これはピクチャーボックスの作成(データ表示ができる迄)に時間がかかり、ファイルパスが渡されても、表示準備に間に合わないのではないか、と考え、Chat-GPT様に相談しました。すると、

 

ご質問の内容から推測するに、アプリ起動時にドラッグ&ドロップされた画像ファイルを読み込んでも PictureBox に画像が表示されないのは、フォームや PictureBox の描画がまだ完了していないタイミングで描画要求をしている可能性が高いです。

これは非常によくある現象で、Form.Load イベントの中で PictureBox.Image = ... を設定しても、まだコントロールの描画準備が整っておらず、描画がスキップされてしまう場合があるためです。

 

という(私と同じ考えの)回答で、より遅く発生するForm.Shownイベントで表示させなさいとのこと。「Form.Shown?なーるほど」ということで読み込み部分をForm.Shownイベントを作成して、転記しました。

 

ところがっ!

 

症状は全く同じで、何ら改善されません。「やっぱ、動かないよっ!」と伝えると(Chat-GPT様はムッとしたようで)基本的な問題を3つあげつらいます。それに対して

 

ご指摘の1と2は、ファイル選択ボタン処理やドラッグアンドドロップで正しく動いているので関係ありません。3についてはMessageBoxで確認済で問題ありません。矢張り、ピクチャーボックスの生成とのタイミングの違い以外に他の二つの正常動作するコードと違いがないので、それしか原因はないと思いますがどうでしょうか?

 

と反論すると、今度は

 

BeginInvoke()を使った遅延初期化は、Windows Forms における「初期描画タイミングのズレ」問題の定番対策です。ぜひ試してみてください。それで表示されるようになるはずです。

 

という返答で(内心、「そんなら早く言えよっ!」と思いましたが)、正直BeginInvoke()注1については知らなかったので、早速試し、正常に動いたので助かりました。(注2

注1:BeginInvoke()とは「コントロールの基になるハンドルが作成されたスレッド上で、非同期的にデリゲート(解説:メソッドを参照するための型を実行」するもので、「同期処理という、複数の処理を1つずつ順番に実行する逐次処理」ではなく、「非同期処理とは、複数の処理を順番を待たないで実行する処理」を行う為に、譬えて言えば、今処理できないメソッドをデリゲートという型にして、処理できる迄キューに入れておき、処理可能な段階で処理するためのものとご理解ください。

注2:C++でネイティブコードを書いていた時にはグラフィック読み込みでこのようなことはなかったので、改めてC#が.NET CLR (共通言語ランタイム) を利用したインタープリターであることを実感しました。

 

最初から一寸長くなりましたが、

 

単なるドンガラ作りで既にこれだけ躓いた

 

というお話でした。次回は此処(「単に原画像データを読み込んで、基本画像を表示させ、一応分割画像ピースも作成する」)迄のコードをご紹介しようかと思います。(

:現在はそのコードに、分割画像ピースを表示させ、空ピースを上下左右ボタンで動かして、8Puzzleの「人間による一人遊び」が出来る状態にありますが、実は乱数で分割ピースをバラバラにすると半分は「完成できない」ものになるので、「さて、それからどう発展させようか?」と思案している状況です。ということで、ここからちょっと停滞することが予想され、それまでは画像をバラバラにする乱数処理等に触れる予定です。