Лабораторна робота 5
Подіє-орієнтоване, функційне та декларативне програмування
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
За допомогою засобів Windows Presentation Foundation (WPF) розробити програму графічного інтерфейсу користувача, яка реалізує завдання попередньої лабораторної роботи. Для пошуку й сортування слід застосувати технологію LINQ. Програма повинна реалізовувати такі функції
- завантаження даних з обраного файлу
- відображення даних про об'єкти у вигляді таблиці (
DataGrid
) - відображення додаткових даних в окремій таблиці
- редагування основних даних (модифікація, додавання, видалення)
- сортування за певною ознакою
- вибір за певною ознакою та відображення в окремому вікні
- зберігання у новому файлі
Програма повинна використовувати бібліотеку класів, яка була створена у попередній лабораторній роботі. Додати до бібліотеки засоби роботи з послідовностями з використанням технології LINQ.
1.2 Робота з файловою системою
В усіх підкаталогах, починаючи з визначеного, знайти та вивести на екран дані про всі файли, довжина яких більше заданої.
1.3 Робота з делегатами
Реалізувати завдання 1.2 ("Корені рівняння") лабораторної роботи № 3. Застосувати підхід, побудований на використанні делегатів. Виконати завдання в трьох варіантах визначення лівої частини рівняння:
- використання окремого статичного методу;
- використання безіменного делегата;
- використання лямбда-виразу.
1.4 Використання лямбда-виразів та LINQ
Створити статичний клас, який містить такі функції для роботи зі списками цілих чисел (додатних та від'ємних):
- пошук елементів, які є точними квадратами (квадрат квадратного кореня дорівнює числу)
- сортування за зменшенням значень модулів елементів
Реалізувати два підходи – через використання лямбда-виразів та через використання LINQ.
1.5 Робота з меню та таблицями даних
За допомогою засобів WPF розробити програму, в якій користувач уводить розмірність двовимірного масиву
(m
та n
), вводить елементи у комірки таблиці (DataGrid
), або заповнює
таблицю випадковими значеннями від -1000 до 1000, а потім за допомогою відповідної функції меню обчислює суму
добутків рядків. Сума виводиться у за допомогою компоненту Label
, або в діалоговому вікні, залежно
від відповідної опції (компонент CheckBox
). Реалізувати головне меню.
2 Методичні вказівки
2.1 Неповні класи та методи
У версії 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.2 Робота з файловою системою
Простір імен 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 FifthLab { 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.3 Делегати
Невіддільною частиною практики сучасного програмування стало використання зворотних викликів (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.4 Події
Реалізація застосунків графічного інтерфейсу користувача базується на механізмі отримання та обробки подій. Уся програма складається з ініціалізації (реєстрації візуальних елементів управління) та основного циклу отримання та обробки подій. Події – це переміщення або натискання кнопок миші, клавіатурне введення тощо. Кожний зареєстрований візуальний елемент управління може отримувати події, які до нього стосуються, та виконувати функції обробки цих подій. Робота з подіями в С# відповідає моделі "видавець – передплатник", де клас публікує подію, яку він може ініціювати, і будь-які класи можуть підписатися на цю подію. При ініціації події середовище стежить за тим, щоб повідомити всіх передплатників про виникнення події.
Метод, який викликається після виникнення події, визначається делегатом. Потрібно, щоб такий делегат приймав
два аргументи. Ці аргументи повинні представляти два об'єкти: той, що ініціював подію (видавець), і
інформаційний об'єкт події, що повинний бути похідним від класу EventArgs
.NET.
Спеціальне ключове слово event
використовують для опису елементів класу типу відповідного
делегату.
Події допомагають нам розділити функціональність системи на незалежні частини. Припустимо, що якийсь клас дозволяє отримати кілька цілих додатних чисел у межах заданого діапазону. Інший клас застосовує дві дії до кожного числа: він перевіряє, чи це ціле число є простим числом і чи ділиться воно на 7:
using System; namespace FifthLab { // Делегат для представлення події: 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.5 Концепції функційного та декларативного програмування
Функційне програмування – це парадигма програмування, за допомогою якої програмне забезпечення подається як набір функцій (без умов та змінних). Функції можна викликати одна з іншої, і таким чином реалізується так зване замикання і рекурсія.
Замикання – це особливий вид функції, який визначається в тілі іншої функції. Внутрішня функція створюється кожного разу, коли вона виконується. Вкладена функція містить посилання на локальні змінні зовнішньої функції. Кожного разу, коли викликається зовнішня функція, створюється новий екземпляр внутрішньої функції з новими посиланнями на змінні.
Рекурсія – це механізм виклику функції, що здійснюється з тіла цієї функції (прямо чи опосередковано).
Такі мови, як LISP, XQuery, F #, можна класифікувати як мови функційного програмування.
Декларативне програмування – це парадигма, за допомогою якої описують результати, які слід отримати замість того, щоб описувати послідовність дій, що ведуть до цього результату. Прикладами декларативних мов є HTML, SQL (мови, призначені для спеціальної предметної області), а також і мова логічного програмування Prolog. Велика кількість декларативних мов базується на XML.
Останні версії мови C# (починаючи з 3.0) насправді є гібридними, оскільки вони підтримують не тільки імперативне, об'єктно-орієнтоване та загальне програмування, але також функційне та декларативне програмування.
2.6 Реалізація мовою C# функційного та декларативного програмування
2.6.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; });
Лямбда-вираз дозволяє реалізувати такий пошук у більш компактний спосіб:
var b = a.FindAll(i => i % 2 == 0);
У наступному прикладі за допомогою методу 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 }
2.6.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.7 Розробка GUI-застосунків за допомогою технології Windows Presentation Foundation
2.7.1 Основні концепції
Починаючи з версії .NET Framework 3.0, для створення desktop-застосунків запропонована нова графічна (презентаційна) підсистема – Windows Presentation Foundation (WPF). Це – сучасна альтернатива попереднім засобам розробки GUI-застосунків, зокрема, бібліотеці Windows.Forms. Головна концепція WPF полягає у відокремленні представлення зовнішнього вигляду програми графічного інтерфейсу користувача від програмного коду. Основними рисами WPF є такі:
- декларативний підхід до опису елементів графічного інтерфейсу
- використання векторної графіки для малювання компонентів
- використання стилів та шаблонів оформлення графічного інтерфейсу користувача
На рівні безпосереднього графічного відображення WPF базується на використанні DirectX.
2.7.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.7.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.7.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.7.5 Зв'язування даних у WPF
Для відображення даних у WPF застосовується технологія зв'язування даних. Для компонентів, обізнаних у даних,
(наприклад, DataGrid
) можна визначити джерело даних – ItemsSource
. Це можуть
бути, наприклад списки, або деякі інші колекції. Крім того, для зв'язування даних використовують рядки Binding
– прив'язку між значенням властивості об'єкта і значенням властивості елемента керування. Зв'язування
даних можна здійснювати у XAML-коді. Наприклад, зв'язуємо колонку таблиці з властивістю Name
:
<DataGridTextColumn Header="Назва" Binding="{Binding Name}" />
Зв'язування даних можна також здійснювати в коді мовою C#.
2.7.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.7.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 Робота з файловою системою
Припустимо, в усіх підкаталогах, починаючи з визначеного, необхідно знайти та вивести на екран дані про всі
файли, які мають розширення "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.2 Використання лямбда-виразів та 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.3 Робота з меню та таблицями даних
Припустимо, нам необхідно створити 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
та дві кнопки типу RadioButton
– RadioButtonTranspose
та 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
-директив виглядатиме так:
using System; using System.Windows; using System.Windows.Controls; using DataArrays; namespace WpfMatrixApp { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private DataArray a = new(0, 0); 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++) { (a[j][i], a[i][j]) = (a[i][j], a[j][i]); } } } 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.4 Створення GUI-застосунку для роботи з книжковою полицею
Припустимо, нам необхідно створити програму графічного інтерфейсу користувача для роботи з даними про публікації на книжковій полиці. Необхідно реалізувати такі функції
- завантаження даних з обраного XML-файлу;
- відображення даних про книги у вигляді таблиці;
- відображення додаткових даних в окремому підвікні;
- підтримка редагування даних;
- сортування книг за назвою з ігноруванням регістру;
- знаходження книг з певною послідовністю літер у назві;
- збереження змінених даних у новому файлі.
Програма повинна використовувати раніше створену бібліотеку класів, але ми будемо використовувати технологію LINQ для роботи з послідовностями.
Спочатку до бібліотеки, створеної у попередній лабораторній роботі додаємо новий клас з назвою BookshelfProcessorWithLinq
(переходимо у Solution Explorer, у контекстному меню BookshelfLib обираємо Add |
Class...). Цей клас буде статичним. Необхідно запропонувати альтернативну реалізацію для методів пошуку й
сортування, які були раніше реалізовані в класі
BookshelfProcessor
.
Увесь код класу BookshelfProcessorWithLinq
матиме такий вигляд:
// BookshelfProcessorWithLinq.cs namespace BookshelfLib { /// <summary> /// Надає методи для пошуку і сортування видань на полиці /// </summary> public static class BookshelfProcessorWithLinq { /// <summary> /// Шукає визначену послідовність символів у назвах видань /// </summary> /// <param name="bookshelf">книжкова полиця</param> /// <param name="characters">послідовність символів, яку треба відшукати</param> /// <returns>масив видань, у назви яких входить визначена послідовність</returns> public static List<Publication> ContainsCharacters(Bookshelf bookshelf, string characters) { var found = from publication in bookshelf.Publications where publication.Title.Contains(characters) select publication; return found.ToList(); } /// <summary> /// Здійснює сортування видань за алфавітом назв без урахування регістру /// </summary> /// <param name="bookshelf">книжкова полиця</param> public static void SortByTitles(Bookshelf bookshelf) { var sorted = from publication in bookshelf.Publications orderby publication.Title.ToUpper() select publication; bookshelf.Publications = sorted.ToList(); } } }
Для тестування нових варіантів методів додаємо новий клас до консольного застосунку:
// LinqTesting.cs using BookshelfLib; namespace BookshelfApp { public static class LinqTesting { /// <summary> /// Демонструє роботу функцій пошуку та сортування видань /// </summary> /// <param name="bookshelf">книжкова полиця, для якої здійснюється демонстрація роботи</param> public static void HandleBookshelf(Bookshelf bookshelf) { Console.WriteLine("\nПочатковий стан:"); Console.WriteLine(bookshelf); Console.WriteLine("\nНазви, які містять \"The\""); var result = BookshelfProcessorWithLinq.ContainsCharacters(bookshelf, "The"); // Змінили назву класу foreach (var publication in result) { Console.WriteLine(publication.Title); } Console.WriteLine("\nЗа алфавітом без урахування регістру:"); BookshelfProcessorWithLinq.SortByTitles(bookshelf); // Змінили назву класу Console.WriteLine(bookshelf); } } }
Відповідну зміну слід внести в код функції Main()
:
/// <summary> /// Стартова точка консольного застосунку /// </summary> static void Main() { Console.OutputEncoding = System.Text.Encoding.UTF8; Bookshelf bookshelf = CreateBookshelf(); LinqTesting.HandleBookshelf(bookshelf); // Використовуємо роботу з LINQ FileUtils.WriteToFile(bookshelf, "publications.txt"); AdditionalProcessing("books.txt"); // Немає такого файлу AdditionalProcessing("publications.txt"); XMLProcessing(bookshelf, "publications.xml"); }
Результат повинен збігатися з результатом попередньої версії консольного застосунку.
Тепер можна переходити до створення застосунку WPF. У нашому випадку головне вікно програми графічного інтерфейсу користувача міститиме таблицю, яка відображає дані про видання, кнопки, пов'язані з конкретними функціями, область відображення даних про авторів (для книг).
До попереднього рішення додаємо новий проєкт – WPF Application і встановлюємо його як стартовий. Ім'я
проєкту буде 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
, ButtonSearch
та ButtonSave
.
Властивості Content
кнопок необхідно встановити відповідно у "Відкрити", "Сортувати
за назвою", "Знайти" та "Зберегти".
Властивості можна редагувати як у вікні Properties, так і у вікні редагування XAML-коду. Крім того, між
другою та третьою кнопками додаємо рядок уведення тексту (TextBox
) з ім'ям
TextBoxSearch
. Для всіх елементів властивість HorizontalAlignment
повинна мати
значення Left
.
До нижньої частини форми додаємо компонент DataGrid
– таблиця даних, яка відображатиме
дані про авторів книг, тому її одразу доцільно перейменувати в DataGridAuthors
. Значення
властивості Width
слід скинути (або видалити зі XAML-коду відповідні атрибути), властивостям DockPanel.Dock
та VerticalAlignment
встановити значення Bottom
.
До середньої частини форми додаємо таблицю (DataGrid
) для відображення та редагування даних про
публікації (DataGridPublications
). На відміну від попередньої таблиці, скинути треба значення як для Width
,
так і для Height
.
До таблиць слід додати колонки. Це можна зробити через редагування властивості Columns
. У вікні
Collection Editor: Columns, у яке потрапляємо, натиснувши на кнопку з "...", додаємо до
таблиці DataGridPublications
чотири нові колонки за допомогою кнопки Add та налаштовуємо
властивості колонок: для першої колонки властивості Header
першої колонки встановлюємо значення
"Назва", для другої – "Рік видання", для третьої та четвертої – "Том" та "Номер" відповідно.
Аналогічно додаємо дві колонки з назвами "Ім'я автора"
та "Прізвище автора" до таблиці DataGridAuthors
. Властивості колонок можна потім налаштовувати
за допомогою редактора властивостей (Properties). Зокрема, слід визначити імена колонок – для
колонок DataGridPublications
відповідно ColumnTitle
, ColumnYear
, ColumnVolume
та ColumnNumber
,
для колонок
DataGridAuthors
відповідно ColumnName
та ColumnSurname
. Для покращення
відображення окремих колонок доцільно встановити конкретні значення ширини (Width
).
Додати контекстне меню можна вручну, включивши після визначення колонок DataGridPublications
такий
XAML-код:
<DataGrid.ContextMenu> <ContextMenu> <MenuItem Header="Додати книгу" /> <MenuItem Header="Додати журнал" /> <MenuItem Header="Видалити публікацію" /> </ContextMenu> </DataGrid.ContextMenu>
Аналогічно контекстне меню з позиціями "Додати автора" і "Видалити автора" додаємо до компоненту DataGridAuthors
.
Після розміщення та налаштування компонентів отримаємо такий 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" mc:Ignorable="d" Title="Книжкова полиця" Height="450" Width="720"> <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="135" /> <Button Content="Сортувати за назвою" Height="42" HorizontalAlignment="Left" Margin="145,6,0,0" Name="ButtonSortByTitle" VerticalAlignment="Top" Width="135" > </Button> <TextBox Height="24" HorizontalAlignment="Left" Margin="285,14,0,0" Name="TextBoxSearch" VerticalAlignment="Top" Width="135" /> <Button Content="Знайти" Height="42" HorizontalAlignment="Left" Margin="425,6,0,0" Name="ButtonSearch" VerticalAlignment="Top" Width="135" /> <Button Content="Зберегти" Height="42" HorizontalAlignment="Left" Margin="565,6,0,0" Name="ButtonSave" VerticalAlignment="Top" Width="135" /> </Grid> <DataGrid AutoGenerateColumns="False" Height="130" Name="DataGridAuthors" DockPanel.Dock="Bottom" VerticalAlignment="Bottom"> <DataGrid.Columns> <DataGridTextColumn Header="Ім'я автора" Width="120" x:Name="ColumnName" /> <DataGridTextColumn Header="Прізвище автора" Width="120" x:Name="ColumnSurname" /> </DataGrid.Columns> <DataGrid.ContextMenu> <ContextMenu x:Name="ContextMenuAuthors"> <MenuItem Header="Додати автора" /> <MenuItem Header="Видалити автора" /> </ContextMenu> </DataGrid.ContextMenu> </DataGrid> <DataGrid AutoGenerateColumns="False" x:Name="DataGridPublications" > <DataGrid.Columns> <DataGridTextColumn Header="Назва" Width="350" x:Name="ColumnTitle" /> <DataGridTextColumn Header="Рік видання" Width="100" x:Name="ColumnYear" /> <DataGridTextColumn Header="Том" Width="50" x:Name="ColumnVolume" /> <DataGridTextColumn Header="Номер" Width="50" x:Name="ColumnNumber" /> </DataGrid.Columns> <DataGrid.ContextMenu> <ContextMenu> <MenuItem Header="Додати книгу" /> <MenuItem Header="Додати журнал" /> <MenuItem Header="Видалити публікацію /> </ContextMenu> </DataGrid.ContextMenu> </DataGrid> </DockPanel> </Window>
Примітка: порядок атрибутів може бути іншим.
Якщо встановити новий проєкт як стартовий та завантажити його на виконання, отримаємо таке вікно:
Тепер можна розпочати безпосереднє кодування. Спочатку до проєкту WpfBookshelfApp
слід додати
посилання на бібліотеку класів BookshelfLib
(Add | Project Reference... | BookshelfLib). Потім відкриваємо
файл MainWindow.xaml.cs
та додаємо
using BookshelfLib;
До коду
класу MainWindow
слід додати поле – посилання на Bookshelf
. Можна
одночасно здійснити ініціалізацію об'єкта. Відповідний код матиме вигляд:
private Bookshelf bookshelf = new(); public MainWindow() { InitializeComponent(); DataGridAuthors.ContextMenu = null; InitGrid(); } void InitGrid() { DataGridPublications.ItemsSource = null; // Зв'язуємо таблицю DataGridPublications зі списком публікацій: DataGridPublications.ItemsSource = bookshelf.Publications; DataGridPublications.CanUserAddRows = false; // Вказуємо, які колонки зв'язані з якими властивостями: ColumnTitle.Binding = new Binding("Title"); ColumnYear.Binding = new Binding("Year"); ColumnVolume.Binding = new Binding("Volume"); ColumnNumber.Binding = new Binding("Number"); // Показуємо авторів для обраної книжки: ShowAuthors(); } private void ShowAuthors() { if (bookshelf.Publications.Count <= 0) { DataGridAuthors.ItemsSource = null; return; } int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem); // Якщо індекс хибний, встановлюємо індекс 0 if (index < 0 || index >= bookshelf.Publications.Count) { index = 0; } DataGridAuthors.ItemsSource = null; // Зв'язуємо таблицю DataGridAuthors зі списком авторів: if (bookshelf.Publications[index] is Book book) { ColumnVolume.IsReadOnly = true; ColumnNumber.IsReadOnly = true; DataGridAuthors.ItemsSource = null; DataGridAuthors.ItemsSource = book.Authors; DataGridAuthors.CanUserAddRows = false; // Вказуємо, які колонки зв'язані з якими властивостями: ColumnName.Binding = new Binding("Name"); ColumnSurname.Binding = new Binding("Surname"); DataGridAuthors.ContextMenu = ContextMenuAuthors; } if (bookshelf.Publications[index] is Magazine) { ColumnVolume.IsReadOnly = false; ColumnNumber.IsReadOnly = false; DataGridAuthors.ItemsSource = null; DataGridAuthors.ContextMenu = null; } }
Для відображення у нижній таблиці авторів книжки, через Properties необхідно додати оброблювач події
SelectionChanged
. Його код буде таким:
private void DataGridPublications_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Відображаємо авторів книжки, обраної в DataGridPublications: ShowAuthors(DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem)); }
Для додавання та видалення рядків таблиць реалізуємо оброблювачі подій:
private void MenuItemAddBook_Click(object sender, RoutedEventArgs e) { // Підтверджуємо зміни даних у таблиці: DataGridPublications.CommitEdit(); // Додаємо порожню книжку: bookshelf.AddPublication(new Book("", 0)); InitGrid(); } private void MenuItemAddMagazine_Click(object sender, RoutedEventArgs e) { // Підтверджуємо зміни даних у таблиці: DataGridPublications.CommitEdit(); // Додаємо порожній журнал: bookshelf.AddPublication(new Magazine() { Title = "", Year = 0, Volume = 0, Number = 0 }); InitGrid(); } private void MenuItemRemove_Click(object sender, RoutedEventArgs e) { // Визначаємо індекс активного рядку: int index = DataGridPublications.SelectedIndex; // Підтверджуємо зміни даних у таблиці: DataGridPublications.CommitEdit(); // Видаляємо активний рядок: bookshelf.Publications.RemoveAt(index); DataGridPublications.ItemsSource = null; InitGrid(); if (bookshelf.Publications.Count == 0) { DataGridAuthors.ContextMenu = null; } } private void MenuItemAddAuthor_Click(object sender, RoutedEventArgs e) { int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem); // Якщо індекс хибний, встановлюємо індекс 0 if (index < 0 || index >= bookshelf.Publications.Count) { index = 0; } if (bookshelf.Publications[index] is Book book) { book.AddAuthor("", ""); ShowAuthors(); } } private void MenuItemRemoveAuthor_Click(object sender, RoutedEventArgs e) { int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem); // Якщо індекс хибний, встановлюємо індекс 0 if (index < 0 || index >= bookshelf.Publications.Count) { index = 0; } if (bookshelf.Publications[index] is Book book) { // Визначаємо індекс активного рядку: int authorIndex = DataGridAuthors.SelectedIndex; // Підтверджуємо зміни даних у таблиці: DataGridAuthors.CommitEdit(); // Видаляємо активний рядок: book.Authors.RemoveAt(authorIndex); DataGridAuthors.ItemsSource = null; ShowAuthors(); } }
Підсистема WPF не надає компонентів для роботи з файловою системою. Відповідні об'єкти створюють у коді.
Оброблювачі подій від кнопок ButtonOpen
та 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 = XMLHandle.ReadFromFile(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 { XMLHandle.WriteToFile(bookshelf, dlg.FileName); MessageBox.Show("Файл збережено"); } catch (Exception) { MessageBox.Show("Помилка запису в файл"); } } }
Оброблювач події від кнопки ButtonSortByTitle
буде таким:
private void ButtonSortByTitle_Click(object sender, RoutedEventArgs e) { BookshelfProcessorWithLinq.SortByTitles(bookshelf); 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 = BookshelfProcessor.ContainsCharacters(bookshelf, sequence); // Формуємо рядок результатів: StringBuilder text = new(""); if (found.Count > 0) { foreach (var publication in found) { text.Append(publication.Title + "\n"); } } else { text.Append("Нічого не знайдено"); } // Створюємо нове вікно: WindowSearchResults windowSearchResults = new(); windowSearchResults.TextBoxSearchResults.Text = text.ToString(); windowSearchResults.ShowDialog(); }
Після додавання оброблювачів подій файл MainWindow.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" mc:Ignorable="d" Title="Книжкова полиця" Height="450" Width="720"> <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="135" Click="ButtonOpen_Click" /> <Button Content="Сортувати за назвою" Height="42" HorizontalAlignment="Left" Margin="145,6,0,0" Name="ButtonSortByTitle" VerticalAlignment="Top" Width="135" Click="ButtonSortByTitle_Click"> </Button> <TextBox Height="24" HorizontalAlignment="Left" Margin="285,14,0,0" Name="TextBoxSearch" VerticalAlignment="Top" Width="135" /> <Button Content="Знайти" Height="42" HorizontalAlignment="Left" Margin="425,6,0,0" Name="ButtonSearch" VerticalAlignment="Top" Width="135" Click="ButtonSearch_Click" /> <Button Content="Зберегти" Height="42" HorizontalAlignment="Left" Margin="565,6,0,0" Name="ButtonSave" VerticalAlignment="Top" Width="135" Click="ButtonSave_Click" /> </Grid> <DataGrid AutoGenerateColumns="False" Height="130" Name="DataGridAuthors" DockPanel.Dock="Bottom" VerticalAlignment="Bottom"> <DataGrid.Columns> <DataGridTextColumn Header="Ім'я автора" Width="120" x:Name="ColumnName" /> <DataGridTextColumn Header="Прізвище автора" Width="120" x:Name="ColumnSurname" /> </DataGrid.Columns> <DataGrid.ContextMenu> <ContextMenu x:Name="ContextMenuAuthors"> <MenuItem Header="Додати автора" Click="MenuItemAddAuthor_Click" /> <MenuItem Header="Видалити автора" Click="MenuItemRemoveAuthor_Click" /> </ContextMenu> </DataGrid.ContextMenu> </DataGrid> <DataGrid AutoGenerateColumns="False" x:Name="DataGridPublications" SelectionChanged="DataGridPublications_SelectionChanged"> <DataGrid.Columns> <DataGridTextColumn Header="Назва" Width="350" x:Name="ColumnTitle" /> <DataGridTextColumn Header="Рік видання" Width="100" x:Name="ColumnYear" /> <DataGridTextColumn Header="Том" Width="50" x:Name="ColumnVolume" /> <DataGridTextColumn Header="Номер" Width="50" x:Name="ColumnNumber" /> </DataGrid.Columns> <DataGrid.ContextMenu> <ContextMenu> <MenuItem Header="Додати книгу" Click="MenuItemAddBook_Click" /> <MenuItem Header="Додати журнал" Click="MenuItemAddMagazine_Click" /> <MenuItem Header="Видалити публікацію" Click="MenuItemRemove_Click" /> </ContextMenu> </DataGrid.ContextMenu> </DataGrid> </DockPanel> </Window>
Остаточний код класу MainWindow
матиме такий вигляд:
using System; using System.Text; 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 Bookshelf bookshelf = new(); public MainWindow() { InitializeComponent(); DataGridAuthors.ContextMenu = null; InitGrid(); } void InitGrid() { DataGridPublications.ItemsSource = null; // Зв'язуємо таблицю DataGridPublications зі списком публікацій: DataGridPublications.ItemsSource = bookshelf.Publications; DataGridPublications.CanUserAddRows = false; // Вказуємо, які колонки зв'язані з якими властивостями: ColumnTitle.Binding = new Binding("Title"); ColumnYear.Binding = new Binding("Year"); ColumnVolume.Binding = new Binding("Volume"); ColumnNumber.Binding = new Binding("Number"); // Показуємо авторів для обраної книжки: ShowAuthors(); } private void ShowAuthors() { if (bookshelf.Publications.Count <= 0) { DataGridAuthors.ItemsSource = null; return; } int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem); // Якщо індекс хибний, встановлюємо індекс 0 if (index < 0 || index >= bookshelf.Publications.Count) { index = 0; } DataGridAuthors.ItemsSource = null; // Зв'язуємо таблицю DataGridAuthors зі списком авторів: if (bookshelf.Publications[index] is Book book) { ColumnVolume.IsReadOnly = true; ColumnNumber.IsReadOnly = true; DataGridAuthors.ItemsSource = null; DataGridAuthors.ItemsSource = book.Authors; DataGridAuthors.CanUserAddRows = false; // Вказуємо, які колонки зв'язані з якими властивостями: ColumnName.Binding = new Binding("Name"); ColumnSurname.Binding = new Binding("Surname"); DataGridAuthors.ContextMenu = ContextMenuAuthors; } if (bookshelf.Publications[index] is Magazine) { ColumnVolume.IsReadOnly = false; ColumnNumber.IsReadOnly = false; DataGridAuthors.ItemsSource = null; DataGridAuthors.ContextMenu = null; } } private void DataGridPublications_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Відображаємо авторів книжки, обраної в DataGridPublications: ShowAuthors(); } private void MenuItemAddBook_Click(object sender, RoutedEventArgs e) { // Підтверджуємо зміни даних у таблиці: DataGridPublications.CommitEdit(); // Додаємо порожню книжку: bookshelf.AddPublication(new Book("", 0)); InitGrid(); } private void MenuItemAddMagazine_Click(object sender, RoutedEventArgs e) { // Підтверджуємо зміни даних у таблиці: DataGridPublications.CommitEdit(); // Додаємо порожній журнал: bookshelf.AddPublication(new Magazine() { Title = "", Year = 0, Volume = 0, Number = 0 }); InitGrid(); } private void MenuItemRemove_Click(object sender, RoutedEventArgs e) { // Визначаємо індекс активного рядку: int index = DataGridPublications.SelectedIndex; // Підтверджуємо зміни даних у таблиці: DataGridPublications.CommitEdit(); // Видаляємо активний рядок: bookshelf.Publications.RemoveAt(index); DataGridPublications.ItemsSource = null; InitGrid(); if (bookshelf.Publications.Count == 0) { DataGridAuthors.ContextMenu = null; } } private void MenuItemAddAuthor_Click(object sender, RoutedEventArgs e) { int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem); // Якщо індекс хибний, встановлюємо індекс 0 if (index < 0 || index >= bookshelf.Publications.Count) { index = 0; } if (bookshelf.Publications[index] is Book book) { // Підтверджуємо зміни даних у таблиці: DataGridAuthors.CommitEdit(); book.AddAuthor("", ""); ShowAuthors(); } } private void MenuItemRemoveAuthor_Click(object sender, RoutedEventArgs e) { int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem); // Якщо індекс хибний, встановлюємо індекс 0 if (index < 0 || index >= bookshelf.Publications.Count) { index = 0; } if (bookshelf.Publications[index] is Book book) { // Визначаємо індекс активного рядку: int authorIndex = DataGridAuthors.SelectedIndex; // Підтверджуємо зміни даних у таблиці: DataGridAuthors.CommitEdit(); // Видаляємо активний рядок: book.Authors.RemoveAt(authorIndex); DataGridAuthors.ItemsSource = null; ShowAuthors(); } } 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 = XMLHandle.ReadFromFile(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 { XMLHandle.WriteToFile(bookshelf, dlg.FileName); MessageBox.Show("Файл збережено"); } catch (Exception) { MessageBox.Show("Помилка запису в файл"); } } } private void ButtonSortByTitle_Click(object sender, RoutedEventArgs e) { BookshelfProcessorWithLinq.SortByTitles(bookshelf); InitGrid(); } private void ButtonSearch_Click(object sender, RoutedEventArgs e) { string sequence = TextBoxSearch.Text; if (sequence.Length == 0) { return; } // Знаходимо книжки за ознакою: var found = BookshelfProcessor.ContainsCharacters(bookshelf, sequence); // Формуємо рядок результатів: StringBuilder text = new(""); if (found.Count > 0) { foreach (var publication in found) { text.Append(publication.Title + "\n"); } } else { text.Append("Нічого не знайдено"); } // Створюємо нове вікно: WindowSearchResults windowSearchResults = new(); windowSearchResults.TextBoxSearchResults.Text = text.ToString(); windowSearchResults.ShowDialog(); } } }
4 Вправи для контролю
- Увести з клавіатури ім'я певної теки та ім'я файлу. Створити нову теку та новий файл. Вивести на екран імена усіх файлів цієї теки. Видалити файл та теку.
- Увести з клавіатури ім'я певної теки та розширення імені файлів. Вивести на екран імена усіх файлів цієї теки з визначеним розширенням. Якщо тека не існує, вивести повідомлення про помилку.
- Увести з клавіатури ім'я певної теки. Вивести на екран імена усіх підкаталогів цієї теки. Якщо тека не існує, вивести повідомлення про помилку.
- Реалізувати програму, яка обчислює визначений інтеграл методом прямокутників. Для визначення вихідної функції застосувати делегати.
- Реалізувати програму, яка обчислює визначений інтеграл методом трапецій. Для визначення вихідної функції застосувати делегати.
- За допомогою засобів LINQ реалізувати пошук у списку цілих чисел таких які починаються та закінчуються однаковою цифрою (за допомогою представлення у вигляді рядка).
- За допомогою засобів LINQ реалізувати сортування списку цілих чисел за зменшенням останньої цифри (за допомогою представлення у вигляді рядка).
- Засобами WPF створити програму, у якій користувач уводить радіус кола та отримує зображення цього кола у вікні.
- Засобами WPF створити програму, у якій різними кольорами здійснюється малювання трьох концентричних кругів.
5 Контрольні запитання
- У чому полягає призначення неповних (часткових) класів?
- Як здійснюється реалізація неповних (часткових) класів?
- Чи можна використовувати клас
FileInfo
для модифікації теки? - Чи можна використовувати клас
FileInfo
для модифікації атрибутів файлів? - Як програмно створити та видалити теку?
- Для чого і як реалізується зворотний виклик (callback)?
- У чому полягає концепція делегатів?
- Чим делегати відрізняються від указівників на функції мови С++?
- Чим визначається тип функції?
- Як описати тип делегату?
- Які є способи створення екземплярів делегатів?
- Як створити безіменний метод?
- У чому полягає концепція програмування, керованого подіями?
- Як події реалізовані в мові C#?
- У чому полягають концепції функційного програмування?
- У чому полягають концепції декларативного програмування?
- З якою метою до мови C# додана технологія LINQ?
- Яким є тип результату LINQ-запиту?
- У чому є особливості, переваги та недоліки технології Windows Presentation Foundation?
- Для чого і як застосовують XAML?
- Які є особливості компонування візуальних елементів у WPF?
- Які є класи-контейнери у WPF?
- Які є особливості зв'язування даних у WPF?
- Як створити WPF-застосунок у Visual Studio?
- Які є особливості графіки у WPF?