Как работать с графикой в windows

Поддержать проект финансово

Оболочка Dev-C++ для Windows

ЧаВо = Часто задаваемые Вопросы

Список вопросов

  1. Как настроить русский язык в консольных программах?
  2. Как писать простые графические программы?
  3. Как работать с графикой в Windows 8/10?
    Новинка!

Как настроить русский язык в консольных программах?

В консольных приложениях (похожих на текстовый режим)
русские буквы выводятся к кодировке CP-866, а в оболочке
Dev-C++ они набираются в кодировке CP-1251. Это значит, что
вместо русских букв вы увидите «кракозябры». Чтобы этого не
случилось, кодировку нужно исправить с помощью простой
дополнительной программы. Сделайте следующее:

Вариант 1.

  1. Запустите оболочку Dev-C++ и войдите в меню Сервис-Параметры компилятора.
  2. Перейдите на вкладку Компилятор и добавьте два параметра
        -fexec-charset=cp866 -finput-charset=cp1251

    как показано на рисунке (см. источник).

    Русский язык в Dev-C++

Вариант 2.

  1. Скачайте архив, содержащий программы
    gccrus.exe и g++rus.exe (193 Кб).
  2. Скопируйте программы
    gccrus.exe и g++rus.exe в папку C:\Dev-Cpp\bin
    (если при установке оболочки вы указали другой каталог
    вместо стандартного C:\Dev-Cpp, скопируйте
    программы в его подкаталог bin).
  3. Запустите оболочку Dev-C++ и войдите в меню Сервис-Параметры компилятора.
  4. Перейдите на вкладку Программа и исправьте названия двух первых программ так,
    как показано на рисунке.

    Русский язык в Dev-C++ (вариант 2)

Примечание от Ю. Проценко: Если программа при запуске
не обнаруживает библиотеки libintl-8.dll
и libiconv-2.dll,
их нужно поместить в каталог C:\WINDOWS\SYSTEM32.

Как писать простые графические программы?

Для работы с графикой через браузер можно использовать
онлайн-сервис
Антона Деникина.

Для того чтобы на локальном компьютере
подключить модуль для работы с графикой, сделайте следуюшее:

  1. Скачайте архив, содержащий файлы
    graphics.h (заголовочный файл) и libbgi.a (библиотека).
    Размер архива 53 Кб.
  2. Скопируйте файл graphics.h в папку C:\Dev-Cpp\include.
  3. Скопируйте файл libbgi.a в папку C:\Dev-Cpp\lib.
  4. Запустите оболочку Dev-C++ и войдите в меню
    Сервис-Параметры компилятора.
  5. Перейдите на вкладку Компилятор, включите флажок Добавить эти команды к командной строке компоновщика
    и добавьте в окно под этим флажком строчку

    -lbgi -lgdi32 -lcomdlg32 -luuid -loleaut32 -lole32

    как на рисунке:

    Графика в Dev-C++

  6. Чтобы выводить русский текст в графическом режиме, в окне Сервис-Параметры компилятора
    на вкладке Программы нужно установить программы по умолчанию
    gcc.exe и g++.exe:

    Программы по умолчанию в Dev-C++

После выполнения этих действий вы можете использовать
команды для рисования графических примитивов (отрезков, прямоугольников,
окружностей и т.д.), так же, как раньше в Turbo C и
Borland C++. Полное описание команд (на английском языке)
можно найти на
странице Михаэля Майна.

Вот так выглядит простейшая программа, которая выводит на экран отрезок
из точки (0,0) (это левый верхний угол окна) в точку (50,50) и ждет нажатия
на любую клавишу:

#include <graphics.h>
int main()
{
initwindow(400,300); // открыть окно для графики
                     // размером 400 на 300 пикселей
moveto(0,0);         // курсор в точку (0,0)
lineto(50,50);       // отрезок в точку (50,50)
getch();             // ждать нажатия на любую клавишу
closegraph();        // закрыть окно с графикой
return 0;
}

Как работать с графикой в Windows 8/10?

Спасибо П.Ф. Муль, который прислал ссылку на эту инструкцию.

Проблема состоит в том, что устаревшая версия Bloodshed Dev-C++ 4.9.9.2 не работает
в операционных системах Windows 8/10.

Полная инструкция по установке современной версии Dev-C++ и подключению
модуля работы с графикой пожно посмотреть на видео
How to Make Graphics in Dev C++ on Windows 10.

Для того, чтобы подключить модуль для работы с графикой
в Windows 8/10, сделайте следуюшее:

  1. Скачайте последнюю версию Dev-C++.
  2. Скачайте архив, содержащий файлы
    graphics.h, winbgim.h (заголовочные файл) и libbgi.a (библиотека).
    Размер архива 28 Кб.
    Далее предполагается, %Dev-Cpp% — это папка,
    в которой установлена среда Dev-C++ (например, C:\Program Files (x86)\Dev-Cpp).
  3. Скопируйте файл graphics.h в папку
    %Dev-Cpp%\MinGW64\x86_64-w64-mingw32\include.
  4. Скопируйте файл libbgi.a в папку
    %Dev-Cpp%\MinGW64\x86_64-w64-mingw32\lib.
  5. Запустите оболочку Dev-C++ и войдите в меню
    Сервис-Параметры компилятора.
  6. Перейдите на вкладку Компилятор.
    Переключитесь на 32-битный компилятор (32-bit Release).
    Включите флажок Добавить эти команды к командной строке компоновщика
    и добавьте в окно под этим флажком строчку

    -lbgi -lgdi32 -luser32

    как на рисунке:

    Графика в Dev-C++ (Windows 10)

  7. Чтобы выводить русский текст в графическом режиме, в окне Сервис-Параметры компилятора
    на вкладке Программы нужно установить программы по умолчанию
    gcc.exe и g++.exe:

    Программы по умолчанию в Dev-C++

Ещё одна инструкция по подключению простой графики от А. Ковалёва:

  1. Сразу после установки Dev-C++ 5.11 при запуске программы появляется
    окно с сообщением о недоступности папки lib32:

    The following library directories don’t exist:
      C:\Program Files\Dev-Cpp\MinGW64\lib32

    Чтобы убрать эту ошибку, нужно скопировать папку lib32 на один уровень
    вверх, в папку …\Dev-Cpp\MinGW64.

  2. Файл graphics.h нужно скопировать в две папки:

    …\Dev-Cpp\MinGW64\include
      …\Dev-Cpp\MinGW64\x86_64-w64-mingw32\include

  3. Библиотеку libbgi.a нужно скопировать в четыре папки:

    …\Dev-Cpp\MinGW64\lib
      …\Dev-Cpp\MinGW64\lib32 (это ранее сделанная копия)
      …\Dev-Cpp\MinGW64\x86_64-w64-mingw32\lib
      …\Dev-Cpp\MinGW64\x86_64-w64-mingw32\lib32

  4. Выбрать 32-битный компилятор TDM-GCC 4.9.2 32bit Release.
  5. В параметрах компилятора к командной строке компоновщика
    добавляется строка

    -lbgi -lgdi32 -lcomdlg32 -luuid -loleaut32 -lole32

Как работать с графикой на Windows: подробное руководство

Графика является неотъемлемой частью современных программ, ведь она позволяет визуализировать данные и сделать пользовательский интерфейс более привлекательным и удобным. Windows предлагает различные инструменты для работы с графикой, и в данной статье мы рассмотрим основные методы и подходы для создания графических элементов на платформе Windows.

1. Графический интерфейс Windows
Перед тем, как начать работу с графикой на Windows, необходимо проанализировать, какие инструменты и технологии она предлагает. В Windows графический интерфейс представлен двумя основными технологиями: GDI (Graphics Device Interface) и DirectX.

GDI является стандартной технологией для рисования графических элементов в Windows. Она предоставляет базовые функции для создания примитивов, таких как линии, прямоугольники и окружности, и поддерживает цвета, шрифты и другие атрибуты.

DirectX, с другой стороны, является набором API для разработки графических приложений, включающим в себя такие возможности, как 3D-рендеринг, анимация, воспроизведение видео и звука и многое другое. DirectX обычно используется для создания игр и других требовательных по производительности приложений.

2. Работа с GDI
GDI предоставляет набор функций для работы с графикой на Windows. Вот некоторые из основных функций, которые можно использовать:

— CreatePen: создание пера с определенными параметрами, такими как цвет и стиль.
— CreateBrush: создание кисти с определенными параметрами, такими как цвет и стиль.
— SelectObject: выбор пера или кисти для рисования.
— LineTo: рисование линии из текущей позиции до указанной.
— Rectangle: рисование прямоугольника с заданными координатами.
— Ellipse: рисование эллипса с заданными координатами.
— TextOut: вывод текста на экран.

У GDI есть и более продвинутые возможности, такие как работа с растровыми изображениями, масштабирование и трансформации. Также можно использовать GDI+, которая предоставляет еще больше функциональности для работы с графикой.

Пример кода на C++ с использованием GDI:

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;

    switch (msg)
    {
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        // рисуем линию
        HPEN hPen = CreatePen(PS_SOLID, 1, RGB(255, 0, 0));
        SelectObject(hdc, hPen);
        MoveToEx(hdc, 10, 10, NULL);
        LineTo(hdc, 100, 100);
        DeleteObject(hPen);
        EndPaint(hWnd, &ps);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    const wchar_t CLASS_NAME[]  = L"Sample Window Class";
 
    WNDCLASS wc = { };
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    HWND hWnd = CreateWindowEx(
        0,
        CLASS_NAME,
        L"Sample Window",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL
    );

    if (hWnd == NULL)
    {
        return 0;
    }

    ShowWindow(hWnd, nCmdShow);
    
    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

В этом примере мы создаем простое окно Windows с обработчиком сообщений WndProc. Внутри обработчика сообщений мы рисуем красную линию с помощью GDI функций.

3. Использование DirectX
Если вам нужны более сложные возможности для работы с графикой, такие как 3D-рендеринг или анимация, то можно использовать DirectX. DirectX состоит из нескольких компонентов, включая Direct3D (для 3D-приложений), Direct2D (для 2D-приложений), DirectSound (для воспроизведения звука) и т.д.

DirectX обеспечивает более низкоуровневый доступ к аппаратным возможностям видеокарты, что позволяет достичь высокой производительности и качества графики. Однако для работы с DirectX требуется больше кода и знаний, чем для работы с GDI.

4. Работа с графикой на Windows Forms и WPF
Если вы разрабатываете приложения с графическим интерфейсом на Windows, то скорее всего вы используете Windows Forms или WPF. Оба эти фреймворка предоставляют собственные инструменты и контролы для работы с графикой.

В Windows Forms для работы с графикой можно использовать классы Graphics и Pen для создания примитивов и рисования на форме. Также можно использовать PictureBox для отображения растровых изображений или контролы, такие как Chart и DataGridView, для создания диаграмм и таблиц.

WPF предоставляет более мощные и гибкие средства для работы с графикой. Он основан на векторной графике, что позволяет создавать более качественные и масштабируемые элементы интерфейса. В WPF можно использовать классы, такие как Shape и Path, для создания и настройки графических объектов, а также классы, такие как Image и DrawingImage, для отображения растровых и векторных изображений.

Пример кода на C# с использованием WPF:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

public class MainWindow : Window
{
    public MainWindow()
    {
        Canvas canvas = new Canvas();

        // рисуем линию
        Line line = new Line();
        line.Stroke = Brushes.Red;
        line.X1 = 10;
        line.Y1 = 10;
        line.X2 = 100;
        line.Y2 = 100;
        canvas.Children.Add(line);

        // рисуем прямоугольник
        Rectangle rect = new Rectangle();
        rect.Width = 100;
        rect.Height = 100;
        rect.Fill = Brushes.Blue;
        canvas.Children.Add(rect);

        // выводим текст
        TextBlock text = new TextBlock();
        text.Text = "Hello, World!";
        canvas.Children.Add(text);

        this.Content = canvas;
    }
}

public static class Program
{
    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new MainWindow());
    }
}

В этом примере мы создаем окно WPF с помощью класса MainWindow. Внутри окна мы используем элементы Canvas, Line, Rectangle и TextBlock для рисования примитивов и вывода текста.

5. Использование сторонних библиотек
Кроме базовых инструментов Windows, существуют также сторонние библиотеки и фреймворки, которые предлагают еще больший функционал для работы с графикой. Некоторые из самых популярных библиотек включают в себя OpenGL, OpenCV, Cairo и SDL.

OpenGL является открытым стандартом для 3D-графики, поддерживаемым большинством видеокарт. Он предоставляет функции для рендеринга полигональных моделей, текстурирования, освещения и т.д.

OpenCV является библиотекой с открытым исходным кодом, предназначенной для компьютерного зрения и обработки изображений. Она предоставляет множество алгоритмов и функций для обнаружения объектов, сегментации изображений, выделения особенностей и многое другое.

Cairo — это векторная графическая библиотека, предоставляющая функции для рисования примитивов, работы со шрифтами и изображениями, а также создание и манипулирование векторными путями.

SDL (Simple DirectMedia Layer) — мощная кросс-платформенная библиотека, предназначенная для создания мультимедийных приложений, включая игры. Она обеспечивает доступ к аппаратным возможностям видеокарты, звуковым устройствам и устройствам ввода.

Выбор сторонней библиотеки зависит от ваших потребностей и предпочтений. При выборе библиотеки следует оценить ее производительность, доступность документации и сообщества разработчиков, а также поддержку платформы Windows.

Выводы
Windows предлагает различные инструменты и технологии для работы с графикой. GDI является базовым инструментом для рисования примитивов на Windows, а DirectX обеспечивает более продвинутые возможности для работы с графикой в играх и требовательных по производительности приложениях.

Windows Forms и WPF предоставляют собственные средства для работы с графикой. Windows Forms предоставляет простые инструменты для рисования и отображения графики на форме, а WPF обеспечивает более гибкий и мощный доступ к графическим возможностям Windows.

В дополнение к базовым инструментам Windows, существуют также сторонние библиотеки, такие как OpenGL, OpenCV, Cairo и SDL, которые предлагают еще больший функционал и возможности для работы с графикой.

Выбор инструментов и библиотек для работы с графикой зависит от ваших потребностей, знаний и предпочтений. При разработке графических приложений на Windows стоит изучить различные инструменты и выбрать наиболее подходящие для ваших задач.

  • Глава 1. Использование графики в Windows приложениях

    • Параграф 1. Где и как возможно отображать графическую информацию в Windows приложениях

    • Параграф 2. Создание линейных графиков

    • Параграф 3. Cоздание гистограмм

    • Параграф 4. Круговые диаграммы и элементы 3D графики

    • Параграф 5. Базовый класс для рисования графиков

  • Глава 2. Основы работа с графикой в Web приложениях

    • Параграф 1. Два подхода к отображению графической информации в ASP.NET

    • Параграф 2. О возможностях
      преобразований графических файлов при их отображении на сайте

