1 Постановка задачі

Припустимо, необхідно розробити програму графічного інтерфейсу користувача, яка знаходить корені квадратного рівняння, коефіцієнтами якого є функції від параметра t.

f(t) · x2 + g(t) · x + c = 0

Функції f(t) та g(t) можуть буди довільними. В нашому прикладі вони визначатимуться таким чином:

f(t) = (a0 + a1) (a1 + a2) ... (am – 1 + am) – t

g(t) = x0 y0 + x1 y1 + ...+ xn yn + t

Програма повинна здійснювати введення (з клавіатури або з XML-файлу) та редагування вихідних даних з можливістю їх зберігання в іншому XML-файлі, обчислення коренів, геометричну інтерпретацію, зберігання та відображення звіту зі включенням до нього графіку, а також отримання контекстної допомоги.

2 Визначення вимог до програмного забезпечення. Аналіз та проєктування

2.1 Розробка діаграми варіантів використання

Вимоги до програмного забезпечення відображаються на діаграмі варіантів використання.

В нашому випадку така діаграма повинна включати одного актора – користувача, якому надається можливість використовувати такі функції:

  • створення нового проєкту для введення даних у порожню таблицю;
  • читання даних з XML-документа;
  • запис модифікованих даних в інший XML-документ;
  • розв'язання рівняння з графічною інтерпретацією; розв'язання рівняння передбачає обов'язкову перевірку дискримінанту;
  • генерація звіту;
  • отримання довідки.

На рис. 2.1 наведена діаграма варіантів використання.

Рисунок 2.1 – Діаграма варіантів використання

2.2 Розробка структури даних та підготовка тестів

До початку програмної реалізації слід підготувати вихідні дані. Відповідно до постановки задачі, дані про рівняння слід представити у вигляді XML-документа. В нашому випадку рівняння характеризується набором чисел ai та bj, а також скалярним значенням c.

Наведемо приклад XML-документа, який описує рівняння.

<?xml version="1.0"?>
<EquationData CCoef="-2">
  <FFunction>
    <ACoefs>
      <ACoef Value = "3" Index="0" />
      <ACoef Value = "1" Index="1" />
      <ACoef Value = "0" Index="2" />
      <ACoef Value = "0.25" Index="3" />
    </ACoefs>
  </FFunction>
  <GFunction>
    <XYCoefs>
      <XYCoef X = "-1" Y = "2" />
      <XYCoef X = "1" Y = "1" />
      <XYCoef X = "0.0" Y = "0.1" />
    </XYCoefs>
  </GFunction>
</EquationData>

Примітка: для коефіцієнтів індекси є істотними; разом з тим, не можна покластися на порядок розташування елементів у XML-документі (порядок може бути довільним), і тому індекси слід зберігати окремо.

Можна зберегти ці дані, скажімо, в файлі EquationCommon.xml (він описує загальний випадок). Слід підготувати декілька варіантів файлів для тестування програмного забезпечення. Наприклад, якщо змінити значення CCoef (-2) на будь-яке додатне, наприклад, 2, рівняння не матиме розв'язків. Цей варіант зберігаємо в файлі EquationNoSolutions.xml. Можна також створити файли, в яких коефіцієнт при другому степені x дорівнюватиме 0. Це LinearEquation.xml:

<?xml version="1.0"?>
<EquationData CCoef="6">
  <FFunction>
    <ACoefs>
      <ACoef Value = "1" Index="0" />
      <ACoef Value = "1" Index="1" />
      <ACoef Value = "0" Index="2" />
      <ACoef Value = "0" Index="3" />
    </ACoefs>
  </FFunction>
  <GFunction>
    <XYCoefs>
      <XYCoef X = "-1" Y = "2" />
      <XYCoef X = "1" Y = "1" />
      <XYCoef X = "0" Y = "1" />
    </XYCoefs>
  </GFunction>
</EquationData>

А також LinearEquationNoSolutions.xml:

<?xml version="1.0"?>
<EquationData CCoef="6">
  <FFunction>
    <ACoefs>
      <ACoef Value = "1" Index="0" />
      <ACoef Value = "1" Index="1" />
      <ACoef Value = "0" Index="2" />
      <ACoef Value = "0" Index="3" />
    </ACoefs>
  </FFunction>
  <GFunction>
    <XYCoefs>
      <XYCoef X = "-1" Y = "0" />
      <XYCoef X = "0" Y = "1" />
      <XYCoef X = "0" Y = "1" />
    </XYCoefs>
  </GFunction>
</EquationData>

3 Програмна реалізація

3.1 Створення класу для представлення рівняння

Програмне рішення .NET з використанням мови програмування C#, яке буде створене, складатиметься з бібліотеки класів, консольного застосунку та застосунку графічного інтерфейсу користувача. Бібліотека міститиме класи, які описують основні сутності предметної області. Ці класи реалізуватимуть функціональність програми, що створюється. Для тестування математичних методів, засобів введення-виведення та генерації звіту створюємо консольний застосунок. Пізніше в цьому рішенні буде створено проєкт, який реалізує застосунок графічного інтерфейсу користувача.

У середовищі MS Visual Studio створюємо новий проєкт – бібліотеку класів (Create a new project | Class Library) з ім'ям (Project name) QuadraticLib. Для рішення обираємо ім'я (Solution name) Quadratic. На сторінці Additional Information у списку Framework обираємо найновішу версію .NET та натискаємо кнопку Create. Отримуємо такий сирцевий код, який міститься у файлі Class1.cs:

namespace QuadraticLib
{
    public class Class1
    {

    }
}

Ім'я класу Class1 можна змінити на Quadratic, а файлу – на Quadratic.cs. Тепер до класу Quadratic можна додавати поля, властивості та методи. Функції f(t) та g(t) будуть представлені відповідними властивостями типу делегату, коефіцієнт – властивістю дійсного типу. Корені, які будуть знайдені, записуватимуться в список. Необхідні методи отримання знайдених коренів, функція розв'язання рівняння та метод тестування.

Перед описом класу можна додати визначення тип переліку, який визначає можливі стани об'єкта "Квадратне рівняння", а також необхідний тип делегату.

Сирцевий код файлу Quadratic.cs буде таким:

// Quadratic.cs
namespace QuadraticLib
{
    /// <summary>
    /// Перелік, який визначає можливі стани об'єкту "Квадратне рівняння"
    /// </summary>
    public enum EquationState 
    {
        NoData, 
        NotSolved, 
        NoRoots, 
        RootsFound, 
        InfinityOfRoots
    }

    /// <summary>
    /// Опис типу функції, яка приймає параметр дійсного типу
    /// та повертає дійсне значення
    /// </summary>
    /// <param name="t">параметр</param>
    /// <returns>значення функції</returns>
    public delegate double DFunction(double t);

    /// <summary>
    /// Представляє квадратне рівняння. 
    /// Коефіцієнти a і b визначаються функціями f(t) та g(t)
    /// </summary>
    public class Quadratic
    {
        /// <summary>
        /// Функція f(t)
        /// </summary>
        public DFunction F { get; set; } = t => 0;

        /// <summary>
        /// Функція g(t)
        /// </summary>
        public DFunction G { get; set; } = t => 0;

        /// <summary>
        /// коефіцієнт c
        /// властивість віртуальна, і це дозволяє у похідних класах
        /// визначити інший спосіб представлення даних
        /// </summary>
        virtual public double C { get; set; }

        /// <summary>
        /// Стан рівняння
        /// </summary>
        public EquationState State { get; set; } = EquationState.NoData;

        /// <summary>
        /// Останнє значення параметру
        /// </summary>
        public double T { get; set; }
        
        /// <summary>
        /// Список коренів рівняння
        /// </summary>
        public List<double> Roots { get; private set; } = new();

        /// <summary>
        /// Розв'язує квадратне рівняння. Коефіцієнти a та b залежать від параметру.
        /// Після завершення виконання функції у списку коренів два значення
        /// (квадратне рівняння, дискримінант більше або дорівнює 0),
        /// одне значення (лінійне рівняння, яке можна розв'язати),
        /// Якщо список порожній, коренів немає.
        /// Відповідно змінюється значення властивості State
        /// </summary>
        /// <param name="t">параметр</param>
        /// <returns>поточний об'єкт зі зміненим станом</returns>
        public Quadratic Solve(double t)
        {
            T = t;
            Roots = new();
            double a = F(t);
            double b = G(t);
            if (a == 0)
            {
                if (b == 0 && C == 0)
                {
                    State = EquationState.InfinityOfRoots; 
                    return this;  // Безмежна кількість розв'язків
                }
                if (b == 0 && C != 0)
                {
                    State = EquationState.NoRoots;
                    return this;  // Немає коренів
                }
                Roots.Add(-C / b);
                State = EquationState.RootsFound;
                return this;      // Один корінь
            }
            // Обчислення дискримінанту:
            double d = b * b - 4 * a * C;
            if (d < 0)
            {
                State = EquationState.NoRoots;
                return this;      // Немає коренів
            }
            Roots.Add((-b - Math.Sqrt(d)) / (2 * a));
            Roots.Add((-b + Math.Sqrt(d)) / (2 * a));
            State = EquationState.RootsFound;
            return this;          // Два кореня
        }

