🎨 Графическое программирование на C#

← Вернуться к модулю 5

Урок 5.4: Интерактивность графиков

Модуль 5 ⏱️ ~2 часа

Подсветка точек при наведении

private int hoveredPointIndex = -1;

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    int newHoveredIndex = FindNearestPoint(e.X, e.Y);
    if (newHoveredIndex != hoveredPointIndex)
    {
        hoveredPointIndex = newHoveredIndex;
        this.Invalidate();
    }
}

private int FindNearestPoint(int x, int y)
{
    float minDistance = float.MaxValue;
    int nearestIndex = -1;
    
    for (int i = 0; i < points.Count; i++)
    {
        float distance = (float)Math.Sqrt(
            Math.Pow(points[i].X - x, 2) + 
            Math.Pow(points[i].Y - y, 2));
        
        if (distance < 10 && distance < minDistance)
        {
            minDistance = distance;
            nearestIndex = i;
        }
    }
    
    return nearestIndex;
}

Zoom и Pan

private float zoom = 1.0f;
private PointF panOffset = new PointF(0, 0);

private void Form1_MouseWheel(object sender, MouseEventArgs e)
{
    // Zoom
    float zoomFactor = e.Delta > 0 ? 1.1f : 0.9f;
    zoom *= zoomFactor;
    this.Invalidate();
}

private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Right)
    {
        panStart = e.Location;
        isPanning = true;
    }
}

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    if (isPanning)
    {
        panOffset.X += e.X - panStart.X;
        panOffset.Y += e.Y - panStart.Y;
        panStart = e.Location;
        this.Invalidate();
    }
}

Tooltip с данными

private ToolTip tooltip = new ToolTip();
private int hoveredIndex = -1;

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    int index = FindNearestPoint(e.X, e.Y);
    if (index != hoveredIndex)
    {
        hoveredIndex = index;
        if (index >= 0)
        {
            tooltip.SetToolTip(this, 
                $"X: {data[index].X:F2}
Y: {data[index].Y:F2}");
        }
        this.Invalidate();
    }
}

Выбор области

private Rectangle selectionRect;
private bool isSelecting = false;

private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        selectionRect = new Rectangle(e.X, e.Y, 0, 0);
        isSelecting = true;
    }
}

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    if (isSelecting)
    {
        selectionRect.Width = e.X - selectionRect.X;
        selectionRect.Height = e.Y - selectionRect.Y;
        this.Invalidate();
    }
}

private void Form1_Paint(object sender, PaintEventArgs e)
{
    // Рисование графика
    // ...
    
    // Рисование области выбора
    if (isSelecting)
    {
        using (Pen pen = new Pen(Color.Blue, 1)
            { DashStyle = DashStyle.Dash })
        {
            e.Graphics.DrawRectangle(pen, selectionRect);
        }
    }
}

📝 Пример задания — выбор функции через ComboBox