Аннотация: Весь изложенный материал, без особого труда, можно найти
в MSDN и сети, но для большинства статей характерен уход в вопросы,
которые затрудняют создание четкого и ясного представления о том,
как работать с графикой. При решении корпоративной задачи вывода графиков
в Web приложении, автору сначала потребовалось обобщить материал, и, лишь
после того, приступить собственно к выполнению задания.
Материалы этого обобщения и приводятся в данной статье.

PS: Материал первоначально касался только отображения графики на Web страницах,
но поступающие от читателей вопросы по использованию графики в Windows стали причиной
дополнения данного материала еще одной главой, которую, в силу методических соображений,
автор поместил первой.

В начало

Глава 1. Использование графики в Windows приложениях

В начало

Параграф 1. Где и как возможно отображать графическую информацию в Windows приложениях

В Visual Studio Net нет стандартных средств для создания графиков, диаграмм…
Поэтому, большинство статей на рассматриваемую тему рекомендуют использовать
Office Web Components (OWC) — компонент Windows для построения диаграмм в Web.
Как достоинство этого подхода обычно отмечается простота построения графиков и
диаграмм и интуитивно близкие задания параметров графической информации с их
заданием при использовании графики в MS Office.
Но простота не всегда достоинство, когда мы строим график «под себя».
Да и достаточно для программиста сомнительна, когда он однажды создав код
графического класса, он всегда легко и быстро может его перенастроить его
для конкретной задачи, не влезая в дебри некого универсального кода, где порой
гораздо больше времени будет затрачено на уяснение параметров и задание их
значений и границ.
Поэтому, речь далее пойдет о создании собственных графиков с «чистого листа».
Основные типы графиков, которые будут рассмотрены, по мнению автора, могут стать
достаточными для большинства практических задач и базой
для дальнейших собственных разработок.

В Microsoft Windows существует несколько средств для вывода графической информации,
включая DirectDraw, OpenGL, GDI и т.д. Мы будем использовать
Graphics Device Interface (GDI, более поздние версии GDI+) — подсистему Windows,
ответственную за вывод графики и текста на дисплей и принтер. Именно CGI+ обеспечивает
вывод на экран всего того, что видит пользователь Windows в окне монитора.
GDI+ является базовым способом вывода графики в Windows.

CGI+ — это библиотека функций и программных средств на базе API,
позволяющая приложениям абстрагироваться от особенностей конкретного
графического оборудования. .Net Framework
использует расширенный графический интерфейс GDI+.

С графикой Windows (при использованием GDI+) связано понятия контекста устройства
(device context, DC). Это структура данных, содержащая информацию о параметрах и
атрибутах вывода графики на устройство (дисплей, принтер…).
.Net Framework освобождает программиста от необходимости обращения к DC.
Основной объект для графического отображения Graphics — класс имитации поверхности
рисования в GDI+. Класс не имеет конструкторов по причине того, что объекты этого
класса зависят от контекста конкретных устройств вывода. Создаются объекты
специальными методами разных классов, например, он может быть создан из объекта
Bitmap, или к нему можно получить доступ, как к некоторому объекту, инкапсулированному
в некоторые контролы, в том числе, и в объект формы приложения. Метод CreateGraphics
класса Control — наследника класса Form — возвращает объект,
ассоциированный с выводом графики на форму.

Рассмотрим простейшие примеры. Создадим решение Windows приложения с одной
кнопкой и следующим обработчиком ее нажатия:

private void button1_Click(object sender, EventArgs e)
{
 //Создаем битовую матрицу
 Bitmap bitmap = new Bitmap(ClientSize.Width, ClientSize.Height);
 //Создаем объект класса Graphics на основе битовой матрицы
 Graphics graph = Graphics.FromImage(bitmap);
 //Рисуем линию на поверхности, Graphics использует в качестве нее 
 //битовую матрицу
 graph.DrawLine(new Pen(Color.Red,2), 0, 0, ClientSize.Width, ClientSize.Height);
 //Присваиваем нарисованноу свойству формы
 BackgroundImage = bitmap;
 graph.Dispose();
}

Результат вывода при нажатии кнопки 1, показан на Рис.1:

graph_01.gif

Рис.1. Создание и использование объект класса Graphics на основе битовой матрицы

Одинакового эффекта (Рис.1.), можно добиться, если использовать
обработчики некоторых событий, которым передается объект класса Graphics как
аргумент (например, обработчик события Paint формы приложения):

private void Form1_Paint(object sender, PaintEventArgs e)
{
 e.Graphics.DrawLine(new Pen(Color.Red, 2), 0, 0, ClientSize.Width, ClientSize.Height);
}

Одинакового эффекта (Рис.1.), можно добиться и при непосредственном создании
объекта Graphics:

private void button1_Click(object sender, EventArgs e)
{
 Graphics graph = CreateGraphics();
 graph.DrawLine(new Pen(Color.Red, 2), 0, 0, ClientSize.Width, ClientSize.Height);
 graph.Dispose();
}

А так можно рисовать (писать) на кнопке и на других контролах, для которых
может быть создан обработчик события Paint
(Рис.2.):

private void button1_Paint(object sender, PaintEventArgs e)
{
 e.Graphics.DrawLine(new Pen(Color.Red, 2), 0, 0, button1.Width, button1.Height);
}

Класс Graphics находятся в пространстве имен Drawing (хотя, забегая вперед, нам понадобится
и пространство имен Drawing.Drawing2D, по сему, целесообразно сразу добавить их в решение).

using System.Drawing;
using System.Drawing.Drawing2D;

Для того чтобы рисовать в окне формы Windows приложения, необходимо иметь не только
связанный с этим окном объект Graphics, как холст для рисования, но и инструменты рисования.
Используют два основных инструмента: карандаш и кисть. Карандаш мы использовали в примерах, приведенных
выше. В конструкторе класса Pen задается цвет пера и можно задать толщину (по умолчанию 1).
Класс Brush определяет кисти для рисования. Это абстрактный класс.
Кисти этого класса создать нельзя, хотя можно создавать кисти классов-потомков Brush:

  • SolidBrush — сплошная закраска цветом кисти;

  • TextureBrush — наложение картинки (image) на область закраски;

  • HatchBrush — закраска области предопределенным узором;

  • LinearGradientBrush — сплошная закраска c переходом цвета кисти (градиентная закраска);

  • PathGradientBrush -сплошная закраска c переходом цвета кисти по специальному алгоритму.

Пример использования конструктора HatchBrush показан на Рис.2.:

private void button1_Click(object sender, EventArgs e)
{
 Graphics graph = CreateGraphics();
 graph.DrawLine(new Pen(Color.Blue, 2), 0, 0, ClientSize.Width, ClientSize.Height);
 graph.FillEllipse(new HatchBrush(HatchStyle.Percent05, Color.Silver), 0, 0, 
                                            ClientSize.Width, ClientSize.Height);
 graph.Dispose();
}

graph_02.gif

Рис.2. Пример использования конструктора HatchBrush

Как видно из приведенных примеров, существует несколько способов отображение
графики. Основное их отличие — при использовании битовой матрицы и свойств
контролов изображение рисуется один раз и не исчезает (нет необходимости его вновь
рисовать) при перерисовки формы.

Здесь нет необходимости подробно описывать все методы объекта Graphics, их можно легко увидеть из контекстной подсказки, как обычно, поставив точку после написания имени объекта. По этой же причине нет необходимости перечислять и преопределенные цвета карандашей (Color.) и кистей (Brushes.)

Отметим еще два важных момента, которые нам понадобятся далее:

  1. Круговые фигуры рисуются при указании в качестве стартовой точки левого верхнего угла
    (см. последний пример);

  2. Использование событий Paint для отображения графики не всегда оправдано из-за
    необходимости постоянной перерисовки изображения при перерисовке формы.

Рисование непосредственно на форме не всегда является и не есть хороший тон, кода
Visual Studio предлагает специальный контрол, который, как нельзя лучше, подходит для вывода графической
информации и обладает всеми преимуществами контролов: программное позиционирование и
масштабирование без перерисовки формы, возможность выполнять Stretch Image и множество
других полезных свойств и событий. Кроме того простой доступ к Graphics, аналогично
через событии Paint и возможность использования битовых карт Bitmap для создания
объекта Graphics, с последующим переносом их в контрол (аналогично, как мы делали это в первом примере):

pictureBox1.Image = bitmap; 

Далее мы будем использовать именно PictureBox, как объект для отображения графической информации.

В начало

Параграф 2. Создание линейных графиков

2.1. В качестве постановки задачи

Однажды мне пришлось делать задачку, на базе продолжения работы над которой появилась
программа «LitFrequencyMeter» — программа определения частоты повторения слов и знаков в
литературных произведениях, графики из которой приведены ниже (Рис.3-5.). И когда я
приступил к заключительному этапу работы над программой, то понял, что материал главы
практически написан в кодах — осталось только его озвучить.

Что и какие аспекты были заложены в программу:

  1. Настройка всего и вся: цветов, шрифтов, фонов, надписей, положения диаграмм на
    холсте, использование пояснений, легенд, смена числа отсчетов и т.д., и т.п.

  2. Хранение всех настроек (включая шрифты) в реестре.

  3. Создание статистики из .txt, .html, .doc файлов.

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

сказка о царе салтане ,
о сыне его славном и могучем богатыре
князе гвидоне салтановиче и
о прекрасной царевне лебеди
три девицы под окном
пряли поздно вечерком .
" кабы я была царица , -
говорит одна девица , -
то на весь крещеный мир
приготовила б я пир " .
- " кабы я была царица , -
говорит ее сестрица , -
то на весь бы мир одна
наткала я полотна " .
- " кабы я была царица , -
третья молвила сестрица , -
я б для батюшки - царя
родила богатыря " .
только вымолвить успела ,
дверь тихонько заскрыпела ,
и в светлицу входит царь ,
стороны той государь .

Далее результаты анализа в графическом виде (программа представляет и текстовый
вариант анализа, но он нам на данном этапе не нужен).

graph_04.gif

Рис.3. Линейеая диаграмма

graph_04.gif

Рис.4. Гистограмма

graph_05.gif

Рис.5. Круговая диаграмма

2.2. Постановка задачи

Стоит задача создать класс для отображения графической информации, который бы
мог стать базовым классом для работы с графикой, позволял
бы не только выводить различные виды графиков и обладал бы
гибкостью настройки форм отображения, но и оставался открытым для дальнейшего
его расширения.

Конечная цель — помещение созданного графического изображения в элемент управления PictureBox.

2.3. Исходные данные

Исходные данные перед их отображением могут находиться где угодно (файл, таблица базы данных…).
Однако рассматривать чтения из базы данных или из файла значений графиков — только засорять
отображение материала. Мы всегда можем прочитать данные с любого источника в массив значений.
Автор предпочитает работать со строковым массивом, как позволяющим хранить цифровые и текстовые
значения. В примерах, приводимых ниже, используется массив строк string[,] rgsValues. В программе, о которой
шла выше речь, этот массив использован для настройки параметров, отображаемых на графике.
Заполнять массив будем с помощью датчиков случайных чисел:

private int viNumInRg=20;//20 - начальное значение
private string[,] rgsValues=null;