        /// <summary>
        /// Формує рядок з результатами рівняння (два корені)
        /// </summary>
        /// <returns>рядок з результатами рівняння</returns>
        public string GetRoots()
        {
            return "X1 = " + Roots[0] + " X2 = " + Roots[1];
        }
    }
}

Окремо доцільно створити клас для отримання представлення даних про рівняння у вигляді рядка:

// QuadraticToString.cs
namespace QuadraticLib
{
    public static partial class Str
    {

        public static String ToString(Quadratic quadratic)
        {
            string st = quadratic.State == EquationState.NoData ||
                        quadratic.State == EquationState.NotSolved ? "" : "t = " + quadratic.T + "\t";
            return st + quadratic.State switch
            {
                EquationState.NoData => "Немає даних",
                EquationState.NotSolved => "Рівняння не було розв\'язане",
                EquationState.InfinityOfRoots => "Безліч коренів",
                _ => quadratic.Roots.Count switch
                {
                    0 => "Немає коренів",
                    1 => "Корінь: " + quadratic.Roots[0],
                    2 => "Корені: " + quadratic.GetRoots(),
                    _ => "Невідома помилка",
                },
            };
        }
    }
}

Атрибут partial в описі класу дозволить розширити цей клас функціями для представлення у вигляді рядка інших об'єктів та розташувати відповідні частини класів в інших файлах.

Для перевірки працездатності цього та інших класів бібліотеки до рішення доцільно додати проєкт – консольний застосунок (File | Add | New Project... | Console App) з ім'ям (Name) QuadraticConsoleApp.

У проєкті створюємо окремий клас, який містить методи для тестування розв'язання рівняння. Цей клас також доцільно визначити з атрибутом partial:

// EquationTests.cs
using QuadraticLib;

namespace QuadraticConsoleApp
{
    /// <summary>
    /// Надає методи для демонстрації роботи консольного застосунку
    /// </summary>
    static partial class ConsoleTests
    {
        /// <summary>
        /// Здійснює тестування розв'язання рівняння для одного значення t
        /// </summary>
        /// <param name="quadratic">посилання на рівняння</param>
        /// <param name="t">параметр</param>
        internal static void Test(Quadratic quadratic, double t)
        {
            Console.Write(" Quadratic: ");
            quadratic.Solve(t);
            Console.WriteLine(Str.ToString(quadratic));
            Console.WriteLine();
        }

        /// <summary>
        /// Тестує квадратне рівняння у загальному вигляді
        /// </summary>
        internal static void TestEquation()
        {
            Console.WriteLine("\n==========Тестування рівняння в найпростішому варіанті:==========");
            // Квадратне рівняння:
            Console.WriteLine("------Загальний випадок (x^2 + x - 2)------");
            ConsoleTests.Test(new Quadratic() { F = t => 1, G = t => 1, C = -2 }, 0); // значення t не впливає на результат

            Console.WriteLine("------Загальний випадок (x^2 + 2x + 1)------");
            ConsoleTests.Test(new Quadratic() { F = t => 1, G = t => t, C = 1 }, 2);

            Console.WriteLine("------Немає розв'язків (x^2 + x + 10)------");
            ConsoleTests.Test(new Quadratic() { F = t => t, G = t => t, C = 10 }, 1);

            // Лінійне рівняння:
            Console.WriteLine("------Лінійне рівняння (один корінь)-----");
            ConsoleTests.Test(new Quadratic() { F = t => 0, G = t => 1, C = -0.5 }, 0);

            Console.WriteLine("------Лінійне рівняння (немає коренів)-----");
            ConsoleTests.Test(new Quadratic() { F = t => 0, G = t => 0, C = 1 }, 0);

            Console.WriteLine("------Лінійне рівняння (безмежна кількість коренів)-----");
            ConsoleTests.Test(new Quadratic() { F = t => 0, G = t => 0, C = 0 }, 0);
        }
    }
}

У методі Main() класу Program здійснюємо демонстрацію можливостей розв'язання рівняння для різних даних:

// Program.cs
using static QuadraticConsoleApp.ConsoleTests;

namespace QuadraticConsoleApp
{
    /// <summary>
    /// Консольний застосунок для демонстрації функцій проєкту
    /// </summary>
    class Program
    {
        /// <summary>
        /// Стартова точка застосунку. 
        /// Послідовно тестує квадратне рівняння у загальному вигляді,
        /// функції, які використовують списки для зберігання даних
        /// і квадратне рівняння, дані про яке зберігаються в XML-файлах
        /// </summary>
        static void Main()
        {
            Console.OutputEncoding = System.Text.Encoding.UTF8;
            TestEquation();            
        }
    }
}

Для того, щоб проєкт міг бути скомпільований, до проєкту QuadraticConsoleApp необхідно додати посилання на проєкт QuadraticLib. У контекстному меню Solution Explorer для проєкту QuadraticConsoleApp вибираємо Add | Project Reference..., далі вибираємо проєкт QuadraticLib і натискаємо OK. Крім того, через контекстне меню Solution Explorer необхідно встановити проєкт QuadraticConsoleApp як стартовий (Set as Startup Project).

Після старту програми отримаємо такі результати:

==========Тестування рівняння в найпростішому варіанті:==========
------Загальний випадок (x^2 + x - 2)------
 Quadratic: t = 0       Корені: X1 = -2 X2 = 1

------Загальний випадок (x^2 + 2x + 1)------
 Quadratic: t = 2       Корені: X1 = -1 X2 = -1

------Немає розв'язків (x^2 + x + 10)------
 Quadratic: t = 1       Немає коренів

------Лінійне рівняння (один корінь)-----
 Quadratic: t = 0       Корінь: 0.5

------Лінійне рівняння (немає коренів)-----
 Quadratic: t = 0       Немає коренів

------Лінійне рівняння (безмежна кількість коренів)-----
 Quadratic: t = 0       Безліч коренів

3.2 Створення класів для представлення функцій f(t) та g(t)

Тепер до бібліотеки можна додати класи, які відповідають за представлення функцій f(t) і g(t) відповідно до до поставленої задачі. До бібліотеки слід додати новий клас (У вікні Solution Explorer у контекстному меню бібліотеки Add | New Item... | Class). Можна вибрати ім'я класу Functions. Далі код класу безпосередньо можна видалити та додати опис інтерфейсу IFunction, який представлятиме універсальну функцію.

До цього ж файлу додаємо класи FFunction та GFunction. Для представлення необхідних даних функцій можна скористатися списками (List). Окремі коефіцієнти функції f(t) матимуть тип ACoef. Окремі пари x та y функції g(t) матимуть тип XYCoef. Вміст файлу Functions.cs буде таким:

// Functions.cs
namespace QuadraticLib
{
    /// <summary>
    /// Абстрактне представлення функції та методу її тестування
    /// </summary>
    public interface IFunction
    {
        /// <summary>
        /// Абстрактний опис функції
        /// </summary>
        /// <param name="t">параметр</param>
        /// <returns>дійсне значення</returns>
        abstract public double Func(double t);
    }

    /// <summary>
    /// Коефіцієнт функції f(t)
    /// </summary>
    public class ACoef
    {
        // Атрибути необхідні для керування записом у XML-документ 
        // під час майбутньої серіалізації
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public double Value { get; set; }

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public int Index { get; set; }
    }

    /// <summary>
    /// Клас, який представляє функцію f(t)
    /// </summary>
    public class FFunction : IFunction
    {
        /// <summary>
        /// Список коефіцієнтів
        /// </summary>
        public List<ACoef> ACoefs { get; set; } = new();

        /// <summary>
        /// Індексатор для доступу до коефіцієнтів
        /// </summary>
        public ACoef this[int index]
        {
            get => ACoefs[index];
            set => ACoefs[index] = value;
        }

        /// <summary>
        /// Повертає кількість коефіцієнтів A
        /// </summary>
        public int ACount
        {
            get => ACoefs.Count;
        }

