MENU
ここでは、Twitter記事を作成するプログラムを通して、C#の文法とフィルのI/Oについて学んでいきます。

今回作るプログラムは、Twitter記事を作成して、指定したフォルダ内にリッチテキスト形式のファイル(拡張子.rtf)として格納していきます。

ファイルの名前は指定したBASEに4桁の数字を付加したものになります。

例えば、BASEにtweetarticleと指定した場合は、tweetarticle0001.rtf、tweetarticle0002.rtf、tweetarticle0003.rtf、....,tweetarticle9999.rtfという名前が付けられます。(一つのフォルダ内に999本分の記事まで作成可能です。)

フォルダとファイルのBASEを指定したら、フォルダ内でBASEをファイル名の先頭に持ったファイルを検索して、既存の名前の数字の最大値+1のファイル名を自動で設定します。(ファイルがまだない場合は、上の例でいえばtweetarticle0001.rtf)

そして、アプリ上に矢印のボタンを配置して、そのボタンをクリックすることで前の数字をファイル名に持つファイルを表示したり、次の数字を持つファイルを表示することを可能にします。

また、一番最後のファイルを超えて、次のファイルを表示した場合は、自動で空白を表示して新しいファイルを作成する準備をします。(そして、何か記入したら、自動でファイルを作成します。)

アプリの上部には4つのPictureBoxを設定して、1つの記事に最大4つの画像を指定できます。他の画像フォルダから、Drag&Dropで画像を持ってくると、指定した画像フォルダに画像を格納します。

また、その画像ファイルの名前を記事ファイルの先頭1行目に,(カンマ)で区分して記入します。

そしてアプリの下部にはTweetする記事を記入します。そして、半角文字やタブ・改行を1文字、全角文字を2文字として計算し、またhttpで始まるURLは23文字で計算した記事の長さを自動で計算します。(Twitterではこのうち280文字分がツイートされます。)

イメージを掴むために、Visual Studio 2019のデザイン画面を作りましょう。

いつもの様にVisual Studio 2019を起動して、「Windows Formアプリケーション(.Net Framework)」を選択してプロジェクトを作成します。

画面のデザイン

C#入門 - ファイルの入出力とナビゲーション

Formに次の様にWindowsのコモンコントロールを載せて上記の様な状態にしていきます。

コントロールの選択と配置


1.メニューコントロール
まずは、メニューコンテストを配置します。ツールボックスの「メニューとツールバー」にあるMenuStripをマウスをクリックして選択したあと、Form上でマウスを移動してクリックします。

C#入門 - ファイルの入出力とナビゲーション


メニューは、とりあえずこのままにしておきましょう。


2.画像保管フォルダ
Label、Button、TextBox、FolderBrowserDialogを順番にツールボックスから探してマウスをクリックして選択したあと、Form上でマウスを移動してクリックします。

C#入門 - ファイルの入出力とナビゲーション


FolderBrowserDialogは下記の様にツールボックスの「ダイアログ」にあります。

C#入門 - ファイルの入出力とナビゲーション


そして、LabelのTextに「画像保管フォルダ:」、ButtonのTextに「Folder」と設定しておきます。

C#入門 - ファイルの入出力とナビゲーション


なお、この段階でForm上にメニューやFolderBrowserDialogは目に見えない状態ですが、デザイナー画面の下の方に、下記の図の様に表示されていることで、キチンと設置されていることが分かります。

C#入門 - ファイルの入出力とナビゲーション


3.記事ファイル保管フォルダ
上と同様にLabel、Button、TextBox、FolderBrowserDialogを順番にツールボックスから探してマウスをクリックして選択したあと、Form上でマウスを移動してクリックします。(コントロールは以下も同様にして配置してください。)

C#入門 - ファイルの入出力とナビゲーション


そして、LabelのTextに「記事保管フォルダ:」、ButtonのTextに「Folder」と設定しておきます。また、それぞれのコントロールをマウスで動かして、配置もある程度綺麗にしておきましょう。

C#入門 - ファイルの入出力とナビゲーション


4.PictureBox4つ
フォルダのしたにPicTureBoxを4つ配置します。プロパティのサイズ欄を使って、正方形にしておきましたが、よく使うサイズの縦横比があれば、それを指定した方が良いでしょう。

5.記事作成用エリア
RichTextBoxを一つ配置します。Twitter用の記事なので、半角で最大でも280文字なので、改行を考慮しても、そこまで大きくしなくても良いかと思います。適当な大きさにしておきます。

6.BASEファイル名
次に、LabelとTextBoxを配置して、LabelのTextはBASE:としておきます。また、TextBoxにはtweetarticleと入れておきました。これで何もしなければ、記事はtweetarticle0001から作成されます。

7.ナビゲーション
次にButtonを5個とTextBoxを一つ配置します。Button二つ配置して、TextBoxを置き、さらにButtonを3つ置く感じにします。

そして、ButtonのTextには順に、「先頭」、「一つ前」、「一つ後」、「最後」、「追加」と記入しておきます。またTextBoxのTextには初期値として「00001」と記入しておきます。

8.保存ボタン
記事を明示的に保存するときのButtonを配置します。Textには「保存」と記入しておきましょう。

フォルダ関係の処理

さて、フォルダを選択する処理をプログラムしていきます。

Folderのボタンが押されたら、行う処理を記述していきます。

Folderボタンが押された場合の処理

ここではFolderBrowserDialogを使って、画像のフォルダを選択してもらいます。

FolderBrowserDialogについての詳細を知りたい方は、下記を確認してください。

FolderBrowserDialog クラス
https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.folderbrowserdialog?view=netcore-3.1


なので、Folderボタンがクリックされたら、FolderBrowserDialogを表示します。

FolderBrowserDialogの表示はfolderBrowserDialog1.ShowDialog();で出来ます。

表示した後に、選択された結果は、DialogResultというクラスの値で返されますので、

DialogResult ret = folderBrowserDialog1.ShowDialog();

とすることで、OKボタンが押されたかどうかが判断できます。

OKボタンが押された場合だけ、FolderのPathを変更します。

OKボタンが押されたかどうかは、次のようなifステートメントで判断できます。

if (ret == DialogResult.OK)

そして、OKボタンが押されたら、folderBrowserDailogのSelectedPathにフォルダのパスが格納されているので、TextBoxにコピーしておきます。

なお、Folderボタンをクリックした際に、TextBoxに既にフォルダへのパスが設定されていたら、そこから表示してあげるのがやさしいかなと思います。

そこで、Buttonがクリックされた際のプログラムの最初に、下の処理を書いておきます。

if (textBox1.Text != "")
    folderBrowserDialog1.SelectedPath = textBox1.Text;


ここまでの処理をまとめると、こんな感じになります。


        private void button1_Click(object sender, EventArgs e)
        {

            if (textBox1.Text != "")
            {
                folderBrowserDialog1.SelectedPath = textBox1.Text;
            }

            //folder
            // ダイアログに表示する説明文
            folderBrowserDialog1.Description = "フォルダを選択してください。";

            // 新しいフォルダを作るボタンを表示するか否か
            folderBrowserDialog1.ShowNewFolderButton = true;

            // ダイアログを表示
            DialogResult ret = folderBrowserDialog1.ShowDialog();

            // OKボタンが押されたら選択されたフォルダを表示
            if (ret == DialogResult.OK)
            {
                if (textBox1.Text != folderBrowserDialog1.SelectedPath)
                {
                    textBox1.Text = folderBrowserDialog1.SelectedPath;
                }
            }
        }


記事保管フォルダの処理も同様です。

なので、二つ目のFolderボタンをダブルクリックして処理を記述する際に、一つ目の処理をコピペして作れば簡単に記述できます。

その際に、folderBrowserDialog1、textBox1はfolderBrowserDialog2、textBox2に修正することを忘れないでください。

