Лабораторна робота 4

Подійно-орієнтоване, функційне та декларативне програмування

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

За допомогою засобів Windows Presentanion Fundation (WPF) розробити програму графічного інтерфейсу користувача, яка реалізує завдання попередньої лабораторної роботи. Програма повинна реалізовувати такі функції

  • завантаження даних з обраного файлу
  • відображення даних про об'єкти у вигляді таблиці (DataGrid)
  • відображення додаткових даних в окремій таблиці
  • редагування основних даних (модифікація, додавання, видалення)
  • сортування за певними ознаками
  • вибір за певною ознакою та відображення в окремому вікні
  • зберігання у новому файлі

Програма повинна використовувати бібліотеку класів, яка була створена у попередній лабораторній роботі.

1.2 Використання типу даних dynamic

Створити клас «Простий дріб». Реалізувати перевантаження для операцій +, -, * та /, зокрема, ділення на ціле число. Створити статичний клас, який надає функції для отримання середнього арифметичного та добутку елементів масиву. Застосувати тип dynamic. Здійснити тестування функцій, використовуючи як масив елементів типу double, так і масив простих дробів.

1.3 Робота з файловою системою

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

1.4 Робота з делегатами

Реалізувати завдання 1.5 ("Корені рівняння") лабораторної роботи № 2. Застосувати підхід, побудований на використанні делегатів.

1.5 Використання лямбда-виразів та LINQ

Створити статичний клас, який містить такі функції для роботи зі списками цілих чисел (додатних та від'ємних):

  • пошук елементів, які є точними квадратами (квадрат квадратного кореня дорівнює числу)
  • сортування за зменшенням значень модулів елементів

Реалізувати два підходи – через використання лямбда-виразів та через використання LINQ.

1.6 Робота з меню та таблицями даних

За допомогою засобів WPF розробити програму, в якій користувач уводить розмірність двовимірного масиву (m та n), вводить елементи у комірки таблиці (DataGrid), або заповнює таблицю випадковими значеннями від -1000 до 1000, а потім за допомогою відповідної функції меню обчислює суму добутків рядків. Сума виводиться у за допомогою компоненту Label, або в діалоговому вікні, залежно від відповідної опції (компонент CheckBox). Реалізувати головне меню.

1.7 Використання графічних засобів (додаткове завдання)

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

2 Методичні вказівки

2.1 Тип dynamic

Контекстне ключове слово dynamic було введено в C# 4.0. Спочатку тип dynamic був розроблений для роботи з COM-об'єктами. Тип dynamic дозволяє створювати змінні, типи яких можна визначити під час виконання, на відміну від ключового слова var, яке дозволяє ідентифікувати тип під час компіляції. Отже, змінна може змінити свій фактичний тип:

dynamic x = 1;
Console.WriteLine(x.GetType().Name); // Int32
x = "A";
Console.WriteLine(x.GetType().Name); // String
x = 2.5;
Console.WriteLine(x.GetType().Name); // Double

Примітка: метод GetType() повертає об'єкт класу System.Type, який інкапсулює інформацію про тип змінної, зокрема її ім'я (властивість Name).

Наведений нижче приклад показує реалізацію парадигми узагальненого програмування на основі типу dynamic. Метод Sum() можна застосувати до аргументів різних типів (int, double, string), оскільки ці типи дозволяють застосовувати оператор +:

class Program
{
    static dynamic Sum(dynamic x, dynamic y)
    {
        return x + y;
    }

    static void Main(string[] args)
    {
        int i1 = 1;
        int i2 = 2;
        Console.WriteLine(Sum(i1, i2)); // 3
        double d1 = 1.1; 
        double d2 = 2.2;
        Console.WriteLine(Sum(d1, d2)); // 3.3
        string s1 = "dot";
        string s2 = "net";
        Console.WriteLine(Sum(s1, s2)); // dotnet
    }
}

Ми також можемо створити клас (наприклад, Complex), який перевантажує операцію +. Об'єкти цього класу також можуть бути використані як фактичні аргументи:

public class Complex
{
    public double A { get; set; }
    public double B { get; set; }
    public static Complex operator+(Complex c1, Complex c2)
    {
        return new Complex { A = c1.A + c2.A, B = c1.B + c2.B };
    }
    public override string ToString()
    {
        return A + " " + B + "i";
    }
}

. . .

Complex c1 = new Complex { A = 1, B = 2 };
Complex c2 = new Complex { A = 3, B = 4 };
Console.WriteLine(Sum(c1, c2));

Ми можемо спробувати надіслати аргументи типів, які не підтримують операцію +. У цьому випадку компілятор не відображає повідомлення про помилки:

object o1 = new object();
object o2 = new object();
Console.WriteLine(Sum(o1, o2));

Але під час виконання ми отримаємо виняток:

Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 
Operator '+' cannot be applied to operands of type 'object' and 'object'

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

Можна створити масив елементів типу dynamic. Насправді елементи можуть бути різних типів:

dynamic[] arr = new dynamic[3];
arr[0] = 1;
arr[1] = 1.2;
arr[2] = "a";
//arr[2] = new object();
dynamic sum = 0;
foreach (dynamic d in arr)
{
    sum += d;
}
Console.WriteLine(sum); // 2.2a

2.2 Неповні класи та методи

У версії 2.0 мови С# з'явилися так звані неповні (часткові) типи (partial types) – розділення реалізації класу на декілька частин. Наприклад:

partial class Partial
{
    public int x1 = 1;
}

class Program
{
    static void Main(string[] args)
    {
        Partial p = new Partial();
        Console.WriteLine(p.x1 + p.x2); // 3
    }
}

partial class Partial
{
    public int x2 = 2;
}

Найчастіше окремі частини класу можуть міститись у кількох різних файлах. Це дозволяє автоматично генерувати деякі частини класу. Тепер автоматична генерація та ручна розробка можуть здійснюватись у різних файлах. Однак у випадку розташування класу в декількох файлах не можна бути впевненим, що ніде не буде додана якась частина класу, яка порушить його нормальне функціонування.

Починаючи з версії 3.0, мова C# підтримує так звані часткові методи, які пов'язані з частковими класами. Частковий метод – це метод, оголошений в одній частині та реалізований в іншій частині класу. Оголошення часткового методу аналогічне оголошенню абстрактних методів, але замість слова abstract використовується partial:

  partial class SomeClass
  {
      partial void SomeFunc(int k);
      public void Caller()
      {
          SomeFunc(0);
      }
  }

  // Інша частина класу, можливо в іншому файлі:
  partial class SomeClass
  {
      partial void SomeFunc(int k)
      {
          // реалізація
      }

  }

Використання часткових методів дозволяє створювати різні реалізації залежно від контексту використання класу. Оскільки реалізація методу може бути взагалі відсутня, такий метод може мати тип результату тільки void та не містини параметрів з атрибутом out. Часткові методи завжди закриті.

2.3 Робота з файловою системою

Простір імен System.IO надає можливість роботи не тільки з вмістом файлів, а також з файловою системою в цілому. Можливість роботи з теками реалізує клас DirectoryInfo. Для створення об'єкта цього класу в якості параметра конструктора слід визначити повний або відносний шлях до файлу. Наприклад:

DirectoryInfo dir = new DirectoryInfo("C:\\Users");
DirectoryInfo currentDir = new DirectoryInfo("."); // Тека проекту (поточна)    

Клас DirectoryInfo містить методи створення нової теки (Create()), видалення теки (Delete()), отримання масиву файлів визначеної теки (GetFiles()), отримання масиву підкаталогів (GetDirectories()), а також властивості для роботи з атрибутами (Attributes), перевірки існування теки (Exists), отримання повного шляху до теки (FullName) тощо.

Робота з окремими файлами здійснюється через клас FileInfo. Його об'єкт можна створити аналогічно. За допомогою функції CreateText() можна створити новий текстовий файл.

FileInfo file = new FileInfo("New.txt");
file.CreateText();    

Існують також функції Create() та Delete(). Властивість Directory повертає теку, в якій знаходиться файл. Властивості Attributes, Exists та FullName аналогічні DirectoryInfo. Роботу деяких з цих методів та властивостей можна продемонструвати на прикладі:

using System;
using System.IO;

namespace FourthLab
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Уведiть iм\'я теки, яку ви хочете створити:");
            string dirName = Console.ReadLine();
            DirectoryInfo dir = new DirectoryInfo(dirName);
            // Створюємо нову теку:
            dir.Create();
            // Створюємо новий файл всередині нової теки: 
            FileInfo file = new FileInfo(dir + "\\temp.txt");
            file.Create();
            // Показуємо список файлів теки:
            FileInfo[] files = dir.GetFiles();
            foreach (FileInfo f in files)
            {
                Console.WriteLine(f);
            }
        }
    }
}

2.4 Делегати

Невід'ємною частиною практики сучасного програмування стало використання зворотних викликів (callback) і повідомлень (notifications). Для реалізації зворотних викликів і повідомлень у мовах C і C++ використовують вказівники на функції. Такий підхід несе в собі деякі недоліки, тому що не надає типової захищеності, що може привести до появи помилок, які не виявляються на стадії компіляції.

У С# є синтаксична конструкція – делегати (delegates). Їхнє призначення аналогічне вказівникам на функції в C++, але делегати є керованими об'єктами і прив'язані до типів. Середовище виконання гарантує, що делегат указує на припустимий об'єкт. У С# є дві основні області застосування делегатів:

  • методи зворотного виклику
  • оброблювачі подій.

Делегати дозволяють викликати методи, вибір яких відбувається під час виконання програми. Делегати – це типи-посилання і є нащадками стандартного типу System.MulticastDelegate. Делегат – це клас, що містить дані про сигнатуру методу. Екземпляр делегата (delegate instance) – об'єкт, що дозволяє прив'язатися до конкретного методу, що відповідає визначеній сигнатурі. У сигнатуру методу входить тип значення, що повертається, і список аргументів.