        /// <summary>
        /// Додає новий елемент до списку
        /// </summary>
        /// <param name="value">новий елемент</param>
        public void AddA(double value, int index)
        {
            ACoefs.Add(new ACoef { Value = value, Index = index});
        }

        /// <summary>
        /// Видаляє останній елемент зі списку
        /// </summary>
        public void RemoveLastA()
        {
            ACoefs.RemoveAt(ACoefs.Count - 1);
        }
        
        /// <summary>
        /// Повертає значення a за індексом
        /// </summary>
        /// <param name="index">індекс</param>
        /// <returns>знайдене значення</returns>
        public double GetValue(int index)
        {
            return new List<ACoef>(from a in ACoefs where a.Index == index select a)[0].Value;
        }

        /// <summary>
        /// Обчислює функцію f(t)
        /// </summary>
        /// <param name="t">параметр</param>
        /// <returns>дійсне значення</returns>
        public double Func(double t)
        {
            double p = 1;
            if (ACount == 0)
            {
                p = 0;
            }
            else
            {
                for (int i = 0; i < ACount - 1; i++)
                    p *= GetValue(i) + GetValue(i + 1);
            }
            return p - t;
        }
    }

    /// <summary>
    /// Пара чисел X та Y
    /// </summary>
    public class XYCoef
    {
        // Атрибути необхідні для керування записом у XML-документ 
        // під час майбутньої серіалізації
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public double X { get; set; }

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public double Y { get; set; }
    }

    /// <summary>
    /// Клас, який представляє функцію g(t)
    /// </summary>
    public class GFunction : IFunction
    {
        /// <summary>
        /// Список пар
        /// </summary>
        public List<XYCoef> XYCoefs { get; set; } = new();

        /// <summary>
        /// Реалізація індексатора через список
        /// </summary>
        public XYCoef this[int index]
        {
            get => XYCoefs[index];
            set => XYCoefs[index] = value;
        }

        /// <summary>
        /// Повертає кількість пар
        /// </summary>
        public int PairsCount
        {
            get => XYCoefs.Count;
        }

        /// <summary>
        /// Додає новий елемент до списку пар
        /// </summary>
        /// <param name="p">нова пара</param>
        public void AddXY(XYCoef p)
        {
            XYCoefs.Add(p);
        }

        /// <summary>
        /// Додає новий елемент до списку пар
        /// </summary>
        /// <param name="x">нове значення x</param>
        /// <param name="y">нове значення y</param>
        public void AddXY(double x, double y)
        {
            XYCoefs.Add(new XYCoef { X = x, Y = y });
        }

        /// <summary>
        /// Видаляє останній елемент зі списку пар
        /// </summary>
        public void RemoveLastPair()
        {
            XYCoefs.RemoveAt(XYCoefs.Count - 1);
        }

        /// <summary>
        /// Генерує ітератор для обходу пар
        /// </summary>
        /// <returns>ітератр</returns>
        public IEnumerator<XYCoef> GetEnumerator()
        {
            for (int i = 0; i < PairsCount; i++)
                yield return this[i];
        }

        /// <summary>
        /// Обчислює функцію g(t)
        /// </summary>
        /// <param name="t">параметр</param>
        /// <returns>дійсне значення</returns>
        public double Func(double t)
        {
            double sum = 0;
            foreach (XYCoef p in this)
                sum += p.X * p.Y;
            return sum + t;
        }
    }
}

Тепер можна також додати тестування функцій. У файлі FunctionsTests.cs реалізуємо другу частину класу ConsoleTests:

// FunctionsTests.cs
using QuadraticLib;

namespace QuadraticConsoleApp
{
    /// <summary>
    /// Надає методи для демонстрації роботи консольного застосунку
    /// </summary>
    static partial class ConsoleTests
    {
        /// <summary>
        /// Виводить на консоль таблицю значень аргументу та функції
        /// </summary>
        /// <param name="name">ім'я функції</param>
        /// <param name="from">початок інтервалу</param>
        /// <param name="to">кінець інтервалу</param>
        /// <param name="step">крок</param>
        internal static void Test(IFunction f, string name, double from, double to, double step)
        {
            Console.WriteLine("*********** " + f.GetType() + " ***********");
            for (double t = from; t <= to; t += step)
                // Форматоване виведення аргументу та функції:
                Console.WriteLine("t = {1}   \t {0}(t) = {2}", name, t, f.Func(t));
        }

        /// <summary>
        /// Тестує функції, які використовують списки для зберігання даних
        /// </summary>
        internal static void TestFunctions()
        {
            Console.WriteLine("\n=====Тестування функцій із використанням списків:=====");
            // Тестування функції F:
            FFunction fFunction = new()
            {
                ACoefs = new List<ACoef>
                {
                    new() { Index = 0, Value = 1 },
                    new() { Index = 1, Value = 2 },
                    new() { Index = 2, Value = 3 }
                }
            };
            ConsoleTests.Test(f: fFunction, name: "F", from: -5, to: 15, step: 1);

            // Тестування функції G:
            GFunction gFunction = new()
            {
                XYCoefs = new List<XYCoef>
                {
                    new() { X = 1, Y = 2 },
                    new() { X = 3, Y = 4 },
                    new() { X = 5, Y = 6 }
                }
            };
            ConsoleTests.Test(f: gFunction, name: "G", from: -5, to: 15, step: 1);
        }
    }
}

До функції Main() додаємо виклик функції TestFunctions():

static void Main()
{
    Console.OutputEncoding = System.Text.Encoding.UTF8;
    TestEquation();
    TestFunctions();
}

Результати тестування функцій, які виводяться після тестування рівняння, будуть такими:

=====Тестування функцій із використанням списків:=====
*********** QuadraticLib.FFunction ***********
t = -5           F(t) = 20
t = -4           F(t) = 19
t = -3           F(t) = 18
t = -2           F(t) = 17
t = -1           F(t) = 16
t = 0            F(t) = 15
t = 1            F(t) = 14
t = 2            F(t) = 13
t = 3            F(t) = 12
t = 4            F(t) = 11
t = 5            F(t) = 10
t = 6            F(t) = 9
t = 7            F(t) = 8
t = 8            F(t) = 7
t = 9            F(t) = 6
t = 10           F(t) = 5
t = 11           F(t) = 4
t = 12           F(t) = 3
t = 13           F(t) = 2
t = 14           F(t) = 1
t = 15           F(t) = 0
*********** QuadraticLib.GFunction ***********
t = -5           G(t) = 39
t = -4           G(t) = 40
t = -3           G(t) = 41
t = -2           G(t) = 42
t = -1           G(t) = 43
t = 0            G(t) = 44
t = 1            G(t) = 45
t = 2            G(t) = 46
t = 3            G(t) = 47
t = 4            G(t) = 48
t = 5            G(t) = 49
t = 6            G(t) = 50
t = 7            G(t) = 51
t = 8            G(t) = 52
t = 9            G(t) = 53
t = 10           G(t) = 54
t = 11           G(t) = 55
t = 12           G(t) = 56
t = 13           G(t) = 57
t = 14           G(t) = 58
t = 15           G(t) = 59

3.3 Створення класів для роботи з XML-документами

Наступним етапом є організація взаємодії з XML-документами для читання та запису даних. Найпростіший та адекватний підхід – серіалізація. Створюємо клас XMLQuadratic, похідний від Quadratic. Також додаємо допоміжний клас EquationData. Сирцевий код буде таким:

// XMLQuadratic.cs
using System.Xml;
using System.Xml.Serialization;

namespace QuadraticLib
{
    /// <summary>
    /// Представляє дані для розв'язання квадратного рівняння
    /// </summary>
    public class EquationData
    {
        /// <summary>
        /// Представляє функцію f списком
        /// </summary>
        public FFunction FFunction { get; set; } = new();

        /// <summary>
        /// Представляє функцію g списком
        /// </summary>
        public GFunction GFunction { get; set; } = new();

        /// <summary>
        /// Коефіцієнт C
        /// </summary>
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public double CCoef { get; set; }

    }

    /// <summary>
    /// Розширює клас для представлення квадратного рівняння 
    /// можливостями читання та запису вихідних даних
    /// </summary>
    public class XMLQuadratic : Quadratic
    {
        /// <summary>
        /// Дані для розв'язання квадратного рівняння
        /// </summary>
        public EquationData Data { get; set; } = new EquationData();

        /// <summary>
        /// коефіцієнт c
        /// </summary>
        public override double C
        {
            get => Data.CCoef;
            set => Data.CCoef = value;
        }

        /// <summary>
        /// Очищує дані та переводить рівняння у початковий стан
        /// </summary>
        public void ClearEquation()
        {
            Data = new();
            State = EquationState.NoData;
        }