【要注意】
この様にコピペでの処理の記述は簡単に出来るのですが、修正漏れが無い様に気を付けてください。

当たり前なのですが、button1の処理でエラーが出ていなければ、中身をbutton2の方にコピペした場合でもエラーは出ません。

なので、修正漏れがあっても、エラーは表示されません。

今回は、短い処理なので、見落としは無いかもしれませんが、長い処理をコピペした場合は、必ず見落としがあると思って下さい。

テストで見つかれば良いのですが、見つからない場合は潜在的なバグとして残ってしまいます。

        private void button2_Click(object sender, EventArgs e)
        {

            if (textBox2.Text != "")
            {
                folderBrowserDialog2.SelectedPath = textBox2.Text;
            }

            //folder
            // ダイアログに表示する説明文
            folderBrowserDialog2.Description = "フォルダを選択してください。";

            // 新しいフォルダを作るボタンを表示するか否か
            folderBrowserDialog1.ShowNewFolderButton = true;

            // ダイアログを表示
            DialogResult ret = folderBrowserDialog2.ShowDialog();

            // OKボタンが押されたら選択されたフォルダを表示
            if (ret == DialogResult.OK)
            {
                if (textBox2.Text != folderBrowserDialog2.SelectedPath)
                {
                    textBox2.Text = folderBrowserDialog2.SelectedPath;
                }
            }
        }


フォルダのパスを表示するテキストボックスが変更された場合

textBox1に直接フォルダへのパスが入力された場合は、textBox1.Textの値をfolderBrowserDialog1.SelectedPathにセットして上げておきます。textBox1が変更された後に、FolderのButtonが押されれば、textBox1.Textの内容がfolderBrowserDialog1.SelectedPathにセットされますが、そうでないと、いつまでもtextBox1.Textの内容とfolderBrowserDialog1.SelectedPathの内容が異なったままになる可能性があるからです。

textBox1が変更された時の処理は、textBox1をダブルクリックされた先に記述すれば良いだろうと考えると思います。ただ、ダブルクリックされた場合に自動で紐づけられる処理は、textBox1が変更された場合に呼び出される処理になります。そしてtextBox1の変更とは、文字が1文字入力されたり、1文字削除されたりした場合を指していて、文字を変更するごとに毎回毎回発生してしまいます。

例えば、c:\windowsと入れると、c、:、\、w、i、n、d、o、w、sをそれぞれ入力するたびにこの処理が呼ばれることになります。コンピュータからすると、どこでフォルダのパスの入力が終わったかは分からないこともあるので、textBox1が変更されたらそれに対応する処理をするという考えは、今回の場合は、余り上手くないと思われます。

では、どうすれば良いでしょうか。

textBox1に入力された状態が一段落して、textBox1の内容が変更されていたら、folderBrowserDialog.SelectedPathにセットすると言うことではどうでしょうか。ただ一段落も良く分かりませんネ。ここで上手いタイミングがあります。