private int iCreateRg()
{
 Random rnd = new Random(DateTime.Now.Millisecond);
 Random rnd1 = new Random(DateTime.Now.Millisecond+5);
 rgsValues = new string[viNumInRg, 2];
 for (int i = 0; i < viNumInRg; i++)
 {
  rgsValues[i, 0] = Convert.ToString(((float)(rnd.Next(0, 10) * 100) + 
                                  (float)rnd1.Next(0, 99)) / (float)100);
  rgsValues[i, 1] = "I-" + Convert.ToString(i+1);
 }

Предполагается, что значение переменной, определяющий размерность массива
хранится в настройках и устанавливается на этапе загрузки приложения.

2.4. Проект решения

Создадим простой проект WindowsApplication решения с любым именем (у меня graph1).
Поместим на форму три кнопки, в свойствах «Текст» которых напишем соответственно:
«Линейная диаграмма», «Гистонрамма» и «Круговая диаграмма». Ниже кнопок поместим
контрол PictureBox. Подберем удобное для себя расположение кнопок и PictureBox
(в реальных программах для размещения удобнее использовать контролы TableLayoutPanel,
но сейчас нас интересует графика, а не размещение).

В окне Solutation Explorer кликаем правой кнопкой мышки на узле решения (у меня graph1)
и в контекстном меню выбираем Add\New Item. В окне Templates выбираем Class, даем ему имя,
например PaintCl.cs и нажимаем кнопку Add.
Будет создан пустой класс.

using System;
using System.Collections.Generic;
using System.Text;
namespace graph1
{
 class PaintCl
 {
 }
}

Нашей задачей будет постепенное наполнение этого класса при
минимуме добавления кода в основной файл кода приложения — Form1.cs.

Для начала создадим обработчик события нажатия кнопки
«Линейный график» (клик мышкой на кнопке), а также обработчики для
событий Load и FormClozed (первый можно кликом мышки на форме, второй через окно
Properties формы — закладка Events — клик в окошечке против события FormClosed).
Слегка преобразуем код, как показано ниже:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace graph1
{
 public partial class Form1 : Form
 {
  private int viNumButton = 0;
  private int viNumInRg=20;//20 - начальное значение
  private string[,] rgsValues=null;

  public Form1()
  {
      InitializeComponent();
  }


  private void Form1_Load(object sender, EventArgs e)
  {
      //Здесь при создании реальной программы необходимо 
      //будет предусмотреть восстановление сохраненных 
      //параметров для приложения и графиков
  }
  private void Form1_FormClosed(object sender, FormClosedEventArgs e)
  {
      //Здесь при создании реальной программы необходимо 
      //будет предусмотреть сохранение параметров
      //для приложения и графиков
  }


  #region Создание массива значений
  private void vCreateRg()
  {
   Random rnd = new Random(DateTime.Now.Millisecond);
   Random rnd1 = new Random(DateTime.Now.Millisecond+5);
   rgsValues = new string[viNumInRg, 2];
   for (int i = 0; i < viNumInRg; i++)
   {
    rgsValues[i, 0] = Convert.ToString(((float)(rnd.Next(0, 10) * 100) + 
                                  (float)rnd1.Next(0, 99)) / (float)100);
    rgsValues[i, 1] = "I-" + Convert.ToString(i+1);
   }
  }
  #endregion

  #region создание линейного графика
  private void button1_Click(object sender, EventArgs e)
  {
     viNumButton = 1;
     vCreateLinGr();            
  }
  private void vCreateLinGr()
  {
   //Создаем массив значений для вывода на графике
   vCreateRg();

  }
  #endregion
 }
}

Назначение переменной viNumButton, будет ясно далее. Массив значений у нас создан.
Осталось нарисовать по значениям массива график, используя класс.

2.5. Конструкторы класса

Начало класса — конструктор и закрытые переменные. В классе лучше иметь несколько конструкторов.
Например, таких как приведено в коде ниже. И, естественно, необходимо сразу определить
основные объекты для графики: битовую матрицу, объект Graphiks, шрифт, кисть и перо
(о чем мы говорили выше), а также переменные для сохранения размеров холста:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
namespace graph1
{
 class PaintCl
 {
   //Основные объекты для рисования
   private Bitmap bmp = null;
   private Graphics graph = null;
   private Font objFont = new Font("Arial", 8, FontStyle.Bold);
   private Brush objBrush = Brushes.Black;        
   private Pen objPenLine = new Pen(Color.Black, 1);
   //Размеры холста
   private int viX = 200;
   private int viY = 100;

   #region Конструкторы
   //Первый
   public PaintCl()
   {
      
   }
   //Второй
   public PaintCl(int a, int b)
   {
     bmp = new Bitmap(a, b);
     //Создаем объект Graphics на основе битовой матрицы 
     graph = Graphics.FromImage(bmp);
     //Запоминаем размеры холста
     viX = a;
     viY = b;            
   }
   #endregion
 }
}

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

2.6. Создаем объект для рисования

Используем второй конструктор и создадим и инициализируем в классе сразу
все объекты, которые нам необходимы:

private void vCreateLinGr()
{
   //Создаем массив значений для вывода на графике
   vCreateRg();
   //Создаем класс и передаем ему размер холсты
   PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
   //Передадим фон холста в класс
   clPaint.vSetBackground(Color.Red);      
   //Принимаем нарисованное в pictureBox
   pictureBox1.Image = clPaint.Bmp;
}

Таким образом, нам понадобятся в классе еще две функции:
установки фона холста и приема изображения из класса. Добавим их в класс:

#region Установка цвет фона диаграммы 
public void vSetBackground(Color bcl)
{
 graph.Clear(bcl);
}
#endregion

#region Доступ к переменным класса
public Bitmap Bmp
{
 get {return bmp;}
}
#endregion

Выполним решение на данном этапе. Результат показан на Рис.6.:

graph_06.gif

Рис.6. Взаимодействие кода формы с кодом класса

В принципе, описанный выше код показал сам принцип создания изображения с
использованием PictureBox и создаваемого нами и претендующего на универсальность
класса для рисования графиков.

2.7. Рисуем оси

Добавим в классе переменные для хранения отступов от краев холста (они нам еще
понадобятся не один раз).

//Отступы от краев холста
private int viDeltaaxisL = 50;
private int viDeltaaxisR = 50;
private int viDeltaaxisH = 20;

Добавим функцию рисования осей и функции запоминания цвета и толщины осей.
Функция выполняет простую задачу — рисует две линии и при необходимости
стрелочки на конце осей:

#region Рисование Осей
//Параметры вызоыва: отступы слева - deltaaxisL, справа - deltaaxisR, 
//сверху(снизу) - deltaaxisH, Цвет осей - colorpenaxis, толщина пера - widthpen, 
//нужны ли стрелки - fArrow (true - да)
public void vDravAxis(int deltaaxisL, int deltaaxisR, 
             int deltaaxisH, Color colorpenaxis, int widthpen, bool fArrow)
{
 //Запоминаем отступы                        
 viDeltaaxisL = deltaaxisL;
 viDeltaaxisR = deltaaxisR;
 viDeltaaxisH = deltaaxisH;
 //Запоминаем цвет осей и толщину
 vSetPenColorLine(colorpenaxis);
 if (widthpen > 0) vSetPenWidthLine(widthpen);
 //Точка начала рисования по х и y           
 int x = deltaaxisL;
 int y = viY - deltaaxisH;
 int x1 = viX - deltaaxisR;
 int y1 = deltaaxisH;
 //Переменная определения длины стрелок
 int d = 0;
 if(fArrow) d = widthpen * 10;
 //Оси на d пикселей длинней для стрелок
 graph.DrawLine(objPenLine, x, y, x1 + d, y);
 graph.DrawLine(objPenLine, x, y, x, y1 - d);
 //Надо рисовать стрелки
 if (fArrow)
 {
  int a = 10 * (int)objPenLine.Width;
  int b = 2 * (int)objPenLine.Width;
  int x2 = x1 - a;
  int y2 = y + b;
  //Стрелки
  graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
  y2 = y - b;
  graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
  x2 = x - b;
  y2 = y1 + a;
  graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
  x2 = x + b;
  graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
 }
}
#endregion

#region Карандаш, шрифт, кисть
//Цвет карандаша
public void vSetPenColorLine(Color pcl)
{
 if (objPenLine == null)
 {
  objPenLine = new Pen(Color.Black, 1);
 }
 objPenLine.Color = pcl;
}
//Установка толщина карандаша        
public void vSetPenWidthLine(int penwidth)
{
 if (objPenLine == null)
 {
   objPenLine = new Pen(Color.Black, 1);
 }
 objPenLine.Width = penwidth;
}
#endregion

Осталось добавить вызов функции рисования осей:

private void vCreateLinGr()
{
  //Создаем массив значений для вывода на графике
  vCreateRg();
  //Создаем класс и передаем ему размер холсты
  PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
  //Фон холста
  clPaint.vSetBackground(Color.White);
  //Параметры вызоыва: отступы слева, справа, сверху(снизу),
  //Цвет осей, толщина пера, необходимость стрелок
  clPaint.vDravAxis(50, 50, 20, Color.Red, 2,true);
  //Принимаем нарисованное в pictureBox
  pictureBox1.Image = clPaint.Bmp ;
}

В функции vDravAxis мы задали параметры непосредственно. Отметим еще раз,
что все величины целесообразно иметь настраиваемыми и их значения
хранить в реестре.

graph_07.gif

Рис.7 Рисование осей линейного графика

2.8. Рисуем сетку

Для рисования сетки нам потребуется: цвет и толщина пера, размер массива отображаемых
значений и непосредственно функция для рисования сетки.
Установку цвета и толщины пера мы уже использовали при рисовании осей, поэтому
в функции vCreateLinGr() добавим вновь вызовы:

//Параметры линии для сетки
clPaint.vSetPenWidthLine(1);
clPaint.vSetPenColorLine(Color.Silver);

Для хранения размера массива в классе определим переменную и определим доступ к ней
через свойство, а также определим функцию, рисующую сетку viMaxRg*viMaxRg клеток. Рисование
сетки сводится к рисованию параллельных осям линий:

//Максимальный размер массива
private int viMaxRg = 20;

public int MaxRg
{
 set { viMaxRg=value; }
}

#region Рисование сетки
public void vDravGrid()
{
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float x1 = viX - viDeltaaxisR;
 float y1 = viDeltaaxisH;
 //Сдвиг линий сетки на один отсчет по Y
 float f = (y - y1) / (float)viMaxRg;
 //Рисуем горизонтальные линии
 for (int i = 1; i < viMaxRg + 1; i++)
 {
  graph.DrawLine(objPenLine, x, y - f * i, x1, y - f * i);
 }
 //Сдвиг линий сетки на один отсчет по X
 f = (x - x1) / (float)(viMaxRg - 1);
 //Рисуем вертикальные линии
 for (int i = 1; i < viMaxRg; i++)
 {
  graph.DrawLine(objPenLine, x - f * i, y, x - f * i, y1);
 }
}
#endregion

В функции vCreateLinGr() добавим код и выполним решение:

clPaint.MaxRg = 20;
clPaint.vDravGrid();

Результат показан на Рис.8.:

graph_08.gif

Рис.8. Рисование сетки

2.8. Рисуем линию графика

Как мы уже делали — зададим цвет и толщину пера. Далее нам понадобятся
данные из нашего массива значений непосредственно в классе. Для этого
в классе определим массив и доступ к нему:

private string[,] rgsValues = null;

public string[,] RgValue
{
 set { rgsValues = value; }
}

В классе создадим функцию рисования линий графика. Линии рисуются по соседним
точкам массива:

#region Рисование линий графика для линейного графика
public void vDrawGraphLines()
{
 string s = string.Empty;
 string s1 = string.Empty;
 string s2 = string.Empty;
 float f = 0;
 float f1 = 0;
 float x1 = 0;
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float x2 = 0;
 float fMax = float.MinValue;
 //Ищем максимальное значение по оси Y
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для рисования по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на одну единицу массива значения по X
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 //Пикселей для рисования по оси y
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на одну единицу массива значений по Y
 fdeltay = fdeltay / fMax;
 for (int i = 0; i < viMaxRg; i++)
 {
  //Первый раз запоминаем точку старта
  if (i == 0)
  {
    s = rgsValues[i, 0];
    s2 = rgsValues[i, 1];
    f = y - (float.Parse(s) * fdeltay);
    x1 = x;
  }
  else
  {   
    //Здесь рисуем линии
    s1 = rgsValues[i, 0];
    f1 = y - (float.Parse(s1) * fdeltay);
    x2 = x + (int)(fdeltax * i);
    graph.DrawLine(objPenLine, x1, f, x2, f1);
    //Запоминаем координаты конечной точки, точки 
    //начала следующего отрезка линии
    s = rgsValues[i, 0];
    s2 = rgsValues[i, 1];
    f = f1;
    x1 = x + (int)(i * fdeltax);
  }
 }
}
#endregion

Код vCreateLinGr() на данный момент:

private void vCreateLinGr()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холста
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Параметры вызоыва: отступы слева, справа, 
 //сверху(снизу),Цвет осей, толщина пера
 clPaint.vDravAxis(50, 50, 30, Color.Red, 2,true);
 //Цвет и толщина пера
 clPaint.vSetPenWidthLine(1);
 clPaint.vSetPenColorLine(Color.Silver);
 clPaint.MaxRg = 20;
 //Рисуем сетку
 clPaint.vDravGrid();
 //Цвет и толщина пера
 clPaint.vSetPenWidthLine(2);
 clPaint.vSetPenColorLine(Color.Green);
 //Передаем массив значений в класс
 clPaint.RgValue = rgsValues;
 //Рисуем линии графика
 clPaint.vDrawGraphLines();
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;
}

Результат выполнения решения на данном этапе показан на Рис.9.:

graph_09.gif

Рис.9. Рисование линий графика

2.10. Надписи на графике

Надписи можно наносить по оси Х, по оси Y и над точками линий графика.
Причем иногда бывает целесообразно выполнять соседние надписи со сдвигом
по оси Y. Кроме того — надписи выполняются не пером, а кистями и требуют задания
шрифта. Таким образом, перед выполнением надписей надо установить в классе
соответственно шрифт и кисть (Brush).

Для передачи шрифта и кисти создадим в классе свойства:

public Brush  brush
{
 set { objBrush  = value; }
}
public Font font
{
 set { objFont = value; }
}

В функции рисования графика запишем код:

Font objFont = new Font("Arial", 12, FontStyle.Bold | FontStyle.Italic);
clPaint.font = objFont;
clPaint.brush = Brushes.Blue;

Для выполнения различных надписей создадим в классе несколько функций. Подробно
давать пояснения нет необходимости. Здесь, как и при рисовании линий, необходимо
постоянно рассчитывать дельны расстояний по осям и точки начала надписей.

#region Текст по оси X - Цифры отсчетов
//Параметр: false - соседние значения без сдвига по оси Y
//          true  - соседние значения со здвигом по оси Y
public void vDrawTextAxXNumber(bool f)
{
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH + objPenLine.Width;
 for (int i = 1; i < viMaxRg + 1; i++)
 {
  if (!f || i % 2 == 0)
  {
   graph.DrawString(Convert.ToString(i), objFont, objBrush, x + (i - 1) * fdeltax, y);
  }
  else
  {
   graph.DrawString(Convert.ToString(i), 
    objFont, objBrush, x + (i - 1) * fdeltax, y + objFont.Size);
  }
 }
}
#endregion

#region Текст по оси X -  Параметр массива
//Параметр: false - соседние значения без сдвига по оси Y
//          true  - соседние значения со здвигом по оси Y
public void vDrawTextAxXValues(bool f)
{
 string s = string.Empty;
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;// +objPenLine.Width;
 for (int i = 0; i < viMaxRg; i++)
 {
  if (!f || i % 2 == 0)
  {
   graph.DrawString(rgsValues[i, 1], objFont, objBrush, x + i * fdeltax, y);
  }
  else
  {
   graph.DrawString(rgsValues[i, 1], objFont, objBrush, x + i * fdeltax, y + objFont.Size);
  }
 }
}
#endregion

#region Текст по оси Y - Значения по отсчетам сетки оси Y
public void vDrawTextAxYValues()
{
 string s = string.Empty;
 float f = 0;
 float fMax = float.MinValue;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 f = fMax / (float)(viMaxRg - 1);
 //Пикселей для надписей по оси х
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на один отсчет
 fdeltay = fdeltay / (float)(viMaxRg - 1);
 float y = viY - viDeltaaxisH - objFont.Size;
 for (int i = 0; i < viMaxRg; i++)
 {
  graph.DrawString(((float)(i * f)).ToString("0.00"), 
     objFont, objBrush, viDeltaaxisL - (objFont.Size) * 5 - 5, y - i * fdeltay);
 }
}
#endregion

#region Надписи - Значения  над точкой 
//1 параметр = false - без отображения процентов, true - с отображением
//2 параметр = false - без сдвига, true - со здвигом по оси Y
public void vDrawTextAxYValuesPoint(bool a, bool b)
{
 string s = string.Empty;
 float fMax = float.MinValue;
 float fSum = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет по х
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float fdeltay = viY - 2 * viDeltaaxisH;
 float y = viY - viDeltaaxisH - objFont.Size;
 //Пикселей на одну единицу
 fdeltay = fdeltay / fMax;
 float fdelta = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
   if (a)
   {
    if (i % 2 == 0) fdelta = objFont.Size;
      else fdelta = 2 * objFont.Size;
   }
   else
   {
     fdelta = objFont.Size;
   }
   if (b)
   {
    graph.DrawString(rgsValues[i, 0], objFont, objBrush, x + i * fdeltax,
           y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
   }
   else
   {
    float fp = float.Parse(rgsValues[i, 0]);
    fp = (fp * 100) / fSum;
    graph.DrawString(rgsValues[i, 0] + "-" + fp.ToString("0.0") + "%", 
                    objFont, objBrush, x + i * fdeltax,
                              y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
   }
 }
}
#endregion

Мы создали полностью код для отображения линейного графика.
Все функции для управления построением и изменения внешнего вида
представлены в void vCreateLinGr():

private void vCreateLinGr()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холсты
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Параметры вызоыва: отступы слева, справа, сверху(снизу),Цвет осей, толщина пера
 clPaint.vDravAxis(50, 50, 30, Color.Red, 2,true);
 clPaint.vSetPenWidthLine(1);
 clPaint.vSetPenColorLine(Color.Silver);
 clPaint.MaxRg = 20;
 clPaint.vDravGrid();
 clPaint.vSetPenWidthLine(2);
 clPaint.vSetPenColorLine(Color.Green);
 clPaint.RgValue = rgsValues;
 clPaint.vDrawGraphLines();
 Font objFont = new Font("Arial", 7, FontStyle.Bold | FontStyle.Italic);
 clPaint.font = objFont;
 clPaint.brush = Brushes.Blue;
 //Здесь необходимо поэксперементировать с
 //использованием различных надписей и изменением параметров
 clPaint.vDrawTextAxXNumber(false);
 //clPaint.vDrawTextAxXValues(true);
 clPaint.vDrawTextAxYValues();
 clPaint.vDrawTextAxYValuesPoint(true,false);
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;        
}