        /// <summary>
        /// Конструктор
        /// </summary>
        public XMLQuadratic()
        {
            Data = new EquationData();
            F = Data.FFunction.Func;
            G = Data.GFunction.Func;
        }

        /// <summary>
        /// Здійснює читання з XML-документу
        /// </summary>
        /// <param name="fileName">ім'я файлу</param>
        /// <returns>поточний об'єкт зі зміненим станом</returns>
        public XMLQuadratic ReadFromXML(string fileName)
        {
            XmlSerializer deserializer = new(typeof(EquationData));
            using TextReader textReader = new StreamReader(fileName);
            Data = (deserializer.Deserialize(textReader) as EquationData) ?? new();
            F = Data.FFunction.Func;
            G = Data.GFunction.Func;
            State = EquationState.NotSolved;
            return this;
        }

        /// <summary>
        /// Здійснює запис у XML-документ
        /// </summary>
        /// <param name="fileName">ім'я файлу</param>
        public void WriteToXML(string fileName)
        {
            XmlSerializer serializer = new (typeof(EquationData));
            using var textWriter = new StreamWriter(fileName);
            using var xmlWriter = XmlWriter.Create(textWriter, new XmlWriterSettings { Indent = true });
            serializer.Serialize(xmlWriter, Data);
        }
    }
}

Як видно з тексту, дані про квадратне рівняння зібрані в окремий клас – EquationData.

Примітка. Присутнє у наведеному вище коді створення об'єктів за допомогою виклику методу на кшталт

var xmlWriter = XmlWriter.Create();

замість виклику конструктора забезпечує більш гнучкий підхід, оскільки у функції створення об'єкта можна передбачити створення та повернення посилань на об'єкти різних типів. Ці типи можуть бути похідними від базового типу, визначено як результат функції Create() (в нашому випадку це XmlWriter). Це фактично є однією з реалізацій патерну проєктування Factory Method (Фабричний метод).

В окремому класі реалізуємо генерацію звіту в форматі HTML:

// Report.cs
namespace QuadraticLib
{
    /// <summary>
    /// Надає засоби генерації звіту
    /// </summary>
    public static class Report
    {
        /// <summary>
        /// Генерує звіт про роботу програми в форматі HTML
        /// </summary>
        /// <param name="fileName">iм\'я файлу</param>
        /// <param name="t">параметр t</param>
        public static void GenerateReport(XMLQuadratic quadratic, string fileName, double t)
        {
            using StreamWriter writer = new(fileName);
            writer.WriteLine("<html>\n<head>");
            writer.WriteLine("<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>");
            writer.WriteLine("<title>Звіт з розв\'язання квадратного рівняння</title>");
            writer.WriteLine("</head>");
            writer.WriteLine("<body>");
            writer.WriteLine("<h2>Звіт</h2>");
            writer.WriteLine("<p>У результаті розв\'язання квадратного рівняння, з такими вихідними даними:</p>");
            writer.WriteLine("<p>Функція F:</p>");
            writer.WriteLine("<table border = '1' cellpadding=4 cellspacing=0>");
            writer.WriteLine("<tr>\n<th>№</th>\n<th>a</th>\n</tr>");
            for (int i = 0; i < quadratic.Data.FFunction.ACoefs.Count; i++)
            {
                writer.WriteLine("<tr>");
                writer.WriteLine("<td>" + quadratic.Data.FFunction.ACoefs[i].Index + "</td>");
                writer.WriteLine("<td>" + quadratic.Data.FFunction.ACoefs[i].Value + "</td>");
                writer.WriteLine("</tr>");
            }
            writer.WriteLine("</table>");
            writer.WriteLine("<p>Функція G:</p>");
            writer.WriteLine("<table border = '1' cellpadding=4 cellspacing=0>");
            writer.WriteLine("<tr>\n<th>№</th>\n<th>X</th>\n<th>Y</th>\n</tr>");
            for (int i = 0; i < quadratic.Data.GFunction.XYCoefs.Count; i++)
            {
                writer.WriteLine("<tr>");
                writer.WriteLine("<td>" + (i + 1) + "</td>");
                writer.WriteLine("<td>" + quadratic.Data.GFunction.XYCoefs[i].X + "</td>");
                writer.WriteLine("<td>" + quadratic.Data.GFunction.XYCoefs[i].Y + "</td>");
                writer.WriteLine("</tr>");
            }
            writer.WriteLine("</table>");
            quadratic.Solve(t);
            if (quadratic.State == EquationState.InfinityOfRoots)
                writer.WriteLine("<p>було встановлено, що рівняння має безліч коренів.</p>");
            else
            {
                if (quadratic.Roots.Count > 0)
                {
                    writer.WriteLine("<p>були отримані такі корені: </p>");
                    writer.WriteLine("<p>");
                    for (int i = 0; i < quadratic.Roots.Count; i++)
                    {
                        writer.WriteLine(quadratic.Roots[i] + "<br>");
                    }
                    writer.WriteLine("</p>");
                }
                else
                {
                    writer.WriteLine("було встановлено, що рівняння не має коренів");
                }
            }
            writer.WriteLine("</body>\n</html>");
        }
    }
}

Додаємо тестування нового класу:

// XMLTests.cs
using QuadraticLib;

namespace QuadraticConsoleApp
{
    /// <summary>
    /// Надає методи для демонстрації роботи консольного застосунку
    /// </summary>
    static partial class ConsoleTests
    {

        /// <summary>
        /// Тестує квадратне рівняння, дані про яке 
        /// зберігаються в XML-файлах
        /// </summary>
        internal static void TestEquationWithXML()
        {
            Console.WriteLine("\n==Тестування рівняння, дані про яке зберігаються в XML-файлах:===");
            XMLQuadratic quadratic = new();
            // Немає даних:
            Console.WriteLine(quadratic);
            // Загальний випадок:
            quadratic.ReadFromXML("EquationCommon.xml");
            Console.WriteLine("    Файл: EquationCommon.xml");
            // Не розв'язували рівняння:
            Console.WriteLine(Str.ToString(quadratic));
            Console.WriteLine(Str.ToString(quadratic.Solve(0)));
            Console.WriteLine(Str.ToString(quadratic.Solve(-5)));
            Console.WriteLine(Str.ToString(quadratic.Solve(-10)));
            Report.GenerateReport(quadratic, "Common.html", -10);
            // Немає коренів:
            Console.WriteLine("    Файл: EquationNoSolutions.xml");
            Console.WriteLine(Str.ToString(quadratic.ReadFromXML("EquationNoSolutions.xml").Solve(0)));
            Report.GenerateReport(quadratic, "NoSolutions.html", 0);
            // Лінійне рівняння:
            Console.WriteLine("    Файл: LinearEquation.xml");
            Console.WriteLine(Str.ToString(quadratic.ReadFromXML("LinearEquation.xml").Solve(0)));
            Report.GenerateReport(quadratic, "Linear.html", 0);
            // Немає коренів:
            Console.WriteLine("    Файл: LinearEquationNoSolutions.xml");
            Console.WriteLine(Str.ToString(quadratic.ReadFromXML("LinearEquationNoSolutions.xml").Solve(0)));
            Report.GenerateReport(quadratic, "NoSolutions.html", 0);
            // Безліч коренів:
            quadratic.C = 0;
            Console.WriteLine("    Файл: Infinity.xml");
            Console.WriteLine(Str.ToString(quadratic.Solve(0)));
            quadratic.WriteToXML("Infinity.xml");
            Report.GenerateReport(quadratic, "Infinity.html", 0);
            // Створюємо рівняння "з нуля":
            quadratic.ClearEquation();
            quadratic.Data.FFunction.AddA(1, 0);
            quadratic.Data.FFunction.AddA(0, 1);
            quadratic.Data.GFunction.AddXY(1, 2);
            quadratic.C = 1;
            Console.WriteLine("Нове рівняння");
            Console.WriteLine(Str.ToString(quadratic.Solve(0)));
        }
    }
}

Вносимо зміни до методу Main():

static void Main()
{
    Console.OutputEncoding = System.Text.Encoding.UTF8;
    TestEquation();
    TestFunctions();
    TestEquationWithXML();
}

Повний результат роботи консольного застосунку буде таким:

==========Тестування рівняння в найпростішому варіанті:==========
------Загальний випадок (x^2 + x - 2)------
 Quadratic: t = 0       Корені: X1 = -2 X2 = 1

------Загальний випадок (x^2 + 2x + 1)------
 Quadratic: t = 2       Корені: X1 = -1 X2 = -1