それは、textBox1から別のコントロールに移動した場合です。それは、Leaveというイベントに対応します。イベントはWindowsがシステムに何か事象が発生したことを教えてくれるのだと思ってもらうと分かり易いかと思います。Leaveはそのコントロールがフォームのアクティブコントロールでなくなった時に発生します。そのタイミングで行う処理を記述することで、Leaveイベントに対する処理が記述できます。(C#では、そのメソッドをイベントハンドラーと呼んでいます。)
Nana@$62702
textBox1から同じフォームの別のコントロールにフォーカスがうつった場合などにLeaveイベントが発生するので、それに対する処理を記述する訳です。

では、単にtextBox1.Textの内容をfolderBrowserDialog1.SelectedPathにセットすれば良いかというとそうではありません。

と言うのは、人間が人で入力するので、誤りが発生して、存在しないフォルダを指定しているかもしれないからです。

ここで、もう一点、こうは書きましたが、どこでエラー処理をするかがまた問題です。

今回は、下の画像を弄る際に、画像の保管フォルダーが必要なので、ここでチェックして、エラー対応しても良いのですが、もしかすると、エラーがあってもここでは気にせず、先に進んだ方が良いケースもあるかもしれません。

というのは、ここで記入した時には、フォルダが無かったけれど、エクスプローラーで別途作れば、実際に使う段では何も問題が無いかもしれない。

ということで、エラー対応は、実際に画像保管フォルダーを使う際にしようと思います。

つまり、textBox1からLeaveイベントが発生した時に、textBox1.Texが変更されていたらfolderBrowserDialog.SelectedPathにセットだけしておきます。

さ、ではLeaveのイベントハンドラを作りましょう。そこで、オレンジ色の矢印の様なものをクリックしてください。すると、下の図の様にイベントの一覧が表示されます。

C#入門 - ファイルの入出力とナビゲーション

textBox1を選択して、プロパティを観に行きます。

ここでLeaveの右をマウスでクリックして、選択します。そこで、マウスをダブルクリックしてみてください。

        private void textBox1_Leave(object sender, EventArgs e)
        {

        }

ここに、textBox1が変更されたら、textBox1.TextをfolderBrowserDialog.SelectedPathにセットする処理を書きます。

textBox1が変更されたかという情報をtextBox1.Modifiedが持っています。結局下の様なプログラムになります。

        private void textBox1_Leave(object sender, EventArgs e)
        {
            if (textBox1.Modified)
                folderBrowserDialog1.SelectedPath = textBox1.Text;
        }

画像の処理

さあ、画像の扱いに行きましょう。

なお、ここでは、今後もファイルの名前や拡張子とかの扱いが必要になるので、System.IOという名前空間を利用します。

コードの上の方にusingという記述が並んでいるところがありますので、そこに
using System.IO;
という1行を追加しておいてください。

すでに、画像表示については、「stopを押すまで画像の表示を繰り返す」で説明しています。
Image_folderに画像保管フォルダーのパス(ただし、最後「\」で終了する文字列)を設定して、ImageLocに画像ファイルの名前を設定していれば、pictureBox1.ImageLocationにImage_folder + ImageLoc;を設定することで、でpictureBox1に画像を表示できます。

また表示する際に、縦横比を保って、画像を拡大縮小して表示する指定も設定します。

pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
pictureBox1.ImageLocation = Image_folder + ImageLoc;


ここでは、画像ファイルをエクスプローラーからdrag&dropでpictureBox1に持ってくることで表示して、さらに、画像保管ファイルに格納してみましょう。

まず、PictureBoxに枠を表示しておきましょう。pictureBox1を選択して、BordeStyleプロパティにFixedSingleを設定しておきます。同様にpictureBox2〜pictureBox4にも設定すると、PictureBoxに枠が設定されます。

そして、Drag&Dropの処理の前に、PictureBoxをクリックしたら、画像格納フォルダを初期設定して、ファイルを選択をしたら、当該画像を表示する機能を付けましょう。その際に、ファイルの一覧を表示させてあげると便利なので、そういう機能を付けましょう。

ですので、デザイナー画面で、OpenFileDialogを4つ分配置します。(配置を1つで、上手く使い回しても良いのですが、余り頭を使わず、4つ配置しておきます。)

さあそれでは、pictureBox1をダブルクリックしましょう。

        private void pictureBox1_Click(object sender, EventArgs e)
        {

        }

PictureBoxクリック時の処理

まず、画像保管フォルダのパスをOpenFileDialogのInitialDirectoryにセットします。

おっと、ここで、画像保管フォルダのパスを使います。一応、パスが実際に存在しているかどうかのチェックをするかどうか決めましょう。

どういう考え方をするのかは、3つ位考えられます。

一つ目は、チェックをしないというものです。

チェックをしない場合、もしフォルダが無ければ、自動で、デフォルトのフォルダでファイルの一覧が表示されます。今回は画像の読み込みなので、仮にその段階で画像が無いことは分かるので、ファイルを探す際に、指定のフォルダに移動してもらえば済む話です。

もう一つは、チェックはして、実在しなければ、その旨を表示するものの、それ以降は、特に何もしないというものです。一応、アナウンスはしてあげたよ、あとはよろしくねという立場です。

最後は、フォルダが無いことを示したうえで、フォルダを作成するかどうかを確認し、作成すると答えたらフォルダ作成をしてもらうというものです。

ところで、今回は、ファイルを指定して画像を読み込むことが趣旨になります。であれば、フォルダを作るのは、あまり意味がないですよね。なので、3つ目の選択肢はないですよね。

一つ目と二つ目ですが、これはどちらでも良いのですが、使い慣れてくれば、間違ったフォルダを指定しているのは、言われなくても分かるので、個人的には、何もしないで良いかなと思います。

そして、表示した後、画像保管フォルダにSaveしておきます。

画像の保存には、PictureBoxの画像を格納しているImageプロパティのSaveメソッドを使います。Saveメソッドでは、保存ファイルのパスと画像の保存形式を指定します。

画像保管フォルダに格納する場合のパスは、画像保管フォルダのフォルダともともとの画像のパスであるopenFileDialog1.FileNameからPath.GetFileNameで取得したファイル名を結び付ければ作成可能です。

さらに保存形式は、ファイル名の拡張子部分で判断できます。

プログラムはこんな感じになりました。


        private void pictureBox1_Click(object sender, EventArgs e)
        {
            openFileDialog1.InitialDirectory = folderBrowserDialog1.SelectedPath;
            // Display the openFile dialog.
            DialogResult result = openFileDialog1.ShowDialog();

            // OK button was pressed.
            if (result == DialogResult.OK)
            {
                try
                {
                    // Output the requested file in pictureBox1.
                    pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
                    pictureBox1.ImageLocation = openFileDialog1.FileName;
                    // Save the image file 
                    string ext = Path.GetExtension(openFileDialog1.FileName);
                    string filename = folderBrowserDialog1.SelectedPath + @"\" + Path.GetFileName(openFileDialog1.FileName);
                    switch (ext.ToUpper())
                    {
                        case ".BMP":
                            // PictureBoxのイメージをGIF形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);
                            break;
                        case ".GIF":
                            // PictureBoxのイメージをGIF形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Gif);
                            break;
                        case ".JPG":
                            // PictureBoxのイメージをJPEG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                        case ".JPEG":
                            // PictureBoxのイメージをJPEG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                        case ".PNG":
                            // PictureBoxのイメージをPNG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
                            break;
                        default:
                            // PictureBoxのイメージをJPEG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                    }
                }
                catch (Exception exp)
                {
                    MessageBox.Show("次のエラーが発生しました。"
                                    + System.Environment.NewLine + exp.ToString() + System.Environment.NewLine);
                }
                Invalidate();
            }
            // Cancel button was pressed.
            else if (result == DialogResult.Cancel)
            {
                return;
            }
        }

一旦、これで全て保存して、実行してみます。

先に書いておくと、この状況で、pictureBox1をクリックすると、エラーになってしまいます。

ImageLocationプロパティで画像を表示しても、Imageには格納されていないので、SaveしようとしてもSaveするものが無くて例外が発生するようです。

そこで、画像の表示部分を、

pictureBox1.Image = Image.FromFile(fileName[0]);

に変更します。これで、表示された画像が保存されます。

維持用をまとめて、こんな感じになります。


        private void pictureBox1_Click(object sender, EventArgs e)
        {
            openFileDialog1.InitialDirectory = folderBrowserDialog1.SelectedPath;
            // Display the openFile dialog.
            DialogResult result = openFileDialog1.ShowDialog();

            // OK button was pressed.
            if (result == DialogResult.OK)
            {
                try
                {
                    // Output the requested file in pictureBox1.
                    pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
                    pictureBox1.Image = Image.FromFile(openFileDialog1.FileName);
                    // Save the image file 
                    string ext = Path.GetExtension(openFileDialog1.FileName);
                    string filename = folderBrowserDialog1.SelectedPath + @"\" + Path.GetFileName(openFileDialog1.FileName);
                    switch (ext.ToUpper())
                    {
                        case ".BMP":
                            // PictureBoxのイメージをGIF形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);
                            break;
                        case ".GIF":
                            // PictureBoxのイメージをGIF形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Gif);
                            break;
                        case ".JPG":
                            // PictureBoxのイメージをJPEG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                        case ".JPEG":
                            // PictureBoxのイメージをJPEG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                        case ".PNG":
                            // PictureBoxのイメージをPNG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
                            break;
                        default:
                            // PictureBoxのイメージをJPEG形式で保存する
                            pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                    }
                }
                catch (Exception exp)
                {
                    MessageBox.Show("次のエラーが発生しました。"
                                    + System.Environment.NewLine + exp.ToString() + System.Environment.NewLine);
                }
                Invalidate();
            }
            // Cancel button was pressed.
            else if (result == DialogResult.Cancel)
            {
                return;
            }
        }

同様に、pictureBox2〜pictureBox4も記述します。

なお、pictureBoxとOpenFileDialogは1〜4とありますが、画像保管フォルダは、folderBrowserDialog1一つだけなので、注意してください。


try catchについて

ここでtry 〜 catch 〜の部分について簡単に説明します。(厳密な説明はgoogleなどで探して読んでみてください)

処理をしていると、プログラムを実行した際に、エラーが発生する場合があります。例えば上のような事例です。そして、実行時にエラーが生じると、そこでプログラムが停止してしまいます。

そこで、本来の処理とエラーが発生した場合の処理を区分して記述出来る様にするのがtry catchです。

もう少し書くと、本来は、プログラムを書く時点では想定しにくい異常事態に対する処理を本来の処理とは区別してまとめるための仕組みで、そういう異常事態を例外と呼びます。

ただ、そういう例外以外にも、本来であれば、想定可能なものをエラーと呼ぶことがあります。エラーに対する処理は本来の処理の中に含めて記述するべきですが、中には、本来の処理に記述するとプログラムが分かりにくいものになる可能性もあります。そういうものを区別するためにも、try catdhを使って記述します。

書き方としては、

エラーの発生を確認したい箇所をtry{}で囲みます。そして、エラー処理をcatch (例外の型 引数){}で記述します。

例外の型は、.Net Frameworkであらかじめ多くのものが定義されています。

例えば、0で割った場合に発生するエラー、ファイルの内容を全て読んだ後にさらに読みこもうとした際に発生するエラー、ファイルを読もうとしたらフォルダに見つからなかった場合に発生するエラーなど、様々なものが定義されています。

なので、エラーの種類に応じて処理をまとめて、処理に関連して発生しうるエラーを列挙して記述するのが本来の形です。

try

本来の処理

catach (例外の種類1 ex)

例外の種類1に対する処理

catach (例外の種類2 ex)

例外の種類2に対する処理

...

ただ、今回は、すべての例外をまとめて、処理しています。


それではいよいよ、Drag&Dropの機能を設けていきましょう。

Drag&Dropの実装

ここでは、まず、エクスプローラーで画像ファイルをDrag&Dropされた時の、処理を実装していきます。

エクスプローラーで、画像ファイルをクリックし、マウスを押したまま、PictuBox1〜PictureBox4に移動してきて、そこでマウスをボタンを離したら、その画像を表示します。

考え方としては、ファイルがPictuBoxにDragされてエリア内に来た時に、Drag&Dropを受け入れるかどうかを決まます。

そして、実際にマウスのボタンが離されてファイルがDropされたら、ファイル名を取得して、PictureBoxに表示します。

Drag&Dropを有効にする場合には、AllowDropプロパティをtrueに設定して、Drag&Dropを受け入れる設定にします。ただ、ここで問題が発生します。それは、デザイナー画面でpictureBox1のプロパティにAllowDropプロパティが表示されないのです。また、プログラムのコード入力の画面でも、pictureBox1.まで入力してもAllowBoxプロパティが表示されません。

つまり、今のままでは、PictureBoxにDrag&Dropは出来無い様に見えます。まあ、あ〜、ダメなんだで終わるのも良いと思います。でも、もう少しあがいてみましょうか。

Googleで「pictureBox.AllowDrop」と検索すると一番上にMicrosoftのドキュメントのサイトが表示されます。そこをクリックすると、

PictureBox.AllowDrop プロパティ

という名前が表示されます。要は、プロパティはある様です。

なので、pictureBox1.AllowDrop = true;というステートメントが書けそうです。

Drag&Dropが出来るよという宣言なので、プログラムが起動してフォームが表示されたところで、設定すれば良いというのは分かりますよね。
そこで、プログラムが起動してformが表示される際に呼ばれる処理で、記載しましょう。

それが、プログラムコードが書かれている画面の上の方にあるForm1()というメソッドです。

        public Form1()
        {
            InitializeComponent();
            pictureBox1.AllowDrop = true;
            pictureBox2.AllowDrop = true;
            pictureBox3.AllowDrop = true;
            pictureBox4.AllowDrop = true;
        }

としておきます。

ところで、今はAllowDropについて検索しましたが、何か処理をしたい場合には、Googleで検索してみることをお勧めします。

例えば、今回のPictureBoxのDrag&Dropも「c# picturebox drag drop」などとして検索してみると、様々な情報が検索されます。

一点、こういうDrag&Dropなどは、今回はエクスプローラなどからpictureBoxにDrag&Dropする処理を実装します。ただ、上の様な検索をすると、PictureBoxからDrag&Dropする例も出てきます。

こういう方向が重要な意味を持つ場合は、常にどちらからどちらへの話なのかを意識して情報収集してください。

まず、ファイルがPictuBoxにDragされてエリア内に来た時に、Drag&Dropを受け入れる処理を書きます。

pictureBox1を選択して、プロパティでイベントを選んだ上で、DragEnterの右側をマウスでダブルクリックします。

        private void pictureBox1_DragEnter(object sender, DragEventArgs e)
        {
            //ドラッグされているデータがファイルか調べ、
            //そうであればドロップ効果をMoveにする
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;
            else
                //string型でなければ受け入れない
                e.Effect = DragDropEffects.None;
        }

Microsoftのドキュメントなどにも記載例がありますので、これは、このまま覚えちゃってください。

同様にマウスを離した際の処理を記載します。
Dragする際には、一般には複数のファイルを選択できるので、システム上は、複数のファイル名が渡されてきます。(今回は1個しか使いませんが)

Dragされた時の情報は、DragEventArgs eに格納されています。そこから下記の様にしてFile名を獲得します。
string[] fileName = (string[])e.Data.GetData(DataFormats.FileDrop, false);

後は、pictureBox1をクリックした際と同様に、PictureBox1にファイルを読み込んで画像を表示して、さらに、画像保管フォルダに格納します。

DragEnterと同様にpictureBox1を選択して、プロパティでイベントを選んだ上で、DragDropの右側をマウスでダブルクリックして、プログラムを記述していきます。

結果は下記の様になります。

        private void pictureBox1_DragDrop(object sender, DragEventArgs e)
        {
            try
            {
                string[] fileName = (string[]) e.Data.GetData(DataFormats.FileDrop, false);
                pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
                pictureBox1.Image = Image.FromFile(fileName[0]);

                // Save the image file 
                string ext = Path.GetExtension(fileName[0]);
                string filename = folderBrowserDialog1.SelectedPath + @"\" + Path.GetFileName(fileName[0]);
                switch (ext.ToUpper())
                {
                    case ".BMP":
                        // PictureBoxのイメージをGIF形式で保存する
                        pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);
                        break;
                    case ".GIF":
                        // PictureBoxのイメージをGIF形式で保存する
                        pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Gif);
                        break;
                    case ".JPG":
                        // PictureBoxのイメージをJPEG形式で保存する
                        pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                        break;
                    case ".JPEG":
                        // PictureBoxのイメージをJPEG形式で保存する
                        pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                        break;
                    case ".PNG":
                        // PictureBoxのイメージをPNG形式で保存する
                        pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
                        break;
                    default:
                        // PictureBoxのイメージをJPEG形式で保存する
                        pictureBox1.Image.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
                        break;
                }
            }
            catch (Exception exp)
            {
                MessageBox.Show("次のエラーが発生しました。"
                                + System.Environment.NewLine + exp.ToString() + System.Environment.NewLine);
            }
            Invalidate();
        }
        private void pictureBox2_DragEnter(object sender, DragEventArgs e)
        {
            //ドラッグされているデータがFileかどうかを調べ、
            //そうであればドロップ効果をCopyにする
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;
            else
                //Copy型でなければ受け入れない
                e.Effect = DragDropEffects.None;
        }