Результат выполнения кода показан на Рис.10.:

graph_10.gif

Рис.10. Линейный график

В начало

Параграф 3. Создание гистограмм

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

private Brush[] br ={
Brushes.LightGreen,Brushes.Chartreuse,Brushes.LimeGreen,Brushes.Green,Brushes.DarkGreen,
Brushes.DarkOliveGreen,Brushes.LightPink,Brushes.LightSeaGreen,Brushes.LightCoral,Brushes.DarkCyan ,
Brushes.Crimson,Brushes.CornflowerBlue ,Brushes.Chocolate,Brushes.CadetBlue,Brushes.BlueViolet, 
Brushes.Maroon, Brushes.Blue,Brushes.Brown,Brushes.DarkBlue, Brushes.Red,
Brushes.Coral,Brushes.DarkRed, Brushes.DarkMagenta, Brushes.DarkOrange,Brushes.DarkOrchid};

И так, нам понадобится всего лишь одна новая функция. Основное отличие —
использование функции FillRectangle.

#region Рисование Гистограммы
//Параметры: a=0 без сдвига цвета a=1 со сдвигом
//b = 0 - без разрыва столбиков > 1 - с разрывом и величина разрыва в %
public void vDrawGraphRectangular(int a, int c)
{
 string s = string.Empty;
 string s1 = string.Empty;
 string s2 = string.Empty;
 float f = 0;
 float x1 = 0;
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float fMax = float.MinValue;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для рисования по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 //Пикселей для рисования по оси y
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на одну единицу массива значений
 fdeltay = fdeltay / fMax;
 float fdx = 0;
 if (c != 0) fdx = (fdeltax * c / 100) / 2;
 Random rand = new Random(DateTime.Now.Millisecond);
 int arn = rand.Next((int)br.Length);
 objBrush = br[arn];
 for (int i = 0; i < viMaxRg - 1; i++)
 {
  s = rgsValues[i, 0];
  f = float.Parse(s);
  x1 = x + ((float)i * fdeltax);
  if (a == 0)
  {
   graph.FillRectangle(objBrush, x1 + fdx, y - fdeltay * f, 
                                   fdeltax - 2 * fdx, fdeltay * f);
  }
  else
  {
   int b = i % br.Length;
   graph.FillRectangle(br[b], x1 + fdx, y - fdeltay * f, 
                                   fdeltax - 2 * fdx, fdeltay * f);
  }
  if (i == viMaxRg - 2)
  {
   int b = (i + 1) % br.Length;
   s = rgsValues[i + 1, 0];
   f = float.Parse(s);
   x1 = x + ((float)(i + 1) * fdeltax);
   if (a == 0)
   {
    graph.FillRectangle(objBrush, x1 - 2, y - fdeltay * f, 
                                         4/*fdeltax*/, fdeltay * f);
   }
   else
   {
    graph.FillRectangle(br[b], x1 - 2, y - fdeltay * f, 
                                         4/*fdeltax*/, fdeltay * f);
   }
  }
 }
}
#endregion

Запишем код обработки нажатия кнопки 2 и выполним решение:

private void button2_Click(object sender, EventArgs e)
{
 viNumButton = 2;
 vCreateRectangleDiagramm();
}
private void vCreateRectangleDiagramm()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холсты
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Параметры вызоыва: отступы слева, справа, сверху(снизу),Цвет осей, толщина пера
 clPaint.vDravAxis(50, 50, 30, Color.Red, 2,true);
 clPaint.vSetPenWidthLine(1);
 clPaint.vSetPenColorLine(Color.Silver);
 clPaint.MaxRg = 20;
 clPaint.vDravGrid();
 clPaint.vSetPenWidthLine(2);
 clPaint.vSetPenColorLine(Color.Green);
 clPaint.RgValue = rgsValues;
 //a=0 без сдвига цвета a=1 со сдвигом,b = 0 - без разрыва, > 1 - с разрывом и величина разрыва в %
 clPaint.vDrawGraphRectangular(1, 5);
 Font objFont = new Font("Arial", 7, FontStyle.Bold | FontStyle.Italic);
 clPaint.font = objFont;
 clPaint.brush = Brushes.Blue;
 clPaint.vDrawTextAxXNumber(false);
 //clPaint.vDrawTextAxXValues(true);
 clPaint.vDrawTextAxYValues();
 clPaint.vDrawTextAxYValuesPoint(true, false);
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;    
}

Цветом показано единственное отличие от кода создания линейной диаграммы.
Результат работы кода приведен на Рис.11.:

graph_11.gif

Рис.11. Гистограмма

В начало

Параграф 4. Круговые диаграммы и элементы 3D графики

Построение круговых диаграмм с элементами 3D графики требует несколько больших
затрат по сравнению с рассмотренным выше материалом. Прежде всего, необходимо
определить дополнительные переменные для величин: оси эллипса (vfDiamX, vfDiamY),
центр круговой диаграммы (vfXcirc, vfYcirc). Кроме того, если мы хотим,
что бы в легенде (пояснению к графику) цвета надписей соответствовали
цветам секторов диаграммы, то потребуется задать массив цветов однозначно соответствующий
массиву цветов кистей. Зададим в классе:

private float vfDiamX = 100;
private float vfDiamY = 100;
private float vfXcirc = 100;
private float vfYcirc = 100;
private Color[] color ={
Color.LightGreen,Color.Chartreuse,Color.LimeGreen,Color.Green,Color.DarkGreen,
Color.DarkOliveGreen,Color.LightPink ,Color.LightSeaGreen,Color.LightCoral ,Color.DarkCyan ,
Color.Crimson , Color.CornflowerBlue ,Color.Chocolate,Color.CadetBlue,Color.BlueViolet,
Color.Maroon,Color.Blue,Color.Brown,Color.DarkBlue, Color.Red,
Color.Coral,Color.DarkRed, Color.DarkMagenta, Color.DarkOrange,Color.DarkOrchid};

Основная функция для рисования диаграммы имеет ряд особенностей, связанных с формированием
объемности и расположением надписей.

Рисовать диаграмму будем в несколько этапов:

  • Первый этап: Против часовой стрелки рисуем последний сектор и сектора,
    выходящие за границу 180 градусов, но не заходящие за границу 270 градусов.
    Рисовать будем кистью с прозрачностью, например 25%, и каждый из них со сдвигом
    на 1 пиксель вниз. Иначе, если толщина диаграммы задана 20 пикселей, то
    сектор потребуется нарисовать 20 раз, каждый раз сдвигая на 1 пиксель
    вниз (Рис.12.1.).

    graph_12_1.gif

    Рис.12.1. Первый этап создания круговой диаграммы

  • Второй этап: Накладываем на данную диаграмму
    сектора от 0 градусов до сектора, заходящего за 270 градусов, используя SolidBrush
    и не выполняя сдвиг — рисуем каждый сектор один раз из точки рисования всей диаграммы
    (Рис.12.2.).

    graph_12_2.gif

    Рис.12.2. Второй этап создания круговой диаграммы

  • Третий этап: по часовой стрелки рисуем сектора, начиная со второго,
    используя HatchBrush. Рисование выполняем до сектора заходящего за границу -90 градусов, со сдвигом на толщину диаграммы (Рис.12.3.).

    graph_12_3.gif

    Рис.12.3. Третий этап создания круговой диаграммы

  • Четвертый этап: По часовой стрелке накладываем без сдвига, начиная со второго
    сектора до сектора, заходящего за границу -90 градусов, используя SolidBrush
    (Рис.12.4.).

    graph_12_4.gif

    Рис.12.4. Четвертый этап создания круговой диаграммы

  • Отдельно рисуем первый сектор, сначала используя
    HatchBrush со сдвигом на толщину диаграммы, затем накладываем сектор SolidBrush
    без сдвига. Координаты сектора определяем с учетом параметров сдвига секторов (Рис.12.5.).

    graph_12_5.gif

    Рис.12.5. Пятый этап создания круговой диаграммы

Алгоритм рисования можно упростить, например, один раз и последним этапом
наложить сектора эллипса, нарисованный кистью SolidBrush, но, в этом случае пострадает наглядность.

Эти этапы рисования выполняет следующая функция:

#region  vDravCircle3D
//Параметры - Отступ от краев по X слева deltaaxisL, от краев по Y справа deltaaxisR,
//deltaaxisH - отступа сверху и снизу, толщина диаграммы viH, сдвиг сектора viDx, viDy
public void vDravCircle3D(int deltaaxisL, int deltaaxisR, 
                 int deltaaxisH, int viH, int viDx, int viDy)
{
 //Запоминаем отступы            
 viDeltaaxisL = deltaaxisL;
 viDeltaaxisR = deltaaxisR;
 viDeltaaxisH = deltaaxisH;
 float a = viX - (deltaaxisL + deltaaxisR);
 //Нужен ли выброс сектора
 int viMov = 1;
 if (viDx == 0 && viDy == 0)
 {
     viMov = 0;
 }
 //Запоминаем диаметр
 vfDiamX = a;
 vfDiamY = viY - 2 * viDeltaaxisH;
 //Запоминаем центр элипса
 vfXcirc = deltaaxisL + a / 2;
 vfYcirc = viY / 2;
 graph.SmoothingMode = SmoothingMode.AntiAlias;
 //Определяем сумму всех значений в массиве
 float fSum = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
 }
 float f = 0;
 float fBSum = 0;
 float fDeltaGrad = (fSum / (float)360);
 SolidBrush objBrush = new SolidBrush(Color.Aqua);
 Random rand = new Random(DateTime.Now.Millisecond);
 float[] frgZn = new float[viMaxRg];
 float[] frgSumGr = new float[viMaxRg];
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     frgZn[i] = float.Parse(s);
     if (i == 0) frgSumGr[i] = 0;
     else frgSumGr[i] = frgZn[i] + frgSumGr[i - 1];
 }
 for (int i = viMaxRg - 1; i >= 0; i--)
 {
  if (i != viMaxRg - 1 && fBSum < 90) break;
  //f в градусах  fBSum в градусах
  f = frgZn[i] / fDeltaGrad;
  //fBSum = frgSumGr[i] / fDeltaGrad;
  if (i == viMaxRg - 1)
  {
      fBSum = 360 - f;
  }
  else
  {
      fBSum -= f;
  }
  //Для цвета
  int j = i % br.Length;
  float k = f;
  if (f < 1) k = 1;
  //objBrush.Color = Color.FromArgb(rand.Next(255), rand.Next(255), rand.Next(255));
  if (i != 0)
  {
   if ((fBSum > 90 && fBSum < 180) || i == viMaxRg - 1)
   {
    for (int d = 0; d < viH; d++)
    {
       //Этап 1
       graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]/*objBrush.Color*/), 
             vfXcirc - a / 2, vfYcirc - vfDiamY / 2 + d,
               vfDiamX, vfDiamY, fBSum, k);
    }
   }
   objBrush.Color = color[j];
   //Этап 2
   graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
        vfDiamX, vfDiamY, fBSum, k);
  }
 }
 fBSum = 0;
 for (int i = viMov; i < viMaxRg; i++)
 {
  //f в градусах  fBSum в градусах
  f = frgZn[i] / fDeltaGrad;
  if (i == 1)
  {
      fBSum = frgZn[0] / fDeltaGrad;
  }
  //Для цвета
  int j = i % br.Length;
  float k = f;
  if (f < 1) k = 1;

  if (fBSum < 90)
  {
      for (int d = 0; d < viH; d++)
      {
          //Этап 3
          graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]), 
           vfXcirc - a / 2, vfYcirc - vfDiamY / 2 + d,
              vfDiamX, vfDiamY, fBSum, k);
      }
      objBrush.Color = color[j];
      //Этап 4
      graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
       vfDiamX, vfDiamY, fBSum, k);
  }
  else
  {
      break;
  }
  fBSum += f;
 }
 //Рисуем сдвинутым первый сектор 
 //Этап 5
 if (viMov == 1)
 {
  f = frgZn[0] / fDeltaGrad;
  fBSum = 0;
  float k1 = f;
  if (f < 1) k1 = 1;
  for (int d = 0; d < viH; d++)
  {
      graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[0]), 
       vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 + d - viDy,
          vfDiamX, vfDiamY, fBSum, k1);
  }
  objBrush.Color = color[0];
  graph.FillPie(objBrush, vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 - viDy,
  vfDiamX, vfDiamY, fBSum, k1);
 }
}
#endregion

Добавляем функции надписи и легенду и, в принципе, построение диаграммы
закончено. Единственное, что потребуется от нас при рисовании надписей на диаграмме — это немного
вспомнить начальную школу при расчете координат нанесения значений:

#region vDravTextCircle
public void vDravTextCircle1(bool vfGde)
{
  float fSum = 0;
  string s = string.Empty;
  for (int i = 0; i < viMaxRg; i++)
  {
      s = rgsValues[i, 0];
      fSum += float.Parse(s);
  }
  float f = 0;
  float fBSum = 0;
  float f1Radian = (float)Math.PI / 180;
  float fDeltaGrad = fSum / 360;
  for (int i = 0; i < viMaxRg; i++)
  {
   s = rgsValues[i, 0];
   f = float.Parse(s);
   //f в градусах
   f = f / fDeltaGrad;
   int j = i % br.Length;
   //Угол в радианах
   float fRad = (f + fBSum) * f1Radian;
   float fty = 0;
   float ftx = 0;
   float fSin = (float)Math.Sin((360 - (f / 2 + fBSum)) * f1Radian);
   float fCos = (float)Math.Cos((360 - (f / 2 + fBSum)) * f1Radian);
   float c = (float)Math.Sqrt((vfDiamX / 2 * vfDiamX / 2 * vfDiamY / 2 * vfDiamY / 2) /
       (vfDiamY / 2 * vfDiamY / 2 * fCos * fCos + vfDiamX / 2 * vfDiamX / 2 * fSin * fSin));
   c -= 3 * objFont.Size;
   if (c < 0) c = 0;
   ftx = c * fCos;
   fty = c * fSin;
   ftx = vfXcirc + ftx;
   fty = vfYcirc - fty;
   if (vfGde)
   {
    graph.DrawString(Convert.ToString(i + 1), objFont, objBrush, ftx, fty);
   }
   else
   {
    graph.DrawString(rgsValues[i, 0], objFont, objBrush, ftx, fty);
   }
   fBSum += f;
  }
}
#endregion

