課題の進め方

基本的には適当なプロジェクト/ソリューションを作って課題を進めた上で,MainForm.csを提出する.

採点はコマンドラインからdotnet new etoappを実行した上でMainForm.csを提出されたもので上書きすることにより行う. 採点者は基本的に採点者自身の環境で動作確認を行うことに注意する.

第6回課題においてMainForm.cs以外に提出に含めたいものがある場合(たとえば画像ファイル等)は事前にメール(メールアドレスはClassroom内「受講者用案内」を参照)で相談のこと.

他の人(受講者・非受講者両方)に解答内容(一部でも)を見せない,そして他の人の解答内容(一部でも)を見ないようお願いします.特に, 公開の場所に解答を置かないようお願いします.githubやbitbucket等は使える人は使えばよいと思いますが,privateレポジトリにするようにお願いします.

プロジェクト/ソリューションの作成

Note

再掲:プロジェクトは一つの実行形式やライブラリを作成するためのコード等を全てまとめたものであり,ソリューションは関連するプロジェクトをまとめたもの(参考:What are solutions and projects in Visual Studio?).

指定された名前(課題5ではQ5,課題6ではQ6とする)の空のフォルダを適当な場所に新規作成し, VSCodeで作成したフォルダを開く.そして,VSCode内のターミナルで以下を実行する.

dotnet new etoapp -sln

課題の実施

MainForm.cs を問題文の指示の通りに編集する(提出・採点手続きの簡略化のため提出する.csファイルは一つのみ).作成した.csファイルには先頭部分に学籍番号と名前をコメントとして含めること.また,自身のプログラムの動作確認を行ったプラットフォームの情報(Mac, Gtk, Wpfの別.複数可.わからないならOS名)も含めるものとする.こちらの情報はあくまで念のためであり,基本的には採点者は自身の環境で動作確認を行う.なので,たとえば学籍番号Z0TB9999の東北 大学さんの提出ファイルは,もし当人が動作確認をプロジェクト名.Macを用いて行ったのであれば

// Z0TB9999
// 東北 大学
// 動作確認:Mac 

という行から始まる.

また,プロジェクト名.csprojに変更を加えた場合は,その旨と具体的な変更内容を上記の後にコメントとして含めるものとする.たとえば,Q5.csprojというプロジェクトファイルにおいてPropertyGroup以下にあるTargetFrameworknet8.0に変更したのであれば,以下のような行を含める.

// Q5.csproj の /Project/PropertyGroup/Target の内容を net8.0 に変更した

提出

できあがった MainForm.csをClassroom内の当該回の「課題」より提出する.また問題文に指示がある場合はそのファイル(例:課題6で提出物に含めたいリソースがある場合)も提出する.提出前には以下を確認しよう.

    • ただし,友人の解答を見ない(一部でも),そして友人に解答を見せない(一部でも)ようお願いします

基本課題

以下の要件を満たすお絵描きプログラムを作成せよ.

  • 少なくとも以下のコントロールが配置されている
    • ColorPicker
    • NumericStepper
    • 「クリア」と書かれたボタン
    • お絵描き用のコントロール(ウィンドウの主要な面積を占める)
  • マウスの主ボタン(右利き用マウスだと通常は左ボタン)を押してマウスを動かすと,線が動きに沿って描かれる.このとき,線の色は ColorPickerで選択した色で太さがNumericStepperで選択した数であるとする.
    • 要はよくあるお絵描きツールの「鉛筆」ツールのような挙動をする.
  • 「クリア」と書かれたボタンが押されたら,書かれた絵がクリアされる.

たとえば,以下のスクリーンショットは実装するプログラムの一例を表している.作成したアプリケーションで,この程度の絵ならばちゃんと描けるようになる(もちろんアプリケーションの挙動の意味で)ことを一つの目標にするとよい.

作成を目指すプログラムのスクリーンショット

Tip