Нижче приведений приклад оголошення делегата:

delegate string MyDelegate(int x);

Делегат може бути оголошений як у просторі імен, так і в класі. Для виклику використовується звичайний синтаксис виклику методу, тільки як ім'я методу використовується ім'я екземпляра делегата. Для ініціалізації делегата методом потрібно передати ім'я цього методу в конструктор делегата під час його створення:

тип-делегата ім'я = new тип-делегата (метод);

Це може бути метод будь-якого класу, за умови, що параметри методу і параметри делегата збігаються за типами.

Застосування делегатів для зворотного виклику можна продемонструвати на прикладі створення класу Solver для розв'язання методом дихотомії будь-якого рівняння. Делегати дозволяють здійснити узагальнений опис функції, яка визначає ліву частину рівняння:

using System;

namespace DelegatesTest
{
    public class Solver
    {
        public delegate double LeftSide(double x);

        public static double Solve(double a, double b, double eps, LeftSide f)
        {
            double x = (a + b) / 2;
            while (Math.Abs(b - a) > eps)
            {
                if (f(a) * f(x) > 0)
                {
                    a = x;
                }
                else
                {
                    b = x;
                }
                x = (a + b) / 2;
            }
            return x;
        }
    }
}

В іншому класі здійснюється розв'язання певного рівняння.

using System;

namespace DelegatesTest
{
    class Program
    {
        public static double F(double x)
        {
            return x * x - 2;
        }

        static void Main(string[] args)
        {
            Solver.LeftSide ls = new Solver.LeftSide(F);
            Console.WriteLine(Solver.Solve(0, 2, 0.000001, ls));
        }
    }
}

Можна обійтися без окремого створення екземпляра делегату. Такий екземпляр буде створено автоматично:

using System;

namespace DelegatesTest
{
    class Program
    {
        public static double F(double x)
        {
            return x * x - 2;
        }

        static void Main(string[] args)
        {
            Console.WriteLine(Solver.Solve(0, 2, 0.000001, F));
        }
    }
}

У версії 2.0 C# уводиться поняття безіменних методів. Такі методи створюють безпосередньо в місці, де потрібен екземпляр делегату. Для створення безіменного методу використовують таку конструкцію:

delegate(відповідний_список_параметрів) { тіло_функції }

Попередній приклад програми, яка використовує делегат, можна буде ще скоротити:

using System;

namespace DelegatesTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Solver.Solve(0, 2, 0.000001, 
                              delegate(double x) { return x * x - 2; } ));
        }
    }
}

Делегати підтримують композицію. Кілька делегатів, які посилаються на різні методи, можна об'єднати в один, а потім можна виконати послідовно. Можна додавати методи до ланцюжка виконання за допомогою операторів "+" або "+=". Оператор "-" дозволяє видалити певний метод з ланцюжка.

2.5 Події

Реалізація застосунків графічного інтерфейсу користувача базується на механізмі отримання та обробки подій. Уся програма складається з ініціалізації (реєстрації візуальних елементів управління) та основного циклу отримання та обробки подій. Події – це переміщення або натискання кнопок миші, клавіатурне введення, тощо. Кожний зареєстрований візуальний елемент управління може отримувати події, які до нього стосуються, та виконувати функції обробки цих подій. Робота з подіями в С# відповідає моделі "видавець – передплатник", де клас публікує подію, яку він може ініціювати, і будь-які класи можуть підписатися на цю подію. При ініціації події середовище стежить за тим, щоб повідомити всіх передплатників про виникнення події.

Метод, який викликається після виникнення події, визначається делегатом. Потрібно, щоб такий делегат приймав два аргументи. Ці аргументи повинні представляти два об'єкти: той, що ініціював подію (видавець), і інформаційний об'єкт події, що повинний бути похідним від класу EventArgs .NET.

Спеціальне ключове слово event використовують для опису елементів класу типу відповідного делегату.

Події допомагають нам розділити функціональність системи на незалежні частини. Припустимо, що якийсь клас дозволяє отримати кілька цілих додатних чисел у межах заданого діапазону. Інший клас застосовує дві дії до кожного числа: він перевіряє, чи це ціле число є простим числом і чи ділиться воно на 7:

using System;

namespace FourthLab
{
    // Делегат для представлення події:
    public delegate void NumberEvent(int n);

    public class NumbersChecker
    {
        // Поле типу делегату:
        public event NumberEvent numberEvent;

        // Метод, який ініціює подію для кожного випадкового числа:
        public void GetNumbers(int min, int max, int count)
        {
            Random rand = new Random();
            for (int i = 0; i < count; i++)
            {
                int n = rand.Next();
                n %= (max - min + 1);
                n += min;
                numberEvent(n);
            }
        }
    }

    class Program
    {
        // Оброблювач події:
        static void PrintIfPrime(int n)
        {
            bool isPrime = true;
            for (int k = 2; k * k <= n; k++)
            {
                if (n % k == 0)
                {
                    isPrime = false;
                    break;
                }
            }
            if (isPrime)
            {
                Console.WriteLine("Просте число: {0}", n);
            }
        }

        // Інший оброблювач події:
        static void IsDividedIntoSeven(int n)
        {
            if (n % 7 == 0)
            {
                Console.WriteLine("{0} ділиться на 7", n);
            }
        }

        static void Main(string[] args)
        {
            NumbersChecker nc = new NumbersChecker();
            nc.numberEvent += PrintIfPrime;
            nc.numberEvent += IsDividedIntoSeven;
            nc.GetNumbers(min: 10, max: 100, count: 40);
        }
    }
}

2.6 Концепції функційного та декларативного програмування

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

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

Рекурсія – це механізм виклику функції, що здійснюється з тіла цієї функції (прямо чи опосередковано).

Такі мови, як LISP, XQuery, F #, можна класифікувати як мови функційного програмування.

Декларативне програмування – це парадигма, за допомогою якої описують результати, які слід отримати замість того, щоб описувати послідовність дій, що ведуть до цього результату. Прикладами декларативних мов є HTML, SQL (мови, призначені для спеціальної предметної області), а також і мова логічного програмування Prolog. Велика кількість декларативних мов базується на XML.

Останні версії мови C# (починаючи з 3.0) насправді є гібридними, оскільки вони підтримують не тільки імперативне, об'єктно-орієнтоване та загальне програмування, але також функційне та декларативне програмування.

2.7 Реалізація мовою C# функційного та декларативного програмування

2.7.1 Лямбда-вирази

Функційне програмування в C# реалізоване за допомогою так званих лямбда-виразів. Лямбда-вирази є формою представлення безіменних методів. Як раніше вже було зазначено, мова C# дозволяє створювати безіменні методи, які реалізують певний делегат. Наприклад, для того, щоб зі списку відселектувати елементи за певною ознакою можна викликати метод FindAll(), який у якості параметра вимагає функцію-предикат (повертає true або false, залежно від того, чи виконується вимога селекції). Метод FindAll() повертає новий список. Можна запропонувати таку реалізацію, яка базується на створенні безіменного методу. Фактично використання лямбда-виразів реалізує замикання:

var a = new List<int> { 1, 3, 6, 7, 8, 9 };
// Отримуємо список парних елементів:
var b = a.FindAll(delegate(int i) { return i % 2 == 0; });

Нова конструкція мови C# (лямбда-вираз) дозволяє реалізувати такий пошук у більш компактний спосіб:

var b = a.FindAll(i => i % 2 == 0);

У наведеному нижче прикладі обчислюється кількість парних елементів списку:

int k = a.FindAll(delegate(int i) { return i % 2 == 0; }).Count;

У наступному прикладі за допомогою методу ConvertAll() реалізується заміна елементів:

var a = new List<int>(){ 1, 2, 3 };
var x = 3;
var b = a.ConvertAll(elem => elem * x); // замикання
foreach (var k in b)
{
    Console.WriteLine(k); // 3, 6, 9
}

Всередині лямбда-виразу можна виділити список параметрів (до "=>") та твердження – тіло функції (після "=>"). Якщо параметрів – більше ніж один, слід вживати дужки. Можна також додати складений оператор { } у твердженні.

У новітніх версіях C# (починаючи з 7.0) синтаксис лямбда-виразів можна застосовувати для визначення методів – конструкторів, деструкторів, перевизначених віртуальних методів, а також всередині опису властивостей та індексаторів, якщо відповідний програмний блок можна реалізувати як один вираз. Наприклад:

class ExpressionBodiedTest
{
    private int a;
    // Конструктор:
    public ExpressionBodiedTest(int a) => this.a = a;
    // Перевизначений метод:
    public override string ToString() => a + "";
    // Властивість:
    public int A
    {
        get => a;
        set => a = value;
    }
}

2.7.2 LINQ

Технологія LINQ (Language Integrated Query) визначає набір операторів для створення запитів, які транслюються у послідовний виклик спеціальних методів. З точки зору користувача LINQ надає можливості включення у програмний код тверджень, аналогічних SQL-запитам. Наприклад, задачу пошуку парних елементів можна було б розв'язати у такий спосіб:

var a = new List<int> { 1, 3, 6, 7, 8, 9 };
var с = from i in a where i % 2 == 0 select i;

У наведеному прикладі можна вказати правило заповнення результуючої послідовності. Наприклад, замість елементів у нову послідовність можна записувати їхні квадрати:

var с = from i in a where i % 2 == 0 select i * i;

Результатом запиту є послідовність спеціального типу, яка реалізує інтерфейс IEnumerable<>. До результату можна застосовувати конструкцію foreach:

foreach (int x in с)
{
    Console.Write(x + " ");
}

Інші можливі оператори LINQ – join, on, equals, into, orderby, ascending, descending, group.

Використання LINQ найбільш ефективне у застосунках баз даних.

2.8 Розробка GUI-застосунків за допомогою технології Windows Presentation Foundation