#region Текст легенды
public void vDravTextKeyCircle(bool vfGde)
{
 float fSum = 0;
 float f = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  fSum += float.Parse(s);
 }
 //Сдвиг от круговой диаграммы
 float vfSdvig = vfXcirc + vfDiamX / 2;
 vfSdvig += (viX - vfSdvig) / 5;
 //Высота места для легенды
 //На одну строку по высоте отводится - +1 на заголовок
 float vfHg = viY / (viMaxRg + 2);
 vSetFont("Arial", 12, true);
 if (viMaxRg > 100)
 {
  graph.DrawString("Легенда не может быть размещена", 
   objFont, Brushes.DarkBlue, vfSdvig + (viX - vfSdvig) / 10, objFont.Size);
 }
 else
 {
  //Шрифт в 2 раза меньше места на строку надписи
  if (viMaxRg > 15)
  {
      vSetFont("Arial", (vfHg / 2), true);
  }
  else
  {
   if (viMaxRg > 10)
   {
       vSetFont("Arial", (vfHg / 3), true);
   }
   else
   {
       vSetFont("Arial", (vfHg / 6), true);
   }
  }
  if (vfGde)
  {
      graph.DrawString("Пояснения к графику", 
       objFont, Brushes.DarkBlue, vfSdvig /*+ (viX - vfSdvig) / 10*/, objFont.Size);
  }
  else
  {
      graph.DrawString("Пояснения к графику", 
        objFont, objBrush, vfSdvig/* + (viX - vfSdvig) / 10*/, objFont.Size);
  }
  if (viMaxRg > 15)
  {
      vSetFont("Arial", (vfHg / 2) + 1, true);
  }
  else
  {
   if (viMaxRg > 10)
   {
       vSetFont("Arial", (vfHg / 4) + 1, true);
   }
   else
   {
       vSetFont("Arial", (vfHg / 7) + 1, true);
   }
  }
  for (int i = 0; i < rgsValues.Length / 2; i++)
  {
   Brush brTxt = null;
   int j = i % br.Length;
   if (vfGde) brTxt = br[j];
   else brTxt = objBrush;
   graph.DrawString(Convert.ToString(i + 1), objFont, brTxt, vfSdvig, vfHg * (i + 2));
   f = float.Parse(rgsValues[i, 0]);
   f = (f * 100) / fSum;
   graph.DrawString(rgsValues[i, 0], objFont, 
       brTxt, vfSdvig + 1 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(f.ToString("0.0") + "%", objFont, 
       brTxt, vfSdvig + 2 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(rgsValues[i, 1], objFont, 
       brTxt, vfSdvig + 3 * (viX - vfSdvig) / 5, vfHg * (i + 2));
  }
 }
}
#endregion


#region Смена шрифта по секторам
private void vSetFont(string name, float size, bool bold)
{
 if (objFont != null) objFont = null;
 if (bold)
 {
     objFont = new Font(name, size, FontStyle.Bold);
 }
 else
 {
     objFont = new Font(name, size);
 }
}
#endregion

Оформим вызовы функций:

private void button3_Click(object sender, EventArgs e)
{
 viNumButton = 3;
 vCreateCircleDiagramm();
}
private void vCreateCircleDiagramm()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холсты
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Передаем значения массива в класс
 clPaint.RgValue = rgsValues;
 //Рисуем график. Параметры: отступ осей x слева, x справа ,
 //y от краев холста, толщина диаграммы,вынос сектора           
 clPaint.vDravCircle3D(20, 250, 50, 20, 20, 40);
 //Круговые надписи true цифры 1-20, false - значения
 clPaint.vDravTextCircle1(true);
 //false - Разноцветные надписи в легенде true - Цветом шрифта
 clPaint.vDravTextKeyCircle(true);
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;
}

Отметим, что при задании толщины диаграммы, равной нулю, получим
обычную эллиптическую диаграмму, а при равенстве осей Х и Y — круговую.

Результат выполнения решения показан на Рис.12.6:

graph_12.gif

Рис.12.6. Круговая диаграмма

В заключении, еще раз повторим, что все параметры целесообразно иметь настраиваемыми,
что позволяет быстро подобрать приемлемый вид графического отображения для демонстрации.
Целесообразно также выполнить автономную настройку диаграмм по тестовым значениям (как это
сделано в программе LitFregMeter — см. Параграф 2.). Тогда мы сможем быстро подбирать параметры, например так:

//Смена фона диаграммы
private void ColorBackGround_Click(object sender, EventArgs e)
{
 if (colorDialog1.ShowDialog() == DialogResult.OK)
 {
  //Переменная objColorBackGroung задана глобально, сохраняется в реестре
  //при закрытии приложения и передается в класс при рисовании диаграммы
  //clPaint.vSetBackground(objColorBackGroung);
  objColorBackGroung = colorDialog1.Color;
  vGhangeDiagramm();
 }
}
//Перерисовка конкретной диаграммы при настройке
private void vGhangeDiagramm()
{
 switch (viNumButton)
 {
  case 1:
   vCreateLinGr();
  break;
  case 2:
   vCreateRectangleDiagramm();
  break;
  case 3:
   vCreateCircleDiagramm();
   break;
 }
}

В начало

Параграф 5. Базовый класс для рисования графиков

В заключении главы приводим полностью класс, созданный нами. Вы его можете
без труда не только использовать в своих приложениях, но и развивать дальше — включая
новые типы диаграмм.

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

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace graph1
{
 class PaintCl
 {
  //Основные объекты для рисования       
  private Bitmap bmp = null;
  private Graphics graph = null;
  private Font objFont = new Font("Arial", 8, FontStyle.Bold);
  private Brush objBrush = Brushes.Black;        
  private Pen objPenLine = new Pen(Color.Black, 1);

  //Размеры холста
  private int viX = 200;
  private int viY = 100;

  //Параметры для рисования круговой диаграммы
  private float vfDiamX = 100;
  private float vfDiamY = 100;
  private float vfXcirc = 100;
  private float vfYcirc = 100;

  //Массив предопределенных цветов для отображения легенды
  private Color[] color ={Color.LightGreen,Color.Chartreuse,Color.LimeGreen,
                          Color.Green,Color.DarkGreen,Color.DarkOliveGreen,
                          Color.LightPink,Color.LightSeaGreen,Color.LightCoral,
                          Color.DarkCyan,Color.Crimson,Color.CornflowerBlue,
                          Color.Chocolate,Color.CadetBlue,Color.BlueViolet,
                          Color.Maroon,Color.Blue,Color.Brown,Color.DarkBlue, 
                          Color.Red,Color.Coral,Color.DarkRed, Color.DarkMagenta, 
                          Color.DarkOrange,Color.DarkOrchid};

  //Массив предопределенных цветов для отображения круговой диаграммы и гистограммы
  private Brush[] br =   {Brushes.LightGreen,Brushes.Chartreuse,Brushes.LimeGreen,
                          Brushes.Green,Brushes.DarkGreen,Brushes.DarkOliveGreen,
                          Brushes.LightPink,Brushes.LightSeaGreen,Brushes.LightCoral,
                          Brushes.DarkCyan,Brushes.Crimson,Brushes.CornflowerBlue,
                          Brushes.Chocolate,Brushes.CadetBlue,Brushes.BlueViolet, 
                          Brushes.Maroon, Brushes.Blue,Brushes.Brown,Brushes.DarkBlue, 
                          Brushes.Red,Brushes.Coral,Brushes.DarkRed, 
                          Brushes.DarkMagenta, Brushes.DarkOrange,Brushes.DarkOrchid};

  //Массив значений для рисования графика
  private string[,] rgsValues = null;
  //Размер массива
  private int viMaxRg = 20;


  //Отступы от краев холста
  private int viDeltaaxisL = 50;
  private int viDeltaaxisR = 50;
  private int viDeltaaxisH = 20;


  #region Конструктор
  public PaintCl(int a, int b)
  {
   bmp = new Bitmap(a, b);
   graph = Graphics.FromImage(bmp);
   viX = a;
   viY = b;            
  }
  #endregion

  #region Установка цвета фона диаграммы
  public void vSetBackground(Color bcl)
  {
   graph.Clear(bcl);
  }
  #endregion

  #region Доступ к переменным класса
  public Bitmap Bmp
  {
   get { return bmp; }
  }
  public int MaxRg
  {
   set { viMaxRg = value; }
  }
  public string[,] RgValue
  {
   set { rgsValues = value; }
  }
  public Font font
  {
   set { objFont = value; }
  }
  public Brush  brush
  {
   set { objBrush  = value; }
  }
  #endregion

  #region Карандаш, шрифт, кисть
  //Цвет карандаша
  public void vSetPenColorLine(Color pcl)
  {
   if (objPenLine == null)
   {
    objPenLine = new Pen(Color.Black, 1);
   }
   objPenLine.Color = pcl;
  }
  //Установка толщина карандаша        
  public void vSetPenWidthLine(int penwidth)
  {
   if (objPenLine == null)
   {
    objPenLine = new Pen(Color.Black, 1);
   }
   objPenLine.Width = penwidth;
  }
  #endregion

  #region Рисование Осей
  //Отступ от левого края - deltaaxis, отступ от правого края - deltaaxisR,
  //отступ от нижнего и верхнего края - deltaaxisH, цвет оси - colorpenaxis,
  //толщина пера для оси - widthpen, наличие стрелок на оси - fArrow(true)
  public void vDravAxis(int deltaaxisL, int deltaaxisR, int deltaaxisH, 
                        Color colorpenaxis,int widthpen, bool fArrow)
  {
   //Запоминаем отступы                        
   viDeltaaxisL = deltaaxisL;
   viDeltaaxisR = deltaaxisR;
   viDeltaaxisH = deltaaxisH;
   //Запоминаем цвет осей и толщину
   vSetPenColorLine(colorpenaxis);
   if (widthpen > 0) vSetPenWidthLine(widthpen);
   //Точка начала рисования по х и y           
   int x = deltaaxisL;
   int y = viY - deltaaxisH;
   int x1 = viX - deltaaxisR;
   int y1 = deltaaxisH;
   int d = 0;
   if (fArrow) d = widthpen * 10;
   //Оси на d пикселей длинней для стрелок
   graph.DrawLine(objPenLine, x, y, x1 + d, y);
   graph.DrawLine(objPenLine, x, y, x, y1 - d);
   if (fArrow)
   {
    int a = 10 * (int)objPenLine.Width;
    int b = 2 * (int)objPenLine.Width;
    int x2 = x1 - a;
    int y2 = y + b;
    //Стрелки
    graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
    y2 = y - b;
    graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
    x2 = x - b;
    y2 = y1 + a;
    graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
    x2 = x + b;
    graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
   }   
  }
  #endregion

  #region Рисование сетки
  public void vDravGrid()
  {
   float x = viDeltaaxisL;
   float y = viY - viDeltaaxisH;
   float x1 = viX - viDeltaaxisR;
   float y1 = viDeltaaxisH;
   float f = (y - y1) / (float)viMaxRg;
   for (int i = 1; i < viMaxRg + 1; i++)
   {
    graph.DrawLine(objPenLine, x, y - f * i, x1, y - f * i);
   }
   f = (x - x1) / (float)(viMaxRg - 1);
   for (int i = 1; i < viMaxRg; i++)
   {
    graph.DrawLine(objPenLine, x - f * i, y, x - f * i, y1);
   }
  }
  #endregion

  #region Рисование линий графика для линейного графика
  public void vDrawGraphLines()
  {
   string s = string.Empty;
   string s1 = string.Empty;
   string s2 = string.Empty;
   float f = 0;
   float f1 = 0;
   float x1 = 0;
   float x = viDeltaaxisL;//-objPenLine.Width;
   float y = viY - viDeltaaxisH;// +objPenLine.Width;
   float x2 = 0;
   float fMax = float.MinValue;
   //float fMin = int.MaxValue;
   for (int i = 0; i < viMaxRg; i++)
   {
    s = rgsValues[i, 0];
    if (fMax < float.Parse(s)) fMax = float.Parse(s);
   }
   //Пикселей для рисования по оси х
   float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
   //Пикселей на один отсчет
   fdeltax = fdeltax / (float)(viMaxRg - 1);
   //Пикселей для рисования по оси y
   float fdeltay = viY - 2 * viDeltaaxisH;
   //Пикселей на одну единицу массива значений
   fdeltay = fdeltay / fMax;
   for (int i = 0; i < viMaxRg; i++)
   {
    //Первый раз запоминаем точку старта
    if (i == 0)
    {
     s = rgsValues[i, 0];
     s2 = rgsValues[i, 1];
     f = y - (float.Parse(s) * fdeltay);
     x1 = x;
    }
    else
    {
     s1 = rgsValues[i, 0];
     f1 = y - (float.Parse(s1) * fdeltay);
     x2 = x + (int)(fdeltax * i);
     graph.DrawLine(objPenLine, x1, f, x2, f1);
     s = rgsValues[i, 0];
     s2 = rgsValues[i, 1];
     f = f1;
     x1 = x + (int)(i * fdeltax);
    }
   }
  }
  #endregion

  #region Текст по оси X - цифры 
  //Параметр = false - без сдвига по оси Y, true - со здвигом по оси Y
  public void vDrawTextAxXNumber(bool f)
  {
   //Пикселей для надписей по оси х
   float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
   //Пикселей на один отсчет
   fdeltax = fdeltax / (float)(viMaxRg - 1);
   float x = viDeltaaxisL;
   float y = viY - viDeltaaxisH + objPenLine.Width;
   for (int i = 1; i < viMaxRg + 1; i++)
   {
    if (!f || i % 2 == 0)
    {
     graph.DrawString(Convert.ToString(i), objFont, 
                                         objBrush, x + (i - 1) * fdeltax, y);
    }
    else
    {
     graph.DrawString(Convert.ToString(i), objFont, 
                                        objBrush, x + (i - 1) * fdeltax, y + objFont.Size);
    }
   }
  }
  #endregion

  #region Текст по оси X 
  //Параметр = false - без сдвига по оси Y, true - со здвигом по оси Y
  public void vDrawTextAxXValues(bool f)
  {
   string s = string.Empty;
   //Пикселей для надписей по оси х
   float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
   //Пикселей на один отсчет
   fdeltax = fdeltax / (float)(viMaxRg - 1);
   float x = viDeltaaxisL;
   float y = viY - viDeltaaxisH;// +objPenLine.Width;
   for (int i = 0; i < viMaxRg; i++)
   {
    if (!f || i % 2 == 0)
    {
     graph.DrawString(rgsValues[i, 1], objFont, objBrush, x + i * fdeltax, y);
    }
    else
    {
     graph.DrawString(rgsValues[i, 1], objFont, 
                 objBrush, x + i * fdeltax, y + objFont.Size);
    }
   }
  }
  #endregion

  #region Текст по оси Y - Значения по сетке
  public void vDrawTextAxYValues()
  {
   string s = string.Empty;
   float f = 0;
   float fMax = float.MinValue;
   for (int i = 0; i < viMaxRg; i++)
   {
    s = rgsValues[i, 0];
    if (fMax < float.Parse(s)) fMax = float.Parse(s);
   }
   f = fMax / (float)(viMaxRg - 1);
   //Пикселей для надписей по оси х
   float fdeltay = viY - 2 * viDeltaaxisH;
   //Пикселей на один отсчет
   fdeltay = fdeltay / (float)(viMaxRg - 1);
   float y = viY - viDeltaaxisH - objFont.Size;
   for (int i = 0; i < viMaxRg; i++)
   {
    graph.DrawString(((float)(i * f)).ToString("0.00"), objFont, 
       objBrush, viDeltaaxisL - (objFont.Size) * 5 - 5, y - i * fdeltay);
   }
  }
  #endregion
  //*****************
#region Надписи по оси Y - Значения  над точкой
public void vDrawTextAxYValuesPoint(bool a, bool b)
{
 string s = string.Empty;
 float fMax = float.MinValue;
 float fSum = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет по х
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float fdeltay = viY - 2 * viDeltaaxisH;
 float y = viY - viDeltaaxisH - objFont.Size;
 //Пикселей на одну единицу
 fdeltay = fdeltay / fMax;
 float fdelta = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
  if (a)
  {
      if (i % 2 == 0) fdelta = objFont.Size;
      else fdelta = 2 * objFont.Size;
  }
  else
  {
      fdelta = objFont.Size;
  }
  if (b)
  {
      graph.DrawString(rgsValues[i, 0], objFont, objBrush, x + i * fdeltax,
          y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
  }
  else
  {
   float fp = float.Parse(rgsValues[i, 0]);
   fp = (fp * 100) / fSum;
   graph.DrawString(rgsValues[i, 0] + "-" + fp.ToString("0.0") + "%", 
                            objFont, objBrush, x + i * fdeltax,
                             y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
  }
 } 
}
#endregion


#region Рисование графика для графика прямоугольниками 
public void vDrawGraphRectangular(int a, int c)
{
 string s = string.Empty;
 string s1 = string.Empty;
 string s2 = string.Empty;
 float f = 0;
 float x1 = 0;
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float fMax = float.MinValue;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для рисования по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 //Пикселей для рисования по оси y
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на одну единицу массива значений
 fdeltay = fdeltay / fMax;
 float fdx = 0;
 if (c != 0) fdx = (fdeltax * c / 100) / 2;
 Random rand = new Random(DateTime.Now.Millisecond);
 int arn = rand.Next((int)br.Length);
 objBrush = br[arn];
 for (int i = 0; i < viMaxRg - 1; i++)
 {
  s = rgsValues[i, 0];
  f = float.Parse(s);
  x1 = x + ((float)i * fdeltax);
  if (a == 0)
  {
      graph.FillRectangle(objBrush, x1 + fdx, y - fdeltay * f, fdeltax - 2 * fdx, fdeltay * f);
  }
  else
  {
   int b = i % br.Length;
   graph.FillRectangle(br[b], x1 + fdx, y - fdeltay * f, fdeltax - 2 * fdx, fdeltay * f);
  }
  if (i == viMaxRg - 2)
  {
   int b = (i + 1) % br.Length;
   s = rgsValues[i + 1, 0];
   f = float.Parse(s);
   x1 = x + ((float)(i + 1) * fdeltax);
   if (a == 0)
   {
    graph.FillRectangle(objBrush, x1 - 2, y - fdeltay * f, 4/*fdeltax*/, fdeltay * f);
   }
   else
   {
    graph.FillRectangle(br[b], x1 - 2, y - fdeltay * f, 4/*fdeltax*/, fdeltay * f);
   }
  }
 }
}
#endregion 

#region  vDravCircle3D
//Параметры - Отступ от краев по X слева deltaaxisL, от краев по Y справа,
//deltaaxisH - отступа сверху и снизу, толщина диаграммы viH, сдвиг сектора viDx, viDy
public void vDravCircle3D(int deltaaxisL, int deltaaxisR, int deltaaxisH, int viH, int viDx, int viDy)
{
 //Запоминаем отступы            
 viDeltaaxisL = deltaaxisL;
 viDeltaaxisR = deltaaxisR;
 viDeltaaxisH = deltaaxisH;
 float a = viX - (deltaaxisL + deltaaxisR);
 //Нужен ли выброс сектора
 int viMov = 1;
 if (viDx == 0 && viDy == 0)
 {
     viMov = 0;
 }
 //Запоминаем диаметр
 vfDiamX = a;
 vfDiamY = viY - 2 * viDeltaaxisH;
 //Запоминаем центр элипса
 vfXcirc = deltaaxisL + a / 2;
 vfYcirc = viY / 2;
 graph.SmoothingMode = SmoothingMode.AntiAlias;
 float fSum = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
 }
 float f = 0;
 float fBSum = 0;
 float fDeltaGrad = (fSum / (float)360);
 SolidBrush objBrush = new SolidBrush(Color.Aqua);
 Random rand = new Random(DateTime.Now.Millisecond);
 float[] frgZn = new float[viMaxRg];
 float[] frgSumGr = new float[viMaxRg];
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     frgZn[i] = float.Parse(s);
     if (i == 0) frgSumGr[i] = 0;
     else frgSumGr[i] = frgZn[i] + frgSumGr[i - 1];
 }
 //Рисуем диаграмму против часовой стрелки со штриховкой до 90градусав по часовой
 //Штриховка убирается каждым следующим сектором от сектора предыдущего
 //В первом нарисованном секторе она сохраняетсяна случай сдвига первого по часовой стрелке
 for (int i = viMaxRg - 1; i >= 0; i--)
 {
 if (i != viMaxRg - 1 && fBSum < 90) break;
 //f в градусах  fBSum в градусах
 f = frgZn[i] / fDeltaGrad;
 //fBSum = frgSumGr[i] / fDeltaGrad;
 if (i == viMaxRg - 1)
 {
     fBSum = 360 - f;
 }
 else
 {
     fBSum -= f;
 }
 //Для цвета
 int j = i % br.Length;
 float k = f;
 if (f < 1) k = 1;
 //objBrush.Color = Color.FromArgb(rand.Next(255), rand.Next(255), rand.Next(255));
 if (i != 0)
 {
  if ((fBSum > 90 && fBSum < 180) || i == viMaxRg - 1)
  {
   for (int d = 0; d < viH; d++)
   {
    graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]/*objBrush.Color*/), 
          vfXcirc - a / 2, vfYcirc - vfDiamY / 2 + d,
                 vfDiamX, vfDiamY, fBSum, k);
   }
  }
    objBrush.Color = color[j];
   graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
          vfDiamX, vfDiamY, fBSum, k);
  }
 }
 //Рисуем до 90градусов без первого сегмента в случае необходимости 
 //сдвига первого по часовой стрелке
 fBSum = 0;
 for (int i = viMov; i < viMaxRg; i++)
 {
  //f в градусах  fBSum в градусах
  f = frgZn[i] / fDeltaGrad;
  if (i == 1)
  {
      fBSum = frgZn[0] / fDeltaGrad;
  }
  //Для цвета
  int j = i % br.Length;
  float k = f;
  if (f < 1) k = 1;

  if (fBSum < 90)
  {
   for (int d = 0; d < viH; d++)
   {
    graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]), vfXcirc - a / 2, 
        vfYcirc - vfDiamY / 2 + d,
          vfDiamX, vfDiamY, fBSum, k);
   }
   objBrush.Color = color[j];
   graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
   vfDiamX, vfDiamY, fBSum, k);
  }
  else
  {
      break;
  }
  fBSum += f;
 }
 //Рисуем сдвинутый сектор при необходимости
 if (viMov == 1)
 {
  f = frgZn[0] / fDeltaGrad;
  fBSum = 0;
  float k1 = f;
  if (f < 1) k1 = 1;
  for (int d = 0; d < viH; d++)
  {
      graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[0]), 
       vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 + d - viDy,
          vfDiamX, vfDiamY, fBSum, k1);
  }
  objBrush.Color = color[0];
  graph.FillPie(objBrush, vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 - viDy,
  vfDiamX, vfDiamY, fBSum, k1);
 }
}
#endregion