------Немає розв'язків (x^2 + x + 10)------
 Quadratic: t = 1       Немає коренів

------Лінійне рівняння (один корінь)-----
 Quadratic: t = 0       Корінь: 0.5

------Лінійне рівняння (немає коренів)-----
 Quadratic: t = 0       Немає коренів

------Лінійне рівняння (безмежна кількість коренів)-----
 Quadratic: t = 0       Безліч коренів


=====Тестування функцій із використанням списків:=====
*********** QuadraticLib.FFunction ***********
t = -5           F(t) = 20
t = -4           F(t) = 19
t = -3           F(t) = 18
t = -2           F(t) = 17
t = -1           F(t) = 16
t = 0            F(t) = 15
t = 1            F(t) = 14
t = 2            F(t) = 13
t = 3            F(t) = 12
t = 4            F(t) = 11
t = 5            F(t) = 10
t = 6            F(t) = 9
t = 7            F(t) = 8
t = 8            F(t) = 7
t = 9            F(t) = 6
t = 10           F(t) = 5
t = 11           F(t) = 4
t = 12           F(t) = 3
t = 13           F(t) = 2
t = 14           F(t) = 1
t = 15           F(t) = 0
*********** QuadraticLib.GFunction ***********
t = -5           G(t) = 39
t = -4           G(t) = 40
t = -3           G(t) = 41
t = -2           G(t) = 42
t = -1           G(t) = 43
t = 0            G(t) = 44
t = 1            G(t) = 45
t = 2            G(t) = 46
t = 3            G(t) = 47
t = 4            G(t) = 48
t = 5            G(t) = 49
t = 6            G(t) = 50
t = 7            G(t) = 51
t = 8            G(t) = 52
t = 9            G(t) = 53
t = 10           G(t) = 54
t = 11           G(t) = 55
t = 12           G(t) = 56
t = 13           G(t) = 57
t = 14           G(t) = 58
t = 15           G(t) = 59

==Тестування рівняння, дані про яке зберігаються в XML-файлах:===
QuadraticLib.XMLQuadratic
    Файл: EquationCommon.xml
Рівняння не було розв'язане
t = 0   Корені: X1 = -1 X2 = 2
t = -5  Корені: X1 = -0.2637626158259733 X2 = 1.2637626158259734
t = -10 Корені: X1 = -0.15712874067277094 X2 = 1.157128740672771
    Файл: EquationNoSolutions.xml
t = 0   Немає коренів
    Файл: LinearEquation.xml
t = 0   Корінь: 6
    Файл: LinearEquationNoSolutions.xml
t = 0   Немає коренів
    Файл: Infinity.xml
t = 0   Безліч коренів
Нове рівняння
t = 0   Немає коренів

Наступний етап роботи (створення GUI-застосунку) може обумовити деякі зміни в створених раніше класах.

2.3 Створення застосунку графічного інтерфейсу користувача

Наступний етап реалізації – створення застосунку графічного інтерфейсу користувача. Засоби бібліотеки WPF дозволяють дотримуватися вимог метапатерну проєктування Model-View-Controller (MVC). Зокрема, класи, реалізовані в бібліотеці QuadraticLib, визначають модель предметної області. Модель фактично ніяк не залежить від способу взаємодії з користувачем. Можна запропонувати різні варіанти представлення (View) з відповідними контролерами. Це можуть бути, наприклад, засоби Windows Forms, Web-застосунок тощо.

Кожен з класів контролерів містить оброблювачі подій, пов'язаних з роботою візуальних елементів інтерфейсу користувача. Реалізація контролера може бути достатньо складною, оскільки саме контролер відповідатиме за створення необхідних об'єктів та має бути обізнаним про розташування всх необхідних методів і властивостей у різних об'єктах моделі. Для того, щоб не відтворювати в різних контролерах складну взаємодію з об'єктами моделі, до неї доцільно додати окремий клас – так званий фасад. Реалізація відповідного патерну передбачає, що створюється єдиний об'єкт, який повинен бути посередником між засобами інтерфейсу користувача й об'єктами моделі. В нашому випадку до бібліотеки QuadraticLib додаємо клас QuadraticFacade:

// QuadraticFacade.cs
namespace QuadraticLib
{
    /// <summary>
    /// Реалізація патерну "Фасад" для створення програм 
    /// графічного інтерфейсу користувача
    /// </summary>
    public class QuadraticFacade
    {
        // Реалізація патерну "Singleton"
        private static QuadraticFacade? instance = null;
        private readonly XMLQuadratic xmlQuadratic = new();

        private QuadraticFacade()
        {
        }

        /// <summary>
        /// Фабричний метод для створення об'єкта
        /// </summary>
        /// <returns>новий об'єкт-фасад</returns>
        public static QuadraticFacade GetInstance()
        {
            if (instance == null)
            {
                instance = new QuadraticFacade();
            }
            return instance;
        }

        /// <summary>
        /// Перевіряє, чи було розв'язане рівняння 
        /// </summary>
        public bool Solved { get; private set; }

        /// <summary>
        /// Список коефіцієнтів функції F
        /// </summary>
        public List<ACoef> ACoefs 
        { 
            get => xmlQuadratic.Data.FFunction.ACoefs; 
            set => xmlQuadratic.Data.FFunction.ACoefs = value;
        }

        /// <summary>
        /// Список пар функції G
        /// </summary>
        public List<XYCoef> XYCoefs 
        { 
            get => xmlQuadratic.Data.GFunction.XYCoefs;
            set => xmlQuadratic.Data.GFunction.XYCoefs = value;
        }

        /// <summary>
        /// коефіцієнт c
        /// </summary>
        public double C 
        { 
            get => xmlQuadratic.C;
            set 
            {
                if (value != C)
                {
                    xmlQuadratic.C = value;
                    Solved = false;
                }
            }
        }

        /// <summary>
        /// Останнє значення параметра t
        /// </summary>
        public double T
        {
            get => xmlQuadratic.T;
            set
            {
                if (value != T)
                {
                    xmlQuadratic.T = value;
                    Solved = false;
                }
            }
        }

        /// <summary>
        /// Рядок з результатами розв'язання рівняння
        /// </summary>
        public string Results 
        { 
            get => Str.ToString(xmlQuadratic);
        }

        /// <summary>
        /// Поліном другого степеня, для якого розв'язується рівняння
        /// </summary>
        /// <param name="x">поточне значення x</param>
        /// <returns></returns>
        public double Parabola(double x)
        {
            return xmlQuadratic.F(T) * x * x + xmlQuadratic.G(T) * x + C;
        }

        /// <summary>
        /// Очищує дані та переводить рівняння у необхідний стан
        /// </summary>
        public void DoNew()
        {
            xmlQuadratic.ClearEquation();
            xmlQuadratic.State = EquationState.NotSolved;
            Solved = false;
        }

        /// <summary>
        /// Здійснює читання з XML-документу
        /// </summary>
        /// <param name="fileName">ім'я файлу</param>
        /// <returns>поточний об'єкт зі зміненим станом</returns>
        public void ReadFromXML(string fileName)
        {
            xmlQuadratic.ReadFromXML(fileName);
            Solved = false;
        }

        /// <summary>
        /// Здійснює запис у XML-документ
        /// </summary>
        /// <param name="fileName">ім'я файлу</param>
        public void WriteToXML(string fileName)
        {
            xmlQuadratic.WriteToXML(fileName);
        }

        /// <summary>
        /// Додає новий елемент до списку
        /// </summary>
        public void AddNewA()
        {
            xmlQuadratic.Data.FFunction.AddA(0, xmlQuadratic.Data.FFunction.ACount);
            Solved = false;
        }

        /// <summary>
        /// Видаляє останній елемент зі списку
        /// </summary>
        public void RemoveLastA()
        {
            if (xmlQuadratic.Data.FFunction.ACount > 0)
            {
                xmlQuadratic.Data.FFunction.RemoveLastA();
                Solved = false;
            }
        }

        /// <summary>
        /// Додає новий елемент до списку пар
        /// </summary>
        public void AddNewPair()
        {
            xmlQuadratic.Data.GFunction.AddXY(0, 0);
            Solved = false;
        }

        /// <summary>
        /// Видаляє останній елемент зі списку пар
        /// </summary>
        public void RemoveLastPair()
        {
            if (xmlQuadratic.Data.GFunction.PairsCount > 0)
            {
                xmlQuadratic.Data.GFunction.RemoveLastPair();
                Solved = false;
            }
        }

        public void Solve()
        {
            xmlQuadratic.Solve(T);
            Solved = true;
        }