2.8.1 Основні концепції

Починаючи з версії .NET Framework 3.0, для створення desktop-застосунків запропонована нова графічна (презентаційна) підсистема – Windows Presentation Foundation (WPF). Це – сучасна альтернатива попереднім засобам розробки GUI-застосунків, зокрема, бібліотеці Windows.Forms. Головна концепція WPF полягає у відокремленні представлення зовнішнього вигляду програми графічного інтерфейсу користувача від програмного коду. Основними рисами WPF є такі:

  • декларативний підхід до опису елементів графічного інтерфейсу
  • використання векторної графіки для малювання компонентів
  • використання стилів та шаблонів оформлення графічного інтерфейсу користувача

На рівні безпосереднього графічного відображення WPF базується на використанні DirectX.

2.8.2 Мова XAML

Мова XAML (eXtensible Application Markup Language) – це побудована на XML мова розмічування, яка призначена для декларативного опису застосунків. Найбільш ефективне застосування XAML – опис компонентів інтерфейсу користувача, визначення властивостей цих компонентів та зв'язування компонентів з оброблювачами подій. За допомогою XAML можна описати всі візуальні об'єкти WPF. Так, наприклад, створюється головне вікно, всередині якого міститься контейнер (Grid):

<Window x:Class="FirstXAML.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:FirstXAML"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
 
    </Grid>
</Window>

Слід пам'ятати, що всередині більшості компонентів, які не є контейнерами, можна розмістити лише один компонент. Тому всередині звичайних компонентів розміщують контейнери (панелі), а до них вже додають необхідні елементи. У наведеному нижче прикладі створюється кнопка, яка додається до панелі (Grid)

<Grid>
    <Button Height="72" Width="160" Content="Click Me" Margin="138,60,0,0" Name="ButtonClickMe"/>
</Grid>

Властивість Margin визначає розміщення елементу відносно контейнеру. Визначаються відповідно відступи відповідно від лівого, верхнього, правого та нижнього боків контейнеру. Властивість Content – в найпростішому випадку це текст всередині кнопки, мітки або іншого аналогічного компоненту.

Під час проектування застосунку WPF у середовищі Visual Studio XAML-код генерується автоматично під час додавання компонентів до форми. Налагодження властивостей можна здійснювати як у вікні Properties, так і безпосередньо в XAML-коді.

2.8.3 Компонування

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

У WPF компонування здійснюється з використанням різних контейнерів. Вікно у WPF може містити тільки один елемент – контейнер. У контейнер, в свою чергу, можна помістити різні елементи користувацького інтерфейсу, а також інші контейнери. Компонування в WPF визначається типом контейнера. Контейнери компонування WPF – це панелі, які визначають логіку розташування вкладених елементів інтерфейсу користувача. Класи контейнерів походять від абстрактного класу System.Windows.Controls.Panel. Для компонування в застосунках використовують такі класи:

  • Grid розміщує елементи в рядки і колонки відповідно до невидимої таблиці; Grid розподіляє елементи по сітці невидимих рядків і стовпців; в одну комірку сітки доцільно поміщати один елемент, який за необхідності може бути сам іншим контейнером компонування, в якому можна створити власну групу елементів керування;
  • UniformGrid, на відміну від Grid, вимагає встановлення тільки кількості рядків і стовпців і формує комірки однакового розміру, які займають весь доступний простір вікна (сторінки) або елемента зовнішнього контейнера;
  • StackPanel – розміщує елементи в горизонтальні і вертикальні стопки; цей контейнер часто використовується для організації невеликих ділянок більш великого і складного вікна;
  • WrapPanel – розміщує елементи управління в доступному просторі, по одному рядку або колонці;
  • DockPanel – розміщує елементи управління щодо одного зі своїх зовнішніх країв;
  • Frame – аналогічний StackPanel, але є кращим способом упаковки вмісту для переходів на сторінки.

Grid є найбільш потужним контейнером в WPF. Більша частина всього, що можна зробити за допомогою інших контейнерів компонування, можна виконати в Grid. Grid є ідеальним інструментом для поділу вікна (сторінки) на більш дрібні області, якими можна буде керувати за допомогою інших панелей. Такий розподіл на дрібні області здійснюється за допомогою визначень колонок та рядків:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="240*" />
        <ColumnDefinition Width="263*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="140*" />
        <RowDefinition Height="171*" />
    </Grid.RowDefinitions>
</Grid>

WrapPanel залежно від властивості Orientation впорядковує елементи управління горизонтально (Horizontal) або вертикально (Vertical), заповнюючи поточний розмір панелі. При горизонтальному розташуванні елементи управління можуть переноситися на наступний рядок, а при вертикальному – на наступний стовпець.

Панель DockPanel здійснює стикування елементів управління до однієї зі своїх сторін залежно від значення властивості Dock, яке може приймати значення Left, Right, Top або Bottom. Так якщо елемент керування був пристикований до верхньої частини DockPanel, то він розтягується і буде займати всю ширину панелі, приймаючи таку висоту, яка визначена параметром MaxHeight.

Frame є елементом керування вмістом, який надає можливість переходу до вмісту і його відображення. Frame можна розмістити всередині іншого вмісту, як і інші елементи управління і елементи. Вміст може бути будь-яким типом об'єкту .NET Framework і файлів HTML.

2.8.4 Основні керуючі елементи WPF та їхнє використання

Бібліотека візуальних компонентів WPF надає типові елементи керування – Button, Label, TextBox, ComboBox, RadioButton тощо. Відмінності стосуються в першу чергу властивостей та їхнього застосування. У кнопках та мітках замість Text використовують Content.

Елемент керування TextBox зазвичай зберігає один рядок тексту. Якщо необхідно створити багаторядкове представлення, то властивості TextWrapping необхідно присвоїти значення Wrap. Для багаторядкового елемента TextBox можна задати мінімальну і максимальну кількість рядків, використовуючи властивості MinLines і MaxLines. Для переходу на новий рядок всередині тексту можна використовувати символ нового рядку ('\n').

Для роботи зі списками використовують елементи ListBox та ComboBox. Елемент керування ListBox зберігає кожен вкладений об'єкт у своїй колекції. При цьому ListBoxItem може зберігати не тільки рядки, але і будь-який довільний елемент. Елемент керування ComboBox подібний ListBox, але для візуалізації використовує випадаючий список і користувач може вибрати тільки один елемент зі списку.

WPF надає елементи керування, що використовують діапазони значень. Це ScrollBar, ProgressBar і Slider. Для них визначені такі властивості як Value (поточне значення елемента керування), Minimum і Maximum (мінімальне і максимальне допустимі значення). ScrollBar – це елемент управління, який надає смугу прокрутки з прямокутником, позиція якого відповідає певному значенню. Елемент ProgressBar показує хід виконання тривалого завдання. Елемент управління Slider використовують для визначення числового значення шляхом переміщення бігунка на лінійці прокрутки.

WPF також дозволяє працювати з випадаючими та контекстними меню. Особливості роботи з меню будуть розглянуті далі в прикладах програм.

2.8.5 Зв'язування даних у WPF

Для відображення даних у WPF застосовується технологія зв'язування даних. Для компонентів, обізнаних у даних, (наприклад, DataGrid) можна визначити джерело даних – ItemsSource. Це можуть бути, наприклад списки, або деякі інші колекції. Крім того, для зв'язування даних використовують рядки Binding – прив'язку між значенням властивості об'єкта і значенням властивості елемента керування. Зв'язування даних можна здійснювати у XAML-коді. Наприклад, зв'язуємо колонку таблиці з властивістю Name:

 <DataGridTextColumn Header="Назва" Binding="{Binding Name}" />

Зв'язування даних можна також здійснювати в коді мовою C#.

2.8.6 Розробка WPF-застосунку в середовищі Visual Studio

Для того, щоб створити новий WPF-застосунок у середовищі Visual Studio, слід створити новий проект, обравши шаблон WPF App (.NET) у вікні шаблонів New Project. Доцільно змінити ім'я проекту (WpfApp1) на змістовне. Припустимо, вибрано ім'я FirstWpf.

Далі можна спостерігати вікно, поділене на дві частини. У верхній частині міститься дизайнер форми з головним вікном майбутнього застосунку. В нижній частині міститься підвікно редагування коду мовою XAML. Цей код зберігається у файлі з розширенням .xaml (усталено MainWindow.xaml) та описує головне вікно та його елементи. Текст буде таким:

<Window x:Class="FirstWpf.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:FirstWpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        
    </Grid>
</Window>

Одночасно автоматично генерується вихідний код мовою C# (файл MainWindow.xaml.cs):

using System;
using System.Collections.Generic;
using System.Linq; 
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace FirstWpf
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

Нові елементи графічного інтерфейсу користувача можна додавати візуальними засобами шляхом перетягання відповідних елементів з підвікна Toolbox, або шляхом редагування XAML-коду. Наприклад, якщо до вікна додати кнопку (Button) з підвікна Toolbox, вона буде розташована на місці, на який указує курсор миші. Наприклад, якщо це були координати 320, 200 відносно верхнього лівого кута форми, то одночасно з появою кнопки на формі до XAML-коду проміж тегами <Grid> та </Grid> буде додано такий рядок (для зручності в нашому прикладі він поділений на два рядки):

<Button Content="Button" HorizontalAlignment="Left" 
        Margin="320,200,0,0" VerticalAlignment="Top"/>

Якщо ми, наприклад хочемо розташувати кнопку посередині форми та змінити текст, це можна зробити за допомогою підвікна Properties шляхом редагування відповідних властивостей, а також через безпосереднє редагування XAML-коду. Наприклад, рядок між тегами <Grid> та </Grid> можна змінити на такий.

<Button Content="Натисни мене" HorizontalAlignment="Left" 
        Margin="320,200,0,0" VerticalAlignment="Top"/>