pictureBox1と同様にpictureBox2〜pictureBox4についてもDragEnterとDragDropにプログラムを記述します。

以上で、画像の処理の大きな部分は終了です。

残っているのは、Tweet記事を補完する際に、1行目に画像ファイルの名前を,(カンマ)で区分して記入することです。こちらは、Tweet記事作成部分で説明します。

ナビゲーションの詳細仕様決定

ここからは、現在一番下にあるナビゲーションで実現したいことを、実現できる出来ないかは置いておいて列挙してみます。

ナビゲーションボタンは順に、先頭ボタン、左ボタン、数字欄、右ボタン、最終ボタン、追加ボタンと呼ぶことにします。さらに、一番のボタンは保存ボタンと呼びます。

なお、前提として記事保管ファイル・BASEのどちらかが指定されていない場合は、記事ファイルの読み書きはしません。

以下、記事保管フォルダとBASEを指定した状態として、"BASE"xxxxはBASEに指定した文字の後ろに4桁の数字がセットされたファイル名を示すことにします。

1.フォルダ内にあるBASEで始まるファイルを読みだして、BASEに続く連番の最大値+1の数値で新しい記事を作成する。

2.ナビゲーションの真ん中のテキストに4桁の数字を入力したらBASEの名前+4桁の数字をファイル名としてファイルを読み込んで、記事欄に表示する。
  ファイルが存在しない場合は、記事欄の内容でファイルを作成する。(記事欄空白の場合は、まだ新規ファイルは作らない)

3.各種ナビゲートボタンの機能
@先頭ボタンを押したら、"BASE"0001のファイルを表示する。

A左ボタンを押したら、記事が書かれていたら現在の数字のファイルに記事を保存する。そして現在の数字-1を数字欄に表示して、該当するファイルを読み込んで表示する。ただし、現在の数字が0001の場合は、それ以上なにもしない。

B数字欄に数字を入力したら、まず記事が書かれていたら現在の数字のファイルに記事を保存する。ただし数字欄が空白だった場合は、記事を保存しない。そして"BASE"数字のファイルを読み込んで表示する。もし、ファイルが無ければ、新規のファイルの名前として、それ以降、記事を書いたら"BASE"4桁の数字ファイルに保存する。

C右のボタンを押したら、記事が書かれていたら現在の数字のファイルに記事を保存する。そして現在の数字1を数字欄に表示して、該当するファイルを読み込んで表示する。ただし、フォルダに該当するファイルが無ければ、新規扱いとするが、記事を書かない限り、それ以上右ボタンを押しても何もしない。

