stefafafan の fa は3つです

"すてにゃん" こと id:stefafafan のブログです

とりあえずメモ帳作ったよ

2014年も残りわずかですが皆さんいかがお過ごしでしょーか。メモ帳使ってますか?今どきメモ帳使っていない人は時代遅れですよ~w 

VimEmacsSublime Text?Atom

そんな横文字のよくわからないエディタ使うのはただのかっこつけだよ。

ってことで取り残されないためにもメモ帳をC#で自作してみました。

準備

正直に言うとC#素人でVisual Studioもそこまで使ったことないです。なので今回はググりながら素人なりにがんばってみました。メモ帳程度なら出来るだろうと。

今回の開発環境はVisual Studio 2013 Professionalです。この前Community版が出たのでそれでもよかったですけどね。起動して新規プロジェクトを作るとこの画面が出ました:

f:id:stefafafan:20141227082400p:plain

ん~?どれにすればいいんだ。説明読む感じではデスクトップアプリなら Windows Forms Application と WPF Application の二択のようです。適当にWindows Formsのほうを選択しました。

あとで検索してみるとどうやらWPFを使ったほうが良いという意見が多いようですがまあいいでしょう。

早速作ろう

新規プロジェクトを作ったらフォームが現れた!あら、簡単じゃないの。とりあえずメモ帳なのでRichTextBoxを配置してみた。

f:id:stefafafan:20141227083446p:plain

おーおー、この調子なら数十分で出来ちゃうんじゃないの?(※実際はのんびりしながら4日くらいかかっちゃいました)

でもこれいきなり問題があるのですよ。ウィンドウをリサイズしてもRichTextBoxがリサイズしてくれないんですよ。どうするんこれ。ってことでググりました:

If the RichTextBox is the only control you can set it's Dock property to Fill.*1

RichTextBox しかないのでとりあえずDockっていうプロパティをFillに変えてやったら解決しました。

さて次にメニューを追加してみます。MenuStripってのを追加したら簡単にできました。

f:id:stefafafan:20141227084945p:plain

あれれ~?見た目おかしくないか?僕の知ってるメモ帳のメニューじゃないなぁ。ってことで更にググったところ:

the theme of the MenuStrip and ContextMenuStrip controls don’t automatically adjust to match the Windows 7 theme. But, as you can see below, the MainMenu and ContextMenu controls do match the Windows 7 theme.*2

んん?MenuStripよりもMainMenuっていうやつを使ったほうがいいらしい?で使うには追加しないといけないらしい。ふーん、ってことで追加してるところ:

f:id:stefafafan:20141227085317p:plain

なるほどね、沢山追加できるものがあるんだね。 MainMenuを入れたところでメニュー項目を入力していきました。

f:id:stefafafan:20141227085637p:plain

実行してみると、おっ、それっぽいね?アイコンもメモ帳のやつにしてタイトルはとりあえず無題にしときました。

f:id:stefafafan:20141227085652p:plain

コンテキストメニュー(右クリックした時に出るやつ)も同じように設定できるみたいですね。

f:id:stefafafan:20141227090016p:plain

そういえばまだコードを書いていないな…

コード書こうぜ

一番簡単そうな「ステータスバーの表示」から書こう。これはチェックボックス風なメニュー項目で、チェックが入ってる間はメモ帳の下にステータスバーが表示され、僕が知ってる限りでは現在マウスがある場所の行と列番号を表示してくれるもの。「Ln n, Col m」みたいな形式で右下に表示されてます。ってことでまずStatusStripってのを追加してっと。

あれ?これ右寄せってどうやんの。

ググった!

You can use a ToolStripLabel to pseudo right align controls by setting the Text property to string.Empty and setting the Spring property to true. This will cause it to fill all of the available space and push all the controls to the right of the ToolStripLabel over.*3

はん…? つまり右寄せしたい物の左側に空白を入れて右寄せになってるように見せかけるってことか?

実際やってみたらできました。

あとは行と列番号を求めるコード。RichTextBoxのSelectionStartというプロパティが現在マウスで選択されてる部分の最初の位置を返してくれるらしいのでそれを利用するみたい。というかググった

そしてこんな感じ:

f:id:stefafafan:20141227091502p:plain

WordWrapというメニュー項目も一応実装しました。実装というより単にチェックをオン・オフにするだけでしたけどね。

セーブしよう

セーブが出来なかったらどうしようもないですよね。SaveFileDialogというめっちゃ便利なものがあるみたいなので追加。セーブというメニュー項目がクリックされたらこのダイアログボックスを表示させるようにしました。次に、セーブが押されたときどうするか?