        /// <summary>
        /// Генерує звіт про роботу програми в форматі HTML
        /// </summary>
        /// <param name="fileName">iм\'я файлу</param>
        public void GenerateReport(string fileName)
        {
            Report.GenerateReport(xmlQuadratic, fileName, T);
        }

        // <returns>набір мінімальних і максимальних значень </returns>
        /// <summary>
        /// Обчислює границі для потенційного малювання графіку
        /// </summary>
        /// <param name="xMargin">додатковий допуск вздовж X</param>
        /// <param name="yMargin">додатковий допуск вздовж Y</param>
        /// <returns></returns>
        public (double xMin, double xMax, double yMin, double yMax) Ranges(double xMargin, double yMargin)
        {
            xmlQuadratic.Solve(T);
            double xMin = xmlQuadratic.Roots.Count > 0 ? xmlQuadratic.Roots.Min() - xMargin : -xMargin;
            double xMax = xmlQuadratic.Roots.Count > 0 ? xmlQuadratic.Roots.Max() + xMargin : xMargin;
            double yFrom = Parabola(xMin);
            double yTo = Parabola(xMax);
            double yMin = Math.Min(Math.Min(yFrom, yTo), 0) - yMargin;
            double yMax = Math.Max(Math.Max(yFrom, yTo), 0) + yMargin;
            return (xMin, xMax, yMin, yMax);
        }
    }
}

Далі до раніше створеного рішення (Solution) слід додати новий проект – застосунок WPF. У вікні Solution Explorer слід вибрати Quadratic та у контекстному меню виконати функцію Add | New Project ..., далі обрати WPF Application з назвою QuadraticWpfApp. Після натиснення кнопки Create на екрані з'являється порожнє вікно з заголовком MainWindow. Файл MainWindow.xaml містить опис елементів вікна у форматі XML.

Спочатку змінюємо заголовок вікна (Title) на "Квадратне рівняння". До автоматично створеної сітки (Grid) додаємо елемент Menu – майбутнє головне меню застосунку.

Після головно меню доцільно розташувати ще одну вкладену сітку (Grid), До неї додаємо мітки з вмістом "Функція F(t)" і "Функція G(t)", під ними – дві таблиці даних (DataGrid) яким доцільно призначити імена (x:Name) – DataGridF і DataGridG відповідно. Кожна таблиця міститиме по дві колонки (DataGridTextColumn), яким теж доцільно вказати імена. Нижче таблиць розташовуємо контейнер Canvas фіксованих розмірів, на якому будуть розташовані необхідні кнопки (Button) та області введення (TextBox). Областям введення доцільно визначити змістовні імена.

В правій частині вікна розташовуємо ще один іменований контейнер Canvas, призначений для відображення графіку функції.

Після здійснення необхідних налаштувань файл MainWindow.xaml матиме такий вигляд:

<Window x:Class="QuadraticWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:QuadraticWpfApp"
        mc:Ignorable="d"
        Title="Квадратне рівняння" Height="600" Width="800" MinHeight="360" MinWidth="377">
    <Grid>
        <Menu VerticalAlignment="Top">
            <MenuItem Header="File">
                <MenuItem Header="Нові дані" />
                <MenuItem Header="Відкрити" />
                <MenuItem Header="Зберегти як..." />
                <Separator/>
                <MenuItem Header="Вихід" />
            </MenuItem>
            <MenuItem Header="Робота">
                <MenuItem Header="Розв'язати рівняння" />
                <MenuItem Header="Згенерувати звіт" />
            </MenuItem>
            <MenuItem Header="Допомога">
                <MenuItem Header="Про програму..." />
            </MenuItem>
        </Menu>
        <Grid Width="360" HorizontalAlignment="Left" Margin="0,20,0,0">
            <Grid Margin="0,0,0,260">
                <Label Content="Функція F(t)" Margin="10,0,0,0" />
                <Label Content="Функція G(t)" Margin="185,0,0,0" />
                <DataGrid x:Name="DataGridF" Margin="10,25,185,10" AutoGenerateColumns="False" CanUserAddRows="False" >
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Індекс" Width="70" x:Name="ColumnIndex" />
                        <DataGridTextColumn Header="A" Width="70" x:Name="ColumnA" />
                    </DataGrid.Columns>
                </DataGrid>
                <DataGrid x:Name="DataGridG" Margin="185,25,10,10" AutoGenerateColumns="False" CanUserAddRows="False" >
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="X" Width="70" x:Name="ColumnX" />
                        <DataGridTextColumn Header="Y" Width="70" x:Name="ColumnY" />
                    </DataGrid.Columns>
                </DataGrid>
            </Grid>
            <Canvas Height="270" VerticalAlignment="Bottom" >
                <Button Content="+" Canvas.Left="60" Canvas.Top="5" Width="30" />
                <Button Content="-" Canvas.Left="95" Canvas.Top="5" Width="30" />
                <Button Content="+" Canvas.Left="235" Canvas.Top="5" Width="30" />
                <Button Content="-" Canvas.Left="270" Canvas.Top="5" Width="30" />
                <Label Content="C" Canvas.Left="5" Canvas.Top="25" />

                <TextBox x:Name="TextBoxC" Canvas.Left="25" Text="0" Canvas.Top="30" Width="150" Height="20" />
                <Label Content="t" Canvas.Left="180" Canvas.Top="25" />
                <TextBox x:Name="TextBoxT" Canvas.Left="200" Text="0" Canvas.Top="30" Width="150" Height="20" />
                <Button Content="Розв'язати рівняння" Height="40" Canvas.Left="10" Canvas.Top="60" Width="340" />
                <Button Content="Згенерувати звіт" Height="40" Canvas.Left="10" Canvas.Top="110" Width="340" />
                <TextBox x:Name="TextBoxResults" Height="100" Canvas.Left="10" Width="340" Canvas.Top="160" IsReadOnly="True" />
            </Canvas>
        </Grid>
        <Canvas x:Name="CanvasGraph" Margin="360,18,0,0" />
    </Grid>
</Window>

Після першого запуску програми головне вікно виглядатиме так:

Тепер до контролера (клас MainWindow) додаємо оброблювачі подій. Окрім подій, пов'язаних з меню та кнопками, для текстових елементів введення даних доцільно створити оброблювачів подї TextChanged, а для CanvasGraphSizeChanged. Події можна додавати автоматично через вікно властивостей. Файл MainWindow.xaml після додавання оброблювачів подій матиме такий вигляд:

<Window x:Class="QuadraticWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:QuadraticWpfApp"
        mc:Ignorable="d"
        Title="Квадратне рівняння" Height="600" Width="800" MinHeight="360" MinWidth="377">
    <Grid>
        <Menu VerticalAlignment="Top">
            <MenuItem Header="File">
                <MenuItem Header="Нові дані" Click="MenuItemNew_Click"/>
                <MenuItem Header="Відкрити" Click="MenuItemOpen_Click"/>
                <MenuItem Header="Зберегти як..." Click="MenuItemSave_Click"/>
                <Separator/>
                <MenuItem Header="Вихід" Click="MenuItemExit_Click"/>
            </MenuItem>
            <MenuItem Header="Робота">
                <MenuItem Header="Розв'язати рівняння" Click="Solve_Click"/>
                <MenuItem Header="Згенерувати звіт" Click="Report_Click"/>
            </MenuItem>
            <MenuItem Header="Допомога">
                <MenuItem Header="Про програму..." Click="MenuItemAbout_Click"/>
            </MenuItem>
        </Menu>
        <Grid Width="360" HorizontalAlignment="Left" Margin="0,20,0,0">
            <Grid Margin="0,0,0,260">
                <Label Content="Функція F(t)" Margin="10,0,0,0" />
                <Label Content="Функція G(t)" Margin="185,0,0,0" />
                <DataGrid x:Name="DataGridF" Margin="10,25,185,10" AutoGenerateColumns="False" CanUserAddRows="False" >
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Індекс" Width="70" x:Name="ColumnIndex" />
                        <DataGridTextColumn Header="A" Width="70" x:Name="ColumnA" />
                    </DataGrid.Columns>
                </DataGrid>
                <DataGrid x:Name="DataGridG" Margin="185,25,10,10" AutoGenerateColumns="False" CanUserAddRows="False" >
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="X" Width="70" x:Name="ColumnX" />
                        <DataGridTextColumn Header="Y" Width="70" x:Name="ColumnY" />
                    </DataGrid.Columns>
                </DataGrid>
            </Grid>
            <Canvas Height="270" VerticalAlignment="Bottom" >
                <Button Content="+" Canvas.Left="60" Canvas.Top="5" Width="30" Click="ButtonAddF_Click" />
                <Button Content="-" Canvas.Left="95" Canvas.Top="5" Width="30" Click="ButtonRemoveF_Click" />
                <Button Content="+" Canvas.Left="235" Canvas.Top="5" Width="30" Click="ButtonAddG_Click" />
                <Button Content="-" Canvas.Left="270" Canvas.Top="5" Width="30" Click="ButtonRemoveG_Click" />
                <Label Content="C" Canvas.Left="5" Canvas.Top="25" />

                <TextBox x:Name="TextBoxC" Canvas.Left="25" Text="0" Canvas.Top="30" Width="150" Height="20" TextChanged="TextBoxC_TextChanged"/>
                <Label Content="t" Canvas.Left="180" Canvas.Top="25" />
                <TextBox x:Name="TextBoxT" Canvas.Left="200" Text="0" Canvas.Top="30" Width="150" Height="20" TextChanged="TextBoxT_TextChanged"/>
                <Button Content="Розв'язати рівняння" Height="40" Canvas.Left="10" Canvas.Top="60" Width="340" Click="Solve_Click" />
                <Button Content="Згенерувати звіт" Height="40" Canvas.Left="10" Canvas.Top="110" Width="340" Click="Report_Click" />
                <TextBox x:Name="TextBoxResults" Height="100" Canvas.Left="10" Width="340" Canvas.Top="160" IsReadOnly="True" />
            </Canvas>
        </Grid>
        <Canvas x:Name="CanvasGraph" Margin="360,18,0,0" SizeChanged="CanvasGraph_SizeChanged"/>
    </Grid>