D追加ボタンを押したら、記事が書かれていたら現在の数字のファイルに記事を保存する。フォルダ内にあるBASEで始まるファイルを読みだして、BASEに続く連番の最大値+1の数値で新しい記事を作成する。ただし、記事が更新されていない場合は、何もしない。

記事欄に何か記入されていた場合に、ナビゲートボタンで数字が変わるような変更があれば、現在の数字でファイルを作成して、記事を格納する。記事欄が空白の場合はファイルには何もしない。なお、ファイルが既に存在して、その記事を表示した上で、記事が空白にされた場合は、現時点では何もしないが、そのファイルを削除するか、空白にした記事を作成するかどうかは要検討。

E保存ボタン

明示的に現在の数字欄のファイルに保存することを示します。


4.新規の扱い
数字が新規のファイルだった場合に、ファイルをいつ作るのか?本来は、記事が書かれていたらだけれど、扱いが結構大変そう。

新規ファイルと認識した段階でファイルを作ると、処理は簡単そうだが、記事に何も書かれていないファイルが沢山出来てしまい、右ボタンや新規ボタンがどんどん押せてしまわ無い様にしないといけない(本当にいけないか?)。

基本はいちいち保存だとか指示しなくても、自動で記事を保存してくれて、さらに、ファイル間を簡単に移動できるようにしたい。

記事が空白の場合は、基本的にファイルは作らないこととします。ただ、空白でも作りたい時は、保存ボタンを使うことにします。

また、上に書いたすでに存在しているファイルで、記事を空白にした場合は、(画像ファイルのパスが1行目に記述されている可能性はあるけれど)記事部分が空白のファイルを作成することとします。

ここでは作りませんが、こういう、ボタンが押されることによって、状態が変わっていく場合は、状態遷移図を書いてみると状況がスッキリします。

ナビゲーションの実装-プログラム仕様作成

それでは、実際にナビゲーションボタンの処理を考えていきます。まさにアルゴリズムを検討する部分でもあります。表現の仕方は様々ですが、いわゆる詳細仕様書(内部設計書)の中でもプログラム設計書と呼ばれる部分を記載することと同じ作業になります。

ナビゲーションの仕様から、前提として、処理しようとしているファイルの番号を現在ファイル番号と呼ぶことにします。また、フォルダ内のファイルの番号部分の最大値をフォルダ内最大番号と呼ぶことにします。さらに、フォルダ内最大番号+1を新規ファイル番号と呼ぶことにします。現在ファイル番号は、ナビゲートボタンの数字欄に表示されます。

また、記事を記載するリッチテキストボックスの内容を記事欄と呼びます。

例えば、プログラムを起動した段階で、記事欄は空白です。仮にその後、記事保管フォルダとBASEが設定されて、記事が書かれた場合には、"BASE"0001に記事を格納しますので、現在ファイル番号は0001です。また記事保管フォルダにはまだ何も設定がされていないので、フォルダ内最大番号は0000です。

記事保管フォルダが設定されて、BASEも設定された場合に、フォルダ内に、"BASE"0001〜"BASE"0100まで記事が格納されていた場合は、上の1.新規ファイル番号をフォルダ内最大番号+1にして新しい記事を作成するので、フォルダ内最大番号は0100、新規フォルダ番号と現在ファイル番号は0101になります。

先頭ボタンの処理

先頭ボタンを押された場合の処理を考えます。
ボタン処理の共通的な仕様として、記事保管ファイルかBASEのどちらかが指定されていない場合は、何もしません。

0)記事保管ファイル・BASEが指定されて、フォルダ内に"BASE"で始まるファイルが無い時
この場合、現在ファイル番号、新規ファイル番号が0001の時です。
既に先頭にいますから、特に変更はしません。


1)現在ファイル番号が、新規ファイル番号の場合(新しい記事を作っているケース)
この場合は、処理が二つに分かれます。

@記事欄が空白であれば、新規ファイル番号のファイルは作らず、"BASE"0001のファイルを読み込み、フォルダ内最大番号と新規ファイル番号は変更せず、現在ファイル番号を0001にします。

A記事欄が空白でなければ、新規ファイル番号のファイルは作った上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を+1します。そして、"BASE"0001のファイルを読み込み、現在ファイル番号を0001にします。

ここで、一つ疑問が生じるかもしれません。それは、記事を何か書いて、その後に記事を消した場合はどうなるのかと。

一旦記事を書いても、特にほかのナビゲーションボタンを押さなければ、ファイルが作られることは無いので、その状態で記事欄が空白にされたのであれば、上の空白の状況と同じと考えても良いですよね。

もし、記事を書いた状態で「保存」ボタンを押したらと言われるかもしれませんが、その場合は、「保存」を明示的に押しているので、保存ボタンの実装で記載しますが、現在ファイル番号は変わりませんが、新規ファイル番号が+1されているので、現在ファイル番号が、新規ファイル番号の場合ではなくなっています。

2)現在ファイル番号が新規ファイル番号より小さい場合
現在ファイル番号のファイルに記事を格納します。そして、フォルダ内最大番号と新規ファイル番号は変更せず、"BASE"0001のファイルを読み込み、現在ファイル番号を0001にします。

【練習問題】
と、書いてきましたが、上の内容には一つ(ではないかも)大きな致命的な欠陥があります。その内容と、その欠陥を回避する処理を記述しなさい。


前ボタンの処理

前ボタンを押された場合の処理を考えます。

0)記事保管ファイル・BASEが指定されて、フォルダ内に"BASE"で始まるファイルが無い時
既に先頭にいますから、前は無く特に変更はしません。

1)現在ファイル番号が、新規ファイル番号の場合(新しい記事を作っているケース)
この場合は、処理が二つに分かれます。

@記事欄が空白であれば、新規ファイル番号のファイルは作らず、現在ファイル番号-1(一つ前)のファイルを読み込み、フォルダ内最大番号と新規ファイル番号は変更せず、現在ファイル番号を現在ファイル番号-1にします。

A記事欄が空白でなければ、新規ファイル番号のファイルは作った上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を+1します。そして、現在ファイル番号-1(一つ前)のファイルを読み込み、現在ファイル番号を現在ファイル番号-1にします。

2)現在ファイル番号が新規ファイル番号より小さい場合
現在ファイル番号のファイルに記事を格納します。そして、フォルダ内最大番号と新規ファイル番号は変更せず、現在ファイル番号-1(一つ前)のファイルを読み込み、現在ファイル番号を現在ファイル番号-1にします。

【練習問題】
と、書いてきましたが、ここにも一つ(ではないかも)大きな致命的な欠陥があります。その内容と、その欠陥を回避する処理を記述しなさい。

数字欄の処理

実は、ここで今回、少し日和ます。

本来なら、数字を入力して、その入力された数字のファイルを表示するということを考えたのですが、数字が入力されたかどうかをどう判断するかが実は難しい。
テキストボックスだけだとにゅうう力が終わったかどうかが分からないんです。

例えば、4桁入力したら終わりと見なすとします。そうするとどんな問題があるか考えてみます。

例えば簡単に思いつくのが、1000と入力して、あ、0100だと思い、入れなおす様な場合です。0100まで詩かファイルがない場合、1000と入力した段階で入力終了だとみなすと、ファイルが存在しないことになります。じゃあ、エラー処理するのか?それとも新規にするのか?さらには、次の0100と入力された段階で再度処理をする訳です。

今は、極端に間違えた例ですが、0011を0010と入力して0013と入力して0012と入力した場合、仮に全てファイルがあったら、数字が4桁位なるたびにファイルを読み込むのか?

色々と検討しないといけません。さらには、入力されたものが数字ではなく文字だったらどうするのか等々、いろいろと検討すべき事項が出てきます。

そこで、冒頭の日和るという話になります。まず、入力を完了したかどうかは人間に判断してもらいます。そこで、数字の入力の横にもう一つボタンを追加します。
ボタンのTextには移動と書いておきます。