#region vDravTextCircle
public void vDravTextCircle1(bool vfGde)
{
 float fSum = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  fSum += float.Parse(s);
 }
 float f = 0;
 float fBSum = 0;
 float f1Radian = (float)Math.PI / 180;
 float fDeltaGrad = fSum / 360;
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  f = float.Parse(s);
  //f в градусах
  f = f / fDeltaGrad;
  int j = i % br.Length;
  //Угол в радианах
  float fRad = (f + fBSum) * f1Radian;
  float fty = 0;
  float ftx = 0;
  float fSin = (float)Math.Sin((360 - (f / 2 + fBSum)) * f1Radian);
  float fCos = (float)Math.Cos((360 - (f / 2 + fBSum)) * f1Radian);
  float c = (float)Math.Sqrt((vfDiamX / 2 * vfDiamX / 2 * vfDiamY / 2 * vfDiamY / 2) /
      (vfDiamY / 2 * vfDiamY / 2 * fCos * fCos + vfDiamX 
            / 2 * vfDiamX / 2 * fSin * fSin));
  c -= 3 * objFont.Size;
  if (c < 0) c = 0;
  ftx = c * fCos;
  fty = c * fSin;
  ftx = vfXcirc + ftx;
  fty = vfYcirc - fty;
  if (vfGde)
  {
   graph.DrawString(Convert.ToString(i + 1), objFont, objBrush, ftx, fty);
  }
  else
  {
   graph.DrawString(rgsValues[i, 0], objFont, objBrush, ftx, fty);
  }
  fBSum += f;
 }
}
#endregion

#region Смена шрифта
private void vSetFont(string name, float size, bool bold)
{
 if (objFont != null) objFont = null;
 if (bold)
 {
  objFont = new Font(name, size, FontStyle.Bold);
 }
 else
 {
  objFont = new Font(name, size);
 }
}
#endregion

#region Текст легенды
public void vDravTextKeyCircle(bool vfGde)
{
 float fSum = 0;
 float f = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
 }
 //Сдвиг от круговой диаграммы
 float vfSdvig = vfXcirc + vfDiamX / 2;
 vfSdvig += (viX - vfSdvig) / 5;
 //Высота места для легенды
 //На одну строку по высоте отводится - +1 на заголовок
 float vfHg = viY / (viMaxRg + 2);
 vSetFont("Arial", 12, true);
 if (viMaxRg > 100)
 {
  graph.DrawString("Легенда не может быть размещена", 
   objFont, Brushes.DarkBlue, vfSdvig + (viX - vfSdvig) / 10, objFont.Size);
 }
 else
 {
  //Шрифт в 2 раза меньше места на строку надписи
  if (viMaxRg > 15)
  {
      vSetFont("Arial", (vfHg / 2), true);
  }
  else
  {
   if (viMaxRg > 10)
   {
    vSetFont("Arial", (vfHg / 3), true);
   }
   else
   {
    vSetFont("Arial", (vfHg / 6), true);
   }
  }
  if (vfGde)
  {
   graph.DrawString("Пояснения к графику", objFont, 
       Brushes.DarkBlue, vfSdvig /*+ (viX - vfSdvig) / 10*/, objFont.Size);
  }
  else
  {
   graph.DrawString("Пояснения к графику", objFont, 
       objBrush, vfSdvig/* + (viX - vfSdvig) / 10*/, objFont.Size);
  }
  if (viMaxRg > 15)
  {
   vSetFont("Arial", (vfHg / 2) + 1, true);
  }
  else
  {
   if (viMaxRg > 10)
   {
       vSetFont("Arial", (vfHg / 4) + 1, true);
   }
   else
   {
       vSetFont("Arial", (vfHg / 7) + 1, true);
   }
  }
  for (int i = 0; i < rgsValues.Length / 2; i++)
  {
   Brush brTxt = null;
   int j = i % br.Length;
   if (vfGde) brTxt = br[j];
   else brTxt = objBrush;
   graph.DrawString(Convert.ToString(i + 1), objFont, brTxt, vfSdvig, vfHg * (i + 2));
   f = float.Parse(rgsValues[i, 0]);
   f = (f * 100) / fSum;
   graph.DrawString(rgsValues[i, 0], objFont, 
        brTxt, vfSdvig + 1 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(f.ToString("0.0") + "%", objFont, 
        brTxt, vfSdvig + 2 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(rgsValues[i, 1], objFont, 
       brTxt, vfSdvig + 3 * (viX - vfSdvig) / 5, vfHg * (i + 2));
  }
 }
}
#endregion
 }
}

Молчанов Владислав 21.09.2005г. Материал переработан 14.10.2008г.

В начало

В начало книги

На главную страницу

C#: работаем с графикой и графическими компонентами в приложениях Windows Forms

Для работы с изображениями в библиотеке .NET определён базовый класс System.Drawing.Image, который предоставляет функциональные возможности для производных классов System.Drawing.Bitmap (растровая графика) и System.Drawing.MetaFile (векторная графика). Этот класс содержит методы для создания (и сохранения) объектов типа Image из указанного файла, потока данных и т.д.

Когда требуется перерисовка элемента управления, происходит событие Paint, которое, в зависимости от задачи, можно как программировать явно, так и полагаться на его автоматический вызов, происходящий, когда изменилась графическая канва объекта.
Для отрисовки готового файла с изображением, имя которого задано или получено из диалога открытия файла OpenFileDialog, мы должны создать или получить из аргумента PaintEventArgs метода Paint графическую канву типа System.Drawing.Graphics а затем уже вывести на неё изображение.

Для использования готовых методов обработки изображений (поворот, масштабирование, изменение цвета и т.п.) мы программно создаём объект типа System.Drawing.Bitmap, копирующий имеющееся изображение, выполняем его обработку, а затем выводим изменённый объект в компоненту, предназначенную для отображения, такую как PictureBox.

Проект Lab5_1. Выведем выбранный в стандартном диалоге открытия файла рисунок на форму (пункт меню Файл — Открыть) и принудительно перерисуем по пункту меню Правка — Перерисовать. В свойствах диалога открытия файла openFileDialog1 указано Filter = Все файлы|*.*|Рисунки BMP|*.bmp|Рисунки JPEG|*.jpg|Рисунки PNG|*.png а свойство FileName равно пустой строке.
В классе формы пропишем объекты «Изображение» и «Имя файла»:

private Image Img;
private String Name;