</Window>

Для побудови графіку можна створити окремий клас:

// GraphBuilder.cs
using QuadraticLib;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace QuadraticWpfApp
{
    /// <summary>
    /// Засоби малювання графіку функції
    /// </summary>
    public class GraphBuilder
    {
        private Canvas canvasGraph = new(); // Полотно, на якому здійснюється малювання

        private double width;  // ширина області відображання
        private double height; // висота області відображення
        private double xScale; // масштаб (кількість точок на одиницю) вздовж осі x
        private double yScale; // масштаб (кількість точок на одиницю) вздовж осі y
        private double x0;     // координата x початку координат
        private double y0;     // координата y початку координат

        /// <summary>
        /// Малює графік з осями та сіткою
        /// </summary>
        /// <param name="canvasGraph">полотно, на якому здійснюється малювання</param>
        /// <param name="func">делегат, який представляє функцію</param>
        /// <param name="xMin">мінімальне значення x</param>
        /// <param name="xMax">максимальне значення x</param>
        /// <param name="yMin">мінімальне значення y</param>
        /// <param name="yMax">максимальне значення y</param>
        public void DrawGraph(Canvas canvasGraph, DFunction func,
                              double xMin = -5, double xMax = 5, double yMin = -5, double yMax = 5)
        {
            this.canvasGraph = canvasGraph;
            width = this.canvasGraph.ActualWidth;
            height = this.canvasGraph.ActualHeight;
            xScale = width / (xMax - xMin);
            yScale = height / (yMax - yMin);
            x0 = -xMin * xScale;
            y0 = yMax * yScale;

            this.canvasGraph.Children.Clear();
            DrawXGrid(xMin, xMax);
            DrawYGrid(yMin, yMax);
            DrawAxes();
            DrawFunc(Brushes.Red, func);
        }

        /// <summary>
        /// Малює відрізок прямої лінії
        /// </summary>
        /// <param name="stroke">тип малювання й колір відрізку</param>
        /// <param name="x1">координата X початку відрізку</param>
        /// <param name="y1">координата Y початку відрізку</param>
        /// <param name="x2">координата X кінця відрізку</param>
        /// <param name="y2">координата Y кінця відрізку</param>
        private void AddLine(Brush stroke, double x1, double y1, double x2, double y2)
        {
            canvasGraph.Children.Add(new Line() { X1 = x1, X2 = x2, Y1 = y1, Y2 = y2, Stroke = stroke });
        }

        /// <summary>
        /// Додавання тексту
        /// </summary>
        /// <param name="text">текст, який додається</param>
        /// <param name="x">координата X тексту</param>
        /// <param name="y">координата Y тексту</param>
        private void AddText(string text, double x, double y)
        {
            TextBlock textBlock = new();
            textBlock.Text = text;
            textBlock.Foreground = Brushes.Black;
            // Визначення координат блоку. "Приєднані" властивості 
            Canvas.SetLeft(textBlock, x);
            Canvas.SetTop(textBlock, y);
            canvasGraph.Children.Add(textBlock);
        }

        /// <summary>
        /// Додає вісі
        /// </summary>
        void DrawAxes()
        {
            AddLine(Brushes.Black, x0, 0, x0, height);
            AddLine(Brushes.Black, 0, y0, width, y0);
            AddText("0", x0 + 2, y0 + 2);
            AddText("X", width - 10, y0 - 14);
            AddText("Y", x0 - 10, 2);
        }

        /// <summary>
        /// Безпосередньо малює графік функції вказаним кольором
        /// </summary>
        /// <param name="solidColor">пензель зі вказаним кольором</param>
        /// <param name="func">делегат, який представляє функцію</param>
        void DrawFunc(SolidColorBrush solidColor, DFunction func)
        {
            Polyline polyline = new() { Stroke = solidColor, ClipToBounds = true };
            for (int x = 0; x < width; x++)
            {
                double dy = func((x - x0) / xScale);
                if (double.IsNaN(dy) || double.IsInfinity(dy))
                {
                    continue;
                }
                // Отримали "нормальне" число
                polyline.Points.Add(new Point(x, y0 - dy * yScale));
            }
            canvasGraph.Children.Add(polyline);
        }

        /// <summary>
        /// Додає сітку вздовж осі X
        /// </summary>
        /// <param name="xMin">мінімальне значення x</param>
        /// <param name="xMax">максимальне значення x</param>
        private void DrawXGrid(double xMin, double xMax)
        {
            double xStep = 1; // Крок сітки
            while (xStep * xScale < 25)
            {
                xStep *= 10;
            }
            while (xStep * xScale > 250)
            {
                xStep /= 10;
            }
            for (double dx = xStep; dx < xMax; dx += xStep)
            {
                double x = x0 + dx * xScale;
                AddLine(Brushes.LightGray, x, 0, x, height);
                AddText(string.Format("{0:0.###}", dx), x + 2, y0 + 2);
            }
            for (double dx = -xStep; dx >= xMin; dx -= xStep)
            {
                double x = x0 + dx * xScale;
                AddLine(Brushes.LightGray, x, 0, x, height);
                AddText(string.Format("{0:0.###}", dx), x + 2, y0 + 2);
            }
        }

        /// <summary>
        /// Додає сітку вздовж осі Y
        /// </summary>
        /// <param name="yMin">мінімальне значення y</param>
        /// <param name="yMax">максимальне значення y</param>
        private void DrawYGrid(double yMin, double yMax)
        {
            double yStep = 1;  // Крок сітки
            while (yStep * yScale < 20)
            {
                yStep *= 10;
            }
            while (yStep * yScale > 200)
            {
                yStep /= 10;
            }
            for (double dy = yStep; dy < yMax; dy += yStep)
            {
                double y = y0 - dy * yScale;
                AddLine(Brushes.LightGray, 0, y, width, y);
                AddText(string.Format("{0:0.###}", dy), x0 + 2, y - 2);
            }
            for (double dy = -yStep; dy > yMin; dy -= yStep)
            {
                double y = y0 - dy * yScale;
                AddLine(Brushes.LightGray, 0, y, width, y);
                AddText(string.Format("{0:0.###}", dy), x0 + 2, y - 2);
            }
        }
    }
}

Доцільно додати окреме вікно для відображення звіту. Через контекстне меню проєкту додаємо нове вікно (Add | Window (WPF)...), Перейменувавши його у WindowShowReport. До головної сітки (Grid) додаємо два рівні (RowDefinition). До верхнього рівня додаємо компонент WebBrowser з ім'ям ReportViewer. До нижнього рівня додаємо дві кнопки для зберігання звіту й закриття вікна. Файл WindowShowReport.xaml матиме такий вміст:

<Window x:Class="QuadraticWpfApp.WindowShowReport"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:QuadraticWpfApp"
        mc:Ignorable="d"
        Title="Звіт з роботи програми" Height="350" Width="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <!-- Нижня панель для кнопок -->
        </Grid.RowDefinitions>
        <WebBrowser x:Name="ReportViewer" Grid.Row="0" />
        <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="10">
            <Button Content="Зберегти звіт" Width="85" Margin="5,0" />
            <Button Content="Закрити" Width="85" Margin="5,0" />
        </StackPanel>
    </Grid>