また、数字以外の入力を未然に防ぐために、テキストボックスではなく数字だけ入力可能なコントロールに変更します。

こんな感じです。

C#入門 - ファイルの入出力とナビゲーション


そこで、数字を入力して、移動をボタンをクリックしたら、その数字のファイルを表示することにします。

処理を考えてみます。処理は移動ボタンをクリックしたところに記述します。

0)記事保管ファイル・BASEが指定されて、フォルダ内に"BASE"で始まるファイルが無い時
@記事欄が空白であれば、数字欄を1に戻して、何もしません。

A記事欄が空白でなければ、"BASE"0001のファイルを作った上で、フォルダ内最大番号に新規ファイル番号を設定し、新規ファイル番号を+1します。
そして、
@.数字欄が1の場合は、新規ファイル番号のファイルを作って記事内容を格納した上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を+1します。記事欄はそのままで現在ファイル番号もそのままにしておきます。(現在ファイル番号でファイルを作成するけれども、操作画面の見た目はそのまま。ただし、ファイルは作成しているので、新規ファイル番号は+1された状態。)

A.数字欄が1より大きい場合は、新規ファイル番号のファイルは作った上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を2にします。そして、記事欄を空白にして現在ファイル番号に新規ファイル番号を設定します。

1)現在ファイル番号が、新規ファイル番号の場合(新しい記事を作っているケース)
この場合は、処理が大きく3つに分かれます。
それは、設定した数字欄の数字が現在ファイル番号より大きい数字か、等しい数字か、小さい数字の場合です。

@数字欄の数字が現在ファイル番号より大きい数字の場合
@.記事欄が空白であれば、数字欄を現在ファイル番号に戻して、何もしません。(数字欄をもとに戻す必要があるので、入力された現在ファイル番号とは別に元の現在ファイル番号が必要になることが分かります。)

A.記事欄が空白でなければ、新規ファイル番号のファイルは作った上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を+1します。そして、記事欄を空白にして現在ファイル番号に新規ファイル番号を設定します。

A数字欄の数字が現在ファイル番号と等しい場合
@.記事欄が空白であれば、何もしません。

A.記事欄が空白でなければ、新規ファイル番号のファイルを作って記事内容を格納した上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を+1します。記事欄はそのままで現在ファイル番号もそのままにしておきます。(現在ファイル番号でファイルを作成するけれども、操作画面の見た目はそのまま。ただし、ファイルは作成しているので、新規ファイル番号は+1された状態。)

B数字欄の数字が現在ファイル番号より小さい数字の場合
@.記事欄が空白であれば、新規ファイル番号のファイルは作りません。そして現在ファイル番号のファイルを読み込み、フォルダ内最大番号と新規ファイル番号は変更せず、現在ファイル番号を入力された現在ファイル番号にします。

A.記事欄が空白でなければ、新規ファイル番号のファイルを作って記事内容を格納した上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を+1します。そして入力された新しい現在ファイル番号のファイルを読み込み、現在ファイル番号を新しい現在ファイル番号にします。


2)現在ファイル番号が新規ファイル番号より小さい場合
現在ファイル番号のファイルに記事を格納します。そして、フォルダ内最大番号と新規ファイル番号は変更せず、入力された新しい現在ファイル番号のファイルを読み込み、現在ファイル番号を新しい現在ファイル番号にします。

【練習問題】
と、書いてきましたが、ここにも一つ(ではないかも)大きな致命的な欠陥があります。その内容と、その欠陥を回避する処理を記述しなさい。

こうやって、いろいろなケースを考えて、それぞれのケースに応じた処理を組んでいきます。このケース分けに漏れがあると、思ったような動作がされない場合が出てきます。ただ、障害が発生しても、場合によっては、どういう条件でそういう問題が発生するのかが、判明せず、再現を待つような場合が発生します。

それは、今回の上の実装の流れを見ても、単にこれだけのシステムなのに、これだけのパターンを考えなければいけないことからも想像可能だと思います。

次ボタンの処理

次ボタンを押された場合の処理を考えます。

0)記事保管ファイル・BASEが指定されて、フォルダ内に"BASE"で始まるファイルが無い時
この場合、何度も書いているように、現在ファイル番号、新規ファイル番号が0001の時です。

@記事欄が空白であれば、何もしません。

A記事欄が空白でなければ、"BASE"0001のファイルを作った上で、フォルダ内最大番号に新規ファイル番号を設定し、新規ファイル番号を+1します。そして、記事欄を空白にして、現在ファイル番号に新規ファイル番号を設定します。

1)現在ファイル番号が、新規ファイル番号の場合(新しい記事を作っているケース)
@記事欄が空白であれば、何もしません。

A記事欄が空白でなければ、新規ファイル番号のファイルを作って記事内容を格納した上で、フォルダ内最大番号に新規ファイル番号を設定し、新規ファイル番号を+1します。そして、記事欄を空白にして、現在ファイル番号に新規ファイル番号を設定します。

2)現在ファイル番号が新規ファイル番号より小さい場合
現在ファイル番号のファイルに記事を格納します。そして、フォルダ内最大番号と新規ファイル番号は変更せず、
@現在ファイル番号+1(一つ後)が新規ファイル番号に等しければ、現在ファイル番号+1のファイルは存在しないので、記事欄を空白にして、現在ファイル番号を現在ファイル番号+1にします。

A現在ファイル番号+1(一つ後)が新規ファイル番号より小さければ、現在ファイル番号+1のファイルを読み込み、現在ファイル番号を現在ファイル番号+1にします。

【練習問題】
と、書いてきましたが、ここにも一つ(ではないかも)大きな致命的な欠陥があります。その内容と、その欠陥を回避する処理を記述しなさい。

最後ボタンの処理

最後ボタンを押された場合の処理を考えます。

0)記事保管ファイル・BASEが指定されて、フォルダ内に"BASE"で始まるファイルが無い時
この場合、何度も書いているように、現在ファイル番号、新規ファイル番号が0001の時です。

@記事欄が空白であれば、何もしません。

A記事欄が空白でなければ、新規ファイル番号のファイル("BASE"0001)を作って記事内容を格納した上で、フォルダ内最大番号にその時点の新規ファイル番号を格納し、新規ファイル番号を+1します。記事欄はそのままで現在ファイル番号もそのままにしておきます。(現在ファイル番号でファイルを作成するけれども、操作画面の見た目はそのまま。ただし、ファイルは作成しているので、新規ファイル番号は+1された状態。)


1)現在ファイル番号が、新規ファイル番号の場合(新しい記事を作っているケース)
@記事欄が空白であれば、フォルダ内最大番号のファイルを読み込み、記事欄にその内容を表示します。そして、現在ファイル番号にフォルダ内最大ファイル番号を設定します。

A記事欄が空白でなければ、新規ファイル番号のファイルを作って記事内容を格納した上で、フォルダ内最大番号に新規ファイル番号を設定し、新規ファイル番号を+1します。そして、記事欄はそのままで現在ファイル番号もそのままにしておきます。(現在ファイル番号でファイルを作成するけれども、操作画面の見た目はそのまま。ただし、ファイルは作成しているので、新規ファイル番号は+1された状態。)

2)現在ファイル番号が新規ファイル番号より小さい場合
現在ファイル番号のファイルに記事を格納します。そして、フォルダ内最大番号と新規ファイル番号は変更せず、
フォルダ内最大ファイル番号のファイルを読み込み、現在ファイル番号にフォルダ内最大ファイル番号を設定します。

【練習問題】
と、書いてきましたが、ここにも一つ(ではないかも)大きな致命的な欠陥があります。その内容と、その欠陥を回避する処理を記述しなさい。

追加ボタンの処理