Тепер кнопка з текстом "Натисни мене" розташована посередині форми. Інтерактивними засобами можна змінити розміри кнопки. В цьому випадку до опису кнопки будуть додані нові властивості:

<Button Content="Натисни мене" ... Height="50" Width="120"/>

Заголовок вікна можна також змінити у два способи – через підвікно Properties, або у XAML-коді (властивість Title).

Тепер можна додати функцію – оброблювач події, пов'язаної з натисненням кнопки. Це можна зробити, наприклад, подвійним клацанням миші на кнопці. До коду класу MainWindow у файлі MainWindow.xaml.cs буде додано новий метод:

...
public partial class MainWindow : Window
{
    ...

    private void Button_Click(object sender, RoutedEventArgs e)
    {

    }
}

Одночасно до опису кнопки в XAML-файлі буде додано посилання на цей метод:

<Button Content="Натисни мене" ... Click="Button_Click" />

Вказані дії можна здійснити вручну без допомоги візуальних засобів Visual Studio.

Тепер всередині тіла методу Button_Click() можна додати необхідний код, наприклад:

private void Button_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Дякую!"); // додаємо цей рядок
}    

Після завантаження програми на виконання відповідне діалогове вікно з текстом "Дякую!" з'явиться всередині екрану.

2.8.7 Використання графічних засобів WPF

Від початку, WPF була анонсована як графічна підсистема. На відміну від попередніх бібліотек, ідеологія малювання побудована не на виклику графічних функцій, а на додаванні графічних примітивів як об'єктів. Для того, щоб розташувати графічні об'єкти, слід використовувати контейнер Canvas (полотно). Ім’я контейнера має бути встановлено у canvas. Необхідне перемалювання здійснюватиметься автоматично. Наприклад, якщо створити новий проект GraphDemo, у вікні розмістити контейнер Canvas та визначити для нього оброблювач події Loaded, отримаємо такий XAML-код:

<<Window x:Class="GraphDemo.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:GraphDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="450">
    <Canvas Name="canvas" Loaded="Canvas_Loaded">

    </Canvas>
</Window>

Примітка: розміри головного вікна також були трохи змінені.

Код файлу MainWindow.xaml.cs з реалізацією оброблювача події Loaded матиме такий вигляд:

using System;
using System.Collections.Generic;
using System.Linq; 
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace GraphDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Canvas_Loaded(object sender, RoutedEventArgs e)
        {
            canvas.Children.Add(new Ellipse() 
            { 
                Width = 150, 
                Height = 100, 
                Margin = new Thickness(100, 100, 0, 0),
                Fill = Brushes.Blue
            }); 
            canvas.Children.Add(new Rectangle()
            {
                Width = 150,
                Height = 100,
                Margin = new Thickness(200, 150, 0, 0),
                Fill = Brushes.Red
            }); 
        }
    }
}

Графічні примітиви можуть бути не тільки зображені програмно, але й додані візуально або через редактор XAML-коду. Наприклад, замість створення програмного коду можна додати відповідні елементи до XAML-коду на налаштувати їхні властивості:

<Window x:Class="GraphDemo.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:GraphDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="450">
    <Canvas Name="canvas">
        <Ellipse Canvas.Left="100" Canvas.Top="100" Height="100" Width="150" Fill="Blue" />
        <Rectangle Canvas.Left="200" Canvas.Top="150" Height="100" Width="150" Fill="Red" />
    </Canvas>
</Window>

Для визначення розташування фігур у наведеному прикладі використані так звані приєднані властивості (Canvas.Left та Canvas.Top).

Другий варіант не потребує реалізації жодної події. В обох випадках ми отримаємо такий результат після запуску програми:

3 Приклади програм

3.1 Обчислення сум елементів масивів різних типів

Припустимо, необхідно створити функцію для обчислення суми елементів масивів різних типів. Можна побудувати таку функцію із застосуванням типу dynamic. Програма буде такою:

namespace DynamicSum
{
    class Program
    {
        public static dynamic Sum(dynamic arr)
        {
            dynamic result = arr[0];
            for (int i = 1; i < arr.Length; i++)
            {
                result += arr[i];
            }
            return result;
        }

        static void Main(string[] args)
        {
            int[] ints = { 1, 2, 3 };
            Console.WriteLine(Sum(ints));
            string[] strings = { "a", "b", "c" };
            Console.WriteLine(Sum(strings));
        }
    }
}

Результат роботи програми буде таким:

6
abc

Як видно з наведеного коду, суму можна знаходити для всіх об'єктів, для яких визначено операцію +.

3.2 Робота з файловою системою

Припустимо, в усіх підкаталогах, починаючи з визначеного, необхідно знайти та вивести на екран дані про всі файли, які мають розширення "txt". Окрім імені, необхідно виводити розмір файлу.

Обхід підкаталогів – дуже поширена задача, пов'язана з файловою системою. Тому доцільно створити універсальну функцію обходу, яка обходить підкаталоги та застосовує конкретні дії, якщо був знайдений необхідний файл. Механізм подій дозволяє нам додавати необхідні дії пізніше.

Програма матиме такий вигляд:

using System;
using System.IO;

namespace FileSystemDemo
{
    public delegate void FileEvent(FileInfo fi);
    public class FileWalker
    {
        public event FileEvent FoundEvent;

        public void WalkFiles(DirectoryInfo dir)
        {
            FileInfo[] files = dir.GetFiles();
            foreach (FileInfo f in files)
            {
                FoundEvent(f);
            }
            DirectoryInfo[] dirs = dir.GetDirectories();
            foreach (DirectoryInfo d in dirs)
            {
                WalkFiles(d);
            }
        }
    }

    class Program
    {
        // Оброблювач події:
        static void TextFileFound(FileInfo fi)
        {
            if (fi.Extension.Equals(".txt"))
            {
                Console.WriteLine(fi.FullName + "\t" + fi.Length);
            }
        }

        static void Main(string[] args)
        {
            Console.Write("Уведiть iм\'я теки: ");
            string dirName = Console.ReadLine();
            DirectoryInfo dir = new DirectoryInfo(dirName);
            FileWalker walker = new FileWalker();
            walker.FoundEvent += TextFileFound;
            walker.WalkFiles(dir);
        }
    }
}

Ми можемо додати кілька оброблювачів подій. Наприклад, інший оброблювач може зберігати інформацію про файл у системному журналі.

3.3 Використання лямбда-виразів та LINQ

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

  • пошук елементів, квадрати яких знаходяться у певному діапазоні
  • сортування за зменшенням значень синусів елементів

Необхідно реалізувати два підходи – через використання лямбда-виразів та через використання LINQ.

Для зручності розширимо можливості класу List<double>. Сирцевий код першого варіанту матиме такий вигляд:

using System;
using System.Collections.Generic;

namespace LambdaTest
{
    public static class ListsWithLambda
    {
        public static IEnumerable<double> SquaresInRange(this List<double> list, double qFrom, double qTo)
        {
            return list.FindAll(x => x * x > qFrom&& x * x <= qTo);
        }

        public static void SortBySines(this List<double> list)
        {
            // Мінус забезпечує сортування за зменшенням:
            list.Sort((d1, d2) => -Math.Sin(d1).CompareTo(Math.Sin(d2))); 
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<double> list = new List<double> { 1, -2, 4, 3 };
            foreach (double x in list.SquaresInRange(3, 10))
            {
                Console.Write(x + " "); // -2 3
            }
            Console.WriteLine();
            list.SortBySines();
            foreach (double x in list)
            {
                Console.WriteLine(x + "\t" + Math.Sin(x));
            }
        }
    }
}

Другий варіант:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LINQ_Test
{
    public static class ListsWithLINQ
    {
        public static IEnumerable<double> SquaresInRange(this List<double> list, double qFrom, double qTo)
        {
            return from x in list 
                   where x * x > qFrom && x * x <= qTo 
                   select x;
        }

        public static List<double> SortBySines(this List<double> list)
        {
            return new List<double>(from x in list
                                    orderby Math.Sin(x) descending 
                                    select x); 
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<double> list = new List<double> { 1, -2, 4, 3 };
            foreach (double x in list.SquaresInRange(3, 10))
            {
                Console.Write(x + " "); // -2 3
            }
            Console.WriteLine();
            list = list.SortBySines();
            foreach (double x in list)
            {
                Console.WriteLine(x + "\t" + Math.Sin(x)); 
            }
        }
    }
}

Примітка: Класи ListsWithLambda та ListsWithLINQ не можна розміщувати в одному просторі, оскільки це призведе до конфліктів імен.

3.4 Робота з меню та таблицями даних

Припустимо, нам необхідно створити WPF-застосунок, у якому користувач уводить розмірність двовимірного масиву (n на n), вводить або заповнює випадковими значеннями елементи у комірки таблиці (DataGrid) та за допомогою відповідної функції меню здійснює транспонування матриці, або обчислює слід матриці (сума діагональних елементів). Сума виводиться у за допомогою компоненту TextBox, або в діалоговому вікні, залежно від вибору користувача (компонент CheckBox).

Вибір певної функції здійснюватиметься за допомогою компоненту RadioButton.

Створюємо новий WPF-застосунок з ім'ям WpfMatrixApp. Текст заголовку головного вікна (Title) змінюємо на "Робота з квадратною матрицею". Головну сітку (Grid) слід розділити на чотири горизонтальних частини. Це можна зробити шляхом клацання мишею на формі ліворуч від сітки на необхідному рівні. В XAML-коді з'являються визначення рівнів (RowDefinition), наприклад:

<Grid.RowDefinitions>
    <RowDefinition Height="22*" />
    <RowDefinition Height="40*" />
    <RowDefinition Height="246*" />
    <RowDefinition Height="43*" />
</Grid.RowDefinitions>

Для того, щоб перша, друга та остання частини сітки мали фіксовану висоту, а не підладжувалися до необхідного значення залежно від фактичних розмірів вікна, слід видалити зірочки після значення Height:

<Grid.RowDefinitions>
    <RowDefinition Height="22" />
    <RowDefinition Height="40" />
    <RowDefinition Height="246*" />
    <RowDefinition Height="43" />
</Grid.RowDefinitions>

Далі до верхньої частини додаємо елемент Menu – майбутнє головне меню застосунку. Для нього слід встановити ім'я MainMenu, а також скинути значення властивостей Height, HorizontalAlignment, Margin, VerticalAlignment та Width в усталені значення. Це можна зробити через контекстне меню властивостей (Reset Value), або шляхом видалення відповідних атрибутів зі XAML-коду. Отримаємо такий XAML-код:

<Menu>

У вікні властивостей (Properties) елемента Menu можна знайти властивість Items. Обравши кнопку з трьома точками в рядку цієї властивості, розкриваємо вікно Collection Editor: Items. У цьому вікні додаємо позиції меню (кнопка Add) та налагоджуємо їхні властивості. Зокрема, Header – це текст позиції меню. Можна також додавати підменю через аналогічну властивість Items. Окрім позицій меню (MenuItem) можна додати розділювач (Separator) Додаємо підменю "Файл" з позиціями "Новий" та "Вихід", "Робота" з позиціями "Заповнити випадковими значеннями" та "Виконати", а також підменю "Довідка" з позицією "Про програму". Опис меню в редакторі XAML-коду матиме такий вигляд:

<Menu>
    <MenuItem Header="Файл">
        <MenuItem Header="Новий"/>
        <Separator />
        <MenuItem Header="Вийти"/>
    </MenuItem>
    <MenuItem Header="Робота">
        <MenuItem Header="Заповнити випадковими значеннями"/>
        <MenuItem Header="Виконати"/>
    </MenuItem>
    <MenuItem Header="Довідка">
        <MenuItem Header="Про програму..."/>
    </MenuItem>
</Menu>    

Розташована нижче горизонтальна частина головної панелі вікна міститиме іншу панель. Для всіх властивостей групи "Layout" слід встановити усталені значення. До панелі додаємо елемент ComboBoxN типу ComboBox та дві кнопки типу RadioButtonRadioButtonTranspose та RadioButtonTrace з текстом (Content) відповідно "Транспонувати" та "Знайти слід". Для ComboBoxN встановлюємо значення властивостей Text ("2") та SelectedIndex ("0"). Для RadioButtonTranspose властивості IsChecked встановлюємо значення true.

Наступна горизонтальна частина міститиме таблицю даних (DataGrid) з ім'ям DataGridA. Вона повинна займати всю третю частину панелі, тому їй треба також скинути значення всіх властивостей групи "Layout".

Остання частина міститиме панель GridBottom з елементами CheckBoxWindow типу CheckBox та TextBoxTrace типу TextBox. Останньому компоненту слід встановити значення властивості IsReadOnly у true.

Після всіх налаштувань отримаємо такий XAML-код:

<Window x:Class="WpfMatrixApp.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:WpfMatrixApp"
        mc:Ignorable="d"
        Title="Робота з квадратною матрицею" Height="390" Width="420">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="22" />
            <RowDefinition Height="40" />
            <RowDefinition Height="246*" />
            <RowDefinition Height="43" />
        </Grid.RowDefinitions>
        <Menu>
            <MenuItem Header="Файл">
                <MenuItem Header="Новий"/>
                <Separator />
                <MenuItem Header="Вийти"/>
            </MenuItem>
            <MenuItem Header="Робота">
                <MenuItem Header="Заповнити випадковими значеннями"/>
                <MenuItem Header="Виконати"/>
            </MenuItem>
            <MenuItem Header="Довідка">
                <MenuItem Header="Про програму..."/>
            </MenuItem>
        </Menu>
        <Grid Grid.Row="1" >
            <ComboBox Height="23" Name="ComboBoxN" Margin="19,6,0,11" Width="70"
                      HorizontalAlignment="Left" Text="2" SelectedIndex="0"/>
            <RadioButton Content="Транспонувати" HorizontalAlignment="Left" Margin="116,11,0,0" 
                         Name="RadioButtonTranspose" IsChecked="True" />
            <RadioButton Content="Знайти слід" HorizontalAlignment="Left" Margin="234,11,0,0" 
                         Name="RadioButtonTrace" />
        </Grid>
        <DataGrid AutoGenerateColumns="True" Grid.Row="2" ColumnWidth="50" 
                  CanUserResizeRows="False" Name="DataGridA"/>
        <Grid Grid.Row="3">
            <CheckBox Content="Виводити результат у вікно" Height="16" HorizontalAlignment="Left" 
                      Margin="163,16,0,10" Name="CheckBoxWindow" VerticalAlignment="Center" />
            <TextBox Height="23" HorizontalAlignment="Left" Margin="19,10,0,0" 
                     Name="TextBoxTrace" VerticalAlignment="Top" Width="120" IsReadOnly="True" />
        </Grid>
    </Grid>
</Window>

Примітка: імена компонентів (властивість Name) доцільно визначати, коли передбачається звернення до цих компонентів у програмному коді; в нашому випадку це компоненти ComboBox, RadioButton, DataGrid, CheckBox і TextBox.

Програмна реалізація передбачає відображення у таблиці квадратної матриці. Один з підходів до створення такої матриці – застосування класу DataTable простору імен System.Data. Його властивість DefaultView дозволяє зв'язувати компонент DataGrid з таблицею даних. Для того, щоб записати дані в комірку таблиці (в рядок з номером i та колонку з номером j), достатньо такого присвоєння:

data.Rows[row][index] = value;

Для того, щоб прочитати значення, необхідне перетворення на рядок:

double y = double.Parse(data.Rows[row][index] + "");

Для того, щоб зручно працювати з двовимірними масивами, можна створити клас-оболонку, робота з яким здійснюватиметься як з двовимірним невирівняним масивом, але фактично дані зберігатимуться в DataGrid. Такий клас може стати до пригоди в різних програмах, пов'язаних з відображенням двовимірних масивів, тому його доцільно визначити в окремій бібліотеці класів (Class Library (.NET Core)). Додаємо до рішення таку бібліотеку з ім'ям DataArrays. Файл DataArray.cs цієї бібліотеки матиме такий вміст:

using System.Data;

namespace DataArrays
{
    /// <summary>
    /// Клас для представлення двовимірного масиву. Значення елементів 
    /// містяться в об'єкті System.Data.DataTable
    /// </summary>
    public class DataArray
    {
        /// <summary>
        /// Допоміжний клас, який представляє окремий рядок масиву
        /// </summary>
        public class DataArrayRow
        {
            private readonly DataTable data;  // посилання на DataTable
            private readonly int row;         // індекс рядку масиву

            /// <summary>
            /// Конструктор допоміжного класу
            /// </summary>
            /// <param name="data">посилання на DataTable</param>
            /// <param name="row">індекс рядку масиву</param>
            public DataArrayRow(DataTable data, int row)
            {
                this.row = row;
                this.data = data;
            }

            /// <summary>
            /// Індексатор для доступу до елементу рядка
            /// </summary>
            /// <param name="index">індекс елемента</param>
            /// <returns>елемент масиву</returns>
            public double this[int index]
            {
                get => double.Parse(data.Rows[row][index] + "");
                set => data.Rows[row][index] = value;
            }
        }

        // Об'єкт, у якому зберігаються дані
        private readonly DataTable data = new();

        /// <summary>
        /// Кількість рядків масиву
        /// </summary>
        public int M { get; }

        /// <summary>
        /// Кількість стовпців масиву
        /// </summary>
        public int N { get; }

        /// <summary>
        /// Об'єкт, у якому зберігаються дані
        /// </summary>
        public DataTable Data
        {
            get => data;
        }

        /// <summary>
        /// Індексатор для доступу до рядка
        /// </summary>
        /// <param name="index">індекс рядка</param>
        /// <returns>рядок</returns>
        public DataArrayRow this[int index]
        {
            get => new(data, index);
        }

        /// <summary>
        /// Конструктор, у якому налаштовується об'єкт DataTable
        /// </summary>
        /// <param name="m">кількість рядків</param>
        /// <param name="n">кількість стовпців</param>
        public DataArray(int m, int n)
        {
            M = m;
            N = n;
            // Додаємо колонки з назвою (номер):
            for (int j = 1; j <= N; j++)
            {
                data.Columns.Add(j + "");
            }
            // Додаємо рядки:
            for (int i = 1; i <= M; i++)
            {
                data.Rows.Add();
            }
        }
    }
}

Примітка: наведений код вимагає використання платформи .NET 5; відповідні налаштування слід зробити у властивостях проекту (Project | DataArrays properties, далі Target framework: .NET 5.0).

Тепер повертаємося до проекту WpfMatrixApp. Сстворюємо оброблювачі подій:

  • Для функцій меню (оброблювачі події Click):
    • для позиції з текстом "Новий": New_Click
    • для позиції з текстом "Вийти": Exit_Click
    • для позиції з текстом "Заповнити випадковими значеннями": Random_Click
    • для позиції з текстом "Виконати": Calc_Click
    • для позиції з текстом "Про програму...": About_Click
  • Для компоненту ComboBox оброблювач події SelectionChanged: ComboBoxN_SelectionChanged

Примітка: імена оброблювачів подій слід уводити у вікні Properties проти відповідних подій.

Після додавання подій отримаємо такий XAML-код:

<Window x:Class="WpfMatrixApp.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:WpfMatrixApp"
        mc:Ignorable="d"
        Title="Робота з квадратною матрицею" Height="390" Width="420">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="22" />
            <RowDefinition Height="40" />
            <RowDefinition Height="246*" />
            <RowDefinition Height="43" />
        </Grid.RowDefinitions>
        <Menu>
            <MenuItem Header="Файл">
                <MenuItem Header="Новий" Click="New_Click"/>
                <Separator />
                <MenuItem Header="Вийти" Click="Exit_Click"/>
            </MenuItem>
            <MenuItem Header="Робота">
                <MenuItem Header="Заповнити випадковими значеннями" Click="Random_Click"/>
                <MenuItem Header="Виконати" Click="Calc_Click"/>
            </MenuItem>
            <MenuItem Header="Довідка">
                <MenuItem Header="Про програму..." Click="About_Click"/>
            </MenuItem>
        </Menu>
        <Grid Grid.Row="1" >
            <ComboBox Height="23" Name="ComboBoxN" Margin="19,6,0,11" Width="70" SelectedIndex="0" 
                      HorizontalAlignment="Left" Text="2" SelectionChanged="ComboBoxN_SelectionChanged" />
            <RadioButton Content="Транспонувати" HorizontalAlignment="Left" Margin="116,11,0,0" 
                         Name="RadioButtonTranspose" IsChecked="True" />
            <RadioButton Content="Знайти слід" HorizontalAlignment="Left" Margin="234,11,0,0" 
                         Name="RadioButtonTrace" />
        </Grid>
        <DataGrid AutoGenerateColumns="True" Grid.Row="2" ColumnWidth="50" 
                  CanUserResizeRows="False" Name="DataGridA"/>
        <Grid Grid.Row="3">
            <CheckBox Content="Виводити результат у вікно" Height="16" HorizontalAlignment="Left" 
                      Margin="163,16,0,10" Name="CheckBoxWindow" VerticalAlignment="Center" />
            <TextBox Height="23" HorizontalAlignment="Left" Margin="19,10,0,0" 
                     Name="TextBoxTrace" VerticalAlignment="Top" Width="120" IsReadOnly="True" />
        </Grid>
    </Grid>
</Window>

Сирцевий код MainWindow.xaml.cs матиме такий вигляд:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfMatrixApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void New_Click(object sender, RoutedEventArgs e)
        {

        }

        private void Exit_Click(object sender, RoutedEventArgs e)
        {

        }

        private void Random_Click(object sender, RoutedEventArgs e)
        {

        }

        private void Calc_Click(object sender, RoutedEventArgs e)
        {

        }

        private void About_Click(object sender, RoutedEventArgs e)
        {

        }

        private void ComboBoxN_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {

        }
    }
}

Після того, як до проекту додане посилання (Add | Project Reference...) на DataArrays, створюємо метод InitTable(), в якому створюється та заповнюється нулями масив визначеного розміру, в конструкторі налаштовуємо ComboBoxN та викликаємо функцію InitTable(). Сирцевий код MainWindow.xaml.cs з реалізацєю оброблювачів виглядатиме так:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using DataArrays;

namespace WpfMatrixApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private DataArray a;

        public MainWindow()
        {
            InitializeComponent();
            for (int i = 2; i <= 10; i++)
            {
                ComboBoxN.Items.Add(i);
            }
            InitTable(2);
        }
        
        private void InitTable(int n)
        {
            a = new DataArray(n, n);
            DataGridA.ItemsSource = a.Data.DefaultView;
            for (int i = 0; i < a.M; i++)
            {
                for (int j = 0; j < a.N; j++)
                {
                    a[i][j] = 0;
                }
            }
            DataGridA.CanUserAddRows = false;
        }

        private void New_Click(object sender, RoutedEventArgs e)
        {
            ComboBoxN.SelectedIndex = 0;
            InitTable(2);
            RadioButtonTranspose.IsChecked = true;
            CheckBoxWindow.IsChecked = false;
            TextBoxTrace.Text = "";
        }

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

        private void Random_Click(object sender, RoutedEventArgs e)
        {
            // Ініціалізація генератора випадкових значень 
            // кількістю мілісекунд поточного часу.
            // Без ініціалізації числа завжди будуть повторюватися
            Random rand = new(DateTime.Now.Millisecond);
            // Заповнення випадковими числами від 0 до 100 з двома цифрами після коми:
            for (int i = 0; i < a.M; i++)
            {
                for (int j = 0; j < a.N; j++)
                {
                    string s =  $"{(rand.NextDouble() * 100):f2}"; // інтерполяція рядка
                    a[i][j] = Double.Parse(s);
                }
            }
        }

        private void Calc_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (RadioButtonTranspose.IsChecked ?? true)
                {
                    // Здійснюємо транспонування:
                    for (int i = 0; i < a.M; i++)
                    {
                        for (int j = i + 1; j < a.N; j++)
                        {
                            double z = a[i][j];
                            a[i][j] = a[j][i];
                            a[j][i] = z;
                        }
                    }
                }
                else
                {
                    // Обчислюємо слід:
                    double trace = 0;
                    for (int i = 0; i < a.M; i++)
                    {
                        trace += a[i][i];
                    }
                    // Виводимо результат:
                    if (CheckBoxWindow.IsChecked ?? true)
                    {
                        TextBoxTrace.Text = "";
                        MessageBox.Show($"Слід матриці: {trace:f2}", "Результат");
                    }
                    else
                    {
                        TextBoxTrace.Text = $"Слід матриці: {trace:f2}";
                    }
                }
            }
            catch (Exception)
            {
                MessageBox.Show("Перевірте дані!", "Помилка");
            }
        }

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

        private void ComboBoxN_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            InitTable(int.Parse(ComboBoxN.SelectedItem + ""));
            TextBoxTrace.Text = "";
        }
    }
}

Як видно з наведеного приклада, модифікація елементів масиву обумовлює автоматичне перемалювання таблиці. Немає потреби в будь-якому ручному оновлені.

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

Далі можна заповнювати матрицю, змінювати її розміри, а також виконувати різні дії через меню.

До недоліків наведеного вище програмного рішення можна віднести розташування математичних алгоритмів транспонування і знаходження сліду всередині методів-оброблювачів подій. У більш складних проектах усі обчислення слід виносити в окремий клас.

3.5 Створення GUI-застосунку для обробки даних про книжки

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

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

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

Спочатку до бібліотеки, створеної у попередній лабораторній роботі додаємо новий клас з назвою BookshelfWithLINQ (переходимо у Solution Explorer, у контекстному меню BookshelfLib обираємо Add | Class...). Цей клас буде похідним від Bookshelf. Необхідно перевизначити конструктор, а також функції ContainsCharacters(), SortByTitle() та SortByAuthorsCount(). Крім того, для нового класу слід перевантажити операції + та -. Увесь код матиме такий вигляд:

// BookshelfWithLINQ.cs

namespace BookshelfLib
{
    public class BookshelfWithLINQ<TAuthor> : Bookshelf<TAuthor>
    {
        // Конструктор
        public BookshelfWithLINQ(params Book<TAuthor>[] books) : base(books)
        {
        }

        public new List<Book<TAuthor>> ContainsCharacters(string characters)
        {
            var found = from book in Books
                        where book.Title.Contains(characters)
                        select book;

            // повертаємо результат пошуку found,
            // створивши на його основі новий список типу List<Book<TAuthor>>
            // для забезпечення відповідності типів:
            return new List<Book<TAuthor>>(found);
        }

        // Сортування за алфавітом назв:
        public new void SortByTitle()
        {
            Books = new List<Book<TAuthor>>(
                from book in Books
                orderby book.Title
                select book);
        }

        // Сортування за кількістю авторів:
        public new void SortByAuthorsCount()
        {
            Books = new List<Book<TAuthor>>(
                from book in Books
                orderby book.Authors.Count
                select book);
        }

        // Перевантажений оператор додавання книжки
        public static BookshelfWithLINQ<TAuthor> operator +
            (BookshelfWithLINQ<TAuthor> bookshelf, Book<TAuthor> newBook)
        {
            var newBooks = new List<Book<TAuthor>>(bookshelf.Books) { newBook };
            return new BookshelfWithLINQ<TAuthor>() { Books = newBooks };
        }

        // Перевантажений оператор видалення книжки.
        // Використання LINQ – суто ілюстративне
        public static BookshelfWithLINQ<TAuthor> operator -
            (BookshelfWithLINQ<TAuthor> bookshelf, Book<TAuthor> oldBook)
        {
            var newBooks = new List<Book<TAuthor>>(
                from book in bookshelf.Books
                where !book.Equals(oldBook)
                select book);
            return new BookshelfWithLINQ<TAuthor>() { Books = newBooks };
        }

    }
}

Примітка: ключове слово new в описі перевизначених функцій не є обов'язковим, оскільки вони не є віртуальними, але використання new робить код більш коректним.

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

using BookshelfLib;