Напишем обработчик открытия файла:

   //Обработка меню Файл - Открыть
   openFileDialog1.ShowDialog();
   Name = openFileDialog1.FileName.Trim();
   if (String.IsNullOrEmpty(Name)) return;
   try {
    Img = new Bitmap(Name);
    //или так: Img = Image.FromFile(Name);
   }
   catch (Exception ex) {
    MessageBox.Show(ex.Message + Environment.NewLine + "(не могу открыть файл)",
     "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    toolStripLabel1.Text = "Файл не выбран";
    Img = null;
    return;
   }
   this.ClientSize = new System.Drawing.Size(Img.Width, Img.Height);
    //Размер формы подгогнали под размер картинки
   toolStripLabel1.Text = Name;
    //Имя файла вывели в панель инструментов
   this.Refresh(); //Потребовать перерисовки!

По событию Paint формы будет выполняться отрисовка объекта Img на текущей канве, Y-координата для вставки рисунка учитывает пространство, занимаемое по вертикали компонентами menuStrip1 и toolStripLabel1:

  private void Form1_Paint(object sender, PaintEventArgs e) {
   if (Img != null) {
    Point p = new Point(0, menuStrip1.Size.Height+ toolStripLabel1.Size.Height);
    e.Graphics.DrawImage(Img, p);
   }
  }

Объект «Графика», представляющий собой поверхность для рисования, также может быть получен для канвы формы (вот обработчик пункта меню Правка — Перерисовать):

   if (Img != null) {
    Graphics g = this.CreateGraphics();
    Point p = new Point(0, menuStrip1.Size.Height + toolStripLabel1.Size.Height);
    g.DrawImage(Img, p);
   }

или из загруженного (сгенерированного) изображения, например, см. код для пункта меню Файл — Создать:

   Img = new Bitmap(200, 200,
    System.Drawing.Imaging.PixelFormat.Format24bppRgb);
   Graphics Gr = Graphics.FromImage(Img);
   // Теперь становятся доступными методы класса Graphics!
   Pen pen = new Pen(Color.ForestGreen, 4.0F);
   Gr.DrawLine(pen, 0, 0, 199, 199);
   Gr.RotateTransform(180.0F); //поворот на 180 градусов
   Img.Save("example.jpg", System.Drawing.Imaging.ImageFormat.Jpeg); //сохранение
   this.Refresh();

Обычно рисунки не отображают на канве формы, а работают с компонентом PictureBox (вкладка Стандартные), представляющим собой контейнер для размещения рисунка, вот его основные свойства:

  • Image — рисунок. Само изображение можно загрузить программно через свойство ImageLocation, например, в методе Load формы:
     openFileDialog1.ShowDialog();
     if (openFileDialog1.FileName != null) 
      this.pictureBox1.ImageLocation = this.openFileDialog1.FileName;

    или же присвоить этому свойству объект Image, как мы делали выше.

  • SizeMode — режим вывода: Normal — по левому верхнему углу контейнера с обрезанием, StretchImage — вписать в компонент, AutoSize — компонент примет размеры изображения, CenterImage — центрировать, не меняя размеры (может обрезать рисунок), Zoom — пропорционально масштабировать по размерам компонента (пространство имен PictureBoxSizeMode).

Для возможности прокрутки загруженного изображения достаточно разместить PictureBox на элементе Panel с установленным свойством AutoScroll = trueDock = Fill, если панель должна занимать всю клиентскую часть окна) и при этом для PictureBox указать SizeMode = AutoSize. После этого можно загрузить рисунок кодом вида

Image Img = new Bitmap(openFileDialog1.FileName);
pictureBox1.Image = Img;
  • ErrorImage — позволяет задать изображение, выводимое при ошибке загрузки;
  • InitialImage — позволяет задать изображение, выводимое в процессе загрузки.

К другим полезным компонентам можно отнести:

  • ImageList (вкладка Компоненты) — список изображений, который можно использовать для «прокрутки» картинок или как список иконок для меню, вкладок и т.п.
  • Timer (вкладка Компоненты), позволяет обрабатывать периодическое событие Tick и организовывать смену картинок в реальном времени, частота повторения события в миллисекундах задаётся свойством Interval.

Мы используем их в следующей теме.

 Скачать пример Lab5_1 в архиве .zip с проектом C# Visual Studio 2019 (12 Кб)

Проект Lab5_2. Основные операции над изображениями. Кроме pictureBox1, размещённого на Panel как описано выше, форма включает в себя стандартный диалог открытия файла openFileDialog1, меню Файл — Открыть (обработчик аналогичен предыдущему примеру) и меню «Правка», откуда мы будем вызывать обработчики загруженного изображения.

5.2.1. Поворот и отражение изображений. В этом примере выведенный в PictureBox рисунок поворачивается на 180 градусов и выводится обратно в PictureBox:

   if (pictureBox1.Image != null) {
    Bitmap bitmap1 = new Bitmap(pictureBox1.Image);
    if (bitmap1 != null) {
     bitmap1.RotateFlip(RotateFlipType.Rotate180FlipY);
     pictureBox1.Image = bitmap1;
    }
   }

Остальные повороты (отражения) – другие значения параметра RotateFlipType.

5.2.2. Масштабирование изображения или его части. Код ниже показывает, как можно программно уменьшить загруженное в компоненту PictureBox изображение в 2 раза:

   if (pictureBox1.Image == null) return;
   Bitmap  bitmap1 = new Bitmap (pictureBox1.Image); //взяли рисунок из компоненты
   Graphics  Gr1 = Graphics.FromImage (bitmap1); 
    //получили поверхность рисования из исходного рисунка
   Bitmap  bitmap2 = new Bitmap (bitmap1.Width / 2, bitmap1.Height / 2, Gr1);
    //сделали вдвое меньший рисунок с тем же разрешением
   Graphics  Gr2 = Graphics.FromImage (bitmap2); 
    //получили поверхность рисования из меньшего рисунка
   Rectangle compressionRectangle = new Rectangle 
     (0, 0, bitmap1.Width / 2, bitmap1.Height / 2); //определили масштабирующий прямоугольник
   Gr2.DrawImage (bitmap1, compressionRectangle);
    //отрисовали на поверхности второго рисунка первый со сжатием
   Pen  MyPen = new Pen (Color.Red); //на измененном рисунке можно что-то подрисовать
   Gr2.DrawRectangle (MyPen, 0, 0, bitmap2.Width - 1, bitmap2.Height - 1);
    //например, сделать красную рамку
   pictureBox1.Image = bitmap2; //назначили второй рисунок компоненте
   pictureBox1.Size = bitmap2.Size; //поставили размер компоненты по размерам нового рисунка
   this.ClientSize = pictureBox1.Size; //...и такой же размер клиентской формы