まずメモ帳のタイトルを「無題」とか「Untitled」とかじゃなくてセーブダイアログボックスで入力したものに変えたいよね。ってことでこうです:

this.Text = System.IO.Path.GetFileName(saveFileDialog.FileName) + " - Notepad";

SaveFileDialogのFileNameというのがファイル名だと思ったら実際はパスだという罠でした。ってことでここ を参考にGetFileNameという関数にたどり着きました。

さて次に実際にファイルに書き込みをして「セーブ」をしないといけないですね。 StreamWriterっていうのを使えば書き込むことができるらしい。こうなりました:

System.IO.StreamWriter writer = new System.IO.StreamWriter(path, false, System.Text.Encoding.GetEncoding("shift_jis"));
writer.Write(mainTextBox.Text);

writer.Flush();
writer.Close();
writer.Dispose();

StreamWriterの第一引数が保存するファイルのパス、第二引数が文字をアペンドするかどうか(僕の場合は全部上書きということでfalse)、で最後の引数が文字コードですね。ググったとき他の人がshift_jis使ってたのでそれにしてますが、まあ他のでもできるはず。 次にそのwriterを使ってRichTextBoxの中身を使って書き込む。これだけで良さそうだけど実行してみたら保存されてなくてどうやらwriter.Flush()必須っぽいですね。この後CloseしてDisposeしてますがこれは多分他の処理でOpenとかSaveとかしてたらプログラムが怒りそうだと思ったのでとりあえずDispose(破棄?)してます。

ところでここで明示的に文字コード指定していますがこれは何故かと言うとSaveFileDialogにはデフォルトでは文字コードを選ぶためのドロップダウンメニューがついていないんですよ~メモ帳のセーブダイアログにはあるのに。で、ちょっとググってみるとどうやら簡単にドロップダウンメニューを追加することができないようなのでとりあえず放置で。

ファイルを開くためには基本的にはセーブと同様で、writerじゃなくてreaderを使ったらできました。

セーブしないの?

保存していないファイルを閉じるときに「保存しますか?」みたいの、出るよね。それやりたい!

やりました。

まずはユーザーがメモ帳を閉じようとするときにそれを阻止しちゃうコードはこちらを参考にしました。FormClosingイベントってイベントがあるんだね。 次にMessageBoxを表示させてセーブしますか?ってするのもいいが、デフォルトでは「セーブボタン」なんてものはないんだよなぁ。YESとかNOとかCANCELとか。

でググってみる:

Just add a new form and add buttons and a label.*4

自分でフォーム作れってさ。仕方ないので作りました:

f:id:stefafafan:20141227094956p:plain

で、どうやって親のフォームにどのボタン押したのか伝えるの。ググったところここをみるとどうやらDialogResultっていうのを設定しておけばどのボタンを押したかがわかるようなのでそうしました。で、Saveボタンを押したらセーブ、Don't Saveを押したら何もせずにそのまま閉じる、Cancelを押したらメモ帳を閉じるのを阻止しちゃう。

出来た。

編集しよう。

Editメニューにはよく使うコピー、ペースト、アンドゥ等がありますが、これらの機能は大体RichTextBoxに定義されてるようなので楽勝です。

// Undo.
private void undoMenu_Click(object sender, EventArgs e)
{
    mainTextBox.Undo();
}

// Cut.
private void cutMenu_Click(object sender, EventArgs e)
{
    mainTextBox.Cut();
}

// Copy.
private void copyMenu_Click(object sender, EventArgs e)
{
    mainTextBox.Copy();
}

// Paste.
private void pasteMenu_Click(object sender, EventArgs e)
{
    mainTextBox.Paste(DataFormats.GetFormat(DataFormats.UnicodeText));
}

// Delete.
private void deleteMenu_Click(object sender, EventArgs e)
{
    mainTextBox.SelectedText = "";
}

Findはまた自前でフォームを作らないといけないですね。

f:id:stefafafan:20141227100305p:plain

他のフォームと違う点があって、FindフォームはFind Nextボタンを押しても閉じずにNotepadのフォームに影響をあたえて行くというところです。ここまでShowDialogの返すDialogResultを利用してどのボタンを押したか判別してたけど今回はフォームを閉じずにやらないといけないので別のやり方になります。

またまたググってこのやり方を使うことにしました。Notepadのファイルにpublicな関数を作っておいて、Findの方でボタンが押されたとき、押されたボタンによってこのNotepadの関数に与える引数を変えていくっという感じ。

最終的にNotepadのほうに書いた関数はこうなりました:

public void passFindData(FindWindow.direction dir, string text, bool match, string replacetext, ReplaceWindow.replacestate replace)
{
    find.direction = dir;
    find.findText = text;
    find.matchCase = match;
    find.replaceText = replacetext;
    find.replaceState = replace;
    findReplace();
}

なんだか色々ありますがこれはFindのフォームにて探す方向、探す文章、大文字・小文字識別するか、そして後に作ったReplaceのための置き換える文章と1個を置き換えるか全部を置き換えるかのenumです。ボタンを押すたびにNotepadのほうの値を設定するようにしてます。

じゃあこれらを受け取った後はどうやって探すの?って話ですが実はRichTextBoxにはFindという関数がもうあるという。その引数の一つにRichTextBoxFindsというのがありここで探す方向とか指定できるのです。

int start, end;
RichTextBoxFinds options = RichTextBoxFinds.None;

// |= を使ってRichTextBoxFindsのオプションを追加していく
if (find.matchCase)
{
    options |= RichTextBoxFinds.MatchCase;
}

/* ... */

// 探す方向が下方向の場合
start = mainTextBox.SelectionStart + mainTextBox.SelectionLength; // 現在選択してる場所の最後
end = mainTextBox.Text.Length; // メモ帳の一番最後

// 探す方向が上方向の場合
start = 0; // すごく紛らわしいけど上方向の場合startに向かって行くのでこれはゼロで良い
end mainTextBox.SelectionStart; //現在選択している箇所

// 指定した探す範囲とオプションで探す、返り値は見つけた位置。-1の場合見つからなかった。
int found = mainTextBox.Find(find.findText, start, end, options);

// 置き換える場合, SelectedTextを設定するだけ
mainTextBox.SelectedText = find.replaceText;

こんな感じですね。Replace Allしたいときは初期位置を0にしてwhileループで回して全部置き換えしていくようにした:

private void replaceAll(RichTextBoxFinds options)
{
    int start = 0;
    int found = mainTextBox.Find(find.findText, start, options);
    while (found != -1 && start != mainTextBox.Text.Length)
    {
        mainTextBox.SelectedText = find.replaceText;
        start = mainTextBox.SelectionStart + mainTextBox.SelectionLength;
        found = mainTextBox.Find(find.findText, start, options);
    }
    this.Focus();
}

ところでReplaceように作ったフォームはこんな感じ

f:id:stefafafan:20141227102211p:plain

後藤さん

後藤さんではなくGo Toです。この行数に飛びたいってための機能。あんま使ったことないけどね。とりあえずフォーム:

f:id:stefafafan:20141227102929p:plain

あまりおもしろくないですが1個ハマったところはツールチップです。数字以外を入力したときに「数字しか入力できませんよ!」って言ってくるようにするやつ。

数字のみを許可するためにはここを、
ツールチップの位置をちゃんと指定するにはここを、
ツールチップが表示されているかどうかはここを参考にしました。

最終的にツールチップの部分のコードはこうなりました:

// キーを押す度に呼ばれる関数
private void textbox_KeyPress(object sender, KeyPressEventArgs e)
{
    // 数字以外が入力された場合
    if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar))
    {
        // 入力を阻止
        e.Handled = true;

        // ツールチップが表示されていない場合
        if (string.IsNullOrEmpty(toolTip.GetToolTip(textbox)))
        {
            // 一時的に空のツールチップを表示した後、実際のツールチップを表示。
            toolTip.Show("", textbox, 10, textbox.Height); 
            toolTip.Show("You can only type a number here.", textbox, 10, textbox.Height);
        }
    }
    // 数字が入力された場合はツールチップを隠す
    else
    {
        toolTip.Hide(textbox);
    }

}

どうやらツールチップは最初に表示したときのみ位置がズレる事があってこれはバグだそうですStackOverflowの人曰く。なので2回表示させているという。

完成(?)

メモ帳とりあえず大体の機能は追加しました。コンテキストメニューのやつもです。ソースコードはこちら

今回無視した機能:

  • 印刷 - とりあえずダイアログは出してるけどちゃんと処理はしていない
  • 開く、保存するときのエンコードの選択
  • ヘルプ・メモ帳についてのフォームの表示
  • コンテキストメニューの"Show Unicode control characters," "Insert Unicode control character," "Open IME," "Reconversion"

それ以外はあんまテストしてないけど多分できてる(?)

次回C#Windowsアプリ作るときはWPF使ってみるかな。

とりあえず明日からは暇があればAndroidアプリを作ってみたいと思ってます。

では~。