お絵描き用のコントロールのMouseMoveイベントを購読(イベントハンドラを登録)する.主ボタンが押されたまま移動されたかどうかは,以下のようにハンドラの第2引数のButtonsプロパティを用いて判定できる.

// oekakiControl の MouseMove イベントを購読
oekakiControl.MouseMove += (s, me) => {
   if ( me.Buttons.HasFlag( MouseButtons.Primary ) ) {
       // 主ボタンが押されたままマウスが移動したときの処理
   }
};

Tip

素朴なアイデアはMouseMoveイベントの度に現在のマウスの位置に円を描くというものだが,それだとマウスが一度に沢山に動いたときにとぎれとぎれの「線」が描かれてしまう.それを避けるためには,以下のようにするとよいだろう.

  • マウスの以前の位置を覚えておく.
  • マウスの主ボタンが押されたら,その位置に円が描かれるようにし,「マウスの以前の位置」を更新する.
  • マウスの主ボタンが押されたままマウスが移動されたら,「マウスの以前の位置」と「マウスの現在の位置」の間に線分が描かれるようにする.そして,「マウスの以前の位置」を更新する.

線分の描画にはGraphics.DrawLine(Pen, PointF, PointF)メソッドが使用できるだろう.PenLineCapプロパティをPenLineCap.Roundに設定するときれいに線が引けるかもしれない(参考).

Important

繰り返すが,以下のコードはDrawableであるdに対し,Paintイベントが発生したとき(≒ 描画要求があってコントロールが描画されるとき)に呼ばれる処理を追加しているのであって,描画そのものを行っているのではない

d.Paint += (s, se) => {
    // ...
};

なので,たとえば

oekakiControl.MouseMove += (s, me) => {
    // ... 
    oekakiControl.Paint += (sp, pe) => {
        // ...
    };
    // ...
};

のようなコードを書くと,そもそもoekakiControl.Paint += (sp, pe) => { ... }の部分で描画は実行されないし,マウスを移動する度に描画要求があったときに実行する処理が増えていきだんだん描画処理が重くなっていく.

作成するお絵描き用コントロールをoekakiControlとすると,そのMouseMoveイベントもPaintイベントも購読することになるだろうが,購読する部分のコードは以下のような形になるはずだ.

oekakiControl.MouseMove += (s, me) => {
    // ...
};
// ...
oekakiControl.Paint += (s, pe) => {
    // ....
};

なお,oekakiControlの(再)描画要求を投げるにはoekakiControl.Invalidate()を呼ぶ.

発展課題

Important

本課題を完了できたのならば本課題の解答のみを提出すればよく,基本課題の解答は提出する必要はない.

基本課題の条件を満たしている限りにおいて,お絵描きプログラムにさまざまな機能を追加せよ.追加する機能は自由に決めたのでよいが,どのような機能を追加したかの説明およびプログラム上の工夫点はコメントとして提出プログラムに含めること.

どういう機能を追加したらよいかまよっている人の参考までに,いくつかの機能追加の方向性を以下に示しておく.もちろん,追加する機能はこれらに限定されない.

  • お絵描きした画像を保存する機能
  • 「消しゴム」機能
  • アンドゥ,リドゥ機能
  • Shiftを押しながらクリックすると,以前に最後にマウスを離した場所との間に直線を引く
  • 適当なコントロールやダイアログ等を追加し,文字列を入力できるようにする
  • 「ぼかし」などのフィルタ処理
  • 拡大・縮小・回転,あるいは一般の線形変換
  • レイヤ機能

Tip

保存や読込機能の実装にはそれぞれ OpenFileDialogSaveFileDialog を使うとよい.以下にSaveFileDialogの使用例を示すが,OpenFileDialogも同様.

var dialog = new SaveFileDialog { Filters = {"png or jpg|png;jpg"} } ; 
if (dialog.ShowDialog(this) == DialogResult.Ok) { // MainForm内なら.そうでなければthisの代わりに
  var filepath = dialog.FileName; 
  // filepath に対する処理.適切な例外処理が必要になるだろう.
}