Добавим пункт меню Файл — Сохранить и сохраним изображение:

   if (pictureBox1.Image == null) return;
   Bitmap bitmap1 = new Bitmap (pictureBox1.Image);
   try {
    bitmap1.Save (openFileDialog1.FileName); 
     //Сохраняем под именем из диалога открытия файла
     //Может вызвать исключение, если исходный файл ещё открыт
   }
   catch (Exception ex) {
    MessageBox.Show (ex.Message + "\nНе удалось сохранить файл", 
      "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    return;
   }

Как вариант:

   pictureBox1.Image.Save (openFileDialog1.FileName);

К сожалению, этот код может вызвать исключение, если исходный файл ещё открыт. Для «надёжных» операций с файлами при открытии изображений используйте FileStream, чтобы контролировать весь процесс (перепишем обработчик пункта меню «Открыть»):

   openFileDialog1.ShowDialog ();
   if (openFileDialog1.FileName.Trim () != "" && openFileDialog1.FileName != null) {
    System.IO.FileStream file;
    try {
     file = new System.IO.FileStream (openFileDialog1.FileName, System.IO.FileMode.Open,
       System.IO.FileAccess.Read, System.IO.FileShare.Inheritable);
    }
    catch (Exception ex) {
     MessageBox.Show (ex.Message + "\nНе удалось открыть файл", "Ошибка",
      MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
     return;
    }
    pictureBox1.Image = Image.FromStream (file);
    file.Close ();
   }

5.2.3. Изменение цвета на изображении. На форму добавлен стандартный СolorDialog и выбранный в нём цвет ставится прозрачным.

   if (pictureBox1.Image == null) return;   
   Bitmap bitmap1 = new Bitmap (pictureBox1.Image);
   if (colorDialog1.ShowDialog () == DialogResult.OK) {
    bitmap1.MakeTransparent (colorDialog1.Color);
    pictureBox1.Image = bitmap1;
   }

5.2.4. Фильтрация всего изображения или его части (по пикселям). В качестве примера уменьшим вдвое интенсивность синего цвета на картинке, избегая пикселей, цвет которых близок к белому (интенсивности красной, зелёной и синей компонент больше значения 250):

   if (pictureBox1.Image == null) return;
   Bitmap bitmap1 = new Bitmap (pictureBox1.Image);
   for (int x = 0; x < bitmap1.Width; x++)
    for (int y = 0; y < bitmap1.Height; y++) {
     Color pixelColor = bitmap1.GetPixel (x, y);
     if (pixelColor.R > 250 && pixelColor.G > 250 && pixelColor.B > 250)
      continue; //не фильтруем пиксели, чей цвет близок к белому 
     Color newColor = Color.FromArgb (pixelColor.R, pixelColor.G, pixelColor.B / 2);
     bitmap1.SetPixel (x, y, newColor);
    }
   pictureBox1.Image = bitmap1;

Аналогично можно реализовать любую другую фильтрацию цветов, но из-за «ручного» прохода по пикселям скорость выполнения процесса может быть заметно ниже, чем для пп. 5.2.1-5.2.3. Более быстрый способ преобразования всех цветов рисунка даёт применение фильтрации на основе класса ColorMatrix. В качестве примера приведём код, преобразующий цветное изображение к оттенкам серого:

   if (pictureBox1.Image == null) return;
   Bitmap bitmap1 = new Bitmap (pictureBox1.Image);
   Bitmap bitmap2 = new Bitmap (bitmap1.Width, bitmap1.Height);
   Graphics g = Graphics.FromImage (bitmap2);
   float [] [] Map = {
    new float[] {0.30f, 0.30f, 0.30f, 0.00f, 0.00f },
    new float[] {0.59f, 0.59f, 0.59f, 0.00f, 0.00f },
    new float[] {0.11f, 0.11f, 0.11f, 0.00f, 0.00f },
    new float[] {0.00f, 0.00f, 0.00f, 1.00f, 0.00f },
    new float[] {0.00f, 0.00f, 0.00f, 0.00f, 1.00f }
   };
   System.Drawing.Imaging.ColorMatrix GrayscaleMatrix = 
    new System.Drawing.Imaging.ColorMatrix (Map);
   System.Drawing.Imaging.ImageAttributes attributes = 
    new System.Drawing.Imaging.ImageAttributes ();
   attributes.SetColorMatrix (GrayscaleMatrix);
   Rectangle rect = new Rectangle (0, 0, bitmap1.Width, bitmap1.Height);
   g.DrawImage (bitmap1, rect, 0, 0, bitmap1.Width, bitmap1.Height, 
    GraphicsUnit.Pixel, attributes);
   pictureBox1.Image = bitmap2;

О классе ColorMatrix можно почитать, например, по ссылке. В нашем фильтре соотношение «весов» красной, зелёной и синей цветовых компонент 0.3 — 0.59 — 0.11 отражает чувствительность человеческого глаза к оттенкам красного, зелёного и синего.

В некоторых случаях фильтровать изображения можно и сменой свойства Image.PixelFormat, но вариант Format16bppGrayScale в GDI+ не сработал.

5.2.5. Сохранить рисунок так, как он выглядит на компоненте. Следует понимать, что свойство SizeMode управляет отображением рисунка в компоненте, при сохранении пропорции рисунка не изменятся от того, что он был выведен, например, при SizeMode=StretchImage (принудительно растянут по размерам компоненты, возможно, с нарушением пропорций). Тем не менее — а можно ли сохранить рисунок так, как он был выведен в компоненту? Да, можно, например, так:

   if (pictureBox1.Image == null) return;
   pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
   pictureBox1.Dock = DockStyle.Fill; //установили растягивание
   Bitmap bitmap1 = new Bitmap (pictureBox1.Image);
   Bitmap bitmap2 = new Bitmap (pictureBox1.Width, pictureBox1.Height);
    //у 2-го рисунка - размер компоненты
   Graphics g = Graphics.FromImage (bitmap2);
    //получили графический контекст из 2-го рисунка
   g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bicubic;
    //настроили режим интерполяции
   g.DrawImage (bitmap1, new Rectangle(0,0, bitmap2.Width, bitmap2.Height));
    //отрисовали в контекст 2-го рисунка исходный, неискажённый рисунок
   pictureBox1.Image = bitmap2; //назначили искажённый рисунок компоненте
   сохранитьToolStripMenuItem_Click (this, e); //вызвали метод сохранения
   pictureBox1.SizeMode = PictureBoxSizeMode.AutoSize;
   pictureBox1.Dock = DockStyle.None; //восстановили свойства
   pictureBox1.Image = bitmap1; //вернули старый рисунок

Убедиться в том, что рисунок был пересохранён в фоновом режиме с размерами, соответствующими клиентской части формы можно, заново открыв его с диска.

5.2.6. Выделить часть рисунка и реализовать обрезку по выделенной области. В класс формы добавим следующие глобальные данные:

  Rectangle selRect; //выделенный прямоугольник
  Point orig; //точка для привязки прямоугольника
  Pen pen; //перо для отрисовки
  bool flag; //флажок показывает, находимся ли в режиме выделения части рисунка

Инициализируем их, например, в имеющемся конструкторе формы:

  public Form1() {
   InitializeComponent();
   pen = new Pen (Brushes.Blue, 0.8f); //цвет и толщина линии выделения
   pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; //штрихи
   selRect = new Rectangle (0, 0, 0, 0);
   flag = false;
  }

Реализация динамического выделения мышью потребует взаимодействия нескольких событий (нажатие кнопки мыши, отпускание кнопки мыши и перемещение мыши). Для удобства используем также динамическое переключение обработчика события Paint (чтобы рисовать рамку выделения только тогда, когда она нужна – происходит перемещение курсора мыши на pictureBox при зажатой левой кнопке).

  private void pictureBox1_Paint (object sender, PaintEventArgs e) {
   //Этот обработчик мы создали в конструкторе
   //Для ситуации, когда выделяем рамку
   e.Graphics.DrawRectangle (Pens.Black, selRect);
  }
  private void Selection_Paint (object sender, PaintEventArgs  e) {
   //Добавили свой обработчик Paint для остальных ситуаций
   e.Graphics.DrawRectangle (pen, selRect);
  }

  private void pictureBox1_MouseDown (object sender, MouseEventArgs e) {
   //Этот обработчик мы создали в конструкторе
   //Нажали мышку - включаем наш обработчик и выключаем стандартный
   pictureBox1.Paint -= new PaintEventHandler (pictureBox1_Paint);
   pictureBox1.Paint += new PaintEventHandler (Selection_Paint);
   orig = e.Location; //запомнили, где начало выделения
   flag = true;
  }

  private void pictureBox1_MouseUp (object sender, MouseEventArgs e) {
   //Этот обработчик мы создали в конструкторе
   //отжали мышку - всё наоборот
   pictureBox1.Paint -= new PaintEventHandler (Selection_Paint);
   pictureBox1.Paint += new PaintEventHandler (pictureBox1_Paint);
   pictureBox1.Invalidate (); //принудительно перерисовать
   flag = false; //выйти из режима выделения
  }

  private void pictureBox1_MouseMove (object sender, MouseEventArgs e) {
   //Этот обработчик мы создали в конструкторе
   if (flag) { //если в режиме выделения
    selRect = GetSelectionRectangle (orig, e.Location); //запоминаем выделенное
    if (e.Button == MouseButtons.Left) {
     pictureBox1.Refresh (); //рефрешим картинку по нажатию левой кнопки
    }
   }
  }

  private Rectangle GetSelectionRectangle (Point orig, Point location) {
   //Этот метод пришлось написать, чтобы координаты выделения правильно запоминались
   //независимо от того, в какую сторону тащим курсор мыши
   Rectangle rect = new Rectangle ();
   int dX = location.X - orig.X, dY = location.Y - orig.Y;
   System.Drawing.Size size = new System.Drawing.Size (Math.Abs (dX), Math.Abs (dY));
    //размеры текущего выделения
   if (dX >= 0 && dY >= 0) rect = new Rectangle (orig, size);
   else if (dX < 0 && dY > 0) rect = new Rectangle (location.X, orig.Y, size.Width, size.Height);
   else if (dX > 0 && dY < 0) rect = new Rectangle (orig.X, location.Y, size.Width, size.Height);
   else rect = new Rectangle (location, size);
   return rect;
  }

Теперь, при наличии на изображении выделенной рамки selRect можно, например, реализовать его обрезание по границам этой рамки:

   if (pictureBox1.Image == null) return;
   if (selRect.Width > 0 && selRect.Height > 0) {
    Bitmap bitmap1 = new Bitmap (pictureBox1.Image);
    Bitmap bitmap2 = new Bitmap (selRect.Width, selRect.Height);
    Graphics g = Graphics.FromImage (bitmap2);
    g.DrawImage (bitmap1, 0, 0, selRect, GraphicsUnit.Pixel);
    pictureBox1.Image = bitmap2;
    selRect = new Rectangle (0, 0, 0, 0);
   }

 Скачать пример Lab5_2 в архиве .zip с проектом C# Visual Studio 2019 (14 Кб)

Проект Lab5_3. Рисование фигур. Показанный выше подход нетрудно применить для рисования геометрических примитивов на канве PictureBox или формы.

Создадим форму как в предыдущем примере с PictureBox, расположенным на Label, у компонент установлены те же свойства.

Рассмотрим варианты рисования линии на канве PictureBox. При движении мыши с зажатой левой кнопкой наша линия должна динамически обновляться, а при отпускании кнопки — добавляться на существующий рисунок.

Опишем в классе формы необходимые данные:

  Point p1, p2; //начало и конец линии
  Pen pen1; //перо
  Brush brush1; //кисть
  Bitmap Img1, Img2; //основная картинка, на которой рисуем и буферная
  Graphics gr; //графический контекст
  bool isPressed; //флажок "кнопка мыши зажата"

Для самой формы нам понадобится запрограммировать событие Load, где мы инициализируем эти объекты, то есть, создадим рисунок размером с клиентскую часть окна формы, назначим его компоненте, создадим перо и выставим в «ложь» флажок:

   Img1 = new Bitmap (ClientSize.Width, ClientSize.Height);
   pictureBox1.Image = Img1;
   gr = Graphics.FromImage (Img1);
   pen1 = new Pen (Color.Black);
   isPressed = false;

Всё остальное запрограммируем в событиях PictureBox. На нажатие кнопки мыши будем включать флажок и запоминать место клика p1:

  private void pictureBox1_MouseDown (object sender, MouseEventArgs e) {
   isPressed = true;
   p1 = e.Location;
  }

На отпускание кнопки получим координаты второй точки p2 и соединим её с первой, проведя линию на образе Img1. Заметим, что в реальном коде можно добавлять точки в контейнер, например, в список List из объектов Point. Если при этом запоминать, какой именно объект рисовался, можно в нужные моменты просто перерисовывать объекты по списку (что может предотвратить «утечки памяти» при работе приложения), а также удалять или динамически изменять их.

  private void pictureBox1_MouseUp (object sender, MouseEventArgs e) {
   p2 = e.Location;
   gr = Graphics.FromImage (Img1);
   gr.DrawLine (pen1, p1, p2);
   gr.Save ();
   isPressed = false;
   pictureBox1.Invalidate ();
  }

На перемещение мыши обработка будет немного хитрей. Если кнопка не зажата, ничего делать не нужно, а в противном случае будем проводить текущую линию на копии рисунка Img2, созданной из Img1, чтобы не получилось «веера» из линий при перемещении мыши с зажатой кнопкой. Img2 всё равно придётся временно назначить рисунком для PictureBox, чтобы линия была видна в процессе движения мыши.

  private void pictureBox1_MouseMove (object sender, MouseEventArgs e) {
   if (!isPressed) return; //Кнопка не зажата - выйти
   p2 = e.Location;
   Img2 = new Bitmap (Img1);
   pictureBox1.Image = Img2;
   gr = Graphics.FromImage (Img2);
   gr.DrawLine (pen1, p1, p2);
   pictureBox1.Invalidate ();
  }

В показанном примере все координаты отсчитывались «внутри PictureBox» и получались непосредственно из аргумента MouseEventArgs обработчика события. По-другому можно сделать, используя координаты курсора относительно экрана Cursor.Position.X, Cursor.Position.Y, а затем вычитая из них координаты верхнего левого угла формы Location.X, Location.Y (и, возможно, дополнительные значения, учитывающие занятое другими компонентами пространство на форме):

   int x1 = Cursor.Position.X - Location.X,
    y1 = Cursor.Position.Y - Location.Y;

Расположив на панели инструментов приложения дополнительные кнопки для выбора геометрического примитива, цвета и т. п., мы можем получить приложение графический-редактор (см. прикреплённый проект, где ряд возможностей уже добавлен).

Обратите внимание в коде проекта Lab5_3: при рисовании линии и эллипса координаты второй точки могут быть и «меньше» (ближе к левому верхнему углу холста), чем первой. При рисовании же прямоугольника область экрана должна быть задана, начиная с левого верхнего угла. Метод GetRectangle, вызываемый при обработке событий pictureBox1_MouseUp и pictureBox1_MouseMove, корректирует эту проблему, как и при выделении прямоугольником в проекте Lab5_2.

 Скачать пример Lab5_3 в архиве .zip с проектом C# Visual Studio 2019 (19 Кб)

Простая рисовалка на Windows Forms C#

Простая рисовалка на Windows Forms C#

Проект Lab5_4. Другой контекст. В завершение заметим, что отрисовка, выполняемая в объекте графического контекста, позволяет без каких-либо изменений основного кода «перенести» графический объект на другую канву, например, на экран вместо окна приложения.

Пусть в классе формы имеется метод Draw, создающий некоторый рисунок:

  void Draw (System.Drawing.Graphics g) { //Графический контекст передан в наш метод
   Pen [] pens =  { Pens.Red, Pens.Yellow, Pens.Green };
    //Разные перья для рисования
   int width = ( this.ClientSize.Width - 1 ) / 2, 
       height = ( this.ClientSize.Height - 1 ) / 2;
    //Половинки ширины и высоты клиентской части окна
   for (int i = 0; i < 3; i++) //Демо - рисуем перьями
    g.DrawRectangle (pens [i], i * width, i * height, width, height);
   Brush [] brushes = { Brushes.Red, Brushes.Yellow, Brushes.Green };
    //Разные кисти для выполнения заливки цветом
   for (int i = 0; i < 3; i++) //Демо - рисуем кистями
    g.FillEllipse (brushes [i], i * width, i * height, width, height);
   g.DrawLine (pens [2], 0, 0, width * 2, height * 2); //Рисуем линию пером
  }

Как и в начале статьи, мы могли бы вызвать его кодом вида

   Graphics g = this.CreateGraphics ();
   Draw (g);

для отображения картинки непосредственно на канве формы.

Теперь выведем рисунок в контексте графического экрана Windows поверх всех окон.

Убедимся, что к файлу формы подключены нужные пространства имён:

using System.Runtime.InteropServices;
using System.Drawing;

В классе формы (например, после конструктора) укажем ссылки на нужные методы библиотеки user32.dll, которые нам потребуются:

  [DllImport ("user32.dll")]
  public static extern IntPtr GetDC (IntPtr hwnd);
  [DllImport ("user32.dll")]
  public static extern void ReleaseDC (IntPtr hwnd, IntPtr dc);

В методе рисования (например, по событию Paint формы) вызовем наш метод с другим контекстом:

  private void Form1_Paint (object sender, PaintEventArgs e) {
   IntPtr desktopPtr = GetDC (IntPtr.Zero);
   Graphics g = Graphics.FromHdc (desktopPtr);

   Draw (g);

   g.Dispose ();
   ReleaseDC (IntPtr.Zero, desktopPtr);
  }

Также можно потребовать где-нибудь принудительной перерисовки, например, по клику на форме:

  private void Form1_Click (object sender, EventArgs e) {
   Invalidate ();
  }

Подключить к проекту внешнюю библиотеку можно и непосредственно, например, для нашего случая:

  • в верхнем меню выбрать команду Проект — Добавить существующий элемент…;
  • в списке типов файлов выбрать «Исполняемые файлы», показать расположение нужного файла (c:\Windows\System32\user32.dll) и нажать «Добавить»;
  • выбрать добавленный файл в Обозревателе решений, в окне «Свойства» указать для него значение «Копировать в выходной каталог» равным «Копировать более новую версию»;
  • после этого прототипы нужных функций библиотеки можно описать в классе формы с атрибутами public static extern и предшествующей директивой [DllImport ("user32.dll")], как мы делали выше.

 Скачать пример Lab5_4 в архиве .zip с проектом C# Visual Studio 2019 (11 Кб)

Задание по теме может быть, например, таким: реализовать графическое приложение в соответствии с вариантом. Предусмотреть в приложении следующие возможности:

  • сохранение полученных графических файлов;
  • прокрутка файлов, если они «не помещаются» в окне компоненты PictureBox;
  • возможность построить изображение более, чем в одном масштабе (или масштабировать его программно);
  • не менее двух настраиваемых параметров, позволяющих изменить внешний вид объекта (например, для объекта «дом» — количество этажей и количество окон на каждом этаже).

05.04.2023, 19:26 [1915 просмотров]


К этой статье пока нет комментариев, Ваш будет первым

Читайте, как управлять производительностью графики с помощью стандартных инструментов Windows 10. Как установить производительность графики отдельно для каждого приложения.

Средние и высокопроизводительные персональные компьютеры обычно имеют специальный графический процессор «GPU». Специальный графический процессор используется вашей системой для запуска приложений, требующих интенсивных ресурсов (например, современные виды игр), которые не может обработать обычная видеокарта. Графический процессор обычно представляет собой чип «NVIDIA» или «AMD», и оба имеют собственную специализированную панель управления.

Панель Windows 10 “Настройки производительности графики”

Панели управления позволяют пользователям принудительно использовать графический процессор для обработки приложения. Когда запущенное приложение вынуждено использовать графический процессор, это означает, что ваш компьютер работает в режиме высокой производительности. Как правило, ваша операционная система сама решает, какое приложение должно использовать выделенный графический процессор, но вы можете, конечно, и самостоятельно выбрать его. «Windows 10» добавляет новую панель, которая позволяет вам устанавливать производительность графики для каждого конкретного приложения.

Эта новая функция доступна только в «Windows 10» с установленным обновлением «Insider Build 17093». Она не позволяет выбирать между встроенной графической видеокартой и графическим процессором. Вместо этого она позволяет устанавливать параметры производительности по умолчанию для каждого из приложений. Приложение можно настроить так, чтобы оно всегда работало в режиме энергосбережения или в режиме высокой производительности. Некоторые приложения могут не использовать выделенный графический процессор, и в этом случае, вы ничего не сможете с этим поделать. Вы можете попытаться принудительно заставить приложение использовать выделенный графический процессор с панели управления вашего «GPU», но он может и не заработать. В соответствии с обновлениями «Microsoft» эта новая панель настроек заменяет такую функцию на панели управления вашего «GPU». Вы можете использовать ее или воспользоваться панелью управления для вашей видеокарты.

Производительность графики для каждого приложения

Откройте приложение «Параметры Windows», нажав в нижней части экрана кнопку «Пуск» и выбрав в левом боковом меню кнопку с изображением шестеренки.

windows-settings.png

Либо используйте более быстрый и простой способ – нажмите сочетание клавиш «Windows + Х» или щелкните правой кнопкой мыши по кнопке «Пуск» в нижнем левом углу экрана и выберите во всплывающем меню раздел «Параметры».

settings.png

Перейдите в группу настроек «Система», выберите вкладку «Дисплей» и прокрутите бегунок вниз до ссылки «Дополнительные графические параметры» и нажмите на нее.

system.png

Затем в открывшемся окне выберите тип приложения, для которого будете выполнять настройки графической производительности. Далее вам будет предложен список, который будет заполнен на основе выбранного вами типа приложения. Выберите приложение и нажмите кнопку «Добавить». Когда приложение появится в списке, щелкните его и нажмите кнопку «Параметры». В нашем случае мы добавили приложение «Paint 3D».

settings02.png

Кнопка «Параметры» отобразит окно с тремя возможными настройками производительности графики, которые вы можете установить для приложения. Параметр «Системное значение по умолчанию» позволяет операционной системе самой выбрать лучшие параметры производительности. Параметр «Энергосбережение» обычно означает, что будет использоваться встроенная графическая видеокарта, а параметр «Высокая производительность – применение целевого графического процессора «GPU». Конечно, есть исключения, которые будут описаны ниже.

system-default-value.png

Исключения

Существуют случаи, кода пользователь не может указать приложению, какие параметры графики ему использовать. На изображении выше, приложение «Paint 3D» может использовать только встроенную графическую видеокарту, хотя наша тестовая система оснащена графическим процессором «NVIDIA». Приложения вроде этого, просто не могут использовать выделенный графический процессор, и нет простого способа заставить их сделать это. Даже если вы сможете заставить приложение, такое как «Paint 3D», использовать выделенный графический процессор, то в итоге лучше работать оно все равно не станет.

Аналогично, некоторые приложения (в основном современные игры) не смогут использовать встроенную графическую карту. Они всегда будут использовать только выделенный графический процессор, и соответствующий вариант будет указан в параметрах графики.

Логично, что возникают вопросы, не бесполезна ли эта настройка, но это не так. Даже если приложение использует встроенную графику, оно все равно может применять параметры высокой производительности. Аналогично, приложение, которое должно использовать выделенный графический процессор, может по-прежнему использовать его в режиме с низким энергопотреблением. Использование этих параметров не будет безболезненным. Если вы обнаружите, что изменение предпочтений в отношении производительности графики для приложения приводит к низкой производительности, медленному отклику или сбою работы приложения, то вы всегда можете вернуть его настройки в режим по умолчанию.

Здесь будет довольно много проб и ошибок, потому что в настоящее время доступно несколько разных моделей графических процессоров, системные спецификации которых сильно различаются, а приложения имеют свои собственные настройки и возможности. Компания «Microsoft» явно пытается предоставить пользователям более совершенную поддержку графического процессора, и, хотя эта новая функция может быть не идеальной, она все же полезна. Это определенно одна из тех функций, которую пользователи получат в следующем официальном обновлении операционной системы «Windows 10».

Полную версию статьи со всеми дополнительными видео уроками читайте в нашем блоге…

Данный материал является частной записью члена сообщества Club.CNews.
Редакция CNews не несет ответственности за его содержание.

  • Как работать с windows forms visual studio
  • Как работать с внешним жестким диском в windows 10
  • Как работать с github на windows
  • Как работать с ext4 на windows
  • Как работать с wireshark в windows 10