Рисовалка (Paint)
Рассмотрим как сделать простейший аналог программы Paint.
Создади шаблон приложения с анимацией
Попробум рисовать закрашенный шарик в позиции курсора на каждой итерации цикла отрисовки, без очистки экрана.
Почему в данной задаче удобно не очищать экран?
Поскольку редактор растровый и мы меняем и работаем именно с пикселями, а не с примитивами вроде линий, окружностей (как это делается в векторных графических редакторах), то хранить сцену удобно тоже именно в пикселях. Смысла очищать и затем выводить опять эти же самые пиксели нет никакого. Поэтому мы работаем с нашей сценой в инкрементальном режиме (только перерисовываем пиксели под кистью). Остальные пиксели мы не трогаем.
Для начала давайте создадим растр Bitmap где будет храниться наша сцена
public Form1()
{
InitializeComponent();
DoubleBuffered = true; // включаем двойную буферизацию, чтобы избежать фликеринга (flickering)
StartPosition = FormStartPosition.CenterScreen;
bmp = new Bitmap(ClientSize.Width, ClientSize.Height); // создаем растр где будет храниться сцена
gr = Graphics.FromImage(bmp); // создает объект Graphics для работы со сценой
gr.Clear(Color.Black); // очищаем один раз при создании все черным цветом
gr.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; //включаем сглаживание
var timer = new System.Windows.Forms.Timer(); // создаем таймер анимации
timer.Interval = 10; // ставим интервал отрисовки
timer.Start(); // запускаем таймер
timer.Tick += Timer_Tick; // подписываемся на хендлер таймера
Paint += Form1_Paint;
}
Теперь давайте попробуем на каждой итерации отрисовки рисовать кружок в позицию курсора без очистки экрана
float brushSize = 16; // радиус кисти
private void Form1_Paint(object? sender, PaintEventArgs e)
{
var cursor = PointToClient(Cursor.Position);
Color color = Color.Orange;
gr.FillEllipse(new SolidBrush(color), cursor .X - brushSize / 2, cursor .Y - brushSize / 2, brushSize, brushSize);
e.Graphics.DrawImageUnscaled(bmp, 0, 0);
}Запустив и подвигав мышью мы увидем что отрисовка происходит рваная, это связано с тем что курсор успевает переместиться быстрее, чем отрабатывает цикл отрисовки.

Чтобы гарантировать непрерывность линии и убрать все разрывы, давайте попробуем рисовать не отдельные блобы, а будем запоминать последнюю позицию курсора и рисовать линию от нее до текущей позиции курсора.
PointF? lastPoint = null; // последня запомненная точка
private void Form1_Paint(object? sender, PaintEventArgs e)
{
var cursor = PointToClient(Cursor.Position);
Color color = Color.Orange;
if (lastPoint != null)
gr.DrawLine(new Pen(Color.Orange, brushSize), cursor .X, cursor .Y, lastPoint.Value.X, lastPoint.Value.Y);
lastPoint = cur;
e.Graphics.DrawImageUnscaled(bmp, 0, 0);
}
Посмотрим результат:

Картина стала намного лучше, но появились множественные артефакты в виде рванных переходов от одной линии к другой. Это вызвано тем что толстые линии рисуются без скругления, а с прямым торцом. Давайте нивелируем эти артефакты добавив отрисовку закрашенного круга в конец линии.
private void Form1_Paint(object? sender, PaintEventArgs e)
{
var cursor = PointToClient(Cursor.Position);
Color color = Color.Orange;
if (lastPoint != null)
{
gr.DrawLine(new Pen(Color.Orange, brushSize), cursor.X, cursor.Y, lastPoint.Value.X, lastPoint.Value.Y);
gr.FillEllipse(new SolidBrush(color), cursor.X - brushSize / 2, cursor.Y - brushSize / 2, brushSize, brushSize);
}
lastPoint = cursor;
e.Graphics.DrawImageUnscaled(bmp, 0, 0);
return;
}Запустим:

Все артефакты пропали и теперь линия выглядит плавно.
Добавляем левую кнопку мыши
Давайте сделаем чтобы линия рисовалась только в случае если была нажата левая кнопка мыши.
Введем флаг - признак нажатия левой кнопки мыши и подпишемся на событий MouseUp, MouseDown
// добавим это в конструктор
MouseDown += Form1_MouseDown;
MouseUp += Form1_MouseUp;
} // конец конструктора Form1
bool isPressed = false; // объявим флаг признак нажатия мышки
// напишем обработчики для мышки
private void Form1_MouseUp(object? sender, MouseEventArgs e)
{
isPressed = false; // если отпустили любую клавишу мышки, то сбрасываем все флаги
lastPoint = null; // также сбрасываем последнюю точку, т.к. следующая линия будет рисоваться с новой точки
}
private void Form1_MouseDown(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) // если была нажата левая кнопка мыши
isPressed = true; // выставляем флаг отрисовки
}Запустим и убедимся, что теперь можно зажимая и отпуская левую кнопку мыши рисовать новые линии.

Добавляем изменение размера кисти колесиком
Добавляем правую кнопку мыши для второго цвета
Добавляем многослойность
Введем второй слой. Для отображения полезных UI хинтов.
Добавим левую панель переключения цветов
Сохранеие и загрузка в файл
Чтобы сделать программу полноценной (смотри жизненный цикл программ), требуется сделать возможность загрузки данных их обработки и выгрузки на диск.