追加ボタンを押された場合の処理を考えます。

0)記事保管ファイル・BASEが指定されて、フォルダ内に"BASE"で始まるファイルが無い時
この場合、何度も書いているように、現在ファイル番号、新規ファイル番号が0001の時です。

@記事欄が空白であれば、何もしません。

A記事欄が空白でなければ、"BASE"0001のファイルを作った上で、フォルダ内最大番号に新規ファイル番号を設定し、新規ファイル番号を+1します。そして、記事欄を空白にして、現在ファイル番号に新規ファイル番号を設定します。

そうでなければ、処理はいくつかのケースに分かれます。

1)現在ファイル番号が、新規ファイル番号の場合(新しい記事を作っているケース)
@記事欄が空白であれば、何もしません。

A記事欄が空白でなければ、新規ファイル番号のファイルを作って記事内容を格納した上で、フォルダ内最大番号に新規ファイル番号を設定し、新規ファイル番号を+1します。そして、記事欄を空白にして、現在ファイル番号に新規ファイル番号を設定します。

2)現在ファイル番号が新規ファイル番号より小さい場合
現在ファイル番号のファイルに記事を格納します。そして、フォルダ内最大番号と新規ファイル番号は変更せず、記事欄を空白にして、現在ファイル番号を新規ファイル番号にします。

【練習問題】
と、書いてきましたが、ここにも一つ(ではないかも)大きな致命的な欠陥があります。その内容と、その欠陥を回避する処理を記述しなさい。

保存ボタンの処理

現在ファイル番号のファイルに記事内容を格納した上で、
現在ファイル番号が新規ファイル番号に等しい場合は、フォルダ内最大番号に新規ファイル番号を設定し、新規ファイル番号を+1します。そして、記事欄はそのままで現在ファイル番号もそのままにしておきます。(現在ファイル番号でファイルを作成するけれども、操作画面の見た目はそのまま。ただし、ファイルは作成しているので、新規ファイル番号は+1された状態。)
そうでない場合は、何もしない。

空白でも明示的に保存ボタンを押すことで、ファイルを作成します。

以上でナビゲーションボタン関連の処理の検討は終了です。(本当はまだ、検討すべき事項があります。)

練習問題にも関連しますが、現時点では、フォルダには、必ず連番でファイルが格納されていることを前提にします。なので、連番になっていなかった場合のエラーはプログラムが処理の途中でストップするという仕様にします。

雑感

ここまで読んでみて、如何ですか?

あ〜、面倒だと思われた方は、正直です。ただ、職業プログラマーは、こういう文章を読んで、プログラムを組んでいくのが仕事になります。(普通はもう少し高いレベルの抽象化がされた仕様を書きます。と言いつつ、現場で作られているのは、なんちゃって仕様ばかりというと語弊があるか。たいていは詳細設計と言いながら大きな流れしか記述されていない仕様書が多い。UML図と言いつつ、組織ごとにローカライズされたなんちゃっても多い。UMLがそもそもなんちゃっての集まりかもしれないけれど)

また、プログラム設計をするSEは、与えられた仕様から、上の様にアルゴリズムを考えていきます。

今回こういう形で公開してますが、プログラム設計書としては、かなりプログラムに近い(低)レベルの仕様を記述しています。さらにはプログラム内で使用するであろう変数にまで言及していたり、ほぼプログラムレベルの記載内容ですが、その様な記述を行っている意図まである程度詳細に記載しました。

普通は、ここまで記載しないと思います。特にここまで詳細に書くと、プログラム開発中にアルゴリズムの誤りがあった場合は、ここの文章を修正して正しくメンテナンスすべきですが、プログラミングのステージに移ると、通常はプログラム設計書のメンテナンスは余り行われないのが実情です。そこで、ある程度のアルゴリズムの選択に幅を持たせて、アルゴリズムを少々変更しても仕様書の記載の修正にならないレベルで書いていくのが通常です。(それでも、プログラミング中に設計書の変更が必要な場合が多く発生しますが、設計書に遡って修正している運用は実質されていないのではないかと思います。少なくとも私が現役で開発をしていた頃は無かった。)

そう書くと正しくなくて、内部統制監査が始まって、プログラム設計書のメンテナンスを違う形で行うようになりました。トラブルが発生した場合には、プログラムの修正段階で直接設計書を直すのではなく、修正指示書の様なものを作成し、プログラムのどこをどのように修正するのかを文書化しています。

内部統制監査上は通常はそれで良しとしていますが、実際には修正指示書が大量に発生して、しかも、トラブルの発生順に綴じられているので、じゃあ、今は何が正しい状態なのかを設計書と修正指示書から再現するのはほぼ不可能です。結局システムを調べるのはプログラムを直接追うことになっているのが実情です。

もっと書くと、修正指示書による運用は、本番稼働した後に行われます。問題は、プログラム設計後に、プログラミングからテストの過程で大量の修正が発生します。それらは、プログラム上にコメントとして残されることもありますが、残されない場合もあります。(修正コメントが全て残されているかどうかは通常検査されません。)
なので、そもそも修正指示書の修正前の内容がプログラム設計書とずれています。(形式的に、プログラム完了した時点でプログラム設計をきちんと修正したことにしている会社も多いですが。あくまで御形式的にです。本当にきちんと書き直している所は...)

また、プログラム設計書をプログラム並みに細かく書いて、プログラミング工程は、プログラム設計書から行うことが行われることがありました。しかし、それはいきなりプログラムを書くことと変わりなく、アルゴリズムの検討を行うことにはならないので、余り採用されませんでした。

強いて言えば、上に書いた様なプログラム設計書の記載を、そのままコメントとしてプログラムに取り込んでおく。そして、プログラムを修正する際にもコメントをきちんとメンテナンスするというのが、現状ではないでしょうか。

最後に、自分でシステムを作ろうという場合、この程度のシステムでも、上の様に、(仕様書として残すかどうかは別として)処理をきちんと考えてからプログラミングをした方が良いです。逆に、上の様に、ケース分けして処理を考えることが出来ないと、少し複雑なシステムになると思うようなプログラムは組めません。


上では書きませんでしたが、いくつかフォルダ中のファイルや画面の状態とボタンによる遷移の図を参考として挙げておきます。なお、画像格納フォルダ、記事格納フォルダ、BASEは指定されている前提です。一番左の状態でボタンがクリックされたらどういう状態に変わるかを矢印で表現しています。

【参考例】
1.まだ記事は格納されておらず、初期画面が表示されている状態

C#入門 - ファイルの入出力とナビゲーション


2.まだ記事は格納されておらず、初期画面で記事が入力されている状態

C#入門 - ファイルの入出力とナビゲーション


3.記事が一つだけは格納されていて、その記事を編集している状態

C#入門 - ファイルの入出力とナビゲーション


4.記事が一つだけは格納されていて、次の記事が空白で表示されている状態

C#入門 - ファイルの入出力とナビゲーション


5.記事が一つだけは格納されていて、次の記事に記事が入力されている状態

C#入門 - ファイルの入出力とナビゲーション



【練習問題】1
上記のそれぞれの矢印で遷移した結果、現在ファイル番号、新規ファイル番号、フォルダ内最大ファイル番号はいくつになるか?

【練習問題】2
上記の5.では一番左側の状態から、先頭、前、最後のボタンでBASE0001、BASE0002の二つのファイルが格納された状態になるが、画面で表示されるのはどのファイルか、それぞれ答えなさい。

【練習問題】3
Tweet格納フォルダに2つのファイルが格納されていrうばあ、さらに3つ以上格納されている場合の遷移図を記述しなさい。

ナビゲーションの実装-プログラミング

さて、ここからナビゲーションをプログラミングしていきます。

ただ、仕様をきちんと書いているので、特に大きな問題になる箇所は無いかなと想定しています。