</Window>

У файлі MainWindow.xaml.cs до класу MainWindow додаємо поле типу QuadraticFacade та створюємо об'єкт за допомогою фабричного методу QuadraticFacade.GetInstance(). Реалізація оброблювачів подій спиратиметься на виклик методів класу-фасаду. Код файлу MainWindow.xaml.cs буде таким:

using QuadraticLib;
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace QuadraticWpfApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly QuadraticFacade facade = QuadraticFacade.GetInstance();

        public MainWindow()
        {
            InitializeComponent();
            InitTables();
        }

        private void MenuItemNew_Click(object sender, RoutedEventArgs e)
        {
            facade.DoNew();
            TextBoxResults.Clear();
            InitTables();
        }

        private void InitTables()
        {
            InitFTable();
            InitGTable();
        }

        private void InitFTable()
        {
            DataGridF.ItemsSource = null;
            // Зв'язуємо таблицю DataGridF зі списком коефіцієнтів:
            DataGridF.ItemsSource = facade.ACoefs;

            // Вказуємо, які колонки зв'язані з якими властивостями:
            ColumnIndex.Binding = new Binding("Index");
            ColumnA.Binding = new Binding("Value");
            DrawGraph();
        }

        private void InitGTable()
        {
            DataGridG.ItemsSource = null;
            // Зв'язуємо таблицю DataGridG зі списком пар:
            DataGridG.ItemsSource = facade.XYCoefs;

            // Вказуємо, які колонки зв'язані з якими властивостями:
            ColumnX.Binding = new Binding("X");
            ColumnY.Binding = new Binding("Y");
            DrawGraph();
        }

        private void MenuItemOpen_Click(object sender, RoutedEventArgs e)
        {
            // Створюємо діалогове вікно та налаштовує його властивості:
            Microsoft.Win32.OpenFileDialog dlg = new();
            dlg.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory; // поточна тека
            dlg.DefaultExt = ".xml";
            dlg.Filter = "Файли XML (*.xml)|*.xml|Усі файли (*.*)|*.*";
            if (dlg.ShowDialog() == true)
            {
                try
                {
                    facade.ReadFromXML(dlg.FileName);
                    TextBoxC.Text = facade.C.ToString();
                }
                catch (Exception)
                {
                    MessageBox.Show("Помилка читання з файлу");
                }
                TextBoxResults.Clear();
                InitTables();
            }
        }

        private void MenuItemSave_Click(object sender, RoutedEventArgs e)
        {
            // Підтверджуємо зміни в таблицях:
            DataGridF.CommitEdit();
            DataGridG.CommitEdit();
            // Створюємо діалогове вікно та налаштовує його властивості:
            Microsoft.Win32.SaveFileDialog dlg = new();
            dlg.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
            dlg.DefaultExt = ".xml";
            dlg.Filter = "Файли XML (*.xml)|*.xml|Усі файли (*.*)|*.*";
            if (dlg.ShowDialog() == true)
            {
                try
                {
                    facade.WriteToXML(dlg.FileName);
                    MessageBox.Show("Файл збережено");
                }
                catch (Exception)
                {
                    MessageBox.Show("Помилка запису в файл");
                }
            }
        }

        private void MenuItemExit_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }

        private void MenuItemAbout_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Приклад курсового проекту\n\nВерсія 1.0", "Про програму");
        }

        private void ButtonAddF_Click(object sender, RoutedEventArgs e)
        {
            // Підтверджуємо зміни в таблиці:
            DataGridF.CommitEdit();
            // Додаємо новий рядок:
            facade.AddNewA();
            TextBoxResults.Clear();
            InitFTable();
        }

        private void ButtonRemoveF_Click(object sender, RoutedEventArgs e)
        {
            // Підтверджуємо зміни в таблиці:
            DataGridF.CommitEdit();
            // Видаляємо останній рядок:
            facade.RemoveLastA();
            TextBoxResults.Clear();
            InitFTable();
        }

        private void ButtonAddG_Click(object sender, RoutedEventArgs e)
        {
            // Підтверджуємо зміни в таблиці:
            DataGridG.CommitEdit();
            // Додаємо новий рядок:
            facade.AddNewPair();
            TextBoxResults.Clear();
            InitGTable();
        }

        private void ButtonRemoveG_Click(object sender, RoutedEventArgs e)
        {
            // Підтверджуємо зміни в таблиці:
            DataGridG.CommitEdit();
            // Видаляємо останній рядок:
            facade.RemoveLastPair();
            TextBoxResults.Clear();
            InitGTable();
        }

        private void Solve_Click(object sender, RoutedEventArgs e)
        {
            facade.C = double.Parse(TextBoxC.Text ?? "0");
            facade.T = double.Parse(TextBoxT.Text ?? "0");
            facade.Solve();
            string text = facade.Results;
            TextBoxResults.Text = text.Replace("\t", Environment.NewLine);
            DrawGraph();
        }

        private void TextBoxC_TextChanged(object sender, TextChangedEventArgs e)
        {
            try
            {
                facade.C = double.Parse(TextBoxC.Text);
                DrawGraph();
            }
            catch (FormatException)
            {
            }
        }

        private void TextBoxT_TextChanged(object sender, TextChangedEventArgs e)
        {
            try
            {
                facade.T = double.Parse(TextBoxT.Text);
                DrawGraph();
            }
            catch (FormatException)
            {
            }
        }

        private void Report_Click(object sender, RoutedEventArgs e)
        {
            if (!facade.Solved)
            {
                MessageBox.Show("Спочатку треба розв\'язати рівняння!");
                return;
            }
            WindowShowReport windowShowReport = new();
            // Відносний шлях до HTML-файлу
            string relativeFilePath = @"temp.html";

            // Перетворюємо відносний шлях у абсолютний
            string absoluteFilePath = Path.GetFullPath(relativeFilePath);

            // Використовуємо абсолютний шлях для навігації
            windowShowReport.ReportViewer.Navigate(new Uri(absoluteFilePath));
            //windowShowReport.Facade = facade;
            windowShowReport.ShowDialog();
        }

        // Перемалювання графіку
        private void DrawGraph()
        {
            if (TextBoxT != null && TextBoxC != null && CanvasGraph != null)
            {
                facade.T = double.Parse(TextBoxT.Text);
                facade.C = double.Parse(TextBoxC.Text);
                (double xMin, double xMax, double yMin, double yMax) = facade.Ranges(xMargin: 2, yMargin: 2);
                new GraphBuilder().DrawGraph(CanvasGraph, facade.Parabola, xMin, xMax, yMin, yMax);
            }
        }

        // Перемальовуємо графік, якщо змінюються його розміри
        private void CanvasGraph_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            DrawGraph();
        }
    }
}

До розмічення вікна WindowShowReport додаємо оброблювачі подій, пов'язаних з кнопками:

<Window x:Class="QuadraticWpfApp.WindowShowReport"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:QuadraticWpfApp"
        mc:Ignorable="d"
        Title="Звіт з роботи програми" Height="350" Width="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <!-- Нижня панель для кнопок -->
        </Grid.RowDefinitions>
        <WebBrowser x:Name="ReportViewer" Grid.Row="0" />
        <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="10">
            <Button Content="Зберегти звіт" Width="85" Margin="5,0" Click="Button_Save_Click" />
            <Button Content="Закрити" Width="85" Margin="5,0" Click="Button_Close_Click" />
        </StackPanel>

    </Grid>
</Window>

Текст файлу WindowShowReport.xaml.cs з оброблювачами подій буде таким:

using System;
using System.Windows;
using QuadraticLib;

namespace QuadraticWpfApp
{
    /// <summary>
    /// Контролер вікна WindowShowReport.xaml
    /// </summary>
    public partial class WindowShowReport : Window
    {
        private readonly QuadraticFacade facade = QuadraticFacade.GetInstance();

        public WindowShowReport()
        {
            InitializeComponent();
        }

        private void Button_Save_Click(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.SaveFileDialog dlg = new();
            dlg.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
            dlg.DefaultExt = ".xml";
            dlg.Filter = "Файли HTML (*.html)|*.html|Усі файли (*.*)|*.*";
            if (dlg.ShowDialog() == true)
            {
                try
                {
                    facade.GenerateReport(dlg.FileName);
                    MessageBox.Show("Звіт збережено");
                }
                catch (Exception)
                {
                    MessageBox.Show("Помилка запису в файл");
                }
            }
        }

        private void Button_Close_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

 

up