namespace BookshelfWithLINQApp
{
    class Program
    {
        static void Main()
        {
            // Створюємо порожню полицю:
            BookshelfWithLINQ<Author> bookshelf = new();


            // Додаємо книжки
            bookshelf += new Book<Author>("The UML User Guide", 1999,
                                          new Author() { Name = "Grady", Surname = "Booch" },
                                          new Author() { Name = "James", Surname = "Rumbaugh" },
                                          new Author() { Name = "Ivar", Surname = "Jacobson" });
            bookshelf += new Book<Author>(@"Об'єктно-орiєнтоване моделювання програмних систем", 2007,
                                          new Author() { Name = "Iгор", Surname = "Дудзяний" });
            bookshelf += new Book<Author>("Thinking in Java", 2005,
                                          new Author() { Name = "Bruce", Surname = "Eckel" });

            // Виводимо дані на екран:
            Console.WriteLine(bookshelf);
            Console.WriteLine();

            // Шукаємо книжки з певною послідовністю літер:
            Console.WriteLine("Уведiть послiдовнiсть лiтер:");
            string sequence = Console.ReadLine() ?? "";
            BookshelfWithLINQ<Author> newBookshelf = new() { Books = bookshelf.ContainsCharacters(sequence) };


            // Виводимо результат на екран:
            Console.WriteLine("Знайденi книжки:");
            Console.WriteLine(newBookshelf);
            Console.WriteLine();

            try
            {
                // Зберігаємо дані про книжки:
                bookshelf.WriteBooks("Bookshelf.xml");

                // Здійснюємо сортування за назвами та зберігаємо у файлі:
                bookshelf.SortByTitle();
                Console.WriteLine("За назвами:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
                bookshelf.WriteBooks("ByTitle.xml");

                // Здійснюємо сортування за кількістю авторів та зберігаємо у файлі:
                bookshelf.SortByAuthorsCount();
                Console.WriteLine("За кiлькiстю авторiв:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
                bookshelf.WriteBooks("ByAuthorsCount.xml");

                // Створюємо нову полицю. Для зберігання даних про автора використовуємо рядок
                TitledBookshelf<string> titledBookshelf = new("Java");
                titledBookshelf += new Book<string>("Thinking in Java", 2005, "Bruce Eckel");
                Console.WriteLine("Полиця з книжками з мови Java:");
                Console.WriteLine(titledBookshelf);
                titledBookshelf.WriteBooks("JavaBooks.xml");

                // Відтворюємо першу полицю в початковому варіанті
                bookshelf.ReadBooks("Bookshelf.xml");
                Console.WriteLine("Початковий стан:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();

                // Видаляємо книжку про Java
                Book<Author> javaBook = bookshelf[2]; // індексатор
                bookshelf -= javaBook;
                Console.WriteLine("Пiсля видалення книжки:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine("------------Виняток:------------");
                Console.WriteLine(ex.GetType());
                Console.WriteLine("-------------Змсiт:-------------");
                Console.WriteLine(ex.Message);
                Console.WriteLine("-------Трасування  стеку:-------");
                Console.WriteLine(ex.StackTrace);
            }
        }
    }
}

Результат повинен збігатися з результатом попередньої версії консольного тесту.

Тепер можна переходити до створення застосунку WPF. У нашому випадку головне вікно програми графічного інтерфейсу користувача міститиме таблицю, яка відображає дані про книги, кнопки, пов'язані з конкретними функціями, область відображення даних про авторів.

До попереднього рішення додаємо новий проект – WPF App і встановлюємо його як стартовий. Ім'я проекту буде WpfBookshelfApp. Спочатку у вікні Properties слід змінити заголовок вікна (Title) на "Книжкова полиця".

В нашому випадку замість сітки (Grid) доцільно використати компонент DockPanel. Це можна зробити у два способи:

  • видалити сітку, додати DockPanel з вікна Toolbox;
  • в редакторі XAML-коду вручну змінити теги <Grid> та </Grid> на <DockPanel> та </DockPanel> відповідно.

До форми всередині DockPanel слід додати компонент Grid (сітка), на якому пізніше будуть розташовані кнопки та рядок уведення. Властивостям VerticalAlignment та DockPanel.Dock слід встановити значення Top, властивість Width слід скинути в усталене значення (функція Reset Value контекстного меню обраної властивості). Припустимо, властивість Height дорівнюватиме "54".

Примітка: альтернативою Grid є контейнер StackPanel, який автоматично розміщує компоненти послідовно у вертикальному або горизонтальному напрямку.

До нової сітки послідовно додаємо кнопки (компонент Button). Кнопки матимуть назву ButtonOpen, ButtonSortByTitle, ButtonSortByAuthorsCount, ButtonSearch та ButtonSave. Властивості Content кнопок необхідно встановити відповідно у "Відкрити", "Сортувати за назвою", "Сортувати за кількістю авторів", "Знайти" та "Зберегти". Властивості можна редагувати як у вікні Properties, так і у вікні редагування XAML-коду. Крім того, між другою та третьою кнопками додаємо рядок уведення тексту (TextBox) з ім'ям TextBoxSearch. Для всіх елементів властивість HorizontalAlignment повинна мати значення Left.

Якщо на кнопці необхідно розмістити текст з кількох рядків (як у нашому випадку), всередині кнопки слід розташувати компонент Grid, а всередині нього – TextBlock, для якого встановити властивості TextWrapping значення Wrap, а TextAlignment – значення Center. Це можна зробити шляхом ручного редагування XAML-коду. Визначення властивості Content слід видалити. Наприклад, отримаємо такі описи кнопок ButtonSortByTitle та ButtonSearch:

<Button Height="42" HorizontalAlignment="Left" Margin="115,6,0,0" 
        Name="ButtonSortByTitle" VerticalAlignment="Top" Width="103" >
    <Grid>
        <TextBlock Text="Сортувати за назвою" 
                   TextWrapping="Wrap" TextAlignment="Center" />
    </Grid>
</Button>
<Button Height="42" HorizontalAlignment="Left" Margin="225,6,0,0" 
            Name="ButtonSortByAuthorsCount" VerticalAlignment="Top" Width="103">
    <Grid>
        <TextBlock Text="Сортувати за кількістю авторів" 
                   TextWrapping="Wrap" TextAlignment="Center" />
    </Grid>
</Button>

Тепер до нижньої частини форми додаємо компонент DataGrid – таблиця даних, яка відображатиме дані про авторів книг, тому її одразу доцільно перейменувати в DataGridAuthors. Значення властивості Width слід скинути (або видалити зі XAML-коду відповідні атрибути), властивостям DockPanel.Dock та VerticalAlignment встановити значення Bottom.

До середньої частини форми додаємо таблицю (DataGrid) для відображення та редагування даних про книжки (DataGridBooks). На відміну від попередньої таблиці, скинути треба значення як для Width, так і для Height.

Якщо встановити новий проект як стартовий та завантажити його на виконання, отримаємо таке вікно:

До таблиць слід додати колонки. Це можна зробити через редагування властивості Columns. У вікні Collection Editor: Columns, у яке потрапляємо, натиснувши на кнопку з "...", додаємо до таблиці DataGridBooks дві нових колонки за допомогою кнопки Add та налаштовуємо властивості колонок: для першої колонки властивості Header першої колонки встановлюємо значення "Назва", а для другої – "Рік видання". Аналогічно додаємо дві колонки з назвами "Ім'я" та "Прізвище" до таблиці DataGridAuthors. Властивості колонок можна потім налаштовувати за допомогою редактора властивостей (Properties). Зокрема, слід визначити імена колонок – для колонок DataGridBooks відповідно ColumnTitle та ColumnYear, для колонок DataGridAuthors відповідно ColumnName та ColumnSurname. Для покращення відображення окремих колонок доцільно встановити конкретні значення ширини (Width).

Додати контекстне меню можна вручну, включивши після визначення колонок DataGridBooks такий XAML-код:

<DataGrid.ContextMenu>
    <ContextMenu>
        <MenuItem Header="Додати рядок" Name="MenuItemAdd" />
        <MenuItem Header="Видалити рядок" Name="MenuItemRemove" />
    </ContextMenu>
</DataGrid.ContextMenu>

Після розміщення та налаштування компонентів отримаємо такий XAML-код:

<Window x:Class="WpfBookshelfApp.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:WpfBookshelfApp"
        mc:Ignorable="d"
        Title="Книжкова полиця" Height="450" Width="685">
    <DockPanel>
        <Grid Height="54" DockPanel.Dock="Top" VerticalAlignment="Top">
            <Button Content="Відкрити" Height="42" HorizontalAlignment="Left" Margin="5,6,0,0" 
                    Name="ButtonOpen" VerticalAlignment="Top" Width="103" />
            <Button Height="42" HorizontalAlignment="Left" Margin="115,6,0,0" 
                    Name="ButtonSortByTitle" VerticalAlignment="Top" Width="103">
                <Grid>
                    <TextBlock Text="Сортувати за назвою" TextWrapping="Wrap" TextAlignment="Center" />
                </Grid>
            </Button>
            <Button Height="42" HorizontalAlignment="Left" Margin="225,6,0,0" 
                    Name="ButtonSortByAuthorsCount" VerticalAlignment="Top" Width="103">
                <Grid>
                    <TextBlock Text="Сортувати за кількістю авторів" TextWrapping="Wrap" 
                               TextAlignment="Center" />
                </Grid>
            </Button>
            <TextBox Height="24" HorizontalAlignment="Left" Margin="335,14,0,0" 
                     Name="TextBoxSearch" VerticalAlignment="Top" Width="109" />
            <Button Content="Знайти" Height="42" HorizontalAlignment="Left" Margin="450,6,0,0" 
                    Name="ButtonSearch" VerticalAlignment="Top" Width="103" />
            <Button Content="Зберегти" Height="42" HorizontalAlignment="Left" Margin="560,6,0,0" 
                    Name="ButtonSave" VerticalAlignment="Top" Width="103" />
        </Grid>
        <DataGrid AutoGenerateColumns="False" Height="130" Name="DataGridAuthors" 
                  DockPanel.Dock="Bottom" VerticalAlignment="Bottom">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Ім'я" Width="100" x:Name="ColumnName" />
                <DataGridTextColumn Header="Прізвище" Width="100" x:Name="ColumnSurname" />
            </DataGrid.Columns>
        </DataGrid>
        <DataGrid AutoGenerateColumns="False" Name="DataGridBooks">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Назва" Width="350" x:Name="ColumnTitle" />
                <DataGridTextColumn Header="Рік видання" x:Name="ColumnYear" />
            </DataGrid.Columns>
            <DataGrid.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Додати рядок" Name="MenuItemAdd" />
                    <MenuItem Header="Видалити рядок" Name="MenuItemRemove" />
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
    </DockPanel>
</Window>

Примітка: порядок атрибутів може бути іншим.

Тепер можна розпочати безпосереднє кодування. Спочатку до проекту WpfBookshelfApp слід додати посилання на бібліотеку класів BookshelfLib. Потім відкриваємо файл MainWindow.xaml.cs та додаємо

using BookshelfLib;

Для спрощення редагування даних замість структури Author доцільно використовувати клас Author, який можна описати у файлі MainWindow.xaml.cs після опису класу MainWindow. До коду класу MainWindow слід додати поле – посилання на BookshelfWithLINQ. Можна одночасно здійснити ініціалізацію об'єкта. Текст файлу матиме такий вигляд:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using BookshelfLib;

namespace WpfBookshelfApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private BookshelfWithLINQ<Author> bookshelf = new();

        public MainWindow()
        {
            InitializeComponent();
        }
    }

    // Клас для опису автора
    public class Author
    {

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Surname { get; set; } = "";

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Name { get; set; } = "";

        // Перевизначення еквівалентності
        public override bool Equals(object? obj)
        {
            if (obj == null)
            {
                return false;
            }
            Author author = (Author)obj;
            return author.Surname == Surname && author.Name == Name;
        }

        // Визначення представлення у вигляді рядку:
        public override string ToString()
        {
            return Name + " " + Surname;
        }

        // Визначається у парі з Equals()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }
}

Додаємо поле типу BookshelfWithLINQ<Author>. Для відображення даних після завантаження програми до конструктору слід додати створення "порожньої" книжки та виклик функції InitGrid(). Відповідний код матиме вигляд:

        private BookshelfWithLINQ<Author> bookshelf = new();

        public MainWindow()
        {
            InitializeComponent();

            // Додаємо порожню книжку:
            bookshelf += new Book<Author>()
            {
                Title = "",
                Authors = new List<Author> { new Author() { Name = "", Surname = "" } }
            };
            InitGrid();
        }

        void InitGrid()
        {
            // Зв'язуємо таблицю DataGridBooks зі списком книг:
            DataGridBooks.ItemsSource = bookshelf.Books;
            DataGridBooks.CanUserAddRows = false;

            // Вказуємо, які колонки зв'язані з якими властивостями:
            ColumnTitle.Binding = new Binding("Title");
            ColumnYear.Binding = new Binding("Year");

            // Показуємо авторів для обраної книжки:
            ShowAuthors(DataGridBooks.Items.IndexOf(DataGridBooks.SelectedItem));
        }

        private void ShowAuthors(int index)
        {
            // Якщо індекс хибний, встановлюємо індекс 0
            if (index < 0 || index >= bookshelf.Books.Count)
            {
                index = 0;
            }

            // Зв'язуємо таблицю DataGridAuthors зі списком авторів:
            DataGridAuthors.ItemsSource = bookshelf.Books[index].Authors;
            DataGridAuthors.CanUserAddRows = false;

            // Вказуємо, які колонки зв'язані з якими властивостями:
            ColumnName.Binding = new Binding("Name");
            ColumnSurname.Binding = new Binding("Surname");
        }

Для відображення у нижній таблиці авторів книжки, через Properties необхідно додати оброблювач події SelectionChanged. Його код буде таким:

        private void DataGridBooks_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // Відображаємо авторів книжки, орбаної в DataGridBooks:
            ShowAuthors(DataGridBooks.Items.IndexOf(DataGridBooks.SelectedItem));
        }

Для додавання та видалення рядків таблиці реалізуємо оброблювачі подій menuItemAdd_Click та menuItemRemove_Click:

        private void MenuItemAdd_Click(object sender, RoutedEventArgs e)
        {
            // Підтверджуємо зміни даних у таблиці:
            DataGridBooks.CommitEdit();
            // Додаємо порожню книжку:
            bookshelf += new Book<Author>()
            {
                Title = "",
                Authors = new List<Author> { new Author() { Name = "", Surname = "" } }
            };
            InitGrid();
        }

        private void MenuItemRemove_Click(object sender, RoutedEventArgs e)
        {
            // Визначаємо індекс активного рядку:
            int index = DataGridBooks.SelectedIndex;
            // Підтверджуємо зміни даних у таблиці:
            DataGridBooks.CommitEdit();
            // Видаляємо активний рядок:
            bookshelf.Books.RemoveAt(index);
            // Якщо видалили всі рядки, додаємо новий порожній:
            if (bookshelf.Books.Count == 0)
            {
                bookshelf = new BookshelfWithLINQ<Author>();
                bookshelf += new Book<Author>("", 0, new Author());
                InitGrid();
            }
            DataGridBooks.ItemsSource = null;
            InitGrid();
        }

Підсистема WPF не надає компонентів для роботи з файловою системою. Відповідні об'єкти створюють у коді. Оброблювачі подій від кнопок ButtonOpen, ButtonSortByTitle, ButtonSortByAuthorsCount та ButtonSave матимуть такий вигляд:

        private void ButtonOpen_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
                {
                    bookshelf.ReadBooks(dlg.FileName);
                }
                catch (Exception)
                {
                    MessageBox.Show("Помилка читання з файлу");
                }
                InitGrid();
            }
        }

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

        private void ButtonSortByTitle_Click(object sender, RoutedEventArgs e)
        {
            bookshelf.SortByTitle();
            InitGrid();
        }

        private void ButtonSortByAuthorsCount_Click(object sender, RoutedEventArgs e)
        {
            bookshelf.SortByAuthorsCount();
            InitGrid();
        }

Для відображення результатів пошуку можна створити окреме вікно. До проекту додаємо нове вікно (Project | Add Window...). Воно матиме назву WindowSearchResults. Властивості Title встановлюємо значення "Результати пошуку". До сітки (Grid) додаємо компонент TextBox з ім'ям TextBoxSearchResults та налаштовуємо його так, щоб він займав усю клієнтську частину вікна. Отримуємо такий XAML-код:

   <Window x:Class="WpfBookshelfApp.WindowSearchResults"
           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:WpfBookshelfApp"
           mc:Ignorable="d"
           Title="Результати пошуку" Height="300" Width="600">
       <Grid>
           <TextBox Name="TextBoxSearchResults" />
       </Grid>
   </Window>

Оброблювач події ButtonSearch_Click матиме такий вигляд:

        private void ButtonSearch_Click(object sender, RoutedEventArgs e)
        {
            string sequence = TextBoxSearch.Text;
            if (sequence.Length == 0)
            {
                return;
            }
            // Знаходимо книжки за ознакою:
            var found = bookshelf.ContainsCharacters(sequence);

            // Формуємо рядок результатів:
            string text = "";
            foreach (var book in found)
            {
                text += book + "\n";
            }
            // Створюємо нове вікно:
            WindowSearchResults windowSearchResults = new();
            windowSearchResults.TextBoxSearchResults.Text = text;
            windowSearchResults.ShowDialog();
        }

4 Вправи для контролю

  1. Увести з клавіатури ім'я певної теки та ім'я файлу. Створити нову теку та новий файл. Вивести на екран імена усіх файлів цієї теки. Видалити файл та теку.
  2. Увести з клавіатури ім'я певної теки та розширення імені файлів. Вивести на екран імена усіх файлів цієї теки з визначеним розширенням. Якщо тека не існує, вивести повідомлення про помилку.
  3. Увести з клавіатури ім'я певної теки. Вивести на екран імена усіх підкаталогів цієї теки. Якщо тека не існує, вивести повідомлення про помилку.
  4. Реалізувати програму, яка обчислює визначений інтеграл методом прямокутників. Для визначення вихідної функції застосувати делегати.
  5. Реалізувати програму, яка обчислює визначений інтеграл методом трапецій. Для визначення вихідної функції застосувати делегати.
  6. За допомогою лямбда-виразів реалізувати пошук у списку цілих чисел таких які починаються та закінчуються однаковою цифрою (за допомогою представлення у вигляді рядка).
  7. За допомогою лямбда-виразів реалізувати сортування списку цілих чисел за зменшенням останньої цифри (за допомогою представлення у вигляді рядка).
  8. За допомогою засобів LINQ реалізувати пошук у списку цілих чисел таких які починаються та закінчуються однаковою цифрою (за допомогою представлення у вигляді рядка).
  9. За допомогою засобів LINQ реалізувати сортування списку цілих чисел за зменшенням останньої цифри (за допомогою представлення у вигляді рядка).
  10. Засобами WPF створити програму, у якій користувач уводить радіус кола та отримує зображення цього кола у вікні.
  11. Засобами WPF створити програму, у якій різними кольорами здійснюється малювання троьох концентричних кругів.

5 Контрольні запитання

  1. Чим відрізняється визначення var від dynamic?
  2. Як реалізувати узагальнений підхід за допомогою типу dynamic?
  3. Чи можна використовувати клас FileInfo для модифікації теки?
  4. Чи можна використовувати клас FileInfo для модифікації атрибутів файлів?
  5. Як програмно створити та видалити теку?
  6. У чому полягає призначення неповних (часткових) класів?
  7. Як здійснюється реалізація неповних (часткових) класів?
  8. Для чого і як реалізується зворотний виклик (callback)?
  9. У чому полягає концепція делегатів?
  10. Чим делегати відрізняються від указівників на функції мови С++?
  11. Чим визначається тип функції?
  12. Як описати тип делегату?
  13. Які є способи створення екземплярів делегатів?
  14. Як створити безіменний метод?
  15. У чому полягає концепція програмування, керованого подіями?
  16. Як події реалізовані в мові C#?
  17. У чому полягають концепції функційного програмування?
  18. Як визначити поняття замикання?
  19. Які переваги надають лямбда-вирази?
  20. Як створити список з кількох параметрів у лямбда-виразі?
  21. У чому полягають концепції декларативного програмування?
  22. З якою метою до мови C# додана технологія LINQ?
  23. Яким є тип результату LINQ-запиту?
  24. У чому є особливості, переваги та недоліки технології Windows Presentation Foundation?
  25. Для чого і як застосовують XAML?
  26. Які є особливості компонування візуальних елементів у WPF?
  27. Які є класи-контейнери у WPF?
  28. Які є особливості зв'язування даних у WPF?
  29. Як створити WPF-застосунок у Visual Studio?
  30. Які є особливості графіки у WPF?

 

up