と書いてから、自分でプログラムを組んでみました。しかも、テストの前に一通り動かしてみると、なかなかうまく動かない状況が発生したり、また仕様のミスも結構ありました。

あ、仕様のミスは、修正しません。実際に、仕様をヒントに作ってみてください。そうすることで、何が問題なのかも分かると思います。

一点だけ伝えておくと、画像については、やはりいろいろと有りました。

自分でやってみると分かったというのが、PictureBoxでLoadFileでイメージを読み込むと、イメージがロックされてしまうと言うこと。

別のフォルダからイメージを取ってくる分には良いのだけれど、画像保管フォルダからイメージを取ってくると、他のファイルに移動する際に、イメージを上書きすることになり、ロックされているためにエラーが発生します。

画像保管フォルダから入力した場合は出力は行わないなど、いくつか回避策は考えられます。

出来れば、一通り自分自身でプログラムを書いてみて動かしてみてください。そして、躓いた時はGoogleで調べてみてください。実に多くの方が、同じような問題で悩んでいて、先人が様々な記録を残してくれています。


さて、稼働イメージはこんな感じです。(コントロールも少し変更しています。)

C#入門 - ファイルの入出力とナビゲーション


C#入門 - ファイルの入出力とナビゲーション


C#入門 - ファイルの入出力とナビゲーション


C#入門 - ファイルの入出力とナビゲーション


C#入門 - ファイルの入出力とナビゲーション



なお、いずれプログラム自体は、Githubにでも公開しようと思います。

以上で、C#入門編は終了です。

ここまで、実際に、Visual Studioを使って、実践してくれていれば、 おそらく、Windows Formのプログラムなら、ある程度は作れる様になっているかと思います。

ただ、おそらく、ここまでの入門編で、毎回90分の授業で、半年分位の分量にはなっていると思うので、さっと流し読みしてもプログラムを作るのは難しいとは伝えておきますね。

無料なので、実際にVisual Studio動かしてみてください。

テストについて

このサイトでは、テストについては余り触れてきませんでした。

というのは、ここまではそkまでテストしなければ分からないようなシステムを組んでいなかったからです。

ただ、今回のシステムはそれなりのコンポーネントがあります。

そこで、最低でも1回は、そのコンポーネントをクリックなり入力なりして、上手く動作するかどうかをチェックするのは当然です。

問題になるのは、ボタンを順番に押した場合にもきちんと動くかどうかという点です。

Windowsのシステムの場合は、通常複数のボタンがあり、基本どんな順番に押されるかはユーザー次第なので、どういう順番で押されても上手く機能することがことが求められます。(機能のうちには、この状況ではこのボタンは押せませんと言ったエラーを表示するような機能も含めてです。)

今回のTweet記事作成でも、ナビゲートボタン部分をとっても、6個のボタンとニューメリックの入力エリアがあります。

それらを順番に押すだけでも、かなりの組み合わせケースが考えられます。

さらに、記事格納フォルダ内の記事が無い状態(で、まっさらな状態と新しい記事を書いている途中の状態)、1つある状態(1つ目を読んでいるのか、2つ目の記事でまっさらな状態か、記事を書いている途中か)、記事が複数ある状態(さらに新しい記事がどういう状況か)、それぞれの状態で、上記のケースがきちんと動作するか確認する必要があります。

単純に考えても、ボタン遷移のケースが40ケース以上あって、状態も14〜5分類位できそうです。なので、最低でも560パターンのテストが必要です。

やるやらないは別として、Windowsのプログラムのテストが大変になる一つの理由は、1画面でさえ、ボタンの数により複数かつ大量の状態遷移が発生するからです。

その状態遷移を一つ一つ洗い出して、テストするのは、大変です。(もちろん、パターンが違っても、内部の処理は同じケースが多いので、そこまでプログラムは細分化されません。かといって、プログラムにテストケースを合わせるのは、危ないです。というのは、そもそも仕様で遷移のパターンを見落としているかもしれないからです。

COBOLの時代は、基本的に、画面上の上から下へ順番にしか項目の入力が行われなかったので、こういう状態の遷移は余り考慮されていません。しかし、Windowsではその点が全く異なります。

ただ、内部的には処理の前提となる条件がグループ化されるので、別の遷移でもプログラム内では同じ処理を行うことも多くなります。そのため、ある程度、個々のコントロールのテストが出来ていれば、多くの遷移が上手く動く可能性が高いです。そこで、、すべての遷移を全てたどるテストはなるべく自動化して、結果が想定と異なる部分を洗い出して、修正する方法が取られたりします。

【状態遷移のイメージ】

C#入門 - ファイルの入出力とナビゲーション

共通モジュール

今回のナビゲートボタンの処理に関連して、「モジュール分割」と言うことで少し書いておきます。

上にも書いた様に、プログラミング設計や実際のプログラミングを行う場合は、少しのパラメータが違うだけで同じような処理や、全く同じ処理が何度も現れてきます。

そう言ったものは、一つの独立した処理にして置きます。

例えば、ナビゲートボタンの処理では、新規に記事を何行か作成している状態で「次」のボタンや数字欄にフォルダ内最大ファイル番号より大きな数字を入力して「移動」ボタンを押したり、「追加」ボタンを押したりする場面で「新規の画面を表示する」状況が頻繁に発生します。

その際、リッチテキストボックスを空白にして、画像も4つともクリアにします。

なので、その処理に、「画面初期化」(initialscreen)とかいう名前を付けて、仕様上では、リッチテキストボックスを空白にして、画像も4つともクリアにするという表現は行わず、「画面初期化」を呼び出すと言った表現にしておきます。

このケースでは、そこまで簡潔になりませんが、処理として何十行にもわたるような共通の処理があったとすると、それが1行で表現できるので、見通しも良くなります。

こう言った共通の処理を、昔のコンピュータ言語では、共通ルーチン、共通モジュールなどと呼びました。また、そういう共通的な処理の塊に分割していくことをモジュール分割と呼んでいます。C#では、共通ルーチンとは言わずにメソッドと言います。そう、今までコントロールで使っていたメソッドと同じです。

今回の入門編では、メソッドの細かな文法を取り上げませんでしたが、コントロールに用意されているメソッドを使うだけでなく、独自のメソッドを作ることが出来ます。

例えば、上記の「画面初期化」ではメソッドの名前をinitialscreenと仮にしておきます。

その場合は、メソッドを一度下記の様に定義すると、

private void initialscreen()
{
    richtextBox1.Text = "";
    pictureBox1.Image = null;
    pictureBox2.Image = null;
    pictureBox3.Image = null;
    pictureBox4.Image = null;
}

使うときは、

    initialscreen();

などと言うように、画面を初期化したいところで、initialscreen();と書けばよいのです。


ここでは、メソッドの文法についてはさらは深入りしませんが、ある程度共通的にまとめられるところはまとめると、処理が分かり易く見通しが良くなるという点です。(逆に見通しが良くなる様に、共通箇所をまとめていきます。)更に言うと、必ずしも、何度も出てくるところをまとめるのではなく、処理を見通し良くするために分割していくという発想が大事です。

特に仕様を作る際に、見通し良くなる様にすることが開発の効率を上げるために有効です。(あえて、今回はダラダラ書いていますが、ナビゲーションの処理のところは、共通化することで処理が見やすくなります。)

で、本来なら、仕様を作る段階で、意識すると良いのですが、プログラムを作った後で、プログラムを見直していくことも、メンテナンスを考えた場合に有効な手段位なります。

もちろん、処理そのものを見直すことも必要に応じて、実施すべきでしょう。

【練習問題】
ナビゲーション処理にモジュールの概念を導入して、もう少し見やすく分かり易い記述に直しなさい。
このページの先頭へ