Форма с ComboBox позволяет выбрать одну из 8 функций. При смене выбора панель перерисовывается с новой функцией через вспомогательный класс GraphPlotter:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace CSharpGraphicsApp
{
    public class InteractiveGraphExample : Form
    {
        private ComboBox functionSelector;
        private Panel graphPanel;

        public InteractiveGraphExample()
        {
            Text = "Интерактивный выбор функции";
            Size = new Size(700, 600);
            BackColor = Color.White;

            var controlPanel = new Panel
            {
                Height = 50, Dock = DockStyle.Top,
                BackColor = Color.WhiteSmoke
            };
            controlPanel.Controls.Add(new Label
            {
                Text = "Функция:", AutoSize = true, Location = new Point(10, 15)
            });

            functionSelector = new ComboBox
            {
                Location = new Point(80, 12), Width = 200,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            functionSelector.Items.AddRange(new object[]
            {
                "sin(x)", "cos(x)", "tan(x)", "x²", "x³", "√x", "e^x", "ln(x)"
            });
            functionSelector.SelectedIndex = 0;
            functionSelector.SelectedIndexChanged += (s, e) => graphPanel.Invalidate();
            controlPanel.Controls.Add(functionSelector);
            Controls.Add(controlPanel);

            graphPanel = new Panel { Dock = DockStyle.Fill, BackColor = Color.White };
            graphPanel.Paint += (s, e) => DrawSelectedGraph(e.Graphics, graphPanel.ClientSize);
            Controls.Add(graphPanel);
        }

        private void DrawSelectedGraph(Graphics g, System.Drawing.Size size)
        {
            // Параметры осей под выбранную функцию
            Func<float, float> fn;
            string title;
            float minX, maxX, minY, maxY;

            switch (functionSelector.SelectedIndex)
            {
                case 1:  fn = x => (float)Math.Cos(x);  title = "cos(x)";
                         minX = -6.3f; maxX = 6.3f; minY = -1.5f; maxY = 1.5f; break;
                case 2:  fn = x => (float)Math.Tan(x);  title = "tan(x)";
                         minX = -1.4f; maxX = 1.4f; minY = -5f; maxY = 5f; break;
                case 3:  fn = x => x * x;                title = "x²";
                         minX = -4f; maxX = 4f; minY = -1f; maxY = 16f; break;
                case 4:  fn = x => x * x * x;            title = "x³";
                         minX = -3f; maxX = 3f; minY = -27f; maxY = 27f; break;
                case 5:  fn = x => x >= 0 ? (float)Math.Sqrt(x) : 0; title = "√x";
                         minX = 0f; maxX = 10f; minY = 0f; maxY = 3.5f; break;
                case 6:  fn = x => (float)Math.Exp(x);   title = "e^x";
                         minX = -2f; maxX = 2f; minY = -0.5f; maxY = 8f; break;
                case 7:  fn = x => x > 0 ? (float)Math.Log(x) : 0; title = "ln(x)";
                         minX = 0.1f; maxX = 10f; minY = -2.5f; maxY = 2.5f; break;
                default: fn = x => (float)Math.Sin(x);   title = "sin(x)";
                         minX = -6.3f; maxX = 6.3f; minY = -1.5f; maxY = 1.5f; break;
            }

            // Простая отрисовка без вспомогательного класса
            int w = size.Width, h = size.Height;
            int ml = 60, mr = 20, mt = 40, mb = 50;

            // Оси
            g.DrawLine(Pens.Black, ml, h - mb, w - mr, h - mb); // X
            g.DrawLine(Pens.Black, ml, mt, ml, h - mb);          // Y
            g.DrawString(title,
                new Font("Segoe UI", 12, FontStyle.Bold), Brushes.Blue, ml + 10, mt);

            // Точки функции
            var pts = new System.Collections.Generic.List<PointF>();
            float step = (maxX - minX) / (w - ml - mr);
            for (float x = minX; x <= maxX; x += step)
            {
                float y = fn(x);
                if (float.IsNaN(y) || float.IsInfinity(y)) { pts.Clear(); continue; }
                float sx = ml + (x - minX) / (maxX - minX) * (w - ml - mr);
                float sy = (h - mb) - (y - minY) / (maxY - minY) * (h - mt - mb);
                pts.Add(new PointF(sx, sy));
            }
            if (pts.Count > 1)
                using (Pen pen = new Pen(Color.Blue, 2))
                    g.DrawLines(pen, pts.ToArray());
        }
    }
}

Практическое задание

Задание: Интерактивные графики

  1. Создайте интерактивный график с масштабированием колесиком мыши
  2. Реализуйте перемещение правой кнопкой мыши
  3. Добавьте отображение значений при наведении

Задание по аналогии: Выбор функции через ComboBox

По аналогии с InteractiveGraphExample создайте форму с динамическим выбором функции.

  1. Добавьте панель управления вверху с ComboBox, содержащим минимум 4 функции (sin, cos, x², √x).
  2. В SelectedIndexChanged вызывайте graphPanel.Invalidate().
  3. В обработчике Paint определяйте нужную функцию Func<float, float> по SelectedIndex.
  4. Реализуйте отрисовку осей и кривой через список точек PointF.
  5. Обрабатывайте случаи NaN и Infinity — сбрасывайте накопленный список точек, чтобы не рисовать разрывы.
💡 Совет: Адаптируйте minX, maxX, minY, maxY под каждую функцию — у tan и ln совсем другие допустимые диапазоны.

Проверка знаний

Ответьте на вопросы, чтобы проверить, насколько хорошо вы усвоили материал:

1. Усвоили ли вы основные концепции урока?
  • Да, материал понятен
  • Частично понятен
  • Нужно повторить
  • Не понял
2. Готовы ли вы применить изученное на практике?
  • Нет, нужно еще изучить
  • Да, готов попробовать
  • Возможно
  • Не уверен
3. Что такое Zoom в графиках?
  • Увеличение масштаба отображения
  • Уменьшение размера окна
  • Изменение цвета
  • Фильтрация данных