Вы находитесь на странице: 1из 765

МИНОБРНАУКИ РОССИИ

Федеральное Государственное Бюджетное Образовательное Учреждение Высшего


Профессионального Образования
«Московский государственный университет инженерной экологии»
(ФГБОУ ВПО «МГУИЭ»)
Факультет: машиностроительный

Кафедра: САПР

УЧЕБНОЕ ПОСОБИЕ
ЛАБОРАТОРНЫЙ ПРАКТИКУМ

ПРИЛОЖЕНИЕ К ДИПЛОМНОМУ ПРОЕКТУ НА ТЕМУ:

Программное обеспечение автоматизированного учебного курса


«Программное обеспечение САПР»

студента-дипломника Ермакова Р. Г.

Москва 2012 г.
СОДЕРЖАНИЕ

1. Лабораторная работа № 1: Знакомство с Visual Studio 2010 на примере консольного


приложения...............................................................................................................................4

2. Лабораторная работа № 2: Создание простого приложения Windows Forms................32

3. Лабораторная работа № 3: Windows Forms и использование некоторых элементов


управления.................................................................................................................................58

4. Лабораторная работа № 4: Windows Forms — работа с формами...................................79

5. Лабораторная работа № 5: Windows Forms — элементы управления.............................104

6. Лабораторная работа № 6: SolidWorks — работа с SolidWorks.......................................144

7. Лабораторная работа № 7: SolidWorks — использование SwCSharpAddin, работа с


макросом и шаблоном детали..................................................................................................165

8. Лабораторная работа № 8: Создание простого приложения Windows Foundation


Presentation.................................................................................................................................195

9. Лабораторная работа № 9: Создание приложения-презентации Windows Foundation


Presentation.................................................................................................................................236

10. Лабораторная работа № 10: Работа с базами данных — XML и OLE...........................280

11. Лабораторная работа № 11: Динамическая связь приложений через библиотеку классов
....................................................................................................................................................324

12. Лабораторная работа № 12: Автоматизация Microsoft Office Word..............................344

13. Лабораторная работа № 13: Автоматизация Microsoft Office Excel..............................376

14. Лабораторная работа № 14: Простой растровый редактор............................................411

15. Лабораторная работа № 15: Векторный редактор изображений...................................448

16. Лабораторная работа № 16: Windows Communication Foundation.................................517

17. Лабораторная работа № 17: Знакомство с Silverlight......................................................550

18. Лабораторная работа № 18: Знакомство с ASP.NET.......................................................559

19. Лабораторная работа № 19: Расширенная работа с GDI+..............................................656

20. Лабораторная работа № 20: Inventor — работа с Inventor..............................................683

21. Лабораторная работа № 21: Знакомство с языком F# на примере простого приложения


для командной строки..............................................................................................................699

2
22. Лабораторная работа № 22: Различные примеры на F#..................................................717

23. Дополнительная лабораторная работа № 1......................................................................736

24. Дополнительная лабораторная работа № 2......................................................................738

25. Дополнительная лабораторная работа № 3......................................................................747

3
1. Лабораторная работа № 1: Знакомство с Visual Studio 2010 на примере
консольного приложения

Лабораторная работа № 1: Знакомство со средой Microsoft Visual Studio


2010 на примере простого приложения для командной строки

Содержание

1. Вводная часть
2. Знакомство со средой разработки на примере создания простого
консольного приложения
3. Модификация консольного приложения
4. Публикация
5. О приложении к Лабораторной работе № 1

1. Вводная часть

Microsoft Visual Studio — линейка продуктов корпорации Microsoft


(Майкрософт), включающих интегрированную среду разработки (Integrated
Development Environment/Integrated Debugging Environment — IDE)
программного обеспечения и ряд других инструментальных средств.

Microsoft Visual Studio позволяет разрабатывать:

 консольные приложения (приложения для командной строки);


 приложения с графическим интерфейсом (англ. Graphical user interface, GUI),
в том числе с поддержкой технологии Windows Forms;
 библиотеки компонентов для приложений;
 надстройки над различными программными продуктами от Microsoft, таких как
например Microsoft Office Word;
 также веб-сайты, веб-приложения, веб-службы как в родном, так и в
управляемом кодах для всех платформ, поддерживаемых Microsoft Windows,
Windows Mobile, Windows CE, .NET Framework, .NET Compact Framework и
Microsoft Silverlight;
 некоторые другие виды проектов и решений.

Среда включает в себя такие компоненты как редактор исходного кода с


поддержкой технологии IntelliSense (технология автодополнения Microsoft) и
возможностью простейшего рефакторинга кода (процесса изменения внутренней
структуры программы, не затрагивающий её внешнего поведения и имеющий целью
облегчить понимание её работы). Встроенный отладчик может работать как отладчик
уровня исходного кода (программа на каком-либо языке), так и как отладчик
машинного уровня (команды в машинных кодах). Остальные встраиваемые
инструменты включают в себя редактор форм для упрощения создания графического
интерфейса приложения, веб-редактор, дизайнер классов и дизайнер схемы базы
данных. Visual Studio позволяет создавать и подключать сторонние дополнения
(плагины) для расширения функциональности практически на каждом уровне, включая
добавление поддержки систем контроля версий исходного кода (как например,
Subversion и Visual SourceSafe), добавление новых наборов инструментов
(например, для редактирования и визуального проектирования кода на предметно-

4
ориентированных языках программирования или инструментов для прочих аспектов
цикла разработки программного обеспечения (например, клиент Team Explorer для
работы с Team Foundation Server).

Visual Studio включает один или несколько компонентов из следующих (в


зависимости от редакции, например Express или Professional):

 Visual Basic .NET, а до его появления — Visual Basic;


 Visual C++;
 Visual C#;
 Visual F# (включён в Visual Studio 2010);

Многие варианты поставки также включают:


 Microsoft SQL Server либо Microsoft SQL Server Express.

В данном лабораторном практикуме будет использоваться версия Visual Studio


2010 (кодовое имя Hawaii, для Ultimate — Rosario; внутренняя версия 10.0) —
выпущена 12 апреля года вместе с .NET Framework 4. Среда разработки Visual Studio
включает поддержку языков C# 4.0 и Visual Basic .NET 10.0, а также языка F# (Эф-
шарп).

Доступные редакции среды разработки:

 Express — бесплатная среда разработки, включающая только базовый набор


возможностей и библиотек.
 Professional — поставка, ориентированная на профессиональное создание
программного обеспечения, и командную разработку, при которой созданием
программы одновременно занимаются несколько человек (используемая в
Практикуме).
 Premium — издание, включающее дополнительные инструменты для работы и
исходным кодом программ и создания баз данных.
 Ultimate — наиболее полное издание Visual Studio, включающие все доступные
инструменты для написания, тестирования, отладки и анализа программ, а также
дополнительные инструменты для работы с базами данных и проектирования
архитектуры ПО.

Используемую в данном лабораторном практикуме версию Visual Studio 2010


Professional Edition можно получить бесплатно, приняв участие в программе
Microsoft DreamSpark.

Microsoft DreamSpark — программа корпорации Microsoft, предоставляющая


школьникам, студентам и аспирантам бесплатный доступ к инструментам Microsoft для
разработки и дизайна. Изначально программа распространялась на учащихся вузов, в
настоящее время она расширена также на учащихся старших классов. Программа была
анонсирована Биллом Гейтсом 18 февраля 2008 года в Пало-Альто, США.

5
Рис. 1. 1. Получение среды разработки на сайте DreamSpark.com
(http://www.dreamspark.com)

2. Знакомство со средой разработки на примере создания простого


консольного приложения

После первого запуска Visual Studio 2010, откроется Начальная страница:

6
Рис. 2. 1. Начальная страница Visual Studio 2010 Professional (русская версия)

Для начала, надо создать пустой проект, для этого выполним последовательно:
Файл -> Создать -> Проект… (также можно просто нажать сочетание клавиш
Ctrl+Shift+N или пункт «Создать проект…» на Начальной странице):

Рис. 2. 2. Создание нового проекта

Откроется окно создания проекта и выбора необходимых нам параметров. Что


же такое Проект?

7
Проект (Project) используется в Visual Studio для логической группировки
нескольких файлов, содержащих исходный код, на одном из поддерживаемых языков
программирования, а также любых вспомогательных файлов (ресурсы для проекта: это
могут быть и дополнительные библиотеки, файлы изображений, иконок и прочее).
Обычно после сборки проекта (которая включает компиляцию всех входящих в проект
файлов исходного кода) создается один исполняемый модуль (исполняемый файл),
либо несколько, если это веб-приложение.
Фактически, Проект это структурированная директория под конкретную
программу написанную программистом и содержащая версии сборки
скомпилированной программы (по конфигурациям по умолчанию: Release или Debug).

Решение же это просто объединение нескольких проектов под одним «именем»


и параметрами.

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Консольное приложение. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

Рис. 2. 3. Окно создания нового проекта

В поле Имя вводим LWP01Console — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект
(значение «по умолчанию» можно поменять, выполнив действия: Сервис ->

8
Параметры… -> Проекты и решения -> меняем путь в поле Размещение
проектов). Выберем расположение удобное для быстрого поиска. В поле Имя
решения вводится либо название программы «по умолчанию» из поля Имя
автоматически, либо можно ввести своё собственное. Под этим именем будет создана
конечная папка проекта (если Имя и Имя решения разные).

Рис. 2. 4. Вводим данные нового проекта консольного приложения

После нажатия клавиши ОК, мы увидим сформированный проект и исходный код


консольного приложения (не пустого изначально).

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

Как видим, среда разработки сформировала один файл Program.cs с исходным


кодом следующего содержания:

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

namespace LWP01Console
{
class Program
{
static void Main(string[] args)
{
}
}
}

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 или


Отладка -> Начать отладку. Если программист хочет запустить программу без
отладки и уверен что программа не нуждается в поиске ошибок и оптимизации кода, то
можно нажать Отладка -> Запуск без отладки.
По умолчанию клавиша отладки вынесена на панель инструментов вверху.
Запускаем приложение в режиме отладки (и компиляции debug-версии программы)

нажав на иконку (Debug выбрано изначально).

10
Рис. 2. 5. Запуск консольного приложения по конфигурации Debug

Рассмотрим подробнее наше приложение. Для начала обратим внимание на


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

Рис. 2. 6. Обозреватель решений: состав проекта консольного приложения

В поле Ссылки можно увидеть все текущие библиотеки изначально


подключённые к проекту. Здесь можно добавлять ссылки, для этого правой кнопкой
мыши кликнем на пункт Ссылки -> Добавить ссылку…:

11
Рис. 2. 7. Окно Добавить ссылку

В Properties окна обозревателя решений находится файл свойств (или файл


сборки) нашего консольного приложения (AssemblyInfo.cs), которые будут
ассоциированы с ним после компиляции. Содержание следующее:

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// Управление общими сведениями о сборке осуществляется с помощью


// набора атрибутов. Измените значения этих атрибутов, чтобы изменить сведения,
// связанные со сборкой.
[assembly: AssemblyTitle("LWP01Console")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LWP01Console")]
[assembly: AssemblyCopyright("Copyright © 2011")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Параметр ComVisible со значением FALSE делает типы в сборке невидимыми


// для COM-компонентов. Если требуется обратиться к типу в этой сборке через
// COM, задайте атрибуту ComVisible значение TRUE для этого типа.
[assembly: ComVisible(false)]

// Следующий GUID служит для идентификации библиотеки типов, если этот проект будет
видимым для COM
[assembly: Guid("eb30278d-af90-4a61-a3e4-d64ba45f6b7c")]

// Сведения о версии сборки состоят из следующих четырех значений:


//
// Основной номер версии
// Дополнительный номер версии
// Номер построения
// Редакция
//
// Можно задать все значения или принять номер построения и номер редакции по умолчанию,

12
// используя "*", как показано ниже:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Содержание пунктов говорит само за себя, здесь можно менять все атрибуты
нашего приложения, включая версию приложения и описание.
Управлять напрямую (то есть менять значения в самом файле) не обязательно.
Для этого есть инструмент, который можно активировать так: правой кнопкой мыши
жмём на название проекта LWP01Console, далее выбираем Свойства (Alt+Enter).
Откроется окно свойств текущего проекта (в решении):

Рис. 2. 8. Свойства: LWP01Console

Жмём на Сведения о сборке…:

13
Рис. 2. 9. Окно Сведения о сборке консольного приложения

Некоторые из свойств сборки можно посмотреть сразу после компиляции в окне


свойств приложения:

14
Рис. 2. 10. Свойства консольного приложения (скомпилированного)

Вернёмся в среду разработки:

При выделении какого либо объекта в обозревателе решений можно увидеть его
текущие свойства и установки для данного проекта. Для пространство имён
Microsoft.Csharp это выглядит так:

15
Рис. 2. 11. Свойства объекта

3. Модификация консольного приложения

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


удобства прочтения исходного кода. Для этого выполним Сервис -> Параметры… ->
Текстовый редактор -> C# -> Показывать: Номера строк.
Теперь слева в окне исходного кода отображаются номера строк для кода C#:

16
Рис. 3. 1. Отображение номера строки

Теперь модифицируем исходный код. Наиболее простая модификация, это вывод


какой-либо фразы в консольном приложении. Сделаем это:

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

namespace LWP01Console
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\
nThis is my first application using C#!");
Console.ReadKey();
}
}
}

Мы добавили всего три строчки:

Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\


nThis is my first application using C#!");
Console.ReadKey();
Console.Clear();

Скомпилируем это приложение, в результате увидим следующее (Рис. 3. 2.):

17
Рис. 3. 2. Модифицированное консольное приложение

Теперь усложним приложение, добавив следующий исходный код:

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

namespace LWP01Console
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\
nThis is my first application using C#!");
Console.ReadKey();
Console.Clear();
//
Console.WriteLine("Пожалуйста, введите символы:\n");
String a;
a = Console.ReadLine();
Console.WriteLine("\n\tВы ввели символ: " + a + ".");
Console.ReadKey();
Console.Clear();
//
Console.WriteLine("Пожалуйста, введите число элементов массива:\n");
int x = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("\nПожалуйста, введите элемент массива:\n");
string[] m1 = new string[x];
int i;

for (i = 0; i < m1.GetLength(0); i++)


{
Console.Write("Элемент {" + i + "}: ");
m1[i] = Console.ReadLine();
}
Console.WriteLine("\n\tВаш массив:");

for (i = 0; i < m1.GetLength(0); i++)


{

18
Console.Write("\t" + m1[i]);
Console.WriteLine();
}
Console.ReadKey();
}
}
}

Скомпилируем его и проверим работоспособность. Теперь поясним исходный


код. Для удобства, исходный код поделим на части символом комментариев (//).

Теперь программа выполняет три итерации. Первая итерация сначала выводит


строку (Рис. 3. 2.). Вторая итерация предлагает нам ввести эту строку самим, затем
выводит её на экран. И третья итерация позволяет нам ввести число элементов
одномерного массива, заполнить его и тоже вывести на экран. После каждой итерации
экран очищается.

Рис. 3. 3. Результат работы третьей итерации работы консольного приложения

Поясним исходный код к каждой итерации. Ключевые слова подсвечены синим.


В самом начале кода мы видим следующую строку:

using System;

Библиотеки классов .NET Framework образуют иерархическую структуру


пространств имён (namespaces). Это может привести к довольно длинным именам,
если требуемый класс или тип имеет очень большую глубину вложения. Чтобы
сократить ввод длинных имён, в C# введена директива using. Посмотрим работу этой
директивы на примере. В нашем приложении есть такая строка:

Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\nThis is my


first application using C#!");

Если бы изначально мы не подключали ссылки, эта строка выглядела бы так:

System.Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\nThis


is my first application using C#!");

19
Ввод такого текста несложен, а представим, что у нас большое приложение с
обширным набором типов и классов. Директива using позволяет компилятору найти
непонятный ему элемент, просматривая указанное пространство имен. В данной работе
директива using используется так:

using System;

namespace LWP01Console
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\
nThis is my first application using C#!");

В ходе синтаксического анализа метода Console.WriteLine компилятор


установит, что этот метод не описан. Поэтому он примется просматривать пространства
имён, заданные директивой using, и, найдя описание метода в пространстве имён
System, завершит компиляцию кода без ошибок.

И так, директива using это зарезервированное слово в языке Си-шарп. Она


используется в двух случаях:

 разрешает использование типов в пространстве имён, поэтому уточнение


использования типа в этом пространстве имён не требуется: using System.Text;
 позволяет создавать псевдонимы пространства имен или типа. Это называется
директива using alias и выглядит примерно так: using Project =
PC.MyCompany.Project;

Ключевое слово using также используется для создания операторов using,


которые обеспечивают правильную обработку объектов IDisposable, например файлов
и шрифтов.

Как видим, также как и в C и C++ язык унаследовал символ «;» (точка с
запятой) как символ разделения операторов, однако можно писать код и без этого
разделителя (частично).

Директива using применима к пространствам имён, но не к классам. В нашем


примере System является пространством имён, Console — классом, a WriteLine —
статическим методом класса Console. Поэтому такой код неверен:

using System.Console; // Нельзя использовать директиву using по отношению к классу


//...
namespace LWP01Console
{
class Program
{
static void Main(string[] args)
{
WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\nThis is my
first application using C#!");

Указывать класс в директиве using нельзя, но существует вариант директивы


using, позволяющий создать псевдоним для класса:

20
using <псевдоним> = <класс>;

Благодаря этой форме директивы using можно написать подобный код:

using Console2011 = System.Console;


//...
class Program
{
static void Main(string[] args)
{
Console2011.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\
nThis is my first application using C#!");

Немного об универсальном программировании:

Описание класса в С# должно содержать встроенное (inline) описание всех


методов — в этом языке заголовочных файлов не бывает (в отличие например от C и
C++). Благодаря этому правилу разработчики классов могут создавать высоко
мобильный код, что является одной из ключевых концепций среды .NET. Создавая Си-
шарп-класс, в конце концов, разработчик имеет полностью инкапсулированную связку
функциональных возможностей, которую можно легко перенести в любую среду
разработки, и ему не нужно беспокоиться, как в другом языке обрабатываются
включаемые файлы, и существует ли в нём вообще механизм включения файлов. Такой
подход — универсальное программирование («one-stop programming») — позволяет,
например, перебросить весь класс на страницу ASP (Active Server Pages), и он будет
функционировать так, будто был скомпилирован для обычного приложения.

namespace LWP01Console
{
class Program

Здесь мы видим имя класса и пространства имён. Автоматически (при создании


консольного приложения средой разработки) было выбрано пространство имён
LWP01Console, и имя класса, который описывает проблемную область. В нашем
случае это Program. Начало и окончание описания класса отмечается фигурными
скобками — открывающей «{» и закрывающей «}» соответственно. Всё, что внутри
них, считается частью класса. Заметим: в нашем случае класс называется Program, а
всё, что есть в приложении, описано в контексте этого класса.
Все члены класса описываются внутри фигурных скобок: методы, поля,
свойства, индексаторы (indexers), атрибуты и интерфейсы.

Создание пространства имён указывается ключевым словом namespace.


Объявляемые пространства имён могут использоваться для структурирования
программы. Например:

namespace Name01.Name02
{
class A { }
}

namespace Name03
{
using Name01.Name02;
class B : A { }
}

Идём дальше по коду:

21
static void Main(string[] args)

Каждое приложение на C# должно иметь метод с именем Main(), описанный в


одном из его классов. Не имеет значения, в каком классе находится этот метод:
классов, может быть столько, сколько нужно для вашего приложения. Главное, чтобы в
каком-нибудь классе был метод с именем Main. Кроме того, этот метод должен быть
описан как открытый (public) и статический (static). Ключевое слово public является
модификатором доступа, который сообщает компилятору Си-шарп, что этот метод
может вызвать кто угодно. Ключевое слово static означает, что метод является
глобальным и что для его вызова не требуется создавать экземпляр этого класса. Это
очень важно, так как иначе компилятор не знал бы, как или когда создавать экземпляр
вашего класса. Раз метод статический, компилятор сохранит адрес метода как точку
входа, и среда .NET начнёт с него выполнение приложения.

Следующие строчки кода таковы:

Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\nThis is my


first application using C#!");
Console.ReadKey();
Console.Clear();

Если System — это пространство имён, Console — класс, определённый в этом


пространстве, то метод, WriteLine1 — метод определённый в этом классе.

Класс Console содержит следующие методы, позволяющие осуществлять чтение


и запись символов из потоков ввода/вывода:

 Read — чтение символов из потока ввода.


 ReadLine — чтение строки символов из потока ввода.
 Write — запись строки символов в поток вывода.
 WriteLine — запись в поток вывода строки символа, ограниченной символами
конца строки.

ReadKey — получает следующий нажаты пользователем символ или


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

Clear — удаляет из буфера консоли и её окна отображаемую информацию.

ПРИМЕЧАНИЕ № 1: Описания применяемых в данной работе методов также


можно посмотреть, наведя курсор на нужное слово методе.

//
Console.WriteLine("Пожалуйста, введите символы:\n");
String a;
a = Console.ReadLine();
Console.WriteLine("\n\tВы ввели символ: " + a + ".");
Console.ReadKey();
Console.Clear();

Двойной правый слеш — комментарии (зелёные). Также комментарии можно


задавать так:

/* Текст комментария */

22
В остальном, здесь всё просто. Объявляем переменную «a» (строковая — String
или string), выполняем считывание символов с клавиатуры (ReadLine) и выводим
введённый символ в потоке с переводом строки. Конструкция «+ a +» служит для
подстановки переменной «a» в поток символов для вывода в окне консольного
приложения.
\n определяется как перевод курсора на следующую строку.
\t определяется как символ табуляции в окне консоли.

Последний участок кода (третья итерация):

//
Console.WriteLine("Пожалуйста, введите число элементов массива:\n");
int x = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("\nПожалуйста, введите элемент массива:\n");
string[] m1 = new string[x];
int i;

for (i = 0; i < m1.GetLength(0); i++)


{
Console.Write("Элемент {" + i + "}: ");
m1[i] = Console.ReadLine();
}
Console.WriteLine("\n\tВаш массив:");

for (i = 0; i < m1.GetLength(0); i++)


{
Console.Write("\t" + m1[i]);
Console.WriteLine();
}
Console.ReadKey();
Console.Clear();

Здесь идёт объявление переменной «x» и присваивание отконвертированного


числа введённого с клавиатуры в int (32-битное число со знаком). Конвертирование
производится методом Convert (преобразует значение одного базового типа данных к
другому базовому типу данных), который использует ToInt32 (преобразует заданное
строковое представление числа в эквивалентное 32-разрядное знаковое целое число).
Далее мы объявляем строковый одномерный массив (string[]) где число элементов
задаётся по числу в переменной «x» (индекс начинается с нуля) и в цикле (ключевая
последовательность i < m1.GetLength(0), где метод получает 32-разрядное целое
число, представляющее количество элементов в заданном измерении массива — в
нашем массиве одно измерение, которое мы пдоставляем в метод под цифрой «0»). В
теле цикла мы последовательно вводим символы элемента массива (m1[i]). Второй
цикл делает обратную операцию6 выводит нш массив.

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

Во-первых, C# позволяет писать код свободно, но каждый оператор должен


заканчивать точкой с запятой2. Также можно написать код приложения даже в одну
строчку2.

Во-вторых, C# чувствителен к регистру. А это значит что «ConsolE», «Static» и


«Public» могут вызвать ошибки. Также важно понимать, что это касается и переменных
и операторов.

23
ПРИМЕЧАНИЕ № 2: Однако, не всегда точка с запятой обязана быть.
Например, конструкция без точек с запятой ниже, выдаст сообщение с тремя кнопками.
Вставим этот код, в наше консольное приложение предварительно добавив ссылку
System.Windows.Forms (Рис. 3. 4.):

Рис. 3. 4. Добавление новой ссылки System.Windows.Forms

while (System.Windows.Forms.MessageBox.Show("Hello World without ';'! Привет мир без


точек с запятой!", "This is My First Message Box!",
System.Windows.Forms.MessageBoxButtons.YesNoCancel) !=
System.Windows.Forms.DialogResult.Yes){}

Рис. 3. 5. Результат работы кода без точек с запятой (нужно нажать «Да»)

4. Публикация

Наше приложение скомпилировано, прошло отладку и проверено на


работоспособность. Оно готово к выпуску. Финальная стадия разработки начинается и
заканчивается публикацией. Перед публикацией можно также позаботиться о
безопасности приложения (актуально для Windows Forms и Windows Foundation

24
Presentation). В частности вкладка свойств проекта LWP01Console Безопасность
выглядит так:

Рис. 4. 1. Безопасность проекта

Немного о технологии ClickOnce:

ClickOnce — это технология разработанная корпорацией Microsoft для


«развёртывания» приложений Windows Forms и Windows Foundation Presentation.
ClickOnce позволяет пользователю устанавливать и запускать Windows
приложение кликая по ссылке на веб-странице, либо в сетевом окружении. Основной
принцип ClickOnce — простое развертывание Windows-приложений пользователем.
Кроме того, ClickOnce нацелена на решения трех других проблем связанных с обычной
моделью развертывания:

 сложность в обновлении развертываемого приложения;


 воздействие приложения на компьютер пользователя;
 необходимость административных полномочий для установки приложения.

ClickOnce-приложения изолированы друг от друга, и одно приложение не может


повлиять на работу других.

Теперь перейдём к публикации (вкладка Публикация):

25
Рис. 4. 2. Публикация проекта

Есть два способа закончить публикацию. Ручной, с выбором всех необходимых


параметров (Рис. 4. 2.) начиная от установки места публикации (веб-узел, ftp-сервер
или путь к файлу), и заканчивая настройкой проверки обновлений у приложения и
выбора место откуда «грабить» обновления для приложения. Полуавтоматический
(упрощённый), который мы используем по нажатию кнопки Мастер публикаций….

Шаг 1 (выбор места публикации):

26
Рис. 4. 3. Мастер публикаций: Место публикации приложения

Шаг 2 (выбор способа установки приложения пользователем):

27
Рис. 4. 4. Мастер публикаций: Способ установки приложения пользователями

Шаг 3 (указание возможности обновления приложения):

28
Рис. 4. 5. Мастер публикаций: Место, где приложение будет искать обновления

Последний шаг:

29
Рис. 4. 6. Готов к публикации

То, что создалось после работы мастера публикаций в конечной директории


публикации указанной в самом начале:

Рис. 4. 7. Файлы публикации (созданная директория D:\Проекты\CS\Application


Files)

Рис. 4. 8. Файлы публикации (в конечной директории D:\Проекты\CS)

30
Запускаем Setup.exe:

Рис. 4. 9. Установка нашего консольного приложения

Жмём Установить и приложение поселится (по умолчанию) на рабочем столе


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

Удаление установленного приложения:

31
Рис. 4. 10. Удаление установленного приложения (Панель управления ->
Программы и компоненты)

5. О приложении к Лабораторной работе № 1

Получившуюся программу (LWP01Console.exe), собранную из кусков кода


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

Приложение № 1: Исходный код программы и всех сопровождающих


файлов с кодом приведён по ссылке в конце этого материала (сслыка доступна в
программном продукте).
Приложение № 2: Обычный шаблон программы на языке С# приведён по
ссылке в конце этого материала (сслыка доступна в программном продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

2. Лабораторная работа № 2: Создание простого приложения Windows Forms

Лабораторная работа № 2: Создание простого приложения Windows


Forms

Содержание

1. Вводная часть
2. Создание простого приложения Windows Forms
3. Модификация приложения Windows Forms
4. Завершающая часть
5. О приложении к Лабораторной работе № 2

1. Вводная часть

Windows Forms представляет собой одну из двух технологий, используемую в


Visual C# для создания интеллектуальных клиентских приложения на основе Windows,
выполняемых в среде .NET Framework. Технология Windows Forms специально

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

 Развёртывание ClickOnce.
 Обширная поддержка баз данных, благодаря элементу управления
DataGridView (Windows Forms).
 Панели инструментов и другие элементы пользовательского интерфейса,
которые могут иметь внешний вид и поведение Microsoft Windows XP/7, Microsoft
Office или Microsoft Internet Explorer.

Windows Forms предоставляет для проекта такие компоненты, как диалоговые


окна, меню, кнопки и многие другие элементы управления, являющиеся частью
стандартного пользовательского интерфейса (UI) Windows. По существу, эти элементы
управления являются просто классами из библиотеки .NET Framework. Конструктор в
Visual Studio позволяет перетаскивать элементы управления в основную форму
приложения и изменять их размеры и расположение. После этого IDE (среда
разработки) автоматически добавит исходный код для создания и инициализации
экземпляра соответствующего класса.

2. Создание простого приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

33
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP02WindowsForms01 — это название программы


(выбрано по названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория где будет находиться весь проект
(значение «по умолчанию» можно поменять выполнив действия: Сервис ->
Параметры… -> Проекты и решения -> меняем путь в поле Размещение
проектов). Выберем расположение удобное для быстрого поиска. В поле Имя
решения вводится либо название программы «по умолчанию» из поля Имя
автоматически, либо можно ввести своё собственное. Под этим именем будет создана
конечная папка проекта (если Имя и Имя решения разные).

34
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

35
Рис. 2. 4. Исходный код приложения Windows Forms сформированного средой
разработки (файл Program.cs)

Рис. 2. 5. Обозреватель решений: состав проекта приложения Windows Forms


сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

36
Рис. 2. 6. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms

Цель данной работы, это показать основные приёмы по работы с формами.


Поэтому соберём простенькую программу калькулятора выполняющего основные
математические операции и выводящие результат в окне программы.

Что необходимо реализовать:

1. Цифры будут вводиться нажатием кнопки с обозначением соответствующей


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

Стоит отметить, что реализаций «калькуляторов» за время существования С#


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

37
Первое что необходимо отметить по сравнению с консольным приложением это
добавление новых инструментов в окне среды разработки. Теперь у нас есть
«визуальная форма» (Рис. 3. 1), на которой можно размещать любые доступные
элементы из специальной панели объектов которая называется Панель элементов (по
умолчанию находится слева сбоку на границе среды разработки):

Рис. 3. 1. Панель элементов: расположение по умолчанию

Изначально, её положение весьма неудобно (она появляется и исчезает при


получении фокуса мышки и её снятия, чем перекрывает поле формы), поэтому её
можно закрепить нажав значок кнопки в шапке панели:

38
Рис. 3. 2. Закреплённая панель элементов

Добавление элементов — простой процесс. Например нам нужно добавить


Текстовое поле редактирования. Для этого выделяем на панели элементов элемент
с названием TextBox и наводим мышку на любое место нашей формы. Указатель
изменится на перекрестие с иконкой от TextBox. Далее нажимаем на точку
«добавления» прямоугольника текстового поля редактирования и протягиваем мышку,
тем самым расширяя добавленное поле.

39
Рис. 3. 3. Добавленное текстовое поле редактирования (сверху) и процесс добавления
(снизу)

Процесс расстановки элементов можно упростить. «На глаз» ставить элементы


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

Рис. 3. 4. Направляющие линии при позиционировании элемента (синие)

Также для позиционирования элементов существует специальная панель


инструментов:

Теперь немного о свойствах элементов. Естественно что все значимые свойства


элементов настраиваемы. Свойство очень много, поэтому остановимся на тех, что будут
необходимы в данной работе лабораторного практикума.
Свойства всех элементов отображаются на соответствующей панели Свойства.
Для отображения свойства элемента достаточно выбрать необходимый установленный
элемент в выпадающем списке:

40
Рис. 3. 5. Выбор свойств для элементов: пока у нас всего два элемента — сама форма
(с именем: Form1) и текстовое поле редактирования (с именем: textBox1)

Также можно выделить этот необходимый элемент на форме 2 и нажатием правой


кнопки мыши по нему выбрать Свойства1.

ПРИМЕЧАНИЕ № 1: В лабораторных работах данного практикума запись


значений для полей свойств элементов управления иногда такова: «Имя свойства:
<Значение>.» — точка после закрывающей кавычки не относится к значению, поэтому
не стоит заносить скажем в поле принимающее только целое числовое значение ещё
точку. Среда выдаст ошибку либо уберёт точку.

Пример (задание точки в имени элемента):

41
Рис. 6. 2. Ошибочное задание поля (Name)

ПРИМЕЧАНИЕ № 2: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

И так, для начала изменим свойства самой формы. Для этого перейдём в
свойства Form1.cs. Нам нужны следующие поля (информация о значении поля можно
получить на панели свойств ниже на тёмно сером поле):

(Name) изменим с Form1.cs3 на LWP02Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Простой
калькулятор (C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico4.
FormBorderStyle изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.

42
ПРИМЕЧАНИЕ № 3: Для того, чтобы поменять имя файла нашей формы,
необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

ПРИМЕЧАНИЕ № 4: Для добавления нового элемента в проект можно сделать


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

проекта ( ), затем выполнить: Добавить -> Создать элемент…


(или нажать сочетание клавиш Ctrl+Shift+A). Далее необходимо выбрать нужный нам
элемент из списка:

Рис. 3. 6. Окно Добавление нового элемента – LWP02WindowsForms01

В нашем случае нам нужна иконка. Выбираем Файл значка, вводим внизу его
Имя и жмём Добавить.

Наш значок появился в обозревателе решений справа. Но нас может не устроить


его вид или количество цветов (по умолчанию выбрано 4 бита цветности!). Дважды
нажимаем на значок. Попадаем в местный Редактор значков. Правой кнопкой мыши
нажимаем на любой место редактора и выбираем из раскрывающегося списка пункт
Создать тип изображений… (клавиша Ins).

43
Рис. 3. 7. Создание нового типа изображения для значка

Теперь выберем тип (в списке Конечный тип изображений выбираем: 32x32,


24-разрядное):

44
Рис. 3. 8. Создание типа изображения для значка приложения

Рисуем значок. Лучше всего сделать два значка (32х32, 24-разрядное и 16х16,
24-разрядное), чтобы отображался и маленький значок и более крупный (маленький к
примеру будет отображаться в шапке приложения, а также в режиме Маленькие
значки у Проводника Windows). Разумеется, старые версии значка необходимо
удалить (правая кнопка мыши по значку -> Удалить тип изображений).
Остался последний штрих. Нужно проассоциировать нашу программу форму со
значком. Для этого переходим в свойства формы, ищем пункт Icon и нажимаем на
«…». Ищем через Проводник нужный файл значка в корневом каталоге проекта и
нажимаем Открыть.
В итоге получим примерно следующее:

Для изменения иконки всего приложения (будет отображаться непосредственно


в Проводнике делаем следующее: открываем свойства нашего проекта (выделяем
, далее нажимаем правую кнопку мыши и в выпадающем меню
ищем Свойства либо жмём Alt+Enter). Теперь переходим на вкладку Приложение.
Нам нужен пункт: Значок и манифест. В выпадающем списке Значок выберем тот,
что уже добавлен в проект: LWP02Icon.ico (Рис. 3. 9):

45
Рис. 3. 9. Выбор значка для приложения

Скомпилируем теперь это приложение, в результате увидим следующее (Рис. 3.


10.):

Рис. 3. 10. Заготовка для приложения Windows Forms

Теперь отредактируем свойства текстового поля редактирования:

46
TextAlign5: изменим с Right на Center
^ Изменим положение курсора и вывода текста в поле.

ПРИМЕЧЕНИЕ № 5: Если необходимо восстановить значение по умолчанию,


нужно дважды нажать на пункт, который нужно вернуть к первоначальному значению
(например двойным нажатием на TextAlign возвращаем значение Right). Если значений
в поле больше двух (или значение не выбирается, а меняется вводом с клавиатуры), то
первоначальным считается то, что не выделено жирным текстом. Также работает
следующее (для вводимых значений): выбираем поле которое мы изменили, нажимаем
на нём правую кнопку мыши, далее жмём на Сброс.

ReadOnly: изменим с False на True


^ Делаем поле неизменяем «извне» программы. Значение из поля можно только
копировать.
(Name): изменим с textBox1 на ResultBox

Осталось только расставить оставшиеся элементы калькулятора. Ими станут


кнопки (Button) с панели элементов и ещё элемент NumericUpDown (числовой
«ползунок»).

NumericUpDown: элемент позволяет двигаться по числовому ряду (зависит от


выбранного шага движения) при помощи нажатия стрелки вверх или вниз, либо ввода
числа из этого диапазона. Определяющие свойства у элемента следующие:
Hexadecimal (True): определяет вывод числа в шестнадцатеричной форме
(иначе в десятичной по умолчанию).
Maximun и Minimun задают диапазон числового ряда. Increment задаёт шаг
по этому ряду.
ThousandSeparator (True): разделяет группы цифр (тысячи).
DecimalPlaces: число отображаемых знаков после запятой.
Value: текущее предустановленное число.

Рис. 3. 11. Панель элементов: элемент управления NumericUpDown

Расставляем четыре кнопки математических действий, 10 кнопок цифр от 0 до 9,


одну кнопку «запятой», одну кнопку «очистить», одну кнопку «=» («вычисление»),
одну кнопку «Округлить», и элемент NumericUpDown справа от «Округлить»:

47
Рис. 3. 12. Готовый шаблон приложения Windows Forms

Теперь осталось добавить события по нажатию кнопок и весь необходимый код.

Кнопки цифр:
(Name): B0…B9 Text: 0…9

Кнопка «запятая»:
(Name): BD. Text: ,

Кнопка «очистить»:
(Name): BC Text: C

Кнопка «=»:
(Name): BResult Text: =

Кнопки действий:
(Name): BOperation1 Text: +
(Name): BOperation2 Text: -
(Name): BOperation3 Text: *
(Name): BOperation4 Text: /

Кнопка «Округлить»:
(Name): BSpecial Text: Округлить

Элемент NumericUpDown:
(Name): NumericSpecial Maximum: 5 Minimum: 0 Increment: 1

Для добавления события нажатия для кнопки, необходимо дважды кликнуть на


соответствующую кнопку, либо перейти в свойства кнопки и нажать на значок
«молнии» (События):

48
Рис. 3. 13. Переключение со страницы свойств кнопки на страницу событий

Нас интересует событие Click. Дважды нажимаем мышкой на слово Click (Рис. 3.
14):

Рис. 3. 14. Событие Click для кнопки B1.

Код добавленного события такой (пока что он пуст):

private void B1_Click(object sender, EventArgs e)


{

49
}

Добавим сюда наш код для кнопки «1»:

private void B1_Click(object sender, EventArgs e)


{

if (Clear == true)
{
ResultBox.Clear();
Clear = false;
Dot = false;
}

if (Operation1 == false && Operation2 == false && Operation3 == false &&


Operation4 == false)
{
ResultBox.AppendText("1");
A = Convert.ToDouble(ResultBox.Text.Replace(".", ","));
}
else
{
ResultBox.AppendText("1");
B = Convert.ToDouble(ResultBox.Text.Replace(".", ","));
}
}

Теперь необходимо объявить переменные, которые есть в коде выше. При


добавлении этого кода видно, что среда уже заметила ошибки (а именно нет этих
самых переменных).

Найдём строчку кода в этом же файле (LWP02Main.cs):

public partial class LWP02Main : Form


{

Добавим после (6 переменных типа bool, и три типа double):

Boolean Operation1 = false;


Boolean Operation2 = false;
Boolean Operation3 = false;
Boolean Operation4 = false;
Boolean Clear = false;
Double A;
Double B;
Double Result;
Boolean Dot = false;

Добавим по аналогии (с кнопкой «1» код для всех цифровых кнопок). Затем
добавим код для кнопки «=»:

private void BResult_Click(object sender, EventArgs e)


{

if (Operation1 == true)
Result = A + B;
if (Operation2 == true)
Result = A - B;
if (Operation3 == true)
Result = A * B;

50
if (Operation4 == true)
Result = A / B;
ResultBox.Text = Result.ToString();
Operation1 = false;
Operation2 = false;
Operation3 = false;
Operation4 = false;
Clear = true;
}

Кнопка «очистить»:

private void BC_Click(object sender, EventArgs e)


{
ResultBox.Clear();
Operation1 = false;
Operation2 = false;
Operation3 = false;
Operation4 = false;
Clear = false;
A = 0;
B = 0;
Result = 0;
Dot = false;
}

Кнопка операции «сложение»:

private void BOperation1_Click(object sender, EventArgs e)


{
Operation1 = true;
Operation2 = false;
Operation3 = false;
Operation4 = false;
Dot = true;
ResultBox.Clear();
}

Кнопка «вычитание»:

private void BOperation2_Click(object sender, EventArgs e)


{
Operation1 = false;
Operation2 = true;
Operation3 = false;
Operation4 = false;
Dot = true;
ResultBox.Clear();
}

«Умножение» и «деление»:

private void BOperation3_Click(object sender, EventArgs e)


{
Operation1 = false;
Operation2 = false;
Operation3 = true;
Operation4 = false;
Dot = true;
ResultBox.Clear();
}

51
private void BOperation4_Click(object sender, EventArgs e)
{
Operation1 = false;
Operation2 = false;
Operation3 = false;
Operation4 = true;
Dot = true;
ResultBox.Clear();
}

Кнопка для ввода дробных чисел (с точкой). Код этой кнопки такой:

private void BD_Click(object sender, EventArgs e)


{

if (Clear == true)
{
ResultBox.Clear();
Clear = false;
Dot = false;
}

if (Operation1 == false && Operation2 == false && Operation3 == false &&


Operation4 == false)
{

if (Dot == false)
{
ResultBox.AppendText(",");
Dot = true;
}
}
else
{

if (Dot == true)
{
ResultBox.AppendText(",");
Dot = false;
}
}
}

Кнопка Округлить:

private void BSpecial_Click(object sender, EventArgs e)


{
SByte d = Convert.ToSByte(NumericSpecial.Value); // 8-битное целое число со
знаком d = конвертируем число из NumericSpecial
/* Выводим в главное текстовое поле округлённый результат
* Округление выполняет метод Round() из класса Math, принимая округляемый
Result и число, до которого выполняется округление (количество дробных разрядов) */
ResultBox.Text = Convert.ToString(Math.Round(Result, d));
}

4. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан


ниже (Рис. 4. 1):

52
Рис. 4. 1. Модифицированное приложение Windows Forms

Поясним исходный код, а точнее принцип работы кода. Ключевые слова


подсвечены синим. Типы CLR (Common Language Runtime) таким цветом6.

ПРИМЕЧАНИЕ № 6: Отметим ещё раз, что разницы между string и String нет. В
первом случае это обозначение переменной как ключевого слова (это alias для String).
Во-вторых мы имеем дело с общепринятым типом CLR для разных языков .NET.

Основной рабочий файл, который был отредактирован нами это файл


LPW02Main.cs (перейти к исходному коду можно нажав на него правой кнопкой
мышки и выбрав в раскрывающемся меню пункт Перейти к коду либо нажав клавишу
F7 после выделении мышкой). Файл начинается стандартно, с подключения ссылок и
задания пространства имён. Дальше идёт объявления класса (LWP02Main) и типа
формы (Form) (класс по ключевому слову partial:

namespace LWP02WindowsForms01
{
public partial class LWP02Main : Form
{
Boolean Operation1 = false;
Boolean Operation2 = false;
Boolean Operation3 = false;
Boolean Operation4 = false;
Boolean Clear = false;
Double A;
Double B;
Double Result;
Boolean Dot = false;

public LWP02Main()
{
InitializeComponent();
}

Касательно ключевого слова partial. В языке C# возможно разбиение


определения класса, структуры или интерфейса между двумя или больше исходными

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

1. При работе над большим проектом, разбиение класса на несколько файлов


позволяет нескольким программистам работать над ним одновременно.
2. При работе с автоматически генерируемыми исходными кодами, код может быть
добавлен в класс без необходимости пересоздавать исходный файл. Visual Studio
использует этот подход при создании компонентов Windows Forms, веб-сервисов
и прочее. Возможно создать код, который использует эти классы без
необходимости редактировать файлы, которые создает Visual Studio.

Для такого разбития класса используется модификатор partial.

Дальше основной метод нашей формы (LWP02Main) и вложенный в него метод


InitializeComponent(). Это автоматически подставляемый обязательный метод для
поддержки конструктора класса. Фактически конструктор всех компонентов формы.
Можно просмотреть содержимое метода в другом файле (об этом ниже).

Далее идёт объявление переменных. Всего их 9, шесть из которых имеют


логический тип («правда» или «ложь»), оставшиеся три — тип double7
(приблизительный диапазон чисел от ±5,0 x 10−324 до ±1,7 x 10308). Переменные A и B
служат для хранения временного слагаемого. Result для хранения результата
вычисления.

ПРИМЕЧАНИЕ № 7: Ключевое слово double обозначает простой тип,


используемый для хранения 64-разрядных значений с плавающей запятой.

Все логические переменные служат для «переключателей» во время работы


программы. И регулируют работы всех кнопок.
К примеру при нажатии на кнопку операции «сложения» флаг Operation1
переходит из false в true, и далее программа выполняет именно сложение (все
остальные флаги остаются false).

private void B0_Click(object sender, EventArgs e)


{

if (Clear == true)
{
ResultBox.Clear();
Clear = false;
Dot = false;
}

if (Operation1 == false && Operation2 == false && Operation3 == false &&


Operation4 == false)
{
ResultBox.AppendText("0");
A = Convert.ToDouble(ResultBox.Text.Replace(".", ","));
}
else
{
ResultBox.AppendText("0");
B = Convert.ToDouble(ResultBox.Text.Replace(".", ","));
}
}

Это метод нажатия кнопки «0». В самом начале во время события нажатия, идёт
проверка на переменную Clear. Если она true, происходит очистка текстового поля,

54
после чего состояние «очистки» отключается вместе с состоянием нажатия кнопки
«точка».
Дальше идёт проверка на нажатие кнопок математических операций. Если
кнопки не нажаты, то вводимое в поле число будет отправлено в переменную A. Точнее
при нажатии кнопки «0» в этом случае к тексту хранимому в данный момент в
ResultBox.Text добавляется ещё один нуль. После чего идёт проверка на символы «.»
(точка) и замена его символом «,» (запятая). В завершении вся строка конвертируется
из String в Double и полученное значение отправляется в переменную A. Если же
нажата кнопка какой-либо операции, все введённые цифры присваивается переменной
B.

private void BOperation1_Click(object sender, EventArgs e)


{
Operation1 = true;
Operation2 = false;
Operation3 = false;
Operation4 = false;
ResultBox.Clear();
}

Кнопка операции «сложения». Здесь реализована защита от ввода неверных


данных, так как используется одно и тоже поле для ввода двух разных чисел. После
нажатия кнопки операции «сложения», поле очищается, а флаги переходят в нужное
состояние.

private void BD_Click(object sender, EventArgs e)


{

if (Clear == true)
{
ResultBox.Clear();
Clear = false;
Dot = false;
}

if (Operation1 == false && Operation2 == false && Operation3 == false &&


Operation4 == false)
{

if (Dot == false)
{
ResultBox.AppendText(",");
Dot = true;
}
}
else
{

if (Dot == true)
{
ResultBox.AppendText(",");
Dot = false;
}
}
}

Кнопка «точка». Идентична кнопкам цифр, за исключением поведения при


нажатии. Изначально переменная Dot в состоянии false. Если мы вводим в переменную
A, нажатие кнопки «точка» меняет Dot на true. После этого в текстовое поле
добавляется собственно знак «точки». И здесь срабатывает механизм замены. После
этого дальнейшие нажатия не дадут эффекта и лишняя точка не будет введена (и не
будет выдана ошибка).

55
Если же математическая операция уже выбрана, то Dot переведена в состояние
true и сработает последняя часть метода. Опять же нажать «точку» можно лишь один
раз.

private void BResult_Click(object sender, EventArgs e)


{
if (Operation1 == true)
Result = A + B;
if (Operation2 == true)
Result = A - B;
if (Operation3 == true)
Result = A * B;
if (Operation4 == true)
Result = A / B;
ResultBox.Text = Result.ToString();
Operation1 = false;
Operation2 = false;
Operation3 = false;
Operation4 = false;
Clear = true;
}

Кнопка «вычисление» производит основное действия по вычислению в


зависимости от выбранной операции, а также от содержания переменных A и B. Если
просто нажать кнопку математической операции, переменная A будет содержать нуль.
Но на ноль поделить нельзя. Результат будет забавным (ошибка выдана не будет, что
примечательно). Основная операция по выводу результата это конвертирование
переменной Result обратно в String.

Кнопка «Округлить» пояснена в комментариях к кода события Click для кнопки.

Теперь немного остановимся на файле (LWP02Main.Designer.cs). Это и есть


пресловутый конструктор формы. Точнее файл с настройками формы.

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

Рис. 4. 2. Содержимое файла LWP02Main.Designer.cs

56
/// <summary>
/// Требуется переменная конструктора.
/// </summary>
private System.ComponentModel.IContainer components = null;

Здесь (строчка выше) держатся все компоненты формы. Генерируется


автоматически. Если компонент не используется, и никто на него не ссылается,
включается в работу метод Dispose() и удаляет, сбрасывает или высвобождается
неуправляемый ресурс.

/// <summary>
/// Освободить все используемые ресурсы.
/// </summary>
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален;
иначе ложно.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

Собственном метод Dispose(). Вызывается в случае необходимости уничтожения


управляемого ресурса.

#region Код, автоматически созданный конструктором форм Windows

/// <summary>
/// Обязательный метод для поддержки конструктора - не изменяйте
/// содержимое данного метода при помощи редактора кода.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new
System.ComponentModel.ComponentResourceManager(typeof(LWP02Main));
this.ResultBox = new System.Windows.Forms.TextBox();
this.B1 = new System.Windows.Forms.Button();

#endregion

Как уже было сказано, этот код генерируется автоматически. По большей части
это создание экземпляров определённых ресурсов и изменение их свойств, такие как
позиция, имена, размеры и прочее. Это «конструктор» всех компонентов формы,
которые пользователь установил при помощи визуального редактора. Разумеется,
добавлять новый компоненты (кнопки, текстовые поля, DataGridView’ы и другое)
можно добавлять «руками», прописывая всё то что делает конструктор автоматически.
Зачастую, ручное добавлением элементов управления и настройку их можно увидеть в
«одно файловых» проектах (где код помещён в один файл *.cs). Единственное что
можно сказать о преимуществе визуального конструктора формы — это его наглядность
и несомненное удобство.

И наконец, обратимся к файлу Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace LWP02WindowsForms01
{

57
static class Program
{
/// <summary>
/// Главная точка входа для приложения.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new LWP02Main());
}
}
}

Можно сказать, здесь использован стандартный шаблон. В этом файле


содержится точка входа в программу (static void Main()). Две строчки ниже относится к
инициализации визуального стиля для приложения и установки значений по умолчанию
для определенных элементов управления программы. Сам запуск и инициализация
формы выполняется здесь:

Application.Run(new LWP02Main());

5. О приложении к Лабораторной работе № 2

Получившуюся программу (LWP02WindowsForms01.exe), собранную из кусков


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

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

3. Лабораторная работа № 3: Windows Forms и использование некоторых элементов


управления

Лабораторная работа № 3: Windows Forms и использование некоторых


элементов управления

Содержание

1. Вводная часть
2. Создание приложения Windows Forms
3. Модификация приложения Windows Forms
4. Модификация приложения Windows Forms: элемент управления
MenuStrip
5. Модификация приложения Windows Forms: элемент управления Button
6. Модификация приложения Windows Forms: элемент управления ToolTip
7. Модификация приложения Windows Forms: элемент управления
ComboBox
8. Модификация приложения Windows Forms: элемент управления
WebBrowser
9. Модификация приложения Windows Forms: добавляем исходный код

58
10.Завершающая часть
11.О приложении к Лабораторной работе № 3

1. Вводная часть

В предыдущей лабораторной работе был рассмотрен случай создания простого


приложения Windows Forms и основные принципы построения такого типа
приложений.
В этой работе будет рассмотрено несколько новых элементов управления и
некоторые особенности по работе с ними. Цель данной работы, это разработка
достаточно простого и в то же время эффективного приложения с использованием
специфических элементов управления. Конечным итогом, станет создание
полноценного приложения веб-браузера. В частности поэтапно рассмотрим:

1. Добавление элемента управления MenuStrip.


2. Добавление элемента управления Button (уже рассматривалось).
3. Добавление элемента управления ToolTip.
4. Добавление и заполнение элемента управления ComboBox.
5. Добавление и использование элемента управления WebBrowser.
6. Обработка вывода данных приложения в текстовый файл *.txt. Будем выводить
историю посещения. А именно текущий выбор (или напечатанную строку) из
ComboBox.

2. Создание приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

59
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP03WindowsForms02 — это название программы


(выбрано по названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория где будет находится весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

60
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

61
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

62
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs3 на LWP03Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Веб-обозреватель
(C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico4.
Size изменим со значений 300; 300 на 640;
480

63
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения

4. Модификация приложения Windows Forms: элемент управления MenuStrip

Элемент MenuStrip — это элемент управления для реализации меню в


приложении. Ищем его на панели элементов (по умолчанию находится слева сбоку на
границе среды разработки):

64
Рис. 4. 1. Панель элементов: поиск элемента MenuStrip

Добавляем элемент — перетаскиваем его в поле нашей формы (LWP03Main).

Рис. 4. 2. Добавленное меню элемента управления MenuStrip

Меню автоматически «прилипнет» к верхней части окна формы под шапкой с


надписью Веб-обозреватель (С#). Наличие меню также отображается под формой (Рис.
4. 3):

65
Рис. 4. 3. Отображение добавления специального элемента управления под формой

Заполним меню. В поле с текстом Вводить здесь введём имя меню, в данном
случае Навигация. После нажатия клавиши Enter появятся новые поля для создания
других меню и пунктов меню. В поле, расположенном ниже, вводим: Домашняя
страница. Нажимаем клавишу Enter, после чего появятся дополнительные поля.
Введём: Назад. Нажмём Enter, введём: Вперёд. В итоге получим следующую картину:

Рис. 4. 4. Заполненное меню элемента управления MenuStrip

При компиляции теперь у нас есть полноценное меню. При помощи инструмента
MenuStrip. Можно создавать сколько угодно большие вложенные меню (с выпадением
списка вниз и вправо).
Обратим внимание ещё вот на что. Если посмотреть поле (Name) свойства
элемента меню Домашняя страница, то оно будет следующим:

домашняяСтарницаToolStripMenuItem

Поэтому события, которые можно подставлять для этого элемента тоже будут «по
умолчанию» содержать символы кириллицы, ссылаясь на внутренне имя объекта.
Страшного ничего нет, так как это не вызовет ошибки. Однако, чтобы не смущать
наличием кириллицы в имени переменной, изменим поля (Name) для получившегося
меню. Вместо домашняяСтраница поставим Home, вместо назад: Back,. Вместо:
вперёд: Forward. Вместо навигацияToolStripMenuItem для первоначального
элемнта меню поставим: NavigateToolStripMenuItem.

5. Модификация приложения Windows Forms: элемент управления Button

66
Разместим справа в углу внутри меню элемент управления Button. Параметры
таковы:

(Name): GoButton
Text: Перейти

Получим следующее:

Рис. 5. 1. Добавленный элемент управления Button (Перейти)

6. Модификация приложения Windows Forms: элемент управления ToolTip

Элемент ToolTip — это событийный элемент, который выполняет роль события


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

Перетаскиваем этот элемент с панели элементов на любое место формы:

Рис. 6. 1. Добавленный элемент управления ToolTip

Теперь ко всем добавленным элементам (и тем что ещё будут добавлены)


привязано новое поле на панели свойств. Это поле: ToolTip на toolTip1. Первая часть
поля означает тип элемента управления, вторая как следует из текста — его текущее
имя в приложении.

67
Рис. 6. 2. Новое поле для элемента управления GoButton: ToolTip на toolTip1

Добавим всплывающую подсказку для кнопки (Рис. 6. 2). Перейдём в свойства


кнопки GoButton, найдём поле ToolTip на toolTip1 и впишем туда подсказку: Перейти
по адресу из ComboBox. Теперь при наведении курсора мышки на кнопку (после
компиляции), через секунду после начала удержания фокуса появится эта подсказка.

Рис. 6. 3. Пример работы всплывающей подсказки ToolTip

7. Модификация приложения Windows Forms: элемент управления ComboBox

Добавим адресную строку в наш веб-обозреватель. Адресной строкой станет


ComboBox. Перетащим элемент между меню и кнопкой так, чтобы между правой гранью
перетаскиваемого ComboBox и кнопкой появилась синяя линия (это будет означать что
элементы встали на одной линии). После установки элемента, протянем левую грань
ComboBox до надписи Навигация.

То что должно получиться:

68
Рис. 7. 1. Добавленный элемент управления ComboBox

Элемент управления ComboBox предоставляет раскрывающийся список


вариантов для выбора. В нашем приложении ComboBox будет содержать список
избранных веб-узлов для быстрого доступа.
Для создания списка веб-узлов выделим ComboBox и отобразите его свойства.
При выделении свойства Items будет отображена кнопка с многоточием (...) и словом
(Коллекция):

Нажмём на эту кнопку для изменения содержимого ComboBox. Добавим любое


количество URL-адресов веб-узлов, нажимая после каждого Enter.

Рис. 7. 2. Добавление новых элементов «по умолчанию» в элемент управления


ComboBox

Параметры ComboBox:

(Name): AddressBox

8. Модификация приложения Windows Forms: элемент управления


WebBrowser

Добавим последний элемент управления: WebBrowser. Перетащим его на


форму и изменим размеры элемента WebBrowser так, чтобы он заполнил форму без

69
перекрытия элементов управления MenuStrip, ComboBox и Button. Если изменение
размеров элемента управления WebBrowser затруднено, откроем его свойства, найдём
параметр Dock и убедимся, что ему задано значение none, после этого установим
желаемый размер. Задание параметру Anchor значения Top, Bottom, Left, Right
заставит элемент управления WebBrowser корректно изменять свой размер при
изменении размера окна приложения.

Элемент WebBrowser выполняет все сложные действия по визуализации веб-


страниц. В приложении доступ к нему осуществляется через экземпляр класса
WebBrowser.

Рис. 8. 1. Добавленый элемент управления WebBrowser

9. Модификация приложения Windows Forms: добавляем исходный код

Приложение должно иметь обработчик событий для всех кнопок. Начнём с


кнопки GoButton.

70
Для добавления события нажатия для кнопки, необходимо дважды кликнуть на
соответствующую кнопку, либо перейти в свойства кнопки и нажать на зачёк «молнии»
(События):

Рис. 9. 1. Переключение со страницы свойств кнопки на страницу событий

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

private void GoButton_Click(object sender, EventArgs e)


{
// Ловим пустые и неправильные адреса
try
{
WebBrowser.Navigate(new Uri(AddressBox.Text.ToString()));
}
catch(UriFormatException Error)
{
// Здесь можно вставить обработчик ошибки, например вывод в MessageBox
// catch { } можно оставить пустым, тогда все ошибки будут выловлены, но
не будут отображены
MessageBox.Show("Ошибка! Неверный формат URL-ссылки\n\nПожалуйста,
используйте http://<ваш адрес>\n\nСодержание ошибки: " + Error.Message);
}
}

Поясним код: значение которое пользователь набирает в ComboBox (или


выбирает из списка оправляется в новое значение для метода Uri() и метод
Navigate() класса WebBrowser выполняет загрузку и отображение страницы по этому
адресу.

А вот конструкция (try { }/catch(UriFormatException) { }) куда более интересная.

71
Оператор try-catch состоит из блока try, за которым следует одно или несколько
предложений catch, в которых определяются обработчики для различных исключений.
Как следует из вышесказанного у метода может быть несколько исключений. О том
какие это исключения, а также о том сколько их, можно узнать да-да, наведя курсор
мыши на используемый метод). При возникновении исключения среда CLR ищет
оператор catch, который обрабатывает это исключение. Если выполняющийся в данный
момент метод не содержит такого блока catch, то среда CLR рассматривает метод,
который вызвал текущий метод, и так далее по стеку вызовов (тоесть до самого
начального метода). Если блок catch не найден, то среда CLR отображает пользователю
сообщение о необработанном исключении и останавливает выполнение программы
(вызывая критическую ошибку и как следствие, потерю всех данных).
Блок try содержит защищаемый код, в котором могут происходить исключения.
Этот блок выполняется до момента возникновения исключения или до своего
успешного завершения.
Хотя предложение catch можно использовать без аргументов для перехвата
любого типа исключения, такой подход не рекомендуется. В общем случае следует
перехватывать только те исключения, устранение причин которых известно.
В нашем случае, у метода Uri() есть два исключения. Одно из них мы «поймали»
и обработали, выведя пользователю сообщение в MessageBox.Show. А поймали мы
пустую строку или неправильный формат URL-адреса. Само сообщение об ошибке,
сгенерированное методом можно увидеть в переменной Error.Message.

Добавим код для меню. Событие Click кнопки Домашняя страница:

private void HomeToolStripMenuItem_Click(object sender, EventArgs e)


{
// Если в ComboBox выбрано "Пустая страница", то нажатие кнопки Домашняя
страница отправит нас по адресу в переменной HomePage
if (AddressBox.Text == "Пустая страница")
WebBrowser.Navigate(new Uri(HomePage));
else
WebBrowser.GoHome(); // Вытаскивает настоящую домашнюю страницу нашего
браузера в операционной системе
}

Для работы этого кода добавим после строк в файле LWP03Main.cs:

public partial class LWP03Main : Form


{

Объявление переменной HomePage:

String HomePage = "about:blank";

Кнопка Назад:

private void BackToolStripMenuItem_Click(object sender, EventArgs e)


{
WebBrowser.GoBack();
}

Кнопка Вперёд:

private void ForwardToolStripMenuItem_Click(object sender, EventArgs e)

72
{
WebBrowser.GoForward();
}

События кнопок Назад и Вперёд вытаскивают из класса WebBrowser


соответствующие методы навигации.

Событие Load для формы LWP03Main:

Рис. 9. 2. Событие Load для формы LWP03Main

Код события:

private void LWP03Main_Load(object sender, EventArgs e)


{
WebBrowser.GoHome();
}

Событие Navigate для элемента управления WebBrowser:

73
Рис. 9. 3. Событие Navigate для элемента управления WebBrowser

Код события:

private void WebBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)


{
AddressBox.Text = WebBrowser.Url.ToString();
}

Здесь, в поле ComboBox записывается текущая запись метода Url(), а именно


содержание адресной строки в правильном формате URL-ардеса.

Последнее событие которое мы добавим в наше приложение, будет событием


нажатия клавиши Enter в фокусе ComboBox (когда каретка переведена в поле ввода
ComboBox). Также для этого события напишем альтернативную функция выполнения
перехода по URL-адресу.

Событие KeyDown для элемента управления ComboBox:

74
Рис. 9. 4. Событие Key Down для элемента управления ComboBox

Код события и нового метода:

private void AddressBox_KeyDown(object sender, KeyEventArgs e)


{
// Если была нажата клавиша Enter в фокусе ComboBox, выполняем метод
Navigate() (рукописный)
if (e.KeyCode == Keys.Enter)
{
Navigate(AddressBox.Text);
}
}
// Метод Navigate()
// Метод является альтернативой нажатия кнопки Перейти для наглядности
private void Navigate(String Address)
{
if (String.IsNullOrEmpty(Address)) return; // Если адрес пуст, завершаем
функцию
if (Address.Equals("about:blank")) return; // Если получаем адрес пустой
страницы, завершаем функцию (чтобы не делать лишнюю работу)
// Проверяем как передан адрес: если передаётся без http:// или https://
подставляем
if (!Address.StartsWith("http://") &&
!Address.StartsWith("https://"))
{
Address = "http://" + Address;
}
try
{
WebBrowser.Navigate(new Uri(Address)); // Выполняем переход по новому
сформированному адресу

75
}
catch (UriFormatException)
{
return;
}
}

Теперь реализуем следующую функциональность. Сразу после запуска,


приложение считывает данные из ComboBox и отсылает их построчно в текстовый
файл. После того, веб-браузер успешно изменит страницу, это значение тоже
записывает в файл.

Текстовые файлы хороши для хранения любого количества информации. Если, к


примеру хранить в них БД какого либо приложения, необходимо условиться с
оператором разделителя для данных и соблюдать структуру строки.
Но для структурированных и специализированных данных в своё время
придумали очень удобный и главное упрощающий жизнь программисту формат *.xml
(об этом формате и работе с ним речь пойдёт в следующей лабораторной работе
практикума).

Для начала нам нужно пространство имён для работы с потоками данных между
приложением и файлами. Добавим строчку в самое начало файла LWP03Main.cs:

using System.IO; // Пространство имён ввода/вывода

Теперь добавим функцию для работы с записью в текстовый файл. Найдём в


нашем коде (в том же самом файле) строчки:

}
catch (UriFormatException)
{
return;
}
}

После них добавим:

public void SaveFile(String FilePath, String[] FileArrayBox, String FileAddress,


Boolean Mode)
{
if (Mode == true)
File.WriteAllLines(FilePath, FileArrayBox); // Записывает в файл
FilePatch массив из ComboBox, запись строк в файл идёт построчно
if (Mode == false)
File.AppendAllText(FilePath, FileAddress + Environment.NewLine); //
Записывает в файл FilePatch введённый URL-адрес в строке а также завершает запись
символом конца строки
}

Мы добавили общую функцию для работы с записью файл. Ключевым


переключателем является переменная Mode метода SaveFile(). Для true выполняется
создание и запись все значений ComboBox и получение в качестве второго параметра
массива FileArrayBox; для false дозапись (и создание нового файла, если вдруг файл
не был создан) значения из ComboBox со строковым параметром FileAdress.

Теперь обновим методы. Добавим две новых переменных (в том же файле):

public partial class LWP03Main : Form


{
String HomePage = "about:blank";
String[] ArrayBox;
String FilePath = "D:\\LWP03WindowsForms02 - ComboBox History.txt";

76
ArrayBox: переменная для массива значений ComboBox. FilePath: конечный
путь куда записывать файл и его название с расширением.

Обновлённое событие Load нашей формы:

private void LWP03Main_Load(object sender, EventArgs e)


{
WebBrowser.GoHome();
ArrayBox = AddressBox.Items.Cast<string>().ToArray();
SaveFile(FilePath, ArrayBox, null, true); // Вызываем метод записи в
текстовый файл
}

Обновлённый метод Navigate() нашей формы:

// Метод Navigate()
// Метод является альтернативой нажатия кнопки Перейти для наглядности
private void Navigate(String Address)
{
if (String.IsNullOrEmpty(Address)) return; // Если адрес пуст, завершаем
функцию
if (Address.Equals("about:blank")) return; // Если получаем адрес пустой
страницы, завершаем функцию (чтобы не делать лишнюю работу)
// Проверяем как передан адрес: если передаётся без http:// или https://
подставляем
if (!Address.StartsWith("http://") &&
!Address.StartsWith("https://"))
{
Address = "http://" + Address;
}
try
{
WebBrowser.Navigate(new Uri(Address)); // Выполняем переход по новому
сформированному адресу
SaveFile(FilePath, null, Address, false); // Вызываем метод записи в
текстовый файл
}
catch (UriFormatException)
{
return;
}
}

Как видно из строчки:

SaveFile(FilePath, null, Address, false); // Вызываем метод записи в


текстовый файл

Чтобы не указывать лишний параметр при вызове метода, используем ключевое


слово null.

10. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан


ниже (Рис. 10. 1):

77
Рис. 10. 1. Модифицированное приложение Windows Forms

Рис. 10. 2. Содержание файла созданного приложением Windows Forms после перехода
по новому адресу в ComboBox

11. О приложение к Лабораторной работе № 3

Получившуюся программу (LWP03WindowsForms02.exe), собранную из кусков


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

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).

78
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

4. Лабораторная работа № 4: Windows Forms — работа с формами

Лабораторная работа № 4: Windows Forms — работа с формами

Содержание

1. Вводная часть
2. Создание приложения Windows Forms
3. Модификация приложения Windows Forms
4. Модификация приложения Windows Forms: добавление новой формы
5. Модификация приложения Windows Forms: динамическое связывание
параметров двух форм и передача параметра через прямой доступ к
элементу формы
6. Модификация приложения Windows Forms: динамическое связывание
параметров двух форм и передача параметра через передачу метода в
конструктор формы
7. Модификация приложения Windows Forms: динамическое связывание
параметров двух форм и передача параметра через класс делегата
8. Модификация приложения Windows Forms: динамическое связывание
параметров двух форм и передача параметра через свойства
9. Завершающая часть
10.О приложении к Лабораторной работе № 4

1. Вводная часть

Работа со множеством окон в одном приложении — обычное дело когда речь


идёт о сложном, многофункциональном приложении со множеством меню,
всплывающих окон и различных форм для настройки работы приложения. Зачастую
приложение работает в одном главном окне, остальные окна же появляются по мере
необходимости лишь для выбора каких-либо настроек или ввода каких-либо данных.
Поэтому проблема передачи параметра в главное рабочее окно остро встаёт при
разработке подобного приложения. Нужно не только позаботиться о передаче
параметров, но также организовать правильный вызов окна в приложении и
правильное уничтожение окна после завершения работы с ним (высвобождение
ресурсов).
В этой работе будут рассмотрены способы создания дополнительных форм
(новых окон) в приложениях Windows Forms и обмен данными между всеми окнами
приложения.

Целью данной работы станет создание приложения для демонстрации работы


приложения с более чем одной формой. В частности рассмотрим:

7. Создание и настройка второй формы. Реализация «многооконности».


8. Многооконность: свободные окна и динамический обмен данными между ними.
9. Многооконность: модальные окна (окна перехватывающие фокус и
блокирующие доступ к другим окнам) и статический/ динамический обмен
данными между ними.

79
10. Передача параметра из одной формы в другую. Применим четыре различных
способа (наиболее интересных).

Первый способ: передача параметров через прямой доступ к элементу формы.


Изменение модификатора доступа к элементу управления.
Второй способ: передача метода в конструктор формы.
Третий способ: создание отдельного класса с делегатом. Совмещение второго и
третьего способов.
Четвёртый способ: передача параметров через свойства.

2. Создание приложения Windows Forms

Запускам Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

80
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP04WindowsForms03 — это название программы


(выбрано по названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория где будет находится весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

81
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

82
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

83
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP04Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Windows Forms
(C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 500;
350
^ Поменяем размер формы.

84
ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,
необходимо выполнить следующее: выделить в обозревателе решений значёк формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения

Добавим на нашу форму ToolTip ( ). Этот


элемент управления уже рассматривался в предыдущей лабораторной работе
практикума.

Параметры добавленного элемента всплывающей подсказки таковы:

(Name): Hint

4. Модификация приложения Windows Forms: добавление новой формы

85
Для добавления новой формы нам необходимо следующее: выделим правой
кнопкой мыши название нашего проекта в обозревателе решений (

), далее в выпадающем списке выполняем Добавить ->


Создать элемент… (Ctrl+Shift+A). В окне добавления нового элемента ищем: Форма
Windows Forms. Вводим и Имя: LWP04Children.cs. Теперь в обозревателе решений
появилась новая форма:

Настроим её:

Text изменим с LWP04Children на Работа с


окнами (C#) :: Подчинённая форма
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку) формы
Size изменим со значений 300; 300 на 500;
100
^ Поменяем размер формы.
FormBorderStyle изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.

Новая форма будет выглядеть примерно так:

Рис. 4. 1. Новая добавленная форма приложения

Теперь надо определиться, как именно быдет вызываться новая форма. Как
модальное окно, или как подчинённая форма (родительская форма будет контейнером
для обращение к дочерней форме). Наиболее интересен второй вариант, так как он
позволяет напрямую обращаться к элементам дочерней формы, а не ждать завершения
формы. Реализуем сначала этот способ.

Первое что необходимо сделать для вызова второй формы из первой это
объявить конструктор класса новой формы:

В файле кода главной формы (изначально созданной средой разработки) ищем:

public partial class LWP04Main : Form


{

После добавляем:

86
LWP04Children dlg = new LWP04Children();

Добавим вызывающую кнопку Button (показан конечный вариант кнопки):

Параметры добавленной кнопки таковы:

(Name): ButtonShowChildren
Text: Подчинённая форма
Size: 150; 23
ToolTip на Hint: Передача параметра через прямой доступ
к элементам управления

Событие Click кнопки ButtonShowChildren:

private void ButtonShowChildren_Click(object sender, EventArgs e)


{
if (!dlg.Visible)
dlg.Show(this);
}

Если откомпилировать приложение, нажать на кнопку тем самым вызван


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

Рис. 4. 2. Ошибка вызванная нажатием кнопки вызова формы

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


повторного вызова нужно добавить событие FromClosing для формы LWP04Childred:

87
private void LWP04Children_FormClosing(object sender, FormClosingEventArgs e)
{
// Определяем кто закрывает приложение
if (e.CloseReason == CloseReason.UserClosing)
{
Hide();
e.Cancel = true;
}
}

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


если в кнопке закрыть для дочерней формы применить код уничтожения объекта и
высвобождения ресурсов, то кнопка Подчинённая форма вновь вызовет подобную
ошибку. Высвобождает ресурс метод Dispose().

Обработчик уничтожения формы по нажатию кнопки Закрыть может выглядеть


так (не добавляем для нашего случая, иначе будет потеряна часть будущей
функциональности именно для этой дочерней формы):

private void ButtonClose_Click(object sender, EventArgs e)


{
Close(); // Кнопка Закрыть
Dispose(); // Уничтожаем окно и все его параметры до следующего вызова
конструктора формы
}

Компилируем приложение, проверяем работу кнопки.

5. Модификация приложения Windows Forms: динамическое связывание


параметров двух форм и передача параметра через прямой доступ к элементу
формы

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

добавим TextBox ( ):

Параметры добавленного текстового поля редактирования таковы:

(Name): TextBoxMain
Modifiers: Public

Событие TextChanged текстового поля редактирования TextBoxMain:

private void TextBoxMain_TextChanged(object sender, EventArgs e)


{
dlg.TextBoxChildren.Text = TextBoxMain.Text; // При изменении текста в поле
TextBoxMain тоже самое происходит в поле TextBoxChildren
}

Для формы LWP04Children добавим TextBox ( ):

88
Рис. 5. 1. Расстановка элементов на добавленной форме LWP04Children

Параметры добавленного тектового поля редактирования таковы:

(Name): TextBoxChildren
Modifiers: Public

Событие Load формы LWP04Main:

private void LWP04Main_Load(object sender, EventArgs e)


{
dlg.TextBoxChildren.TextChanged += new EventHandler(LWP04Main_Load); //
Определяем, что прозизошло изменение текста в TextBoxChildren и вызываем событие Load
TextBoxMain.Text = dlg.TextBoxChildren.Text;
}

Рис. 5. 2. Свойства элемента управления TextBoxMain: значения поля свойства


Modifiers

Параметры добавленной кнопки Закрыть:

(Name): ButtonClose

89
Text: Закрыть
Size: 75; 23

Событие Click кнопки ButtonCLose:

private void ButtonClose_Click(object sender, EventArgs e)


{
Close();
}

Компилируем приложение, проверяем работу текстовых полей. При изменении


текста у одного (любого) меняется значение во втором:

Рис. 5. 3. Динамическое изменение значений текстовых полей двух форм

Единственны «минус» такого варианта работы с формами, это атрибут public для
поля Modifiers элементов управления. То есть этот вариант можно использовать, но
безопасность данных в элементе управления в этом случае ставится под сомнение
изначально. Потому что этот элемент и все его свойства, поля и данные становятся
«видимыми» из любого места приложения. Под «безопасностью» понимаем не взлом
приложения, а доступность данных внутри приложения и возможное нарушение
согласования типов при манипуляции с переменными. То есть нарушается принцип
инкапсуляции. Если приходится открывать то что должно приватным, значит что то не
правильно в архитектуре приложения.

Следующие два способа, объединены общими принципами сохранения основ


ООП и инкапсуляции. Но в основе лежит одна и также возможность языка C#. Мы
попробуем организовать динамическое обновление полей при помощи так называемых
делегатов.

6. Модификация приложения Windows Forms: динамическое связывание


параметров двух форм и передача параметра через передачу метода в
конструктор формы

90
Создаём новую форму LWP04ChildrenDelegate1 со следующими элементами:

Рис. 6. 1. Расстановка элементов на добавленной форме LWP04ChildrenDelegate1

Параметры формы LWP04ChildrenDelegate1 таковы:

(Name): LWP04ChildrenDelegate1
Text: Работа с окнами (C#) :: Подчинённая
форма для делегата № 1
Size: 500; 100
FormBorderStyle: Fixed Dialog
MaximizeBox: False
MinimizeBox: False
ShowInTaskbar: False

Рис. 6. 1. Свойства формы LWP04ChildrenDelegate1: значения поля свойства


ShowInTaskbar

91
Рис. 6. 2. Свойства формы LWP04ChildrenDelegate1: значения поля свойства
FormBorderStyle

Здесь также был использован новый элемент управления из коллекции


элементов Label: обычный «текст» на форме:

Для элемента Label ключевым свойством является Text.

Параметры добавленного текстового поля редактирования таковы:

(Name): TextBoxChildrenDelegate1

Событие TextChanged текстового поля редактирования TextBoxChildrenDelegate1:

private void TextBoxChildrenDelegate1_TextChanged(object sender, EventArgs e)


{
Text1 = TextBoxChildrenDelegate1.Text; // Присваиваем переменной обратной
передачи значение текстового поля

92
}

Параметры добавленной кнопки Закрыть:

(Name): ButtonClose
Text: Закрыть
Size: 75; 23

Событие Click кнопки ButtonClose:

private void ButtonClose_Click(object sender, EventArgs e)


{
Close();
}

Для формы LWP04Main добавим Button:

Рис. 6. 3. Расстановка элементов на добавленной форме LWP04Main: кнопка Для


делегата № 1

Параметры добавленной кнопки таковы:

(Name): ButtonShowChildrenDelegate1
Text: Для делегата № 1
Size: 150; 23
ToolTip на Hint: Передача параметра через метод в
конструкторе

Событие Click кнопки ButtonShowChildrenDelegate1:

private void ButtonShowChildrenDelegate1_Click(object sender, EventArgs e)


{

93
/* Создаём экземпляр класса формы LWP04ChildrenDelegate1, вызывает
конструктор
* Вместе с формой создаём экземпляр делегата с вызовом метода
TextBoxNewDelegate1() */
LWP04ChildrenDelegate1 D1 = new LWP04ChildrenDelegate1(new
NewDelegate1(TextBoxNewDelegate1));
D1.ShowDialog(); // Вызываем модальный диалог нашей формы
MessageBox.Show("Текст который был введён в форме:\n\n" + D1.Text1, D1.Text +
" :: Результат выполнения"); // Возвращаем с формы переменную Text1 И показываем её в
окошке MessageBox.Show
TextBoxMain.Text = D1.Text1; // Отправляем переменную в поле TextBoxMan
}

Ниже в этом же файле добавляем следующее:

string TextBoxNewDelegate1()
{
// Метод возвращает значение TextBoxMain, нужен для работы делегата
return TextBoxMain.Text;
}

В файле Program.cs ищем:

Application.Run(new LWP04Main());
}
}

Добавляем после:

public delegate string NewDelegate1(); // Создаём делегата № 1

Полный исходный код файла LWP04ChildrenDelegate1.cs (формы) с


комментариями:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace LWP04WindowsForms03
{
public partial class LWP04ChildrenDelegate1 : Form
{
private NewDelegate1 D1; // Объявляем экземпляр делегата NewDelegate1
public String Text1; // Объявляем переменную для передачи параметра обратно в
главную форму
// Меняем конструктор формы, чтобы он мог узнать какой метод ему был отправлен...
public LWP04ChildrenDelegate1(NewDelegate1 sender)
{
InitializeComponent();
D1 = sender; // ...и кем
}

private void ButtonClose_Click(object sender, EventArgs e)


{
Close(); // Кнопка Закрыть
}

94
private void LWP04ChildrenDelegate_Load(object sender, EventArgs e)
{
/* Вызываем созданный экземпляр класса NewDelegate1
* После вызова экземпляр вытаскивает из главной формы метод, а точнее
переменную TextBoxNewDelegate1
* Далее делегат становится переменной, которую мы отправляем в
TextBoxChildren этой формы
* Сама переменная делегата является возвращаемой строкой от TextBoxMain */
TextBoxChildrenDelegate1.Text = D1();
}

private void TextBoxChildren_TextChanged(object sender, EventArgs e)


{
Text1 = TextBoxChildrenDelegate1.Text; // Присваиваем переменной обратной
передачи значение текстового поля
}
}
}

Компилируем приложение, проверяем работу текстовых полей второй


добавленной формы и главной формы. При изменении текста в главной форме меняется
значение в поле вызываемой формы (по нажатию кнопки Для делегата № 1).
Изменений значения поля в вызванной форме приводит к обратному эффекту.

Рис. 6. 4. Статическое изменение значений текстовых полей двух форм (до открытия и
закрытия формы)

7. Модификация приложения Windows Forms: динамическое связывание


параметров двух форм и передача параметра через класс делегата

По аналогии с пунктом под номер 6 данной лабораторной работы создаём третью


форму. Элементы расставляем также.

Создаём новую форму LWP04ChildrenDelegate1 со следующими элементами:

95
Рис. 7. 1. Расстановка элементов на добавленной форме LWP04ChildrenDelegate1

Параметры формы LWP04ChildrenDelegate1 таковы:

(Name): LWP04ChildrenDelegate1
Text: Работа с окнами (C#) :: Подчинённая
форма для делегата № 2
Size: 500; 100
FormBorderStyle: Fixed Dialog
MaximizeBox: False
MinimizeBox: False
ShowInTaskbar: False

Параметры добавленного текстового поля редактирования таковы:

(Name): TextBoxChildrenDelegate2

Событие TextChanged текстового поля редактирования TextBoxChildrenDelegate2:

private void TextBoxChildrenDelegate2_TextChanged(object sender, EventArgs e)


{
Text2 = TextBoxChildrenDelegate1.Text; // Присваиваем переменной обратной
передачи значение текстового поля
}

Параметры добавленной кнопки Закрыть:

(Name): ButtonClose
Text: Закрыть
Size: 75; 23

Событие Click кнопки ButtonClose:

private void ButtonClose_Click(object sender, EventArgs e)


{
Close();
}

Также, добавим ещё одну кнопку для формы LWP04Main:

96
Рис. 7. 2. Расстановка элементов на добавленной форме LWP04Main: кнопка Для
делегата № 2

Параметры добавленной кнопки таковы:

(Name): ButtonShowChildrenDelegate2
Text: Для делегата № 2
Size: 150; 23
ToolTip на Hint: Передача параметра через класс делегата

Событие Click кнопки ButtonShowChildrenDelegate2:

/* Создаём экземпляр класса формы LWP04ChildrenDelegate2, вызываем


конструктор
* Вместе с формой создаём экземпляр делегата с вызовом метода
TextBoxNewDelegate2() */
LWP04ChildrenDelegate2 D2 = new LWP04ChildrenDelegate2(new
NewDelegate2In(TextBoxNewDelegate2));
D2.ShowDialog(); // Вызываем модальный диалог нашей формы

Это событие на кнопке аналогично тому, что уже было использовано. Мы


совместим возможности показанные в пункте № 6 с новой функциональностью.

В этом же файле (LWP04Main.cs) добавляем следующее (новая функция void, и


копия метода и пункта №6):

void TextBoxNewDelegate2Out(String Text)


{
/* Основной обработчик передачи параметра, вызывается всякий раз когда
происходит событий обновления текста поле дочерней формы
* Вызывает MessageBox.Show с главной формы, что важно, сообщая параметр с
дочерней формы */
MessageBox.Show("Текст который был введён в форме:\n\n" + Text, D2.Text +
" :: Результат выполнения");
TextBoxMain.Text = Text; // Отправляем полученную переменную в поле
TextBoxMan (выполняется динамически)
}

string TextBoxNewDelegate2()
{
// Метод возвращает значение TextBoxMain, нужен для работы делегата
return TextBoxMain.Text;
}

Основной метод главной формы отредактируем так (ВАЖНО, чтобы весь код
находился после строчки (InitializeComponent();), так как здесь должен сработать
сначала конструктор, а затем всё остальное.

public LWP04Main()
{
InitializeComponent();
NewDelegate2Out.EventHandler = new
NewDelegate2Out.NewEventOut(TextBoxNewDelegate2Out);
}

Ключевым же является метод дочерней формы для текстового поля


редактирования. Событие TextChanged текстового поля редактирования
TextBoxChildrenDelegate2:

97
// Метод отлавливающий изменения текстового поля является здесь основным
private void TextBoxChildrenDelegate2_TextChanged(object sender, EventArgs e)
{
/* Срабатывает делегат NewDelegate2Out при получения события изменений текста
этого поля
* и в метод (на главную форму) отправляется значение этого поля
* То есть изменение текста приводит к ВЫЗОВУ события EventHandler и отправку
через делегат данных текстового поля */
NewDelegate2Out.EventHandler(TextBoxChildrenDelegate2.Text);
}

Сам класс делегата в файле Program.cs реализован так:

/* Создаём делегата № 2 */
public static class NewDelegate2Out
{
public delegate void NewEventOut(String Data);
public static NewEventOut EventHandler;
}
// Делаем по аналогии
public delegate string NewDelegate2In();

Последний способ достаточно сложен для понимания. В основе лежит


инициализация EventHandler. Он представляет метод, который будет обрабатывать
событие, не имеющее данных. Поэтому, он «ловит» событие изменение текса, узнаёт
кто (какой элемент) проинициализировал это событие и вытаскивает текст в текстовом
поле редактирования дочерней формы. Итак, что же такое делегаты?

В платформе .NET Framework в основе модели события лежит существование


делегата события, связывающего событие с его обработчиком. Для вызова события
требуются два элемента:

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


(делегат был создан);
2. класс, в котором хранятся данные события (мы организовывали в как раз таки
класс).

Делегат — это тип, который определяет сигнатуру, иными словами тип


возвращаемого значения, и тип списка параметров метода. Тип делегата можно
использовать для объявления переменной, которая может ссылаться на любой метод с
той же сигнатурой, что и у делегата.
В стандартной сигнатуре делегата обработчика событий определяется не
возвращающий значение метод, первый параметр которого относится к типу Object и
ссылается на экземпляр, который инициировал событие, а второй параметр является
производным от типа EventArgs и содержит данные события. Если событие не создает
данных, второй параметр просто является экземпляром класса EventArgs. В противном
случае, второй параметр является пользовательским типом, производным от EventArgs,
и предоставляет всевозможные поля и свойства, необходимые для хранения данных
события (что мы и получили, считав текст с поля).
EventHandler является предопределённым делегатом, представляющим метод
обработчика событий для события, не генерирующего данные. Если событие не
генерирует данные, необходимо предоставить собственный пользовательский тип
данных события и либо создать делегат, в котором типом второго параметра является
этот пользовательский тип, либо использовать универсальный класс делегата
EventHandler(Of TEventArgs) и подставлять свой пользовательский тип в параметр

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

Именно событийная модель делает делегаты мощным инструментом (что можно


было увидеть при выполнении данной лабораторной работы).

Немного об объявлении делегата в общем виде: объявление создания делегата


происходит так:

delegate void MyDelegate();

Сначала идёт ключевое слово, затем возвращаем тип метода (void), и потом
имя делегата. В нашем случае, делегат не возвращает зачения и не получает
параметров, то есть это относится к любому методу этого делегата.

Используя конструктор, создаём новый объект нашего типа-делегата:

MyDelegate NewDelegate = new MyDelegate(MyMethod);

Мы видим, что в конструктор передалось имя целевого метода. Тип этого метода
должен быть обязательно void (раз мы решили так сделать в самом начале). И
конструктором создаётся экземпляр под именем NewDelegate.

Целевой метод:

static void MyMethod()


{
/* Тут может быть какой-либо код */
}

Для запуска делегата вызываем объект нашего делегата (будет выполнен метод
MyMethod()):

NewDelegate();

Компилируем приложение, проверяем работу текстовых полей третьей


добавленной формы и главной формы. При изменении текста в главной форме меняется
значение в поле вызываемой формы (по нажатию кнопки Для делегата № 2).
Изменение значения поля в вызванной форме приводит к обратному эффекту, а также
вызову MessageBox.

99
Рис. 7. 3. Динамическое изменение значений текстового полей главной формы (во
время ввода данных в текстовом поле дочерней формы)

8. Модификация приложения Windows Forms: динамическое связывание


параметров двух форм и передача параметра через свойства

Последний, наиболее простой в понимании и удобный для работы способ


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

Для реализации этого способа воспользуемся двумя элементами и разместим их


на уже созданой ранее форме. Возмём форму LWP04Children, и два элемента:

ComboBox ( ) и ListBox.

Рис. 8. 1. Элемент управления ListBox на панели элементов

Разместим оба элемента как показано на рисунке ниже (ComboBox слева):

100
Рис. 7. 1. Расстановка элементов на форме LWP04Children

Слегка расширим форму, чтобы наглядно уместить всё.

Параметры ComboBox таковы:

(Name): CB
Size: 225; 21

Параметры ListBox таковы:

(Name): LB
Size: 225; 69

Теперь добавим свойство, которое будет переходным классом для параметров


форм. Откроем файл LWP04Children.cs. И найдём:

{
Close();
}

Добавим после:

public string ComboText


{
get { return CB.Text; }
set { if (value != "") CB.Items.Add("ComboBox: [" + value + "]"); }
}

Меняем обработчик самой первой нашей кнопки (ButtonShowChildren)


следующим образом (добавляем последнюю строчку работы со свойством:

private void ButtonShowChildren_Click(object sender, EventArgs e)


{
if (!dlg.Visible)
dlg.Show(this);
dlg.ComboText = TextBoxMain.Text; // Вызываем строковый параметр ComboText и
метод set
dlg.ListText = TextBoxMain.Text;
if (dlg.ComboText != "")
LBMain.Items.Add(dlg.ComboText);
}

Смысл тут ясен. Вызывается метод set если ComboText слева. И get если… да,
справа (операция получения значения). При нажатии на кнопку вызова формы, в
ComboBox добавляется значение (если значение которое надо добавить не пустое).

101
Теперь реализуем получение значения из ComboBox например при изменении
значения. Добавим на нашу главную форму ещё один ListBox с именем:

(Name): LBMain

Код события двойного нажатия по элементу ListBox главной формы обновляем


следующим образом:

private void LBMain_MouseDoubleClick(object sender, MouseEventArgs e)


{
if (dlg.ComboText != "") // Проверяем значение которое добавляем (должно быть
не пусто)
LBMain.Items.Add(dlg.ComboText);
if (dlg.ListText != "") // Проверяем значение которое добавляем (должно быть
не пусто)
LBMain.Items.Add(dlg.ListText);
if (LBMain.Items.Count > 4) // Очищаем все значения элемента, если их число
больше 4
LBMain.Items.Clear();
}

И разберёмся с ListBox для дочерней формы. Код для формы такой (добавим
после ComboText):

public string ListText


{
get { return LB.Text; }
set { if (value != "") LB.Items.Add("ListBox: [" + value + "]"); }
}

Код для кнопки Подчинённая форма:

private void ButtonShowChildren_Click(object sender, EventArgs e)


{
if (!dlg.Visible)
dlg.Show(this);
dlg.ComboText = TextBoxMain.Text; // Вызываем строковый параметр ComboText и
метод set
dlg.ListText = TextBoxMain.Text; // Вызываем строковый параметр ListText и
метод set
}

Код для события самого ListBox (двойном нажатии на элементе):

private void LB_SelectedIndexChanged(object sender, EventArgs e)


{
Close();
}

Добавим также очистку самих элементов дочерней формы от переполнения. Для


этого воспользуемся событием MosueEnter дочерней формы LWP04Children:

private void LWP04Children_MouseEnter(object sender, EventArgs e)


{
if (CB.Items.Count > 4)
CB.Items.Clear();
if (LB.Items.Count > 4)
LB.Items.Clear();
}

102
Теперь при нажатии на кнопку Подчинённая форму, в ComboBox И ListBox будут
добавлять записи типа ComboBox: [<текст из поля главной формы>]. Выбираем
значение в ComboBox или ListBox, после чего дважды нажимаем на поле ListBox
главной формы и туда автоматически добавятся новые значения (выбранные курсором
в дочернем окне). При переполнения какого-либо элемента управления (более четырёх
записей), все значения удаляются переполненного из элемента.

Последнее что мы сделаем, это покажем на примере, как работает уничтожение


формы при помощи метода Dispose(). Для формы, вызываемой кнопкой Для делегата №
1 поменяем код кнопки Закрыть:

private void ButtonClose_Click(object sender, EventArgs e)


{
Close(); // Кнопка Закрыть
Dispose(); // Уничтожаем окно и все его параметры до следующего вызова
конструктора формы
}

Теперь если закрыть форму, вызванную кнопкой Для делегата № 1 через кнопку
Закрыть (не крестик в заголовке или Alt+F4), то данные дочеренего окна будут
уничтожены (после передачи в главную форму) и к ним не буде доступа до следующего
вызова формы. Это можно увидеть по всплывающему окошку, а точнее его
обрезанному заголовку после уничтожения формы:

Рис. 8. 3. Всплывающее окошко без части заголовка

9. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан


ниже (Рис. 9. 1):

103
Рис. 9. 1. Модифицированное приложение Windows Forms

10. О приложении к Лабораторной работе № 4

Получившуюся программу (LWP04WindowsForms03.exe), собранную из кусков


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

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

5. Лабораторная работа № 5: Windows Forms — элементы управления

Лабораторная работа № 5: Windows Forms — элементы управления

Содержание

104
1. Вводная часть
2. Создание приложения Windows Forms
3. Модификация приложения Windows Forms
4. Модификация приложения Windows Forms: динамическое добавление и
уничтожение элемента управления
5. Модификация приложения Windows Forms: стандартные диалоговые
окна
6. Модификация приложения Windows Forms: открытие файла, сохранение
файла и работа с текстом
7. Модификация приложения Windows Forms: прочее
8. Завершающая часть
9. О приложении к Лабораторной работе № 5

1. Вводная часть

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


которые ещё не были рассмотрены, но с которыми рано или поздно придётся
столкнуться и некоторые особенности по работе с ними. Элементы управления, это 50%
успеха программы (остальные 50% это её оформление…). Красивая программа
зачастую больше ценится, чем более функциональная, но содержащая невзрачный,
недружелюбный к пользователю интерфейс.
С каждым выпуском Visual Studio (и в частности «фреймворков»), стандартных
элементов для использования становится только больше, что неудивительно. Также, на
данный момент энтузиастами-программистами написано множество новых элементов
управления с различной функциональностью, подчас очень удобной.

Целью данной работы станет создание приложения с различными элементами


управления и демонстрацией их основной функциональности на простых примерах. В
частности рассмотрим:

11. Динамическое создание элемента управления.


12. Получение доступа к элементу управления по его имени.
13. Уничтожение элемента управления.
14. Поиск по однотипным элементам управления.
15. Создание массива элементов управления.
16. Работа со стандартными диалоговыми окнами Windows (окно выбора цвета,
выбора шрифта и прочее).
17. Организация функциональности текстового расширенного редактора (формат
сохранения Rich Text Format) и подключение возможности сохранения и
открытия файлов.
18. Пример добавления изображения во время работы приложения и некоторые
другие элементы управления.

2. Создание приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

105
Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

Рис. 2. 2. Окно создания нового проекта

106
В поле Имя вводим LWP05WindowsForms04 — это название программы
(выбрано по названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК, мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

107
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

108
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP05Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Элементы
управления (C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 640;
480
^ Поменяем размер формы.

109
ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,
необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя, СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения

Добавим на нашу форму ToolTip ( ).

Параметры добавленного элемента всплывающей подсказки таковы:

(Name): Hint

4. Модификация приложения Windows Forms: динамическое добавление и


уничтожение элемента управления

110
Расставим первую группу элементов. Наша цель, по нажатию кнопки, в
определённом месте получить новую (созданную) кнопку и добавить для неё событие
нажатия, которое выполнит изменение текста кнопки. Также добавим кнопку по
уничтожению этой добавленной кнопки. И, наконец, добавим кнопку, которая
автоматически добавит массив однотипных текстовых полей.

Расставим первую группу следующим образом:

Рис. 4. 1. Начальная расстановка элементов

Здесь у нас есть такие элементы как MenuStrip (меню сверху), GroupBox
(рамка с текстом), кнопки Button, NumericUpDown (элемент «ползунка») и элемент
StatusStrip (отдельное поле внизу).

Рис. 4. 2. Панель элементов: NumericUpDown

111
Рис. 4. 3. Панель элементов: StatusStrip

MenuStrip:
(Name): MainMenu
ToolTip на Hint: Меню
Первый главный пункт: Файл
Первый всплывающий пункт: вводим произвольное имя, далее
выделяем этот пункт меню, жмём по нему
правую кнопку мыши и выбираем
Преобразовать в -> Separator
Второй всплывающий пункт: Выход

Рис. 4. 4. Типы для пункта меню (текущий выделенный элемент 1: преобразуем в


Separator)

112
Событие Click пункта меню Выход:

private void выходToolStripMenuItem_Click(object sender, EventArgs e)


{
Close();
}

Добавим «горячую» клавишу. Для этого выделим элемент меню и войдём в его
свойства. Ищем там пункт ShortcutKeys. Выставляем галочку в Ctrl
(Модификаторы), и ищем в списке Клавиши нужную: E:

Добавим также отображение текстовой строки для будущей строки состоящая. В


поле ShortcutKeyDislpayString впишем: Ctrl+E.
Теперь нажатие выбранного сочетания Ctrl+E приведёт к закрытию приложения
(сочетание клавиш имитирует нажатие пункта меню Выход и соответственно
выполнения события нажатия).

Выберем для StatusStrip отображаемый тип, в который будем заносить что-либо.


Например, мы хотим сделать из него элемент строки состояния (отображающий текст
подсказки внизу и индикатор выполнения прохождения процесса). Для этого
выделим элемент, нажмём символ «стрелки вниз» и выберем сначала StatusLabel,
затем ProgressBar (после добавления элемента, «чистый» макет появится справа).

Рис. 4. 4. Добавление нового элемента для строки состояния

StatusStrip:
(Name): MainStatus
ToolTip на Hint: Строка состояния

StatusLabel:

113
(Name): StatusLabel
Text: <пусто>

PrgoressBar:
(Name): StatusProgressBar

Организуем вывод подсказки в строке состояния для кнопки Выход. Для этого
инициализируем событие MouseEnter (ожидание получения фокуса). Код события
модифицируем:

private void выходToolStripMenuItem_MouseEnter(object sender, EventArgs e)


{
StatusLabel.Text = выходToolStripMenuItem.Text + " (" +
выходToolStripMenuItem.ShortcutKeyDisplayString + ")";
}

При потере фокуса с элемента организуем стандартное обнуление строки


состояния. Событие MouseEnter для всей формы (выделяем заголовок формы для
перехода к свойствам формы):

private void LWP05Main_MouseEnter(object sender, EventArgs e)


{
StatusLabel.Text = "";
StatusProgressBar.Value = 0;
}

И событие MouseLeave пункта меню Выход:

private void выходToolStripMenuItem_MouseLeave(object sender, EventArgs e)


{
StatusLabel.Text = "";
}

Добавим визуальный эффект. При нажатии на кнопку выход приложение будет


замирать на секунду. И в строке состояния будет заполняться индикатор прогресса на
100%. После этого приложение закроется.

Добавим таймер с панели элементов:

Меняем его свойства:

Timer:
(Name): TimerOneSecond
Interval: 1000
^ Интервал между событиями Elapsed в миллисекундах.

114
Единственное событие таймера инициализируем двойным нажатием на иконку
таймера в конструкторе форм (Tick) и модифицируем:

private void TimerOneSecond_Tick(object sender, EventArgs e)


{
Close();
}

Модифицируем событие нажатия кнопки Выход:

private void выходToolStripMenuItem_Click(object sender, EventArgs e)


{
TimerOneSecond.Start(); // Запускаем таймер
TimerOneSecond.Tick += new EventHandler(TimerOneSecond_Tick); // Прошла
секунда: генерируем событие "один такт" таймера
StatusProgressBar.Maximum = 100; // Устанавливаем максимум для индикатора
StatusProgressBar.Value = 100; // Переводим текущее значение на максимум
}

Компилируем, проверяем работоспособность. Индикатор заполняется после


нажатия кнопки, затем через секунду приложение закрывается. При наведении на
пункт меню отображаются все подсказки.

Основы «украшательств» мы разобрали. Теперь перейдём к кнопкам. Начнём с


кнопки Добавить:

Button (Добавить):
(Name): button1
Text: Добавить
ToolTip на Hint: Добавить элемент управления

Событие Click кнопки Добавить:

private void button1_Click(object sender, EventArgs e)


{
System.Windows.Forms.Button NewButton = new System.Windows.Forms.Button(); //
Создаём кнопку
NewButton.Location = new System.Drawing.Point(17, 70); // Устанавливаем
необходимые свойства
NewButton.Name = "NewButton"; // Устанавливаем имя кнопки
NewButton.Size = new System.Drawing.Size(237, 23); // Устанавливаем размер
кнопки
NewButton.TabIndex = 0; // Порядок переключение по клавише Tab
NewButton.Text = "NewButton"; // Устанавливаем текст на кнопке
NewButton.UseVisualStyleBackColor = true; // Устанавливаем способ отрисовки
(стандартный)
NewButton.Click += new System.EventHandler(NewButton_Click); // button1_Click
- функция обработчик события нажатия на кнопку
Controls.Add(NewButton); // Добавляем на форму
NewButton.BringToFront(); // Помещаем элемент на передний план
button2.Enabled = true; // Включаем кнопку Удалить
}

115
Получаем доступ к элементу управления по его имени, для того чтобы поменять
текст на добавленной кнопке по её нажатию. Добавляем следующий код после кода
события Click:

private void NewButton_Click(object sender, EventArgs e)


{
/* Получаем доступ к элементу управления по его имени */
(Controls["NewButton"] as Button).Text = "Ура, заработало!";
}

Кнопка Удалить:

Button (Удалить):
(Name): button2
Text: Удалить
ToolTip на Hint: Удалить элемент управления
Enabled: False
^ Выключаем кнопку изначально.

Событие Click кнопки Удалить:

private void button2_Click(object sender, EventArgs e)


{
Controls.Remove((Controls["NewButton"] as Button)); // Удаляем с формы кнопку
NewButton
//(Controls["NewButton"] as Button).Dispose(); // Освобождаем ресурсы занятые
кнопкой
}

Вторая строчка закомментирована по причине того, что уничтожение


виртуального объекта невозможно. Нужно уничтожать конкретный объект либо
очистить все ссылки на объект, тогда он будет уничтожен сборщиком мусора. Для этого
вставим ещё одну кнопку (button4):

Пусть она выполняет функции выхода из приложения.

Удалим эту кнопку по нажатию на Удалить:

private void button2_Click(object sender, EventArgs e)


{
Controls.Remove((Controls["NewButton"] as Button)); // Удаляем с формы кнопку
NewButton
//(Controls["NewButton"] as Button).Dispose(); // Освобождаем ресурсы занятые
кнопкой
Controls.Remove(button4);
button4.Dispose();
button2.Enabled = false; // Выключаем кнопку после завершения операций
уничтожения кнопок
}

Кнопка Массив:

116
Button (Массив):
(Name): button3
Text: Массив
ToolTip на Hint: Добавить массив элементов управления
(число выбираем справа)

Элемент числового ползунка:

NumericUpDown:
(Name): NumericButtons
Maximum: 3
^ Максимальное число.
Mininum: 1
^Минимальное число.
Increment: 1
^Шаг.

Событие Click кнопки Массив3, 4:

private void button3_Click(object sender, EventArgs e)


{
StatusProgressBar.Value = 0; // Сбрасываем индикатор при нажатии
StatusProgressBar.Step = 34; // Задаём шаг для индикатора (делим на 3 части
100)
TextBox[] TB = new TextBox[Convert.ToInt32(NumericButtons.Value)]; //
Объявляем массив элементов управления по числву выбранному в NumericButtons

for (int i = 0; i < TB.Length; i++) // Организуем цикл по числу элементов


массива
{
StatusProgressBar.PerformStep(); // Увеличиваем значение индикатора
TB[i] = new System.Windows.Forms.TextBox(); // Создаём элемент i
TB[i].Location = new System.Drawing.Point(365 + i * 80, 50); // Позиция
элемента i
TB[i].Name = "textBox" + i.ToString(); // Заполняем элемент i
TB[i].Size = new System.Drawing.Size(75, 23);
TB[i].TabIndex = i;
TB[i].Text = "textBox" + i.ToString();
Controls.Add(TB[i]);
TB[i].BringToFront();
StatusLabel.Text = "Массив элементов управлений успешно создан (" +
StatusProgressBar.Value + "%)"; // выводим текст в строку состояния
}
}

ПРИМЕЧАНИЕ № 3: Вообще, для того, чтобы динамически добавить какой-то


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

ПРИМЕЧАНИЕ № 4: Поиск в элементах управления можно реализовать через


массив всех элементов определённого типа. Эти элементы, в которых нужно вести
поиск и замену должны существовать.

117
Пример реализации поиска текста в однотипных элементах управления типа
TextBox (можно сделать отдельной кнопкой):

foreach (Control control in Controls)


{
TextBox TB = control as TextBox;

if (TB != null) // Если элемент существует


{

if (TB.Text == "textBox1") // Обращаемся к конкретному элементу


правления
{
TB.Text = "Надейно и заменено";
}
else
{
TB.Text = "Text"; // Меняем текст во всех остальных подобных
элементах
}
}
}

Компилируем, проверяем работоспособность. Индикатор заполняется после


нажатия кнопки Массив, и добавляется текстовое поле редактирования под номером i,
указанным в числовом поле справа от кнопки:

Рис. 4. 5. Окончательная работа блока добавления и уничтожения элементов


управления

118
5. Модификация приложения Windows Forms: стандартные диалоговые окна

Расставим вторую группу элементов. Наша цель, по нажатию кнопок Изменить


фон формы, вызвать стандартное окно выбора цветов. По нажатию в нём кнопки ОК
меняем цвет фона. Кнопка Отмена справа возвращает фон по умолчанию. Вторая
кнопка Изменить шрифт формы, меняет весь шрифт используемый на форме. Кнопка
Отмена возвращает всё «как было». Дальше, здесь же поставим элемент
CheckedListBox. При выборе пункта Овальность, поменяем форму нашего окна на
овал. При выборе пункта Непрозрачность окна в %, окно меняет прозрачность в
зависимости от числа (от 0 до 100) в NumericUpDown. Кнопка Выбор директории
открывает стандартный диалог выбор папки через Проводник Windows. По нажатию
кнопки ОК отобразив в текстовом поле редактирования полный путь к выбранной
директории. Добавим также ещё один ProgressBar который будет отсчитывать время по
секундам и обнуляться по прошествии минуты. Отсчёт времени будем вести по
текущему системному.

Расставим вторую группу следующим образом:

Рис. 5. 1. Расстановка элементов второй группы (Стандартные диалоговые окна)

Кнопка Изменить фон формы:


(Name): button5
Text: Изменить фон формы

Кнопка Отмена:
(Name): button6

119
Text: Отмена.
ToolTip на Hint: Возвращает фон формы по умолчанию

Кнопка Изменить шрифт формы:


(Name): button8
Text: Изменить шрифт формы

Кнопка Отмена:
(Name): button7
Text: Отмена
ToolTip на Hint: Возвращает шрифт формы по умолчанию

Список с выбором флажками CheckedListBox5:


(Name): checkedListBox1

ПРИМЕЧАНИЕ № 5: Для этого элемента, как и для ComboBox и ListBox можно


изначально задавать «коллекцию» уже готовых элементов. Для этого в свойствах
элемента ищем поле Items и добавляем новые элементы через нажатие кнопки (…).
Разделителем элементов будет перевод строки в окне добавления элементов.

NumericUpDown:
(Name): NumericAplha
Maximum: 100
^ Максимальное число.
Mininum: 1
^ Минимальное число.
Increment: 1
^ Шаг.

Кнопка Выбор директории:


(Name): button9
Text: Выбор директории

Текстовое поле редактирования TextBox:


(Name): textBox1

Индикатор выполнения ProgressBar:


(Name): progressBar1

Добавим ещё один таймер, который активируем сразу же при запуска


приложения:

Timer:
(Name): TimerOneMinute
Enabled: True
^ Запускаем таймер при инициализации приложения.

Добавим диалоговые окна с панели элементов (перетаскиванием на форму):

120
ColorDialog:
(Name): ColorSelect
FontDialog:
(Name): FontSelect
FolderBrowserDialog:
(Name): SelectBrowser

Теперь пропишем все необходимые события (в том порядке, в котором задавали


свойства элементов управления):

Событие Click кнопки Изменить фон формы:

private void button5_Click(object sender, EventArgs e)


{
ColorSelect.ShowDialog(); // Показываем окно выбора цвета (Windows)
this.BackColor = ColorSelect.Color; // Устанавливаем в качестве текущего для
фона формы
StatusLabel.Text = "Выбан цвет: " + ColorSelect.Color; // Выводит текст в
строку состояния с названием цвета (если определено) или его RGB-представлением
}

Событие Click кнопки Отмена:

private void button6_Click(object sender, EventArgs e)


{
this.BackColor = Color.Empty; // Возвращаем первоначальный фон
}

Событие Click кнопки Изменить шрифт формы:

private void button8_Click(object sender, EventArgs e)


{
try
{
FontSelect.ShowDialog();
this.Font = FontSelect.Font;
StatusLabel.Text = "Выбран шрифт: " + FontSelect.Font.ToString();
}
catch
{
// Ловим ошибку: шрифт не TrueType
}
}

Событие Click кнопки Отмена:

private void button7_Click(object sender, EventArgs e)


{
this.Font = DefaultFont;
StatusLabel.Text = "Выбран первоначальный шрифт: " + DefaultFont.Name;
}

Событие SelectedIndexCahged списка с выбором флажками:

121
private void checkedListBox1_SelectedIndexChanged(object sender, EventArgs e)
{
GraphicsPath G = new GraphicsPath(); // Создаём новый графический элемент
Rectangle Rect = new Rectangle(0, 0, this.Width, this.Height); // Создаём
прямоугольник

if (checkedListBox1.CheckedItems.Contains("Непрозрачность окна в %") == true)


// Если элемент выделен
{
this.Opacity = Convert.ToDouble(NumericAplha.Value) / 100; //
Прозрачонть: значение NumericAplha / 100 (0 - полна прозрачность, 1 - полная
непрозрачность)
}

if (checkedListBox1.CheckedItems.Contains("Овальное окно") == true)


{
G.AddEllipse(0, 0, this.Width, this.Height); // Добавляем эллипс
покрывающий всю форму в графический элемент
this.Region = new System.Drawing.Region(G); // Создаём объект нового
региона и меняем регион формы на эллепис
}
else
{
G.AddRectangle(Rect); // Добавляем прямоугольник
this.Region = new System.Drawing.Region(G); // Выбираем прямоугольник
}
}

Событие Click кнопки Выбор директории:

private void button9_Click(object sender, EventArgs e)


{
SelectBrowser.ShowDialog();
textBox1.Text = SelectBrowser.SelectedPath; // Заносим значение пути в
textBox1
}

Событие Tick таймера:

private void TimerOneMinute_Tick(object sender, EventArgs e)


{
Byte Seconds = Convert.ToByte(DateTime.Now.Second);
progressBar1.Value = Seconds;
}

Изменим код метода LWP05Main() файла LWP05Main.cs:

public LWP05Main()
{
InitializeComponent();
/* Инициализируем массив элементов для checkedListBox1 */
String[] CheckItems = { "Непрозрачность окна в %", "Овальное окно" };
checkedListBox1.Items.AddRange(CheckItems);
/* Устанавливаем режим выбора элемента с двойного нажатия на одинарный */
checkedListBox1.CheckOnClick = true;
NumericAplha.Value = 100; // Начальное значение
progressBar1.Maximum = 60; // Граница индикаторы выполнения
}

Добавим в этом же файле два пространства имён:

using System.Drawing.Drawing2D; // Объявляем пространство имён для рисования 2D-графики

122
using System.IO;

Компилируем, проверяем работоспособность:

Рис. 5. 2. Окончательная работа блока стандартных диалоговых окон (другой цвет


формы, результат выбора шрифта для формы и директории)

123
Рис. 5. 3. Окончательная работа блока стандартных диалоговых окон
(25% прозрачности на фоне рабочего стола, овальность и диалог выбора шрифта)

Единственное о чём мы забыли. Для того чтобы изменить прозрачность на новое


значение, нужно снять галочку и снова установить после выбора нового значения.
Исправим это событием ValueChanged элемента управления NumericAlpha:

private void NumericAplha_ValueChanged(object sender, EventArgs e)


{

if (checkedListBox1.CheckedItems.Contains("Непрозрачность окна в %") == true)


// Если элемент выделен
{
this.Opacity = Convert.ToDouble(NumericAplha.Value) / 100; //
Прозрачность: значение NumericAplha / 100 (0 - полна прозрачность, 1 - полная
непрозрачность)
}
}

6. Модификация приложения Windows Forms: открытие файла, сохранение


файла и работа с текстом

Расставим третью группу элементов. Добавим поле расширенной работы с


текстом и организуем небольшой текстовый редактор с сохранением и открытием

124
файлов, а также добавим контекстное меню по щелчку правой кнопки мыши на любой
пустой позиции формы.

Расставим третью группу следующим образом:

Рис. 5. 1. Расстановка элементов третьей группы (Открытие файла, сохранение


файла и работа с текстом)

Перетащим с панели элементов новый элемент ContextMenuStrip. Установим


для него свойство (Name): MenuRight. Заполним его элементами также как и главное
меню Файл:

Если редактирование прямо на форме не слишком наглядно, можно также


воспользоваться расширенным редактором коллекции элементов для меню. Он также
подходит для обычного меню MenuStrip, элемента ToolStrip и строки состояния
StatusStrip.

125
Рис. 6. 1. Редактор коллекции элементов (для MouseRight)

С панели инструментов добавим также элементы диалоговых окон сохранения и


загрузки: OpenFileDialog и FileSaveDialog. Установим им имена (Name)
соответственно: FileOpen и FileSave:

Свойства элемента FileSave:


(Name): FileSave
FileName: Документ
^ Здесь указываем название файл без разширения по умолчанию.
InitialDirectory: <ваш путь>
Если нужно указать стартовую директорию для сохранения. Путь может
выглядеть так: D:\Folder1\Folder2
DefaultExt: Файлы RTF|*.rtf
^ Здесь указывается суффикс файла сохранения и название этого суффикса.
Filter: Файлы RTF|*.rtf|Текстовый документ|*.txt|
Текст OpenDocument|*.odt|Все файлы|*.*

ПРИМЕЧЕНИЕ № 4: Свойства DefaultExt и Filter записываются через


разделители «|». Напомним, что в лабораторных работах данного практикума запись
значений для полей свойств элементов управления иногда такова: «Имя свойства:
<Значение>.» — точка после закрывающей кавычки не относится к значению, поэтому
не стоит заносить скажем в поле принимающее только целое числовое значение ещё и
точку. Среда выдаст ошибку либо уберёт точку.

Пример (задание точки в имени элемента):

126
Рис. 6. 2. Ошибочное задание поля (Name)

Свойства элемента FileOpen:


(Name): FileOpen
FileName: Документ
^ Здесь указываем название файл без расширения по умолчанию.

Имена остальных элементов оставляем как есть:

(Name): button10
Text: Цвет
ToolTip на Hint: Цвет выделенного текста

(Name): button11
Text: Шрифт
ToolTip на Hint: Шрифт выделенного текста

(Name): button14
Text: Фон
ToolTip на Hint: Фон выделенного текста

(Name): button15
Text: Копировать
ToolTip на Hint: Копировать выделенный текст в буфер

127
обмена

(Name): button13
Text: Выделить всё

(Name): button12
Text: Очистить

Событие Click кнопки Цвет:

private void button10_Click(object sender, EventArgs e)


{
ColorSelect.ShowDialog();
richTextBox1.SelectionColor = ColorSelect.Color; // Меняем цвет выделенного
текста richTextBox1
}

Событие Click кнопки Шрифт:

private void button11_Click(object sender, EventArgs e)


{
try
{
FontSelect.ShowDialog();
richTextBox1.SelectionFont = FontSelect.Font; // Меняем шрифт выделенного
текста richTextBox1
}
catch
{
// Ловим ошибку: шрифт не TrueType
}
}

Событие Click кнопки Фон:

private void button14_Click(object sender, EventArgs e)


{
ColorSelect.ShowDialog();
richTextBox1.SelectionBackColor = ColorSelect.Color; // Меняем фон
выделенного текста richTextBox1
}

Событие Click кнопки Копировать:

private void button15_Click(object sender, EventArgs e)


{
richTextBox1.Copy(); // Копируем выделенный фрагмент текста в элементе
}

Событие Click кнопки Очистить:

private void button12_Click(object sender, EventArgs e)


{
richTextBox1.Clear(); // Очистка richTextBox1
}

Событие Click кнопки Выделить всё:

128
private void button13_Click(object sender, EventArgs e)
{
richTextBox1.Focus(); // Фокус на элемент
richTextBox1.SelectAll(); // Выделяем весь текст
}

Событие Click кнопки главного меню Открыть:

private void открытьToolStripMenuItem_Click(object sender, EventArgs e)


{
try
{
// Открываем диалог открытия файла
FileOpen.ShowDialog();
// Создаём поток для содержимого файла
Stream FStream = FileOpen.OpenFile();
// Загружаем файл (открываем форматированный текст)
richTextBox1.LoadFile(FStream, RichTextBoxStreamType.RichText);
// Закрываем поток и высвобождаем память
FStream.Close();
this.Text = Title + " :: " + FileOpen.FileName;
}
catch
{
// Ловим ошибку: файл не найден
}
}

Событие Click кнопки главного меню Сохранить как…:

private void сохранитьToolStripMenuItem_Click(object sender, EventArgs e)


{
// Создаём строковую переменную для хранения имени сохранённого файла
String FileSavedAs;
// Создаём поток в памяти
MemoryStream MStream = new MemoryStream();
// Cоздаём поток для файл
Stream FStream;

if (FileSave.ShowDialog() == DialogResult.OK) // По завершении диалога и


нажатии кнопки ОК...
{
// Связываем поток с именем существующего файла, если файла нет, то
создаём новый файл
FStream = FileSave.OpenFile();
// Меняем положение в потоке
MStream.Position = 0;
// Сохраняем в поток содержимое richTextBox1 (сохраняем форматированный
текст)
richTextBox1.SaveFile(MStream, RichTextBoxStreamType.RichText);
// Записываем информацию в файл
MStream.WriteTo(FStream);
// Закрываем поток и высвобождаем память
FStream.Close();
FileSavedAs = FileSave.FileName;
this.Text = Title + " :: " + FileSavedAs; // После сохранения укажем
сохранённый файл в заголовке
}
}

Модифицируем событие Click кнопки :

private void button4_Click(object sender, EventArgs e)

129
{
SaveQuestion(); // Вызов метода обработки "правильного" закрытия предложения
с выводом диалога сохранения данных
}

Сам метод SaveQuestion(), добавим в этом же файле LWP05Main.cs:

void SaveQuestion()
{
DialogResult Result = MessageBox.Show("Сохранить изменения в документе?",
Title + " :: Сохранение изменений", MessageBoxButtons.YesNo);

switch (Result) // Оператор переключения switch, получает значения Result


результата выполнения диалога
{
case DialogResult.Yes:
{
сохранитьToolStripMenuItem_Click(null, null); // Генерируем событие
нажатия кнопки Сохранить как...
Close();
break;
}
case DialogResult.No: { Close(); return; }
}
}
В начале файла добавим строковую переменную:

public partial class LWP05Main : Form


{
String Title; // Строковая переменная для хранения заголовка приложения

Модифицируем метод LWP05Main() следующим образом:

public LWP05Main()
{
InitializeComponent();
/* Инициализируем массив элементов для checkedListBox1 */
String[] CheckItems = { "Непрозрачность окна в %", "Овальное окно" };
checkedListBox1.Items.AddRange(CheckItems);
/* Устанавливаем режим выбора 'ktvtynf с двойного нажатия на одинарный */
checkedListBox1.CheckOnClick = true;
NumericAplha.Value = 100; // Начальное значение
progressBar1.Maximum = 60; // Граница индикаторы выполнения
Title = this.Text; // Сохраняем первоначальный заголовок
}

Теперь обработаем контекстное меню. События Click кнопок контекстного меню


Открыть, Сохранить как… и Выход:

private void открытьToolStripMenuItem1_Click(object sender, EventArgs e)


{
открытьToolStripMenuItem_Click(null, null); // Вызываем метод события Click
главного меню для пункта Открыть
}

private void сохранитьКакToolStripMenuItem_Click(object sender, EventArgs e)


{
сохранитьToolStripMenuItem_Click(null, null); // Вызываем метод события Click
главного меню для пункта Сохранить как...
}

private void выйтиToolStripMenuItem_Click(object sender, EventArgs e)

130
{
SaveQuestion();
}

События MouseEnter оставшихся кнопок главного меню:

private void открытьToolStripMenuItem_MouseEnter(object sender, EventArgs e)


{
StatusLabel.Text = открытьToolStripMenuItem1.Text + " (" +
открытьToolStripMenuItem1.ShortcutKeyDisplayString + ")";
}

private void сохранитьToolStripMenuItem_MouseEnter(object sender, EventArgs e)


{
StatusLabel.Text = сохранитьКакToolStripMenuItem.Text + " (" +
сохранитьКакToolStripMenuItem.ShortcutKeyDisplayString + ")";
}

Компилируем, проверяем работоспособность:

Рис. 6. 3. Окончательная работа третьего блока (Открытие файла, сохранение файла и


работа с текстом)

7. Модификация приложения Windows Forms: прочее

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

131
содержит один флаг, нажатие на который отображает два элемента RadioButton.
Выбор одного из них высвечивает определённый элемент. Один из них: текстового
поля с форматированием по маске, а второй: выбор даты и времени по календарю.

Расставим последнюю группу следующим образом:

Рис. 7. 1. Расстановка элементов последней группы (Прочее)

Рассмотрим группу подробнее. На рисунке выше в GroupBox с текстом Прочее,


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

Оставим для этого элемента имя по умолчанию (tabControl1). Нас интересует


добавление новых вкладок. Для этого перейдём в свойства элемента и найдём поле
TabPages. Здесь мы видим значение: (Коллекция). Добавление элементов

132
аналогично ComboBox и подобным элементам. Через TabPages переходим в Редактор
коллекции TabPages:

Рис. 7. 2. Редактор коллекции TabPages

Здесь мы можем отредактировать всё, что связано с каждой вкладкой


(фактически вкладка похожа на отдельную форму). Также можем изменить порядок
отображение вкладок, добавить новые или удалить ненужные вкладки.
Оставим имена вкладок, как есть, изменишь лишь заголовки (поля Text
вкладки). У нас должно получиться три вкладки с заголовками Вкладка № 1, Вкладка
№ 2 и Простые часы соответственно (под именами tabPage, tabPage2 и tabPage3).
На первой вкладке расположим CheckBox:

Расположение CheckBox:

На второй вкладке установим кнопку:

133
Параметры для этой кнопки:
(Name): button16
Text: Выбрать изображение

На третьей вкладке установим кнопку:

Параметры для этой кнопки:


(Name): button17
Text: Простые часы

В правой части от вкладок расположим сначала элемент PictureBox (имя


pictureBox1 оставим как есть):

А затем поверх расположим две элемента выбора (переключателя) RadioButton:

Элемент MasketTextBox:

134
(Name): masketTextBox1
PromptChar: *
Visible: False

Элемент DateTimePicker:

(Name): dateTimePicker1
Visible: False

На рисунке № 7. 1. PictureBox обозначен пунктирными линиями. Параметры


RadioButton () такие:

(Name): radioButton1
Text: Специальное поле
Visible: False

(Name): radioButton2
Text: Дата и время
Visible: False

Для работы переключателей RadioButton необходимо объединять их в группы.


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

Для поля форматирования по маске важное свойство указывается в поле Mask:


там выбирается формат отображения данных. Выберем маску ввода Short date and
time (US). Значение поле установится такое: 00/00/0000 90:00. Для отображения
всех возможных предустановленных масок ввода, нужно войти в (…) поля Mask:

135
Рис. 7. 3. Все возможные маски ввода

Также здесь же можно изменить готовую или задать свою (<Специальный>).

Добавим обработчики событий. Для кнопки Выбрать изображение:

private void button16_Click(object sender, EventArgs e)


{
try
{
checkBox1.Checked = false;
radioButton1.Visible = false;
radioButton2.Visible = false;
maskedTextBox1.Visible = false;
dateTimePicker1.Visible = false;
// Открываем диалог открытия файла
FileOpen.FileName = "Изображение";
FileOpen.ShowDialog();
String ImageName = FileOpen.FileName;
// Создаём поток для содержимого файла
Stream FStream = FileOpen.OpenFile();
// Загружаем файл (открываем форматированный текст)
pictureBox1.Image = Image.FromFile(ImageName);
// Закрываем поток и высвобождаем память
FStream.Close();
this.Text = Title + " :: " + FileOpen.FileName;
}
catch
{
// Ловим ошибку: файл не найден
}
}

Для выделения переключателя с флагом CheckBox (событие CheckedChanged,


аналог Click):

136
private void checkBox1_CheckedChanged(object sender, EventArgs e)
{

if (checkBox1.Checked == true)
{
radioButton1.Visible = true;
radioButton2.Visible = true;
}
else
{
radioButton1.Visible = false;
radioButton2.Visible = false;
maskedTextBox1.Visible = false;
dateTimePicker1.Visible = false;
}
}

И аналогичные события для RadioButton’ов:

private void radioButton1_CheckedChanged(object sender, EventArgs e)


{
maskedTextBox1.Visible = true;
dateTimePicker1.Visible = false;
}

private void radioButton2_CheckedChanged(object sender, EventArgs e)


{
maskedTextBox1.Visible = false;
dateTimePicker1.Visible = true;
}

Событие MouseHover (возникает в случае задержки курсора мышки на


элементе) для PictureBox:

private void pictureBox1_MouseHover(object sender, EventArgs e)


{
if (pictureBox1.Image != null)
Hint.SetToolTip(pictureBox1, "Размер изображения: " + "\nШирина: " +
pictureBox1.Image.PhysicalDimension.Width + "\nВысота: " +
pictureBox1.Image.PhysicalDimension.Height);
}

Теперь разберёмся с третьей вкладкой TabControl. Единственная кнопка данной


вкладки («Простые часы») будет вызывать новую форму. Добавим её, выполним
последовательно «Проект» –> «Добавить форму Windows...». В открывшемся окне в
поле Имя вводим LWP05Clock.cs и жмём Добавить. Получаем новую форму в
обозревателе решений. Поменяем её свойства следующим образом:

Text изменим с LWP05Clock на Элементы


управления (C#) :: Простые часы
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 400;

137
150
^ Поменяем размер формы.
FormBorderStyle изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.

Основу часов должен составлять всё тот же простой таймер (Timer). Но, к
сожалению, точность таймера ОС оставляет желать лучшего. Сообщения таймера,
создающие события Tick, проходят через очередь приложения. К тому же, другие
приложения могут блокировать на некоторое время работу Вашего приложения.
Поэтому события таймера возникают в общем случае нерегулярно. Кроме того,
несмотря на возможность указания интервалов времени в миллисекундах, реальная
дискретность таймера определяется периодом прерываний, посылаемых таймером.

Нерегулярность прихода сообщений таймера не вызывает особых проблем, если


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

Добавим на форму LWP05Clock элемент Timer и изменим его свойства


следующим образом:

(Name): Clock
Interval: 1000

Рис. 7. 4. Свойства добавленного таймера Clock

138
По умолчанию таймер создается в заблокированном состоянии. Чтобы его
разблокировать, необходимо записать значение true в свойство Enabled. Сделаем это в
коде инициализации формы. В файле LWP04Clock.cs найдём:

public LWP05Clock()
{
InitializeComponent();
}

Заменим этим кодом:

public LWP05Clock()
{
InitializeComponent();
Clock.Enabled = true;
}

Таймер генерирует события Tick, период которых задается свойством Interval.


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

Установим на форме один TextBox со следующими свойствами:

(Name): Now
ReadOnly: True

Две кнопки слева и справа под TextBox со свойствами:

(Name): button1
Text: Старт

(Name): Button2
Text: Стоп

А также поставим на форму не совсем стандартный элемент (отсутствует в


группе Стандартные элементы правления на панели элементов). Им станет
TrackBar:

Найти его можно просто раскрыв список Все формы Windows Forms панели
элементов.

TrackBar:
(Name): Seconds

139
Maximum: 60
Minimum: 1

Элемент управления TrackBar представляет собой шкалу с движком, с помощью


которого пользователь может изменять (регулировать) численное значение. Это может
быть уровень громкости, баланс звуковых каналов, насыщенность отдельных
компонентов цвета, яркость и пр.
Движок элемента управления TrackBar можно передвигать мышью, клавишами
перемещения курсора, а также клавишами Home, End, Page Up и Page Down. При
перемещении движка создаются события Scroll.
Помимо движка, в окне элемента управления TrackBar есть деления. Они
отображаются в виде коротких штрихов, расположенных на равном расстоянии друг от
друга.
Можно выбрать горизонтальное или вертикальное расположение окна TrackBar.
Деления могут находиться с любой стороны, с обеих сторон или их может не быть
совсем.
При создании элемента управления TrackBar приложение должно определить
диапазон значений, соответствующих положению движка, шаг делений, а также шаг
изменения этих значений.

Свойство Value хранит текущее значение, непосредственно связанное с


положением движка в окне регулятора. Программа может не только читать это
свойство, но и писать в него. При этом положение движка будет изменяться
соответствующим образом.
Свойства Minimum и Maximum определяют, соответственно, минимальное и
максимальное значение, связанное с движком. По умолчанию минимальное значение
равно 1, а максимальное — 10.
Дискретность изменения значения при использовании клавиш перемещения
курсора задается свойством SmallChange. По умолчанию она равна 1. Что же
касается дискретности при использовании клавиш Page Up и Page Down, то её можно
задать с помощью свойства LargeChange. По умолчанию значение этого свойства
равно 5.
Чтобы задать количество штрихов, отображаемых на шкале элемента управления
TrackBar, нужно отредактировать свойство TickFrequency.
Изменяя свойство Orientation, можно задать горизонтальное или вертикальное
расположение окна элемента управления TrackBar. В первом случае свойство должно
содержать значение System.Windows.Forms.Orientation.Horizontal, а во втором —
System.Windows.Forms.Orientation.Vertical.
Свойство TickStyle задает стиль шкалы и ползунка. Вот возможные значения:
None; TopLeft; BottomRight; Both.
В первом случае, при использовании значения None, штрихи не отображаются на
шкале. Остальные константы позволяют задать расположение штрихов сверху или
снизу (справа или слева) от движка, а также по обе стороны движка (значение Both).

Последний штрих. Размести под TrackBar один Label (чтобы использовать


TrackBar по назначению).

Окончательный вид формы будет таким:

140
Рис. 7. 5. Расстановка элементов последней формы LWP05Clock

Перепишем код метода LWP05Clock():

public LWP05Clock()
{
InitializeComponent();
Clock.Enabled = true;
label1.Text = "";
}

Инициализируем единственное событие таймера Clock — Tick:

private void Clock_Tick(object sender, EventArgs e)


{
DateTime dt = DateTime.Now;
Now.Text = dt.Hour + ":" + dt.Minute + ":" + dt.Second;

if (Clock.Enabled == true)
{
button1.FlatStyle = FlatStyle.Standard;
button1.BackColor = Color.LawnGreen;
}
label1.Text = "";
Seconds.Value = dt.Second;
}

Проинициализируем событие Scroll для TrackBar:

private void Seconds_Scroll(object sender, EventArgs e)


{
label1.Text = Seconds.Value.ToString();
}

Событие Click кнопки Старт:

private void button1_Click(object sender, EventArgs e)


{
Clock.Start();
button2.FlatStyle = FlatStyle.System;
button1.FlatStyle = FlatStyle.Standard;
button1.BackColor = Color.LawnGreen;
}

Событие Click кнопки Стоп:

private void button2_Click(object sender, EventArgs e)


{
Clock.Stop();
button1.FlatStyle = FlatStyle.System;
button2.FlatStyle = FlatStyle.Standard;

141
button2.BackColor = Color.IndianRed;
}

Событие Click кнопки Простые часы главной формы LWP05Main:

private void button17_Click(object sender, EventArgs e)


{
LWP05Clock Form = new LWP05Clock();
Form.ShowDialog();
}

8. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан на


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

Рис. 8. 1. Модифицированное приложение Windows Forms

Отображение выбранного с помощью диалога изображения и всплывающей


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

142
Рис. 8. 2. Модифицированное приложение Windows Forms : результат работы
переключателей на фоне уже выбранного изображения

143
Рис. 8. 3. Модифицированное приложение Windows Forms: результат работы формы
Простые часы (ползунок двигается с каждой секундой при работающих часах)

9. О приложении к Лабораторной работе № 5

Получившуюся программу (LWP05WindowsForms04.exe), собранную из кусков


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

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

6. Лабораторная работа № 6: SolidWorks — работа с SolidWorks

Лабораторная работа № 6: SolidWorks — работа с SolidWorks

Содержание

10.Вводная часть
11.Создание приложения Windows Forms
12.Модификация приложения Windows Forms: eDrawings 2012
13.Модификация приложения Windows Forms: организация работы с
SolidWorks через приложение

144
14.Завершающая часть
15.О приложении к Лабораторной работе № 6

1. Вводная часть

В этой работе будет рассмотрены некоторые особенности работы с программным


комплексом САПР: SolidWorks (версии 2012 с установленным пакетом обновлений
Service Pack 2.0).

Рис. 1. 1. Логотип разработчика Dassault Systemes

SolidWorks — система автоматизированного проектирования, инженерного


анализа и подготовки производства изделий любой сложности и назначения,
разрабатываемая и поддерживаемая компанией SolidWorks Corp. (США),
подразделением Dassault Systemes (Франция). SolidWorks является ядром
интегрированного комплекса автоматизации предприятия, с помощью которого
осуществляется поддержка жизненного цикла изделия в соответствии с концепцией
CALS-технологий, включая двунаправленный обмен данными с другими Windows-
приложениями и создание интерактивной документации.

Разработка SolidWorks началась в 1993 г., продажи стартовали в 1995 г. Система


стала первой САПР, поддерживающей твердотельное моделирование для платформы
Windows. 30 апреля 2009 г. было объявлено о продаже 1,000,000-ой лицензии на
SolidWorks.

В версии SolidWorks 2009 помимо стандартного мощного набора


инструментария API (Application Programming Interface) была добавлена
поддержка языков Microsoft Visual Basic .NET и Visual C#.

Рис. 1. 2. Выдержка из русскоязычной справки о новых возможностях SolidWorks 2009

По умолчанию, для работы с макросами на VB.NET или Си-шарп (создаваемыми


при записи самой программой SolidWorks) используется так называемый VSTA: Visual
Studio Tools for Applications. VSTA представляет собой набор инструментов, который
независимые поставщики программного обеспечения (ISV) могут использовать для
создания настроечных возможностей в своих приложениях для автоматизации и

145
расширения. Эти возможности могут использоваться конечными пользователями в
рамках управляемых расширений.
Visual Studio Tools for Applications был объявлен Microsoft с выпуском Visual
Studio 2005. Первый Community Technology Preview (CTP) из Visual Studio для
приложений был выпущен в апреле 2006 года. Он входит в состав Office 2007 для
использования конечными пользователями и разработчиками бизнес-приложений, и
SDK доступна отдельно для независимых поставщиков ПО.
Текущей версией является Visual Studio Tools for Applications 2.0. Вторая версия
Visual Studio Tools for Applications включает в себя такие функции, как динамическое
программирование модели и поддержка WPF, WCF, WF, LINQ и NET 3.5.
Независимые поставщики программных продуктов желающих интегрировать
Visual Studio Tools for Applications в свои приложения должны платить за лицензию
Microsoft.

2. Создание приложения Windows Forms

Для работы с макросами в SolidWorks используется VSTA (Visual Studio 2005), и


запись макроса на C# производится в специальный шаблон проекта. После окончания
записи, происходит запуск VSTA, загрузка шаблона и весь код из SolidWorks
копируется в определённое место шаблона. По умолчанию пустой шаблон
(поставляемый с SolidWorks 2012) достаточно примитивен и несовместим для
преобразования (из старого типа проекта в новый) с Visual Studio 2010 (только с Visual
Stduio 2005 и 2008). Не меняя шаблон по умолчанию и не используя так называемые
шаблоны «дополнений» (Addin) работать с проектами макросов напрямую из Visual
Studio 2010 нельзя. Однако ничто не запрещает копировать записанный код в заранее
подготовленный проект под Visual Studio 2010. Также сам VSTA не позволяет создавать
исполняемые файлы на основе записанных макросов, а лишь библиотеки. Сам
SolidWorks способен запускать и исполнять эти библиотеки (*.dll) или собственные
макросы (*.swp и *.swb):

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

146
Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

Рис. 2. 2. Окно создания нового проекта

147
В поле Имя вводим LWP06SW01 — это название программы (выбрано по
названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

148
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

149
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms: eDrawings 2012

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP06Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Работа с SolidWorks
*C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 640;
480
^ Поменяем размер формы.

150
ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,
необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения

Добавим на нашу форму ToolTip ( ).

Параметры добавленного элемента всплывающей подсказки таковы:

(Name): Hint

Установим на форме в левом верхнем углу кнопку Button. Параметры кнопки


будут следующими:

(Name): eDButton

151
Text: Выбрать деталь, чертёж или сборку
Size: 200; 23

Теперь добавим сам элемент, отвечающий за просмотр детали (чертежа или


сборки). Для этого добавим на панель элементов новый элемент: eDrawings 2012
Control.

Для добавления последовательно выполним: Сервис -> Выбрать элементы


панели элементов... ->в открывшемся окне перейти на вкладку COM-компоненты:

Рис. 3. 2. Добавление нового элемента eDrawings 2012 Control

Если по каким-либо причинам в списке COM-объектов его не окажется, нажмём


Обзор и найдём EModelView.dll. Искать необходимо в директории: <исходный путь
до директории установки SolidWorks>\SolidWorks eDrawings\
Добавленный элемент теперь можно использовать:

Перетащим этот элемент на форму и растянем, как показано на рисунке ниже:

152
Рис. 3. 3. Начальная расстановка элементов

Параметры добавленного элемента eDrawings таковы:

(Name): eDView
ToolTip на Hint: Деталь, чертёж или сборка

Двойным щелчком по кнопке eDButton создаём событие Click, код такой:

private void eDButton_Click(object sender, EventArgs e)


{
eDview.OpenDoc("C:\\Test.sldprt", false, false, true, "");
}

Путь к файлу указывается в самом начале, первый параметр false указывает на


принадлежность isTemp файла (временный ли файл?). Второй false: определяет
можно ли сохранять в файл что-либо. Третий параметр true устанавливает параметр
только для чтения на файл.

Компилируем приложение (Debug) и запускаем. В корневой каталог поместим


любой файл с деталью, чертежом или сборкой под названием «Test.sldprt». После
нажатия кнопки, этот файл будет открыт компонентом eDrawings и будет отображено
содержимое (на рисунке ниже сборка показана циклона, отображённая в компоненте).
Результат работы показан ниже (Рис. 3. 4):

153
Рис. 3. 4. Работа COM-компонента eDrawings 2012 Control

При нажатии правой клавиши мышки в области просмотра открывается


встроенное меню просмотра (на рисунке выше). Вид можно приближать и отдалять
(среднее колесо мыши), а также вращать (при нажатой ЛКМ).

Немного усовершенствуем приложение. Организуем возможность открытия


любого файла детали, чертежа или сборки, а также файлов, которые можно открывать
в самой eDrawings 2012. Добавим обычное поле редактирования TextBox:

(Name): eDPath
ReadOnly: True

Разместим его в свободном месте между кнопкой и COM-компонентом на нашей


форме. Также добавим элемент OpenFileDialog, открывающий стандартный диалога
выбора файла при помощи проводника Windows ( ):

(Name): eDFind
FileName: Деталь, чертёж или сборка

Отредактируем код единственной кнопки так:

private void eDButton_Click(object sender, EventArgs e)


{
eDFind.ShowDialog();
eDPath.Text = eDFind.FileName;
eDview.OpenDoc(eDPath.Text, false, false, true, "");

154
}

Компилируем приложение (Debug) и запускаем. Теперь можно открывать для


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

Рис. 3. 4. Работа COM-компонента eDrawings 2012 Control

4. Модификация приложения Windows Forms: организация работы с


SolidWorks через приложение

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

Для начала необходимо подключить необходимые библиотеки (подключим их


через добавление ссылок в проект). Ссылки две: SldWorks и SwConst. Для
добавления, нажмём ПКМ в обозревателе решений на пункте Ссылки, далее в
контекстном меню Добавить ссылку... и в открывшемся окне перейдём на вкладку
COM. Теперь, нам нужно добавить два компонента (для SolidWorks 2010-2012 все
компоненты называются одинаково, различаются только цифрой в версии). Нам нужено
(Имя компонента): SldWorks 2012 Type Library и SolidWorks 2012 Constant type
library. Обе библиотеки располагаются в корневом каталоге SolidWorks (<исходный
путь до директории установки SolidWorks>\SolidWorks\):

155
Рис. 4. 1. Добавить ссылку: выбор нужных компонентов для добавления

Рис. 4. 2. Результат добавления новых ссылок

Теперь нужно, чтобы наше приложение выполняло запуск лишь в том случае,
если не запущено других экземпляров SolidWorks. Сделаем проверку на загрузку
процесса SLDWORKS.exe в коде. Добавим в самом низу формы три кнопки: Запустить
SolidWorks и Послать команду.

Запустить SolidWorks, создать деталь (Button):


(Name): BStart
Text: Запустить SolidWorks, создать деталь

156
Size: 300; 23
Послать команду, выгрузить (Button):
(Name): BSend
Text: Послать команду, выгрузить
Size: 300; 23
Фоновый режим (запуск, действие, выгрузка) (Button):
(Name): BHide
Text: Фоновый режим (запуск, действие,
выгрузка)
Size: 300; 23

Впишем ещё одну ссылку для работы со списком запущенных процессов в самое
начало файла LWP06Main.cs:

using System.Diagnostics;

А также ещё две ссылки:

using SldWorks;
using SwConst;

Первая кнопка будет запускать SolidWorks (если не запущено других


экземпляров приложения) и создавать там пустую деталь. Добавим код события Click
для кнопки BStart:

private void BStart_Click(object sender, EventArgs e)


{
BHide.Enabled = false;
swApp = new SldWorks.SldWorks(); // Создаём экземпляр Solidworks приложения
(SolidWorks 2012)

if (swApp == null) // Если SolidWorks не запустился, посылаем сообщение; если


запустился, гасим кнопку запуска
{ MessageBox.Show("Невозможно запустить экземпляр SolidWorks 2012!", "Работа
с SolidWorks (C#) :: Запуск SolidWorks"); }
else
{
BStart.Enabled = false;
BHide.Enabled = false;
}
swApp.Visible = true; // Вытаскиваем окно SolidWorks из фонового режима
swApp.NewPart(); // Создаём документ с новой деталью
}

Вторая кнопка будет вызывать всплывающее окно в SolidWorks средствами API с


той информацией, которую будет передавать из нашего приложения, а затем выгружать
SolidWorks. Добавим код события Click для кнопки BSend:

private void BSend_Click(object sender, EventArgs e)


{
try
{
swApp.SendMsgToUser2("Посылаем команды в открытое приложение
SolidWorks!", (int)SwConst.swMessageBoxIcon_e.swMbInformation,
(int)SwConst.swMessageBoxBtn_e.swMbOk);

157
// Выгрузка фонового процесса не произойдёт из-за некорректного
объявления экземпляра класса;
// это было допущено сознательно для демонстрации возможностей работы
открытого приложения и самого Solidworks.
// Правильная работа ExitApp() будет реализована так:
// некая_функция() {
// SldWorks.SldWorks swApp = new SldWorks.SldWorks();
// /* действия */
// swApp.ExitApp();
// swApp = null;
// }
swApp.ExitApp();
/* Выгрузим процесс */
foreach (Process PSW in Process.GetProcessesByName(SW))
PSW.Kill();
}
catch { MessageBox.Show("Невозможно послать команду экземпляру SolidWorks
2012: экземпляр приложения не найден!", "Работа с SolidWorks (C#) :: Отсылка команды в
SolidWorks"); }
}

Также для главного окна формы (LWP06Main) инициализируем событие


MouseEnter. Для этого один нажмём на заголовок формы в поле конструктора,
перейдём на свойства формы и нажмём на знак молнии (События). Код события
MouseEnter:

private void LWP06Main_MouseEnter(object sender, EventArgs e)


{
PList = Process.GetProcessesByName(SW); // Получаем список процессов с именем
SLDWORKS

if (PList.Length == 0) // Если такого процесса нет, активируем кнопку запуска


SolidWorks
{
BStart.Enabled = true;
BHide.Enabled = true;
}
}

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

Добавим объявление переменных после строчки:

public partial class LWP06Main : Form


{

Впишем:

String SW = "SLDWORKS";
Process[] PList;
SldWorks.SldWorks swApp;

Функцию LWP06Main() перепишем так:

public LWP06Main()
{
InitializeComponent();
BStart.Enabled = false;

158
BHide.Enabled = false;
}

Последняя кнопка (BHide) будет выполнять фоновую загрузку SolidWorks,


построение3 простой детали, сохранение на диск этой детали и выгрузку самого Solid’а.

ПРИМЕЧАНИЕ № 3: При записи макроса на языке C# могут возникнуть


проблемы с работой VSTA и непосредственно передачей кода в шаблон. В случае, если
макрос на основе шаблона не создаётся или не записывается код (открывается пустой
VSTA), необходимо:
1. Вручную, в самом VSTA создать проект макроса (File -> New Project... далее
выбрать SolidWorksMacro).
2. Не закрывая VSTA, попытаться снова записать свои действия.
3. После нажатия на кнопку Остановить запись макроса, SolidWorks
предложит выбрать имя для проекта и место куда сохранить код).
4. VSTA должен будет создать корректный проект на основе шаблона и
скопировать в него код из SolidWorks, имя проекта будет соответствовать выбранному.
5. Ранее открытый пустой проект в VSTA не будет сохранён.

Рис. 4. 3. VSTA: New Project

Если в VSTA отсутствует шаблон проекта, этот шаблон необходимо поместить в


соответствующую директорию. По умолчанию VSTA подгружает шаблоны из директории
(для Windows XP, русская версия): Мои документы\SolidWorks Visual Studio Tools
for Applications\Templates\ProjectTemplates\Visual C#. В эту директорию
необходимо скопировать шаблон из стандартной поставки SolidWorks
(solidworks_macro.zip), который можно найти по следующему пути:<исходный путь
до директории установки SolidWorks>\SolidWorks\data\VSTA\csharp\1033\

159
Копировать необходимо архив без распаковки. Также стоит проверить пути,
которые использует VSTA для работы с SolidWorks. Для этого выполним в VSTA: Tools -
> Options, нажать на Project and Solutions.

Главные пути:

Visual Studio user project templates location:


Для Windows XP, русская версия: Мои документы\SolidWorks Visual Studio
Tools for Applications\Templates\ProjectTemplates
Для Windows 7, русская версия: Documents\SolidWorks Visual Studio Tools
for Applications\Templates\ProjectTemplates

Visual Studio user item templates location:


Для Windows XP, русская версия: Мои документы\SolidWorks Visual Studio
Tools for Applications\Templates\ItemTemplates
Для Windows 7, русская версия: Documents\SolidWorks Visual Studio Tools
for Applications\Templates\ItemTemplates

Также, в случае если есть проблемы с записью макросов, стоит сделать


следующее: в SolidWorks зайти в Параметры, в разделе Общие найти и снять галочку
с пункта Остановить отладчик VSTA при завершении работы макроса и поставить
галочку в пункте Автоматически редактировать макрос после записи.

160
Рис. 4. 4. Настройка SolidWorks для работы с VSTA

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


образом: скомпилировать dll в VSTA: Build -> Build <имя макроса>. Запустить
скомпилированный dll (из директории <имя макроса>\SwMacro\bin\Release) в
SolidWorks на новой детали (Выполнить макрос). Основная панель для работы с
макросами в SolidWorks выглядит так:

Возвращаемся к кнопке нашего приложения, которая будет строить деталь в


SolidWorks. Скопируем код записанного макроса построения простенькой детали в наш
проект, а именно в код события Click для кнопки BHide:

private void BHide_Click(object sender, EventArgs e)


{
BStart.Enabled = false;
SldWorks.SldWorks swAppHide = new SldWorks.SldWorks();

if (swAppHide == null)
{ MessageBox.Show("Невозможно запустить экземпляр SolidWorks 2012!", "Работа
с SolidWorks (C#) :: Запуск SolidWorks"); }
else
{

161
BStart.Enabled = false;
BHide.Enabled = false;
}
swAppHide.Visible = false;
swAppHide.NewPart();
// Начало макроса
ModelDoc2 swDoc = null;
PartDoc swPart = null;
DrawingDoc swDrawing = null;
AssemblyDoc swAssembly = null;
bool boolstatus = false;
int longstatus = 0;
int longwarnings = 0;
swDoc = ((ModelDoc2)(swAppHide.ActiveDoc)); // В макросе было использовано
swApp
swDoc = ((ModelDoc2)(swAppHide.ActiveDoc)); // В макросе было использовано
swApp
boolstatus = swDoc.Extension.SelectByID2("Спереди", "PLANE", 0, 0, 0, true,
0, null, 0);
RefPlane myRefPlane = null;
myRefPlane = ((RefPlane)(swDoc.FeatureManager.InsertRefPlane(8, 0.01, 0, 0,
0, 0)));
swDoc.ClearSelection2(true);
swDoc.ClearSelection2(true);
Array vSkLines = null;
vSkLines = ((Array)(swDoc.SketchManager.CreateCornerRectangle(0, 0, 0,
0.070797287636592099, 0.045512542052094929, 0)));
swDoc.ClearSelection2(true);
SketchSegment skSegment = null;
skSegment = ((SketchSegment)(swDoc.SketchManager.CreateCircle(0.035399,
0.022756, 0.000000, 0.048625, 0.029175, 0.000000)));
swDoc.ClearSelection2(true);
swDoc.SketchManager.InsertSketch(true);
boolstatus = swDoc.Extension.SelectByID2("Line2@Эскиз1", "EXTSKETCHSEGMENT",
0, 0.017504823866190358, 0, false, 0, null, 0);
swDoc.ShowNamedView2("*Триметрия", 8);
swDoc.ClearSelection2(true);
boolstatus = swDoc.Extension.SelectByID2("Line2@Эскиз1", "EXTSKETCHSEGMENT",
0, 0.017504823866190358, 0, false, 0, null, 0);
Feature myFeature = null;
myFeature = ((Feature)(swDoc.FeatureManager.FeatureExtrusion2(true, false,
false, 0, 0, 0.020000000000000004, 0.01, false, false, false, false,
0.017453292519943334, 0.017453292519943334, false, false, false, false, true, true, true,
0, 0, false)));
swDoc.ISelectionManager.EnableContourSelection = false;
swDoc.SetPickMode();
swDoc.ClearSelection2(true);
// Конец макроса
swDoc.SaveAsSilent("D:\\LWP06SW01-Деталь.sldprt", false);
swAppHide.ExitApp();
swAppHide = null;
MessageBox.Show("Деталь отрисована и сохранена на диск, работа SolidWorks
2012успещно завершена!", "Работа с SolidWorks (C#) :: Фоновый режим");
}

Код который записывает SolidWorks достаточно прост. Все операции по


построению детали совершаются последовательно. SolidWorks записывает абсолютно
все действия пользователя (даже повороты камеры во время построения), поэтому
иногда целесообразнее останавливать запись, чтобы в макрос не шло лишних
ненужных данных. Также имеет смысл останавливать запись и обновлять макрос для
«вылавливания» части кода, который например, отвечает за размер детали (чтобы
поставить в это место комментарий).

162
Макрос выше, сначала на виде Спереди вставляет справочную плоскость
(расстояние от вида Спереди: 10 мм). Затем рисует Эскиз (который находится
непосредственно на виде Спереди). Эскиз состоит из двух фигур: прямоугольника и
окружности. Затем происходит вытягивание на 20 мм фигуры заключённой между
внешнем прямоугольником и внутренней окружностью. Поясним код, который отвечает
за размеры фигур:

boolstatus = swDoc.Extension.SelectByID2("Спереди", "PLANE", 0, 0, 0, true,


0, null, 0);
RefPlane myRefPlane = null;
myRefPlane = ((RefPlane)(swDoc.FeatureManager.InsertRefPlane(8, 0.01, 0, 0,
0, 0)));
swDoc.ClearSelection2(true);

Здесь по порядку:
1. Выбор вида Спереди.
2. Задание экземпляра объекта Плоскость.
3. Отрисовка плоскости, где 8 это параметр первого ограничения (первая
привязка), а второй (0.01) и есть отступ в 10 мм по первой привязке (вид Спереди).\
4. Дальше идёт сбор выделения.

Array vSkLines = null;


vSkLines = ((Array)(swDoc.SketchManager.CreateCornerRectangle(0, 0, 0,
0.070797287636592099, 0.045512542052094929, 0)));
swDoc.ClearSelection2(true);

Это построение прямоугольника (эскиз как массив линий). Строит четыре линии
от начальной точки (три первых нуля), до конечной. В качестве начальной выбрана
Исходная точка. Конечная точка это:
 double X2 (ось X): ~70.7 мм.
 double Y2 (ось Y): ~45.5 мм.
 double Z2: 0.

SketchSegment skSegment = null;


skSegment = ((SketchSegment)(swDoc.SketchManager.CreateCircle(0.035399,
0.022756, 0.000000, 0.048625, 0.029175, 0.000000)));
swDoc.ClearSelection2(true);
swDoc.SketchManager.InsertSketch(true);

Построение окружности (по двум точкам: точка центра и точка на самой


окружности). Первый три координаты (double XC, double YC и double Zc) отвечают за
положение точки центра на эскизе. Последние три: за положение точки на окружности.
Как видно, функция отрисовки окружности не хранит напрямую параметр радиуса и
диаметра. В конце кода выше после строчки выполняющей снятие выделения идёт
строчка вставки эскиза (завершение его редактирования).

boolstatus = swDoc.Extension.SelectByID2("Line2@Эскиз1", "EXTSKETCHSEGMENT",


0, 0.017504823866190358, 0, false, 0, null, 0);
Feature myFeature = null;
myFeature = ((Feature)(swDoc.FeatureManager.FeatureExtrusion2(true, false,
false, 0, 0, 0.020000000000000004, 0.01, false, false, false, false,
0.017453292519943334, 0.017453292519943334, false, false, false, false, true, true, true,
0, 0, false)));

Вытягивание. Выбираем весь эскиз (в данном случае была выделена линия


прямоугольника). После выделения, весь эскиз выделяется жёлтым цветом. Далее идёт

163
создание экземпляра объекта для вытягивания и собственно последняя строчка
выполняет вытягивание в одну сторону (Направление 1) на 20 мм (0.02000...4).

Итак. «Читать» код SolidWorks достаточно просто. К тому же, при наведении
курсора на функции, сам VSTA также как и Visual Studio 2010 высвечивает подсказки
Достаточно скудные подсказки: лишь тип функций, их аргументы и тип аргументов,
однако в названии, как функций, так и аргументов уже заложено определение того,
для чего всё это можно использовать. Проще всего изучать API на многочисленных
примерах как в справочной системе SolidWorks (на локальной машине с установленным
SolidWorks), так и на официальном сайте (www.solidworks.com). Также можно
записывать макросы с различными действиями.

5. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан


ниже (Рис. 5. 1):

Рис. 5. 1. Работа кнопки Фоновый режим: на просмотр было открыто то что SolidWorks
собрал (деталь) и сохранил на диск D (LWP06SW01-Деталь.sldprt)

164
Рис. 5. 2. Работа кнопки Послать команду, выгрузить: окно SolidWorks с пустой деталью
и всплывающее окошко

6. О приложении к Лабораторной работе № 6

Получившуюся программу (LWP06SW01.exe), собранную из кусков кода


приведённых в данной лабораторной работе, деталь (файл LWP06SW01-Деталь.sldprt),
а также стандартный шаблон из поставки SolidWorks 2012 (solidworks_macro.zip) можно
загрузить по ссылке в конце этого материала (сслыка доступна в программном
продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

7. Лабораторная работа № 7: SolidWorks — использование SwCSharpAddin, работа с


макросом и шаблоном детали

Лабораторная работа № 7: SolidWorks — использование SwCSharpAddin,


работа с макросом и шаблоном детали

Содержание

16.Вводная часть
17.Создание приложения SwCSharpAddin
18.Модификация приложения SwCSharpAddin: макрос отрисовки болта
19.Модификация приложения SwCSharpAddin: построение через шаблон
детали
20.Завершающая часть
21.О приложении к Лабораторной работе № 6

165
1. Вводная часть

В этой работе будет рассмотрены некоторые особенности работы с программным


комплексом САПР: SolidWorks (версии 2012 с установленным пакетом обновлений
Service Pack 2.0).

В отличие от предыдущей работы (Лабораторная работа № 6), в данной


работе будет рассмотрен класс приложений подключаемых к SolidWorks и работающих
только при запуске самого SolidWекorks. Речь идёт об Addin-приложениях
(Добавление). Дополнения являются полноценными модулями для Solidworks, могут
использовать как собственный интерфейс (те же формы Windows Forms), так и
элементы пользовательского интерфейса SolidWorks. Фактически, добавления
встраиваются непосредственно в оболочку SolidWorks. Грамотно собранное стороннее
добавление можно запросто не отличить от «родных» инструментов SolidWorks.
Также в данной работе на основе добавления будет реализовано построение
шестигранного болта на основе разных «подходов» при создании детали.
1. Работа с кодом макроса, отстраивающего шестигранный болт по трём
параметрам, задаваемым пользователем во время исполнения кода.
2. Работа с кодом, отстраивающим шестигранный болт на основе шаблона
(используется заранее подготовленный шаблон, меняются внутренние
размеры детали, затем происходит сохранение новой детали). Пользователь
также указывает три параметра болта перед отрисовкой (изменением)
детали.

Вместе с установкой SolidWorks обязательно идёт установщик API SDK, который


содержит также шаблоны проектов Addin под разные среды разработки Visual Studio
(Visual Studio .NET, .ET 2003, 2005, 2008 и 2010, а также более ранние) и разные
языки, один из которых C#. SolidWorks API SDK устанавливается вручную.
Выбранный шаблон устанавливается в пользовательские документы (в независимости
от языка операционной системы, в папке пользователя будет создана папка: My
Documents. Весь путь будет выглядеть так: My Documents\<версия Visual Studio
под которую ставится шаблон>\ Templates\ProjectTemplates\Visual C#
После установки шаблона в конечной папке появится архив swcsharpaddin.zip.
Для русской версии ОС Windows XP соответственно необходимо переместить
конечное содержимое директории …\Visual C# в аналогичною директорию, но уже
находящуюся в «правильной» директории шаблонов: Мои документы\<версия
Visual Studio под которую ставится шаблон>\ Templates\ProjectTemplates\
Visual C#
Для Windows 7 и Visual Studio 2010 конечный путь выглядит так: C:\Users\
<имя текущего пользователя>\Documents\Visual Studio 2010\Templates\
ProjectTemplates\Visual C#
Разумеется, конечной директорией, где хранятся шаблоны, может быть выбрана
любая другая директория на дисек. Главное, необходимо правильно указать путь к этой
директории с шаблонами в visual Studio 2010:

166
Рис. 1. 1. Сервиc –> Параметры...: Размещения пользовательских шаблонов
проектов

Если Addin устанавливается простым копированием шаблона в папку для


шаблонов Visual Studio 2010, то его перед первым запуском необходимо будет
зарегистрировать, путём копирования библиотеки SolidWorksToolsWizard.dll в папку
C:\Windows\assembly

2. Создание приложения SwCSharpAddin

Добавление при сборке в среде разработки регистрируется в реестре


операционной системы и изменяет некоторые ключи, поэтому для работы с
добавлением необходимы права Администратора ОС. Запускаем Visual Studio 2010 под
учётной записью с административными параметрами, выбирая пункт: Запуск от имени
администратора (правая кнопка мыши по ярлыку). Либо изменив параметр
Совместимость -> Уровень прав: (поставить галочку) Выполнять эту программу
от имени администратора в свойствах ярлыка или самого приложения Visual Studio
— devenv.exe):

167
Рис. 2. 1. Расположение исполняемого файла devenv.exe

Откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 2. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке SwCSharpAddin. Также здесь можно выбрать какой использовать
«фреймворк» (набора компонентов для написания программ). В нашем случае выберем
.NET Framework 4.

168
Рис. 2. 3. Окно создания нового проекта

В поле Имя вводим LWP07SW02 — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

169
Рис. 2. 4. Вводим данные нового проекта приложений SwCSharpAddin

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения SwCSharpAddin (не пустого изначально).

170
Рис. 2. 5. Обозреватель решений: состав проекта приложения SwCSharpAddin
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально). После компиляции автоматически откроется
окно Solidworks. Проверим подключение нашего добавления. Для этого откроем окно
Добавления...:

Рис. 2. 6. Запуск приложения SwCSharpAddin по конфигурации Debug: проверка


подключение добавления LWP07SW02

Если добавление было успешно подключено (появилось в списке Другие


добавления и стоят обе галочки слева и справа от имени добавления), то можно
проверить его работу. По умолчанию, поставляемое добавление уже достаточно
«навороченное» и представляет из себя полноценный пример демонстрирующий
основные возможности добавлений в принципе. Потому, даже не внося изменений в
исходный код, добавление обладает рядом интересных возможностей.
Создаём новую деталь (Файл -> Новый... или сочетание клавиш Ctrl+N):

171
Рис. 2. 7. Создание новой детали для проверки работы добавления

Теперь подключим его функционал. Вначале включим панель инструментов. Для


этого нажмём ПКМ на свободном месте рядом с названием Деталь1:

Найдём в открывшемся списке C# Addin и нажмём:

И подключим широкую панель инструментов (Элементы, Эскиз,


Анализировать и прочие). ПКМ по области надписей под широкой панелью
инструментов:

172
Нажмём на C# Addin и сразу же перейдём на эту панель, в результате увидим
следующее:

Нажмём Create cube, а затем на Show PMP и Flyout Tooltip:

173
Рис. 2. 8. Запуск приложения SwCSharpAddin по конфигурации Debug: демонстрация
всех возможностей

Разбирать весь код добавления не имеет смысла. Код достаточно прост для
понимания и внесения изменений. Весь проект изначально состоит из четырёх
основных файлов кода *.cs, главных из которых два.
1. UserPMPage.cs отвечает за панель пользовательских элементов управления
Solidworks (на рис. 2. 8 слева, панель Sample PMP). Этот файл отвечает за
оформление панели (функция AddControls() реализует инициализацию конструктора).
Разумеется, таких панелей может быть много. Событийные инструменты (функции для
работы с пользовательскими элементами и событиями от них) такой панели описаны в
PMPHandler.cs. Для каждой панели может быть своя событийная модель и свой
обработчик событий.
2. SwAddin.cs отвечает за построение всего добавления. По сути является
главным файлов с которым придётся работать большую часть времени. Содержит
инициализацию всего меню и всех функций, которое это меню реализует. И файл
который является фундаментом для всего добавления это: EventHandling.cs.
Реализует событийную модель (обработку событий) для всего добавления.

В качестве примера работы с добавлением и кодом, сделаем клон панели


Sample PMP (Property Manager Page). Для начала нам нужна копия файла
UserPMPage.cs. Нажмём ПКМ в обозревателе решений на этот файл, далее Копировать
(либо просто выделим его и нажмём сочетание клавиш Crtl+C). Далее выделим
название нашего проекта, зелёное ( ) -> ПКМ и Вставить (Ctrl+V).
Переименуем (ЛКМ по файлу в обозревателе и нажмём F2) файл Копия
UserPMPage.cs в UserPMPage2.cs. Зайдём в файл. Найдём строчку:

public class UserPMPage

Заменим на:

public class UserPMPage2

174
Найдём:

public UserPMPage(SwAddin addin)

Заменим:
public UserPMPage2(SwAddin addin)

Открываем файл SwAddin.cs, находим:

Description = "LWP07SW02 description",


Title = "LWP07SW02",

Заменяем на:

Description = "Лабораторная работа № 7, приложение-добавление для SolidWorks


2012: LWP07SW02.dll",
Title = "Использование SwCSharpAddin (C#)",

Находим в этом же файле:

#region Property Manager Variables


UserPMPage ppage = null;
#endregion

Заменяем на:

#region Property Manager Variables


UserPMPage ppage = null;
// Клонируем панель элементов PMP
UserPMPage2 ppage2 = null;
#endregion

Находим:

#region Setup Sample Property Manager


AddPMP();
#endregion

Заменяем на:

#region Setup Sample Property Manager


AddPMP();
// Клонируем панель элементов PMP
AddPMP2();
#endregion

Находим:

int cmdIndex0, cmdIndex1;


string Title = "C# Addin", ToolTip = "C# Addin";

Заменяем на:

int cmdIndex0, cmdIndex1, cmdIndex2;


string Title = "Использование SwCSharpAddin (C#)", ToolTip = "LWP07SW02.dll:
все возможности";

175
Находим:

int[] knownIDs = new int[2] { mainItemID1, mainItemID2 };

Заменяем на:

int[] knownIDs = new int[3] { mainItemID1, mainItemID2, mainItemID3 };

Находим:

int menuToolbarOption = (int)(swCommandItemType_e.swMenuItem |


swCommandItemType_e.swToolbarItem);
cmdIndex0 = cmdGroup.AddCommandItem2("CreateCube", -1, "Create a cube",
"Create cube", 0, "CreateCube", "", mainItemID1, menuToolbarOption);
cmdIndex1 = cmdGroup.AddCommandItem2("Show PMP", -1, "Display sample property
manager", "Show PMP", 2, "ShowPMP", "EnablePMP", mainItemID2, menuToolbarOption);

Заменяем на:

int menuToolbarOption = (int)(swCommandItemType_e.swMenuItem |


swCommandItemType_e.swToolbarItem);
cmdIndex0 = cmdGroup.AddCommandItem2("CreateCube", -1, "Create a cube",
"Create cube", 0, "CreateCube", "", mainItemID1, menuToolbarOption);
cmdIndex1 = cmdGroup.AddCommandItem2("Show PMP", -1, "Display sample property
manager", "Show PMP", 1, "ShowPMP", "EnablePMP", mainItemID2, menuToolbarOption);
// Клонируем панель элементов PMP
cmdIndex2 = cmdGroup.AddCommandItem2("Показать ещё одну PMP", -1, "Открыть
ещё одну страницу с элементами", "Показать ещё одну PMP", 2, "ShowPMP2", "EnablePMP2",
mainItemID3, menuToolbarOption);

Находим:

int[] cmdIDs = new int[3];


int[] TextType = new int[3];

cmdIDs[0] = cmdGroup.get_CommandID(cmdIndex0);

TextType[0] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[1] = cmdGroup.get_CommandID(cmdIndex1);

TextType[1] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[2] = cmdGroup.ToolbarId;

TextType[2] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal |
(int)swCommandTabButtonFlyoutStyle_e.swCommandTabButton_ActionFlyout;

bResult = cmdBox.AddCommands(cmdIDs, TextType);

Заменяем на:

int[] cmdIDs = new int[4];


int[] TextType = new int[4];

cmdIDs[0] = cmdGroup.get_CommandID(cmdIndex0);

176
TextType[0] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[1] = cmdGroup.get_CommandID(cmdIndex1);

TextType[1] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[2] = cmdGroup.ToolbarId;

TextType[2] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal |
(int)swCommandTabButtonFlyoutStyle_e.swCommandTabButton_ActionFlyout;

cmdIDs[3] = cmdGroup.get_CommandID(cmdIndex2);

TextType[3] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

bResult = cmdBox.AddCommands(cmdIDs, TextType);

Находим:

public Boolean RemovePMP()


{
ppage = null;
return true;
}

Добавляем после:
// Клонируем панель элементов PMP
public Boolean AddPMP2()
{
ppage2 = new UserPMPage2(this);
return true;
}

public Boolean RemovePMP2()


{
ppage2 = null;
return true;
}

Находим:

public int EnablePMP()


{
if (iSwApp.ActiveDoc != null)
return 1;
else
return 0;
}

Добавляем после:

// Клонируем панель элементов PMP


public void ShowPMP2()
{
if (ppage2 != null)
ppage2.Show();
}

177
public int EnablePMP2()
{
if (iSwApp.ActiveDoc != null)
return 1;
else
return 0;
}

Всё. Компилируем приложение (Debug) и запускаем. Результат показан на


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

Рис. 2. 9. Результат работы добавления с клонированной страницей PMP

3. Модификация приложения SwCSharpAddin: макрос отрисовки болта

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


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

Для начала организуем в добавлении две новые кнопки. Одна для


вышеописанного макроса, вторая же для будущей функциональности. Нарисуем эти
кнопки открыв в обозревателе решений файлы ToolbarLarge.bmp и
ToolbarSmall.bmp.

178
В ToolbarLarge.bmp нарисуем:

В ToolbarSmall.bmp нарисуем:

Теперь добавим обработчики для новых кнопок. Откроем файл SwAddin.cs.


Найдём:

public const int mainCmdGroupID = 5;


public const int mainItemID1 = 0;
public const int mainItemID2 = 1;
public const int mainItemID3 = 2;

Добавим после:

public const int mainItemID4 = 3;


public const int mainItemID5 = 4;

Найдём:

int cmdIndex0, cmdIndex1, cmdIndex2;

Изменим на:

int cmdIndex0, cmdIndex1, cmdIndex2, cmdIndex3, cmdIndex4;

Найдём:

179
int menuToolbarOption = (int)(swCommandItemType_e.swMenuItem |
swCommandItemType_e.swToolbarItem);
cmdIndex0 = cmdGroup.AddCommandItem2("CreateCube", -1, "Create a cube",
"Create cube", 0, "CreateCube", "", mainItemID1, menuToolbarOption);
cmdIndex1 = cmdGroup.AddCommandItem2("Show PMP", -1, "Display sample property
manager", "Show PMP", 1, "ShowPMP", "EnablePMP", mainItemID2, menuToolbarOption);
// Клонируем панель элементов PMP
cmdIndex2 = cmdGroup.AddCommandItem2("Показать ещё одну PMP", -1, "Открыть
ещё одну страницу с элементами", "Показать ещё одну PMP", 2, "ShowPMP2", "EnablePMP2",
mainItemID3, menuToolbarOption);

Добавим после:

cmdIndex3 = cmdGroup.AddCommandItem2("UsingMacro", -1, "Создать болт при


помощи макроса", "Создать болт (макрос)", 3, "UsingMacro", "", mainItemID4,
menuToolbarOption);
cmdIndex4 = cmdGroup.AddCommandItem2("UsingTemp", -1, "Создать болт с
использованием шаблона", "Создать болт (шаблон)", 4, "UsingTemp", "", mainItemID5,
menuToolbarOption);

Найдём:

int[] knownIDs = new int[3] { mainItemID1, mainItemID2, mainItemID3 };

Изменим на:

int[] knownIDs = new int[5] { mainItemID1, mainItemID2, mainItemID3,


mainItemID4, mainItemID5 };

Найдём:

int[] cmdIDs = new int[4];


int[] TextType = new int[4];

cmdIDs[0] = cmdGroup.get_CommandID(cmdIndex0);

TextType[0] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[1] = cmdGroup.get_CommandID(cmdIndex1);

TextType[1] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[2] = cmdGroup.ToolbarId;

TextType[2] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal |
(int)swCommandTabButtonFlyoutStyle_e.swCommandTabButton_ActionFlyout;

cmdIDs[3] = cmdGroup.get_CommandID(cmdIndex2);

TextType[3] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

bResult = cmdBox.AddCommands(cmdIDs, TextType);

Изменим на:

int[] cmdIDs = new int[6];

180
int[] TextType = new int[6];

cmdIDs[0] = cmdGroup.get_CommandID(cmdIndex0);

TextType[0] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[1] = cmdGroup.get_CommandID(cmdIndex1);

TextType[1] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[2] = cmdGroup.ToolbarId;

TextType[2] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal |
(int)swCommandTabButtonFlyoutStyle_e.swCommandTabButton_ActionFlyout;

cmdIDs[3] = cmdGroup.get_CommandID(cmdIndex2);

TextType[3] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[4] = cmdGroup.get_CommandID(cmdIndex3);

TextType[4] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

cmdIDs[5] = cmdGroup.get_CommandID(cmdIndex4);

TextType[5] =
(int)swCommandTabButtonTextDisplay_e.swCommandTabButton_TextHorizontal;

bResult = cmdBox.AddCommands(cmdIDs, TextType);

Найдём:

public void CreateCube()


{

Добавим до:

public void UsingMacro()


{
string partTemplate =
iSwApp.GetUserPreferenceStringValue((int)swUserPreferenceStringValue_e.swDefaultTemplateP
art);
if ((partTemplate != null) && (partTemplate != ""))
{
IModelDoc2 swDoc = (IModelDoc2)iSwApp.NewDocument(partTemplate,
(int)swDwgPaperSizes_e.swDwgPaperA2size, 0.0, 0.0);
// Начало макроса
bool boolstatus = false;

// Спираль: часть 1
double C = 1 / 1000.0;
double Height, Pitch, Radius;

// Загружаем форму LWP07Macro


LWP07Macro Macro = new LWP07Macro();
Macro.Text = "Использование SwCSharpAddin (C#) :: Выбор размеров
шестигранного болта (макрос)";
Macro.ShowDialog();

181
Height = Macro.H * C;
Pitch = Macro.P * C;
Radius = Macro.R * C;
Macro.Dispose();

// Начало первого эскиза. Создаём круг для спирали


boolstatus = swDoc.Extension.SelectByID2("Сверху", "PLANE", 0, 0, 0,
false, 0, null, 0);
swDoc.SketchManager.InsertSketch(true);
swDoc.ClearSelection2(true);
SketchSegment skSegment = null;
skSegment = ((SketchSegment)(swDoc.SketchManager.CreateCircleByRadius(0,
0, 0, Radius)));
swDoc.InsertHelix(false, true, false, true, 2, Height, Pitch, 0, 0,
(double)Math.PI);
boolstatus = swDoc.SelectedFeatureProperties(0, 0, 0, 0, 0, 0, 0, true,
false, "Спираль1");
swDoc.ClearSelection2(true);
boolstatus = swDoc.Extension.SelectByID2("", "SKETCH", 0, 0, -Radius,
false, 0, null, 0);
boolstatus = swDoc.SelectedFeatureProperties(0, 0, 0, 0, 0, 0, 0, true,
false, "Эскиз1");
swDoc.ClearSelection2(true);

// Спираль: часть 2

// create reference plane


swDoc.ViewRotateplusx(); swDoc.ViewRotateplusx();
swDoc.ViewRotateplusx();
//boolstatus = swDoc.Extension.SelectByID2("", "POINTREF", 0, 0, -Radius,
false, 0, null, 0); // Выбираем конечную точку
boolstatus = swDoc.Extension.SelectByID2("Спираль1", "REFERENCECURVES",
0, 0, 0, false, 0, null, 0); // Выбираем всё
boolstatus = swDoc.Extension.SelectByID2("", "POINTREF", 0, 0, -Radius,
true, 1, null, 0);
swDoc.ViewRotateminusy();
RefPlane myRefPlane = null;
myRefPlane = ((RefPlane)(swDoc.FeatureManager.InsertRefPlane(2, 0, 4, 1,
0, 0)));
//swDoc.ClearSelection2(true);
boolstatus = swDoc.Extension.SelectByID2("", "PLANE", 0, 0, -Radius,
false, 0, null, 0);
boolstatus = swDoc.SelectedFeatureProperties(0, 0, 0, 0, 0, 0, 0, true,
false, "Плоскость1"); // Изменяем имя
swDoc.BlankRefGeom(); // Скрываем Плоскость1
swDoc.ShowNamedView2("*Изометрия", -1);
swDoc.ViewZoomtofit2();

// Эскиз2: треугольник/резьба формы


boolstatus = swDoc.Extension.SelectByID2("Справа", "PLANE", 0, 0, 0,
false, 0, null, 0);
swDoc.SketchManager.InsertSketch(true);
swDoc.ShowNamedView2("*Normal To", 0);
swDoc.ViewRotateminusx();
swDoc.ViewTranslateminusx();
swDoc.ClearSelection2(true);
double x, y, z, dx, dy;
//x = .049903; y = -.001803; z = 0; // Старые координаты. Те что удалось
установить вручную изначально
x = y = z = 0;
dx = dy = Radius / 10.0 + Pitch / 4.0;
skSegment = ((SketchSegment)(swDoc.SketchManager.CreateLine(x, y, z, x, y
+ dy, z)));
skSegment = ((SketchSegment)(swDoc.SketchManager.CreateLine(x, y + dy, z,
x + dx, y + dy, z))); // Горизонталь

182
skSegment = ((SketchSegment)(swDoc.SketchManager.CreateLine(x + dx, y +
dy, z, x, y, z)));
swDoc.ClearSelection2(true);
//boolstatus = swDoc.Extension.SelectByID2("", "SKETCHSEGMENT", x+dx/2.0,
y+dy, z, false, 0, null, 0);
//swDoc.SketchAddConstraints("sgHORIZONTAL2D"); // Больше не нужно
//swDoc.ClearSelection2(true);
//swDoc.ViewRotateminusx(); // Тоже не нужно
swDoc.SetPickMode();
boolstatus = swDoc.Extension.SelectByID2("Точка1", "SKETCHPOINT", x, y,
z, false, 0, null, 0);
boolstatus = swDoc.Extension.SelectByID2("Спираль1", "REFERENCECURVES",
0, 0, 0, true, 1, null, 0);
swDoc.SketchAddConstraints("sgATPIERCE");
swDoc.ClearSelection2(true);
boolstatus = swDoc.Extension.SelectByID2("Точка1", "SKETCHPOINT", x, y,
z, false, 0, null, 0);
swDoc.SketchConstraintsDel(0, "sgCOINCIDENT"); // Автоматически
"совпадающие"
swDoc.SketchManager.InsertSketch(true);

// Перемещаем Эскиз2 на Плоскость1


boolstatus = swDoc.Extension.SelectByID2("Эскиз2", "SKETCH", 0, 0, 0,
false, 0, null, 0);
boolstatus = swDoc.Extension.SelectByID2("Справа", "PLANE", 0, 0, 0,
true, 0, null, 0);
boolstatus = swDoc.DeSelectByID("Справа", "PLANE", 0, 0, 0);
boolstatus = swDoc.Extension.SelectByID2("Эскиз2", "SKETCH", 0, 0, 0,
false, 0, null, 0);
boolstatus = swDoc.Extension.SelectByID2("Плоскость1", "PLANE", 0, 0, 0,
true, 0, null, 0);
boolstatus = swDoc.ChangeSketchPlane();
boolstatus = swDoc.EditRebuild3();
swDoc.SetPickMode();
swDoc.ClearSelection2(true);
swDoc.ViewZoomtofit2();

// Делаем резьбу; 1: эскиз пути, 2: маршрут спирали, 4: выполняем


//boolstatus = swDoc.Extension.SelectByID2("Эскиз2", "SKETCH",
0.005135831118, -0.0038831893819, 0, true, 1, null, 0);
boolstatus = swDoc.Extension.SelectByID2("Эскиз2", "SKETCH", 0, 0, 0,
true, 1, null, 0);
boolstatus = swDoc.Extension.SelectByID2("Спираль1", "REFERENCECURVES",
0, 0, 0, true, 4, null, 0);
Feature myFeature = null;
myFeature = ((Feature)(swDoc.FeatureManager.InsertProtrusionSwept3(false,
false, 0, false, false, 0, 0, false, 0, 0, 0, 0, true, true, true, 0, true)));

// Снимаем выделение, скрываем окружность (созданную вначале), меняем


положение камеры (вид)
swDoc.ClearSelection2(true);
boolstatus = swDoc.Extension.SelectByID2("Эскиз1", "SKETCH", 0, 0, 0,
false, 0, null, 0);
swDoc.BlankSketch();
swDoc.ClearSelection2(true);
swDoc.ShowNamedView2("*Изометрия", -1);
swDoc.ViewZoomtofit2();

// Спираль: часть 3

// Вытягивание
double Height2 = Height + Radius / 4.0 + dx * 1.5;
boolstatus = swDoc.Extension.SelectByID2("Эскиз1", "SKETCH", 0, 0, 0,
false, 0, null, 0);

183
myFeature = ((Feature)(swDoc.FeatureManager.FeatureExtrusion2(true,
false, false, 0, 0, Height2, 0.00254, false, false, false, false, 0.01745329251994,
0.01745329251994, false, false, false, false, true, true, true, 0, 0, false)));
swDoc.ISelectionManager.EnableContourSelection = false;
swDoc.ClearSelection2(true);

// Вырезаем вытягивание (наверх)


//boolstatus = swDoc.Extension.SelectByID2("", "FACE", 0, Height2, 0,
false, 0, null, 0);
//swDoc.SketchManager.InsertSketch(true);
//swDoc.ClearSelection2(true);
//skSegment = ((SketchSegment)
(swDoc.SketchManager.CreateCircleByRadius(0, 0, 0, Radius + c)));
//swDoc.SketchManager.InsertSketch(true);
//swDoc.ClearSelection2(true);
//boolstatus = swDoc.Extension.SelectByID2("Sketch3", "SKETCH", 0, 0, 0,
false, 0, null, 0);
//myFeature = ((Feature)(swDoc.FeatureManager.FeatureCut(true, false,
true, 0, 0, c, 0.0508, false, false, false, false, 0.01745329251994, 0.01745329251994,
false, false, false, false, false, true, true)));
//swDoc.ISelectionManager.EnableContourSelection = false;
//swDoc.ClearSelection2(true);

// Шляпка болта
double extrHeight = Radius + c / 4;
boolstatus = swDoc.Extension.SelectByID2("", "FACE", 0, Height2, 0,
false, 0, null, 0);
swDoc.SketchManager.InsertSketch(true);
//skSegment = ((SketchSegment)
(swDoc.SketchManager.CreateCircleByRadius(0, 0, 0, Radius*1.5))); // Созданём полигон (6
граней) вместо окружности
Array vSkLines = null;
vSkLines = ((Array)(swDoc.SketchManager.CreatePolygon(0, 0, 0, -Radius *
2, 0, 0, 6, true)));
swDoc.SketchManager.InsertSketch(true);
swDoc.ClearSelection2(true);
boolstatus = swDoc.Extension.SelectByID2("Эскиз3", "SKETCH", 0, Height2,
0, false, 0, null, 0);
myFeature = ((Feature)(swDoc.FeatureManager.FeatureExtrusion2(true,
false, false, 0, 0, extrHeight, 0.00254, false, false, false, false, 0.01745329251994,
0.01745329251994, false, false, false, false, true, true, true, 0, 0, false)));
swDoc.ISelectionManager.EnableContourSelection = false;
swDoc.ClearSelection2(true);

// Кромки
boolstatus = swDoc.Extension.SelectByID2("", "FACE", 0, Height2 +
extrHeight, 0, false, 0, null, 0);
swDoc.ViewRotateplusx();
swDoc.ViewRotateplusx();
swDoc.ViewRotateplusx();
swDoc.ViewRotateplusx();
boolstatus = swDoc.Extension.SelectByID2("", "FACE", Radius * 1.5,
Height2, 0, true, 1, null, 0);
//boolstatus = swDoc.Extension.SelectByID2("", "FACE", 0, 0, 0, true, 2,
null, 0);
Array radiiArray3 = null;
double[] radiis3 = new double[1];
Array setBackArray3 = null;
double[] setBacks3 = new double[0];
Array pointArray3 = null;
double[] points3 = new double[0];
radiiArray3 = radiis3;
setBackArray3 = setBacks3;
pointArray3 = points3;

184
myFeature = ((Feature)(swDoc.FeatureManager.FeatureFillet(195, Radius *
2.0 / 25.0, 0, 0, radiiArray3, setBackArray3, pointArray3)));
swDoc.ClearSelection2(true);
boolstatus = swDoc.Extension.SelectByID2("", "FACE", 0, 0, 0, false, 0,
null, 0);
myFeature = ((Feature)(swDoc.FeatureManager.FeatureFillet(195, Radius *
2.0 / 25.0, 0, 0, radiiArray3, setBackArray3, pointArray3)));

// Завершающий вид на деталь


swDoc.ShowNamedView2("*Изометрия", -1);
swDoc.ViewZoomtofit2();
// Конец макроса
}
else
{
System.Windows.Forms.MessageBox.Show("Нет части доступных шаблонов.
Пожалуйста, проверьте ваши настройки чтобы убедиться, что часть шаблонов выбрана или
недостающие шаблоны.");
}
}

public void UsingTemp()


{
// Пока пусто
}

Добавим недостающую форму для работы кода макроса. Нажмём ПКМ на


названии проекта в обозревателе решений, далее в раскрывающемся списке Добавить
-> Создать элемент... (Ctrl+Shift+A). Выберем в открывшемся окне Форма Windows
Forms. В поле Имя внизу окна вписываем LWP07Macro.cs, далее жмём ОК.

Задаём следующие параметры формы на панели Свойства:

Text: Использование SwCSharpAddin (C#) ::


Ввод размеров болта для макроса
Size: 600; 100
FormBorderStyle: FixedDialog
MaximizeBox: False

Рис. 3. 1. Модифицированная форма LWP07Macro

Добавим на нашу форму ToolTip ( ).

Параметры добавленного элемента всплывающей подсказки таковы:

(Name): Hint

Слева направо установим три TextBox со следующими параметрами:

(Name): TBHeight

185
Text: 75
ToolTip на Hint: Высота спирали

(Name): TBRadius
Text: 10
ToolTip на Hint: Радиус стержня

(Name): TBPitch
Text: 3
ToolTip на Hint: Шаг спирали

Добавим над каждым TextBox слева направо по элементу Label. Свойства Text
каждого текстового элемента будут соответственно H, R и P.

Установим на форме в правом нижнем углу кнопку Button. Параметры кнопки


будут следующими:

(Name): B_OK
Text: Размер выбран
Size: 100; 23

Рис. 3. 2. Модифицированная форма LWP07Macro

Двойным щелчком по кнопке B_OK создаём событие Click, код такой:

double.TryParse(TBHeight.Text, out H);


double.TryParse(TBRadius.Text, out R);
double.TryParse(TBPitch.Text, out P);
if (H > 0 && R > 0 && P > 0)
Close();
else
{
MessageBox.Show("Ошибка ввода размеров: нужно ввести все неотрицательные
числа в поля формы.\n\nРазмеры по умолчанию:\nH: 75 мм.\nR: 10 мм.\nP: 3 мм.",
"Использование SwCSharpAddin (C#) :: Ввод размеров болта для макроса");
}

Добавим после строчки:

public partial class LWP07Macro : Form


{

Код:

public double H, R, P;

186
Компилируем приложение (Debug) и запускаем. Загружаем новую деталь (или
непосредственно панель инструментов) и нажимаем на кнопку в нашем добавлении с
буквой «М». Результат работы показан ниже (Рис. 3. 3):

Рис. 3. 3. Результат построение шестигранного болта при помощи макроса

4. Модификация приложения SwCSharpAddin: построение через шаблон


детали

А теперь немного реалий. Да, наше добавление справляется со своей задачей.


Строит болты (  ), но вот незадача. Редактировать построенное макрос уже не может.
Естественно, что записать в макрос можно любые действия (почти любые), и даже
действия по редактированию, но целесообразность подобного можно поставить под
сомнение хотя бы потому, что это уже лишняя работа, надстройка над надстройкой.
Костыль. Один макрос чтобы нарисовать с «нуля», второй чтобы изменить размер у
нарисованного. А если, допустим, у нас не простенький болт, а модель какого-нибудь
аппарата высокого давления с парочкой тысяч деталей или ракеты «Протон-М» с
миллионом?
Хорошо, можно скажем сделать разных деталей с разными размерами на все
случаи жизни. Пару сотен тысяч файлов с разными размерами. Или создать грамотный
макрос для изменения размеров, но здесь опять возникает вопрос количества деталей и
затрат на редактирование двух макросов в случае изменения детали. А также
постоянный контроль кода на ошибки. И очень большие затраты по времени на
переделку в случае перехода команды разработки на новую версию SolidWorks. API с

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

Профессиональные разработчики не используют макросы для построения с


«нуля». Или даже полного редактирования. Наиболее успешным вариантом является
совмещение макросов и шаблонов деталей. Использование шаблонов в разы сокращает
тот объём работ, который нужно проделать при работе с достаточно серьёзной по
количеству деталей моделью. К тому же моментально снимается проблема
редактирования только что построенной модели. В качестве шаблона можно подгрузить
модель с нормальными размерами и поменять любой непонравившийся размер той же
самой программой. Размер кода такой программы фактически составляет десятую или
сотую часть от аналогичного кода «записанного» макроса. В кода, где главным
является шаблон, редактируются значения размеров в этом шаблоне. Эти размеры
можно как получать из модели, так и отсылать в модель. Сли, например встанет вопрос
об изменении шаблона, достаточно отредактировать сам шаблон (не забывая о
вездесущих привязках и уравнениях для размеров) и внести новые размеры в код.
Допустим, время на создание модели для макроса и для шаблона одинаково.
Тогда время на расстановку переменных в макрос нужно в разы больше, чем время на
создание кода по изменению ключевых размеров модели. Также для макроса требуется
большое количество лишних вычислений, которые в шаблоне решаются через
привязку.
Почему нельзя использовать макрос и расстановку размеров одновременно?
Разумеется, можно. Можно кодом менять размеры модели, построенной через макрос,
но, у данного метода возникают те же самые проблемы: низкая визуальная
информативность кода по сравнению с шаблоном, а также присутствует необходимость
расстановки привязок. При этом в разы увеличивается время на контроль и отлов
ошибок кода макроса...
Теперь, попытаемся сделать вот что. Допустим нам нужно, чтобы наше
приложение запускалось отдельно, но при необходимости оно могло открывать
SolidWorks и что-то делать (например, строить элемент по размерам из приложения).
Такой способ работы противоположен концепции использования макросов после
запуска SolidWorks и гораздо медленнее (необходимость запускать SolidWorks пусть
даже и в фоновом режиме). Однако бывают ситуации, когда такой подход необходим.

Итак. Пускай теперь у нас есть шаблон модели шестигранного болта с резьбой с
расставленными привязками и сформированными зависимостями размеров друг от
друга. В шаблоне выделено три ключевых размера. Назовём его
Bolt_Default_Template.sldprt. Шаблон выглядит так:

188
Рис. 4. 1. Заготовка шаблона для добавления SwCSharpAddin

Добавим ещё одну форму. Нажмём ПКМ на названии проекта в обозревателе


решений, далее в раскрывающемся списке Добавить -> Создать элемент...
(Ctrl+Shift+A). Выберем в открывшемся окне Форма Windows Forms. В поле Имя внизу
окна вписываем LWP07Temp.cs, далее жмём ОК.

Задаём следующие параметры формы на панели Свойства:

Text: Использование SwCSharpAddin (C#) ::


Ввод размеров болта для шаблона
Size: 600; 300
FormBorderStyle: FixedDialog
MaximizeBox: False

189
Рис. 4. 2. Модифицированная форма LWP07Temp

Добавим на нашу форму ToolTip ( ).

Параметры добавленного элемента всплывающей подсказки таковы:

(Name): Hint

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

Для начала размести на форме два диалога: FileOpenDialog и SaveFileDialog с


параметрами:

OpenFileDialog:
(Name): OFD_Template
FileName: Bolt_Default_Template.sldprt
SaveFileDialog:
(Name): SFD_Template
FileName: Bolt_Final.sldprt

Сверху под заголовком размести две кнопки Button и две TextBox:

Сверху вниз, слева направо идёт:

190
Button:
(Name): B_Template
Text: Выбор файла шаблона
TextBox:
(Name): TB_Template
ReadOnly: True
Button:
(Name): B_Final
Text: Выбор файла для сохранения
TextBox:
(Name): TB_Final
ReadOnly: True

Снизу разместим три TextBox и кнопку:

TextBox:
(Name): TB_H
ToolTip на Hint: Высота болта
TextBox:
(Name): TB_R
ToolTip на Hint: Радиус стержня
TextBox:
(Name): TB_C
ToolTip на Hint: Шаг спирали
Button:
(Name): B_OK
Text: Размер выбран

Открываем код главного файла формы (LWP07Temp.cs). Выбираем форму в


обозревателе решений, далее правая кнопка мыши и во всплывающем меню Перейти
к коду. Найдём:

public partial class LWP07Temp : Form


{

Добавим после:

public string P_Template, P_Final; // Строковый переменные пути к шаблону и путь


куда сохранить готовую деталь
public double H, R, P; // Высота, радиус и шаг
private bool B1, B2; // Логические переменные для организации работы с диалогами

191
Найдём:

public LWP07Temp()
{
InitializeComponent();

Добавим после:
// Инициализируем переменные при старте формы
P_Template = "D:\\Bolt_Default_Template.sldprt";
P_Final = "D:\\Bolt_Final.sldprt";
B1 = false;
B2 = false;
TB_Template.Text = P_Template;
TB_Final.Text = P_Final;
TB_H.Text = "100";
TB_R.Text = "10";
TB_P.Text = "3";

Событие Click кнопки B_Template:

private void B_Template_Click(object sender, EventArgs e)


{
B1 = true; // Пользователь открыл диалог выбора файла
OFD_Template.ShowDialog();
TB_Template.Text = OFD_Template.FileName; // Отправляем путь в TextBox
P_Template = TB_Template.Text; // Значение TextBox присваиваем переменной
}

Событие Click кнопки B_Final:

private void B_Final_Click(object sender, EventArgs e)


{
B2 = true;
SFD_Final.ShowDialog();
TB_Final.Text = SFD_Final.FileName;
P_Final = TB_Final.Text;
}

Событие Click кнопки B_OK:

private void B_OK_Click(object sender, EventArgs e)


{
if (B1 == false)
{
P_Template = "D:\\Bolt_Default_Template.sldprt";
}
else { P_Template = OFD_Template.FileName; } // Если диалог выбора файла был
открыт, сохраняем пользовательский выбор в переменную
if (B2 == false)
{
P_Final = "D:\\Bolt_Final.sldprt";
}
else { P_Final = SFD_Final.FileName; }
double.TryParse(TB_H.Text, out H);
double.TryParse(TB_R.Text, out R);
double.TryParse(TB_P.Text, out P);
if (H > 0 && R > 0 && P > 0)
Close();
else
{

192
MessageBox.Show("Ошибка ввода размеров: нужно ввести все неотрицательные
числа в поля формы.\n\nРазмеры по умолчанию:\nH: 100 мм.\nR: 10 мм.\nP: 3 мм.",
"Использование SwCSharpAddin (C#) :: Ввод размеров болта для шаблона");
}
}

Теперь изменим код функции UsingTemp() файла SwAddin.cs:

public void UsingTemp()


{
String Path_Template, Path_Final;
Double FormH, FormR, FormP;
Double NewH, NewR, NewP;
// Загружаем форму LWP07Temp
LWP07Temp Temp = new LWP07Temp();
Temp.Text = "Использование SwCSharpAddin (C#) :: Ввод размеров болта для
шаблона";
Temp.ShowDialog();
Path_Template = Temp.P_Template;
Path_Final = Temp.P_Final;
FormH = Temp.H / 1000.0;
FormR = Temp.R * 2 / 1000.0;
FormP = Temp.P / 1000.0;
Temp.Dispose();
IModelDoc2 swDoc = (IModelDoc2)iSwApp.OpenDoc6(Path_Template,
(int)swDocumentTypes_e.swDocPART, 0, "", 0, 0); // Открываем шаблон
bool boolstatus = false;
swDoc = (IModelDoc2)iSwApp.ActivateDoc2(Path_Template, false, 0); // Делаем
шаблон активным
Dimension myDimension = null; // Объявляем переменную для размеров
myDimension = ((Dimension)(swDoc.Parameter("D1@Бобышка-Вытянуть1"))); //
Высота болта
myDimension.SystemValue = FormH; // Присваиваем размеру значение с формы
myDimension = ((Dimension)(swDoc.Parameter("D1@Эскиз1"))); // Радиус стержня
myDimension.SystemValue = FormR;
myDimension = ((Dimension)(swDoc.Parameter("D4@Спираль1"))); // Шаг спирали
myDimension.SystemValue = FormP;
swDoc.SaveAs2(Path_Final, 0, false, false); // Сохраняем новую модель как
новый файл, шаблон при это закрывается
boolstatus = swDoc.EditRebuild3(); // Перестраиваем вид модели (обновляем по
размерам)
swDoc = (IModelDoc2)iSwApp.ActivateDoc2(Path_Final, false, 0);
swDoc.ClearSelection2(true);
// Получаем новые размеры непосредственно из детали
myDimension = ((Dimension)(swDoc.Parameter("D1@Бобышка-Вытянуть1"))); //
Высота болта
NewH = myDimension.SystemValue;
myDimension = ((Dimension)(swDoc.Parameter("D1@Эскиз1"))); // Радиус стержня
NewR = myDimension.SystemValue;
myDimension = ((Dimension)(swDoc.Parameter("D4@Спираль1"))); // Шаг спирали
NewP = myDimension.SystemValue;
System.Windows.Forms.MessageBox.Show("В результате изменения размеров шаблона
шестигранного болта были установлены слпедующие размеры:\n\n\tВысота болта: " + (NewH *
1000) + "мм\n\tРадиус стрежня: " + (NewR * 1000) + "мм\n\tШаг спирали: " + (NewP * 1000)
+ "мм\n\nЭти данные были успешно получены из сохранённой при работе дополнения детали.",
"Использование SwCSharpAddin (C#) :: Результат изменения рамзеров шаблона");
}

5. Завершающая часть

Компилируем приложение (Release) и запускаем. Не создавая новой детали


добавим на панель инструментов наше добавление:

193
Нажимаем на кнопку с буквой «Ш»:

В открывшемся окне указываем необходимые размеры и пути к шаблону и месту


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

Результат работы показан ниже (Рис. 5. 1):

194
Рис. 5. 1. Работа кнопки Создать болт (шаблон): на просмотр было открыто то что
SolidWorks собрал (шестигранный болт на основе шаблона) и сохранил на диск по
указанному в форме пути и с именем заданным пользователем (Bolt_Final.sldprt)

6. О приложении к Лабораторной работе № 7

Получившуюся программу (LWP07SW02.exe), собранную из кусков кода


приведённых в данной лабораторной работе, деталь шаблона (файл
Bolt_Default_Template.sldprt), а также шаблон добавления SolidWorks 2012 из установки
SolidWorks API SDK (swcsharpaddin.zip) можно загрузить по ссылке в конце этого
материала (сслыка доступна в программном продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

8. Лабораторная работа № 8: Создание простого приложения Windows Foundation


Presentation

Лабораторная работа № 8: Создание простого приложения Windows


Foundation Presentation

Содержание

1. Вводная часть
2. Создание приложения Windows Foundation Presentation
3. Модификация приложения Windows Foundation Presentation
4. Модификация приложения Windows Foundation Presentation:
добавление нового элемента из библиотеки компонентов WPF
5. Модификация приложения Windows Forms: расширение
функциональности приложения и работа с оформление

195
6. Модификация приложения Windows Foundation Presentation: различные
возможности WPF
7. Модификация приложения Windows Foundation Presentation: немного о
стилях и шаблонах
8. Завершающая часть
9. О приложении к Лабораторной работе № 8

1. Вводная часть

«Гзамл», «ви-эф-пи». Эти слова идут неразрывно. Оба слова обозначают одну
достаточно интересную технологию, которой можно найти великое множество полезных
применений... Эта технология, можно сказать ― прямой конкурент Windows Forms.

Первые упоминания о новой технологии создания приложений Windows


Foundation Presentation и нового языка для работы с этой технологией пришли ещё
со времён, когда Windows Vista была Windows Longhorn. Если платформа .NET
Framework 2.0 научила нас что существует Common Language Runtime (CLR) и
базовая библиотека классов, то релиз .NET Framework 3.0 подарил нам WFP и XAML.

Windows Foundation Presentation (кодовое имя Avalon, здесь и далее WFP) —


система для создания полноценных автономных приложений для Windows либо
создание приложений запускаемых в браузере (а значит «собираемых» на стороне
клиента). WFP предлагает множество визуально привлекательных возможностей для
взаимодействия с пользователем. Фактически WFP нужна для того, чтобы создать
приложение наиболее «красивое» и графически привлекательное (презентационное).

Основу WFP составляет система визуализации, не зависящая от разрешения


устройства ввода. WFP предоставляет:

 элементы управления;
 двухмерную (2D) и трёхмерную (3D) графику;
 анимацию в приложении;
 шаблоны и стили;
 работа с мультимедиа и оформлением;
 привязка данных;
 язык eXtensible Application Markup Language (далее XAML).

Графическая основа в отличие от Windows Forms (GDI/GDI+) является DirectX.


Производительность WFP выше за счёт аппаратного ускорения (всё-таки DirectX это
полноценное 3D) графики через DirectX. Урезанная версия CLR: WFP/E есть версия
Silverlight.

XAML («гзЭмл» или «гзАмл») — расширяемый язык разметки для приложений.


Основан на XML (eXtensible Marku Language, «экс-эм-Эл»). Разработан корпорацией
Microsoft.
XAML прежде всего нужен для разработки и описания пользовательского
интерфейса. Логикой приложения естественно по-прежнему руководит язык
программирования (C#, Visual Basic и прочее). Очевидный плюс, это «двоякость»
разработанных приложений. Приложение можно разработать как для веб-браузера, так
и оконный вариант для запуска в Windows.
В основе работы такого приложения лежит объект Application. Этот объект
контролирует выполнение программы и генерирует события пользовательского кода.

196
Набор свойств, методов и событий объекта позволяет объединить веб-документы в
связанное приложение. Этот набор свойств и описывается при помощи XAML.
Также XAML используется в Windows Workflow Foundation (WF) и Silverlight.
При помощи XAML в WFP можно определять элементы пользовательского интерфейса,
привязку данных, поддержку событий и прочее. В WF XAML определяет
последовательности выполняемых действия (workflows).

Итак, типичное приложение WFP (WPF) можно рассмотреть как набор


страниц содержащих некий процедурный код. Страницы описаны на XAML, код на C#.
Платформу WFP можно использовать либо в браузере (частичное доверие) в виде
приложения XBAP (XAML Browser Application), либо в полнофункциональном
приложении, как правило, в исполняемом файле (*.exe).

2. Создание приложения Windows Foundation Presentation

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение WPF. Также здесь можно выбрать какой использовать
«фреймворк» (набора компонентов для написания программ, конечную платформу). В
нашем случае выберем .NET Framework 4.

197
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP08WPF01 — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

198
Рис. 2. 3. Вводим данные нового проекта приложения WPF

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения WPF (не пустого изначально). Среда разработки сформировала исходный
код в двух файлах (не считая стандартного AssemblyInfo.cs). Один из файлов
отвечает за программу в целом (это файл App.xaml.cs), а второй за формирование
окна приложения — инициализацию конструктора формы и всех её элементов (это
файл MainWindow.xaml.cs). Как видим, исходный код, например файла App.xaml.cs
не отличается от шаблонного:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;

namespace LWP08WPF01
{
/// <summary>
/// Логика взаимодействия для App.xaml
/// </summary>
public partial class App : Application
{
}
}

Но, что же это? В файле отсутствует точка входа: публичный и статичный метод
Main(). Don’t panic! Приложение (объект Application) вызывается конструкцией файла
App.xaml:

<Application x:Class="LWP08WPF01.App"

199
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>

</Application.Resources>
</Application>

Не забываем, что сейчас мы имеем дело с языком разметки.

Файл MainWindows.xaml.cs содержит следующий исходный код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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 LWP08WPF01
{
/// <summary>
/// Логика взаимодействия для MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

А вызов конструктора формы (MainWindows.xaml) выглядит так:

<Window x:Class="LWP08WPF01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>

</Grid>
</Window>

Сама разметка, как уже было рассказано в вводной части данной лабораторной
работы очень напоминает другой формат для разметки текстовых файлов, а именно
небезызвестный расширяемый язык разметки XML. Собственно XAML основан на XML. И
это неудивительно. Ведь XML при ближайшем рассмотрении очень удобный язык
разметки текстовых файлов (например весьма эффективен для организации баз данных
типа «поле-значение» без необходимости использования того же SQL).

200
Рис. 2. 4. Обозреватель решений: состав проекта приложения WPF сформированного
средой разработки

Теперь посмотрим текущий макет нашей главной формы, которая должна


запустить в качестве главного окна:

201
Рис. 2. 5. Макет формы MainWindows.xaml: отображение конструктора формы (сверху)
и представление этой формы в качестве разметки XAML (внизу)

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

202
Рис. 2. 6. Запуск приложения WPF по конфигурации Debug

3. Модификация приложения Windows Foundation Presentation

Пока что у нас есть пустое приложение и всего один элемент: сетка (Grid).
Исправим это.

Как видно, в панели элементов прибавилось элементов, причём порядком.


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

Среда разработки значительно упрощает нам жизнь не заставляя вручную


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

203
Поменяем заголовок нашей формы, иконку, сделаем окно «неизменяемым» по
размерам. Выделим окно MainWindow в конструкторе (двойное нажатие мышки по
MainWindow.xaml в обозревателе решений). Окно свойств формы находится справа
внизу окна среды разработки:

Рис. 3. 1. Свойства: окно MainWindow

Первое что бросается в глаза: у нашего окна нет имени. Исправим это так:
выдели серое поле <без имени> и введём имя Main:

Теперь поменяем остальные свойства:


Title: Простое приложение WPF (C#)
ResizeMode: NoResize
ToolTip: Главное окно
^ Это свойство можно найти в группе: Прочее.

Сброс значения делается просто. Нажимаем на маленький квадратик справа от


текста свойства, далее Сбросить значение:

204
Установка иконки тоже занятие простое: ищем поле Icon, нажимаем на .
Откроется окно:

Рис. 3. 2. Окно выбор добавленных в проект изображений

Нажмём Добавить…, выберем в проводнике нужное изображение, далее ОК:

205
Рис. 3. 3. Добавленное изображение

Выберем изображение, нажмём ОК. Как видим изображение импортировалось в


проект (папка Images обозревателя решений). Установка иконки на само
компилируемое приложение выполняется также как и для Windows Forms (через
свойства проекта, на вкладке Приложение в пункте Ресурсы -> Значок и
манифест).

Получим нечто подобное:

206
Рис. 3. 4. Модифицированная форма приложения

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

Перед тем как приступать к дальнейшимдействия посмотрим XAML-код формы:

<Window x:Class="LWP08WPF01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Простое приложение WPF (C#)" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="350"
Width="525" WindowStyle="SingleBorderWindow" ToolTip="Главное окно" ResizeMode="NoResize"
Name="Main" ForceCursor="False" Icon="/LWP08WPF01;component/Images/LWP08WPF01.ico">
<Grid></Grid>
</Window>

Обратите внимание на конструкцию: <Window></Window>. Это ключевое


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

Для демонстрации работы XAML-страницы создадим в произвольном месте


текстовый файл, назовём его XAML, поместим туда код:

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Hello


XAML!" ToolTip="Одна единственная кнопка">
<Button>Hello, XAML!</Button>
</Page>

Сохраним страницу как XAML.xaml (в кодировке Юникод) и запустим через


браузер:

Рис. 3. 5. Работа XAML-разметки в браузере

207
4. Модификация приложения Windows Foundation Presentation: добавление
нового элемента из библиотеки компонентов WPF

Реализуем простенькую функциональность. Добавим из библиотеки компонентов


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

Рис. 4. 1. Добавление нового элемента из библиотеки компонентов WPF

Откроется окно со всеми доступными компонентами. Перейдём на вкладку


Компоненты WPF, далее в поле Фильтр вводим Ink (отсеивая лишнее) и ставим
галочку в InkCanvas.

208
Рис. 4. 2. Окно Выбор элементов панели элементов

Жмём ОК. Новый элемент появится в панели элементов. Перетащим его на


форму. Изменим размер: потянем за уголок добавленного элемента и поведём его до
сетки. Появятся направляющие (оранжевые) относительно сетки (элемента Grid).
Красные цифры у границ формы это отступы от сетки (и границы формы), а также
обозначение высоты и ширины элемента в красных прямоугольниках:

Рис. 4. 3. Изменение размеров элемента

209
Рис. 4. 4. Выравнивание элемента по левому верхнему углу

Установим свойства в элемента InkCanvas такими (группа полей Макет):

Width: впишем Auto


Height: впишем Auto
HorizontalAlignment: выберем из выпадающего списка Stretch
^ Элемент будет растягиваться вместе с окном по горизонтали.
VerticalAlignment: выберем из выпадающего списка Stretch
^ Элемент будет растягиваться вместе с окном по вертикали.
Margin: 12,12,12,199
^ Свойство Margin выполняет исключительно важную роль. С его помощью
можно задавать поля вокруг текущего элемента управления (то есть элемента, для
которого задаётся свойство Margin). В WPF предусмотрен объект ValueConverter,
который принимает строку в формате «5,5,5,5». О чём это говорит? О том, что мы хотим
оставить поля размером 5 пикселей со всех сторон элемента. Строка Margin определяет
левое, верхнее, правое и нижнее поля. Это один из трёх перегруженных конструкторов

210
класса со странным именем Thickness, который используется при задании свойства
Margin в коде программной части.
MinHeight: впишем 100
^ Минимальная высота элемента (попробуйте уменьшить размер элемента
меньше этого значения в конструкторе мышкой, среда не даст этого сделать).

Поменяем цвет фона на светло-жёлтый:

Background: LightYellow
^ Выбор цвета может также зависеть от числа, например: #[Aplha|Red|Green|
Blue] является маской для ввода цвета с альфа-каналом прозрачности или:
#FFFFFFE0 для светло-жёлтого.

Обратим также внимание, что нашему элементу InkCanvas было дано имя
inkCanvas1.

Теперь сделаем кнопку очистки содержимого. Добавим на форму с панели


элементов элемент Button. Чтобы наша кнопка встала там где нужно, изменим
свойства следующим образом:

<Button Content="Очистить" HorizontalAlignment="Right" Margin="0,0,12,170"


Name="button1" VerticalAlignment="Bottom" Width="75" Click="button1_Click" Height="23" />

211
Рис. 4. 5. Добавление кнопки Очистить

Теперь вернём нашей форме свободу и изменим для неё значения для
свободного изменения размера1:
ResizeMode: CanResize

ПРИМЕЧАНИЕ № 1: Совсем необязательно лезть в свойства элемента и искать


снова искать уже изменённое свойство. Можно поступить проще. Ищем свойство в
описании элемента в XAML-кода, удаляем значение поля свойства и вписываем
«пробел», появится контекстное меню всех возможных значений свойства:

Добавить событие можно разными способами. Наиболее простой (для Click) это
дваждый нажать на кнопку в конструкторе формы. Также можно перейти на вкладку
события для кнопки Button, найти там Click и дваждый щёлкнуть по надписи:

212
Также можно добавить событие вручную, редактированием кода XAML для
формы:

<Button Content="Очистить" Height="23" HorizontalAlignment="Left"


Margin="416,118,0,0" Name="button1" VerticalAlignment="Top" Width="75"
Click="button1_Click" />
</Grid>

За событие нажатия отвечает текст: Click=”button1_Click”. Разумеется при


ручном добавлении, имя события может быть каким угодно на усмотрение
программиста. Главное чтобы у нас был соответствующий метод в файле
MainWindow.xaml.cs:

private void button1_Click(object sender, RoutedEventArgs e)


{

Впишем в обработчик кнопки строчку:

this.inkCanvas1.Strokes.Clear(); // Очищаем элемент inkCanvas1

Компилируем, проверяем работоспособность. Можем нарисовать что-то в


жёлтоватом поле и затем стереть это нажатием кнопки Очистить. Можно изменить
размер окна и при этом элементы будут двигаться как надо. Но, всё равно скучно и
бедно.

213
Рис. 4. 6. Модифицированное приложение WPF

Добавим элементу ручного ввода немного функциональности. Возможность


стирать нарисованные элементы не по точкам или сразу всё, а посимвольно. Символом
или строчкой будет считать элемент от начала момента рисования, до конца (до
момента отжатия клавиши мышки на поле рисования). Добавим кнопки переключения
между режимами. Слева от кнопки Очистить поместим ещё две: Стереть элемент
(ButtonClear), Рисовать (ButtonDraw) и Рисовать со стиранием (ButtonDC). Имена
кнопок указаны в скобках.

Событие Click для кнопки Рисовать (переопределяет режим рисования,


возвращает в состояние «по умолчанию» для элемента, или в состояние EditingMode:
Ink):

private void ButtonDraw_Click(object sender, RoutedEventArgs e)


{
inkCanvas1.EditingMode = InkCanvasEditingMode.Ink;
}

Событие Click кнопки Рисовать со стиранием:

private void ButtonDC_Click(object sender, RoutedEventArgs e)


{
inkCanvas1.EditingMode = InkCanvasEditingMode.InkAndGesture;
}

Событие Click кнопки Стереть элемент:

private void ButtonClear_Click(object sender, RoutedEventArgs e)


{
inkCanvas1.EditingMode = InkCanvasEditingMode.EraseByStroke;
}

214
5. Модификация приложения Windows Forms: расширение функциональности
приложения и работа с оформление

Такой мощный инструмент как WPF просто нельзя не применить сейчас.


Например возможности по оформление одной лишь кнопки просто безграничны.
Начнём пожалуй с самой формы.

Модифицируем XAML-код формы следующим образом и откомпилируем


приложение:

<Window x:Class="LWP08WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Простое приложение WPF (C#)" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="350"
Width="525" WindowStyle="SingleBorderWindow" ToolTip="Главное окно"
ResizeMode="CanResizeWithGrip" Name="Main" ForceCursor="False"
Icon="/LWP08WPF;component/Images/LWP08WPF.ico">
<Grid>
<InkCanvas HorizontalAlignment="Stretch" Margin="12,12,12,199" Name="inkCanvas1"
VerticalAlignment="Stretch" MinHeight="100" EditingMode="Ink" Background="LightYellow" />
<Button Content="Очистить" HorizontalAlignment="Right" Margin="0,0,12,170"
Name="button1" VerticalAlignment="Bottom" Width="75" Click="button1_Click" Height="23" />
</Grid>
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFD08E8E" Offset="0" />
<GradientStop Color="#FF881E1E" Offset="0.99" />
</LinearGradientBrush>
</Window.Background>
</Window>

Комментарии расставляем при необходимости следующим образом (<!-- Текст


комментария -->):

Рис. 5. 1. Комментарии в коде XAML и добавление комментария через IntelliSense

Новый вид приложения (с новым кодом XAML):

215
Рис. 5. 2. Градиент для фона окна приложения

Фактически установка градиента выполняется изменением свойства Background


для формы:

6. Модификация приложения Windows Foundation Presentation: различные


возможности WPF

Расширим возможности приложения. Расставим в главном окне


(MainWindow.xaml) новые элементы: один групповой элемент Expander (

), реализующий скрытие и показ элементов


находящихся внутри этого элемента; одну кнопку в левом нижнем углу. Внутри
элемента Expander размести одну кнопку и 5 переключателей типа RadioButton.

216
Рис. 6. 1. Новые элементы в окне формы MainWindow.xaml

Имена элементов управления оставляем по умолчанию (имя элемента в этом


случае задаётся как <тип элемента>[следующий номер элемента на форме]).

Свойства элемента Expander (группа свойств Общее):


ExpandDirection: Up
^ Реализует направление раскрытия содержимого элемента. Установим раскрытие
«вверх».
Header: Выберите оформление

Кнопка в главном меню слева (Button):


Имя (первое свойство элемента Button): button7
Content: Команды без событий

Переключатели RadioButton группового элемента Expander:


Имя (первое свойство элемента radioButton1
RadioButton):
Content: Canvas
Имя (первое свойство элемента radioButton2
RadioButton):
Content: StackPanel
Имя (первое свойство элемента radioButton3
RadioButton):
Content: WrapPanel
Имя (первое свойство элемента radioButton4
RadioButton):
Content: DockPanel
Имя (первое свойство элемента radioButton5
RadioButton):

217
Content: Всё

Кнопка группового элемента Expander:


Имя (первое свойство элемента Button): Button2
Content: Сменить оформление

Теперь добавим две новые формы (в качестве окна Window). Для этого выделим
правой кнопкой мыши название проекта в обозревателе решений ( ),
далее выполним Добавить -> Создать элемент… (Ctrl+Shift+A). Выберем Окно
(WPF), введём Имя: Special.xaml:

Рис. 6. 2. Добавление нового элемента – LWP08WPF01

Добавим второе такое же окно с именем NoEventsWindow.xaml, в итоге


получим:

Теперь заполним окна элементами. Для этого можно просто вставить код XAML
для определённого окна. Код для окна Special.xaml:

<Window x:Class="LWP08WPF01.Special"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

218
Title="Простое приложение WPF (C#) :: Специальное окно" Height="480" Width="480"
Name="SpecialWindow" Icon="/LWP08WPF;component/Images/LWP08WPF01.ico">
<DockPanel Width="Auto" Height="Auto" LastChildFill="True">
<!--Верхняя область меню-->
<Menu Width="Auto" Height="20" Background="#FFA9D1F4" DockPanel.Dock="Top">
<!--Меню Файл -->
<MenuItem Header="Файл">
<MenuItem Header="Сохранить как..." />
<Separator/>
<MenuItem Header="Выход" />
</MenuItem>
<!-- Меню Помощь -->
<MenuItem Header="Помощь">
<MenuItem Header="О программе" />
</MenuItem>
</Menu>
<!-- Нижняя область строки состояния объявляется до средней области (чтобы
заполнить весь низ строкой состояния)
что не удалось бы сделать при наличии зафиксированной слева панели -->
<StackPanel Width="Auto" Height="31" Background="#FFCAC5C5"
Orientation="Horizontal" DockPanel.Dock="Bottom">
<Label Width="155" Height="23" Content="Здесь находится строка состояния"
FontFamily="Arial" FontSize="10" />
</StackPanel>
<!-- Левая область основного содержимого -->
<StackPanel Width="136" Height="Auto" Background="White">
<Button Width="Auto" Height="26" Content="Кнопка № 1" Margin="5,5,5,5" />
<Button Width="126" Height="26" Content="Кнопка № 2" Margin="5,5,5,5" />
<Button Width="126" Height="26" Content="Кнопка № 2" Margin="5,5,5,5" />
</StackPanel>
<!-- Правая область основного содержимого. Обратим внимания, что элемент Grid —
последний дочерний элемент, поэтому он занимает всё оставшееся место -->
<Grid Width="Auto" Height="Auto" Background="#FFCC9393">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Рисуем квадраты -->
<Rectangle Fill="LightCyan" Margin="10,10,10,10" Grid.Row="0" Grid.Column="0"
/>
<Rectangle Fill="LightCyan" Margin="10,10,10,10" Grid.Row="0" Grid.Column="1"
/>
<Rectangle Fill="LightCyan" Margin="10,10,10,10" Grid.Row="1" Grid.Column="0"
/>
<Rectangle Fill="LightCyan" Margin="10,10,10,10" Grid.Row="1" Grid.Column="1"
/>
</Grid>
</DockPanel>
</Window>

Абсолютно весь этот генерируется при ручной расстановке элементов


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

Тут же, после вставки кода сформируется окно в конструкторе:

219
Рис. 6. 3. Конструктор окна Special.xaml

Сформируем окно NoEventsWindow.xaml:

<Window x:Class="LWP08WPF01.NoEventsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Приложение WPF (C#) :: Работа с командами без событий" Height="179"
Width="500"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen" Name="SimpleEditor"
Icon="/LWP08WPF;component/Images/LWP08WPF01.ico">
<StackPanel Orientation="Vertical" Width="auto">
<StackPanel Orientation="Horizontal" Background="Gainsboro" Margin="10"
Height="40">
<Button Command="Cut" CommandTarget="{Binding ElementName=textBox1}"
Margin="5,5,5,5" Content ="Вырезать"/>
<Button Command="Copy" CommandTarget="{Binding ElementName=textBox1}"
Margin="5,5,5,5" Content="Копировать"/>
<Button Command="Paste" CommandTarget="{Binding ElementName=textBox1}"
Margin="5,5,5,5" Content="Вставить"/>
<Button Command="Undo" CommandTarget="{Binding ElementName=textBox1}"
Margin="5,5,5,5" Content="Откат"/>
<Button Command="Redo" CommandTarget="{Binding ElementName=textBox1}"
Margin="5,5,5,5" Content="Вернуть"/>
</StackPanel>
<TextBlock HorizontalAlignment="Left" Margin="5,5,5,5" Text="Введите текст,
попробуйте воспользоваться командами. Выделите текст, посмотрите какие кнопки (команды)
станут активными" TextWrapping="Wrap" Height="Auto" Width="Auto" />

220
<TextBox x:Name="textBox1" Margin="5,5,5,5" MaxLines="60" Height="23" Width="470"
Background="#FFF9EBA9" VerticalContentAlignment="Bottom" />
</StackPanel>
</Window>

Обратим внимание, что для элемента TextBlock форматирование в окне XAML


влияет на отображение текста в конструкторе.

Рис. 6. 4. Конструктор окна NoEventsWindow.xaml

Перед тем как добавлять код событий и компилировать приложение немного


расскажем про то, что мы здесь делаем. Наше приложение при помощи переключателей
RadioButton в группе Выберите оформление должно инициализировать выбранный
переключателем элемент, создавать новый объект и загружать в него новое
оформление, после чего изменять оформление главного окна, демонстрируя работу
определённого элемента управления. Первый такой элемент это Canvas (

).

Canvas— один из удобнейших элементов управления макета. Это простой


контейнер положений X и Y. Чтобы располагаться в родительском элементе Canvas,
каждый из его дочерних элементов должен задавать следующие четыре свойства:

 Canvas.Left
 Canvas.Right
 Canvas.Top
 Canvas.Bottom

Если эти четыре свойства заданы, элемент управления будет располагаться в


родительском элементе управления Canvas с учётом этих значений. Эти свойства
выглядят несколько странно, например Canvas.Left. Это не обычные свойства,
используемые в платформе .NET 2.0, а присоединённые свойства зависимости.
Если элемент Canvas — простой контейнер положений X и Y, что происходит с
наложением двух элементов управления и какой дочерний элемент будет находиться на
переднем плане? Всё это контролируется ещё одним присоединённым свойством
зависимости элемента управления Canvas. Это свойство под названием Canvas.ZIndex
определяет, какой элемент управления должен находиться сверху. Как правило, чем
больше значение Canvas.ZIndex, тем выше элемент управления, который задаёт это
присоединенное свойство зависимости. Если свойство Canvas.ZIndex не задано ни для
одного дочернего элемента, оно будет задаваться в порядке добавления дочерних
элементов в элемент Canvas.

221
Нажатие кнопки (для radioButton1: Canvas) будет выполнять создание этого
элемента через код C#. XAML-представление (аналог) кода будет таким:

<Window x:Class="LWP08WPF01.CanvasSpecial"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Приложение WPF (C#) :: Canvas" Width="640" Height="480">
<Canvas Margin="0,0,0,0" Background="White">
<Rectangle Fill="Red"
Stroke="Red"
Width="145"
Height="126"
Canvas.Left="124" Canvas.Top="122"/>
<Ellipse Fill="Blue"
Stroke="Blue"
Width="121" Height="100"
Panel.ZIndex="1"
Canvas.Left="195" Canvas.Top="191"/>
</Canvas>
</Window>

Второй элемент это StackPanel ( ). Он


располагает своё содержимое по вертикали или горизонтали в зависимости от значения
свойства Orientation.

<Window x:Class="LWP08WPF01.StackPanelSpecial"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Приложение WPF (C#) :: StackPanel" Width="640" Height="480">
<StackPanel Margin="0,0,0,0" Background="White" Orientation="Vertical">
<Button Content="Кнопка сверху"/>
<Button Content="Кнопка снизу"/>
</StackPanel>
</Window>

WarpPanel ( ), также очень просто


использовать он просто «оборачивает» своё содержимое.

<Window x:Class="LWP08WPF01.WrapPanelSpecial"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Приложение WPF (C#) :: WrapPanel" Width="640" Height="480">
<WrapPanel Margin="0,0,0,0" Background="White">
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/>
</WrapPanel>
</Window>

DockPanel ( ). Элемент управления DockPanel


— один из самых полезных элементов управления макета. Он, вероятно, будет
использоваться как базовый в каждом новом окне.

222
По сути, с помощью DockPanel (или двух таких элементов) можно реализовать
основной макет большинства современных приложений. Можно закрепить строку меню
сверху, затем левую и правую области основного содержимого, а строку состояния
снизу. И всё это благодаря паре свойств элемента управления DockPanel. Как правило,
закрепление любого дочернего элемента в элементе DockPanel управляется следующим
присоединенным свойством зависимости:

 DockPanel.Dock

Этому свойству можно присвоить значения Left, Right, Top или Bottom. Есть ещё
одно полезное свойство (обычное свойство CLR) элемента управления DockPanel,
называемое LastChildFill. Если этому свойству присвоено значение true, последний
добавленный дочерний элемент будет занимать всё оставшееся свободное
пространство. Оно переопределяет свойство DockPanel.Dock, которое может быть уже
задано дочерним элементом.

Окно реализующее панель меню и свойство описанное выше, в XAML-коде


выглядит так:

<Window x:Class="LWP08WPF01.DockPanelSpecial"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Приложение WPF (C#) :: DockPanel" Width="640" Height="480">
<DockPanel Width="Auto" Height="Auto" LastChildFill="True">
<Rectangle Fill="CornflowerBlue" Stroke="CornflowerBlue" Height="20"
DockPanel.Dock="Top"/>
<Rectangle Fill="Orange" Stroke="Orange" />
</DockPanel>
</Window>

Элемент Grid ( ) — самый сложный элемент


управления макета в WPF. Он немного похож на табличный элемент управления HTML,
в котором можно задавать строки и столбцы, а ячейки могут содержать несколько строк
или столбцов. Для свойств Width и Height столбцов и строк может применяться
странный синтаксис с использованием символа звездочки (*), предоставляемый с
помощью класса GridLength. Его можно представить в виде процентного разделителя
свободного места. Рассмотрим следующую разметку:

<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>

Здесь в элементе Grid объявлены три элемента ColumnDefinition. Ширина


первого элемента ColumnDefinition зафиксирована (40 пикселей), а оставшееся
пространство делится между двумя другими элементами ColumnDefinition, причем
последнему из них выделяется в два раза больше места, чем предпоследнему. Этот же
принцип применяется в отношении RowDefinition.
Для уведомления системы макета WPF о ячейке, к которой относятся дочерние
элементы элемента Grid, используются следующие присоединенные свойства
зависимости (нумерация индекса начинается с 0):

 Grid.Column
 Grid.Row

А для задания количества строк или столбцов, занимаемых ячейкой,


используются следующие присоединенные свойства зависимости (значения начинаются
с 1):

223
 Grid.ColumnSpan
 Grid.RowSpan

При умелом использовании элемента управления Grid можно имитировать


практически любые другие элементы управления макета. Пример окна с тремя
панелями реализованного через Grid:

<Window x:Class="LWP08WPF01.GridSpecial"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Приложение WPF (C#) :: Grid" Width="640" Height="480">
<Grid Width="Auto" Height="Auto" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Rectangle Fill="Aqua" Grid.Column="0" Grid.Row="0"/>
<Rectangle Fill="Plum" Grid.Column="1" Grid.ColumnSpan="2"/>
</Grid>
</Window>

Некоторые панели могут быть связаны с данными, поэтому иногда на панели


может отображаться слишком много дочерних элементов. Например, если элемент
StackPanel содержит элемент ListBox, привязанный к большому запросу базы данных.
В этом случае в списке будет множество элементов, то есть у элемента ListBox будет
множество дочерних элементов. Однако по умолчанию в элементе ListBox используется
вертикальный элемент StackPanel для обработки своих элементов. В платформе WPF
доступен ещё один прием, который может помочь в этой ситуации. Присоединённое
свойство зависимости VirtualizingStackPanel.IsVirtualizing в элементе ListBox
обеспечит виртуализацию отображения элементов внутреннего элемента управления
StackPanel в элементе ListBox. Но что такое этот элемент VirtualizingStackPanel?
Виртуализация панели означает, что создаются только видимые элементы.
Остальные элементы не отображаются. Рассмотрим создание элемента ListBox с
изображениями, привязанного к базе данных, содержащей 100 500 строк... На загрузку
такого элемента уйдёт очень много времени. Если используется виртуализированная
панель, в интерфейсе пользователя будут созданы только видимые изображения. При
прокрутке списка элементы, видимые в данный момент, будут уничтожаться, а новые
видимые элементы загружаться в интерфейс пользователя. Только одна панель
поддерживает виртуализацию, и это панель VirtualizingStackPanel. Новые панели
виртуализации необходимо разрабатывать самостоятельно.

Теперь немного про окно NoEventsWindows.xaml и перенаправление команд.

Система команд WPF построена на основе классов RoutedCommand и


RoutedEvent. От простого обработчика событий, присоединённого к кнопке или
таймеру, команды отличаются тем, что они отделяют семантику и инициатор действия
от логики. Это позволяет вызывать для нескольких разнородных источников одну и ту
же логику команды, а также настраивать её для различных целевых объектов.
Примерами команд служат операции редактирования Копировать, Вырезать и
Вставить, доступные во многих приложениях. Семантика команды унифицирована для
различных приложений и классов, однако логика действия специфична для
конкретного объекта. Сочетание клавиш Ctrl+X вызывает команду Вырезать в классах
текста, классах изображений и веб-браузерах. Однако фактическая логика выполнения
этой операции определяется объектом или приложением, в котором она выполняется, а
не источником, в котором она вызывается. В текстовом объекте можно вырезать и

224
поместить выделенный текст в буфер обмена. В графическом объекте можно вырезать
выделенное изображение. Однако для вызова команды в обоих классах может
использоваться один и тот же источник команды, например объект KeyGesture или
кнопка на панели инструментов. В платформах .NET3.0/3.5/4.0 предусмотрено
множество готовых команд для выполнения типичных задач.

 Класс ApplicationCommands содержит такие команды, как вырезание,


копирование и вставка.
 Класс MediaCommands содержит такие команды, как Усиление баса,
Следующий канал, Предыдущий канал и Отключение звука.
 Класс NavigationCommands содержит такие команды, как Назад, Вперёд и
Избранное.
 Класс ComponentCommands содержит такие команды, как Вниз, Фокус на
страницу вверх и В конец.
 Класс EditingCommands содержит такие команды, как Выровнять по центру,
Возврат и Удалить.

Полный список команд, если нужно, можно посмотреть в библиотеке MSDN


(http://msdn.microsoft.com/ru-ru/ms348103).

С помощью встроенных команд можно реализовать довольно сложную


функциональность без процедурного кода.

Закончим построение приложение добавив все обработички. Кнопка Команды


без событий, событие Click:

private void button7_Click(object sender, RoutedEventArgs e)


{
NoEventsWindow Window = new NoEventsWindow();
Window.Show();
}

Кнопка Сменить оформление:

private void button2_Click(object sender, RoutedEventArgs e)


{

if (radioButton1.IsChecked == true)
{
Canvas Canv = new Canvas(); // Создаём экземпляр объекта Canvas
// Добавить элемент Canvas в качестве единственного дочернего элемента
Window
this.Content = Canv;
Canv.Margin = new Thickness(0, 0, 0, 0); // Создаём поле расстояния до
ближайшего элемента. Так как ближайшего элемента нет, Canvas заполняет всю форму
Canv.Background = new SolidColorBrush(Colors.White); // Задаём фон
// Прямоугольник
Rectangle r = new Rectangle();
r.Fill = new SolidColorBrush(Colors.Blue); // Заливка прямоугольник
r.Stroke = new SolidColorBrush(Colors.Red); // Граница прямоугольника
r.Width = 145; // Ширина прямоугольника
r.Height = 126; // Высота прямоугольника
r.SetValue(Canvas.LeftProperty, (double)124); // Устанавливаем левую
верхнюю границу прямоугольника
r.SetValue(Canvas.TopProperty, (double)122); // Устанавливаем верхнюю
границу первого элемента
Canv.Children.Add(r); // Добавляем прямоугольник в качестве дочернего для
Canvas
// Эллипс
Ellipse el = new Ellipse();
el.Fill = new SolidColorBrush(Colors.Green);

225
el.Stroke = new SolidColorBrush(Colors.Green);
el.Width = 121;
el.Height = 100;
el.SetValue(Canvas.ZIndexProperty, 1);
el.SetValue(Canvas.LeftProperty, (double)195);
el.SetValue(Canvas.TopProperty, (double)191);
Canv.Children.Add(el);
// Кнопка Вернуть оформление
Button button3 = new Button();
button3.Content = "Вернуть оформление";
button3.Width = 131;
button3.Height = 23;
button3.HorizontalAlignment = HorizontalAlignment.Right;
button3.VerticalAlignment = VerticalAlignment.Bottom;
button3.Margin = new Thickness(12, 12, 12, 12); // Кнопка будет
распологаться в левом верхнем углу
button3.Click += new RoutedEventHandler(button3_Click); // Переопределяем
события для кнопки
Canv.Children.Add(button3);
}

if (radioButton2.IsChecked == true)
{
StackPanel SP = new StackPanel();
// Добавить элемент StackPanel в качестве единственного дочернего
элемента Window
this.Content = SP;
SP.Margin = new Thickness(0, 0, 0, 0);
SP.Background = new SolidColorBrush(Colors.White);
SP.Orientation = Orientation.Vertical;
// Кнопка 1
Button b1 = new Button();
b1.Content = "Кнопка сверху";
SP.Children.Add(b1);
// Кнопка 2
Button b2 = new Button();
b2.Content = "Кнопка снизу";
SP.Children.Add(b2);
// Кнопка Вернуть оформление
Button button4 = new Button();
button4.Content = "Вернуть оформление";
button4.Width = 131;
button4.Height = 23;
button4.HorizontalAlignment = HorizontalAlignment.Right;
button4.VerticalAlignment = VerticalAlignment.Bottom;
button4.Margin = new Thickness(12, 12, 12, 12); // Кнопка будет
располагаться справа вверху под "Кнопка внизу"
button4.Click += new RoutedEventHandler(button3_Click);
SP.Children.Add(button4);
}

if (radioButton3.IsChecked == true)
{
WrapPanel WP = new WrapPanel();
// Добавить элемент WrapPanel в качестве единственного дочернего элемента
Window
this.Content = WP;
WP.Margin = new Thickness(0, 0, 0, 0);
WP.Background = new SolidColorBrush(Colors.White);
// Добавить прямоугольники (создание одинаковых объектов)
Rectangle r;

for (int i = 0; i <= 10; i++)


{
r = new Rectangle();

226
r.Fill = new SolidColorBrush(Colors.Blue);
r.Margin = new Thickness(10, 10, 10, 10);
r.Width = 60;
r.Height = 60;
WP.Children.Add(r);
}
// Кнопка Вернуть оформление
Button button5 = new Button();
button5.Content = "Вернуть оформление";
button5.Width = 131;
button5.Height = 23;
button5.HorizontalAlignment = HorizontalAlignment.Right;
button5.VerticalAlignment = VerticalAlignment.Bottom;
button5.Margin = new Thickness(12, 12, 12, 12); // Кнопка будет
рсполагаться слева вверху под всеми элементами
button5.Click += new RoutedEventHandler(button3_Click);
WP.Children.Add(button5);
}

if (radioButton4.IsChecked == true)
{
DockPanel DP = new DockPanel();
DP.LastChildFill = true;
// Это эквивалентно Width = "Auto" в XAML, кроме элементов GridColumn
Width/Height и GridRow Width/Height
DP.Width = Double.NaN;
DP.Height = Double.NaN;
// Добавить элемент WrapPanel в качестве единственного дочернего элемента
Window
this.Content = DP;
// Добавить прямоугольник (верхний)
Rectangle rTop = new Rectangle();
rTop.Fill = new SolidColorBrush(Colors.CornflowerBlue);
rTop.Stroke = new SolidColorBrush(Colors.CornflowerBlue);
rTop.Height = 20;
DP.Children.Add(rTop);
// Добавить прямоугольник (нижний)
rTop.SetValue(DockPanel.DockProperty, Dock.Top);
Rectangle rFill = new Rectangle();
rFill.Fill = new SolidColorBrush(Colors.Orange);
rFill.Stroke = new SolidColorBrush(Colors.Orange);
rFill.Height = 20;
DP.Children.Add(rFill);
rFill.SetValue(DockPanel.DockProperty, Dock.Bottom);
// Кнопка Вернуть оформление
Button button6 = new Button();
button6.Content = "Вернуть оформление";
button6.Width = 131;
button6.Height = 23;
button6.HorizontalAlignment = HorizontalAlignment.Right;
button6.VerticalAlignment = VerticalAlignment.Bottom;
button6.Margin = new Thickness(12, 12, 12, 12); // Кнопка будет
располагаться между верхним и нижним прямоугольником
button6.Click += new RoutedEventHandler(button3_Click);
DP.Children.Add(button6);
}

if (radioButton5.IsChecked == true)
{
Special Window = new Special();
Window.Show();
}
}

227
Теперь организуем событие нажатия виртуальной кнопки Вернуть
оформление:

private void button3_Click(object sender, RoutedEventArgs e)


{
this.Content = Default; // Возвращаем первоначальное оформление
}

И добавим код для сохранения оформления в главном окне (модифицируем код


как показано ниже):

public partial class MainWindow : Window


{
Object Default = new Object(); // Инициализируем контейнер для первоначального
оформления

public MainWindow()
{
InitializeComponent();
Default = this.Content; // Объекту Default отдаём текущий набор элементов
оформления
}

7. Модификация приложения Windows Foundation Presentation: немного о


стилях и шаблонах

В WPF существует две принципиальные концепции для разработки своих


собственных элементов управления и уникального оформления. Это понятия шаблона и
стиля.

Стили позволяют разработчику WPF вести общий список значений свойств в


удобном месте. До некоторой степени они напоминают таблицы CSS в веб-разработке.
Как правило, стили находятся в разделе ресурсов или в отдельном словаре ресурсов.
Благодаря стилям в платформе WPF реализуются элементы управления с поддержкой
темы.

Элемент Style обладает следующими свойствами:

Возвращает или задает определенный стиль, являющийся основой текущего


BasedOn
стиля
Возвращает объект Dispatcher, с которым связан этот объект DispatcherObject
Dispatcher
(наследуемый от DispatcherObject)
IsSealed Возвращает значение, указывающее, доступен ли стиль только для чтения
Возвращает или задает коллекцию ресурсов, которые могут использоваться в
Resources
области видимости данного стиля
TargetType Возвращает или задает тип, для которого предназначен данный стиль
Setters Возвращает коллекцию объектов Setter и EventSetter
Возвращает коллекцию объектов TriggerBase, применяющих значения
Triggers
свойств на основе заданных условий

Из этих свойств самые важные следующие:

 BasedOn («наследование» стиля, каждый стиль поддерживает только одно


значение).

<Style x:Key="Style1">
...
</Style>

228
<Style x:Key="Style2" BasedOn="{StaticResource Style1}">
...
</Style>

 TargetType (ограничивает типы элементов управления, которые могут


использовать данный стиль).

Например, если свойству TargetType объекта Style присвоено значение Button,


этот стиль нельзя использовать для элемента управления типа TextBox. Задать
свойство TargetType следует так:

<Style TargetType="{x:Type Button}">


...
</Style>

 Setters (задают событию или свойству некоторое значение; при задании


события они подключают это событие; при задании свойства они присваивают
свойству значение).

Объекты EventSetter используются для задания событий, например для


связывания события Click элемента Button, как в примере ниже. Однако значительно
чаще объекты Setter используются просто для задания значений свойствам (второй
участок кода):

<Style TargetType="{x:Type Button}">


<EventSetter Event="Click" Handler="b1SetColor"/>
</Style>

<Style TargetType="{x:Type Button}">


<Setter Property="BackGround" Value="Yellow"/>
</Style>

 Triggers (по сути, объекты Trigger позволяют применять изменения при


выполнении определенных условий (например, когда значение некоторого
свойства становится равным true или когда происходит некоторое событие).

В примере показан именованный стиль, доступный для элементов управления


Button. В стиле определен элемент Trigger, который изменяет свойство Foreground
элемента Button, когда свойство IsPressed равно true:

<Style x:Key="Triggers" TargetType="Button">


<Style.Triggers>
<Trigger Property="IsPressed" Value="true">
<Setter Property = "Foreground" Value="Green"/>
</Trigger>
</Style.Triggers>
</Style>

Напишем же небольшое приложение, демонстрирующее шаблоны и стили и


добавим его в текущее приложение. Вызов окна с новой функциональностью будем
производить по кнопке. Окно сделаем немодальным. Разместим кнопку Изменённые
кнопки (button8) где-нибудь на свободном участке приложения. Добавим новое окно
(Window) и зададим ему имя ButtonStylesWindow.xaml. XAML-код нового окна
сделаем таким:

<Window x:Class="LWP08WPF01.ButtonStylesWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LWP08WPF01"

229
Title="Приложение WPF (C#) :: Изменённые кнопки" SizeToContent="WidthAndHeight"
Name="lol">
<Window.Resources>
<Style TargetType="Button" x:Key="BaseButtonStyle">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="10" />
</Style>

<Style TargetType="Button" x:Key="BigButtonStyle" BasedOn="{StaticResource


BaseButtonStyle}">
<Setter Property="Width" Value="170" />
<Setter Property="Height" Value="50" />
<Setter Property="FontSize" Value="16" />
</Style>

<Style TargetType="Button" x:Key="SmallButtonStyle" BasedOn="{StaticResource


BaseButtonStyle}">
<Setter Property="Width" Value="150" />
<Setter Property="Height" Value="25" />
<Setter Property="FontSize" Value="12" />
</Style>

<Style TargetType="Button" x:Key="RedButtonStyle" BasedOn="{StaticResource


BaseButtonStyle}">
<Setter Property="Foreground" Value="Red" />
</Style>

<Style TargetType="Button" x:Key="GreenButtonStyle" BasedOn="{StaticResource


BaseButtonStyle}">
<Setter Property="Foreground" Value="Green" />
</Style>

<Style TargetType="Button" x:Key="BoldButtonStyle" BasedOn="{StaticResource


BaseButtonStyle}">
<Setter Property="FontWeight" Value="Bold" />
</Style>
</Window.Resources>

<Grid Margin="30">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>

<Button Style="{local:MultiStyle BigButtonStyle GreenButtonStyle}"


Content="Большая, зелёная"/>
<Button Style="{local:MultiStyle BigButtonStyle RedButtonStyle}"
Content="Большая, красная" Grid.Column="1"/>

<Button Style="{local:MultiStyle SmallButtonStyle GreenButtonStyle}"


Content="Мальная, зелёная" Grid.Row="1"/>
<Button Style="{local:MultiStyle SmallButtonStyle RedButtonStyle}"
Content="Маленькая, красная" Grid.Row="1" Grid.Column="1"/>

<Button Style="{local:MultiStyle BigButtonStyle GreenButtonStyle


BoldButtonStyle}" Content="Б., зелёная, жирная" Grid.Row="2"/>
<Button Style="{local:MultiStyle BigButtonStyle RedButtonStyle BoldButtonStyle}"
Content="Б., красная, жирная" Grid.Row="2" Grid.Column="1"/>

230
<Button Style="{local:MultiStyle SmallButtonStyle GreenButtonStyle
BoldButtonStyle}" Content="М., зелёная, жирная" Grid.Row="3" />
<Button Style="{local:MultiStyle SmallButtonStyle RedButtonStyle
BoldButtonStyle}" Content="М., красная, жирная" Grid.Row="3" Grid.Column="1"/>
</Grid>
</Window>

Добавим два следующих файла с кодом (добавляем как Код -> Класс). Имя
первого файла: ButtonStylesExtension.cs (расширение стилей кнопок). Содержимое
файла делаем таким (комментарии включены):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Markup;
using System.Windows;

namespace LWP08WPF01
{
[MarkupExtensionReturnType(typeof(Style))] // Сообщаем что класс Style может
возвращать расширение разметки (typeof используется для получение объекта типа, является
альтернативой создания экземпляра класса new и получение типа методом GetType() )
public class MultiStyleExtension : MarkupExtension // Создаём класс как базовый
класс для расширенной разметки XAML
{
private string[] resourceKeys;

/// <summary>
/// Публичный конструктор.
/// </summary>
/// <param name="inputResourceKeys">Конструктор ввода должен быть строкой,
состоящей из одного или нескольких имен стилей, разделенных пробелами.</param>
public MultiStyleExtension(string inputResourceKeys) // получаем входящий ключ на
ресурс
{
if (inputResourceKeys == null) // Если ничего не получаем, генерируем (throw)
новое (new) исключение ArgumentNullException (пустой аргумент) для ключа
{
throw new ArgumentNullException("inputResourceKeys");
}
this.resourceKeys = inputResourceKeys.Split(new char[] { ' ' },
StringSplitOptions.RemoveEmptyEntries); // Выполняем слияние строк для ключа, удаляя
лишни символы пробелов

if (this.resourceKeys.Length == 0) // Если не вышло изначально (0) или мы


убили ключ для ресурса, генерируем новое исключение об отсутсвиии ключа
{
throw new ArgumentException("Нет указанных входных ключей ресурсов.");
}
}

/// <summary>
/// Возвращает стиль, который объединяет все стили с ключами, указанными в
конструкторе.
/// </summary>
/// <param name="serviceProvider">Поставщиков услуг для данного расширения
разметки.</param>
/// <returns>Стиль, который объединяет все стили с ключами, указанными в
конструкторе.</returns>
public override object ProvideValue(IServiceProvider serviceProvider)
{
Style resultStyle = new Style(); // Создаём объект класса Style

231
try
{
foreach (string currentResourceKey in resourceKeys) // Пробегаем в цикле
по всем ключам ресорусов (foreach повторяет группу вложенных операторов для каждого
элемента массива или коллекции объектов)
{
Style currentStyle = new
StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style; //
Передаём через конструктор исходный ключ конкретного текущего стиля

if (currentStyle == null) // Проверям что стиль есть, если нет, то


выполняем вывод ошибки (генерируем новое исключение)
{
throw new InvalidOperationException("Не удалось найти стиль для
ключа ресурса: " + currentResourceKey + ".");
}
resultStyle.Merge(currentStyle); // Выполняем слияние для текущего
стиля (смотрим файл StyleExtensionMethods.cs)
}
}
catch
{
// Ловим ошибку: IXamlSchemaContextProvider
}
return resultStyle; // Возвращаем готовый стиль
}
}
}

Второй файл (StyleExtensionMethods.cs) будет содержать нужный нам метод


Merge():

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;

namespace LWP08WPF01
{
public static class StyleExtensionMethods
{
/// <summary>
/// Слияние двух стилей передается в качестве параметров. Первый стиль будет
изменен, чтобы получить всю информацию, присутствующую во втором.
/// Если имеет место совпадение стилей, второй стиль имеет больший приоритет.
/// </summary>
/// <param name="style1">Первый стиль для слияния, который будет изменен, чтобы
получить информацию из второго.</param>
/// <param name="style2">Второй стиль для слияния, который передают информацию
для первого стиля.</param>
public static void Merge(this Style style1, Style style2)
{
if (style1 == null) { throw new ArgumentNullException("style1"); }
if (style2 == null) { throw new ArgumentNullException("style2"); }

if (style1.TargetType.IsAssignableFrom(style2.TargetType)) // проверка типов,


если совпадают "сливаем" TargetType стиля
{
style1.TargetType = style2.TargetType;
}

if (style2.BasedOn != null) // Если базовый стиль второго стиля не нуль

232
{
Merge(style1, style2.BasedOn); // Сливаем базовый стиль style2.VaseOn в
style1
}

foreach (SetterBase currentSetter in style2.Setters) // Пробегаем по все


объектам типа Setter второго стиля и записываем каждый найденный в первый стиль
{
style1.Setters.Add(currentSetter);
}

foreach (TriggerBase currentTrigger in style2.Triggers) // Пробегаем по всем


объектам типа Trigger
{
style1.Triggers.Add(currentTrigger);
}
// Этот код нужен только при использовании DynamicResources
foreach (object key in style2.Resources.Keys)
{
style1.Resources[key] = style2.Resources[key];
}
}
}
}

Конструктор сформирует представление окна:

Рис. 7. 1. Конструктор окна ButtonStylesWindow.xaml

Выделим мышкой любую кнопку в конке и посмотрим на свойства. Ищем поле


Общее: Style, нажимаем на значок «ромбика» ( ). Жмём на него, далее Применить
ресурс…, увидим следующее:

233
Откроется список всех доступных стилей для этой кнопки. Выбор конкретного
стиля осуществляется например для кнокпи Большая, зелёная так. Объявляем
базовый стиль от кнопки Button, даём ему имя BaseButtonStyle:

<Style TargetType="Button" x:Key="BaseButtonStyle">


<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="10" />
</Style>

Далее создаём на основе кнопки базового стиль стиль «большой» кнопки и даём
стилю имя BigButtonStyle:

<Style TargetType="Button" x:Key="BigButtonStyle" BasedOn="{StaticResource


BaseButtonStyle}">
<Setter Property="Width" Value="170" />
<Setter Property="Height" Value="50" />
<Setter Property="FontSize" Value="16" />
</Style>

Теперь создаём стиль зелёной кнопки:

<Style TargetType="Button" x:Key="GreenButtonStyle" BasedOn="{StaticResource


BaseButtonStyle}">
<Setter Property="Foreground" Value="Green" />
</Style>

И применяем всё это к макету кнопки всталенной на форму:

<Button Style="{local:MultiStyle BigButtonStyle GreenButtonStyle}"


Content="Большая, зелёная"/>

Как видим, такую кнопку можно создать и без всех этих стилей, просто задав
необходимые свойства. Однако шаблонизация стилей хороша когда меняется не один
два параметра для элемента, а ОЧЕНЬ много и кардинально. И на настройку каждой
кнопки просто уйдёт много времени, тогда как можно воспользоваться заранее
приготовленным шаблоном.

234
Обработчик кнопки вызова последнего окна такой:

private void button8_Click(object sender, RoutedEventArgs e)


{
ButtonStylesWindow Window = new ButtonStylesWindow();
Window.Show();
}

8. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан


ниже (Рис. 8. 1):

Рис. 8. 1. Модифицированное приложение Windows Foundation Presentation (Работа


главного окна и окна с кнопками без обработчиков событий)

235
Рис. 8. 2. Работа окна с изменёнными кнопками с новыми стилями

Рис. 8. 3. Работа кнопки Сменить оформление в режиме Всё (справа) и WrapPanel


(слева)

236
Рис. 8. 4. Работа кнопки Сменить оформление в режиме Всё (справа), изменённый
размер окна и Canvas (слева), изменённый размер окна

9. О приложении к Лабораторной работе № 8

Получившуюся программу (LWP08WPF01.exe), собранную из кусков кода


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

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

9. Лабораторная работа № 9: Создание приложения-презентации Windows Foundation


Presentation

Лабораторная работа № 9: Создание приложения-презентации Windows


Foundation Presentation

Содержание

1. Вводная часть
2. Создание приложения Windows Foundation Presentation
3. Модификация приложения Windows Foundation Presentation
4. Модификация приложения Windows Foundation Presentation: работа с
решением в Expression Blend
5. Завершающая часть
6. О приложении к Лабораторной работе № 9

1. Вводная часть

В предыдущей лабораторной работе данного практикума были рассмотрены


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

237
примеру Windows Forms). И разумеется создать такое приложение на WPF только в
среде разработки Visual Studio и при этом применить уникальный стиль можно, но для
этого нужно хорошо знать XAML в его первозданном виде. Целью этого практикума не
ставится ознакомить читателя с XAML на уровне кода, потому можно пойти другими
путями к конечной цели.
Стандартные инструменты редактирования элементов управления и
формирования разметки для среды разработки хороши, но не более. Специально для
иных, более расширенных оформлений и дизайнов создан другой мощнейший
инструмент: Microsoft Expression Blend (текущая версия 4).

Целью данной работы станет разработка приложения-презентации,


оперирующего страницами XAML как «слайдами» (привет PowerPoint!). Для
разработки такого приложения используем класс презентации (самописный) и
организуем стандартный набор функциональности приложения. Слайды делать можно
(и я считаю нужно) не в среде разработки, а непосредственно в Expression Studio 4
Ultimate.

Рис. 1. 1. Рис. 1. 1. Получение Epression Studio 4 Ultimate (DreamSpark.com)

238
Как показано на рисунке выше, пакет Expresson Studio доступен бесплатно. В
пакет включены четыре отдельных приложения (Web, blend, Desing, Encoder), а также
набор документации. Другие компоненты кроме Blend на не интересуют (опять же цель
данного практикума не ознакомить читателя с работой иных сторонних продуктов, но
основы работы в Expression Blend будут изложены в кратком виде).

2. Создание приложения Windows Foundation Presentation

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение WPF. Также здесь можно выбрать какой использовать
«фреймворк» (набора компонентов для написания программ, конечную платформу). В
нашем случае выберем .NET Framework 4.

239
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP08WPF02 — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория где будет находится весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

240
Рис. 2. 3. Вводим данные нового проекта приложения WPF

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения WPF (не пустого изначально). Среда разработки сформировала исходный
код в двух файлах (не считая стандартного AssemblyInfo.cs). Один из файлов
отвечает за программу в целом (это файл App.xaml.cs), а второй за формирование
окна приложения — инициализацию конструктора формы и всех её элементов (это
файл MainWindow.xaml.cs). Как видим, исходный код, например файла App.xaml.cs
не отличается от шаблонного:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;

namespace LWP08WPF02
{
/// <summary>
/// Логика взаимодействия для App.xaml
/// </summary>
public partial class App : Application
{
}
}

XAML-код App.xaml выглядит так:

<Application x:Class="LWP08WPF01.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

241
StartupUri="MainWindow.xaml">
<Application.Resources>

</Application.Resources>
</Application>

Файл MainWindows.xaml.cs содержит следующий исходный код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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 LWP08WPF01
{
/// <summary>
/// Логика взаимодействия для MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

XAML-код MainWindows.xaml выглядит так:

<Window x:Class="LWP08WPF01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>

</Grid>
</Window>

242
Рис. 2. 4. Обозреватель решений: состав проекта приложения WPF сформированного
средой разработки

Теперь посмотрим текущий макет нашей главной формы, которая должна


запуститься в качестве главного окна:

243
Рис. 2. 5. Макет формы MainWindows.xaml: отображение конструктора формы (сверху)
и представление этой формы в качестве рамзетки XAML (внизу)

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

Рис. 2. 6. Запуск приложения WPF по конфигурации Debug

3. Модификация приложения Windows Foundation Presentation

Начнём сразу с нового класса. Добавим новый класс в проект. Правая кнопка

мыши по названию проекта ( ), затем Добавить -> Создать элемент…


(Ctrl+Shift+N). Выбираем (Установленные шаблоны: Visual C#): Класс, вводим
Имя: Presenation.cs (пространство имён будет выбрано как у приложения, имя класса
будет выбрано по названию файла):

244
Рис. 3. 1. Добавление нового элемента – LWP09WPF02

Сформируется новый файл в обозревателе решений ( ), его код


такой:

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

namespace LWP09WPF02
{
class Presentation
{
}
}

Модифицируем код следующим образом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace LWP09WPF02 // Пространство имён приложения (должно быть такое же как и у


главного окна)
{
public class Presentation : INotifyPropertyChanged
{
private string[] slides = { "01Title.xaml", "02Slide.xaml", "03Slide.xaml" }; //
Список слайдов (последовательность определяет вызов)
private int currentIndex = 0;

245
private bool currentTimer = false;

public string[] Slides // Свойство для получения списка всех слайдов


{
get { return this.slides; }
}

public string CurrentSlide // Свойство для получения текущего выбранного слайда


{
get { return this.slides[this.currentIndex]; }
}

public int CurrentIndex // Свойство для получения текущего выбранного индекса для
слайда или установки нового индекса и генерации PropertyChanged
{
get { return this.currentIndex; }
set
{

if (this.currentIndex != value)
{
this.currentIndex = value;
this.OnPropertyChanged("CurrentSlide");
this.OnPropertyChanged("CanGoBack");
this.OnPropertyChanged("CanGoNext");
}
}
}

public bool CanGoBack // Логическая переменая (свойство): проверка, есть ли ещё


страницы для перехода назад
{
get { return this.currentIndex > 0; }
}

public bool CanGoNext // Логическая переменая (свойство): проверка, есть ли ещё


страницы для перехода вперёд
{
get { return this.currentIndex < this.slides.Length - 1; }
}

public void GoBack() // Метод Назад


{

if (this.CanGoBack)
{
this.CurrentIndex--;
}
}

public void GoNext() // Метод Вперёд


{

if (this.CanGoNext)
{
this.CurrentIndex++;
}
}

public event PropertyChangedEventHandler PropertyChanged; // Событие


PropertyChanged

private void OnPropertyChanged(string propertyName) // Метод получения параметра


для события PropertyChanged (при изменении свойств компонента), компонент для события:
propertyName

246
{

if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

public void GoHome() // Метод Домой


{
this.CurrentIndex = 0;
}

public bool GoAuto // Метод Авто


{
get { return this.currentTimer; }
set
{

if (this.currentTimer == true)
{
this.currentTimer = false;
}
else
{
this.currentTimer = true;
}
}
}
}
}

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

Все слайды, добавляемые в студии автоматически или в ручную должны быть


типа Page (страница), а не Window. Page, как уже было показано в предыдущей
лабораторной работе прекрасно подходит для веб-браузера и отображает всё
содержимое.

Добавим три слайда (потом ещё один, четвёртый в Expression Blend). Для этого
нам нужно выбрать в окне добавления нового элемента (Установленные шаблоны ->
WPF) выберем элемент Страница (WPF). Для первой страницы Имя укажем как
01Title.xaml:

247
Рис. 3. 2. Добавление новой страницы: Страница (WPF), тип Page

XAML-код для 01Title.xaml:

<Page x:Class="LWP09WPF02._01Title"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="_01Title">

<Grid>

</Grid>
</Page>

Добавим ещё две такие же страницы под именами 02Slide.xaml и 03Slide.xaml.

Код добавленных страниц трогать мы не будем (это просто не нужно). Всё будет
выполнять главное окно и наш класс. Поэтом добавление новых страниц слайда
максимально упрощено и превращается в работу по заполнению их содержимым (как в
PowerPoint). Естественно, чтобы добавить для страницы уникальную функциональность,
код придётся редактировать. Но лучше всю функциональность организовывать в уже
готовом классе, тогда будет максимально просто добавлять новые функции для новых
слайдов не заботясь о правильном перенесении кода. Для слайдов же останется лишь
добавлять обработчики новых методов и расставлять новые элементы с привязкой.

Изменим код главного окна следующим образом:

248
<Window x:Class="LWP09WPF02.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:LWP09WPF02="clr-namespace:LWP09WPF02"
Title="Презентация WPF (C#)" Height="600" Width="800" ResizeMode="NoResize"
WindowState="Maximized" WindowStyle="None" Name="Window" KeyDown="Window_KeyDown"
PreviewMouseMove="Window_PreviewMouseMove"
PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown">
<Window.Resources>
<LWP09WPF02:Presentation x:Key="presentation"/>
</Window.Resources>

<Grid>
<Viewbox Margin="10,20,10,40" Stretch="Uniform">
<StackPanel>
<!-- Подставляем всегда: заголовок страницы -->
<TextBlock VerticalAlignment="Top" Height="84" FontFamily="Calibri"
FontSize="65" FontWeight="Bold" Text="{Binding Content.Title, ElementName=Frame,
Mode=Default}" TextAlignment="Center" TextWrapping="Wrap">
</TextBlock>
<!-- Подставляем всегда: содержимое слайда -->
<Frame Width="1000" Height="600" Source="{Binding CurrentSlide,
Source={StaticResource presentation}}" x:Name="Frame" NavigationUIVisibility="Hidden"
Background="{x:Null}" Focusable="False"/>
</StackPanel>
</Viewbox>

<Button Width="63.684" Height="36.709" Content="Назад" Click="Back_Click"


RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Left" Margin="10,0,0,10"
VerticalAlignment="Bottom" IsEnabled="{Binding CanGoBack, Source={StaticResource
presentation}}" />
<Button HorizontalAlignment="Right" Margin="0,0,10,10" VerticalAlignment="Bottom"
Width="63.684" Height="36.709" Content="Вперёд" Click="Next_Click" IsEnabled="{Binding
CanGoNext, Source={StaticResource presentation}}" />
<Button Content="Домой" Height="36.709" Margin="80,0,0,10"
VerticalAlignment="Bottom" Click="Home_Click" HorizontalAlignment="Left" Width="64" />
<Button Content="Авто" Height="36.709" HorizontalAlignment="Right"
Margin="0,0,80,10" VerticalAlignment="Bottom" Width="63.684" Click="Auto_Click" />
<Ellipse Height="23" HorizontalAlignment="Right" Margin="0,0,150,12" Name="EAuto"
Stroke="Black" VerticalAlignment="Bottom" Width="23" Visibility="Hidden" />
<Label Content="А" Height="26" HorizontalAlignment="Right" Margin="0,0,150,12"
Name="LAuto" VerticalAlignment="Bottom" Width="20" Visibility="Hidden" />
</Grid>
</Window>

На самом деле это было сконструрировано в местном визуальном редакторе.


Сначала были изменены свойства главного окна:
Tilte: Презентация WPF (C#)
Height: 600
Width: 800
ResizeMode: NoResize
WindowState: Maximized
WindowStyle: None

Затем добавили ресуры окна:

<LWP09WPF02:Presentation x:Key="presentation"/>

Добавили элемент ViewBox ( ), растянув его по


сетке и закрепив, дав свойство:

249
Stretch: Uniform

Затем внутрь ViewBox запихнули StackPanel с двумя элементами: TextBox и

Frame ( ). Ключевые привязки для отображения


подгружаемого содержимого для этих элементов такие:

TextBox:
Text="{Binding Content.Title, ElementName=Frame, Mode=Default}"

Frame:
Source="{Binding CurrentSlide, Source={StaticResource presentation}}"

Поставили кнопки Button (4 штуки), а также нарисовали маленький эллипс и


внутрь поставили Label с надписью «А». Кнопки, эллипс и надпись были поставлены
вне элемента ViewBox, так как они должны отображаться на каждом слайде.

Ключевые привязки (если включена) на события: для кнопки Назад:


IsEnabled="{Binding CanGoBack, Source={StaticResource presentation}}"

Кнопка Вперёд:
IsEnabled="{Binding CanGoNext, Source={StaticResource presentation}}"

Модифицируем код самого главного (MainWindow.xaml) окна следующим


образом: для начала задаём окну имя Window:

Добавим новые пространства имён в начало файла MainWindow.xaml.cs:

using System.IO; // Для работы с классом File


using System.Windows.Threading; // Для создания таймеров

Объявим новые переменные после объявления классаокна:

public partial class MainWindow : Window


{
private Presentation NewPresentation; // Создаём экземпляр нашего класса
Presenation
private DispatcherTimer Timer1;
private DispatcherTimer Timer2;
private bool AutoClick;

Изменим код метода MainWindow():

public MainWindow()
{
InitializeComponent();
NewPresentation = (Presentation)this.FindResource("presentation");
AutoClick = false;

250
// После перезапуска приложения, стартует слайд, дата изменения которого
наиболее поздняя (дата изменения страницы XAML)
int IndexLastWritten = 0;
DateTime latestDateTimeWritten = DateTime.MinValue;

for (int i = 0; i < NewPresentation.Slides.Length; i++) // Проходим по общему


числу слайдов (через наш собственный класс и метод)
{
String Slide = NewPresentation.Slides[i];
DateTime DateLastWritten = File.GetLastWriteTime(@"..\..\" + Slide); //
Узнаём дату из каталога проекта по файлам слайдов

if (DateLastWritten.CompareTo(latestDateTimeWritten) > 0)
{
latestDateTimeWritten = DateLastWritten;
IndexLastWritten = i;
}
}
NewPresentation.CurrentIndex = IndexLastWritten;
//presentation.CurrentIndex = 0; // Если нам нужно, можем всегда стартовать с
первой страницы

// Создаём таймеры, которые обрабатывают исчезновение мышки после 5 секунд и


перелистывание слайда через 15 секунд
Timer1 = new DispatcherTimer();
Timer1.Interval = TimeSpan.FromSeconds(5);
Timer1.Tick += new EventHandler(Timer1_Tick);
Timer1.Start();

Timer2 = new DispatcherTimer();


Timer2.Interval = TimeSpan.FromSeconds(15);
Timer2.Tick += new EventHandler(Timer2_Tick);
}

Впишем события для таймеров. Первый таймер Timer1 скрывает курсор по


событию Tick, а второй Timer2 вызывает из класса Presentation метод GoNext():

private void Timer1_Tick(object sender, EventArgs e)


{
this.Cursor = Cursors.None; // Убираем курсор мышки
}

private void Timer2_Tick(object sender, EventArgs e)


{
NewPresentation.GoNext();
}

Метод для показа курсора мышки ShowCursor() после того, как её скрыли
(вызов метода находится в ином месте: в методе нажатия левой клавиши мышки).
После вызова, снова запускает Timer1 отвечающий за событие скрытия курсора.

private void ShowCursor()


{
Timer1.Start();
this.ClearValue(FrameworkElement.CursorProperty);
}

Теперь определим события окна Window. Первым станет событие


PrieviewMouseLeftButtonDown отвечающее за нажатие левой кнопки мышки в окне
формы:

251
private void Window_PreviewMouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
ShowCursor();
Point CurrentPoint = e.GetPosition(this);
TranslateTransform Transform = new TranslateTransform(CurrentPoint.X,
CurrentPoint.Y);
//this.ClickCanvas.RenderTransform = Transform;
}

Закомментированная строчка относится к ещё не определённой нами


функциональности (а именно к анимации «нажатия», о которой ниже). Пока что
поясним, почему выбрана группа методов PreviewMouse… вместо обычных методов
Mouse…. Разница в уровне восприятия события. В первом события
MouseLeftButtonDown отслеживаются нажатия только в самой форме, если же
нажатие было совершено в каком-либо элемента, событие не возникнет.

Событие PreviewMouseMove формы:

private void Window_PreviewMouseMove(object sender, MouseEventArgs e)


{
ShowCursor();
}

События Click всех кнопок:

private void Back_Click(object sender, RoutedEventArgs e)


{
NewPresentation.GoBack();
}

private void Next_Click(object sender, RoutedEventArgs e)


{
NewPresentation.GoNext();
}

private void Home_Click(object sender, RoutedEventArgs e)


{
NewPresentation.GoHome();
}

private void Auto_Click(object sender, RoutedEventArgs e)


{

if (AutoClick == false)
{
AutoClick = true;
NewPresentation.GoAuto = true;
Timer2.Start();
}
else
{
AutoClick = false;
NewPresentation.GoAuto = false;
Timer2.Stop();
}

if (!NewPresentation.GoAuto)
{
LAuto.Visibility = Visibility.Hidden;
EAuto.Visibility = Visibility.Hidden;

252
}
else
{
LAuto.Visibility = Visibility.Visible;
EAuto.Visibility = Visibility.Visible;
}
}

Кнопки Назад, Домой и Вперёд вызывают методы класса Presentation и работают


просто. Если есть слайд до текущего слайда, Назад отображается активной и её можно
нажать, будет выбран предыдущий слайд из списка, определяющего порядок показа.
Если нажата кнопка Домой, происходит вызов самого первого слайда. Нажатие кнопки
Вперёд (если активна), переводит отображение на следующий слайд.
Работа кнопки Авто заключается в следующем: проверяем состояние (была
нажата или нет). Если нажата, передаём параметр нажатия в класс Presentation,
активируем таймер или останавливаем, и затем показываем или скрываем элементы на
главной формы, отображающие состояние нажатия кнопки (эллипс и текст «А»).
Передачу параметра в свойство можно и не делать. Но лучше всё-таки передавать
ключевые параметры формы чтобы можно было получить их в любом месте
приложения.

Событие KeyDown (нажатие клавиши) окна формы. Ловим нажатия клваш


стрелок (лево, право) и Esc:

private void Window_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)


{
if (e.Key == Key.Left)
{
NewPresentation.GoBack();
e.Handled = true;
}
else if (e.Key == Key.Right)
{
NewPresentation.GoNext();
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
Application.Current.Shutdown();
e.Handled = true;
}
}

253
Рис. 3. 3. Модифицированная форма приложения

Компилируем, проверяем работоспособность. Попробуем изменить какой-либо


слайд и добавить на него элементов, а также изменить свойство Title для слайда (не
забываем что открывается тот слайд, который мы редактировали последним, полезно
когда в данный момент нам нужно посмотреть как выглядит слайд «в живую»; если не
хотим такую функциональность, выключаем как написано в комментариях к коду)

4. Модификация приложения Windows Foundation Presentation: работа с


решением в Expression Blend

Всё. Код теперь можно не менять, у нас есть готовый каркас для презентации.
Нам осталось только облагородить кнопки и добавить немного «красоты» в наш каркас.
Когда будет готово и это, можно приступать к заполнению приложения слайдами.
Процесс заполнения слайдов практически напоминает аналогичную работу в Microsoft
Office PowerPoint. Если создавать слайды в Visual Studio то проблем с заполнением не
возникнет. Элементов достаточно, возможностей много. Для добавления слайдов
используем добавление новых страниц (Page), а занесения их в порядок вывода,
добавляем полное имя XAML-файла в переменную slides класса Presentation.

Для получения по настоящему красивых вещей лучше использовать


предназначенные для этого инструменты. При создании полноценной презентации
необходимо знание английского хотя бы на базовом уровне для работы в Expression
Blend 4 (эта среда пока что не доступна на русском языке). Небольшой опыт работы в
этой мощнейшей дизайнерской среде конечно тоже необходим.

254
Важно отметить, что Expression Blend 4 поддерживает редактирование кода С# и
добавление новых страниц. Фактически это замена среды разработки Visual Studio (в
ней даже можно компилировать приложение).
Этого нам будет достаточно.

Откроем наше решение в Microsoft Expression Blend 4 (


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

Рис. 4. 1. Окно Microsoft Expression Blend 4 с запросом действия

Выбираем Open Project…: выберем наше решение (LWP09WPF02.csproj),


после чего оно будет загружено:

255
Рис. 4. 2. Загруженное решение презентации (вид на файл 02Slide.xaml)

Как видим, всё напоминает среды разработки Visual Studio. Аналог обозревателя
решений находится слева вверху, справа всю область занял местный аналог свойств
выбранного элемента. Слева внизу окно доступных в файле XAML объектов и «линия
времени» для анимации. В центре внизу, привычное окно вывода (со вкладками Errors
и Output). Вверху над окном представления (конструктора по сути) находятся вкладки
для переключения между выбранным файлов XAML.

Нажмём F5 (Project -> Run Project), увидим сообщения выводы в нижнем окне
И запустится наше приложение (если не будет ошибок).

Как добавить новый слайд (страницу)? Как и в среде разработки, парой


действий. Project -> Add New Item… (Ctrl+N).

256
Рис. 4. 3. Окно New Item

Добавим новую страницу (Page) с именем Name: 04Slide.xaml. И внесём ей в


код:

Рис. 4. 4. Редактируем код в Expression Blend

Расставим элементы в главном на слайде 01Title.xaml:

257
Рис. 4. 5. Расстановка элементов на первом слайде

«Рисовать» можно при помощи боковой панели слева. Иноки понятные. Если
нужно добавить элемент, которого нет изначально на боковой панели, можно поискать
в контекстном меню кнопки (нужно правой кнопкой мышки нажать на кнопку боковой
панели). Например, выбор инструмента рисования Pencil (Y):

Если элемента нет в контекстном меню кнопки, можно поискать его из


библиотеки доступных элементов. Нажимаем на боковой панели на символ «кавычек
вправо»:

258
Рис. 4. 6. Выбор элемента из библиотеки элементов

В данной работы для кнопок были «собраны» новые стили. Для создания стиля
на подобие элемента можно нажать правой кнопкой мыши объект формы в Objects and
Timeline -> Edit Template -> Create Empty…. Если нужно скопировать конкретный
стиль на основе выбранного элемента (например Button), жмём на Create Copy….

Окно, всплывающие при создании на основе кнопки Button:

Рис. 4. 7. Создание чистого стиля на основе кнопки

Далее даём стилю имя, после чего новый стиль должен появиться в окне
ресурсов справа (Resources). Выбираем тот файл, куда добавили ресурс, далее новый
стиль и жмём на Edit resource.

259
Далее размещаем в месте под элемент (чёрная граница) сетку Grid:

И после этого внутри этой сетки рисуем что угодно. Например такой шаблон
(Rectangle и Label):

Теперь, сохраним решение. Далее можно собрать события наведения курсора на


кнопку и событие нажатия. Для этого выделяем нужный элемент кнопки, для которой
хотим менять значения при «свершении» события и переходим на вкладку Triggres и
жмём на +Property:

260
Рис. 4. 8. Добавление Property для стиля кнопки, элемента Rectangle

Появилось новое свойство Activated when. Оно отвечает на вопрос: «куда,


когда и как» активировать событие. Выберем в Activated when: target-element (можно
выбрать и rectangle), после «точки» выберем IsMouseOver, а после равно впишем
True. В поле конструктора элемента мы видим надпись: «Событие = Параметр запись
триггера включена».

261
Рис. 4. 9. Запись триггера

В этом режиме меняем например цвет заполнения прямоугольника:

262
Рис. 4. 10. Записываем изменение цвета заполнения прямоугольника для события
наведения мышки на элемент

Добавим ещё одно Property и на этот раз запишем изменение цвета


прямоугольника для события IsPressed = True:

263
Рис. 4. 11. Записываем изменение цвета заполнения прямоугольника для события
нажатия на элемент

Всё. Наш элемент можно вставлять на форму. Для этого можно просто
перетащить его как новую кнопку из окна ресурсов справа или если был создан стиль
(Style), то из окна доступных стилей хранящихся в файлах решения (на рисунке ниже
отображены все доступные стили):

Для добавления нашей кнопки из она ресурсов справа, сначало надо вернуть на
форму. Для этого в окне Objects and Timiline ждмём на символ «стрелочки вверх»:

Далее размещаем наш новый элемент на форме перетаскиванием, либо можем


применить стиль к свеже созданной или уже существующей кнопки. Размещаем новую
кнопку (вкладка Assets -> Controls -> Button):

И применяем стиль к этой кнопке (выделяем кнопку в окне добавленных


элементов формы, далее жмём правую кнопку мыши, ищем Edit Template -> Apply
Resource -> ButtonControlTemplate1:

264
Рис. 4. 12. Применение стиля к новой кнопке

Если всё сделано правильно, кнопка будет корректно менять цвет при наведении
мышки и нажатии на эту кнопку. Обработчик события добавляется либо в среде
разработки (двойным нажатием), либо ручным редактированием кнопки в редакторе
XAML. Если редактор не вызван, выбираем любой элемент на любой форме, далее
нажимаем правую кнопку мышки на этом элементе и выбираем в раскрывающемся
списке View XAML.

Рис. 4. 13. Открытие редактора XAML

Редактор откроется под окном конструктора формы.

Expression Blend также предоставляет возможности создания анимации. Как уже


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

265
доступных элементов формы MainWindow.xaml, далее в строке с текстом (No
Storyboard open) нажмём на двойнуу стрелку вниз:

Рис. 4. 14. Доступная Storyboard для элемента ClickCanvas

Далее увидим открывшуюся полоску для управления анимацией и сам элемент в


конструкторе формы. Анимацию (кадры) можно посмотреть в окне элементов формы):

266
Рис. 4. 15. Выполнение анимационного элемента (текущий кадр указан жёлтой
полосой)

Единственное что можно сказать про анимацию в целом. Запись действия


происходит точно также как запись привязки Property (рисунок 4. 15, красная рамка).

Можем проверить это на простом примере. Выделим любую кнопку, которую


хотим «записать». Нажмём на «плюсик» правее от надписи (No Strotyboar open):

267
Откроется окно добавления новой «истории» для записи. Впишем имя
ButtonMove:

ОК. Перемещаем кнопку влево на несколько пикселей. Перемещаем жёлтую


линию «временной зоны». Снова перемещаем кнопку влево на несколько пикселей… И
так далее. У нас получится «дорожка» из положений для кнопки (можно и другие
элементы двигать или менять, в режими записиь всё будет записано):

Всё. Теперь раскомментируем код для анимации в файле MainWindows.xaml.cs (в


среде разработки Visual Studio или при помощи Expression Blend отредактируем файл
кода):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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 System.IO; // Для работы с классом File
using System.Windows.Threading; // Для создания таймеров

namespace LWP09WPF02
{
/// <summary>
/// Логика взаимодействия для MainWindow.xaml
/// </summary>
public partial class MainWindow : Window

268
{
private Presentation NewPresentation; // Создаём экземпляр нашего класса
Presenation
private DispatcherTimer Timer1;
private DispatcherTimer Timer2;
private bool AutoClick;

public MainWindow()
{
InitializeComponent();
NewPresentation = (Presentation)this.FindResource("presentation");
AutoClick = false;

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


наиболее поздняя (дата изменения страницы XAML)
int IndexLastWritten = 0;
DateTime latestDateTimeWritten = DateTime.MinValue;

for (int i = 0; i < NewPresentation.Slides.Length; i++) // Проходим по общему


числу слайдов (через наш собственный класс и метод)
{
String Slide = NewPresentation.Slides[i];
DateTime DateLastWritten = File.GetLastWriteTime(@"..\..\" + Slide); //
Узнаём дату из каталога проекта по файлам слайдов

if (DateLastWritten.CompareTo(latestDateTimeWritten) > 0)
{
latestDateTimeWritten = DateLastWritten;
IndexLastWritten = i;
}
}
NewPresentation.CurrentIndex = IndexLastWritten;
//presentation.CurrentIndex = 0; // Если нам нужно, можем всегда стартовать с
первой страницы

// Создаём таймеры, которые обрабатывают исчезновение мышки после 5 секунд и


перелистывание слайда через 15 секунд
Timer1 = new DispatcherTimer();
Timer1.Interval = TimeSpan.FromSeconds(5);
Timer1.Tick += new EventHandler(Timer1_Tick);
Timer1.Start();

Timer2 = new DispatcherTimer();


Timer2.Interval = TimeSpan.FromSeconds(15);
Timer2.Tick += new EventHandler(Timer2_Tick);
}

private void Timer1_Tick(object sender, EventArgs e)


{
this.Cursor = Cursors.None; // Убираем курсор мышки
}

private void Timer2_Tick(object sender, EventArgs e)


{
NewPresentation.GoNext();
}

private void Window_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)


{
if (e.Key == Key.Left)
{
NewPresentation.GoBack();
e.Handled = true;
}
else if (e.Key == Key.Right)

269
{
NewPresentation.GoNext();
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
Application.Current.Shutdown();
e.Handled = true;
}
}

private void ShowCursor()


{
Timer1.Start();
this.ClearValue(FrameworkElement.CursorProperty);
}

private void Window_PreviewMouseLeftButtonDown(object sender,


MouseButtonEventArgs e)
{
ShowCursor();
Point CurrentPoint = e.GetPosition(this);
TranslateTransform Transform = new TranslateTransform(CurrentPoint.X,
CurrentPoint.Y);
this.ClickCanvas.RenderTransform = Transform;
}

private void Window_PreviewMouseMove(object sender, MouseEventArgs e)


{
ShowCursor();
}

private void Back_Click(object sender, RoutedEventArgs e)


{
NewPresentation.GoBack();
}

private void Next_Click(object sender, RoutedEventArgs e)


{
NewPresentation.GoNext();
}

private void Home_Click(object sender, RoutedEventArgs e)


{
NewPresentation.GoHome();
}

private void Auto_Click(object sender, RoutedEventArgs e)


{

if (AutoClick == false)
{
AutoClick = true;
NewPresentation.GoAuto = true;
Timer2.Start();
}
else
{
AutoClick = false;
NewPresentation.GoAuto = false;
Timer2.Stop();
}

if (!NewPresentation.GoAuto)
{

270
LAuto.Visibility = Visibility.Hidden;
EAuto.Visibility = Visibility.Hidden;
}
else
{
LAuto.Visibility = Visibility.Visible;
EAuto.Visibility = Visibility.Visible;
}
}
}
}

Раскомментированная строка:

this.ClickCanvas.RenderTransform = Transform;

Обновим главным файл App.xaml для добавление ресурсов анимации (цветов


отображения):

<Application x:Class="LWP09WPF02.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>

<SolidColorBrush x:Key="MouseClickEffectColor" Color="#FFFFFFFF"/>


<SolidColorBrush x:Key="OrangeClick" Color="#FF41DE67"/>

</Application.Resources>
</Application>

И обновим код MainWindow.xaml для добавления всей описанной


функциональности, включая анимацию:

<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:LWP09WPF02="clr-namespace:LWP09WPF02"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
x:Class="LWP09WPF02.MainWindow"
Title="Презентация WPF (C#)" Height="600" Width="800" ResizeMode="NoResize"

WindowState="Maximized" WindowStyle="None" x:Name="Window"

KeyDown="Window_KeyDown" PreviewMouseMove="Window_PreviewMouseMove"

PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown">
<Window.Resources>
<LWP09WPF02:Presentation x:Key="presentation"/>

<Style x:Key="NextButtonStyle" BasedOn="{x:Null}" TargetType="{x:Type Button}">


<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Path Stretch="Fill" Stroke="Black" x:Name="path" Width="61.908"
Height="36.709" Data="M576.19695,27.913945 L576.08822,68.249537 C576.08822,68.249537
623.05612,58.293283 623.05612,58.293283 L621.74723,74.696597 647.68857,47.340314 621,20
623.05612,37.870199 C623.05612,37.870199 576.19695,27.913945 576.19695,27.913945 z"
Fill="#FFCC5252" OpacityMask="Black"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">

271
<Setter Property="Fill" TargetName="path"
Value="#4C176074"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="path"
Value="#FF0C303A"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Fill" TargetName="path"
Value="{x:Null}"/>
<Setter Property="Stroke" TargetName="path"
Value="#FF72A1AE"/>
<Setter Property="StrokeThickness" TargetName="path"
Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="AutoButtonStyle" BasedOn="{x:Null}" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Label x:Name="label" Content="Авто" Height="Auto"
Width="Auto" BorderBrush="{x:Null}" Background="#02FF0000" Foreground="#FFCC5252"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Stretch" FontWeight="Bold"
FontFamily="UkrainianBrushScript" FontSize="42.667" ToolTip="Автоматический показ
слайдов">
<Label.Effect>
<BlurEffect Radius="2"/>
</Label.Effect>
</Label>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground"
TargetName="label" Value="#4C176074"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground"
TargetName="label" Value="#FF0C303A"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground"
TargetName="label" Value="#FF72A1AE"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="HomeButtonStyle" BasedOn="{x:Null}" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Path Stretch="Fill" Stroke="Black" x:Name="path"
Width="44.15" Height="41" Data="M589.13955,33.208404 L598.54392,33.208406
C598.54392,33.208406 598.54392,69.969662 598.54392,69.969662 L630.46071,69.969662
630.46071,33.208404 639.86497,33.208404 614.37289,8.7009021 C614.37289,8.7009021
589.13955,33.208404 589.13955,33.208404 z" Fill="#FFCC5252" OpacityMask="Black"
VerticalAlignment="Stretch"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">

272
<Setter Property="Fill" TargetName="path"
Value="#4C176074"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="path"
Value="#FF0C303A"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Fill" TargetName="path"
Value="{x:Null}"/>
<Setter Property="Stroke"
TargetName="path" Value="#FF72A1AE"/>
<Setter Property="StrokeThickness"
TargetName="path" Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Storyboard x:Key="ClickAnimation">
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ClickCanvas" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static
Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="00:00:00.7000000">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="4.749"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[3].(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-5.623"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[3].(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-0.082"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle3"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].
(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="4.996"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle3"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="5.328"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle3"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-2.748"/>
</DoubleAnimationUsingKeyFrames>

273
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.7000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle4" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.7000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle1" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.7000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle2" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.7000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle3" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.7000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle4"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].
(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="4.685"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle4"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-3.261"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle4"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="4.463"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].
(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="5.573"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-3.388"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-5.964"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle2"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].
(ScaleTransform.ScaleX)">

274
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="5.555"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle2"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="2.621"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle2"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-6.309"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<ControlTemplate x:Key="ButtonControlTemplate1" TargetType="{x:Type Button}">
<Grid Height="29.188" Width="82.416">
<Rectangle x:Name="rectangle" Fill="#FFB85858"
Margin="22.958,0,27.458,0" Stroke="Black" StrokeThickness="3"/>
<TextBlock Margin="34.708,5.094,35.042,8" TextWrapping="Wrap"
Text="T" FontFamily="Poplar Std" FontSize="18.667"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter
Property="IsEnabled" TargetName="rectangle"
Value="True"/>
<Setter
Property="Fill" TargetName="rectangle"
Value="#4C176074"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="rectangle"
Value="#FF0C303A"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Storyboard x:Key="ButtonMove">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.Y)" Storyboard.TargetName="button">
<EasingDoubleKeyFrame KeyTime="0" Value="-4.413"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="-3.638"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.6" Value="-2.863"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.9" Value="-2.088"/>
<EasingDoubleKeyFrame KeyTime="0:0:1.2" Value="-1.313"/>
<EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="-0.538"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].
(TranslateTransform.X)" Storyboard.TargetName="button">
<EasingDoubleKeyFrame KeyTime="0" Value="5"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="-35"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.6" Value="-75"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.9" Value="-115"/>
<EasingDoubleKeyFrame KeyTime="0:0:1.2" Value="-150"/>
<EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="-198"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
<Window.Triggers>
<!-- Подключаем RoutedEvent и вытаскиваем событие нажатия левой кнопки мышки -->
<EventTrigger RoutedEvent="UIElement.PreviewMouseLeftButtonDown">
<BeginStoryboard Storyboard="{StaticResource ClickAnimation}"/>
</EventTrigger>

275
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource ButtonMove}"/>
</EventTrigger>
</Window.Triggers>

<Grid x:Name="LayoutRoot">
<Viewbox Margin="10,20,10,40" Stretch="Uniform">
<StackPanel>
<!-- Подставляем всегда: заголовок страницы -->
<TextBlock VerticalAlignment="Top" Height="84" FontFamily="Calibri"
FontSize="65" FontWeight="Bold" Text="{Binding Content.Title, ElementName=Frame,
Mode=Default}" TextAlignment="Center" TextWrapping="Wrap">
<TextBlock.Effect>
<BlurEffect/>
</TextBlock.Effect>
</TextBlock>
<!-- Подставляем всегда: содержимое слайда -->
<Frame Width="1000" Height="600" Source="{Binding CurrentSlide,
Source={StaticResource presentation}}" x:Name="Frame" NavigationUIVisibility="Hidden"
Background="{x:Null}" Focusable="False"/>
</StackPanel>
</Viewbox>

<!-- Кнопка Назад (перевёрнутая кнопка Вперёд) -->


<Button Style="{DynamicResource NextButtonStyle}" Width="63.684" Height="36.709"
Content="Button" Click="Back_Click" RenderTransformOrigin="0.5,0.5"
HorizontalAlignment="Left" Margin="10,0,0,10" VerticalAlignment="Bottom"
IsEnabled="{Binding CanGoBack, Source={StaticResource presentation}}" Cursor="Cross"
ToolTip="Назад">
<Button.Effect>
<DropShadowEffect Direction="490"/>
</Button.Effect>
<Button.RenderTransform>
<ScaleTransform ScaleX="-1" ScaleY="-1"/>
</Button.RenderTransform>
</Button>

<!-- Кнопка Вперёд -->


<Button HorizontalAlignment="Left" Margin="159,0,0,8" Style="{DynamicResource
NextButtonStyle}" VerticalAlignment="Bottom" Width="63.684" Height="36.709"
Content="Button" Click="Next_Click" IsEnabled="{Binding CanGoNext, Source={StaticResource
presentation}}" Cursor="Cross" ToolTip="Вперёд">
<Button.Effect>
<DropShadowEffect/>
</Button.Effect>
</Button>
<Button Content="Button" Style="{DynamicResource HomeButtonStyle}" Width="75"
HorizontalAlignment="Left" Margin="80,0,0,10" VerticalAlignment="Bottom" ToolTip="В
начало" Cursor="Cross" Click="Home_Click">
<Button.Effect>
<DropShadowEffect/>
</Button.Effect>
</Button>
<Button Content="Button" HorizontalAlignment="Right" Margin="0,0,0,-0.986"
Style="{DynamicResource AutoButtonStyle}" VerticalAlignment="Bottom" Width="107.378"
Cursor="Cross" Click="Auto_Click">
<Button.Effect>
<DropShadowEffect/>
</Button.Effect>
</Button>
<Button x:Name="button" Content="Button" HorizontalAlignment="Right"
Margin="0,0,141,11.587" VerticalAlignment="Bottom" Width="75" Template="{DynamicResource
ButtonControlTemplate1}" RenderTransformOrigin="0.5,0.5">
<Button.RenderTransform>
<TransformGroup>

276
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Button.RenderTransform>
</Button>
<Ellipse x:Name="EAuto" Fill="#FFE0C0C0" HorizontalAlignment="Right" Height="28"
Margin="0,0,122.316,12.775" Stroke="Black" VerticalAlignment="Bottom" Width="28"
Visibility="Hidden">
<Ellipse.Effect>
<DropShadowEffect ShadowDepth="3" RenderingBias="Quality"/>
</Ellipse.Effect>
</Ellipse>
<Label x:Name="LAuto" Content="A" HorizontalAlignment="Right" Height="30"
Margin="0,0,107.378,14.709" VerticalAlignment="Bottom" Width="40" Background="#00E07878"
FontFamily="Trajan Pro" FontSize="18.667" Foreground="#FFB82913" Visibility="Hidden">
<Label.Effect>
<BlurEffect Radius="2"/>
</Label.Effect>
</Label>

<Canvas x:Name="ClickCanvas" Visibility="Collapsed">


<Rectangle RenderTransformOrigin="0.5,0.5" x:Name="rectangle" Width="3"
Height="7" Fill="{DynamicResource OrangeClick}" Stroke="#FF000000" StrokeThickness="0"
Canvas.Left="-19" Canvas.Top="-3.75">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0.836"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Width="3" Height="7" Fill="{DynamicResource OrangeClick}"
Stroke="#FF000000" StrokeThickness="0" RenderTransformOrigin="0.5,0.5"
x:Name="rectangle4" Canvas.Left="-10" Canvas.Top="10.125">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="-53.842"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Width="3" Height="7" Fill="{DynamicResource OrangeClick}"
Stroke="#FF000000" StrokeThickness="0" RenderTransformOrigin="0.5,0.5"
x:Name="rectangle1" Canvas.Left="-10" Canvas.Top="-17.375">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="-119.602"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Width="3" Height="7" Fill="{DynamicResource OrangeClick}"
Stroke="#FF000000" StrokeThickness="0" RenderTransformOrigin="0.5,0.5"
x:Name="rectangle2" Canvas.Left="5.625" Canvas.Top="-17.375">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>

277
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="-67.444"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Width="3" Height="7" Fill="{DynamicResource OrangeClick}"
Stroke="#FF000000" StrokeThickness="0" RenderTransformOrigin="0.5,0.5"
x:Name="rectangle3" Canvas.Left="15.875" Canvas.Top="-4.875">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="-10.339"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
</Grid>
</Window>

Файл первого слайда 01Title.xaml сформировали таким образом:

<Page x:Class="LWP09WPF02._01Title"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="LeftToRight"
Width="1000" Height="600">

<Grid x:Name="LayoutRoot">

<Ellipse RenderTransformOrigin="0.5,0.5" Fill="#33628FB2" StrokeThickness="3"


Margin="-494,274.5,-148,-230.5">
<Ellipse.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="-9.455"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<Ellipse RenderTransformOrigin="0.5,0.5" Fill="#33628FB2" Stroke="{x:Null}"
StrokeThickness="3" Margin="-446.5,0,-401.5,-345.5" VerticalAlignment="Bottom"
Height="513.5">
<Ellipse.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="-23.748"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<Ellipse RenderTransformOrigin="0.5,0.5" Fill="#33628FB2" Stroke="{x:Null}"
StrokeThickness="3" Margin="-342,0,-598,-136.563" VerticalAlignment="Bottom"
Height="424">
<Ellipse.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="14.911"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="93.734"/>

278
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<TextBlock HorizontalAlignment="Center" Margin="0,123,0,0"
VerticalAlignment="Top" Height="113" FontSize="72" FontWeight="Bold" Text="Название
презентации" TextAlignment="Center" TextWrapping="Wrap" Foreground="#FF9B7979">
<TextBlock.Effect>
<DropShadowEffect/>
</TextBlock.Effect>
</TextBlock>
<TextBlock Margin="499,0,150.213,170" VerticalAlignment="Bottom" Height="65"
FontSize="36" Text="Ваше имя" TextAlignment="Right" TextWrapping="Wrap"/>
<TextBlock Margin="499,0,150.213,127" VerticalAlignment="Bottom" Height="63"
FontSize="36" Text="Другие данные" TextAlignment="Right" TextWrapping="Wrap"/>
<Path Fill="{x:Null}" Stretch="Fill" Stroke="#FF895D80" StrokeThickness="3"
HorizontalAlignment="Right" Margin="0,251.5,125.5,123.5" Width="311" Data="M875,253
L875,477 565,477">
<Path.Effect>
<BlurEffect/>
</Path.Effect>
</Path>
<Path Fill="{x:Null}" Stretch="Fill" Stroke="#FF895D80" StrokeThickness="3"
Width="309" Data="M872.98701,253 L872.98701,477 565,477" HorizontalAlignment="Right"
Margin="0,259.5,117.5,115.5">
<Path.Effect>
<BlurEffect/>
</Path.Effect>
</Path>
<Path Data="M80.5,237.5 L81.5,87.5 246.5,86.5" Fill="{x:Null}"
HorizontalAlignment="Left" Height="154" Margin="82.5,123,0,0" Stretch="Fill"
Stroke="#FF683F52" StrokeThickness="3" VerticalAlignment="Top" Width="169">
<Path.Effect>
<BlurEffect/>
</Path.Effect>
</Path>
<Path Data="M72.499038,227.50034 L73.653041,79.509324 263.49905,78.500323"
Fill="{x:Null}" HorizontalAlignment="Left" Height="152" Margin="73.5,113,0,0"
Stretch="Fill" Stroke="#FF683F52" StrokeThickness="3" VerticalAlignment="Top"
Width="194">
<Path.Effect>
<BlurEffect/>
</Path.Effect>
</Path>
</Grid>
</Page>

Готово. Приложение-презентацию можно запускать. Учитывая возможности,


которые открываются при применении WPF, альтернатива PowerPoint получилась
достойная.

5. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан


ниже (Рис. 5. 1):

279
Рис. 5. 1. Модифицированное приложение Windows Foundation Presentation (Зелёным
анимирован щелчок по движущейся кнопки, включена автопрокрутка, отображается
первый слайд 01Title.xaml)

Рис. 5. 2. Модифицированное приложение Windows Foundation Presentation (слайд


02Slide.xaml)

6. О приложении к Лабораторной работе № 9

Получившуюся программу (LWP09WPF02.exe), собранную из кусков кода


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

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).

280
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

10. Лабораторная работа № 10: Работа с базами данных — XML и OLE

Лабораторная работа № 10: Работа с базами данных — XML и OLE

Содержание

1. Вводная часть
2. Создание приложения Windows Forms
3. Модификация приложения Windows Forms: ODBC
4. Модификация приложения Windows Forms: OLE
5. Модификация приложения Windows Forms: XML
6. Завершающая часть
7. О приложении к Лабораторной работе № 10

1. Вводная часть

В этой работе будет рассмотрена работа с базами данных. А именно с


текстовыми файлами *.xml и языком расширяемой разметки (от англ. eXtensible
Markup Language – расширяемый язык разметки) XML, а также ADO.NET (ActiveX
Data Objects .NET) и один из провайдеров данных ADO.NET: в частности OLE
(Object Linking and Embedding) и ODBC (Open Database Connectivity).
По данной тематике (работе с базами данных в C#) на данный момент
существует достаточное количество материалов, как в печатном издании, так и в сети
Интернет (различных статей и курсов). В данной работе будут рассмотрены лишь
основные обобщённые моменты работы с базами данных (через ODBC, OLE и XML).

Что такое ADO.NET?

ActiveX Data Objects .NET является набором классов, реализующих программные


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

ADO.NET широко используется совместно с технологией web-программирования с


использованием объектов ASP.NET для доступа к расположенным на сервере базам
данных со стороны клиента.
В ADO.NET используется доступ к отсоединенным данным. При этом соединение
устанавливается лишь на то время, которое необходимо для проведения определенной
операции над базой данных.
Поставщик данных для приложения (Provider) – объект, предназначенный для
обеспечения взаимодействия приложения с хранилищем информации (базами данных).
Естественно, приложению нет никакого дела до того, где хранится и как извлекается
потребляемая приложением информация. Для приложения источником данных является
тот, кто передает данные приложению. И как сам этот источник эту информацию
добывает – никого не касается.

281
Источник данных (Data Provider) – это набор взаимосвязанных компонентов,
обеспечивающих доступ к данным. Функциональность и само существование
провайдера обеспечивается набором классов, специально для этой цели
разработанных.
ADO.NET поддерживает следующие типа источников данных:

Имя API-
Описание источника данных
провайдера префикс

ODBC Data
Odbc Источники данных с ODBC-интерфейсом. Устаревший провайдер.
Provider

OleDb Data
OleDb Источники данных с OleDb-интерфейсом, для Access или Excel.
Provider

Oracle Data
Oracle Для баз данных Oracle.
Provider

SQL Data
Sql Для работы с Microsoft SQL Server.
Provider

Borland Data Общий доступ к множеству баз данных, таких как Interbase, SQL
Bdp
Provider Server, IBM DB2, и Oracle.

Нас пока интересует только ODBC и собственно OLE.

Что такое ODBC?

ODBC — это программный интерфейс (API) доступа к базам данных,


разработанный фирмой Microsoft, в сотрудничестве с Simba Technologies на основе
спецификаций Call Level Interface (CLI), который разрабатывался организациями SQL
Access Group, X/Open и Microsoft. Впоследствии CLI был стандартизован ISO ISO/IEC
9075-3:2003. Стандарт CLI призван унифицировать программное взаимодействие с
СУБД, сделать его независимым от поставщика СУБД и программно-аппаратной
платформы.

В начале 1990 г. существовало несколько поставщиков баз данных, каждый из


которых имел собственный интерфейс. Если приложению было необходимо общаться с
несколькими источниками данных, для взаимодействия с каждой из баз данных было
необходимо написать свой код. Для решения возникшей проблемы Microsoft и ряд
других компаний создали стандартный интерфейс для получения и отправки
источникам данных различных типов. Этот интерфейс был назван Open Database
Connectivity, или открытый механизм взаимодействия с базами данных.

C помощью ODBC прикладные программисты могли разрабатывать приложения


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

MFC (Microsoft Foundation Classes, библиотека на языке С++, Microsoft)


усовершенствовала ODBC для разработчиков приложений. Истинный интерфейс ODBC
является обычным процедурным API. Вместо создания простой оболочки процедурного

282
API разработчики MFC создали набор абстрактных классов, представляющих
логические сущности в базе данных.

Что такое OLE?

OLE (произносится как oh-lay [олэй]) — технология связывания и внедрения


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

В 1996 году Microsoft переименовала технологию в ActiveX.

OLE позволяет передавать часть работы от одной программы редактирования к


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

Основное преимущество использования OLE (кроме уменьшения размера файла)


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

OLE используется при обработке составных документов (compound


documents), может быть использована при передаче данных между различными
несвязанными между собой системами посредством интерфейса переноса (drag-and-
drop), а также при выполнении операций с буфером обмена. Идея внедрения широко
используется при работе с мультимедийным содержанием на веб-страницах (пример —
Веб-ТВ), где используется передача изображения, звука, видео, анимации в страницах
HTML (язык гипертекстовой разметки) либо в других файлах, также использующих
текстовую разметку (например, XML и SGML). Однако, технология OLE использует
архитектуру «толстого клиента», то есть сетевой ПК с избыточными вычислительными
ресурсами. Это означает, что тип файла либо программа, которую пытаются внедрить,
должна присутствовать на машине клиента. Например, если OLE оперирует таблицами
Microsoft Excel, то программа Excel должна быть инсталлирована на машине
пользователя.

Что такое XML?

XML (произносится [экс-эм-э́л]) — рекомендованный Консорциумом


Всемирной паутины (W3C) язык разметки, фактически представляющий собой свод
общих синтаксических правил. XML — текстовый формат, предназначенный для
хранения структурированных данных (взамен существующих файлов баз данных), для
обмена информацией между программами, а также для создания на его основе более
специализированных языков разметки (например, XHTML). XML является упрощённым
подмножеством языка SGML. Подробнее об XML будет рассказано ниже.

Что будет представлять собой приложение, разрабатываемое в данной работе?

Приложение будет представлять собой окно Windows Forms, поделённое на


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

2. Создание приложения Windows Forms

283
Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

284
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP10DB — это название программы (выбрано по названию


лабораторного практикума, номеру и названию работы). В поле Расположение
указана конечная директория, где будет находиться весь проект. Выберем
расположение удобное для быстрого поиска. В поле Имя решения вводится либо
название программы «по умолчанию» из поля Имя автоматически, либо можно ввести
своё собственное. Под этим именем будет создана конечная папка проекта (если Имя и
Имя решения разные).

285
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

286
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

287
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms: ODBC

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP10Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Работа с базами
данных (C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 800;
600
^ Поменяем размер формы.

288
ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,
необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения

Добавим на нашу форму ToolTip ( ).

Параметры добавленного элемента всплывающей подсказки таковы:

(Name): Hint

Теперь приступим к формированию первой группы элементов и её


функциональности. Для начала расставим элементы как показано на рисунке ниже:

289
Рис. 3. 2. Расстановка элементов первой группы (ODBC)

Здесь у нас четыре кнопки Button, один RichTextBox (слева внизу), и простой
погашенный TextBox.

По порядку распишем Свойства каждого элемента:

Button:
(Name): B_ODBC_Search
Text: Выбрать базу данных (*.mdb)
Size: 200; 23
Button:
(Name): B_ODBC_Connect
Text: Открыть соединение
Size: 200; 23
Button:
(Name): B_ODBC_Add
Text: Добавить запись
Size: 200; 23
Button:
(Name): B_ODBC_Disconnect
Text: Закрыть соединение
Size: 200; 23
TextBox:
(Name): TB_ODBC_Path
ReadOnly: True
RicTextBox:
(Name): RTB_ODBC
ReadOnly: True
GroupBox:
(Name): GB_ODBC
Text: ODBC
OpenFileDialog:
(Name): OFD_ODBC
FileName: LWP10-DB-ODBC
InitialDirectory: D:\
Filter База данных *.mdb|*.mdb

Теперь отправляемся в код формы (правая кнопка мыши на значке формы, далее
Перейти к коду или нажмём на клавишу F7):

290
В самое начало кода добавим:

using System.Data.Odbc; // ODBC

Найдём:

public partial class LWP10Main : Form


{

Добавим после:

Double Counter = 4;
OdbcConnection ConnectionOBDC;

Изменим следующую функцию LWP10Main():

public LWP10Main()
{
InitializeComponent();
B_ODBC_Add.Enabled = false;
B_ODBC_Connect.Enabled = false;
B_ODBC_Disconnect.Enabled = false;
TB_ODBC_Path.Text = "D:\\LWP10-DB-ODBC.mdb";
}

Событие Click кнопки B_ODBC_Search («Выбрать базу данных»):

private void B_ODBC_Search_Click(object sender, EventArgs e)


{

if (OFD_ODBC.ShowDialog() == DialogResult.OK)
{
B_ODBC_Add.Enabled = true;
B_ODBC_Connect.Enabled = true;
B_ODBC_Disconnect.Enabled = true;
Directory.CreateDirectory(Path.GetDirectoryName(OFD_ODBC.FileName) + @"\
Копии"); // Создаём директорию под изменённые БД
File.Copy(OFD_ODBC.FileName, Path.GetDirectoryName(OFD_ODBC.FileName) +
@"\Копии\" + OFD_ODBC.SafeFileName, true); // Копируем туда выбранную БД (перезаписываем,
в случае обнаружения похожего файла)

291
if (Path.GetDirectoryName(OFD_ODBC.FileName) ==
Directory.GetDirectoryRoot(OFD_ODBC.FileName)) // Проверяем путь, если находимся в
корневой директории диска, режем один слеш
TB_ODBC_Path.Text = Path.GetDirectoryName(OFD_ODBC.FileName) +
@"Копии\" + OFD_ODBC.SafeFileName;
else
TB_ODBC_Path.Text = Path.GetDirectoryName(OFD_ODBC.FileName) + @"\
Копии\" + OFD_ODBC.SafeFileName;
}
}

Небольшое замечание по коду выше. После выбора БД в окне диалога,


приложение будет создавать в директории с БД новую папку «Копии» и копировать
туда выбранную в диалоге БД. Все операции с БД (из нашего приложения) будут
происходить с копией, а не с оригиналом. Копия БД будет затираться всякий раз при
открытии диалога выбора и выбора там БД.

Событие Click кнопки B_ODBC_Connect («Открыть соединение»):

private void B_ODBC_Connect_Click(object sender, EventArgs e)


{
String ConnetionStringODBC = null;
ConnetionStringODBC = "Driver={Microsoft Access Driver (*.mdb)}; DBQ=" +
TB_ODBC_Path.Text + ";"; // Выбираем источник данных ("провайдера") и указываем путь к
нему через TextBox
ConnectionOBDC = new OdbcConnection(ConnetionStringODBC); // Инициализируем
объект соединения с новыми параметрами

try
{
ConnectionOBDC.Open(); // Открываем соединение
MessageBox.Show("Соединение с базой данных " + TB_ODBC_Path.Text + "
успешно открыто!", "Работа с базами данных (C#) :: ODBC");
}
catch (Exception ex)
{
MessageBox.Show("Невозможно открыть соединение с базой данных " +
TB_ODBC_Path.Text + " (" + ex.Message + ")!", "Работа с базами данных (C#) :: ODBC");
}
}

Событие Click кнопки B_ODBC_Add («Добавить запись»):

private void B_ODBC_Add_Click(object sender, EventArgs e)


{
String SQL_ODBC = "INSERT INTO \"Главная таблица\" VALUES( '" + Counter++ +
"', 'Число', '999', 'ABC' );"; // Запрос на на добавление записей в нашу таблицу,
ключевое поле будет числовым, начинается с 4 и далее растёт инкрементом
OdbcCommand Command = new OdbcCommand(SQL_ODBC, ConnectionOBDC); // Формируем
команду

try
{
Command.ExecuteNonQuery(); // Выполняем команду
RTB_ODBC.Clear(); // Очищаем RichTextBox
RTB_ODBC.AppendText(Command.CommandText); // Вставляем результат
выполнения команды с нашей базой
}
catch (Exception ex)
{
RTB_ODBC.Clear();
RTB_ODBC.AppendText(ex.Message);

292
}
}

Событие Click кнопки B_ODBC_Disconnect («Закрыть соединение»):

private void B_ODBC_Disconnect_Click(object sender, EventArgs e)


{
String SQL_ODBC = "DELETE FROM \"Главная таблица\" WHERE \"Главная
таблица\".\"Первое поле\" = 'Число';"; // Запрос на удаление всего добавленного (чтобы не
делать это вручную потом)
OdbcCommand Command = new OdbcCommand(SQL_ODBC, ConnectionOBDC); // Формируем
команду

try
{
Command.ExecuteNonQuery(); // Выполняем команду
RTB_ODBC.Clear(); // Очищаем RichTextBox
RTB_ODBC.AppendText(Command.CommandText); // Вставляем результат
выполнения команды с нашей базой
ConnectionOBDC.Close(); // Закрываем соединение
MessageBox.Show("Соединение с базой данных " + TB_ODBC_Path.Text + "
успешно закрыто!", "Работа с базами данных (C#) :: ODBC");
}
catch (Exception ex)
{
RTB_ODBC.Clear();
RTB_ODBC.AppendText(ex.Message);
MessageBox.Show("Невозможно закрыть соединение с базой данных " +
TB_ODBC_Path.Text + " (" + ex.Message + ")!", "Работа с базами данных (C#) :: ODBC");
}
}

Последнее что нам нужно, это база данных в формате Microsoft Access 2000.
Сделаем её, например в Microsoft Office Access 2010. База будет содержать одну
таблицу (Главная таблица) и четыре столбца: Ключевое поле (являющее
ключевым, числовое), : Первое поле, Второе поле и Третье поле (все текстовые).
Заполним первые три записи (Ключевое поле: 1, 2 и 3):

Рис. 3. 3. База данных LWP10-DB-ODBC.mdb

Для сохранения в формат Access 2000 выполним: Файл -> Сохранить и


опубликовать, далее выберем формат Базы данных Access 2000:

293
Рис. 3. 4. Сохранение в формате Access 2000

Компилируем приложение (Debug) и запускаем. Выбираем нашу базу кнопкой в


левом верхнем углу приложения, затем жмём на Открыть соединение, далее
несколько раз на Добавить запись (при этом наблюдая за тем что пишет RichTextBox).
Если после нескольких добавлений открыть базу данных (не нажимая на Закрыть
соединение или не закрывая само приложение), то можно увидеть новые записи:

Удаляем все новые записи кнопкой Закрыть соединение и тем самым также
закрываем соединение с базой.

294
Рис. 3. 5. Окончательная работа блока: ODBC

4. Модификация приложения Windows Forms: OLE

Для соединения с базой данных Microsoft Office Access 2003 и ниже (файл
*.mdb) в C# следует использовать класс OleDbConnection со следующими
параметрами соединения:
Provider=Microsoft.Jet.OLEDB.4.0; Data Source=DataBaseFile
Здесь DataBaseFile — абсолютный путь к файлу базы данных Access.
«Провайдер» соединения должен иметь значение Microsoft.Jet.OLEDB.4.0.
Для соединения с базой данных Microsoft Office Access 2007 и выше (файл
*.accdb) в C# следует использовать класс OleDbConnection со следующими
параметрами соединения:
Provider=Microsoft.ACE.OLEDB.12.0; Data Source=DataBaseFile
Здесь DataBaseFile — абсолютный путь к файлу базы данных Access.
«Провайдер» соединения должен иметь значение Microsoft.ACE.OLEDB.12.0 либо для
версии Access 2010: Microsoft.ACE.OLEDB.14.0.

Пример:

String ConnetionString = null;


ConnetionStringO = "Provider=Microsoft.ACE.OLEDB.12.0;" +
@"Data Source=D:\Database.accdb";
ConnectionOLE1 = new OleDbConnection(ConnetionStringOLE1);

Для отправки SQL-запросов и чтения их результатов используются объекты


OleDbCommand и OleDbDataReader.

295
Для выполнения запросов на вставку, изменение, или удаление данных из базы
данных следует использовать метод класса OleDbCommand: ExecuteNonQuery(). Его
вызов выполняет указанный в свойстве CommandText класса OleDbCommand запроc и
возвращает int-число затронутых запросом полей.

Пример:

OleDbConnection Conn = new OleDbConnection(@"Provider=Microsoft.ACE.OLEDB.12.0;


Data Source=Database.mdb"); // Инициализируем параметры соединения
Conn.Open(); // Открываем соединение с базой даннфх
OleDbCommand Cmd = new OleDbCommand(); // Создаём команду
Cmd.CommandText = "INSERT INTO Main_Table VALUES (1,2,3)"; // Текст команды
int rowsAffected = Cmd.ExecuteNonQuery(); // Выполнение команды с возвратом в int
System.Windows.Forms.MessageBox.Show(rowsAffected.ToString()); // Вывод результата

Если база данных из примеры выше пустая, то int будет равно единице.

Во время выполнения команд вставки (SQL: INSERT INTO) можно использовать


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

Пример:

OleDbCommand Сmd = new OleDbCommand("INSERT INTO Main_Table (columns) VALUES


(@param)", Сonn);
Сmd.Parameters.Add("@param", "abc[[''[]''kl'm");
Сmd.ExecuteNonQuery();

При каких-либо ошибках в синтаксисе, или структуре SQL-запроса, программа


падает в OS Loader Lock (фактически прерывается работа), и не дает никакой
информации о произошедшей ошибке. Чтобы этого избежать и получить довольно
исчерпывающую информацию о произошедшей ошибке, следует выполнять запросы в
try-catch блоках. В коде работы подобные приёмы будут использованы повсеместно.

Теперь приступим к формированию второй группы элементов и её


функциональности. Для начала расставим элементы как показано на рисунке ниже:

Рис. 4. 1. Расстановка элементов первой группы (OLE # 1)

Здесь представлены три кнопки Button, один ListBox (по центру), простой
погашенный TextBox, как в предыдущей группе, и четыре TextBox’а для ввода с
клавиатуры данных, которые будут добавлять в базу данных. Слева от каждого
размещены по одному текстовому элементу: Label. Также с панели инструментов было
добавлен ещё один OpenFileDialog-элемент.

Функциональность блока будет следующей. При нажатии на кнопку Выбрать


базу данных, будет предложено выбрать одну из двух возможных типов базы: это

296
база данных для старых версий Access (*.mdb) и для новых (*.accdb). После этого
будет выдано сообщение об успехе или неудаче выбора с описанием причины ошибки в
случае неудачи. Далее при нажатии Прочитать все записи, в центральный ListBox
будут занесены все записи из базы данных. Ввод значений в TextBox’ы слева и нажатие
кнопки Добавить записи сохранит в нашей базе новый записи. Будет использована та
же самая база данных, что и для предыдущего блока: LWP10-DB-ODBC.mdb, а также
копия этой же базы, сохранённая как файл *.accdb, для версии Access 2007 или Access
2010. База: LWP10-DB-OLE.accdb.

По порядку распишем Свойства каждого элемента:

Button:
(Name): B_OLE_1_Search
Text: Выбрать базу данных
Size: 200; 23
Button:
(Name): B_OLE_1_Read
Text: Прочитать все записи
Size: 200; 23
Button:
(Name): B_OLE_1_Add
Text: Добавить записи
Size: 200; 23
TextBox:
(Name): TB_OLE_1_Path
ReadOnly: True
ListBox:
(Name): LB_OLE_1
GroupBox:
(Name): GB_OLE_1
Text: OLE # 1
OpenFileDialog:
(Name): OFD_OLE_1
InitialDirectory: D:\
Filter База данных *.mdb|*.mdb|База данных
*.accdb|*.accdb
TextBox:
(Name): TB_OLE_1_1
TextBox:
(Name): TB_OLE_1_2
TextBox:
(Name): TB_OLE_1_3
TextBox:
(Name): TB_OLE_1_4

Откроем файл LWP10Main.cs и в самом начале добавим две ссылки:

using System.Data.OleDb; // OLE


using System.IO; // Для получения расширения файла базы данных

Найдём:

297
public partial class LWP10Main : Form
{
Double Counter = 4;
OdbcConnection ConnectionOBDC;

Добавим после:

String ConnetionStringOLE1 = null; // Переменная для сохранения данных соединения


OleDbConnection ConnectionOLE1; // Объект для открытия подключения к базе данных
String SQL_OLE = null; // Переменная для поискового запроса
String SQL_OLE_ADD = null; // Переменная для добавления данных

Найдём:

public LWP10Main()
{
InitializeComponent();
TB_ODBC_Path.Text = "D:\\LWP10-DB-ODBC.mdb";

Добавим после:

TB_OLE_1_Path.Text = "D:\\LWP10-DB-OLE.accdb";
B_OLE_1_Read.Enabled = false;
B_OLE_1_Add.Enabled = false;
TB_OLE_1_1.Text = Counter.ToString();
SQL_OLE = "SELECT * FROM [Главная таблица]";

Событие Click кнопки Выбрать базу данных:

private void B_OLE_1_Search_Click(object sender, EventArgs e)


{

if (OFD_OLE_1.ShowDialog() == DialogResult.OK)
{
B_OLE_1_Read.Enabled = true; // Активируем кнопку "Прочитать все записи"
B_OLE_1_Add.Enabled = true;
//TB_OLE_1_Path.Text = OFD_OLE_1.FileName;
Directory.CreateDirectory(Path.GetDirectoryName(OFD_OLE_1.FileName) + @"\
Копии"); // Создаём директорию под изменённые БД
File.Copy(OFD_OLE_1.FileName, Path.GetDirectoryName(OFD_OLE_1.FileName) +
@"\Копии\" + OFD_OLE_1.SafeFileName, true); // Копируем туда выбранную БД
(перезаписываем, в случае обнаружения похожего файла)
if (Path.GetDirectoryName(OFD_OLE_1.FileName) ==
Directory.GetDirectoryRoot(OFD_OLE_1.FileName)) // Проверяем путь, если находимся в
корневой директории диска, режем один слеш
TB_OLE_1_Path.Text = Path.GetDirectoryName(OFD_OLE_1.FileName) +
@"Копии\" + OFD_OLE_1.SafeFileName;
else
TB_OLE_1_Path.Text = Path.GetDirectoryName(OFD_OLE_1.FileName) + @"\
Копии\" + OFD_OLE_1.SafeFileName;

if (Path.GetExtension(OFD_OLE_1.FileName) == ".mdb") // Узнаём расширение


файла выбранного в диалоге открытия (указываем полный путь) и сравниваем его с ".mdb"
{
ConnetionStringOLE1 = "Provider=Microsoft.Jet.OLEDB.4.0;" +
@"Data Source=" + TB_OLE_1_Path.Text + "";
MessageBox.Show("Выбрана база данных " + TB_OLE_1_Path.Text + "
формата: *" + Path.GetExtension(TB_OLE_1_Path.Text) + "!", "Работа с базами данных
(C#) :: OLE # 1");
}

298
if (Path.GetExtension(OFD_OLE_1.FileName) == ".accdb")
{
ConnetionStringOLE1 = "Provider=Microsoft.ACE.OLEDB.12.0;" +
@"Data Source=" + TB_OLE_1_Path.Text + "";
MessageBox.Show("Выбрана база данных " + TB_OLE_1_Path.Text + "
формата: *" + Path.GetExtension(TB_OLE_1_Path.Text) + "!", "Работа с базами данных
(C#) :: OLE # 1");
}
}
}

Событие Click кнопки Показать все записи:

private void B_OLE_1_Read_Click(object sender, EventArgs e)


{
LB_OLE_1.Items.Clear();
ConnectionOLE1 = new OleDbConnection(ConnetionStringOLE1);

try
{
ConnectionOLE1.Open(); // Открываем соединение
MessageBox.Show("Соединение с базой данных " + TB_OLE_1_Path.Text + "
успешно открыто!", "Работа с базами данных (C#) :: OLE # 1");
}
catch (Exception ex) // Ловим исключение и вытаскиваем ошибку через
ex.Message
{
MessageBox.Show("Невозможно открыть соединение с базой данных " +
TB_OLE_1_Path.Text + " (" + ex.Message + ")!", "Работа с базами данных (C#) :: OLE # 1");
}
OleDbCommand Command = new OleDbCommand(SQL_OLE, ConnectionOLE1); //
Формируем SQL-команду для текущего подключения
OleDbDataReader DataReader = Command.ExecuteReader(); // Формируем объект для
чтения данных из базы данных
LB_OLE_1.Items.Add(Command.CommandText); // Посылаем текст команды в ListBox
// Организуем циклический перебор полученных записей
while (DataReader.Read())
{
LB_OLE_1.Items.Add(DataReader["Ключевое поле"].ToString() + " | " +
DataReader["Первое поле"].ToString() + " | " + DataReader["Второе поле"].ToString() + " |
" + DataReader["Третье поле"].ToString());
}
// Закрываем потоки чтения и соединения
DataReader.Close();
ConnectionOLE1.Close();
}

Событие Click кнопки Добавить записи:

private void B_OLE_1_Add_Click(object sender, EventArgs e)


{
LB_OLE_1.Items.Clear(); // Очищаем ListBox перед использованием
ConnectionOLE1 = new OleDbConnection(ConnetionStringOLE1); // Передаём
параметры объекту соединения

try
{
ConnectionOLE1.Open(); // Открываем соединение
MessageBox.Show("Соединение с базой данных " + TB_OLE_1_Path.Text + "
успешно открыто!", "Работа с базами данных (C#) :: OLE # 1");
}
catch (Exception ex)
{

299
MessageBox.Show("Невозможно открыть соединение с базой данных " +
TB_OLE_1_Path.Text + " (" + ex.Message + ")!", "Работа с базами данных (C#) :: OLE # 1");
}
SQL_OLE_ADD = "INSERT INTO [Главная таблица] VALUES('" + Counter.ToString() +
"', '" + this.TB_OLE_1_2.Text + "', '" + this.TB_OLE_1_3.Text + "', '" +
this.TB_OLE_1_4.Text + "');";
OleDbCommand Command = new OleDbCommand(SQL_OLE_ADD, ConnectionOLE1);

try
{
Command.ExecuteNonQuery(); // Выполняем команду
}
catch (Exception ex)
{
MessageBox.Show("Невозможно выполнить команду с базой данных " +
TB_OLE_1_Path.Text + " (" + ex.Message + ")!", "Работа с базами данных (C#) :: OLE # 1");
}
LB_OLE_1.Items.Add(Command.CommandText); // Отправляем текст команды в
ListBox
Counter++;
TB_OLE_1_1.Text = Counter.ToString();
}

Компилируем приложение (Debug) и запускаем:

Рис. 4. 2. Окончательная работа блока: OLE # 1

Предположим, что работа в таком виде с базами данных нас не устраивает.


Например, весьма неудобно для базы, где в таблице есть более сотни столбцов, делать
эту самую сотню TextBox’ов. В этом случае, когда требуется наглядность данных в

300
приложении и малые трудозатраты лучше нужно использовать элемент управления
DataGridView и связанные с ним другие элементы, такие как например DataSet.

Оба элемента можно найти на панели элементов на вкладе Данные.

Собственно DataGridView полезен не только для работы с базами данных. Это


просто элемент для предоставления данных в программе (в том числе и в визуальной
форме).
Для демонстрации работы с этими элементами организуем третью группу
элементов: OLE # 2. Приложение же будет выполнять тот же запрос, что и в
предыдущем примере, но будет помещать результат в объект DataSet, который
подключается к элементу управления DataGridView, а тот автоматически отображает
все данные. Для сохранения данных в базе будет использоваться метод
«двухсторонней привязки3 данных», который позволит не только просматривать
открывшуюся таблицу, но и вводить изменения в элемент DataGridView, добавляя
новые строки, изменяя содержимое имеющихся строк и удаляя ненужные. Объекты
класса DataAdapter способны выполнять как операцию SELECT, получая данные
запроса из базы данных, так и команды INSERT, UPDATE и DELETE, изменяя
содержимое таблицы базы данных.

Но прописывать данные команды самостоятельно надобности нет. Пространство


имен System.Data содержит небольшой, но очень полезный класс CommandBuilder,
который умеет создавать команды SQL и автоматически их выполнять.

ПРИМЕЧАНИЕ № 3: Подключение источника данных к визуальному элементу


управления называется привязкой, или связыванием данных.

Теперь приступим к формированию третьей группы элементов и её


функциональности. Для начала расставим элементы как показано на рисунке ниже:

Рис. 4. 3. Расстановка элементов первой группы (OLE # 2)

301
Здесь представлены три кнопки Button, один, простой погашенный TextBox, как
в предыдущей группе и два DataGridView. Также с панели инструментов было добавлен
ещё один OpenFileDialog-элемент и один DataSet. Сразу оговоримся, для работы с
добавлением данных через DataGridView база данных была переделана и название
таблиц и столбцов были изменены (убраны символы кириллицы). Это связано с тем, что
при работе с базой, в которой есть не английские символы в названиях таблиц (и
столбцов) можно столкнуться с ошибками (не всегда). Сама же база осталась без
изменений, Название: LWP10-DB-OLE-Special.accdb. Содержание таблицы
Main_Table:

Функциональность блока будет следующей. Выбираем базу данных как в


предыдущих случаях и жмём на Показать все записи. Всплывающее сообщение с
результатами, за которым идёт добавление всех данных базы в левый (по рисунку 4. 3)
DataGridView. Правый же будет загружен сразу при старте приложения (база будет
взята по статическому пути D:\LWP10-DB-Special.accdb. Вносим изменения в ячейки
(в правом DataGridViewv) и жмём на Сохранить записи. Результат можно будет
увидеть перезагрузив приложение, либо открыв базу через Access. Изменения ячеек в
левом DataGridView не будут нигде сохранены.

По порядку распишем Свойства каждого элемента:

Button:
(Name): B_OLE_2_Search
Text: Выбрать базу данных
Size: 200; 23
Button:
(Name): B_OLE_2_Read
Text: Прочитать все записи
Size: 200; 23
Button:
(Name): B_OLE_2_Save
Text: Сохранить записи
Size: 200; 23
TextBox:
(Name): TB_OLE_2_Path
ReadOnly: True
GroupBox:
(Name): GB_OLE_2
Text: OLE # 2
OpenFileDialog:
(Name): OFD_OLE_2
InitialDirectory: D:\
Filter База данных *.mdb|*.mdb|База данных

302
*.accdb|*.accdb
DataGridView:
(Name): DataGridViewOLE
DataGridView:
(Name): DataGridViewOLE_S
DataSetw:
(Name): DataSetOLE

Теперь перейдём к коду. Найдём:

String SQL_OLE_ADD = null; // Переменная для добавления данных

Добавим после:

String ConnetionStringOLE2 = null;


OleDbConnection ConnectionOLE2;
String ConnetionStringOLE_S = null;
OleDbConnection ConnectionOLE_S;
OleDbDataAdapter DataAdapter_S;

Найдём:

SQL_OLE = "SELECT * FROM [Главная таблица]";

Добавим после:

TB_OLE_2_Path.Text = "D:\\LWP10-DB-OLE.accdb";
B_OLE_2_Read.Enabled = false;
DataGridViewOLE.DataMember = "Table"; // Указываем на тип подсписка для
DataGridView

Событие Click кнопка Выбор базы данных:


private void B_OLE_2_Search_Click(object sender, EventArgs e)
{

if (OFD_OLE_2.ShowDialog() == DialogResult.OK)
{
B_OLE_2_Read.Enabled = true;
B_OLE_2_Save.Enabled = true;
//TB_OLE_2_Path.Text = OFD_OLE_2.FileName;
Directory.CreateDirectory(Path.GetDirectoryName(OFD_OLE_2.FileName) + @"\
Копии"); // Создаём директорию под изменённые БД
File.Copy(OFD_OLE_2.FileName, Path.GetDirectoryName(OFD_OLE_2.FileName) +
@"\Копии\" + OFD_OLE_2.SafeFileName, true); // Копируем туда выбранную БД
(перезаписываем, в случае обнаружения похожего файла)
if (Path.GetDirectoryName(OFD_OLE_2.FileName) ==
Directory.GetDirectoryRoot(OFD_OLE_2.FileName)) // Проверяем путь, если находимся в
корневой директории диска, режем один слеш
TB_OLE_2_Path.Text = Path.GetDirectoryName(OFD_OLE_2.FileName) +
@"Копии\" + OFD_OLE_2.SafeFileName;
else
TB_OLE_2_Path.Text = Path.GetDirectoryName(OFD_OLE_2.FileName) + @"\
Копии\" + OFD_OLE_2.SafeFileName;

if (Path.GetExtension(OFD_OLE_2.FileName) == ".mdb")
{
ConnetionStringOLE2 = "Provider=Microsoft.Jet.OLEDB.4.0;" +
@"Data Source=" + TB_OLE_2_Path.Text + "";

303
MessageBox.Show("Выбрана база данных " + TB_OLE_2_Path.Text + "
формата: *" + Path.GetExtension(TB_OLE_2_Path.Text) + "!", "Работа с базами данных
(C#) :: OLE # 2");
}

if (Path.GetExtension(OFD_OLE_2.FileName) == ".accdb")
{
ConnetionStringOLE2 = "Provider=Microsoft.ACE.OLEDB.12.0;" +
@"Data Source=" + TB_OLE_2_Path.Text + "";
MessageBox.Show("Выбрана база данных " + TB_OLE_2_Path.Text + "
формата: *" + Path.GetExtension(TB_OLE_2_Path.Text) + "!", "Работа с базами данных
(C#) :: OLE # 2");
}
}
}

Событие Click кнопка Показать все записи:

private void B_OLE_2_Read_Click(object sender, EventArgs e)


{
DataSetOLE.Clear(); // Очищаем DataSetOLE перед повторным заполнением из базы
данных
ConnectionOLE2 = new OleDbConnection(ConnetionStringOLE2);

try
{
ConnectionOLE2.Open(); // Открываем соединение
MessageBox.Show("Соединение с базой данных " + TB_OLE_2_Path.Text + "
успешно открыто!", "Работа с базами данных (C#) :: OLE # 2");
}
catch (Exception ex)
{
MessageBox.Show("Невозможно открыть соединение с базой данных " +
TB_OLE_2_Path.Text + " (" + ex.Message + ")!", "Работа с базами данных (C#) :: OLE # 2");
}
// Создаем объект DataAdapter и передаём ему данные запроса
OleDbDataAdapter DataAdapter = new OleDbDataAdapter(); // DataAdapter -
посредник между базой данных и DataSet
DataAdapter.SelectCommand = new OleDbCommand(SQL_OLE, ConnectionOLE2);
DataAdapter.Fill(DataSetOLE); // Данные из адаптера поступают в DataSet
DataGridViewOLE.DataSource = DataSetOLE; // Связываем данные с элементом
DataGridView
// Закрываем соединение
ConnectionOLE2.Close();
}

Событие Click кнопка Сохранить записи:

private void B_OLE_2_Save_Click(object sender, EventArgs e)


{

try
{
DataAdapter_S.Update((DataTable)DataGridViewOLE_S.DataSource);
MessageBox.Show("Изменения в базе данных D:\\Копии\\LWP10-DB-OLE-
Special.accdb успешно внесены!", "Работа с базами данных (C#) :: OLE # 2");
}
catch (Exception ex)
{
MessageBox.Show("Невозможно сохранить изменения в базе данных D:\\Копии\\
LWP10-DB-OLE-Special.accdb (" + ex.Message + ")!", "Работа с базами данных (C#) :: OLE #
2");
}

304
}

Событие Load формы:

private void LWP10Main_Load(object sender, EventArgs e)


{
Directory.CreateDirectory(@"D:\" + @"\Копии");
File.Copy(@"D:\LWP10-DB-OLE-Special.accdb", @"D:\" + @"\Копии\" + @"LWP10-DB-
OLE-Special.accdb", true);

ConnetionStringOLE_S = "Provider=Microsoft.ACE.OLEDB.12.0;" +
@"Data Source=D:\Копии\LWP10-DB-OLE-Special.accdb";
ConnectionOLE_S = new OleDbConnection(ConnetionStringOLE_S);

try
{
ConnectionOLE_S.Open(); // Открываем соединение
MessageBox.Show("Соединение с базой данных D:\\Копии\\LWP10-DB-OLE-
Special.accdb успешно открыто!", "Работа с базами данных (C#) :: OLE # 2");
}
catch (Exception ex)
{
MessageBox.Show("Невозможно открыть соединение с базой данных D:\\Копии\\
LWP10-DB-OLE-Special.accdb (" + ex.Message + ")!", "Работа с базами данных (C#) :: OLE #
2");
}
DataTable DataTable_S = new DataTable();
// Создаём команду
OleDbCommand Command = new OleDbCommand("SELECT * FROM Main_Table",
ConnectionOLE_S);
// Создаём адаптер DataAdapter_S: посредник между базой данных и DataSet
DataAdapter_S = new OleDbDataAdapter(Command);
// Создаём построитель команд
// Для адаптера становится доступной команда Update и другие команды
OleDbCommandBuilder CommandBuilder = new OleDbCommandBuilder(DataAdapter_S);
// Данные из адаптера поступают в DataTable_S
DataAdapter_S.Fill(DataTable_S);
// Связываем данные с элементом DataGridView
DataGridViewOLE_S.DataSource = DataTable_S;
// Закрываем соединение
ConnectionOLE_S.Close();
}

Компилируем приложение (Debug) и запускаем:

305
Рис. 4. 4. Окончательная работа блока: OLE # 2

5. Модификация приложения Windows Forms: XML

Немного общих слов об XML-формате в целом.


Язык XML (расширяемый язык разметки) предназначен для хранения
структурированных данных. Данные, хранящиеся в формате XML, можно передавать
между программами. Поскольку данные в XML структурированные, в некоторых случаях
использование этого формата может заменить базы данных. Кроме этого, у XML есть
еще много других полезных применений.
XML весьма похож на другой язык разметки HTML, но в HTML набор тегов
фиксирован, и у каждого тега есть свое строго определённое назначение и правила
написания. В XML можно определять собственные теги, но при этом все равно нужно
соблюдать синтаксические правила языка разметки.

Типичный документ выглядит так:

<?xml version="1.0" encoding="UTF-8"?>


<recipe name="хлеб" preptime="5" cooktime="180">
<title>Простой хлеб</title>
<ingredient amount="3" unit="стакан">Мука</ingredient>
<ingredient amount="0.25" unit="грамм">Дрожжи</ingredient>
<ingredient amount="1.5" unit="стакан">Тёплая вода</ingredient>
<ingredient amount="1" unit="чайная ложка">Соль</ingredient>
<instructions>
<step>Смешать все ингредиенты и тщательно замесить.</step>
<step>Закрыть тканью и оставить на один час в тёплом помещении.</step>

306
<!-- <step>Почитать вчерашнюю газету.</step> - это сомнительный шаг... -->
<step>Замесить ещё раз, положить на противень и поставить в духовку.</step>
</instructions>
</recipe>

Главным является объявление XML:

<?xml version="1.0" encoding="UTF-8"?>

Объявление отвечает за кодировку документа (кодировка символов), наличие


внешних зависимостей и версия документа. В версии 1.0 объявление можно не
указывать. В версии 1.1 оно обязательно.

Важнейшее обязательное синтаксическое требование заключается в том, что


документ имеет только один корневой элемент (англ. root element) (также иногда
называемый элемент документа (англ. document element)). Это означает, что текст
или другие данные всего документа должны быть расположены между единственным
начальным корневым тегом и соответствующим ему конечным тегом.

Следующий простейший пример — правильно построенный документ XML:

<book>Это книга: "Книжечка"</book>

Комментарии доступны и здесь. Тэги внутри комментариев не обрабатываются:

<!-- Это комментарий -->

Остальная часть этого XML-документа состоит из вложенных элементов,


некоторые из которых имеют атрибуты и содержимое. Элемент обычно состоит из
открывающего и закрывающего тегов, обрамляющих текст и другие элементы.
Открывающий тег состоит из имени элемента в угловых скобках, например, <step>, а
закрывающий тег состоит из того же имени в угловых скобках, но перед именем ещё
добавляется косая черта, например, </step>. Имена элементов, как и имена атрибутов,
не могут содержать пробелы (иначе эти пробелы заменяются специальной
последовательностью символов, что будет показано ниже), но могут быть на любом
языке, поддерживаемом кодировкой XML-документа. Имя может начинаться с буквы,
подчёркивания, двоеточия. Остальными символами имени могут быть те же символы, а
также цифры, дефис, точка.

Содержимым элемента (англ. content) называется всё, что расположено между


открывающим и закрывающим тегами, включая текст и другие (вложенные) элементы.
Ниже приведён пример XML-элемента, который содержит открывающий тег,
закрывающий тег и содержимое элемента:

<step>Замесить ещё раз, положить на противень и поставить в духовку.</step>

Кроме содержания у элемента могут быть атрибуты — пары имя-значение,


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

307
<ingredient amount="3" unit="стакан">Мука</ingredient>

Для данной работы вышеописанных свойств достаточно. Хотя XML обладает ещё
целым рядом особенностей. Например, применение тэгов HTML, нетерпимость к
перекрывающимся тэгам (один тэг, открывшийся раньше, закрывается раньше
другого), наличие специальных символов HTML, пустых элементов и прочее.

Для начала добавим функциональности к уже имеющемся кода нашего


приложения. Для этого в начале файла LWP10Main.cs добавим следующее:

using System.Xml; // XML

Найдём:

OleDbDataAdapter DataAdapter = new OleDbDataAdapter(); // DataAdapter -


посредник между базой данных и DataSet
DataAdapter.SelectCommand = new OleDbCommand(SQL_OLE, ConnectionOLE2);
DataAdapter.Fill(DataSetOLE); // Данные из адаптера поступают в DataSet

Добавим после:

DataSetOLE.DataSetName = "Главная таблица"; // Устанавливаем название


корневого элемента XML-файла
DataSetOLE.WriteXml("D:\\LWP10-DB-XML.xml"); // Запишем данные таблицы в XML-
файл

Скомпилируем приложение (Debug) и запустим. В третьем блоке элементов


выберем базу данных и нажмём на Прочитать все записи. На диске D появится новый
XML-файл: LWP10-DB-XML.xml с содержанием:

308
Рис. 5. 1. Содержание файла LWP10-DB-XML.xml

Как видно, пробелы в именах тэгов были заменены на символы «_x0020_». В


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

Теперь создаём последнюю группу элементов. Она простая:

Рис. 5. 2. Расстановка элементов первой группы (XML)

Здесь у нас четыре кнопки, TextBox и большой ListBox. Также справа внизу
элемент SaveFileDialog. Свойства элементов таковы:

Button:
(Name): B_XML_Search
Text: Выбрать место сохранения
Size: 200; 23
Button:
(Name): B_XML_Create

309
Text: Создать простой документ
Size: 200; 23
Button:
(Name): B_XML_Read
Text: Прочитать простой документ
Size: 200; 23
Button:
(Name): B_XML_DB
Text: База данных обоев
Size: 200; 23
TextBox:
(Name): TB_XML_Path
ReadOnly: True
ListBox:
(Name): LB_XML
GroupBox:
(Name): GB_XML
Text: XML
SaveFileDialog:
(Name): SFD_XML
FileName: XML-Test
InitialDirectory: D:\
Filter XML-файл|*.xml

Самая нижняя кнопка (База данных обоев) является кнопкой, открывающей


новую форму, о чём пойдём немного ниже.

Событие Click кнопки Выбрать место сохранения:

private void B_XML_Search_Click(object sender, EventArgs e)


{
SFD_XML.ShowDialog();
TB_XML_Path.Text = SFD_XML.FileName;
}

Событие Click кнопки Создать простой документ:

private void B_XML_Create_Click(object sender, EventArgs e)


{
XmlWriterSettings SettingsXML = new XmlWriterSettings();
SettingsXML.Indent = true; // Включаем отступ для элементов XML-документа
SettingsXML.IndentChars = " "; // Задаём отступ (пробелами)
SettingsXML.NewLineChars = "\n"; // Задаём переход на новую строку
// Нужно ли опустить строку декларации формата XML документа
// Речь идёт о строке вида "<?xml version="1.0" encoding="utf-8"?>"
SettingsXML.OmitXmlDeclaration = false;

using (XmlWriter OutputXML = XmlWriter.Create(SFD_XML.FileName, SettingsXML))


{
// Создали открывающийся тег
OutputXML.WriteStartElement("XML-Test");
// Добавляем атрибут для XML-Test
OutputXML.WriteAttributeString("Count_Parameters", "4");
// Создаем элемент <имя>элемент</имя>

310
OutputXML.WriteElementString("Question", "Answer");
Random R = new Random();
OutputXML.WriteElementString("A", R.Next(0,1000).ToString());
OutputXML.WriteElementString("B", SQL_OLE);
OutputXML.WriteElementString("C", TB_XML_Path.Text);
OutputXML.WriteStartElement("Names");
OutputXML.WriteStartElement("Name");
OutputXML.WriteAttributeString("Type", "Male");
OutputXML.WriteString("John");
OutputXML.WriteEndElement();
OutputXML.WriteStartElement("Name");
OutputXML.WriteAttributeString("Type", "Male");
OutputXML.WriteString("Teo");
OutputXML.WriteEndElement();
OutputXML.WriteStartElement("Name");
OutputXML.WriteAttributeString("Type", "Famale");
OutputXML.WriteString("Miana");
OutputXML.WriteEndElement();
// Закрываем XML-Test
OutputXML.WriteEndElement();
// Сбрасываем буфферизированные данные
OutputXML.Flush();
// Закрываем фаил, с которым связан output
OutputXML.Close();
MessageBox.Show("Документ " + SFD_XML.FileName + " успешно создан!",
"Работа с базами данных (C#) :: XML");
}
}

Событие Click кнопки Прочитать простой документ:

private void B_XML_Read_Click(object sender, EventArgs e)


{
S = null;
LB_XML.Items.Clear();
// Создаём экземпляр класса
XmlDocument InputXML = new XmlDocument();
// Загружаем XML-документ из файла
InputXML.Load(SFD_XML.FileName);

// Загружаем XML-документ из строки


// InputXML.LoadXML(Path);

// Получаем всех детей корневого элемента


// InputXML.DocumentElement - корневой элемент
foreach (XmlNode Table in InputXML.DocumentElement.ChildNodes)
{
// Перебираем все атрибуты элемента
foreach (XmlAttribute A in Table.Attributes)
{
// A.Name - имя текущего атрибута
// A.Value - значение текущего атрибута
S = A.Name + ": " + A.Value;
}
// Перебираем всех детей текущего узла
foreach (XmlNode CN in Table.ChildNodes)
{
}
// Получаем текст хранящийся в текущем узле
S = S + Table.InnerText + "\n\t";
}
MessageBox.Show("Значения аттрибутов для элеметов и узлов файла "+
SFD_XML.FileName +":\n\n\t" + S + "\nЗначения получены автоматическим перебором!",
"Работа с базами данных (C#) :: XML");

311
// Вытаскиваем значения "руками"
XmlNodeList XMLTestAttributes = InputXML.SelectNodes("/XML-
Test[@Count_Parameters='4']");

foreach (XmlNode XN in XMLTestAttributes)


{
LB_XML.Items.Add(XN.InnerText + " -- все значения"); // В ListBox
получаем значения всех узлов
}
XmlNodeList Names =
InputXML.SelectNodes("/XML-Test/Names/Name[@Type='Male']");

foreach (XmlNode XN in Names)


{
LB_XML.Items.Add(XN.InnerText + " -- одно мужское имя"); // В ListBox
получаем только два мужских имени по отдельности
}
XmlNode Question = InputXML.DocumentElement.SelectSingleNode("Question");
LB_XML.Items.Add(Question.InnerXml);
XmlNode ANode = InputXML.DocumentElement.SelectSingleNode("A");
LB_XML.Items.Add(ANode.InnerText);
XmlNode BNode = InputXML.DocumentElement.SelectSingleNode("B");
LB_XML.Items.Add(BNode.InnerText);
XmlNode CNode = InputXML.DocumentElement.SelectSingleNode("C");
LB_XML.Items.Add(CNode.InnerText);
XmlNode NamesNode = InputXML.DocumentElement.SelectSingleNode("Names");
LB_XML.Items.Add(NamesNode.InnerText + " -- все имена"); // В Listbox
получаем все имена
}

Компилируем приложение (Debug) и запускаем:

312
Рис. 5. 3. Окончательная работа блока: XML

Нажатие на кнопку Создать простой документ, создаёт по указанному пути файл


(в данном случае по рисунку: D:\XML-Test.xml) следующего содержания:

При нажатии на кнопку Прочитать простой документ, происходит чтение всех


записей в цикле (с выводом всплывающего окна), а затем вывод в ListBox. Выводятся
все значения (верхняя строчка), затем только два мужских имени из блока
<Names>...</Names>, затем все значения узлов Question, A, B и С и наконец все
имена.
Почему существует несколько классов для работы с одним и тем же (в
частности в XML)? На этот вопрос, есть два ответа: «на вкус и цвет все фломастеры
разные» и «в зависимости от предоставляемой функциональности один класс может
оказаться удобнее в той или иной ситуации».

Несколько причин, почему стоит использовать классы для генерации XML


документа, а не создавать его вручную:
1. Практически нулевой риск создать неправильный XML документ.
2. Можно задать форматирование (отступы и прочее) XML документа (очень
сильно улучшает читаемость).

Наиболее подходящим для формирования XML-документа «за раз» является


класс XmlWriter. Плюсы данного класса:
1. Форматирование результирующего XML документа.
2. Создание открывающего и закрывающего тега в одну команду, причём для
создания закрывающего тега не требуется знать имя открывающего.
Для форматирования выводимого XML документа в XmlWriter используется класс
XmlWriterSettings.

Используя функцию WriteElementString, тэг не только открывается, в него


записываются данные, но и тут же закрывается, поэтому добавить атрибут к такому
элементу НЕЛЬЗЯ! Для решения данной проблемы нужно использовать
WriteStartElement, WriteAttributeString, WriteEndElement.

313
Для чтения XML-документа предусмотрен класс XMLReader, но он крайне не
удобен, потому что осуществляет последовательное считывание и не позволяет
«прыгать через»/обращаться сразу к нужному элементу.

Чтение и редактирование удобно производить через класс XmlDocument.


Из чего состоит XML-документ:
1. Строка вида <?xml … ?>
2. Родительский элемент (единственный).
3. Дальше, внутри родительского элемента располагаются элементы-потомки
(child’ы) и далее по иерархии.

Теперь давайте рассмотрим как изменять уже имеющийся XML-


документ?

Для вставки элемента в иерархию XML-документа необходимо создать элемент


типа XmlNode и задать его родителя, делается это следующим образом:

// Создаём node
// book - имя узла
XmlNode node = xmlDoc.CreateElement("book");
// Добавляем его в качестве ребенка
parentNode.AppendChild(node);

Удаление узла?

// Удаление узла
parentNode.RemoveChild(node);

Работа с атрибутами:

// Создаём новый атрибут


// genre - имя атрибута
XmlAttribute newAttr = doc.CreateAttribute("genre");
newAttr.Value = "novel"; // Задаём его значение
// Добавляем атрибут в коллекцию атрибутов элемента
node.SetNamedItem(newAttr);
// Удаляем атрибут
node.Attributes.RemoveNamedItem("genre");
// Изменяем значение атрибута
XmlAttributeCollection Attribs = node.Attributes;
XmlAttribute attr = (XmlAttribute)Attribs.GetNamedItem("genre");
attr.Value = "fiction";

Настало время разработать более-менее полезную функциональность.


Используем возможности XML и сделаем на основе него полноценную базу данных.

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

Для XML справедливо: «Содержимым элемента (content) называется всё, что


расположено между открывающим и закрывающим тегами. Это текст, вложенные
элементы, комментарии и т.п.». Все спецификации XML документов подчёркивают, что
именно текст, то, что является содержимым документа. Все средства для работы с XML
документами, в том числе и методы класса XmlDocument, также ориентированы на
текст (CreateTextNode и т.п.).

314
Из сказанного можно сделать вывод: хранить двоичные данные можно, только
если они будут представлены как текстовые строки. Однако, прямое преобразование
всего массива байт (двоичных данных) в текстовую строку не будет решением в силу
того, что возможно присутствие в двоичном коде значений, совпадающих со
служебными символами кодировки строк и значений, равных завершению строки. Как
результат - мы сможем поместить в контент тэга только часть строки (до первого
совпадения двоичного символа со значением окончания строки) при возможном
отображении помещенного кода на нескольких строках XML файла для одного
содержимого контента. Следовательно, строка должна формироваться и присваиваться
значению контента как единое целое. Способы выполнить это могут быть разные, от
посимвольного преобразования байт в значение char и последовательного добавления
результата в содержимое контента (медленный способ), до использования
StringBuilder (быстрый способ).

Наиболее часто хранение двоичных данных связано с задачей хранения


рисунков (хотя приведенный ниже код будет равно хорошо работать и при
использовании его для хранения в виде двоичных данных Web-страниц, Word или
Excel-документов и прочее). Содержимое файла рисунка: массив шестнадцатеричных
символов. Как следствие, и код представления рисунка в content опирается на
шестнадцатеричное отображение строк.

Так что же это будет за функциональность? Приложение будет по


указанному через диалог изображению создавать запись в базе данных (XML-файл),
куда будет помещаться как сведения об изображении, так и само изображение в виде
длинной строки символов. Также по номеру (ID рисунка) из базы можно будет
восстановить это изображение и просмотреть его. Изображения можно будет только
добавлять в базу, и просматривать добавленное уже из базы. Для этой
функциональности у нас предусмотрена кнопка База данных обоев. А также заранее
заготовленный файл Wallpapper-DB.xml с содержанием:

<?xml version="1.0" encoding="windows-1251"?>


<NewDataSet>
<wallpapper>
<id></id>
<name></name>
<pichash></pichash>
<picext></picext>
<size></size>
</wallpapper>
</NewDataSet>

Немного о тэге «NewDataSet». Поскольку мы будем работать с DataSet, то при


сохранении содержимого DataSet в XML файл, данный тэг добавляется автоматически.
Также этот тэг является параметром свойства DataSetName.
Первым делом нам нужна новая форма. Добавим её в наше приложение: Проект
-> Добавить новый элемент... (Ctrl+Shift+A). В открывшемся окне ищем Форма
Windows Forms, в поле Имя ниже вводим LWP10Wallpapper.cs:

Свойства формы:
(Name): LWP10Wallpapper
Text: Работа с базами данных (C#) :: База
данных обоев

315
Size: 500; 500

Расставим элементы как показано на рисунке ниже:

Рис. 5. 4. Расстановка элементов в новой форме

Основным элементом здесь является PictureBox ( ),


который разместим в самом центре (пунктирный прямоугольник). Остальные элементы
это три кнопки, четыре TextBox, два «именных» Label’а слева под рисунком (Имя
файла: и Разрешение рисунка:).

Свойства элементов:

Button:
(Name): B_OPEN
Text: Выбрать рисунок

316
Size: 200; 23
Button:
(Name): B_SEARCH
Text: Выбрать рисунок для добавления
Size: 200; 23
Button:
(Name): B_SAVE
Text: Сохранить рисунок в базе данных
Size: 200; 23
TextBox:
(Name): TB_NUMBER
TextBox:
(Name): TB_FORMAT
TextBox:
(Name): TB_SIZE
ReadOnly: True
TextBox:
(Name): TB_NAME
PictureBox:
(Name): PB_MAIN
OpenFileDialog:
(Name): OFD_FIND
InitialDirectory: D:\
Filter GIF-файлы|*.gif|BMP-файлы|*.bmp|JPEG-
файлы|*.jpg
Label:
(Name): L_NAME
Text: Имя файла:
Label:
(Name): L_SIZE
Text: Разрешение рисунка:

Обработчик события Click для кнопки База данных обоев главной формы
выглядит так:

private void B_XML_DB_Click(object sender, EventArgs e)


{
LWP10Wallpapper DBWallpapper = new LWP10Wallpapper();
DBWallpapper.ShowDialog();
}

В начало файла LWP10Wallpapper.cs добавим следующие строчки:

using System.Xml;
using System.IO;
using System.Drawing.Imaging;

В этом же файле найдём:

public partial class LWP10Wallpapper : Form


{

Добавим после:

317
// Директория выполнения приложения
private string String_Path = String.Empty;
XmlDocument XML;
// Классы для работв с XML-документом как с объектом базы данных
DataTable WallpapperDataTable = null;
DataSet WallpapperDataSet = null;

Событие Load нашей дочерней формы:

private void LWP10Wallpapper_Load(object sender, EventArgs e)


{
//Path = Directory.GetCurrentDirectory(); // Текущая директория, из которой
запущено приложения
String_Path = @"D:\\";
L_NAME.Text = "Имя рисунка: ";
L_SIZE.Text = "Разрешение рисунка: ";
// Инициализируем объект StreamReader, считывающий символы из потока байтов в
определённой кодировке
using (StreamReader SR = new StreamReader(String_Path + @"\Wallpapper-
DB.xml", System.Text.Encoding.UTF8))
{
WallpapperDataSet = new DataSet();
WallpapperDataSet.ReadXml(SR, XmlReadMode.Auto); // Считываем XML-схему и
данные в DataSet
WallpapperDataTable = WallpapperDataSet.Tables[0];
}
}

Обратим внимание на параметр метода ReadXml: XmlReadMode.Auto - он


позволит DataSet правильно создать схему с учётом значений мегатэгов, а также на
значение Encoding.UTF8. Причина использования именно этой кодировки: DataSet по
умолчанию будет сохранять данные в кодировке UTF-8.
В самом этом коде инициализируются основные пути к файлу базы данных, а
также создаётся объект StreamReader (который занимается считыванием символов с
файла в определённой кодировке, в данном случае UTF-8), после чего на основе XML-
схемы формируется DataSet (столбцы таблицы).

Событие Click кнопки Выбрать рисунок для добавления:

private void B_SEARCH_Click(object sender, EventArgs e)


{
OFD_FIND.InitialDirectory = String_Path;

if (OFD_FIND.ShowDialog() == DialogResult.OK)
{
PB_MAIN.Image = Image.FromFile(OFD_FIND.FileName);
TB_FORMAT.Text = Path.GetExtension(OFD_FIND.FileName);
TB_NAME.Text = Path.GetFileNameWithoutExtension(OFD_FIND.FileName);
TB_SIZE.Text = PB_MAIN.Image.Width.ToString() + "x" +
PB_MAIN.Image.Height.ToString();
}
}

Событие Click кнопки Сохранить рисунок в базе данных:

private void B_SAVE_Click(object sender, EventArgs e)


{
// Сохранять не будем - если что-то не ввели
if (PB_MAIN.Image == null) return;
if (TB_NAME.Text == "") return;
if (TB_FORMAT.Text == "") return;
if (TB_SIZE.Text == "") return;

318
// Строки для метода SELECT в XML-документе
DataRow[] datarows = null;
// Ищем максимальное ID в DataSet (в DataTable)
string s = string.Empty;

try
{
datarows = WallpapperDataTable.Select("id=max(id)");
s = datarows[0]["id"].ToString();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Работа с базами данных (C#) :: База данных
обоев");
}

if (s == "" || s == string.Empty) // Если база данных пустая, то...


{
s = "0";
}
// Для формирования строки рисунка создаём StringBuilder
StringBuilder SB = new StringBuilder();
int i = int.Parse(s) + 1; // Если база пустая, то начинаем с 1, иначе, с
максимального номера + 1
// Создаём новую строку для WallpapperDataSet
DataRow datarow = WallpapperDataSet.Tables[0].NewRow(); // Формируем DataRow
на основе DataSet
// Присваиваем значения столбцам строки
datarow[0] = Convert.ToString(i);
datarow[1] = TB_NAME.Text.Trim();
// Формируем строковое представление рисунка
using (MemoryStream MS = new MemoryStream())
{
PB_MAIN.Image.Save(MS, ImageFormat.Gif); // Сохраняем изображение в потом
MemoryStream, расширение *.gif
byte[] b = new byte[MS.Length]; // 8-битное число (массив) длины потока в
байтах
//memorystream.Read(b, 0, (int)memorystream.Length);
b = MS.GetBuffer(); // Присваиваем byte b массив байтов потока
s = string.Empty;
foreach (Byte zb in b)
{
int a = (int)zb;
SB.Append(a.ToString("X2")); // Формируем окончательную строку (путём
добавления) из данных массива байтов в шестнадцатеричном виде (X2) (шестнадцатеричное
представление каждого байта рисунка)
//value = 123456789;
//Console.WriteLine(value.ToString("X"));
// Выведет: 75BCD15
//Console.WriteLine(value.ToString("X2"));
// Выведет: 75BCD15
}
datarow[2] = Convert.ToString(SB); // Отправляем всю строку в столбец
pichash нашей базы данных
}
datarow[3] = TB_FORMAT.Text.Trim();
datarow[4] = TB_SIZE.Text.Trim();
WallpapperDataSet.Tables[0].Rows.Add(datarow); // Формируем всю запись базы
данных в DataSet
// Удаляем строку с пустыми значениями, которые при первоначальной
// загрузке были использованы для формирования схемы
if (i == 1)
{
WallpapperDataSet.Tables[0].DefaultView.AllowDelete = true;
WallpapperDataSet.Tables[0].DefaultView.Delete(0);

319
}
PB_MAIN.Image = null;
TB_SIZE.Text = "";
TB_FORMAT.Text = "";
TB_NAME.Text = "";
// Сохраняем данные
WallpapperDataSet.WriteXml(String_Path + @"\Wallpapper-DB.xml",
XmlWriteMode.WriteSchema);
WallpapperDataSet = new DataSet();
// Вновь загружаем сохраненные данные
WallpapperDataSet.ReadXml(String_Path + @"\Wallpapper-DB.xml",
XmlReadMode.Auto);
WallpapperDataTable = WallpapperDataSet.Tables[0];
}

Этот код выполняем основные функции по сохранению данных выбранного


рисунка в базе данных. Вначале проверяется заполнение всех TextBox’ов (кроме
верхнего). Если данные были введены и рисунок указан, далее проверяется наличие в
базе ещё добавленных записей (по максимально числу в тэгах <id>X</id>). Если БД
пустая, начинаем заполнение с номера 1. После чего инициализируется объект
StringBuilder и производятся основные действия по переведению байтов рисунков в
строчное представление для сохранение в базе. Данные TextBox’ов также заносятся в
базу.

Теперь инициализируем извлечение данных из базы. Событие KeyPress для


TB_SEARCH (верхний TextBox):

private void TB_NUMBER_KeyPress(object sender, KeyPressEventArgs e)


{
// Введённые символы должны быть только цифрами, иначе ввода не будет (символ
не введеётся)
if (!Char.IsDigit(e.KeyChar))
{
e.Handled = true;
}
}

Событие Click кнопки Выбрать рисунок:

private void B_OPEN_Click(object sender, EventArgs e)


{

if (TB_NUMBER.Text.Trim() != "")
{
TB_NAME.Text = "";
TB_FORMAT.Text = "";
TB_SIZE.Text = "";
PB_MAIN.Image = null;
GetPicture(TB_NUMBER.Text.Trim()); // Отправляем номер функции, которая
вытащит из XML-документа все данные
}

И код функции для выбора по номеру, вводимому в TB_NUMBER:

private void GetPicture(string X)


{
DataRow[] datarows = null;

try
{

320
datarows = WallpapperDataTable.Select("id=" + X); // Получаем все данные
DataTable по ключу <id>X</id>
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Работа с базами данных (C#) :: База данных
обоев");
return;
}

if (datarows.Length > 0) // Не пусто ли?


{

foreach (DataRow datarow in datarows) // Перебираем все столбцы записи


{
L_NAME.Text = "Имя файла: " + datarow["name"].ToString();
string s3 = datarow["pichash"].ToString();
L_SIZE.Text = "Разрешение рисунка: " + datarow["size"].ToString();
using (MemoryStream MS = new MemoryStream())
{

for (int i = 0; i < s3.Length / 2; i++) // Обходим половину


знаков строки из <pichash>s3</pichash> в верхнем цикле, макс. i = 249 если s3.Length =
500
{

if (i * 2 + 1 < s3.Length) // Последнее условие: 249 * 2 + 1


< 500
{
string s02 = Convert.ToString(s3[i * 2]); // Чётные
символы (посл.: 249 * 2 = 498 символ, предпоследний в строке)
string s03 = Convert.ToString(s3[i * 2 + 1]); // Нечётные
символы (посл.: 249 * 2 + 1 = 499 символ, последний в строке)
string s04 = s02 + s03; // Объединяем в одну строку (A +
B)
int a = int.Parse(s04,
System.Globalization.NumberStyles.HexNumber); // 32 разряда получаем из двух 16 разрядных
чисел юникода (A + B)
MS.WriteByte(Convert.ToByte(a)); // Восстанавливает один
байт за проход (из 32 разрядного представления знакового целого числа), всего 250
проходов по два символа за раз
}
}
PB_MAIN.Image = Image.FromStream(MS); // Восстанавливаем рисунок
(создаём рисунок из потока байтов)
}
}
}
}

Все данные для одного рисунка по номеру вытаскиваются этим кодом из файла
базы данных в объект DataTable. Данные выводятся из файла через массив DataRow[]
с непосредственным выбором по именам узлов. Из строки символов
<pichash>...</pichash> формируются байты рисунка и затем передаются элементу
PictureBox.

Готово. Можно компилировать и проверять работоспособность.

6. Завершающая часть

Компилируем приложение (Release) и запускаем. Результат работы показан


ниже (Рис. 6. 1):

321
Рис. 6. 1. Модифицированное приложение Windows Forms: результат работы
приложения по сохранению рисунка в базе данных (сохранение выбранного рисунка в
базе)

322
Рис. 6. 2. Модифицированное приложение Windows Forms: результат работы
приложения по сохранению рисунка в базе данных (выбор рисунка из базы по номеру)

Содержание базы данных (файл Wallpapper-DB.xml):

323
Рис. 6. 3. Содержимое файла Wallpappaer-DB.xml: четыре рисунка, длинные строчки
<pichash>...</pichash> содержат символы рисунков (окончания строк не видны)

Недостаток хранения данных в XML-файлах: длина строки, хотя это не значит,


что при хранении в таблицах баз данных мы выиграем в объёме или скорости
обработки (скорее наоборот проиграем за счёт сетевого трафика).

Если имена рисунков4 будут на русском (символы кириллицы), они будут


корректно сохранены в базе.

ПРИМЕЧАНИЕ № 4: Точно таким образом можно сохранять и восстанавливать


любые данные (Web-страницы, Word или Excel-документы и прочее). Подобный способ
хранения может быть полезен, для решения многих задач связанных с хранением и
передачей информации, в том числе и конфиденциальной (например, после
преобразования файла всю строку можно закодировать паролем через MD5-хэш).

Если по каким-то причинам русские символы не читаются, сделаем следующее:

Найдём:

using (StreamReader SR = new StreamReader(String_Path + @"\Wallpapper-


DB.xml", System.Text.Encoding.UTF8))

И заменим:

using (StreamReader SR = new StreamReader(String_Path + @"\Wallpapper-


DB.xml", System.Text.Encoding.Default))

Найдём:

// Сохраняем данные

324
WallpapperDataSet.WriteXml(String_Path + @"\Wallpapper-DB.xml",
XmlWriteMode.WriteSchema);
WallpapperDataSet = new DataSet();
// Вновь загружаем сохраненные данные
WallpapperDataSet.ReadXml(String_Path + @"\Wallpapper-DB.xml",
XmlReadMode.Auto);
WallpapperDataTable = WallpapperDataSet.Tables[0];

Заменим:

XML = new XmlDocument();


XML.InnerXml = WallpapperDataSet.GetXml();
XmlDeclaration XMLDeclaration = XML.CreateXmlDeclaration("1.0", "windows-
1251", "yes");
XML.InsertBefore(XMLDeclaration, XML.DocumentElement);
XML.Save(String_Path + @"\Wallpapper-DB.xml");
WallpapperDataSet = new DataSet();
WallpapperDataSet.ReadXml(String_Path + @"\Wallpapper-DB.xml",
XmlReadMode.Auto);
WallpapperDataTable = WallpapperDataSet.Tables[0];

7. О приложении к Лабораторной работе № 10

Получившуюся программу (LWP10DB.exe), собранную из кусков кода


приведённых в данной лабораторной работе, три базы данных (LWP10-DB-ODBC.mdb,
LWP10-DB-OLE.accdb и LWP10-DB-OLE-Special.accdb), а также XML-файл для базы
данных Wallpapper-DB.xml, использованные в данной работе можно загрузить по
ссылке в конце этого материала (сслыка доступна в программном продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

11. Лабораторная работа № 11: Динамическая связь приложений через библиотеку


классов

Лабораторная работа № 11: Динамическая связь приложений через


библиотеку классов

Содержание

1. Вводная часть
2. Удалённый объект: создание библиотеки классов удалённого объекта
3. Клиент: создание приложения Windows Forms
4. Сервер: создание консольного приложения
5. Завершающая часть
6. О приложении к Лабораторной работе № 11

1. Вводная часть

В этой работе будет рассмотрена работа с динамически связанными


приложениями. Нас интересует именно те случаи, когда одно приложение способно и
может при необходимости передавать данные в другое приложение, причём оба

325
приложения запущены в один и тот же момент и для передачи данных не используются
временные файла на жёстком диске. Фактически приложения будут общаться друг с
другом словно клиент и сервер. То есть это общение будет аналогично общению между
удалёнными машинами (через протоколы HTTP, FTP или TELNET). Для реализации
данной функциональности лучше всего использоваться так называемые «каналы».
Для чего может понадобится подобное? Динамическая связь приложений как удалённо,
так и на локальной машине удобна для передачи малых объёмов информации. При этом
данные передаются по защищённому каналу (данные можно предварительно
зашифровать тем же MD5-хэшем). Также, такая связь приложений не оставляет
лишнего «мусора» изначально. А также, возможно реализовать удалённое
взаимодействие клиента и сервера.

Что такое каналы?

Каналы используются инфраструктурой удалённого взаимодействия


платформы .NET Framework для передачи удаленных вызовов. Если клиент вызывает
удаленный объект, вызов «сериализуется» (перевод структуры данных в
последовательность битов) в сообщение, которое отправляется по клиентскому каналу,
а получается по каналу сервера. После получения сообщения, оно «десериализуется» и
обрабатывается. Возвращаемые значения передаются по каналу сервера, а их
получение производится по каналу клиента.
Класс IpcChannel является удобным классом, в котором сочетаются
функциональные возможности класса IpcClientChannel и класса IpcServerChannel.

Для работы с каналами нам потребуются следующие пространства имён:

1. Обеспечивает реализацию канала, который использует протокол


межпроцессорного взаимодействия для передачи сообщений.
Пространство имён: System.Runtime.Remoting.Channels.Ipc;
Сборка: System.Runtime.Remoting (в System.Runtime.Remoting.dll).
2. А также основные пространства имён:
System.Runtime.Remoting;
System.Runtime.Remoting.Channels;

Что такое IPC?

Межпроцессное взаимодействие (Inter-Process Communication, IPC) —


набор способов обмена данными между множеством потоков в одном или более
процессах. Процессы могут быть запущены на одном или более компьютерах,
связанных между собой сетью. IPC-способы делятся на методы обмена сообщениями,
синхронизации, разделяемой памяти и удалённых вызовов (RPC). Методы IPC зависят
от пропускной способности и задержки взаимодействия между потоками и типа
передаваемых данных.
IPC также может упоминаться как межпотоковое взаимодействие (inter-thread
communication), межпоточное взаимодействие и межпрограммное взаимодействие
(inter-application communication). IPC наряду с концепцией адресного пространства
является основой для разграничения адресного пространства.

Что будет представлять собой приложение, разрабатываемое в данной работе?

Первое приложение будет представлять собой окно пустое Windows Forms. Так
будет реализован клиент. Реализация сервера будет в виде консольного окна.

326
Удалённый объект будет сформирован в виде библиотеки классов. Всего три проекта и
три готовых объекта, два из которых — приложения.

2. Удалённый объект: создание библиотеки классов удалённого объекта

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать первый проект, для этого выполним последовательно:
Файл -> Создать -> Проект… (также можно просто нажать сочетание клавиш
Ctrl+Shift+N или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Библиотека классов. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

327
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP11-RemoteObject — это название программы (выбрано


по названию лабораторного практикума, номеру и названию работы, через тире в
названии обозначаем принадлежность будущего приложения). В поле Расположение
указана конечная директория, где будет находиться весь проект. Выберем
расположение удобное для быстрого поиска. В поле Имя решения вводится либо
название программы «по умолчанию» из поля Имя автоматически, либо можно ввести
своё собственное. Под этим именем будет создана конечная папка проекта (если Имя и
Имя решения разные).

328
Рис. 2. 3. Вводим данные нового проекта библиотеки классов

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


библиотеки классов (не пустой изначально).

329
Рис. 2. 4. Обозреватель решений: состав проекта библиотеки классов
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F6


(Построение -> Построить решение). Тем самым мы собираем библиотеку в DLL-
файл (и производим компиляцию debug-версии библиотеки) (Debug выбрано
изначально).

Рис. 2. 5. Скомпилированная библиотека классов по конфигурации Debug

Теперь вставим место всего кода единственного файла Class1.cs следующий


код:

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

namespace LWP11_RemoteObject
{

330
public interface IRemoteSend1
{
void Send(int x, int y);
}

public interface IRemoteSend2


{
void Send(int x, string y);
}
}

Сам файл Class1.cs переименуем в LWP11RO.cs. Для переименования


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

«Перестроим» нашу библиотеку, нажав клавишу F6 (Построение -> Построить


решение) но уже по конфигурации Release (для этого выбираем другую

конфигурацию: в выпадающем списке Конфигурация решения). Тем


самым мы собираем библиотеку в DLL-файл (и производим компиляцию уже release-
версии библиотеки).
Найдём откомпилированную библиотеку классов и поместим её в любое удобное
место на диске (для последующего быстрого поиска).

3. Клиент: создание приложения Windows Forms

Снова запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице).

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее найдём в
списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

В поле Имя вводим LWP11-Client — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы, через тире в
названии обозначаем принадлежность будущего приложения). В поле Расположение
указана конечная директория, где будет находиться весь проект. Выберем
расположение удобное для быстрого поиска. В поле Имя решения вводится либо
название программы «по умолчанию» из поля Имя автоматически, либо можно ввести
своё собственное. Под этим именем будет создана конечная папка проекта (если Имя и
Имя решения разные).

331
Рис. 3. 1. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

332
Рис. 3. 2. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

333
Рис. 3. 3. Запуск приложения Windows Forms по конфигурации Debug

Модифицируем приложение. Для начала изменим размер нашей единственной


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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP11Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Динамическая
связь приложений (C#) :: Клиент
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 500;
200
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

334
) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести
необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Получим нечто подобное:

Рис. 3. 4. Модифицированная форма приложения

Теперь необходимо добавить удалённый объект в качестве Ссылки. Выполним:


Проект -> Добавить ссылку, либо ПКМ на Ссылки в обозревателе решений, далее
Добавить ссылку...:

В открывшемся окне перейдём на вкладку Обзор, укажем путь к библиотеке


классов, которую мы уже скомпилировали:

335
Рис. 3. 5. Добавить ссылку: выбор нужного объекта, а именно ранее
скомпилированной библиотеки классов: LWP11-RemoteObject.dll

Жмём ОК. Удалённый объект добавлен в наше приложение и теперь можно


работать с классами, предоставленными этим объектом:

Теперь приступим к формированию элементов и их функциональности. Для


начала расставим элементы как показано на рисунке ниже:

Рис. 3. 6. Расстановка элементов на форме (на форме LWP11Main проекте LWP11-


Client)

Здесь два простых TextBox и два Label.

По порядку распишем Свойства каждого элемента:

TextBox:
(Name): TB_1
Size: 30; 20

336
TextBox:
(Name): TB_2
Size: 30; 20

Теперь отправляемся в код формы (правая кнопка мыши на значке формы, далее
Перейти к коду или нажмём на клавишу F7):

В самое начало кода добавим:

using LWP11_RemoteObject; // Подключаем объект

Найдём:

public partial class LWP11Main : Form


{

Добавим после:

// Переменная для объекта


private IRemoteSend1 _rs1;
private IRemoteSend2 _rs2;

Событие KeyPress верхнего TextBox:

private void TB_1_KeyPress(object sender, KeyPressEventArgs e)


{
// Введённые символы должны быть только цифрами, иначе ввода не будет (символ
не введётся)
if (!Char.IsDigit(e.KeyChar))
{
e.Handled = true;
}
}

Событие KeyPress нижнего TextBox:

private void TB_2_KeyPress(object sender, KeyPressEventArgs e)


{
// Введённые символы должны быть только буквами, иначе ввода не будет (символ
не введётся)

337
if (!Char.IsLetter(e.KeyChar))
{
e.Handled = true;
}
}

Событие MouseClick формы LWP11Main:

private void LWP11Main_MouseClick(object sender, MouseEventArgs e)


{
// Получаем объект
_rs1 = (IRemoteSend1)Activator.GetObject(typeof(IRemoteSend1),
"ipc://4567/RemoteSend1.rem"); // Открываем первый канал
// Передаём координаты нажатия ЛКМ
try
{
_rs1.Send(e.X, e.Y);
}
catch { }
}

Последнее событие TextChanged нижнего TextBox:

private void TB_2_TextChanged(object sender, EventArgs e)


{
// Получаем объект
_rs2 = (IRemoteSend2)Activator.GetObject(typeof(IRemoteSend2),
"ipc://4567/RemoteSend2.rem"); // Открываем первый канал
// Передаём данные из TextBox
try
{
_rs2.Send(Convert.ToInt16(TB_1.Text), TB_2.Text);
}
catch { }
}

Компилируем приложение (Release) и запускаем. Проверяем работоспособность


текстовых полей. Вводим в верхнее только цифры, в нижнее только буквы:

Рис. 3. 7. Окончательная работа приложения-клиента (LWP11-Client.exe)

4. Сервер: создание консольного приложения

Снова запускаем Visual Studio 2010, откроется Начальная страница:

338
Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице).

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее найдём в
списке Консольное приложение. Также здесь можно выбрать какой использовать
«фреймворк» (набора компонентов для написания программ). В нашем случае выберем
.NET Framework 4.

В поле Имя вводим LWP11-Server — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы, через тире в
названии обозначаем принадлежность будущего приложения). В поле Расположение
указана конечная директория, где будет находиться весь проект. Выберем
расположение удобное для быстрого поиска. В поле Имя решения вводится либо
название программы «по умолчанию» из поля Имя автоматически, либо можно ввести
своё собственное. Под этим именем будет создана конечная папка проекта (если Имя и
Имя решения разные).

Рис. 4. 1. Вводим данные нового проекта консольного приложения

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


консольного приложения (не пустого изначально).

339
Рис. 4. 2. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально). При первом запуске пустого консольного
приложения, окно появится и сразу же исчезнет.

Теперь необходимо добавить удалённый объект в качестве Ссылки. Выполним:


Проект -> Добавить ссылку, либо ПКМ на Ссылки в обозревателе решений, далее
Добавить ссылку....
В открывшемся окне перейдём на вкладку Обзор, укажем путь к библиотеке
классов, которую мы уже скомпилировали:

340
Рис. 4. 3. Добавить ссылку: выбор нужного объекта, а именно ранее скомпилированной
библиотеки классов: LWP11-RemoteObject.dll

Жмём ОК. Удалённый объект добавлен в наше приложение и теперь можно


работать с классами, предоставленными этим объектом:

Также, необходимо добавить в качестве ссылки ещё один объект (вкладка


.NET): System.Runtime.Remoting:

341
Рис. 4. 4. Добавить ссылку: добавление ссылки System.Runtime.Remoting

Модифицируем код консольного приложения. В самом начале добавим


следующие строчки:

using LWP11_RemoteObject;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;

Непосредственно главную функцию, метод Main() изменим следующим образом:

static void Main(string[] args)


{
Console.Title = "Динамическая связь приложений (C#) :: Сервер";
Console.WriteLine("Динамическая связь приложений (C#) :: Сервер");
// Регистрируем канал № 4567
var ipcChannel = new IpcChannel("4567");
ChannelServices.RegisterChannel(ipcChannel, false);
// Получаем данные по каналам
RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteSend1),
"RemoteSend1.rem", WellKnownObjectMode.Singleton);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteSend2),
"RemoteSend2.rem", WellKnownObjectMode.Singleton);

Console.WriteLine("\nНажмите Enter для выхода из приложения...\n\nДанные


полученные по каналам:\n\t");
Console.ReadLine();
}

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


данных от клиента. Реализуем их в одном файле. Для этого добавим новый файл в наш
проект (Проект -> Добавить новый элемент..., либо нажать сочетание клавиш

342
Ctrl+Shift+A). В открывшемся окне выберем Класс, в качестве имени укажем
RemoteSend.cs:

Рис. 4. 5. Добавление нового элемента: Класс с именем RemoteSend.cs

Изменим весь код нового файла класса следующим образом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LWP11_RemoteObject;

namespace LWP11_Server
{
class RemoteSend1 : MarshalByRefObject, IRemoteSend1
{
public void Send(int x, int y)
{
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("\tX = {0}, Y = {1}", x, y);
}
}

class RemoteSend2 : MarshalByRefObject, IRemoteSend2


{
public void Send(int x, string y)
{
Console.ForegroundColor = ConsoleColor.DarkRed;
Console.WriteLine("\tTB_1.Text = {0}, TB_2.Text = {1}", x, y);
}
}
}

343
Компилируем приложение (Release) и запускаем.

Рис. 4. 7. Окончательная работа приложения-сервера (LWP11-Server.exe)

5. Завершающая часть

Теперь запускаем только приложение-клиент и приложение-сервер. Щёлкаем на


любом место формы клиента, а также вводим числом и букву в текстовые поля. Сервер
будет «ловить» все щелчки по форме клиента (левой кнопкой мыши), а также
изменения текста в «буквенном» текстовом поле (после изменения, значения двух
текстовых полей будут переданы серверу). Результат действий видим в окне консоли
сервера. Результат работы показан ниже (Рис. 5. 1):

344
Рис. 5. 1. Динамическая связь приложений: результат работы приложения-клиента
(сверху) и приложения-сервера (снизу)

6. О приложении к Лабораторной работе № 11

Получившиеся программы (LWP11-Server.exe и LWP11-Client.exe), собранные


из кусков кода приведённых в данной лабораторной работе, а также DLL-файл для
удалённого объекта (LWP11-RemoteObject.dll) можно загрузить по ссылке в конце этого
материала (сслыка доступна в программном продукте).

Приложение № 1: Исходный код программы-сервера и всех


сопровождающих файлов с кодом приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).
Приложение № 2: Исходный код программы-клиента и всех
сопровождающих файлов с кодом приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).
Приложение № 3: Исходный код удалённого объекта-библиотеки и всех
сопровождающих файлов с кодом приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

12. Лабораторная работа № 12: Автоматизация Microsoft Office Word

Лабораторная работа № 12: Автоматизация Microsoft Office Word

345
Содержание

1. Вводная часть
2. Создание приложения Windows Forms
3. Модификация приложения Windows Forms: создание и заполнение
нового документа Microsoft Word
4. Модификация приложения Windows Forms: работа с шаблонами
5. Завершающая часть
6. О приложении к Лабораторной работе № 12

1. Вводная часть

В этой работе будет рассмотрена работа с программой, входящей в состав


известного пакета программ Microsoft Office. В частности, речь пойдёт о Microsoft
Word и некоторых возможностях по автоматизации работы с ним. Для демонстрации
работы будет использована версия Word 2010. В данной работе будет рассмотрено
следующее:
1. Автоматическое создание нового документа (код адаптирован для старых
Word 2003 и выше).
2. Автоматизация работы с шаблонами документов (тестирование и
гарантированная работа кода с Word 2010).
На этих примерах будут продемонстрированы основные возможности работы с
Microsoft Word при помощи сторонних приложений на C#.

2. Создание приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

346
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP12Word — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

347
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

348
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

349
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms: создание и заполнение нового


документа Microsoft Word

Что представляет из себя полная автоматизация документа? Заполнение


документа и всех важных полей, например фамилий, дат и названий. Разумеется, для
редактирования правописания или более глубокого изменения документа надо
использовать сам Word. Автоматизация части документа подходит в основном для
рутинного заполнения однотипных документов, будь то всякие бланки или ведомости...
Применений можно найти массу.
Первое, что может сделать стороннее приложение, это создать полностью
готовый документ получая данные, непосредственно из приложения, а следовательно и
из базы данных или с удалённого сервера. Рассмотрим такой случай.

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP12Main


^ Поменяем внутреннее имя формы.

350
Text изменим с Form1 на Автоматизация
Microsoft Office Word (C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 500;
315
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя, СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Также, разместим первую группу элементов для работы с автоматическим


созданием нового документа. Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения и расстановка первой группы


элементов

Здесь у нас семь кнопок Button, один GroupBox, один OpenFileDialog и один
NumericUpDown.

По порядку распишем Свойства каждого элемента:

Button:
(Name): B_Open

351
Text: Открыть Word, создать документ
Size: 200; 23
Button:
(Name): B_Page_1
Text: Страница № 1
Size: 100; 23
Button:
(Name): B_Page_2
Text: Страница № 2
Size: 200; 23
GroupBox:
(Name): GB_1
Text: Автоматическое создание и заполнение
нового документа
Button:
(Name): B_Print
Text: Печать
Size: 100; 23
Button:
(Name): B_Scale
Text: Масштаб
Size: 100; 23
Button:
(Name): B_Numeric
Text: Пронумеровать страницы
Size: 200; 23
Button:
(Name): B_Picture
Text: Вставить рисунок
Size: 200; 23
NumericUpDow:
(Name): NUD_Scale
Text: Increment
Size: 25
Minimum: 25
Maximum: 200
OpenFileDialog:
(Name): OFD_Picture
Filter: Файлы JPEG|*.jpg
DefaultExt: jpg
FileName: Рисунок для Word

Также, нам необходимо добавить связь приложения и библиотеку компонентов и


объектов самого Word. Для этого выполним следующее: Проект -> Добавить ссылку.
В открывшемся окне выбираем COM, далее ищем Microsoft Word 14.0 Object Library
(для Word 2010) и Microsoft Word 12.0 Object Library (для Word 2007):

352
Рис. 3. 2. Добавить ссылку: добавление библиотеки компонентов от Word 2010

В случае наличия на «машине» только Word 2007, при сборке приложения


необходимо добавить библиотеку «12-ой» версии. Если же установлено две версии
(2007 и 2010), то необходимо добавлять библиотеку той версии, что является активной
в данный момент (при запуске не производит действий по перенастройке пакета
установки).

Теперь отправляемся в код формы (правая кнопка мыши на значке формы, далее
Перейти к коду или нажмём на клавишу F7).

Найдём:

public partial class LWP12Main : Form


{

Добавим после:

object oMissing = System.Reflection.Missing.Value;


object oEndOfDoc = "\\endofdoc"; // \endofdoc - предустановленная закладка
// Запускаем Word и создаём новый документ
Word._Application oWord;
Word._Document oDoc;

Изменим следующую функцию LWP12Main():

public LWP12Main()
{
InitializeComponent();
B_Page_1.Enabled = false;
B_Page_2.Enabled = false;
B_Print.Enabled = false;
B_Number.Enabled = false;
B_Scale.Enabled = false;
B_Picture.Enabled = false;
}

Событие Click кнопки B_Open («Открыть Word, создать документ»):

353
private void B_Open_Click(object sender, EventArgs e)
{
B_Open.Enabled = false;
oWord = new Word.Application(); // Запускаем Word
oWord.Visible = true; // Делаем окно Word видимым
// Старый способ: здесь отражено наличие ключевого слова ref и параметра
oMissing, которые можно не использовать
oDoc = oWord.Documents.Add(ref oMissing, ref oMissing, ref oMissing, ref
oMissing); // Создаём новый документ
B_Page_1.Enabled = true;
B_Print.Enabled = true;
B_Number.Enabled = true;
B_Scale.Enabled = true;
B_Picture.Enabled = true;
}

Событие Click кнопки B_Page_1 («Страница № 1»):

private void B_Page_1_Click(object sender, EventArgs e)


{
// Вставка текста в начало документа и отступа после
Word.Paragraph oPara1;
// Стары способ
//oPara1 = oDoc.Content.Paragraphs.Add(ref oMissing);
oPara1 = oDoc.Content.Paragraphs.Add();
oPara1.Range.Text = "Заголовок № 1 с тенью";
oPara1.Range.Font.Size = 20; // Размер шрифта: 20
oPara1.Range.Font.Shadow = 1; // Тенью от шрифта
oPara1.Range.Font.Bold = 1; // "Жирный" шрифт
oPara1.Format.SpaceAfter = 24; // 24 пт.: оступ после параграфа
oPara1.Range.InsertParagraphAfter();
oPara1.Range.Font.Size = 12; // Размер шрифта: 12
oPara1.Range.Font.Shadow = 0; // Тенью от шрифта: выключаем

// Вставка текста и отступа после (для последующих частей документа)


Word.Paragraph oPara2;
object oRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
oPara2 = oDoc.Content.Paragraphs.Add(ref oRng);
oPara2.Range.Text = "Заголовок № 2";
oPara2.Format.SpaceAfter = 6; // Отступ после
oPara2.Range.InsertParagraphAfter();

// Вставка текста
Word.Paragraph oPara3;
oRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
oPara3 = oDoc.Content.Paragraphs.Add(ref oRng);
oPara3.Range.Text = "Обычный текст. Дальше идёт таблица:";
oPara3.Range.Font.Bold = 0;
oPara3.Format.SpaceAfter = 24;
oPara3.Range.InsertParagraphAfter();

// Вставка таблицы 3 на 5, заполнение данными, и изменение первой строки:


"жирный" и "курсив".
Word.Table oTable;
Word.Range wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
oTable = oDoc.Tables.Add(wrdRng, 3, 5); // 3 строки, 5 столбцов (Add требует
5 параметров, но мы записываем без двух последних параметров oMissing)
oTable.Range.ParagraphFormat.SpaceAfter = 6;
int r, c;
string strText;

for (r = 1; r <= 3; r++) // Заполняем строки


{

354
for (c = 1; c <= 5; c++) // Заполняем столбцы
{
strText = "r" + r + "c" + c;
oTable.Cell(r, c).Range.Text = strText;
}
oTable.Rows[1].Range.Font.Bold = 1; // Меняем стиль первой строки:
"жирный"
oTable.Rows[1].Range.Font.Italic = 1; // Меняем стиль первой строки:
"курсив"
}

// Вставка текста после таблицы


Word.Paragraph oPara4;
oRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
oPara4 = oDoc.Content.Paragraphs.Add(ref oRng);
oPara4.Range.InsertParagraphBefore(); // Вставка отступ до с параметром 24
пт. (подтягиваем из oPara3 по умолчанию)
oPara4.Range.Text = "Вставляем другую таблицу:";
oPara4.Format.SpaceAfter = 24;
oPara4.Range.InsertParagraphAfter(); // Вставка оступа после с параметром 24
пт.

// Вставка таблицы 5 на 2, заполнение данными, и изменение размера ширины


столбцов
wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
oTable = oDoc.Tables.Add(wrdRng, 5, 2);
oTable.Range.ParagraphFormat.SpaceAfter = 6;

for (r = 1; r <= 5; r++)


{

for (c = 1; c <= 2; c++)


{
strText = "r" + r + "c" + c;
oTable.Cell(r, c).Range.Text = strText;
}
oTable.Columns[1].Width = oWord.InchesToPoints(2); // Изменение ширины
столбца 1
oTable.Columns[2].Width = oWord.InchesToPoints(3); // Изменение ширины
столбца 2
}

// Продолжаем вставку текста. Когда получим 7 дюймов от верхней части


документа, вставим жёсткий разрыв страницы (иммитация нажатия Ctrl+Enter в документе)
object oPos;
double dPos = oWord.InchesToPoints(7);
oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range.InsertParagraphAfter();

do
{
wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
wrdRng.ParagraphFormat.SpaceAfter = 6;
wrdRng.InsertAfter("Текстовая строка");
wrdRng.InsertParagraphAfter();
oPos =
wrdRng.get_Information(Word.WdInformation.wdVerticalPositionRelativeToPage); // Вставляем
текст и получаем после каждой вставки информацию о положении на странице
}
while (dPos >= Convert.ToDouble(oPos));
// Вставка разрыва страницы
object oCollapseEnd = Word.WdCollapseDirection.wdCollapseEnd;
object oPageBreak = Word.WdBreakType.wdPageBreak;
wrdRng.Collapse(ref oCollapseEnd);
wrdRng.InsertBreak(ref oPageBreak);

355
wrdRng.Collapse(ref oCollapseEnd);
B_Page_2.Enabled = true;
}

Событие Click кнопки B_Page_2 («Страница № 2»):

private void B_Page_2_Click(object sender, EventArgs e)


{
Word.Range wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
wrdRng.InsertAfter("Текст на странице № 2. График:");
wrdRng.InsertParagraphAfter();

// Вставка диаграммы (одной из демонстрационных) из MSGraph


Word.InlineShape oShape; // Поле под диаграмму
object oClassType = "MSGraph.Chart.8"; // Тип диаграммы по умолчанию
wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
oShape = wrdRng.InlineShapes.AddOLEObject(ref oClassType); // Вставка
"пустой" (без парметров) диаграммы

// oChart и oChartApp объекты: работа с объектами диаграмм через MSGraph


object oChart;
object oChartApp;
oChart = oShape.OLEFormat.Object;
oChartApp = oChart.GetType().InvokeMember("Application",
BindingFlags.GetProperty, null, oChart, null);

// Изменение типа диаграммы (линии)


object[] Parameters = new Object[1];
Parameters[0] = 4; // xlLine = 4
oChart.GetType().InvokeMember("ChartType", BindingFlags.SetProperty, null,
oChart, Parameters);

// Изменение рисунка диаграммы и выход из MSGraph


oChartApp.GetType().InvokeMember("Update", BindingFlags.InvokeMethod, null,
oChartApp, null);
oChartApp.GetType().InvokeMember("Quit", BindingFlags.InvokeMethod, null,
oChartApp, null);
// После добавления диаграммы, её можно менять как полноценный объект
диаграмм MSGraph
// Отсюда, из документа, можно перейти из диаграммы в документе в Microsoft
Graph и внести дополнительные изменения в диаграмме

oShape.Width = oWord.InchesToPoints(6.25f); // Изменение ширины диаграммы


oShape.Height = oWord.InchesToPoints(3.57f); // Изменение высоты диаграммы

// Вставка текста без последующего отступа


wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
wrdRng.InsertParagraphAfter();
wrdRng.InsertAfter("Конец.");
}

Событие Click кнопки B_Print («Печать»):

private void B_Print_Click(object sender, EventArgs e)


{
Object time = 10000;
oWord.Dialogs[Word.WdWordDialog.wdDialogFilePrint].Show(ref time);
}

Многие действия в Word сопровождаются вызовом диалоговых окон (создание,


открытие, сохранение и печать документа, изменение параметров шрифта, текста и
прочее).

356
Допустим, необходимо предоставить пользователю возможность отправить
документ на печать прямо из вашего приложения. Можно использовать функцию
Document.PrintOut, но у неё «устрашающее» количество параметров, как и у
большинства функций связи C# с COM объектами.
Решением этой проблемы может быть использование стандартных диалоговых
окон Word, привычных пользователю и позволяющих сократить разработчику
программный код.

Dialogs — это коллекция диалоговых окон Word (всего более 238 элементов
класса Microsoft.Office.Interop.Word.Dialog). Для того, что бы посмотреть все
доступные диалоговые окна, кликните правой кнопкой мышки на WdWordDialog и
выберите пункт меню Перейти к определению (F12):

Элементы доступны по своим именам и имеют понятные названия, например:

wdDialogFileNew — Новый документ;


wdDialogFileOpen — Открыть документ;
wdDialogFilePageSetup — Параметры страницы;
wdDialogFilePrint — Печать;
wdDialogFileSaveAs — Сохранить документ;
wdDialogFormatFont — Параметры шрифта.

Определение функции Dialog.Show в Microsoft.Office.Interop.Word.dll:

[DispId(336)]
int Show(ref object TimeOut = Type.Missing);

Параметр: «TimeOut» — время в миллисекундах, через которое окно будет


закрыто автоматически. Практика показывает, что некоторые виды окон игнорируют
это время и сами не закрываются. Это относится к сохранению, открытию, созданию
нового документа... Окна для указания параметров «печати, параметров страницы,
шрифта....» закрываются по истечении указанного времени с нажатием «ОК» в этих
окнах автоматически.
Значение, которое возвращает функция Show, зависит от вида окна и от того,
как оно было закрыто. Если вышло время, или пользователь нажал «OK», или
«Закрыть», то «-1», если «Отмена», то «0», если «По умолчанию», то «1». А вот если
окно закрыто крестиком, то может быть «-1» или «-2», зависит от самого окна.
Обратим внимание на то, что попытка вызвать диалоговое окно, которое не
может быть вызвано (например, сохранение, если пока не создано ни одного
документа) приводит к возникновению исключительной ситуации.
Заметим также, что при Object time = 0 диалоговое окно появляется на не
установленное время, иначе, программа будет ожидать действий пользователя с
диалоговым окном.

357
Событие Click кнопки B_Scale («Масштаб»):

private void B_Scale_Click(object sender, EventArgs e)


{
oWord.ActiveWindow.ActivePane.View.Zoom.Percentage =
Convert.ToInt32(NUD_Scale.Value);
}

Функция для нумерации страниц и событие Click кнопки B_Number


(«Пронумеровать страницы»):

// Метод пронумеровки страниц


public void vInsertNumberPages(int viWhere, bool bPageFirst)
{
object alignment = Word.WdPageNumberAlignment.wdAlignPageNumberCenter;
object bFirstPage = bPageFirst;
object bF = true;
// Создаём колонтитулы
oDoc.ActiveWindow.ActivePane.View.SeekView =
Word.WdSeekView.wdSeekCurrentPageFooter; // Нижние колонтитулы страниц

switch (viWhere)
{
case 1:
alignment = Word.WdPageNumberAlignment.wdAlignPageNumberRight;
break;
case 2:
alignment = Word.WdPageNumberAlignment.wdAlignPageNumberLeft;
break;
}
oWord.Selection.HeaderFooter.PageNumbers.Add(ref alignment, ref bFirstPage);
}

private void B_Number_Click(object sender, EventArgs e)


{
vInsertNumberPages(1, true); // Цифра номера справа (1), с первой страницы
(true); 0 – по центру
}

Событие Click кнопки B_Picture («Вставить рисунок»):

private void B_Picture_Click(object sender, EventArgs e)


{
OFD_Picture.ShowDialog();
oWord.Selection.InlineShapes.AddPicture(OFD_Picture.FileName, ref oMissing,
ref oMissing, ref oMissing);
}

Компилируем приложение (Debug) и запускаем. Нажимаем на «Открыть


Word, ...», должно открыть окно Word (текущей версии, в данном случае Word 2010) с
чистым документом. Активируется кнопка Страница № 1. Жмём. Видим следующее:

358
Рис. 3. 3. Окончательная работа первого блока: заполнение первой страницы
документа Word (в Word 2010)

Затем жмём на кнопку Страница № 2 и наслаждаемся заполненным документом.

На рисунках 3. 3 и 3. 4. Включено отображение знаков форматирования ( , в


Word’е).

359
Рис. 3. 4. Окончательная работа первого блока: заполнение второй страницы
документа Word (в Word 2010)

360
Рис. 3. 5. Окончательная работа первого блока: нумерация страниц, вставка рисунка
(на позицию курсора!) и масштаб в 50% в Word (в Word 2010)

Проверяем работу других кнопок. Меняем масштаб отображения документа,


вставляем нумерацию страниц и рисунок.

Небольшое замечание по поводу вставки изображения. Наш код вставляет


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

private void B_Picture_Click(object sender, EventArgs e)


{
OFD_Picture.ShowDialog();

361
// Вставит изображение на место выделения (если не перемещать выделение, то в
начало документа)
oWord.Selection.InlineShapes.AddPicture(OFD_Picture.FileName);
Image newImage = Image.FromFile(OFD_Picture.FileName);
Clipboard.SetImage(newImage);
Word.Range wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
// Вставит изображение из буфера обмена в конец документа
wrdRng.Paste();
}

Исполнение кода затирает буфер обмена! Можно прибегнуть к методу


сохранения текущего буфера отдельно и восстановление после операции вставки
изображения:

private void B_Picture_Click(object sender, EventArgs e)


{

if (OFD_Picture.ShowDialog() == DialogResult.OK)
{
// Вставит изображение на место выделения (если не перемещать выделение,
то в начало документа)
oWord.Selection.InlineShapes.AddPicture(OFD_Picture.FileName);
Image newImage = Image.FromFile(OFD_Picture.FileName);
// Получаем данные из буфера обмена
IDataObject iData;
iData = Clipboard.GetDataObject();
//MessageBox.Show(iData.GetData(DataFormats.Text).ToString());
// Заносим туда изображение
Clipboard.SetImage(newImage);
Word.Range wrdRng = oDoc.Bookmarks.get_Item(ref oEndOfDoc).Range;
// Вставит изображение из буфера обмена в конец документа
wrdRng.Paste();
// Восстанавливаем буфер
Clipboard.SetDataObject(iData);
}
}

Также код выше обезопасит приложение от ошибки, возникающей, в случае если


пользователь не выбрал файла изображения в окне диалога.

Пример выполнения работы кода:

362
Рис. 3. 6. Окончательная работа первого блока: нумерация страниц, вставка рисунков в
начало (на позицию курсора) и в конец документа и масштаб в 50% в Word (в Word
2010)

4. Модификация приложения Windows Forms: работа с шаблонами

Для работы с шаблоном документа Word, необходим собственно сам шаблон.


Создаём его. Для этого запускаем Microsoft Word 2010 и создаём новый документ.
Теперь, изменим верхний колонтитул по своему вкусу:

Рис. 4. 1. Меняем стиль верхнего колонтитула шаблона документа

Этот стиль обеспечит два поля. Одно для названия, другое для даты. Выберем
дату:

363
Теперь, необходимо связать два поля с приложения. Для этого будем
использовать «Закладки». По имени закладки на какой-либо можно будет отыскать
нужное текстовое поле в этом шаблоне. Сделаем закладку из текстового поля
Название. Выделим текст в фиолетовом поле:

Теперь выполним: Вставка -> Закладка:

Далее вводим Имя закладки: Top_Name жмём Добавить:

364
Для даты правее сделаем закладку с именем Date_Now:

365
Закрываем колонтитул:

Теперь заполним нижний колонтитул по своему вкусу. Например так:

Рис. 4. 2. Меняем стиль нижнего колонтитула шаблона документа

366
Удалим номер страницы, оставив только текст слева под чертой. Выделим этот
текст и сделаем из него новую закладку с именем Bottom_Name:

Закрываем колонтитул. Теперь нарисуем небольшую таблицу 3 на 4 клетки (3


столбца, 4 строки).

Рис. 4. 3. Создаём таблицу 3 на 4

Оформим её любым способом. Например, так и заполним:

Далее можно было бы поставить шесть закладок, задать им уникальные имена и


искать их как в предыдущем случае, но это слишком накладно. Можно поступить

367
проще: выделим всю таблицу и добавим закладку на всё выделение сразу. Выберем
любое поле таблицы далее жмём правую кнопку мыши -> Выделить -> Таблица:

Назовём новую закладку на основе таблицы: Table:

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

Рис. 4. 4. Создаём диаграмму

Выберем Гистограмма -> Объёмная цилиндрическая:

368
Рис. 4. 5. Выбор типа диаграммы

Откроется окно Microsoft Excel 2010 с параметрами диаграммы. Потянем за


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

Рис. 4. 6. Изменение области диапазона диаграммы

Далее вставим в область 3 на 4 (А1-D3) данные нашей таблицы, удалим лишние


данные из ненужных ячеек и в пустые ячейки самой таблицы поставим нули:

369
Закроем лист Excel. Теперь присвоим диаграмме в Word имя Graph (закладка на
всю таблицу).

Рис. 4. 7. Добавление имени для диаграммы

Сохраняем шаблон в произвольном месте диска под именем Шаблон: Файл ->
Сохранить как -> Шаблон Word:

Необходимо добавить наш шаблон в проект приложения, чтобы при компиляции


этот шаблон был «под рукой». Выполним: Проект -> Существующий элемент...
(Shift+Alt+A). Выберем Все файлы (*.*) в окне добавления и укажем путь к нашему
шаблону (Шаблон.dotx). Свойства добавленного файла:

370
Изменим: Копировать в выходной каталог: Всегда копировать.

Разместим вторую группу элементов как показано на рисунке ниже:

Рис. 4. 8. Модифицированная форма приложения и расстановка второй группы


элементов

Здесь у нас два TextBox, два элемента Button, один GroupBox и DataGridView.
Свойства задаём такие:

Button:
(Name): B_Save
Text: Сохранить документ Word
Size: 200; 23
Button:
(Name): B_Exit
Text: Выгрузить Word
Size: 200; 23

371
GroupBox:
(Name): GB_2
Text: Автоматическое заполнение шаблона
документа
TextBox:
(Name): TB_Name_1
Text: Введите верхнее имя
Size: 200; 23
TextBox:
(Name): TB_Name_2
Text: Введите нижнее имя
Size: 200; 23
DataGridView:
(Name): DGV_Table

События MouseEnter и MouseLeave верхнего TextBox:

private void TB_Name_1_MouseEnter(object sender, EventArgs e)


{
TB_Name_1.Text = "";
}

private void TB_Name_1_MouseLeave(object sender, EventArgs e)


{
if (TB_Name_1.Text == "")
{
TB_Name_1.Text = "Введите верхнее имя";
}
}

События MouseEnter и MouseLeave нижнего TextBox:

private void TB_Name_2_MouseEnter(object sender, EventArgs e)


{
TB_Name_2.Text = "";
}

private void TB_Name_2_MouseLeave(object sender, EventArgs e)


{
if (TB_Name_2.Text == "")
{
TB_Name_2.Text = "Введите нижнее имя";
}
}

События Click кнопки Сохранить документ Word:

private void B_Save_Click(object sender, EventArgs e)


{
B_Save.Enabled = false;
oWordTemplate = new Word.Application();
Word._Document oDocTemplate = GetDoc(Environment.CurrentDirectory + "\\
Шаблон.dotx"); // Получаем шаблон документа
oDocTemplate.SaveAs(FileName: Environment.CurrentDirectory + "\\Новый
документ.docx"); // Сохраняем изменённый шаблон с новыми данными из приложения
oDocTemplate.Close(); // Закрываем документ (Word при этом не выгружается)
}

События Click кнопки Выгрузить Word:

372
private void B_Exit_Click(object sender, EventArgs e)
{
// Выходим из всех открытых приложением окон Word
try
{
B_Save.Enabled = true;
oWordTemplate.Quit();
}
catch { }

try
{
B_Open.Enabled = true;
B_Page_1.Enabled = false;
B_Page_2.Enabled = false;
B_Print.Enabled = false;
B_Number.Enabled = false;
B_Scale.Enabled = false;
B_Picture.Enabled = false;
oWord.Quit();
}
catch { }
}

Найдём в файле LWP12Main.cs строчку:

// Запускаем Word и создаём новый документ


Word._Application oWord;
Word._Document oDoc;

Добавим после:

Word._Application oWordTemplate;
DataTable T;

В начало файла добавим:

using Excel = Microsoft.Office.Interop.Excel; // Для графиков

Найдём:

InitializeComponent();
...
B_Picture.Enabled = false;

Добавим после:

// Формируем таблицу для заполнения DataGridView


T = new DataTable();
T.Columns.Add();
T.Columns.Add();
T.Columns.Add();
var row = T.NewRow();
row[0] = "Главное поле";
row[1] = "Параметры № 1";
row[2] = "Параметры № 2";
T.Rows.Add(row);
T.Rows.Add(T.NewRow()[0] = "Значение № 1");
T.Rows.Add(T.NewRow()[0] = "Значение № 2");
T.Rows.Add(T.NewRow()[0] = "Значение № 3");
DGV_Table.DataSource = T;

373
Необходимые для работы функции выполняющие действия по изменению двух
надписей, таблицы и диаграммы:

private Word._Document GetDoc(string path)


{
Word._Document oDocTemplate = oWordTemplate.Documents.Add(path);
SetTemplate(oDocTemplate); // Вызываем метод изменяющий шаблон
return oDocTemplate;
}

private void SetTemplate(dynamic oDocTemplate)


{

try
{
oDocTemplate.Bookmarks["Top_Name"].Range.Text = TB_Name_1.Text; //
Заполняем поле вверху документа
oDocTemplate.Bookmarks["Bottom_Name"].Range.Text = TB_Name_2.Text; //
Заполняем поле внизу документа
oDocTemplate.Bookmarks["Date_Now"].Range.Text =
DateTime.Now.Date.ToLocalTime();
SetTable(oDocTemplate, "Table", T); // Заполняем таблицу
SetChart(oDocTemplate, "Graph", T); // Заполняем график
}
catch { }
}
// Метод, заполняющий данными нашу таблицу в шаблоне
private void SetTable(Word._Document oDocTemplate, string bookmark, DataTable
dataContext)
{
dynamic tbl = oDocTemplate.Bookmarks[bookmark].Range.Tables[1];
int tblRow = 0;
int tblCell = 0;

foreach (Word.Column col in tbl.Columns)


{

foreach (Word.Cell cell in col.Cells)


{

try
{
SetCell(cell, (string)dataContext.Rows[tblRow][tblCell]); //
Вызываем метод SetCell() для проверки числе внутри ячеек и пометки их цветом
}
catch { } // Ловим пустые ячейки
tblRow++;
}
tblCell++;
tblRow = 0;
}
}
// Помечаем ячейки таблицы цветами. Красным цветом те ячейки, где значения меньше
10, зелёным те ячейки, где значения больше 100
private void SetCell(Word.Cell cell, string text)
{
int val;

if (int.TryParse(text, out val))


{
if (val < 10) cell.Shading.BackgroundPatternColor =
Word.WdColor.wdColorRed;
if (val > 100) cell.Shading.BackgroundPatternColor =
Word.WdColor.wdColorLightGreen;
}

374
cell.Range.Text = text;
}
// Метод, заполняющий данными график из таблицы в приложении
private void SetChart(Word._Document oDocTemplate, string bookmark, DataTable
dataContext)
{
Word.Chart chart =
oDocTemplate.Bookmarks[bookmark].Range.InlineShapes[1].Chart; // Получаем диаграмму
Word.ChartData chartData = chart.ChartData; // Переводим данные диаграммы
chartData.Activate();

Excel.Workbook dataWorkbook = (Excel.Workbook)chartData.Workbook; // Создаём


новую книгу Excel на основе диаграммы
Excel.Worksheet dataSheet = (Excel.Worksheet)dataWorkbook.Worksheets[1]; //
Выбиаем данные для заполнения диаграммы
dataSheet.Cells.Range["B2"].FormulaR1C1 = dataContext.Rows[1][1]; //
Сохраняем данные непосредственно в ячейки книги Excel из DataTable
dataSheet.Cells.Range["B3"].FormulaR1C1 = dataContext.Rows[1][2];
dataSheet.Cells.Range["C2"].FormulaR1C1 = dataContext.Rows[2][1];
dataSheet.Cells.Range["C3"].FormulaR1C1 = dataContext.Rows[2][2];
dataSheet.Cells.Range["D2"].FormulaR1C1 = dataContext.Rows[3][1];
dataSheet.Cells.Range["D3"].FormulaR1C1 = dataContext.Rows[3][2];
dataWorkbook.Close(); // Закрываем книгу Excel
}

Готово. Можно компилировать и проверять работоспособность.

5. Завершающая часть

Компилируем приложение (Release) и запускаем. Заполняем все поля. В ячейки


таблицы DataGridView вводим числовые значения, а в текстовые поля вводим
произвольные данные:

Рис. 5. 1. Модифицированное приложение Windows Forms: заполняем форму

Далее жмём на Сохранить документ Word. Результат работы показан ниже (Рис.
5. 2):

375
Рис. 5. 2. Модифицированное приложение Windows Forms: результат изменения
шаблона (Новый документ.docx)

Жмём Выгрузить Word для того чтобы выгрузить процесс WINWORD.EXE из


списка процессов (будет выгружен процесс, который был создан нашим приложением).
Наше приложение «защищено» (защита висит на кнопках) и максимально загружает
два экземпляра приложения Word, которые и будут выгружены.

376
Немного о работоспособности приложения:

Окончательная работоспособность приложения на версии ниже Word 2010 будет


зависеть от «состояния» самого Word (полной/правильной установки компонентов и
наличия необходимых обновлений). Приложение было протестировано на нескольких
машинах с различными сочетаниями ОС и Office. Полная работоспособность
обеспечивалась на ПК с Windows 7 (Service Pack 1)/Office 2010 и Windows 7/Office
2007.
Также, из-за некорректной установки Microsoft Office (переустановки и
обновления на новую более версию) возможно полное отсутствие функциональности
разрабатываемого приложения. Во время написания приложения индикатором
неработоспособности Word будет специфическая ошибка, сообщающая об отсутствии
библиотеки Microsoft Word 1X.0 Object Library, и сопутствующие сообщения Visual
Studio об отсутствии пространства имён Microsoft.Office.Interop.Word (и как следствие,
всего, что включает в себя это пространство имён). Возможное решение проблемы:
полное удаление Microsoft Office с ПК и последующая переустановка. Лучше также
установить все вышедшие на данный момент обновления.

6. О приложении к Лабораторной работе № 12

Получившуюся программу (LWP12Word.exe), собранную из кусков кода


приведённых в данной лабораторной работе, а также шаблон-файл (Шаблон.dotx),
использованный в данной работе можно загрузить по ссылке в конце этого материала
(сслыка доступна в программном продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

13. Лабораторная работа № 13: Автоматизация Microsoft Office Excel

Лабораторная работа № 13: Автоматизация Microsoft Office Excel

Содержание

1. Вводная часть
2. Создание приложения Windows Forms
3. Модификация приложения Windows Forms: создание и заполнение
нового документа Microsoft Excel
4. Модификация приложения Windows Forms: чтение данных с документа
Microsoft Excel
5. Модификация приложения Windows Forms: прочие возможности
Microsoft Excel
6. Завершающая часть
7. О приложении к Лабораторной работе № 13

1. Вводная часть

В этой работе будет рассмотрена работа с программой, входящей в состав


известного пакета программ Microsoft Office. В частности, речь пойдёт о Microsoft
Excel и некоторых возможностях по автоматизации работы с ним. Для демонстрации

377
работы будет использована версия Excel 2010. В данной работе будет рассмотрено
следующее:
1. Автоматическое создание и заполнение нового документа.
2. Открытие и изменение документа.
3. Чтение данных из документа Excel.
4. Сохранение документа Excel.
5. Установка парольной защиты на документ Excel.
6. Автоматическое заполнение документа Excel (функции автоматического
заполнения ячеек Excel).
7. Объединение ячеек на листе документа Excel.
8. Создание графиков и диаграмм при помощи «Мастера диаграмм».
9. Работа с приложением, книгами и листами документа Excel.
10. Некоторые другие возможности.
На этих примерах будут продемонстрированы основные возможности работы с
Microsoft Excel при помощи сторонних приложений на C#.

2. Создание приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

378
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP13Excel — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

379
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

380
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

381
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms: создание и заполнение нового


документа Microsoft Excel

Что представляет из себя полная автоматизация документа? Заполнение


документа и всех важных полей, например фамилий, дат и названий. Разумеется, для
редактирования правописания или более глубокого изменения документа надо
использовать сам Word. Автоматизация части документа подходит в основном для
рутинного заполнения однотипных документов, будь то всякие бланки или ведомости...
Применений можно найти массу.
Первое, что может сделать стороннее приложение, это создать полностью
готовый документ получая данные, непосредственно из приложения, а следовательно и
из базы данных или с удалённого сервера. Рассмотрим такой случай.

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP13Main

382
^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Автоматизация
Microsoft Office Excel (C#)
^ Поменяем заголовок формы (то, что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 500;
400
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя, СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Также, поместим первую группу элементов для работы с автоматическим


созданием нового документа. Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения и расстановка первой группы


элементов

383
Здесь у нас две кнопки Button, один GroupBox, три TextBox, два
NumericUpDown и ToolTip.

По порядку распишем Свойства каждого элемента:

ToolTip:
(Name): Hint
Button:
(Name): B_Open
Text: Открыть Excel, создать документ
Size: 312; 23
Button:
(Name): B_Insert
Text: Вставить данные
Size: 130; 23
TextBox:
(Name): textBox1
TextBox:
(Name): textBox2
TextBox:
(Name): textBox3
GroupBox:
(Name): GB_Insert
Text: Вставка данных в Excel
NumericUpDown:
(Name): NUD_1
Minimum: 1
ToolTip на Hint: Строка
NumericUpDown:
(Name): NUD_2
Minimum: 1
ToolTip на Hint: Столбец

Также, нам необходимо добавить связь приложения и библиотеку компонентов и


объектов самого Excel. Для этого выполним следующее: Проект -> Добавить ссылку.
В открывшемся окне выбираем COM, далее ищем Microsoft Excel 14.0 Object Library
(для Excel 2010) и Microsoft Excel 12.0 Object Library (для Excel 2007):

384
Рис. 3. 2. Добавить ссылку: добавление библиотеки компонентов от Excel 2010

В случае наличия на «машине» только Excel 2007, при сборке приложения


необходимо добавить библиотеку «12-ой» версии. Если же установлено две версии
(2007 и 2010), то необходимо добавлять библиотеку той версии, что является активной
в данный момент (при запуске не производит действий по перенастройке пакета
установки).

Теперь отправляемся в код формы (правая кнопка мыши на значке формы, далее
Перейти к коду или нажмём на клавишу F7).

В начало файла LWP13Main.cs добавим:

using Excel = Microsoft.Office.Interop.Excel;

Найдём:

public partial class LWP13Main : Form


{

Добавим после:

Excel.Application ObjExcel1;
Excel.Workbook ObjWorkBook1;
Excel.Worksheet ObjWorkSheet1;

Изменим следующую функцию LWP13Main():

public LWP13Main()
{
InitializeComponent();
B_Insert.Enabled = false;
}

385
Событие Click кнопки B_Open («Открыть Excel, создать документ»):

private void B_Open_Click(object sender, EventArgs e)


{
// Запускаем Excel
ObjExcel1 = new Excel.Application();
// Создаём книгу
ObjWorkBook1 = ObjExcel1.Workbooks.Add();
// Создаём лист
ObjWorkSheet1 = (Excel.Worksheet)ObjWorkBook1.Sheets[1];
// Делаем Excel видимым
ObjExcel1.Visible = true;
ObjExcel1.UserControl = true;
}

Событие Click кнопки B_Insert («Вставить данные»):

private void B_Insert_Click(object sender, EventArgs e)


{
// Значения Cells[y - строка, x - столбец]
ObjWorkSheet1.Cells[NUD_1.Value, NUD_2.Value] = textBox1.Text;
ObjWorkSheet1.Cells[NUD_1.Value, NUD_2.Value + 1] = textBox2.Text;
ObjWorkSheet1.Cells[NUD_1.Value, NUD_2.Value + 2] = textBox3.Text;
}

Компилируем приложение (Debug) и запускаем. Нажимаем на «Открыть


Excel, ...», должно открыться окно Excel (текущей версии, в данном случае Excel 2010)
с чистым документом. Активируется кнопка Вставить данные. Заполняем три пустых
текстовых поля, а также «крутим» счётчики строки и столбца. Жмём. Видим
следующее:

Рис. 3. 3. Окончательная работа первого блока: заполнение листа документа Excel (в


Excel 2010)

386
4. Модификация приложения Windows Forms: чтение данных с документа
Microsoft Excel

Разместим вторую группу элементов как показано на рисунке ниже:

Рис. 4. 1. Модифицированная форма приложения и расстановка второй группы


элементов

Здесь у нас один RichTextBox, один Button, один GroupBox, TextBox и


OpenFileDialog. Свойства задаём такие:

Button:
(Name): B_Get
Text: Получить данные
Size: 130; 23
GroupBox:
(Name): GB_Get
Text: Получение данных из Excel
TextBox:
(Name): textBox4
ToolTip на Hint: Столбец
Size: 130; 23
RichTextBox:
(Name): richTextBox1
OpenFileDialog:
(Name): OFD_Get
FileName: Данные для извлечения
Filter: Файлы Excel или файлы Access|*.*

События Click кнопки Получить данные:

387
private void B_Get_Click(object sender, EventArgs e)
{
// Открываем файл Excel
if (OFD_Get.ShowDialog() == DialogResult.OK)
{
// Создаём приложение
Excel.Application ObjExcel2 = new Excel.Application();
// Открываем книгу
Excel.Workbook ObjWorkBook2 = ObjExcel2.Workbooks.Open(OFD_Get.FileName,
0, false, 5, "", "", false, Excel.XlPlatform.xlWindows, "", true, false, 0, true, false,
false);
// Выбираем таблицу (лист)
Excel.Worksheet ObjWorkSheet2;
ObjWorkSheet2 = (Excel.Worksheet)ObjWorkBook2.Sheets[1];
// Очищаем от старого текста окно вывода
richTextBox1.Clear();

for (int i = 1; i < 101; i++)


{
// Выбираем область таблицы (в нашем случае просто ячейку)
Excel.Range range = ObjWorkSheet2.get_Range(textBox4.Text +
i.ToString(), textBox4.Text + i.ToString());
// Добавляем полученный из ячейки текст
richTextBox1.Text = richTextBox1.Text + range.Text.ToString() + "\n";
// Чтобы форма прорисовывалась (не подвисала)
Application.DoEvents();
}
//Удаляем приложение (выходим из Excel)
ObjExcel2.Quit();
}
}

Найдём в файле LWP13Main.cs строчку:

InitializeComponent();
B_Insert.Enabled = false;

Добавим после:

textBox4.Text = "A";

Готово. Можно компилировать и проверять работоспособность.

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


столбцов. При открытии файла (базы данных Access или непосредственно книги Excel),
программа считывает все данные из одного конкретного столбца (зависит от буквы или
цифры в текстовом поле) и заносит всё в RichTexBox.

388
Рис. 4. 2. Окончательная работа второго блока: открытие базы данных Access и запрос
на выбор таблицы или запроса для открытия в Excel (в Excel 2010)

Рис. 4. 3. Окончательная работа второго блока: открытие таблицы базы данных через
Excel (в Excel 2010)

Жмём Не сохранять.

389
Рис. 4. 4. Окончательная работа второго блока: вывод данных столбца таблицы через
Excel (столбец А) в приложение (в Excel 2010)

О проблеме разных версий Microsoft Office (Excel):

Существует несколько вариантов работы с Excel из C#: автоматизация Excel,


подключение через OleDB/ODBC, дополнительные библиотеки (Aspose Excel), работа
через XML, через Open XML и прочее.

Наиболее простой вариант — воспользоваться автоматизацией Excel. Да,


скорость работы - не впечатляет. Зато удобно использовать, код пишется быстро,
объёмы кода не велики. Из .NET автоматизация подключается парой кликов мыши.
Достаточно добавить в Ссылки сборку Microsoft.Office.Interop.Excel и можно
работать с привычными объектами COM: Application, Workbook, Worksheet и прочее.
Проблема одна — сборки «Microsoft.Office.Interop.Excel» для каждой версии Excel
разные. Если установлен Office 2003 и, соответственно, Interop-сборка версии 11. А что
делать, если мне нужно разработать приложение, которое может работать с Excel 97?
Или с любыми версиями Excel? Для того, чтобы считать или записать пару значений в
ячейки листа Excel сложного API не требуется — любой Excel сгодится. Но приходится
привязываться к конкретной версии.
Можно воспользоваться поздним связыванием. Но если его использовать «в
лоб», то ни о каком удобстве работы речи уже не идёт — код станет сложным, а вызовы
методов — косвенными, не типизированными. Ошибок будет много.

На СodeProject (http://www.codeproject.org) существует статья


«SafeCOMWrapper - Managed Disposable Strongly Typed safe wrapper to late
bound COM». В ней изложена элегантная методика использования позднего
связывания, устраняющая все проблемы: связывание становится поздним (привязки к
конкретной версии Excel нет), для объектов автоматизации автоматически реализуется
шаблон IDisposable (отработанные объекты уничтожаются автоматически), все
вызовы методов явные.

390
Идея реализации, вкратце, следующая. Для каждого COM-объекта
автоматизации прописывается отдельный интерфейс. В интерфейс включаются все
методы и свойства, которые необходимо использовать у этого COM-объекта. Обратим
внимание — только те, которые необходимо использовать, а вовсе не все, реализуемые
COM-объектом.
Каждый COM-объект автоматизации «заворачивается» в класс COMWrapper,
унаследованный от RealProxy. RealProxy — это стандартный класс, позволяющий
организовать перехват вызовов методов. При создании COM-объекта создаетётся
экземпляр COMWrapper и указывается требуемый интерфейс. Среда динамически
генерирует прокси-объект, реализующий этот интерфейс. С этим прокси-объектом в
дальнейшем и нужно работаетть как с объектом автоматизации. Вызов любого метода
прокси-объетка перехватывается и транслируется в вызов метода Invoke класса
RealProxy, перекрытый в классе COMWrapper. Здесь его можно обработать как угодно.
В реализации по умолчанию, вызовы свойств транслируются в вызовы
соответствующих методов get_ и set_, создаваемых .NET, возвращаемые объекты
автоматизации автоматически заворачиваются в COMWrapper и прочее.

В оригинальной статье приведен пример использования данной методики для


Microsoft Outlook. Сам код подобной реализации из статьи можно загрузить по ссылке
в конце этого материала (сслыка доступна в программном продукте).

5. Модификация приложения Windows Forms: прочие возможности Microsoft


Excel

Богатства возможностей по управлению запущенным приложением потрясающе.


Практически, всё, что можно сделать в автономно запущенном приложении доступно и
из приложения на C#. Программист может достаточно легко выполнить то или иное
действие из приложения, если создаст макрос для этого действия, а, затем,
«переведёт» код VBA в коды C#.

Для нашего приложения, третья группа элементов продемонстрирует широкие


возможности по работе в Excel и документами.

Объекты, которыми оперирует COM-сервер Excel, несколько десятков. Будем


рассматривать лишь основные, которые непосредственно требуются для обмена
информацией приложения и сервера. Все объекты имеют иерархическую структуру.
Сам сервер — объект Application или приложение Excel, может содержать одну или
более книг, ссылки на которые содержит свойство Workbooks. Книги — объекты
Workbook, могут содержать одну или более страниц, ссылки на которые содержит
свойство Worksheets или (и) диаграмм — свойство Charts. Страницы — Worksheet,
содержать объекты ячейки или группы ячеек, ссылки на которые становятся
доступными через объект Range. Ниже в иерархии: строки, столбцы... Аналогично и
для объекта Chart серии линий, легенды...
Обратим внимание на то, что, интерфейс C# вместо понятия ячейки использует
объекты Range (выбранная ячейка или группа ячеек). Отметим также группу объектов
ActiveCell, ActiveChart и ActiveSheet, относящихся к активному окну
(расположенному поверх других). Они, по набору свойств и методов, полностью
аналогичны объектам Range, Chart и Sheet и, в ряде случаев, просто облегчают
получение ссылки.
Немного обособленно от этой иерархической структуры объектов находится
свойство Windows объекта Excel.Application, предназначенное для управления окнами
сервера Excel. Свойство Windows содержит набор объектов Window, которые имеют, в

391
свою очередь, набор свойств и методов для управления размерами, видом, масштабом
и упорядочиванием открытых окон, отображением заголовков, цветами и прочее. Эти
же возможности доступны и для свойств и методов объекта Excel.Application -
ActiveWindow (ссылка на активное окно).
Все эти объекты принято определять глобально для того, чтобы обеспечить
доступ к ним из любой функции проекта.

Вторым в иерархии объектов Excel.Application является объект Workbook.


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

Начнём с создания рабочих книг:

Создание двух рабочих книг из 3-х и 5-ти листов (на примере нашего
приложения):

Excel.Application ObjExcel;

public LWP13Main()
{
InitializeComponent();
ObjExcel = new Excel.Application();
ObjExcel.Visible = true;
ObjExcel.SheetsInNewWorkbook = 3;
ObjExcel.Workbooks.Add(Type.Missing);
ObjExcel.SheetsInNewWorkbook = 5;
ObjExcel.Workbooks.Add(Type.Missing);
}

Свойство SheetsInNewWorkbook возвращает или устанавливает количество


листов, автоматически помещаемых Excel в новые рабочие книги.

В качестве параметра методу Add можно передать имя шаблона рабочей книги,
однако, в этом случае мы привязываемся к пути, по которому инсталлированы
приложения Microsoft Office. В примере использован другой способ: Type — класс
декларации типов, Type.Missing — отсутствие значения. Некоторые методы Excel
принимают необязательные параметры, которые не поддерживаются в C#. Для
решения этой проблемы в коде на C# требуется передавать поле Type.Missing вместо
каждого необязательного параметра, который является ссылочным типом (reference
type). Кроме того, (этого нет в документации) при задании в методе ADD чисел от 1 до
7 будет создана книга с одним листом (1, 6), диаграмма (2), макрос (3, 4) и книга с
четырьмя листами (5).
Из других свойств отметим свойство TemplatesPath. С его помощью, зная имя
файла шаблона, можно напрямую задавать имя шаблона (правда, в этом нет
необходимости, если мы не хотим использовать, например, свой собственный шаблон).
Свойство StartupPath возвращает путь к папке, которая содержит надстройки,
выполняемые при запуске Excel и, хотя свойство для отображения информации нам
ничего не дает, все же порой бывает необходимо найти имя файла настроек и удалить
его для того, чтобы приложение работало только с собственными настройками.

Книги могут быть не только добавлены, но и закрыты. Следующие вызовы


закрывают все или конкретную рабочую книгу:

392
ObjExcel.Workbooks.Close();
ObjExcel.Windows[1].Close(false, Type.Missing, Type.Missing);

Подробнее:

ObjExcel.Windows[1].Close(
SaveChanges, // Если в книге нет никаких изменений в документе, то
параметр игнорируется.
// Иначе, если есть изменения, но есть ссылки на закрываемую
книгу
// в других открытых окнах - этот параметр также
игнорируется.
// При отсутствии ссылок и наличии изменений - этот параметр
// определяет, должны ли быть сохранены изменения.
// При true и определенном параметре Filename - изменения
// сохраняются, иначе запрашиваетcя имя файла. При false
сохранения
// нет. Если Type.Missing - вызывается диалоговое окно Save
As
Filename, // Имя файла
RouteWorkbook // Если файл не должен быть перенаправлен другому получателю
// этот параметр игнорируется. Иначе при true файл
направляется
// следующему получателю. При false пересылки нет
);

Теперь о сохранении документов:

Документы Excel можно сохранить программно и обычным для Excel способом. В


любом случае перед выходом из Excel необходимо вызвать метод Quit. Если свойство
Excel.Application.DisplayAlerts имеет значение true, Excel предложит сохранить
несохранённые данные, если после старта в документ были внесены какие либо
изменения. Excel автоматически не возвращает это свойство в значение по умолчанию,
поэтому его рекомендуется возвращать в исходное состояние.

Excel.Workbook ObjWorkBook;
Excel.Workbooks ObjWorkBooks;

public LWP13Main()
{
...
// Запрашивать сохранение
ObjExcel.DisplayAlerts = true;
// Получаем набор ссылок на объекты Workbook (на созданные книги)
ObjWorkBooks = ObjExcel.Workbooks;
// Получаем ссылку на книгу 1 - нумерация от 1
ObjWorkBook = ObjWorkBooks[1];
// Ссылку можно получить и так, но тогда надо знать имена книг,
// причём, после сохранения - знать расширение файла
// ObjWorkBook = ObjWorkBooks["Book 1"];
// Запроса на сохранение для книги не должно быть
ObjWorkBook.Saved = true;
// Используем свойство Count, число Workbook в Workbooks
if (ObjWorkBooks.Count > 1)
{
ObjWorkBook = ObjWorkBooks[2];
// Запрос на сохранение книги 2 должен быть
ObjWorkBook.Saved = false;
}
}

393
Теперь, если выйти на конкретную книгу, как показано в примере, приведенном
выше, и присвоить свойству Saved объекта Workbook значение true, Excel согласно
документации не должен предлагать сохранение независимо от того, были или нет
изменения в данной книге.

Для получения формата открываемого документа и задания формата


сохраняемого служит свойство Excel.Application.DefaultSaveFormat. Свойство имеет
много значений типа XlFileFormat (какие могут быть легко, посмотреть в диалоговом
окне «Сохранение документа» в поле «Тип файла», открыв Excel и выбрав пункт меню
«Файл» -> «Сохранить как»).

Например:

ObjExcel.DefaultSaveFormat = Excel.XlFileFormat.xlHtml;

В окне диалога сохранения файла будет установлен тип файла «Веб-страница».

Для сохранения документов можно использовать методы Excel.Workbook.Save


и SaveAs. Метод Save сохраняет рабочую книгу в папке по умолчанию (выбирается в
настройках Excel: «Файл» ->» Параметры «-> «Сохранение» -> «Расположение файлов
по умолчанию») с именами, присваиваемыми документу по умолчанию («Книга1.xls»,
«Книга2.xls» ...) или в директорию и с именем под которым документ уже был
сохранён.

Пример сохранения «по умолчанию»:

// Устанавливаем формат
ObjExcel.DefaultSaveFormat = Excel.XlFileFormat.xlExcel9795;
// Будем спрашивать разрешение на запись поверх существующего документа
ObjExcel.DisplayAlerts = true;
ObjWorkBook = ObjWorkBooks[1];
// Сохраняем книгу 1
ObjWorkBook.Save();
ObjWorkBook = ObjWorkBooks[2];
// Сохраняем книгу 2
ObjWorkBook.Save();

При значении свойства DisplayAlerts = true Excel будет спрашивать: записать


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

Метод SaveAs позволяет сохранить документ с указанием имени, формата файла,


пароля, режим доступа и прочее. Данный метод, как и метод Save, присваивает
свойству Saved значение true.

ObjWorkBook.SaveAs(
Filename, // Имя сохраняемого файла
FileFormat, // Формат сохраняемого файла
Password, // Пароль доступа к файлу до 15 символов
WriteResPassword, // Пароль на доступ на запись
ReadOnlyRecommended, // При true режим только для чтения
CreateBackup, // Создать резервную копию файла при true
AccessMode, // Режим доступа к рабочей книге
ConflictResolution, // Способ разрешения конфликтов
AddToMru, // При true сохраненный документ добавляется
// в список ранее открытых файлов

394
TextCodePage, // Кодовая страница
TextVisualLayout, // Направление размещения текста
Local // Идентификатор Excel.Application
);

Для доступа к книге используются значение AccessMode xlShared — общая


рабочая книга, xlExclusive — монопольный доступ или xlNoChange — запрет
изменения режима доступа.
Параметр ConflictResolution — способ разрешения конфликтов при
одновременном внесении несколькими пользователями изменений в один документ -
может иметь значения: xlUserResolution — отображение диалогового окна
разрешения конфликтов (параметр по умолчанию), xlLocalSessionChanges —
принятие изменений, внесенных пользователем или xlOtherSessionChanges —
принятие изменений, внесенных другими пользователями. О применении SaveAs будет
написано в рабочем коде приложения ниже.

Также, для сохранения документа может быть использован метод SaveCopyAs,


который сохраняет копию рабочей книги в файле.
Метод SaveCopyAs не производит преобразование документа и, поэтому,
например вместо Веб-страницы Book1.html сохранит копию *.xlsx (Excel 2007 и выше)
документа (изменит только расширение).

И наконец, об открытии документов:

Для открытия существующего документа основным методом является метод


Open набора Excel.Workbooks. Для открытия текстовых файлов как рабочих книг, баз
данных, файлов в формате *.xml, используются методы OpenText, OpenDatabase или
OpenXml.

ObjExcel.Workbooks.Open(@"C:\Документ.html",
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing);
ObjExcel.Workbooks.Open(@"C:\Документ.xlsx",
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing);

Параметров как видим очень много. Метод Open и его свойства таковы:

ObjWorkBooks.Open(
FileName, // Имя открываемого файла
UpdateLinks, // Способ обновления ссылок в файле
ReadOnly, // При значении true открытие только для
чтения
Format, // Определение формата символа разделителя
Password, // Пароль доступа к файлу до 15 символов
WriteResPassword, // Пароль на сохранение файла
IgnoreReadOnlyRecommended, // При значении true отключается вывод
// запроса на работу без внесения изменений
Origin, // Тип текстового файла
Delimiter, // Разделитель при Format = 6
Editable, // Используется только для надстроек Excel 4.0
Notify, // При значении true имя файла добавляется в
// список нотификации файлов

395
Converter, // Используется для передачи индекса
конвертера файла
// используемого для открытия файла
AddToMRU, // При true имя файла добавляется в список
// открытых файлов
Local, // Языковые параметры загрузки (true или
false)
CorruptLoad // Режим загрузки файла (всего 3 возможных)
);

UpdateLinks — позволяет задать способ обновления ссылок в файле. Если


данный параметр не задан, то выдается запрос на указание метода обновления.
Значения: 0 — не обновлять ссылки; 1 — обновлять внешние ссылки; 2 — обновлять
только удаленные ссылки; 3 — обновлять все ссылки.

Format — при работе с текстовыми файлами определяет символ разделителя для


полей, заносимых в различные ячейки документа. Значения параметра: 1 — символ
табуляции; 2 — запятая; 3 — пробел; 4 - точка с запятой; 5 — нет разделителя; 6 —
другой символ, определённый в параметре Delimiter.

Теперь перейдём к нашему приложению. Для начала подготовим третий блок


элементов управления на форме.

Рис. 5. 1. Модифицированная форма приложения и расстановка третьей группы


элементов

Здесь 7 кнопок и один GroupBox.

GroupBox:
(Name): GB_Other
Text: Прочие возможности работы с Excel

396
Button:
(Name): B_Create
Text: Сформировать готовый документ с
графиком
Size: 312; 23
Button:
(Name): B_Other
Text: Другие листы
Size: 100; 23
Button:
(Name): B_Action
Text: Выполнить различные действия
Size: 206; 23
Button:
(Name): B_Unfreeze
Text: Разморозить
Size: 100; 23
Button:
(Name): B_Merge
Text: Объединение
Size: 100; 23
Button:
(Name): B_Exit
Text: Выгрузить все открытые приложением
экземпляры Excel
Size: 312; 23
Button:
(Name): B_AutoFill
Text: Автозаполнение
Size: 100; 23

Начнём с самой первой кнопки (B_Create). Нажатие кнопки вызывает окно


Excel, далее создаётся книга с тремя листами, в ней формируются данные о продажах
для некоей фирмы, после чего приложение через Excel спрашивает, для скольких
кварталов за год вывести данные о продажах. После выбора числа кварталов (от
четырёх до одного), Excel строит график (на основе случайных данных для каждого
сотрудника фирмы), перемещает этот график на основной (первый) лист и сохраняет
документ под двумя паролями в директории с приложением (Новый документ.xslx).
Два пароля: первый пароль на доступ к документу («123»), второй на изменение
данных («321»).

Для работы кода нам потребуется следующее. Найдём в LWP13Main.cs строчки:

public partial class LWP13Main : Form


{
Excel.Application ObjExcel1;
Excel.Workbook ObjWorkBook1;
Excel.Worksheet ObjWorkSheet1;

Добавим после (с заделом на будущую функциональность):

//

397
Excel.Application ObjExcel3;
//
Excel.Application ObjExcel4;
Excel.Workbook ObjWorkBook4;
Excel.Workbooks ObjWorkBooks4;
Excel.Worksheet ObjWorkSheet4;
Excel.Sheets ObjSheets4;
Excel.Range ObjRange4;
//
Excel.Application ObjExcel5;
Excel.Workbook ObjWorkBook5;
Excel.Workbooks ObjWorkBooks5;
Excel.Worksheet ObjWorkSheet5;
Excel.Sheets ObjSheets5;
Excel.Range ObjRange5;
//
Excel.Application ObjExcel6;
Excel.Workbook ObjWorkBook6;
Excel.Workbooks ObjWorkBooks6;
Excel.Worksheet ObjWorkSheet6;
Excel.Sheets ObjSheets6;
Excel.Range ObjRange6;
//
Excel.Application ObjExcel7;
Excel.Workbook ObjWorkBook7;
Excel.Workbooks ObjWorkBooks7;
Excel.Worksheet ObjWorkSheet7;
Excel.Sheets ObjSheets7;
Excel.Range ObjRange7;

Код достаточно хорошо прокомментирован, событие Click нажатия кнопки


B_Create таково:

private void B_Create_Click(object sender, EventArgs e)


{
B_Create.Enabled = false;
B_Other.Enabled = true;
B_Merge.Enabled = true;
Excel._Workbook ObjWorkBook3;
Excel._Worksheet ObjWorkSheet3;
Excel.Range ObjRange3;

try
{
ObjExcel3 = new Excel.Application();
ObjExcel3.Visible = true;
// Задаём число листов в новом документе (Книге)
ObjExcel3.SheetsInNewWorkbook = 3;
ObjWorkBook3 = (Excel._Workbook)(ObjExcel3.Workbooks.Add(Type.Missing));
ObjWorkSheet3 = (Excel._Worksheet)ObjWorkBook3.ActiveSheet;
// Заполняем заголовочные ячейки
ObjWorkSheet3.Cells[1, 1] = "Имя";
ObjWorkSheet3.Cells[1, 2] = "Фамилия";
ObjWorkSheet3.Cells[1, 3] = "Полное имя";
ObjWorkSheet3.Cells[1, 4] = "Продажи";
// Форматируем A1:D1 как "жирный", вертикальное положение: по центру
ObjWorkSheet3.get_Range("A1", "D1").Font.Bold = true;
ObjWorkSheet3.get_Range("A1", "D1").VerticalAlignment =
Excel.XlVAlign.xlVAlignCenter;
// Создаём массив для заполнения ячеек данными (имена и фамилии)
string[,] saNames = new string[5, 2];
saNames[0, 0] = "Иван";
saNames[0, 1] = "Иванов";
saNames[1, 0] = "Антон";

398
saNames[1, 1] = "Антонов";
saNames[2, 0] = "Пётр";
saNames[2, 1] = "Петров";
saNames[3, 0] = "Андрей";
saNames[3, 1] = "Андреев";
saNames[4, 0] = "Кейв";
saNames[4, 1] = "Джонсон";
// Заполняем A2:B6 из массива ("Имя" и "Фамилия").
ObjWorkSheet3.get_Range("A2", "B6").Value2 = saNames;
// Заполняем C2:C6 по формуле (=A2 & " " & B2).
ObjRange3 = ObjWorkSheet3.get_Range("C2", "C6");
ObjRange3.Formula = "=A2 & \" \" & B2";
// Заполняем D2:D6 по формлуе (=RAND()*100000) и применяем формат
ObjRange3 = ObjWorkSheet3.get_Range("D2", "D6");
ObjRange3.Formula = "=RAND()*1000";
ObjRange3.NumberFormat = "0.00р";
// Автозаполнение A:D.
ObjRange3 = ObjWorkSheet3.get_Range("A1", "D1");
ObjRange3.EntireColumn.AutoFit();
// Манипулируем с переменным числом столбцов для квартальных
// данных продаж (вызываем метод DisplayQuarterlySales).
// Для построение графика будет использован ChartWizard
DisplayQuarterlySales(ObjWorkSheet3);
// Делаем Excel видимым и передаём управления пользователю
ObjExcel3.Visible = true;
ObjExcel3.UserControl = true;
}
catch (Exception ex)
{
String errorMessage;
errorMessage = "Ошибка: ";
errorMessage = String.Concat(errorMessage, ex.Message);
errorMessage = String.Concat(errorMessage, "\nЛиния: ");
errorMessage = String.Concat(errorMessage, ex.Source);
MessageBox.Show(errorMessage, "Автоматизация Microsoft Office Excel
(C#) :: Ошибка построения диаграммы");
}
ObjExcel3.Quit();
}

private void DisplayQuarterlySales(Excel._Worksheet ObjWorkSheet3)


{
Excel._Workbook ObjWorkBook3;
Excel.Series ObjSeries3;
Excel.Range ObjResizeRange3;
Excel._Chart ObjChart3;
String sMsg;
int iNumQtrs = 4;
// Определяем, сколько кварталов частей для отображения данных
for (iNumQtrs = 4; iNumQtrs >= 2; iNumQtrs--)
{
sMsg = "Ввод данных с продаж за ";
sMsg = String.Concat(sMsg, iNumQtrs);
sMsg = String.Concat(sMsg, " квартала(-л)?");
DialogResult iRet = MessageBox.Show(sMsg, "Сколько кварталов учитывать?",
MessageBoxButtons.YesNo);
if (iRet == DialogResult.Yes) break;
}
sMsg = "Вывод данных за ";
sMsg = String.Concat(sMsg, iNumQtrs);
sMsg = String.Concat(sMsg, " квартала(-л)");
MessageBox.Show(sMsg, "Данные о продажах за кварталы");
// Начиная с E1, заполнить заголовки (и далее по количество столбцов
выбранных столбцов)

399
ObjResizeRange3 = ObjWorkSheet3.get_Range("E1",
"E1").get_Resize(Missing.Value, iNumQtrs);
ObjResizeRange3.Formula = "=\"К\" & COLUMN()-4 & CHAR(10) & \"(квартал)\"";
// Меняем ориентацию текста (38) и параметр поворота текста (true)
ObjResizeRange3.Orientation = 38;
ObjResizeRange3.WrapText = true;
// Заполняем заголовки (верхнюю ячейку столбца) начиная с E1 жёлтым цветом
ObjResizeRange3.Interior.ColorIndex = 36;
// Заполняем E2:E6 формулами (случайное число до 100) и меняем формат
отображения (рубли)
ObjResizeRange3 = ObjWorkSheet3.get_Range("E2",
"E6").get_Resize(Missing.Value, iNumQtrs);
ObjResizeRange3.Formula = "=RAND()*100";
ObjResizeRange3.NumberFormat = "0.00р";
// Применяем к E1:E6 чёрные границы ячеек
ObjResizeRange3 = ObjWorkSheet3.get_Range("E1",
"E6").get_Resize(Missing.Value, iNumQtrs);
ObjResizeRange3.Borders.Weight = Excel.XlBorderWeight.xlThin;
// Для всех ячеек ниже E1 формируем сумму, под числом рисуем двойную черту
ObjResizeRange3 = ObjWorkSheet3.get_Range("E8",
"E8").get_Resize(Missing.Value, iNumQtrs);
ObjResizeRange3.Formula = "=SUM(E2:E6)";
ObjResizeRange3.Borders.get_Item(Excel.XlBordersIndex.xlEdgeBottom).LineStyle
= Excel.XlLineStyle.xlDouble;
ObjResizeRange3.Borders.get_Item(Excel.XlBordersIndex.xlEdgeBottom).Weight =
Excel.XlBorderWeight.xlThick;
// Создаём график для выбранных данных
ObjWorkBook3 = (Excel._Workbook)ObjWorkSheet3.Parent;
ObjChart3 = (Excel._Chart)ObjWorkBook3.Charts.Add(Missing.Value,
Missing.Value, Missing.Value, Missing.Value);
// Используем "Мастер диаграмм" чтобы создать новый график на основе
выбранных данных
ObjResizeRange3 = ObjWorkSheet3.get_Range("E2:E6",
Missing.Value).get_Resize(Missing.Value, iNumQtrs);
// Делаем график объёмной
ObjChart3.ChartWizard(Missing.Value, Excel.XlChartType.xl3DColumn,
Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value,
Missing.Value, Missing.Value, Missing.Value);
// Меняем подписи срава (по вертикали) на "Полное имя"
ObjSeries3 = (Excel.Series)ObjChart3.SeriesCollection(1);
// В цикле пробегаем по каждому элементу подписи по вертикали справа
for (int iRet = 1; iRet <= 5; iRet++)
{
ObjSeries3 = (Excel.Series)ObjChart3.SeriesCollection(iRet);
ObjSeries3.Name = ObjWorkSheet3.get_Range("C" + (iRet + 1), "C" + (iRet +
1)).Value2;
}
ObjChart3.Location(Excel.XlChartLocation.xlLocationAsObject,
ObjWorkSheet3.Name);
// Перемещаем график так, чтобы он не закрывал покрыть данные
ObjResizeRange3 = (Excel.Range)ObjWorkSheet3.Rows.get_Item(10,
Missing.Value);
ObjWorkSheet3.Shapes.Item("Chart 1").Top = (float)
(double)ObjResizeRange3.Top;
ObjResizeRange3 = (Excel.Range)ObjWorkSheet3.Columns.get_Item(2,
Missing.Value);
ObjWorkSheet3.Shapes.Item("Chart 1").Left = (float)
(double)ObjResizeRange3.Left;
// Сохраняем документ и устанавливаем статус документа "сохранён"
ObjWorkBook3.Saved = true;
// Не будем спрашивать разрешение на запись поверх существующего документа
ObjExcel3.DisplayAlerts = false;
// Сохраняем документ в папке с приложением
ObjWorkBook3.SaveAs(

400
Environment.CurrentDirectory + "\\Новый документ", // Имя файла (object
FileFormat)
ObjExcel3.DefaultSaveFormat, // Формат файла
"123", // Пароль (object Password)
"321", // Повтор пароля (object
WriteResPassword)
Type.Missing, // (object ReadOnlyRecommended)
Type.Missing, // (object object CreateBackup)
Excel.XlSaveAsAccessMode.xlNoChange,// (object XlSaveAsAccessMode
AccessMode)
Type.Missing, // (object ConflictResolution)
Type.Missing, // (object AddToMru)
Type.Missing, // (object TextCodepage)
Type.Missing, // (object TextVisualLayout)
Type.Missing); // (object Local)
System.Threading.Thread.Sleep(1000);
}

После нажатия кнопки и совершения действия в Excel, код принудительно


закрывает Excel:

ObjExcel3.Quit();

Это сделано с целью недопущения «размножения» процессов Excel в памяти ПК.


Так как наше приложение в процессе работы будет создавать несколько
самостоятельных объектов-приложений Excel (для каждого действия своя копия). Если
закрыть окно Excel вручную, связь с объектом из приложения для окна будет утеряна, а
процесс останется в памяти даже после закрытия формы нашего приложения. Если
такое всё-таки случилось, можно воспользоваться следующим кодом (снять
комментарии с кода):

private void LWP13Main_FormClosing(object sender, FormClosingEventArgs e)


{
// Выгружает все (!) процессы EXCEL.EXE из памяти
//foreach (System.Diagnostics.Process currentProcess in
System.Diagnostics.Process.GetProcessesByName("EXCEL.EXE"))currentProcess.Kill();
}

Это событие «закрытия формы» возникающее во время завершения работы


формы, до полного высвобождения ресурсов. Код события выгружает все процессы
EXCEL.EXE из памяти. Вернее «убивает».

Событие Click кнопки B_Exit:

private void B_Exit_Click(object sender, EventArgs e)


{
// Выходим из всех открытых приложением окон Excel
try
{
B_Open.Enabled = true;
ObjExcel1.Quit();
}
catch { }

try
{
B_Create.Enabled = true;
B_Other.Enabled = false;
B_Merge.Enabled = false;
ObjExcel3.Quit();

401
}
catch { }

try
{
ObjExcel4.Quit();
}
catch { }

try
{
ObjExcel5.Quit();
}
catch { }

try
{
B_Unfreeze.Enabled = false;
ObjExcel6.Quit();
}
catch { }

try
{
B_AutoFill.Enabled = true;
ObjExcel7.Quit();
}
catch { }
}

Кнопка B_Other делают следующее. Открывает ранее сохранённый документ


(«Новый документ»), вносит в ячейку А7 первого листа значение «10,5», в ячейку А1
второго листа слово «Лист 2» (форматированное), и заполняет третий лист. Затем
сохраняет всё и закрывает документ и Excel. Код события Click:

private void B_Other_Click(object sender, EventArgs e)


{
B_Other.Enabled = false;
int n, m;
ObjExcel4 = new Excel.Application();
ObjExcel4.Visible = true;
// Получаем набор ссылок на объекты Workbook
ObjWorkBooks4 = ObjExcel4.Workbooks;
// Открываем книгу и получаем на нее ссылку
ObjWorkBook4 = ObjExcel4.Workbooks.Open(Environment.CurrentDirectory + "\\
Новый документ", Type.Missing, Type.Missing, Type.Missing, "123", "321", Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing);
// Если бы открыли несколько книг, то получили ссылку так
// ObjWorkBook4 = ObjWorkBooks4[1];
// Получаем массив ссылок на листы выбранной книги
ObjSheets4 = ObjWorkBook4.Worksheets;
// Получаем ссылку на лист 1
ObjWorkSheet4 = (Excel.Worksheet)ObjSheets4.get_Item(1);
// Выбираем ячейку для вывода A7
ObjRange4 = ObjWorkSheet4.get_Range("A7", "A7");
// Выводим число
ObjRange4.Value2 = 10.5;
// Выбираем лист 2
ObjWorkSheet4 = (Excel.Worksheet)ObjSheets4.get_Item(2);
// При выборе одной ячейки можно не указывать вторую границу
ObjRange4 = ObjWorkSheet4.get_Range("A1", Type.Missing);
// Выводим значение текстовую строку

402
ObjRange4.Value2 = "Лист 2";
ObjRange4.Font.Size = 20;
ObjRange4.Font.Italic = true;
ObjRange4.Font.Bold = true;
// Выбираем лист 3
ObjWorkSheet4 = (Excel.Worksheet)ObjSheets4.get_Item(3);
// Делаем третий лист активным
ObjWorkSheet4.Activate();
// Вывод в ячейки используя номер строки и столбца Cells[строка, столбец]
for(m = 1; m < 20; m++)
{

for(n = 1; n < 15; n++)


{
ObjRange4 = (Excel.Range)ObjWorkSheet4.Cells[m, n];
// Выводим координаты ячеек
ObjRange4.Value2 = m.ToString() + " " + n.ToString();
}
}
// Сохраняем результат
ObjWorkBooks4 = ObjExcel4.Workbooks;
ObjWorkBook4 = ObjWorkBooks4[1];
ObjWorkBook4.Save();
System.Threading.Thread.Sleep(1000);
ObjExcel4.Quit();
}

Кнопка B_Merge также работает с тем же самым документом. Нажатие кнопки:


загрузка документа, объединение ячеек второго листа, заполнение тёмно-красным
цветом и изменение границы объединённой ячейки (красный цвет, толщина), затем
сохранение и выход из Excel.

private void B_Merge_Click(object sender, EventArgs e)


{
B_Merge.Enabled = false;
ObjExcel7 = new Excel.Application();
ObjExcel7.Visible = true;
// Получаем набор ссылок на объекты Workbook
ObjWorkBooks7 = ObjExcel7.Workbooks;
// Открываем книгу и получаем на нее ссылку
ObjWorkBook7 = ObjExcel7.Workbooks.Open(Environment.CurrentDirectory + "\\
Новый документ", Type.Missing, Type.Missing, Type.Missing, "123", "321", Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing);
// Если бы открыли несколько книг, то получили ссылку так
// ObjWorkBook7 = ObjWorkBooks7[1];
// Получаем массив ссылок на листы выбранной книги
ObjSheets7 = ObjWorkBook7.Worksheets;
// Получаем ссылку на лист 2
ObjWorkSheet7 = (Excel.Worksheet)ObjSheets7.get_Item(2);
// Делаем первый лист активным (другим способом)
//ObjWorkSheet7.Activate();
iMySheetActivate(2);
ObjRange7 = ObjWorkSheet7.get_Range("B2", "D7");
// Объединяем ячейки
ObjRange7.Merge(Type.Missing);
// Устанавливаем цвет обводки
ObjRange7.Borders.ColorIndex = 3;
// Устанавливаем стиль и толщину линии
ObjRange7.Borders.LineStyle = Excel.XlLineStyle.xlContinuous;
ObjRange7.Borders.Weight = Excel.XlBorderWeight.xlThick;
// Сделаем заливку
ObjRange7.Interior.ColorIndex = 30;

403
ObjRange7.Interior.PatternColorIndex = Excel.Constants.xlAutomatic;
// Сохраняем результат
ObjWorkBooks7 = ObjExcel7.Workbooks;
ObjWorkBook7 = ObjWorkBooks7[1];
ObjWorkBook7.Save();
System.Threading.Thread.Sleep(1000);
ObjExcel7.Quit();
}

private int iMySheetActivate(int viNumSheet)


{

try
{
Excel.Worksheet sheet = (Excel.Worksheet)ObjWorkBook7.Sheets[viNumSheet];
Excel.DocEvents_Event sheetEvents = (Excel.DocEvents_Event)sheet;
Excel._Worksheet _sheet = (Excel._Worksheet)sheet;
sheetEvents.Activate += new
Excel.DocEvents_ActivateEventHandler(sheetEvents_Activate);
_sheet.Activate();
}
catch (Exception)
{
return 1;
}
return 0;
}

public void sheetEvents_Activate()


{
// Пусто!
}

Зачем использовать метод iMySheetActive вместо:

//ObjWorkSheet7.Activate();

Сняв комментарии со строчки получим предупреждение:

«Неоднозначность между методом


"Microsoft.Office.Interop.Excel._Worksheet.Activate()" и
"Microsoft.Office.Interop.Excel.DocEvents_Event.Activate", который методом не
является. Используйте группу методов.»

Эта двусмысленность в использовании одноимённых свойства и метода


объявленных в интерфейсе _Worksheet и интерфейсе DocEvents. Оба эти интерфейса
наследует класс Worksheet. И, хотя использование метода Activate не приводит к
двусмысленности в выполнении кода, для тех, кто привык писать «чистый код» этот
«глюк» лучше устранить. Устранение можно выполнить через события Excel.

Работа кнопки B_AutoFill: загружается заранее подготовленный документ


(«Пример автозапонения») и на основе данных из документа производит
автоматическое заполнение ячеек, затем сохраняет как новый документ:
Автозаполнение.xlsx и закрывает Excel. Код события Click кнопки B_AutoFill:

private void B_AutoFill_Click(object sender, EventArgs e)


{
B_AutoFill.Enabled = false;
ObjExcel5 = new Excel.Application();

404
ObjExcel5.Visible = true;
ObjExcel5.UserControl = true;
ObjWorkBooks5 = ObjExcel5.Workbooks;
ObjWorkBook5 = ObjExcel5.Workbooks.Open(Environment.CurrentDirectory + "\\
Пример автозаполнения", Type.Missing, Type.Missing, Type.Missing, "123", "321",
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing);
ObjSheets5 = ObjWorkBook5.Worksheets;
ObjWorkSheet5 = (Excel.Worksheet)ObjSheets5.get_Item(1);
// Делаем первый лист активным
ObjWorkSheet5.Activate();
ObjRange5 = ObjWorkSheet5.get_Range("B1", "B1");
ObjRange5.Value2 = "1";
ObjRange5 = ObjWorkSheet5.get_Range("B2", "B2");
ObjRange5.Value2 = "3";
Excel.Range ObjRange51 = ObjExcel5.get_Range("B1:B2", Type.Missing);
Excel.Range ObjRange52 = ObjExcel5.get_Range("B1:B15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillSeries);
ObjRange51 = ObjExcel5.get_Range("C1", Type.Missing);
ObjRange52 = ObjExcel5.get_Range("C1:C15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillYears);
ObjRange51 = ObjExcel5.get_Range("D1", Type.Missing);
ObjRange52 = ObjExcel5.get_Range("D1:D15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillMonths);
ObjRange51 = ObjExcel5.get_Range("E1", Type.Missing);
ObjRange52 = ObjExcel5.get_Range("E1:E15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillDefault);
ObjRange51 = ObjExcel5.get_Range("F1", Type.Missing);
ObjRange52 = ObjExcel5.get_Range("F1:F15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillWeekdays);
ObjRange51 = ObjExcel5.get_Range("G1", Type.Missing);
ObjRange52 = ObjExcel5.get_Range("G1:G15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillDays);
ObjRange51 = ObjExcel5.get_Range("H1", Type.Missing);
ObjRange52 = ObjExcel5.get_Range("H1:H15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillDays);
ObjRange51 = ObjExcel5.get_Range("I1:I2", Type.Missing);
ObjRange52 = ObjExcel5.get_Range("I1:I15", Type.Missing);
ObjRange51.AutoFill(ObjRange52, Excel.XlAutoFillType.xlFillSeries);
// Сохраняем результат
ObjWorkBooks5 = ObjExcel5.Workbooks;
ObjWorkBook5 = ObjWorkBooks5[1];
ObjWorkBook5.Saved = true;
// Не будем спрашивать разрешение на запись поверх существующего документа
ObjExcel5.DisplayAlerts = false;
ObjWorkBook5.SaveAs(
Environment.CurrentDirectory + "\\Автозаполнение",
ObjExcel5.DefaultSaveFormat, Type.Missing,
Type.Missing, Type.Missing, Type.Missing,
Excel.XlSaveAsAccessMode.xlNoChange, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing);
System.Threading.Thread.Sleep(1000);
ObjExcel5.Quit();
}

Исходный документ выглядит так:

405
Рис. 5. 2. Исходный документ «Пример автозаполнения» (Файл типа Лист Microsoft
Excel, разрешение *.xlsx)

Ячейка C1 исходного документа имеет тип «Дата». Остальные: «Общий». Сам


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

Наконец две оставшиеся кнопки. Первая (B_Action) выполняет достаточно


много разнообразных действий с выводом результата либо через MessageBox либо
непосредственно в приложении. Например, изменяет размер окна до 50%, выключает
строку формул, отключает возможность редактирования ячеек напрямую, выводит
список всех недавно открытых документов и прочее. Также после нажатия, окно Excel
будет заморожено (нельзя закрыть и совершить любые действия с окном и
содержимым). Для «размораживания» введена кнопка B_Unfreeze. Весь код
прокомментирован. Событие Click для кнопки B_Action:

private void B_Action_Click(object sender, EventArgs e)


{
B_Other.Enabled = false;
B_Unfreeze.Enabled = true;
// Создаём книги для примеров. Для некоторых примеров нам понадобятся
// несколько книг, для других будет достаточно одной. Для приводимых ниже
// примеров может понадобиться не только объекты книг, но и объекты
// листов и ячеек
ObjExcel6 = new Excel.Application();
ObjExcel6.Visible = true;
ObjExcel6.UserControl = true;
ObjExcel6.SheetsInNewWorkbook = 2;
ObjExcel6.Workbooks.Add(Type.Missing);
ObjExcel6.SheetsInNewWorkbook = 3;
ObjExcel6.Workbooks.Add(Type.Missing);
ObjWorkBooks6 = ObjExcel6.Workbooks;
// Получаем ссылку на объект: Книга1 - номерация от 1
ObjWorkBook6 = ObjWorkBooks6[1];
// Можно, используя свойства WindowState задать размер приложения.
// Возможные значения: xlNormal, xlMinimized, xlMaximized
ObjExcel6.WindowState = Excel.XlWindowState.xlNormal;
// Можно задать полноэкранный режим отображения при значении свойства
// DisplayFullScreen равным true и вернуться к обычному режиму (false)
ObjExcel6.DisplayFullScreen = false;
// Можно задать размеры при DisplayFullScreen = false; и
XlWindowState.xlNormal;,
// используя соответствующие значения свойств Width, Height, Left и Top
ObjExcel6.Width = 500;
ObjExcel6.Height = 400;
ObjExcel6.Left = 10;
ObjExcel6.Top = 10;
// Можно убрать любую из панелей инструментов при значении свойства Visible
равным
// false для соответствующего CommandBars, который может иметь значения:
// Standard, Formatting, Visual Basic, Web, WordArt, Clipboard, External
Data,

406
// Exit Design Mode, Stop Recording, Chart, Picture, Reviewing, Drawing,
PivotTable
// Forms, Control Toolbox и другие
ObjExcel6.CommandBars["Standard"].Visible = true;
// Можно получить или изменить форму курсора - свойство Cursor.
// Возможные значения: xlDefault, xlIBeam, xlNorthwestArrow, xlWait.
// Посмотреть имя курсора:
Text = ObjExcel6.Cursor.ToString();
MessageBox.Show("Текущий курсор: " + Text, "Автоматизация Microsoft Office
Excel (C#) :: Курсор мыши");
// Можно изменить курсор
ObjExcel6.Cursor = Excel.XlMousePointer.xlWait;
// Можно изменить масштаб отображения документа (свойство Zoom):
ObjExcel6.ActiveWindow.Zoom = 50;
// Можно изменить шрифт по умолчанию и его размер. После перезапуска
// Excel все выведенное будет отображено данным шрифтом:
ObjExcel6.StandardFont = "Arial";
ObjExcel6.StandardFontSize = 10;
// Можно не отображать строку редактирования содержимого ячейки
// (свойство DisplayFormulaBar)
ObjExcel6.DisplayFormulaBar = false;
// Можно запретить редактирование ячеек в самих ячейках (свойство
EditDirectlyInCell),
// разрешив редактирование только в строке формул
ObjExcel6.EditDirectlyInCell = false;
// Можно вообще запретить доступ к документу. Если свойство Interactiv
// не вернуть в true, то нельзя будет даже закрыть Excel
ObjExcel6.Interactive = false;
// Можно программно запретить обновление экрана после каждого изменения
// и, после выполнения большого объема выводимой информации, разрешить.
// Результат: увеличение скорости вывода
ObjExcel6.ScreenUpdating = false; // Запретить
// ... здесь большой объём выводимой информации
ObjExcel6.ScreenUpdating = true; // Разрешить
// Можно принудительно выполнить пересчёты формул, используя метод Calculate,
// в диапазоне ячеек, в книге или во всех открытых рабочих книгах
ObjSheets6 = ObjWorkBook6.Worksheets;
ObjWorkSheet6 = (Excel.Worksheet)ObjSheets6.get_Item(1);
// Для диапазона:
// ObjRange6 = ObjWorkSheet6.get_Range("A1", "С10").Calculate();
// Для книги:
// ObjWorkBook6.Calculate();
// Для всех книг:
// ObjExcel6.Calculate();
// Можно проверить правильность написания текста. Например,
// следующие строки дадут результат "написан некорректно".
// В методе CheckSpelling можно задать словарь (второй параметр)
// и задать игнорировать ли регистр (третий параметр) при проверки
ObjRange6 = ObjWorkSheet6.get_Range("A1", Type.Missing);
ObjRange6.Value2 = "Текьст";
Text = (ObjExcel6.CheckSpelling(ObjRange6.Value2.ToString(), Type.Missing,
true) ? "написан корректно" : "написан некорректно");
MessageBox.Show("Текст в ячейке: \"" + ObjRange6.Value2 + "\", " + Text,
"Автоматизация Microsoft Office Excel (C#) :: Проверка орфографии");
// Можно отменить последнее из выполненных действий (метод Undo), выполненное
// в самом приложении (не влияет на операции, выполненные из приложения)
// ObjExcel6.Undo();
// Можно получить и изменить путь сохранения и открытия файлов по умолчанию
Text = ObjExcel6.DefaultFilePath; // Выведет Ваш путь
MessageBox.Show("Текущий путь: " + Text, "Автоматизация Microsoft Office
Excel (C#) :: Текущий путь");
ObjExcel6.DefaultFilePath = @"C:\";
Text = ObjExcel6.DefaultFilePath; // Выведет C:\
// Можно создать копию документа, используя метод Workbook.NewWindow()
// Например для документа "a" будут созданы окна "a:1" и "a:2":

407
Excel.Window ObjWindow6 = ObjWorkBook6.NewWindow();
// Можно создать копию документа и по другому - через свойства
Application.Workbooks.
// Если окон много, то для проверки наличия окна целесообразно
// использовать свойство Count
if (ObjExcel6.Windows.Count > 1)
{
ObjWindow6 = ObjExcel6.Windows[1];
ObjWindow6.Application.Workbooks[1].NewWindow();
}
// Можно изменить расположение окон используя метод Arange. Порядок
расположения
// определяет первый параметр метода: xlArrangeStyleCascade,
xlArrangeStyleHorizontal,
// xlArrangeStyleTiled, xlArrangeStyleVertical. Второй параметр при true
означает, что
// требуется упорядочить только видимые окна активной книги, при false - все.
// Третий и четвертый параметр - синхронизация разверток горизонтальной и
вертикальной
ObjExcel6.Windows.Arrange(Excel.XlArrangeStyle.xlArrangeStyleVertical, true,
true, true);
// Можно убрать заголовки строк и столбцов, используя свойство
DisplayHeadings
ObjWindow6.DisplayHeadings = false;
// Или так
ObjExcel6.ActiveWindow.DisplayHeadings = false;
// Можно при значении свойства DisplayFormulas равным true показываеть в
// ячейках формулы (там где они есть), а при false - значения
ObjWindow6.DisplayFormulas = false;
// Можно, используя свойство DisplayWorkbookTabs при true показываеть помимо
// Scrollbars позиции табуляции для выбора листов книг и кнопки навигации по
// листам, или, убрать их, при значении свойства равным false
ObjWindow6.DisplayWorkbookTabs = true;
// Можно разделить лист путем отделения как, отдельной части, несколько
// cтолбцов или строк, используя свойства SplitColumn или SplitRow
ObjWindow6.SplitColumn = 5;
ObjWindow6.SplitRow = 5;
// Можно разделить окно вертикально или горизонтально используя свойства
SplitVertical
// или SplitHorizontal (практически аналог предыдущего пункта)
ObjWindow6.SplitVertical = 10;
ObjWindow6.SplitHorizontal = 10;
// Можно изменить цвет сетки для листов.
// 1. Используя свойство GridlineColor
ObjWindow6.GridlineColor = ColorTranslator.ToOle(Color.Blue);
// 2. Используя свойство GridlineColorIndex
ObjWindow6.GridlineColorIndex = (Excel.XlColorIndex)3;
// Можно вообще убрать сетку, используя свойство DisplayGridlines
ObjWindow6.DisplayGridlines = false;
// Можно получить список всех недавно открывавшихся файлов.
// Для этого используется свойство Eccel.Application.RecentFiles
for (int j = 0; j < ObjExcel6.RecentFiles.Count; j++)
{
ObjRange6 = (Excel.Range)ObjWorkSheet6.Cells[j + 1, 1];
ObjRange6.Value2 = ObjExcel6.RecentFiles[j + 1].Name;
}
// Можно перейти на последнюю заполненную ячейку Excel
ObjExcel6.ActiveCell.SpecialCells(Excel.XlCellType.xlCellTypeLastCell,
Type.Missing).Select();
}

Событие Click для кнопки B_Unfreeze:

private void B_Unfreeze_Click(object sender, EventArgs e)


{

408
ObjExcel6.Interactive = true;
}

На этом всё. Приложение готово.

6. Завершающая часть

Компилируем приложение (Release) и запускаем. Проверяем работоспособность


первых двух групп элементов и переходим к третьей:

Нажимаем на кнопку Сформировать готовый документ с графиком:

Рис. 6. 1. Модифицированное приложение Windows Forms: результат создания новой


книги для графика (указываем число кварталов, нажатие «Нет» уменьшает число)

409
Рис. 6. 2. Модифицированное приложение Windows Forms: результат создания новой
книги для графика (данные за четыре квартала и график на основе данных)

Жмём Другие листы. Результат:

Рис. 6. 3. Модифицированное приложение Windows Forms: результат создания


изменения книги с графиком (заполненный Лист3)

Жмём Объединение:

410
Рис. 6. 4. Модифицированное приложение Windows Forms: результат создания
изменения книги с графиком (заполненный Лист2)

Жмём Автозаполнение:

Рис. 6. 5. Модифицированное приложение Windows Forms: результат создания книги с


автоматическим заполнением ячеек (заполненный Лист1)

Жмём Выполнить различные действия и затем Разморозить, чтобы


разблокировать Excel:

411
Рис. 6. 6. Модифицированное приложение Windows Forms: результат работы кнопки
«Выполнить различные действия» и «Разморозить»

7. О приложении к Лабораторной работе № 13

Получившуюся программу (LWP13Excel.exe), собранную из кусков кода


приведённых в данной лабораторной работе, архив с проектом из статьи
(SafeCOMWrapper_src.zip), а также документ Excel в архиве для работы
автоматического заполнения (AutoFill.zip), использованный в данной работе можно
загрузить по ссылке в конце этого материала (сслыка доступна в программном
продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

14. Лабораторная работа № 14: Простой растровый редактор

Лабораторная работа № 14: Простой растровый редактор

Содержание

412
22.Вводная часть
23.Создание приложения Windows Forms
24.Модификация приложения Windows Forms: подготовка интерфейса
редактора
25.Модификация приложения Windows Forms: функциональность
растрового редактора
26.Завершающая часть
27.О приложении к Лабораторной работе № 14

1. Вводная часть

В этой работе будет рассмотрена работа с растровой графикой и «простым»


рисованием. Фактически мы будем реализовывать функциональность известного
приложения Microsoft Paint, входящего в состав любого Windows.

Что же такое GDI+? Официальная документация скромно называет её Class-


based API, то есть основанным на классах интерфейсе прикладных программ для ОС
Windows поколения «не GDI». Так как она встроена в Windows XP и все последующие
ОС вплоть до Windows 7, её называют частью этих операционных систем. Часто
встречается также определение «библиотека» или «библиотека классов». В
действительности, предоставляемый GDI+ набор классов является тонкой оболочкой
над множеством обычных функций, реализованных в одной динамической библиотеке
GdiPlus.dll.
Итак, GDI+ — это библиотека, призванная заменить существующий уже больше
11 (или 18, в зависимости от того, как считать) лет интерфейс GDI, являющийся
графическим ядром предыдущих версий Windows. Она сочетает в себе (по крайней
мере, по замыслу) все достоинства своего предшественника и предоставляет множество
новых мощных возможностей. Кроме того, при её проектировании заранее ставилась
цель наименее болезненного переноса приложений на 64-битные платформы.

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


составлять стандартная библиотека System.Drawing. Пространство имён
System.Drawing обеспечивает доступ к функциональным возможностям графического
интерфейса GDI+. Пространства имён System.Drawing.Drawing2D,
System.Drawing.Imaging, и System.Drawing.Text обеспечивают дополнительные
функциональные возможности.

Класс Graphics предоставляет методы рисования на устройстве отображения.


Такие классы, как Rectangle и Point, инкапсулируют элементы GDI+. Класс Pen
используется для рисования линий и кривых, а классы, производные от абстрактного
класса Brush, используются для заливки фигур.

Основу рисунка составит элемент управления PictureBox пространства имён


System.Windows.Forms, и класс Bitmap, который инкапсулирует точечный рисунок
GDI+, состоящий из данных пикселей графического изображения и атрибутов рисунка.
Объект Bitmap используется для работы с изображениями, определяемыми данными
пикселей.

Приложение будет функционировать как растровый редактор изображений. А


именно:
 будет доступна функция рисования линий и непрерывных линий с разной
толщиной пера и цветом пера при рисовании;
 будет доступна функция рисования фигур, таких как прямоугольник и эллипс;

413
 будет доступна функция заливки фигур произвольным цветом, а также
изменения толщины пера и цвета пера для контуров фигур при рисовании;
 будет доступна возможность очистки поля рисования (полная очистка и ластик),
а также сохранения рисунка в разные графические форматы (например: JPEG);
 будет доступна возможность загрузки сохранённого рисунка или любого другого
рисунка;
 будет доступна возможность работы с «эффектами», а именно осветление
выбранного участка рисунка.

Пространства имён GDI+:

GDI+ — это уровень, находящийся между GDI и приложением, которые


предоставляет более интуитивно понятную, основанную на наследовании объектную
модель. Хотя GDI+ — это в основном оболочка вокруг GDI, тем не менее, Microsoft
посредством GDI+ предлагает ряд новых возможностей и увеличенную
производительность по сравнению с некоторыми старыми средствами GDI.

Часть базовой библиотеки классов .NET, связанная с GDI+, огромна! Гораздо


важнее понять фундаментальные принципы, касающиеся рисования. Полные списки
всех классов и методов, доступных в GDI+, конечно же, содержатся в документации
SDK и MSDN.

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

System.Drawing:
Содержит большинство классов, структур, перечислений и делегатов,
обеспечивающих базовую функциональность рисования.

System.Drawing.Drawing2D:
Представляет основную поддержку для двумерной и векторной графики,
включая сглаживание, геометрические трансформации и графические пути.

System.Drawing.Imaging:
Содержит различные классы, обеспечивающие манипуляции с графическими
изображениями (битовые карты, файлы GIF и тому подобное).

System.Drawing.Printing:
Содержит классы, имеющие отношение к печати и предварительному просмотру
выводимых на печать изображений.

System.Drawing.Design:
Включает некоторые предопределенные диалоговые окна, таблицы свойств и
другие элементы интерфейса, имеющие отношение к расширению пользовательского
интерфейса времени проектирования.

System.Drawing.Text:
Включает классы для выполнения более сложных манипуляций со шрифтами и
семействами шрифтов.

Контексты устройств и объект Graphics:

В GDI способ идентификации устройства, на которое нужно направить вывод,


заключается в обращении к объекту, называемому контекстом устройства (device

414
context — DC). DC сохраняет информацию об определенном устройстве и может
транслировать вызовы функций программного интерфейса GDI в конкретные команды,
направляемые устройствам. Вы также можете опросить контекст устройства на предмет
того, какие возможности он предоставляет (например, может ли принтер печатать в
цвете или же только в черно-белом изображении), дабы соответствующим образом
откорректировать вывод. Если вы пытаетесь заставить устройство делать что-то такое,
что оно не способно сделать, то DC обычно обнаруживает это и предпринимает
соответствующие действия (которые, в зависимости от ситуации, могут означать
генерацию исключения либо модификацию запроса таким образом, чтобы получить как
можно более близкий результат в рамках возможностей данного устройства).
Однако DC не только имеет дело с аппаратным устройством. Он служит в
качестве моста между приложением и Windows, и принимает во внимание любые
требования и ограничения, налагаемые на рисование Windows. Например, если
Windows знает, что необходимо перерисовать лишь часть окна вашего приложения, DC
перехватит и отменит попытки рисования вне этой области. Благодаря связи DC с
Windows, работа через контекст устройств может упростить ваш код и в других
отношениях.
Например, аппаратным устройствам необходимо сообщать, где следует рисовать
объекты, и обычно им нужны координаты, отсчитываемые относительно верхнего
левого угла экрана (или другого выходного устройства). Однако приложения обычно
отображают нечто в клиентской области (области, зарезервированной для рисования)
собственного окна, возможно, используя собственную систему координат. Поскольку
окно может быть позиционировано в любом месте экрана, и пользователь в любой
момент может его переместить, преобразования между этими системами координат
могут оказаться непростой задачей. Однако DC всегда знает, где находится ваше окно,
и может выполнять такие преобразования автоматически.

В GDI+ контекст устройства помещен в оболочку базового класса .NET с именем


System.Drawing.Graphics. Большая часть рисования выполняется вызовом методов
экземпляра Graphics. Фактически, поскольку класс Graphics отвечает за выполнение
большинства операций рисования, очень немного в GDI+ происходит такого, что не
касалось бы тем или иным образом экземпляра Graphics, а потому понимание того, как
управлять этим объектом, является ключом к пониманию того, как рисовать на
устройствах отображения с помощью GDI+.

2. Создание приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

415
Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

Рис. 2. 2. Окно создания нового проекта

416
В поле Имя вводим LWP14SimpleRasterEditor — это название программы
(выбрано по названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

417
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

418
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms: подготовка интерфейса


редактора

Так как наша программа будет похожа на редактор изображений, нам


необходимы все атрибуты такого редактора. У нас будет верхнее меню для навигации и
нижняя строка состояния для отображения подсказок.

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP14Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Простой растровый
редактор (C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения

419
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 800;
600
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Размести на форме (перетаскиванием в любое место) три панели:

ToolStrip ( );

MenuStrip ( );

StatusStrip ( ).

Все эти элементы расположены на панели инструментов в разделе Меню и


панели инструментов:

То, что должно получиться в итоге показано на рисунке ниже:

420
Рис. 3. 1. Модифицированная форма приложения и расстановка необходимых
элементов управления

Теперь разместим главный элемент управления: PicureBox, растянув его до


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

421
Рис. 3. 2. Свойства: элемент PictureBox, свойство Dock

Все свойства PictureBox:


(Name): PB_Bitmap
Dock: Fill
BackColor: White

422
Рис. 3. 2. Свойства: элемент PictureBox, свойство BackColor

Теперь у нас должно получиться нечто подобное:

423
Рис. 3. 3. Модифицированная форма приложения и расстановка необходимых
элементов управления и PictureBox

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

424
Рис. 3. 4. Модифицированная форма приложения и окончательная расстановка
необходимых элементов управления и PictureBox

Теперь расставим все необходимые элементы (как рисунке выше). Начнём с


главного меню (верхнего), за которое отвечает панель MenuStrip. Свойство панели не
меняем, имя панели оставляем как есть: menuStrip1. Создаём три корневых меню:
Файл, Рисование и Параметры. Для создания такого меню нужно выделить

MenuStrip, затем нажать на появившуюся надпись «Вводить здесь» ( ),


после чего ввести текст:

После создания трёх корневых элементов меню, создаём вложенные меню


аналогичным образом. Для создания разделителя (горизонтальной черты между
элементами меню) необходимо сначала создать пустой элемент в качестве вложенного
(разделителем не может быть корневой элемент меню). Текст можно вводить любой.
Затем, после создания элемента, нажимаем правую кнопку мыши на том элементе
меню, которое хотим сделать разделителем, далее Преобразовать в -> Separator.

425
Для перемещения разделителя, например, наверх списка элементов меню нужно
выделить разделитель левой кнопкой мыши и не отпуская клавишу перетащить в
нужном направлении:

Меню Файл выглядит так:

Три элемента-кнопки (MenuItem): Выбрать рисунок, Сохранить как... и


Выход и два разделителя. Свойства:

MenuItem:
(Name): выбратьРисунокToolStripMenuItem
Text: Выбрать рисунок
ToolTipText: Выбрать рисунков для загрузки в рабочее
поле

Свойство (Name) оставлено без изменений. Имя было выбрано автоматически


самой Visual Studio 2010.

MenuItem:
(Name): сохранитьКакРисунокToolStripMenuItem
Text: Сохранить как...
ToolTipText: Сохранить изображение в рабочем поле
как...
MenuItem:
(Name): выходКакРисунокToolStripMenuItem
Text: Выход

426
ToolTipText: Выйти из приложения

Меню Рисование выглядит так:

В левом подменю четыре кнопки и один разделитель, в правом всего одна


кнопка.

MenuItem:
(Name): очиститьToolStripMenuItem
Text: Очистить
ToolTipText: Очистить рабочее поле

Как и раньше, свойство (Name) оставлено без изменений. Имя было выбрано
автоматически.

MenuItem:
(Name): линияToolStripMenuItem
Text: Линия
ToolTipText: Режим рисования линии
MenuItem:
(Name): прямоугольникToolStripMenuItem
Text: Прямоугольник
ToolTipText: Режим рисования прямоугольника
MenuItem:
(Name): непрерывнаяToolStripMenuItem
Text: Непрерывная
ToolTipText: Режим непрерывной линии

Меню Параметры выглядит так:

Две кнопки, разделитель и текстовое поле (TextBox):

427
MenuItem:
(Name): цветПераToolStripMenuItem
Text: Цвет пера
ToolTipText: Установить цвет пера для линий и границ
фигур
MenuItem:
(Name): цветЗаливкиToolStripMenuItem
Text: Цвет заливки
ToolTipText: Установить цвет заливки фигур
MenuItem:
(Name): толщинаПераToolStripMenuItem
Text: Толщина пера
ToolTipText: Введите значение толщины пера (по
умолчанию: 1)

Перейдём к нижнему меню (ToolStrip). Здесь всё просто. Все кнопки этого меню
по функциям будут повторять кнопки верхнего меню. А точнее нажатие на кнопку
нижнего меню, будет приводить к нажатию кнопки верхнего меню. Мы добавим нижнее
меню просто для наглядности.

Для добавления элемента в меню ToolStrip нужно выделить это меню и далее в
специальном выдающем списке выбрать нужны элемент для добавления:

Меню выглядит так:

Шесть кнопок с изображениями (Button), три кнопки без изображений и четыре


вертикальных разделителя (Separator).

При создании кнопки в ToolStrip ей автоматически присваивается изображение (


). Изображение можно поменять, импортировав его в проект, либо создав
собственное изображение при помощи встроенного в Visual Studio 2010 редактора
изображений. Так как работа с созданием иконок и изображений во встроенном
редакторе уже была рассмотрена в предыдущих лабораторных работах, теперь можно
первым путём и импортировать изображения. Пусть это будут изображения для кнопок
Сохранить как..., Выход, Очистить, Цвет пера и Цвет заливки:

428
На панели ToolStrip создаём кнопку Сохранить как... (вторая на панели) со
следующими свойствами:

Button:
(Name): сохранитьКакToolStripButton
Text: Сохранить как...
ToolTipText: Сохранить как...
DisplayStyle: Image

Теперь ищем свойство Image и жмём справа от свойства на троеточие (


). Откроется окно Выбор ресурса:

Рис. 3. 5. Выбор ресурса: окно выбора файла ресурса изображения для кнопки в
ToolStrip

На рисунке выше все ресурсы уже импортированы. Для импорта жмём на


соответствующую кнопку (если нужно чтобы импортируемое изображение осталось в
проекте в качестве отдельного файла, должно быть выбрано Файл ресурсов
проекта). Далее выбираем нужный нам файл с изображением и жмём Открыть,
изображение будет отображено в окне Выбор ресурса. Жмём ОК. Кнопка теперь с
иконкой.

Все добавленные ресурсы можно посмотреть, открыв файл Resources.resx в


обозревателе решений:

429
В нашем случае этот файл выглядит так:

Рис. 3. 6. Resources.resx: все ресурсы проекта импортированные извне

В этом же окне можно работать с этими ресурсами (добавлять новые, удалять и


редактировать).

Рис. 3. 7. Resources.resx: меню Добавить ресурс

Двойной клик по ресурсу изображения откроет встроенный редактор


изображений.

Свойства всех кнопок этого меню:

Button:
(Name): выбратьРисунокToolStripButton
Text: Выбрать рисунок
ToolTipText: Выбрать рисунок

430
DisplayStyle: Image
Button:
(Name): выходToolStripButton
Text: Выход
ToolTipText: Выход
DisplayStyle: Image
Button:
(Name): очиститьToolStripButton
Text: Очистить
ToolTipText: Очистить
DisplayStyle: Image
Button:
(Name): линияToolStripButton
Text: Л
ToolTipText: Выход
DisplayStyle: Text
Button:
(Name): прямоугольникToolStripButton
Text: П
ToolTipText: Прямоугольник
DisplayStyle: Text
Button:
(Name): окружностьToolStripButton
Text: О
ToolTipText: Окружность
DisplayStyle: Text
Button:
(Name): цветПераToolStripButton
Text: Цвет пера
ToolTipText: Цвет пера
DisplayStyle: Image
Button:
(Name): цветЗаливкиToolStripButton
Text: Цвет заливки
ToolTipText: Цвет заливки
DisplayStyle: Image

Строка состояния StatusStrip будет содержать всего один элемент из возможных:

Один StatusLabel со следующими свойствами:

StatusLabel:
(Name): StatusLabel
Text: Приложение готово к работе

431
4. Модификация приложения Windows Forms: функциональность растрового
редактора

Есть два основных способа работы с изображением и последующим его


сохранением. Работа с растровым изображением и работа с векторным изображением.
Не путать с типами изображений.

Начнём с того, что любое растровое изображение является простым набором


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

Важными характеристиками изображения являются:


1. Количество пикселей — количество цветов. Может указываться отдельно
количество пикселей по ширине и высоте (1024×768, 640×480, …) или же, редко,
общее количество пикселей (часто измеряется в мегапикселях).
2. Количество используемых цветов или глубина цвета (эти характеристики
имеют следующую зависимость: N = 2k, где N — количество цветов, а k — глубина
цвета);
3. Цветовое пространство (цветовая модель) RGB, CMYK, XYZ, YCbCr и др.
4. Разрешение — справочная величина, говорящая о рекомендуемом размере
пикселя изображения.

Растровую графику редактируют с помощью растровых графических редакторов.


Создаётся растровая графика фотоаппаратами, сканерами, непосредственно в
растровом редакторе, также путём экспорта из векторного редактора или в виде снимка
экрана.

Типичный растровый редактор: Paint. Фактически редактор оперирует только


точками. Нарисованную линию нельзя передвинуть или растянуть. А всё рисование
состоит только из заполнения массива координат и пикселей. Именно так и будет
работать наше приложение. Второй же способ, это на этапе рисования (до сохранения
в обычные графические форматы типа *.bmp) представлять каждый объект рисунка
как самостоятельный объект. Для линии это координаты положения, толщина, цвет и
что самое главное — аналоги имени. Таким образом, такую линию можно переместить,
растянуть, удалить как объект (ближайший аналог: символ в Word’е). Для приложения
такой рисунок предстаёт как векторное изображение, которое можно экспортировать в
растровое (данные о примитивах удаляются, остаётся лишь слитое воедино
изображение).

Теперь о нашем приложении:

Первая кнопка загружает сторонний рисунок с жёсткого диска через диалог


открытия файла. После загрузки рисунок отображается в качестве растрового
изображения в PictureBox и этот рисунок становится доступен для редактирования.

Событие Click для элемента меню (Выбрать рисунок) можно создать просто
дважды кликнув на элементе меню. Также можно выбрать этот элемент меню, далее на
панели Свойства справа перейти на вкладку События и далее ввести имя метода для
события Click, либо просто дважды клинуть по Click (Действия):

432
Рис. 3. 5. Свойства: Событие Click элемента меню Выбрать рисунок

Код события Click для кнопки Выбрать рисунок:

private void выбратьРисунокToolStripMenuItem_Click(object sender, EventArgs e)


{
// Если рисунок был выбран
if (OFD_Picture.ShowDialog() == DialogResult.OK)
{
// Получаем изображения по указанному в диалоге OpenFileDialog пути
(OFD_Picture.FileName)
image = Image.FromFile(OFD_Picture.FileName);
int width = image.Width; // Получаем ширину изображения
int height = image.Height; // Получаем высоту изображения
// Устанавливаем размеры изображения в PictureBox
PB_Bitmap.Width = width;
PB_Bitmap.Height = height;
// Создаём Bitmap на основе изображения
bitmap = new Bitmap(Image.FromFile(OFD_Picture.FileName), width, height);
PB_Bitmap.Image = bitmap; // Отправляем Bitmap в PictureBox
StatusLabel.Text = "Изображение " +
Path.GetFileName(OFD_Picture.FileName) + " успешно загружено! Полный путь: " +
OFD_Picture.FileName;
}
}

Кнопка Сохранить как... сохраняет всё изображение в рабочей области в


выбранном формате (BMP-, JPEG-, GIF, TIF- или PGN-файл) и выводит результат в
качестве текста в строке состояния.

433
Событие Click кнопки Сохранить как...:

private void сохранитьКакToolStripMenuItem_Click(object sender, EventArgs e)


{

// Если выбрали, сохраняем


if (SFD_Picture.ShowDialog() == DialogResult.OK)
{
// Получаем имя файла из диалога
string fileName = SFD_Picture.FileName;
// Получаем расширения файла из диалога
string strFilExtn = fileName.Remove(0, fileName.Length - 3);
// Сохраняем файл в выбранном расширении
switch (strFilExtn)
{
case "bmp":
bitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Bmp);
break;
case "jpg":
bitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);
break;
case "gif":
bitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Gif);
break;
case "tif":
bitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Tiff);
break;
case "png":
bitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Png);
break;
default:
break;
}
StatusLabel.Text = "Изображение " +
Path.GetFileName(SFD_Picture.FileName) + " успешно сохранено! Полный путь: " +
SFD_Picture.FileName;
}
}

Событие Click кнопки Выход:

private void выходToolStripMenuItem_Click(object sender, EventArgs e)


{
Close();
}

Кнопка Очистить, полностью очищает рабочую область и Bitmap в которой


сохраняются изменения.

Событие Click кнопки Очистить:

private void очиститьToolStripMenuItem_Click(object sender, EventArgs e)


{
// Очищаем рабочее поле
// Создаём пустой Bitmap на основе размеров PictureBox
bitmap = new Bitmap(PB_Bitmap.Size.Width, PB_Bitmap.Size.Height);
// Инициализируем фон для поля рисования
graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.White); // Задаём белый цвет фона, иначе фон будет
прозрачным
//graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; //
Выключаем сглаживание

434
PB_Bitmap.Image = bitmap; // Отправляем Bitmap в PictureBox
StatusLabel.Text = "Рабочее поле очищено";
}

Кнопка Линия активирует режим в котором в PictureBox можно рисовать линии.


Код события содержит лишь действия по управлению элементами меню и выводом
текстового сообщения в строку состояния. При нажатии, снимается выделение (галочка
слева от надписи) с кнопок меню «Рисование»: «Прямоугольник» или «Окружность»,
если до этого они были активированы. Событие Click:

private void линияToolStripMenuItem_Click(object sender, EventArgs e)


{

if (!DrawLine)
{
DrawLine = true;
линияToolStripMenuItem.Checked = true;
DrawRect = false;
прямоугольникToolStripMenuItem.Checked = false;
DrawCirc = false;
окружностьToolStripMenuItem.Checked = false;
StatusLabel.Text = "Включен режим рисования линии";
}
}

Событие Click кнопки Прямоугольник:

private void прямоугольникToolStripMenuItem_Click(object sender, EventArgs e)


{

if (!DrawRect)
{
DrawLine = false;
линияToolStripMenuItem.Checked = false;
DrawRect = true;
прямоугольникToolStripMenuItem.Checked = true;
DrawCirc = false;
окружностьToolStripMenuItem.Checked = false;
StatusLabel.Text = "Включен режим рисования прямоугольника";
}
}

Событие Click кнопки Окружность:

private void окружностьToolStripMenuItem_Click(object sender, EventArgs e)


{

if (!DrawCirc)
{
DrawLine = false;
линияToolStripMenuItem.Checked = false;
DrawRect = false;
прямоугольникToolStripMenuItem.Checked = false;
DrawCirc = true;
окружностьToolStripMenuItem.Checked = true;
StatusLabel.Text = "Включен режим рисования окружности";
}
}

435
Кнопка «Непрерывная» включает режим рисования линий при котором
предыдущая линия становится началом следующей. Получается ломаная линия.
Событие Click этой кнопки выглядит так:

private void непрерывнаяToolStripMenuItem_Click(object sender, EventArgs e)


{
if (!CurveLine)
{
CurveLine = true;
непрерывнаяToolStripMenuItem.Checked = true;
StatusLabel.Text = "Включен режим непрерывной линии";
}
else
{
CurveLine = false;
непрерывнаяToolStripMenuItem.Checked = false;
StatusLabel.Text = "Выключен режим непрерывной линии";
}
}

Кнопка Цвет пера вызывает стандартный диалог Windows по выбору цвета и


задаёт цвет главного пера, которым рисуется линия или контуры фигур. Если цвет не
выбран, значение устанавливается по умолчанию как чёрный. Событие Click кнопки:

private void цветПераToolStripMenuItem_Click(object sender, EventArgs e)


{

if (CD_Pen.ShowDialog() == DialogResult.OK)
{
Color_Pen = CD_Pen.Color;
Main_Pen = new Pen(Color_Pen, Convert.ToInt16(X));
Main_Pen.EndCap = System.Drawing.Drawing2D.LineCap.NoAnchor;
}
else
{
Main_Pen = new Pen(Color.Black, Convert.ToInt16(X));
Main_Pen.EndCap = System.Drawing.Drawing2D.LineCap.NoAnchor;
}
}

Кнопка Цвет заливки устанавливает цвет заполнения SolidBrush-объекта для


прямоугольника или окружности. Если цвет не выбран, значение сбрасывается на цвет
с именем Color.Violet, который выбран в качестве цвета для сбора заливки (заливка не
осуществляется и фигура не заполняется цветом.

private void цветЗаливкиToolStripMenuItem_Click(object sender, EventArgs e)


{
if (CD_Fill.ShowDialog() == DialogResult.OK)
{
Color_Fill = CD_Fill.Color;
Fill = new SolidBrush(Color_Fill);
}
else
{
Color_Fill = Color.Violet; // Цвет сбора заливки
Fill = new SolidBrush(Color_Fill);
}
}

Для элемента TextBox (толщинаПераToolStripMenuItem) определены четыре


события. Основное событие TextChanged вызывается всякий раз когда будет меняться

436
текст внутри поля. Само поле предназначено для ввода числа отвечающего за толщину
пера (положительное значение от 0 и до примерно 1000 пикселей0; после 1000
значение не нельзя будет заменить разницы):

private void толщинаПераToolStripMenuItem_TextChanged(object sender, EventArgs e)


{
try
{
X = Convert.ToDouble(толщинаПераToolStripMenuItem.Text);
Main_Pen = new Pen(Color_Pen, Convert.ToInt16(X));
}
catch { }
}

Событие KeyDown выполняющее операцию сохранения числа в текстовом поле


(аналогично предыдущему событию), но после нажатия клавиши Enter:

private void толщинаПераToolStripMenuItem_KeyDown(object sender, KeyEventArgs e)


{

if (e.KeyCode == Keys.Enter) // Если нажат Enter


{
X = Convert.ToDouble(толщинаПераToolStripMenuItem.Text);
Main_Pen = new Pen(Color_Pen, Convert.ToInt16(X));
}
}

События MouseEnter и MouseLeave реализуют визуальное «оформление»


элемента. Наводим курсор и получаем значение 1. «1» остаётся, если ничего не
вводить в поле или оставить поле пустым:

private void толщинаПераtoolStripMenuItem_MouseEnter(object sender, EventArgs e)


{

if (толщинаПераToolStripMenuItem.Text == "Толщина пера")


{
толщинаПераToolStripMenuItem.Text = "1";
}
}

private void толщинаПераtoolStripMenuItem_MouseLeave(object sender, EventArgs e)


{
if (толщинаПераToolStripMenuItem.Text == "")
{
толщинаПераToolStripMenuItem.Text = "1";
}
}

События кнопок ToolStrip-меню похожи и выполняют роль переопределения


вызовов некоторых событий нажатия кнопок верхнего меню. Все события Click по
порядку следования кнопок на рисунке 3. 4.:

private void выбратьРисунокToolStripButton_Click(object sender, EventArgs e)


{
выбратьРисунокToolStripMenuItem.PerformClick(); // Вызываем нажатие
аналогичной кнопки в MenuStrip
}

private void сохранитьКакToolStripButton_Click(object sender, EventArgs e)


{

437
сохранитьКакToolStripMenuItem.PerformClick();
}

private void выходToolStripButton_Click(object sender, EventArgs e)


{
выходToolStripMenuItem.PerformClick();
}

private void очиститьToolStripButton_Click(object sender, EventArgs e)


{
очиститьToolStripMenuItem.PerformClick();
}

private void линияToolStripButton_Click(object sender, EventArgs e)


{
линияToolStripMenuItem.PerformClick();
линияToolStripButton.Checked = true;
прямоугольникToolStripButton.Checked = false;
окружностьToolStripButton.Checked = false;
}

private void прямоугольникToolStripButton_Click(object sender, EventArgs e)


{
прямоугольникToolStripMenuItem.PerformClick();
линияToolStripButton.Checked = false;
прямоугольникToolStripButton.Checked = true;
окружностьToolStripButton.Checked = false;
}

private void окружностьToolStripButton_Click(object sender, EventArgs e)


{
окружностьToolStripMenuItem.PerformClick();
линияToolStripButton.Checked = false;
прямоугольникToolStripButton.Checked = false;
окружностьToolStripButton.Checked = true;
}

private void цветПераToolStripButton_Click(object sender, EventArgs e)


{
цветПераToolStripMenuItem.PerformClick();
}

private void цветЗаливкиToolStripButton_Click(object sender, EventArgs e)


{
цветЗаливкиToolStripMenuItem.PerformClick();
}

Теперь перейдём к редактированию непосредственно кода и основным события


формы. Для начала откроем код файла LWP14Main.cs (правая кнопка мыши на значке
формы, далее Перейти к коду или нажмём на клавишу F7). В самом начале файла
добавим ссылку:

using System.IO; // Для работы класса Path

Найдём:

public partial class LWP14Main : Form


{

Добавим после:

// Диалоги

438
OpenFileDialog OFD_Picture;
SaveFileDialog SFD_Picture;
ColorDialog CD_Pen;
ColorDialog CD_Fill;
// Рабочее поле
Image image;
Graphics graphics;
Bitmap bitmap;
// Инструменты рисования
Pen Main_Pen;
Color Color_Pen;
Color Color_Fill;
// Объекты рисования
Point FirstPoint = new Point();
Point ToPoint = new Point();
Point LastPoint = new Point();
Rectangle SelectRect = new Rectangle();
Rectangle CircleRect = new Rectangle();
Rectangle LightRect = new Rectangle();
SolidBrush Fill;
// Вспомогательные переменные
Boolean DrawLine;
Boolean CurveLine;
Boolean DrawRect;
Boolean DrawCirc;
Double X;

Код метода LWP14Main() изменим следующим образом:

public LWP14Main()
{
InitializeComponent();
// Инициализируем диалоги
OFD_Picture = new OpenFileDialog();
OFD_Picture.Filter = "Файлы изображений (*.bmp, *.jpg, *.gif, *.tif, *.png,
*.ico, *.emf, *.wmf)|*.bmp;*.jpg;*.gif; *.tif; *.png; *.ico; *.emf; *.wmf";
SFD_Picture = new SaveFileDialog();
SFD_Picture.Title = "Сохранить как";
SFD_Picture.OverwritePrompt = true;
SFD_Picture.CheckPathExists = true;
SFD_Picture.Filter = "Изображение в формате PNG|*.png|" + "Изображение в
формате JPEG|*.jpg|" + "Изображение в формате BMP|*.bmp|" + "Изображение в формате GIF|
*.gif|" + "Изображение в формате TIF|*.tif";
CD_Pen = new ColorDialog();
CD_Fill = new ColorDialog();
// Инициализируем рабочее поле
// Создаём пустой Bitmap на основе размеров PictureBox
bitmap = new Bitmap(PB_Bitmap.Size.Width, PB_Bitmap.Size.Height);
// Инициализируем фон для поля рисования
graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.White); // Задаём белый цвет фона, иначе фон будет
прозрачным
//graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; //
Выключаем сглаживание
PB_Bitmap.Image = bitmap; // Отправляем Bitmap в PictureBox
// Инициализируем инструменты рисования по умолчанию
Main_Pen = new Pen(Color.Black, 1);
Main_Pen.EndCap = System.Drawing.Drawing2D.LineCap.NoAnchor; // Не задаём
маркер на конце пера
//graphics.DrawLine(Main_Pen, 10, 10, 11, 10); // Рисует точку в один
пиксель, при отсутствии маркера на Pen и толщине в 1
Color_Pen = Color.Black;
Color_Fill = Color.Violet;
Fill = new SolidBrush(Color_Fill);

439
// Инициализируем объекты рисования...
// ...нету их :)
// Инициализируем вспомогательные переменные по умолчанию
DrawLine = true;
линияToolStripMenuItem.Checked = true;
линияToolStripButton.Checked = true;
CurveLine = false;
DrawRect = false;
прямоугольникToolStripButton.Checked = false;
DrawCirc = false;
окружностьToolStripButton.Checked = false;
X = 1;
// Инициализируем прочее
StatusLabel.Text = "Приложение готово к работе";
}

Код выше помимо подготовки диалогов сохранения и открытия изображений,


также инициализируем поле для рисования во всём PictureBox. Заполняем при старте
PictureBox объектом Bitmap, очищает и задаёт белый фон. Важно понимать, что
сохранение происходит не из самого PictureBox, а из объекта растрового изображения,
загруженного в качестве текущего PictureBox.Image. Без задания фона пустого
Bitmap’а, сохранённое изображение будет иметь чёрный или прозрачный фон, в
зависимости от типа сохраняемого изображения (поддержки альфа-канала).

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

Первый этап реализован в методе PictureBox: MouseDown. Нажатие ЛКМ в


любом месте рабочей зоны приводит к сохранению координат (так устроено само
событие и его аргументы). В зависимости от выбранного режима эти координаты
используются по разному. Для отрисовки линии координаты нажатия отправляются в
значения X и Y для объекта Point,а для фигур формируется начальная точка объекта
Rectangle. Событие MouseDown для элемента PictureBox (PB_Bitmap) выглядит так:

private void PB_Bitmap_MouseDown(object sender, MouseEventArgs e)


{

if (e.Button == MouseButtons.Right) // Если нажата правая кнопка мыши


{
LightRect.Width = 0;
LightRect.Height = 0;
LightRect.X = e.X;
LightRect.Y = e.Y;
}

if (e.Button == MouseButtons.Left) // Если нажата левая кнопка мыши


{
// Если рисуем линию
if (DrawLine)
{
// Получаем координаты нажатия левой кнопки мыши в PictureBox
FirstPoint.X = e.X;
FirstPoint.Y = e.Y;
ToPoint = FirstPoint;

if (CurveLine)
{
FirstPoint = LastPoint;

440
}
}
// Если рисуем прямоугольник
if (DrawRect)
{
SelectRect.Width = 0;
SelectRect.Height = 0;
SelectRect.X = e.X;
SelectRect.Y = e.Y;
}
// Если рисуем окружность
if (DrawCirc)
{
CircleRect.Height = 0;
CircleRect.Width = 0;
CircleRect.X = e.X;
CircleRect.Y = e.Y;
}
}
}

Промежуточный этап для наглядности «усовершенствован». Если зажать ЛКМ во


время рисования линии, то за курсором от начальной точки будет тянуться чёрная
тонкая линия. В случае если будет включен режим рисования прямоугольника или
окружности, то вместо линии будет пунктирный прямоугольник. Эти действия
реализованы в событии MouseMove для PictureBox:

private void PB_Bitmap_MouseMove(object sender, MouseEventArgs e)


{

if (e.Button == MouseButtons.Right)
{
LightRect.Width = e.X - LightRect.X;
LightRect.Height = e.Y - LightRect.Y;
}

if (e.Button == MouseButtons.Left)
{
// Если рисуем линию, отображаем заготовку линии
if (DrawLine)
{
// Отображаем заготовку линии до тех пор пока не отпустим левую
кнопку мыши
ControlPaint.DrawReversibleLine(PB_Bitmap.PointToScreen(FirstPoint),
PB_Bitmap.PointToScreen(ToPoint), Color.Black);
ToPoint = new Point(e.X, e.Y);
ControlPaint.DrawReversibleLine(PB_Bitmap.PointToScreen(FirstPoint),
PB_Bitmap.PointToScreen(ToPoint), Color.Black);
if (CurveLine) // Убираем возможные "артефакты", возникающие при
отрисовки непрерывной линии
PB_Bitmap.Refresh();
}
// Если рисуем прямоугольник, отображаем заготовку прямоугольника
пунктирными линиями
if (DrawRect)
{

ControlPaint.DrawReversibleFrame(PB_Bitmap.RectangleToScreen(SelectRect), Color.Black,
FrameStyle.Dashed);
SelectRect.Width = e.X - SelectRect.X; // Получаем значение ширины
прямоугольника
SelectRect.Height = e.Y - SelectRect.Y; // Получаем значение высоты
прямоугольника

441
ControlPaint.DrawReversibleFrame(PB_Bitmap.RectangleToScreen(SelectRect), Color.Black,
FrameStyle.Dashed);
//PB_Bitmap.Refresh();
}
// Если рисуем окружность, отображаем заготовку окружности пунктирными
линиями
if (DrawCirc)
{

ControlPaint.DrawReversibleFrame(PB_Bitmap.RectangleToScreen(CircleRect), Color.Black,
FrameStyle.Dashed);
CircleRect.Width = e.X - CircleRect.X;
CircleRect.Height = e.Y - CircleRect.Y;

ControlPaint.DrawReversibleFrame(PB_Bitmap.RectangleToScreen(CircleRect), Color.Black,
FrameStyle.Dashed);
//PB_Bitmap.Refresh();
}
}
}

Третий этап собственно реализует рисование объекта. После «отжатия» левой


кнопки мыши будут получены координаты конечной точки и отрисован объект. Это
реализует событий MouseUp для PictureBox:

private void PB_Bitmap_MouseUp(object sender, MouseEventArgs e)


{

if (e.Button == MouseButtons.Right)
{
graphics = Graphics.FromImage(PB_Bitmap.Image);
graphics.DrawRectangle(Main_Pen, LightRect); // Нарисуем прямоугольник-
контур для осветлённой области
graphics.Dispose();
PB_Bitmap.Invalidate();
}

if (e.Button == MouseButtons.Left) // Если нажата левая кнопка мыши


{
// Рисуем в PictureBox (только в загруженном в элементе изображении)
graphics = Graphics.FromImage(PB_Bitmap.Image);

if (DrawLine)
{
LastPoint.X = e.X;
LastPoint.Y = e.Y;
graphics.DrawLine(Main_Pen, FirstPoint, LastPoint);
}

if (DrawRect)
{
if (Color_Fill != Color.Violet) { graphics.FillRectangle(Fill,
SelectRect); } // Заполнение цветом прямоугольной области ограниченной SelectRect
graphics.DrawRectangle(Main_Pen, SelectRect);
}

if (DrawCirc)
{
if (Color_Fill != Color.Violet) { graphics.FillEllipse(Fill,
CircleRect); } // Заполнение цветом эллептической области ограниченной CircleRect
graphics.DrawEllipse(Main_Pen, CircleRect);
}
graphics.Dispose();

442
PB_Bitmap.Invalidate(); // Обновляем PictureBox
}
}

Реализуем ещё одну интересную функцию, которую «повесим» на событие


нажатия правой кнопки мыши в области PictureBox. Код события MouseClick:

private void PB_Bitmap_MouseClick(object sender, MouseEventArgs e)


{

if (e.Button == MouseButtons.Right)
{
StatusLabel.Text = "Произведено осветление зоны";

int newRed, newGreen, newBlue;


Color pixel;
// Операция увеличения яркости может занять много времени, пользователя
предупредят, если выбран большой участок
if ((LightRect.Width > 500) || (LightRect.Height > 500))
{
DialogResult result = MessageBox.Show("Выделенная область велика! " +
"Изменение яркости может требовать значительного времени!", "Простой растровый редактор
(C#) :: Изменения яркости", MessageBoxButtons.OKCancel);
// При нажатии кнопки "Отмена" выходим из метода
// и возвращаемся к месту его вызова
if (result == DialogResult.Cancel) return;
}
/* Перебираем последовательно все пиксели данного участка и удваиваем
значение яркости компонент RGB пикселей */
// Перебор по горизонтали слева направо...
for (int x = LightRect.X; x < LightRect.X + LightRect.Width; x++)
{
// и по вертикали сверху вниз...
for (int y = LightRect.Y; y < (LightRect.Y + LightRect.Height); y++)
{
// Считываем текущий пиксель
pixel = bitmap.GetPixel(x, y);
// Увеличиваем яркость цветовых компонент пикселя
newRed = (int)Math.Round(pixel.R * 2.0, 0);
if (newRed > 255) newRed = 255;
newGreen = (int)Math.Round(pixel.G * 2.0, 0);
if (newGreen > 255) newGreen = 255;
newBlue = (int)Math.Round(pixel.B * 2.0, 0);
if (newBlue > 255) newBlue = 255;
// Присваиваем пикселю новые цветовые значения
bitmap.SetPixel(x, y, Color.FromArgb((byte)newRed,
(byte)newGreen, (byte)newBlue));
}
}
}
}

Код выше «осветляет» прямоугольную область при зажатой правой кнопке


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

Наконец, сделаем ластик. Работать он будет при зажатой клавиши Shift (+


нажатие ПКМ в области, которую нужно стереть). Сделаем ластик также ещё и
карандашом. Его цвет будет завесить от цвета заливки, а толщина поля «стирания» от
толщины пера.

Найдём:

443
Boolean DrawCirc;
Double X;

Добавим после:

// Ластик
Pen Pen_Erase;
Boolean DrawErase;
SolidBrush Erase;
Rectangle EraseRect = new Rectangle();

Найдём:

// Инициализируем прочее
StatusLabel.Text = "Приложение готово к работе";

Добавим после:

// Ластик
Pen_Erase = new Pen(Color.White, 1);
DrawErase = false;
Erase = new SolidBrush(Color.White);

Найдём:

private void PB_Bitmap_MouseMove(object sender, MouseEventArgs e)


{

Добавим после:

// Ластик
if (e.Button == MouseButtons.Right && DrawErase == true)
{
EraseRect.X = e.X - Convert.ToInt32(X);
EraseRect.Y = e.Y - Convert.ToInt32(X);
EraseRect.Width = Convert.ToInt32(X) * 2;
EraseRect.Height = Convert.ToInt32(X) * 2;
graphics = Graphics.FromImage(PB_Bitmap.Image);
graphics.FillEllipse(Erase, EraseRect);
graphics.Dispose();
PB_Bitmap.Invalidate();
StatusLabel.Text = "Режим ластика/карандаша";
}

Перепишем код события нажатия на кнопку «Цвет заливки»:

private void цветЗаливкиToolStripMenuItem_Click(object sender, EventArgs e)


{
if (CD_Fill.ShowDialog() == DialogResult.OK)
{
Color_Fill = CD_Fill.Color;
Fill = new SolidBrush(Color_Fill);
Erase = new SolidBrush(Color_Fill);
}
else
{
Color_Fill = Color.Violet; // Цвет сбора заливки
Fill = new SolidBrush(Color_Fill);
Erase = new SolidBrush(Color.White);
}
}

444
А также инициализируем два события главной формы (отлов нажатия и отжатия
клавиш левый и правый Shift): KeyDown и KeyUp:

private void LWP14Main_KeyDown(object sender, KeyEventArgs e)


{

if (e.KeyCode == Keys.ShiftKey) // Если нажат Shift


{
DrawErase = true;
}
}

private void LWP14Main_KeyUp(object sender, KeyEventArgs e)


{

if (e.KeyCode == Keys.ShiftKey) // Если нажат Shift


{
DrawErase = false;
}
}

Готово. Можно компилировать и проверять работоспособность.

5. Завершающая часть

Компилируем приложение (Release) и запускаем. Рисуем что хотим, при помощи


всех доступных инструментов, осветляем области и жмём Сохранить как..., после чего
сохраняем творение на диск и проверяем что и как сохранилось:

Рис. 5. 1. Модифицированное приложение Windows Forms: рисуем

Сохранённый документ в формате *.gif (Рис. 5. 2):

445
Рис. 5. 2. Результат работы приложения: изображение Рисунок.gif, открытое в
стандартном редакторе изображений Windows 7

Теперь жмём Очистить и далее откроем при помощи нашего приложения


сторонний рисунок:

446
Рис. 5. 3. Модифицированное приложение Windows Forms: загружаем сторонний
рисунок

И осветляем его:

447
Рис. 5. 4. Модифицированное приложение Windows Forms: осветляем сторонний
рисунок

Сохраняем полученный рисунок и проверяем, всё ли сохранилось.

448
Рис. 5. 5. Модифицированное приложение Windows Forms: применяем
«ластик/карандаш» с толщиной пера в 25 пикселей (Shift+ПКМ) на заново открытом
рисунке

6. О приложении к Лабораторной работе № 14

Получившуюся программу (LWP14SimpleRasterEditor.exe), собранную из


кусков кода приведённых в данной лабораторной работе, а также архив с
изображениями иконок для меню (Icons.zip), использованный в данной работе, можно
загрузить по ссылке в конце этого материала (сслыка доступна в программном
продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

15. Лабораторная работа № 15: Векторный редактор изображений

Лабораторная работа № 15: Векторный редактор изображений

Содержание

28.Вводная часть

449
29.Создание решения, приложения Windows Forms и библиотеки классов
30.Модификация приложения Windows Forms: подготовка интерфейса
редактора и добавление файлов ресурсов
31.О будущей функциональности векторного редактора изображений
32.Модификация приложения Windows Forms: подготовка библиотеки
классов
33.Модификация приложения Windows Forms: функциональность
векторного редактора
34.Завершающая часть
35.О приложении к Лабораторной работе № 15

1. Вводная часть

В этой работе будет рассмотрена работа с векторной графикой и рисованием в


массиве простых объектов на подобии точек и линий, кривых, эллипсов и
прямоугольников. Однако, в отличие от предыдущей лабораторной работы практикума,
приложение будет способно «запоминать» нарисованный объект — вести историю
рисования. В любой момент можно «выделить» ранее нарисованный объект при
помощи мыши и изменить его свойства (толщину, цвет, положение и размер). Мы будем
создавать векторный графический редактор.
Конечным итогом редактора в любом случае должно стать некий файл с
изображением. Однако, способность формировать BMP или JPEG-файл была работе.
Для нашего редактора, сохраняемым типом файлов станет некий собственный тип,
«сериализирующий» и сохраняющий все объекты как массив (список) параметров.
Фактически такой файл не будет отличаться от того же JPEG (файл это всегда
последовательность битов), и будет являться «собственным» графическим форматом.
После открытия такого файла, можно изменить параметры ранее нарисованных
объектов, а также можно продолжить рисование.
В данной работе будет рассмотрено, как создать приложение Windows Forms
для рисования графических объектов в области клиента Windows (непосредственно в
специальном пользовательском элементе управления), рисование будет осуществляться
с помощью мыши. Инструментами рисования, реализованные в данной работе
являются: прямоугольник, эллипс, линия и карандаш. Есть хорошо известные методы
создания таких типов приложений, в частности: взаимодействие с мышью (без
мерцания при рисовании), осуществления рисования и выделенным инструментом,
выделение нарисованных объектов, управление объектами Z-порядка и прочее.

Заходя вперёд скажем, что наше решение будет содержать два проекта:
LWP15Draw— Приложение Windows Forms и LWP15Toolkit — Библиотека классов.
LWP15Tools реализует функциональность приложения, а LWP15Toolkit содержит классы
для управления документами.

Данная лабораторная работа была создана и переработана, на основе статьи


«DrawTools» (Alex Fr, 25 января 2007 года).

2. Создание решения, приложения Windows Forms и библиотеки классов

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать решение, для этого выполним последовательно: Файл
-> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

450
Выберем слева в пункте Установленные шаблоны подпункт Другие типы
проектов и далее Решения Visual Studio, далее найдём в списке Новое решение. В
поле Имя вводим LWP15. Так будет назваться общая директория под два будущих
проекта.

Рис. 2. 1. Создание нового проекта (пустого решения)

Жмём ОК. Теперь у нас есть пустое решение:

Заполним его первым проектом. Выполним последовательно Файл -> Создать ->
Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N).

В открывшемся окне выберем слева в пункте Установленные шаблоны язык


Visual C#, далее найдём в списке Приложение Windows Forms. Также здесь можно
выбрать какой использовать «фреймворк» (набора компонентов для написания
программ). В нашем случае выберем .NET Framework 4.

451
Рис. 2. 2. Окно создания нового проекта (проекта приложения Windows Forms)

В поле Имя вводим LWP15Draw — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле решение выбираем из
списка Добавить в решение, таким образом объединяя решение и проект. При этом
поменяется строка в пункте Расположение (добавится путь к директории решения). В
поле Имя решения вводится либо название программы «по умолчанию» из поля Имя
автоматически, либо можно ввести своё собственное. Под этим именем будет создана
конечная папка проекта (если Имя и Имя решения разные).

452
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

И наконец, нужно создать последний проект в решении. Им станет проект типа


Библиотека классов с именем LWP15Tools (рис. 2. 4).

453
Рис. 2. 4. Вводим данные нового проекта библиотеки классов

Рис. 2. 5. Обозреватель решений: состав итогового решения сформированного


средой разработки

454
Выберем также, какой проект считать главным и запускать в режиме отладки. В
обозревателе решений нажмём на имя решения ( ).
Перейдём вниз на панель Свойства. Параметр для пункта Запускаемый проект
ставим LWP15Draw.

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug (и компиляция


всего решения)

3. Модификация приложения Windows Forms: подготовка интерфейса


редактора и добавление файлов ресурсов

455
Так как наша программа будет похожа на редактор изображений, нам
необходимы все атрибуты такого редактора. У нас будет верхнее меню для навигации и
нижняя строка состояния для отображения подсказок.

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


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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP15Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Векторный
графический редактор (C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 640;
480
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя, СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Размести на форме (перетаскиванием в любое место) три панели:

ToolStrip ( );

MenuStrip ( );

StatusStrip ( ).

Все эти элементы расположены на панели инструментов в разделе Меню и


панели инструментов:

456
То, что должно получиться в итоге, показано на рисунке ниже:

Рис. 3. 1. Модифицированная форма приложения и расстановка необходимых


элементов управления

Главным элементом рисования будет пользовательский элемент управления.


Нужно его создать. Выполним последовательно: Проект -> Добавить
пользовательский элемент управления...: откроется окно «Добавление нового
элемента – LWP15». Нужный элемент для добавления будет уже выбран. Впишем
только строчку в поле Имя: DrawArea.cs. Жмём ОК.

457
Рис. 3. 2. Добавление нового элемента – LWP15Draw: Пользовательский элемент
управления

Перейдём к свойствам элемента (одинарное нажатие на DrawArea.cs в


обозревателе решений). Изменим свойство BackColor на значение White. Получим
следующее:

DrawArea.cs:
(Name): DrawArea
BackColor: White

Наше поле для рисования готово. Добавим его на форму позже. А пока, нам
необходимы файлы ресурсов. Ими станут файлы изображений для курсора и панели
инструментов.

458
Начнём с изображений для курсора. Курсоров будет пять. Для создания курсора
выполним: Проект -> Добавить новый элемент... (Ctrl+Shift+A). В открывшемся
окне ищем Файл курсора.

Рис. 3. 3. Добавление нового элемента – LWP15Draw: Файл курсора

Первый курсор для рисования линий. Имя: C_Line.cur. Вписываем это имя и
жмём Добавить. Откроется редактор курсоров. Рисуем нечто подобное:

Второй курсор для рисования эллипсов. Имя: C_Ellipse.cur. Рисуем нечто


подобное:

459
Третий курсор для рисования прямоугольников. Имя: C_Rectangle.cur. Рисуем
нечто подобное:

Четвёртый курсор для рисования карандашом. Имя: C_Pencil.cur. Рисуем нечто


подобное:

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


Имя: C_PolyHandle.cur. Рисуем нечто подобное:

460
ПРИМЕЧАНИЕ № 3: Очень важно для каждого добавленного курсора (для
файла курсора *.cur в обозревателе решений ИЗМЕНИТЬ свойство Действие по
построению на значение Внедрённый ресурс. Если этого не сделать, могут
возникнуть ситуация при которой приложение будет компилироваться, но работать
некорректно (будет отсутствовать функциональность рисования).

Рис. 3. 4. Изменение свойства «Действие по построению» для файла курсора


(C_Ellipse.cur)

Остались иконки для панели инструментов. Создаём кнопку и сразу же


применяем на кнопки иконку и меняем свойства. Для создания кнопки на панели
инструментов, нажимаем на toolStrip1, далее на правую кнопку мыши и жмём
Вставить стандартные элементы:

Получим следующее:

Снова выделим toolStrip1 и нажмём ПКМ, затем выберем Правка элементов...:

461
Рис. 3. 5. Редактор коллекции элементов для toolStrip1

Удаляем кнопки печать..., вырезать..., копировать... и вставка... (выделяем


элементов слева и жмём крестик ):

Добавляем ещё один разделитель (Separator) и перемещаем его стрелкой вверх


на позицию под toolSeparator1:

Добавляем первую кнопку Button (Выделение) под первый разделитель


(toolStripSeparator) на панель со следующими свойствами:

(Name): выделениеToolStripButton
Text: &Выделение
ToolTipText: Выделение
Image
Импортируем иконку

462
Для вставки изображения, выделяем свойство Image и жмём «...» справа в поле
значения:

В открывшемся окне Выбор ресурса, жмём Импорт (Локальный ресурс) и


выбираем файл Pointer.png из архива Icons.zip (архив можно загрузить по ссылке в
конце этого материала (сслыка доступна в программном продукте)). Также,
изображение для иконки можно создать в самой среде разработки (открываем
Resources.resx в директории Properties проекта LWP15Draw, далее Добавить
ресурс -> Создать изображение -> Изображение PNG...).

Добавляем вторую кнопку Button (Карандаш) под первую кнопку на панель со


следующими свойствами:

(Name): карандашToolStripButton
Text: &Карандаш
ToolTipText: Карандаш
Image:
Импортируем иконку

Добавляем третью кнопку Button (Линия) под вторую кнопку на панель со


следующими свойствами:

(Name): линияToolStripButton
Text: &Линия
ToolTipText: Линия
Image:
Импортируем иконку

Добавляем четвёртую кнопку Button (Эллипс) под предыдущую кнопку на


панель со следующими свойствами:

(Name): эллипсToolStripButton
Text: &Эллипс
ToolTipText: Эллипс
Image:
Импортируем иконку

Добавляем пятую кнопку Button (Прямоугольник) под предыдущую кнопку на


панель со следующими свойствами:

(Name): прямоугольникToolStripButton
Text: &Прямоугольник
ToolTipText: Прямоугольник
Image:
Импортируем иконку

После разделителя добавим ещё две кнопки. Отменить и Вернуть.

(Name): отменитьToolStripButton
Text: &Отменить
ToolTipText: Отменить

463
Image:
Импортируем иконку

(Name): вернутьToolStripButton
Text: В&ернуть
ToolTipText: Вернуть
Image:
Импортируем иконку

Итог:

Строка меню (menuStrip1), будет содержать те же команды плюс некоторые


новые. Поступаем точно также как и с панелью инструментов. Вначале добавляем на
панель стандартные элементы (menuStrip1 -> ПКМ -> Вставить стандартные
элементы):

Переименуем через свойства меню Сервис в Рисование (выделим элемент


меню и перейдём на свойства элемента):

(Name): рисованиеToolStripMenuItem
Text: Р&исование

Для меню Файл итоговые элементы такие:

Удаляем лишние элементы (Печать и Предварительный просмотр) и


добавляем новые (Последние файлы и ещё один Separator). Редактирование меню
можно упросить следующим образом. Выделяем левой кнопкой мыши пункт Файл ->
ПКМ -> Правка DropDownItems...:

464
Рис. 3. 6. Редактируем меню Файл для menuStrip1

Для меню Правка итоговые элементы такие:

Для меню Рисование итоговые элементы такие:

Иконки добавляются для элемента меню через свойство Image. Импортируем


соответствующие иконки для соответствующего пункта меню.

465
Для меню Справка итоговые элементы такие:

Добавим две формы. Первая форма станет диалоговым окном, вызываемым по


кнопке О программе меню Справка. Выполняем Проект -> Добавить форму
Windows..., Имя указываем как LWP15About. Жмём Добавить. Меняем свойства
формы:

Рис. 3. 7. Добавление нового элемента – LWP15Draw: Форма Windows Forms

(Name) Должно быть LWP15About


^ Поменяем внутреннее имя формы.
Text изменим с LWP15About на Векторный
графический редактор (C#) :: О
программе
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.
MinimizeBox изменим с True на False
^ Уберём кнопку Свернуть.
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.

466
Size изменим со значений 300; 300 на 400;
150
^ Поменяем размер формы.
FormBorderStyle изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.
StartPosition изменим с WindowsDefaultLocation на
CenterScreen
^ Определим первоначальное положение формы при вызове.

Размести на форме одну кнопку Button и один Label со следующими свойствами:

Button:
(Name): B_OK
Text: Закрыть
Label:
(Name): L_About
Text: О программе
AutoSize: False
Size: 250; 65

Вернёмся к свойствам формы и изменим свойство AcceptButton на B_OK:

В итоге получим следующую форму:

Рис. 3. 8. Форма LWP15About

Вторая форма и последняя форма, которую необходимо добавить, будет


выполнять функции диалога выбора параметров рисования. Имя формы:
LWP15Properties. Свойства следующие:

(Name) Должно быть LWP15Properties


^ Поменяем внутреннее имя формы.
Text изменим с LWP15About на Векторный
графический редактор (C#) ::
Параметры
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.
MinimizeBox изменим с True на False
^ Уберём кнопку Свернуть.

467
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 385;
135
^ Поменяем размер формы.
FormBorderStyle изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.
StartPosition изменим с WindowsDefaultLocation на
CenterScreen
^ Определим первоначальное положение формы при вызове.

Рис. 3. 9. Форма LWP15Properties

Расставим элементы:

Button:
(Name): B_OK
Text: Применить
Button:
(Name): B_Cancel
Text: Отменить
Button:
(Name): B_SelectColor
Text: ...
TextAlign: MiddleCenter
Label:
(Name): label1
Text: Текущий цвет:
Label:
(Name): label2
Text: Толщина пера:
ComboBox:
(Name): CB_PenWodth
DropDownStyle: DropDownList
Label:
(Name): L_Color
BorderStyle: Fixed3D
TextAlign: MiddleCenter
Text:
Label:
(Name): L_PenWidth

468
BorderStyle: Fixed3D
TextAlign: MiddleCenter
Text:

Вернёмся к свойствам формы и изменим свойство AcceptButton на B_OK и


CancelButton на B_Cancel.

Наконец создаём обычный StatusLabel в строке состояния главной формы


LW15Main:

StatusLabel:
(Name): toolStripStatusLabel
Text: Строка состояния

Приготовления завершены.

Окончательный вид приложения должен быть таким:

469
Рис. 3. 10. Модифицированная форма приложения и расстановка необходимых
элементов управления
4. О будущей функциональности векторного редактора изображений

LWP15Tools:

Библиотека LWP15Tools содержит набор классов, которые могут быть


использованы для работы с документами в приложении Windows Forms. Экземпляры
классов, из библиотеки LWP15Tools хранятся на главной форме проекта LWP15Draw и
используются для общих операций, связанных с файлом документа.

Библиотека LWP15Tools будет состоять из следующих полноценных классов:

 Класс DocManager: осуществляет операции, связанные с файлом: открытие,


создание, сохранение, обновление названия формы и регистрация типа файлов
для оболочки Windows.
 Класс DragDropManager: позволяет открывать файлы из проводника Windows в
приложении Windows Forms.
 Класс MruManager: список управляет наиболее часто используемыми файлами
(Последние файлы в пункте меню Файл).
 Класс PersistWindowState: позволяет сохранить последнее состояние окна в
реестре и восстановить его при загрузке главной формы.

LWP15Draw:

Структура классов приложения и наследование реализовано следующим


образом:

Рис. 4. 1. Классы проекта LWP15Draw

Назначения классов (файлов) следующее:

 DrawArea — пользовательский элемент управления, который заполняет главное


окно клиентской области. Содержит экземпляр класса GraphicsList. Рисует
графические объекты, обрабатывает ввод от мыши передачей команд в
GraphicsList. Реализует графическое пространство в явном виде для
интерфейса формы.
 GraphicsList — список графических объектов. Содержит ArrayList графических
объектов. «Общается» с графическими объектами в общем виде, с
использованием методов DrawObject. Реализует графическое пространство в
виртуальном виде приложения.
 DrawObject — абстрактный базовый класс для всех графических объектов.
 DrawRectangle — рисование графического объекта прямоугольника.

470
 DrawEllipise — рисование графического объекта эллипса.
 DrawLine — рисование графического объекта линии.
 DrawPolygon — рисование графического объекта непрерывной
линии/карандаш.
 Tool — абстрактный базовый класс для всех инструментов рисования.
 ToolPointer — указатель инструмента (нейтральный инструмент). Содержит
реализации для выбора, перемещения, изменения размера графических
объектов.
 ToolObject — абстрактный базовый класс для всех инструментов, создающих
новый графический объект.
 ToolRectangle — реализует инструмент «прямоугольник».
 ToolEllipse — реализует инструмент «эллипс».
 ToolLine — реализует инструмент «линия».
 ToolPolygon — реализует инструмент «непрерывная линия/карандаш».

Сериализация:

Класс GraphicList реализует интерфейс ISerializable, который позволяет


производить двоичной сериализации объекта класса. Класс DrawObject имеет две
virtual-функции, используемые для сериализации:

public virtual void SaveToStream(SerializationInfo info, int orderNumber)


{
// ...
}

public virtual void LoadFromStream(SerializationInfo info, int orderNumber)


{
// ...
}

Эти функции реализованы в каждом производном классе. Двоичный


сохранённый файл имеет следующий формат:

Число объектов
Имя типа
Объект
Имя типа
Объект
...
Имя типа
Объект

Это позволяет писать код общий сериализации в классе GraphicList не зная


никаких подробностей о сериализованных объектов (абстрактно, для любого объекта):

private const string entryCount = "Count";


private const string entryType = "Type";
// Сохранить список в поток
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter =
true)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext
context)
{
// Число объектов
info.AddValue(entryCount, graphicsList.Count);
int i = 0;

471
foreach (DrawObject o in graphicsList)
{
// Тип объекта
info.AddValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}",
entryType, i),
o.GetType().FullName);
// Сам объект
o.SaveToStream(info, i);
i++;
}
}
// Загружаем из потока
protected GraphicsList(SerializationInfo info, StreamingContext context)
{
graphicsList = new ArrayList();
// Число объектов
int n = info.GetInt32(entryCount);
string typeName;
object drawObject;

for ( int i = 0; i < n; i++ )


{
// Тип объекта
typeName = info.GetString(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}",
entryType, i));
// Создаём объект по имени типа через using Reflection)
drawObject = Assembly.GetExecutingAssembly().CreateInstance(
typeName);
// Заполняем объект из потока
((DrawObject)drawObject).LoadFromStream(info, i);
graphicsList.Add(drawObject);
}
}

Проверка нажатия кнопки мыши:

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


объект?

Класс DrawObject имеет virtual-функцию HitTest, которая определяет,


принадлежит ли точка графическому объекту (проверят, куда нажал пользователь):

public virtual int HitTest(Point point)


{
return -1;
}

Производные классы используют virtual PointInObject для проверки нажатия.


Эта функция вызывается из HitTest. Класс DrawRectangle реализует эту функцию:

protected override bool PointInObject(Point point)


{
return rectangle.Contains(point);
// rectangle - принадлежит типу Rectangle
}

Чуть более сложный вариант определения нажатия по линии:

472
protected override bool PointInObject(Point point)
{
GraphicsPath areaPath;
Pen areaPen;
Region areaRegion;
// Создаём путь, который содержит широкую линию
// Для лёгкого выбора мышью
AreaPath = new GraphicsPath();
AreaPen = new Pen(Color.Black, 7);
AreaPath.AddLine(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y);
// startPoint и endPoint - принадлежат типу Point
AreaPath.Widen(AreaPen);
// Создаём область из пути
AreaRegion = new Region(AreaPath);
return AreaRegion.IsVisible(point);
}

Обработка состояния бездействия приложения:

Каждое Windows Forms приложение в своём составе имеет элементы управления


такие как кнопки пунктов меню, кнопки панелей инструментов и прочее. В зависимости
от текущей ситуации и команды пользователя, эти элементы управления могут иметь
различные состояния: включены/выключены, отмечены/не отмечены,
видимые/невидимые и так далее. Действие пользователя может изменить это
состояние. Настройка состояний элементов управления в каждом обработчике
сообщений вызывать ошибку. Вместо этого, это управлять состояние элемента
управления лучше через функции, которые вызываются после каждого действия
пользователя. В MFC (Visaul C++) существует функция ON_UPDATE_COMMAND_UI,
которая позволяет обновить состояние кнопок панели инструментов во время
бездействия приложения. Такая возможность может осуществляться также и в .NET
программе.

Рассмотрим ситуацию, когда пользователь нажимает кнопку на панели


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

public void SetStateOfControls()


{
// Выбор активного инструмента
tbPointer.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Pointer);
tbRectangle.Pushed = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Rectangle);
tbEllipse.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Ellipse);
tbLine.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Line);
tbPolygon.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Polygon);

menuDrawPointer.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Pointer);
menuDrawRectangle.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Rectangle);
menuDrawEllipse.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Ellipse);
menuDrawLine.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Line);

473
menuDrawPolygon.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Polygon);
// ...
}
// Инструмент "Прямоугольник" выбран
private void CommandRectangle()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Rectangle;
}

5. Модификация приложения Windows Forms: подготовка библиотеки классов

Проект LWP15Tools будет содержать четыре самостоятельных класса.

Для выбора, куда именно вставлять новый класс, выбираем в обозревателе


решений проект LWP15Tools:

Для добавления нового файла в проект необходимо выполнить: Проект ->


Добавить класс... (Shift+Alt+C). Также аналогично действие можно произвести
щёлкнув на имени проекта правой кнопкой мыши, затем Добавить -> Класс...:

В окне Добавление нового элемента – LWP15Tools в поле Имя вводим


DocManager.cs. Также добавим в проект добавим ссылку на библиотеку
System.Windows.Forms (вкладка .NET окна Добавить ссылку). Для добавления
ссылки в обозревателе решений нажмём правую кнопку мыши по пункту Ссылки в
проекте LWP15Tools затем Добавить -> Добавить ссылку...:

Текст первого файла класса DocManager.cs: [искомый код можно найти в


приложении к данной лабораторной работе в (описания можно того или иного
приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 1 (Библиотека классов LWP15Tools)].

Небольшое замечание. Ранее они уже были даны в предыдущих лабораторных


работах, но приводим напоминание...

Обратим внимание на следующий код:

474
#region Класс DocManagerData
...
#endregion

Этот код формирует из участков кода файла блоки, которые можно закрыть
нажав на «минус» слева от строчи #region и развернуть нажав «плюс» слева от
свёрнутого элемента. Выглядит это так:

Также обратим внимание вот на этот код:

/// <summary>
/// Открываем документ
/// </summary>
/// <param name="newFileName">
/// Имя файла документа. Empty - функция выводит OpenFileDialog
/// </param>
/// <returns></returns>

Это описание, в данном случае для функции. Отображается во время


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

О самом классе. Класс выполняет достаточно много функций связанных с


сопровождением файлов приложения (создание нового документа, сохранение и
открытие), а также работает с системным реестром (ассоциирует тип файла программы
с нашим приложением). Забегая вперёд, можно сказать что для поддержки работы с
реестром необходимо Запустить приложение один раз от имени администратора
для Windows Vista и Windows 7 (если включён UAC и параметрами для него выставлены
по умолчанию). В этом случае всех ключи в реестре будут созданы, и дальнейшая
работа приложения с ними будет обеспечена. Основной функцией класса является
следующая функция:

/// <summary>
/// Инициализация
/// </summary>
/// <param name="data"></param>
public DocManager(DocManagerData data)
{

Функция инициализирует класс и принимает данные (data) для работы класса.


Это данные связанные с типом файлов для приложения, в частности основная
инициализация для главной формы LWP15Main выглядит так:

private DocManager docManager;


...
// DocManager
DocManagerData data = new DocManagerData();
data.FormOwner = this;
data.UpdateTitle = true;

475
data.FileDialogFilter = "Файлы LWP15Draw (*.lwp)|*.lwp|Все файлы (*.*)|*.*";
data.NewDocName = "New.lwp";
data.RegistryPath = registryPath;

docManager = new DocManager(data);


docManager.RegisterFileType("lwp", "lwpfile", "Файл LWP15Draw");
...

Переходим к следующему классу. Назовём файл для него


PersistWindowState.cs. Для работы кода добавим новую ссылку System.Drawing.
Код файла такой: [искомый код можно найти в приложении к данной лабораторной
работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8
протокола работы), а именно необходимо открыть Приложение № 1 (Библиотека
классов LWP15Tools)].

Класс отвечает за работу с окном приложения (сохраняет в реестр размер и


положение окна, а также состояние минимизации окна, если приложение было
свёрнуто).

Следующий файл класса будет носить имя MruManager.cs (класс MruManager).


Класс будет обеспечивать работу со список недавно использованных (последних)
файлов. По умолчанию будет отображаться 10 наиболее часто используемых файлов.
Число символов в пути и имени не будет превышать 40. Работа списка будет выглядеть
примерно так:

Код файла класса следующий: [искомый код можно найти в приложении к


данной лабораторной работе в (описания можно того или иного приложения можно
посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть
Приложение № 1 (Библиотека классов LWP15Tools)].

Последний класс отвечает за открытие файла приложение перетаскиванием из


проводника Windows. Имя файла класса: DragDropManager.cs. Код файла
следующий: [искомый код можно найти в приложении к данной лабораторной работе в
(описания можно того или иного приложения можно посмотреть в пунтке № 8
протокола работы), а именно необходимо открыть Приложение № 1 (Библиотека
классов LWP15Tools)].

Таким образом, был сформирован файл библиотеки LWP15Tools.dll для


приложения (можно откомпилировать). Можно переходить к самому приложению.

6. Модификация приложения Windows Forms: функциональность векторного


редактора

Для начала определим все необходимые для работы приложения класса внутри
самого приложения.

Первый класс с именем файла DrawObject.cs станет основным классом для


рисования любых объектов. На его основе будут созданы классы для рисования
конкретных объектов типа линий или прямоугольников. Класс будет содержать все
возможные функции, события и свойства, необходимы для работы с графическим
объектом. А именно этот класс будет определять функция рисования объекта (Draw),
выделения, перемещения, изменения размеров и определения количества ключевых
точек. Что такое ключевая точка? Для линии это точки на концах линии, которые
«подсвечиваются» небольшими прямоугольниками. Если выделить линию и навести

476
мышь на такую точку, курсор будет изменён (зависит от типа точки и объекта
рисования). Нажатие на ключевую точку обеспечивает операцию изменения размеров
объекта либо перемещения (зависит от объекта рисования). Любой объект во время
выделения мышью подсвечивается ключевыми точками. Например, нарисованный и
выделенный прямоугольник имеет 8 ключевых точек:

Код файла DrawObject.cs: [искомый код можно найти в приложении к данной


лабораторной работе в (описания можно того или иного приложения можно посмотреть
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 2
(Файлы классов Draw...)].

Класс GraphicsProperties напрямую связан с дочерней формой LWP15Properties.


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

Откроем код формы (выделим LWP15Properies.cs -> нажмём правую кнопку


мыши -> Перейти к коду (F7)). Найдём:

public LWP15Properties()
{
InitializeComponent();
}

Добавим после:

private GraphicsProperties properties;


private const string undefined = "Не задано";
private const int maxWidth = 10;

public GraphicsProperties Properties


{
get { return properties; }
set { properties = value; }
}

В самое начало формы добавим код:

using System.Globalization;

Найдём:

namespace LWP15Draw
{
public partial class LWP15Properties : Form
{

Изменим на:

namespace LWP15Draw
{

477
partial class LWP15Properties : Form
{

Проинициализируем первое событие формы LWP15Properties: Load со


следующим кодом:

private void LWP15Properties_Load(object sender, EventArgs e)


{
InitControls();
SetColor();
SetPenWidth();
}

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

private void InitControls()


{
for (int i = 1; i <= maxWidth; i++)
{
CB_PenWidth.Items.Add(i.ToString(CultureInfo.InvariantCulture));
}
}

private void SetColor()


{
if (properties.Color.HasValue) L_Color.BackColor = properties.Color.Value;
else L_Color.Text = undefined;
}

private void SetPenWidth()


{

if (properties.PenWidth.HasValue)
{
int penWidth = properties.PenWidth.Value;
if (penWidth < 1) penWidth = 1;
if (penWidth > maxWidth) penWidth = maxWidth;
label2.Text = penWidth.ToString(CultureInfo.InvariantCulture);
CB_PenWidth.SelectedIndex = penWidth - 1;
}
else { label2.Text = undefined; }
}

private void ReadValues()


{
if (CB_PenWidth.Text != undefined) { properties.PenWidth =
CB_PenWidth.SelectedIndex + 1; }
if (L_Color.Text.Length == 0) { properties.Color = L_Color.BackColor; }
}

Событие SelectedIndexChanged для ComboBox этой формы:

private void CB_PenWidth_SelectedIndexChanged(object sender, EventArgs e)


{
int width = CB_PenWidth.SelectedIndex + 1;
L_PenWidth.Text = width.ToString(CultureInfo.InvariantCulture);
}

Событие Click нажатия кнопки B_SelectColor:

private void B_SelectColor_Click(object sender, EventArgs e)


{

478
ColorDialog dlg = new ColorDialog();
dlg.Color = L_Color.BackColor;

if (dlg.ShowDialog(this) == DialogResult.OK)
{
L_Color.BackColor = dlg.Color;
L_Color.Text = "";
}
}

Событие Click нажатия кнопки B_OK:

private void B_OK_Click(object sender, EventArgs e)


{
ReadValues();
this.DialogResult = DialogResult.OK;
}

Форма готова. Если возникнут ошибки, перепроверяем имена элементов


управления.

Следующим классом станет класс, отвечающий за работу со списком


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

Логика работы с объектами после рисования будет такой:

Каждый новый объект помещается вначале Z-порядка (списка расположения


«под курсором»). Таким образом, нарисовав в одном и том же месте несколько
объектов и нажав инструментов «Выделение» на этом же месте «по умолчанию» будет
выделен последний нарисованный объект. Если изменить положение объекта в Z-
списке, отправив объект «назад» («Переместить назад»), наверху окажется
предыдущий нарисованный объект. Сам же объект, который мы отправили «назад»,
будет расположен в конце Z-порядка и будет выделен, если удалить из области все
другие объекты.

Файл для класса назовём GraphicsList.cs, код файла будет таким: [искомый код
можно найти в приложении к данной лабораторной работе в (описания можно того или
иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 3 (Файлы классов Graphics...)].

Игнорируем возникшие ошибки (3 штуки, исправим позже добавлением других


классов).

Также добавим вспомогательный класс для передачи параметров:


GraphicsProperties (файл GraphicsProperties.cs)со следующим кодом: [искомый код
можно найти в приложении к данной лабораторной работе в (описания можно того или
иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 3 (Файлы классов Graphics...)].

Добавляем новый класс, являющийся базовым для команд типа «Отменить» и


«Вернуть» («Undo» и «Redo»). Имя файла класса будет Command.cs, код файла
вставляем следующий: [искомый код можно найти в приложении к данной
лабораторной работе в (описания можно того или иного приложения можно посмотреть

479
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 4
(Файлы классов Command...)].

Следующий класс (CommandChangeState.cs) станет основным классом для


поддержки команд перемещений, изменения размеров и изменения параметров объекта
рисования для операций «Отменить» и «Вернуть». Код файла будет следующим:
[искомый код можно найти в приложении к данной лабораторной работе в (описания
можно того или иного приложения можно посмотреть в пунтке № 8 протокола
работы), а именно необходимо открыть Приложение № 4 (Файлы классов
Command...)].

Класс CommandAdd (CommandAdd.cs), отвечающий за команду добавления


объекта для операций «Отменить» и «Вернуть»: [искомый код можно найти в
приложении к данной лабораторной работе в (описания можно того или иного
приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 4 (Файлы классов Command...)].

Непосредственно реализация команд «Отменить» и «Вернуть» будет выполнена


следующим классом UndoManager (файл UndoManager.cs) с кодом: [искомый код
можно найти в приложении к данной лабораторной работе в (описания можно того или
иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 5 (Прочие файлы)].

Класс CommandDelete (CommandDelete.cs), отвечающий за команду удаления


выделенного объекта для операций «Отменить» и «Вернуть»: [искомый код можно
найти в приложении к данной лабораторной работе в (описания можно того или иного
приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 4 (Файлы классов Command...)].

Класс CommandDeleteAll (CommandDeleteAll.cs), отвечающий за команду


удаления всех объектов для операций «Отменить» и «Вернуть»: [искомый код можно
найти в приложении к данной лабораторной работе в (описания можно того или иного
приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 4 (Файлы классов Command...)].

Теперь перейдём к классам, поддерживающим рисование графических объектов.


Первый и самый примитивный графический объект это линия. Класс на основе
DrawObject: DrawLine (DrawLine.cs) реализует всё необходимое для поддержки
рисования объекта линии, выделения нарисованного объекта, изменения положения и
перемещения объекта линии. Линия рисуется через два три события (нажатие левой
кнопки мыши, перемещения и отжатия левой кнопки мыши). Объект имеет де ключевые
точки (точка начала и точка конца линии). Сериализуется в файл путём указания
следующих параметров: точка начала и точка конца линии, а также строки записей
для сохранения в файле: Start и End. Перемещение линии обеспечивается созданием
для линии небольшой линейной области (толщина в 7 пикселей от центра линии).
Нажатие левой кнопки мыши в этой области приведёт к выделению объекта и
активации возможности перемещение, изменения. Изменение размеров линии
осуществляется в ключевых точках.

Код файла для реализации класса: [искомый код можно найти в приложении к
данной лабораторной работе в (описания можно того или иного приложения можно

480
посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть
Приложение № 2 (Файлы классов Draw...)].

Объект получает параметры из инструмента «Линия» следующим обрахом:

private Point startPoint;


private Point endPoint;

public DrawLine(int x1, int y1, int x2, int y2) : base()
{
startPoint.X = x1;
startPoint.Y = y1;
endPoint.X = x2;
endPoint.Y = y2;
Initialize();
}

Рисует линию так:

/// <summary>
/// Главная функция рисования линии на форме
/// </summary>
public override void Draw(Graphics g)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
Pen pen = new Pen(Color, PenWidth);
g.DrawLine(pen, startPoint.X, startPoint.Y, endPoint.X, endPoint.Y);
pen.Dispose();
}

Прямоугольник. Класс на основе DrawObject: DrawRectangle


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

Код файла для реализации класса: [искомый код можно найти в приложении к
данной лабораторной работе в (описания можно того или иного приложения можно
посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть
Приложение № 2 (Файлы классов Draw...)].

Нормализация нужна для правильного определения направления рисования


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

Эллипс. Класс на основе DrawRectangle: DrawEllipse (DrawEllipse.cs)


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

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

Код файла для реализации класса: [искомый код можно найти в приложении к
данной лабораторной работе в (описания можно того или иного приложения можно
посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть
Приложение № 2 (Файлы классов Draw...)].

Объект получает следующие параметры из инструмента «Эллипс»:

public DrawEllipse(int x, int y, int width, int height) : base()


{
Rectangle = new Rectangle(x, y, width, height);
Initialize();
}

Рисование эллипса реализовано так:

/// <summary>
/// Главная функция рисования эллипса на форме
/// </summary>
/// <param name="g"></param>
public override void Draw(Graphics g)
{
Pen pen = new Pen(Color, PenWidth);
g.DrawEllipse(pen, DrawRectangle.GetNormalizedRectangle(Rectangle));
pen.Dispose();
}

Карандаш: последний объект рисования. Класс на основе DrawLine:


DrawPolygon (DrawPolygon.cs) реализует всё необходимое для поддержки рисования
объекта непрерывной линии с малым расстоянием между точками, выделения
нарисованного объекта, изменения положения и перемещения объекта карандаша.
Объект имеет столько ключевых точек, сколько нарисовано «кусков» + 1 точка).
Сериализуется в файл путём указания следующих параметров: точка и длина (для
одного участка), а также строки записей для сохранения в файле: Point и Length.
Карандаш не является цельным объектом (даже в случае замыкания линий). Для
перемещения, как и в случае с линией нужно выделить объект (в любом месте
нарисоанной ломаной) и зажать левую кнопку мыши. Путь для выделения создаётся
также как и в случае с линией, но для каждого участка. Параметр расстояния (до
ближайшей точки) регулируется так (будущий файл ToolPolygon.cs):

private const int minDistance = 15*15; // Дистанция между ключевыми точками

Кусок кода событиея перемещения мыши во время рисования:

Point point = new Point(e.X, e.Y);


int distance = (e.X - lastX)*(e.X - lastX) + (e.Y - lastY)*(e.Y - lastY);

if (distance < minDistance)


{
// Если расстояние между последними двумя точками меньше минимального -
// перемещаем последнюю точку
newPolygon.MoveHandleTo(point, newPolygon.HandleCount);
}
else

482
{
// Добавляем новую точку
newPolygon.AddPoint(point);
lastX = e.X;
lastY = e.Y;
}

Код файла для реализации класса: [искомый код можно найти в приложении к
данной лабораторной работе в (описания можно того или иного приложения можно
посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть
Приложение № 2 (Файлы классов Draw...)].

Последние 7 классов относятся к классам реализации инструмента рисования, и


именно они инициализируются до вызова класса рисования объекта. Экземпляр класса
инструмента передаёт в экземпляр класса рисования все параметры. Первый класс из
цепочки является абстрактным базовым классом для реализации работы инструментов.
Реализует три простых события действия с мышью (нажатие, перемещение и снятие
нажатия). Имя: Tool.cs (класс Tool). Код файла с классом: [искомый код можно найти
в приложении к данной лабораторной работе в (описания можно того или иного
приложения можно посмотреть в пунтке № 8 протокола работы), а именно
необходимо открыть Приложение № 6 (Файлы классов Tool...)].

ToolObject.cs (класс ToolObject). Это базовый класс для реализации остальных


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

/// <summary>
/// Добавление нового объекта в область рисования.
/// Функция вызывается когда пользователь нажимает ЛКМ на области рисования,
/// и один из полученных ToolObject-инструментов активен.
/// </summary>
/// <param name="drawArea"></param>
/// <param name="o"></param>
protected void AddNewObject(DrawArea drawArea, DrawObject o)
{
drawArea.GraphicsList.UnselectAll();
o.Selected = true;
drawArea.GraphicsList.Add(o);
drawArea.Capture = true;
drawArea.Refresh();
drawArea.SetDirty();
}

Код файла с классом: [искомый код можно найти в приложении к данной


лабораторной работе в (описания можно того или иного приложения можно посмотреть
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6
(Файлы классов Tool...)].

ToolLine.cs (класс ToolLine). Это базовый класс для реализации инструмента


рисования «Линия». Передаёт параметры объекту рисования. Реализует свойства
курсора для инструмента, а также функцию добавления нового объекта линии в список
объектов документа.

Пример события нажатия ЛКМ в области DrawArea с созданием экземпляра


класса DrawLine и передачей параметров нажатия:

483
public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)
{
AddNewObject(drawArea, new DrawLine(e.X, e.Y, e.X + 1, e.Y + 1));
}

Код файла с классом: [искомый код можно найти в приложении к данной


лабораторной работе в (описания можно того или иного приложения можно посмотреть
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6
(Файлы классов Tool...)].

ToolRectangle.cs (класс ToolRectnagle). Это базовый класс для реализации


инструмента рисования «Прямоугольник». Передаёт параметры объекту рисования.
Реализует свойства курсора для инструмента, а также функцию добавления нового
объекта прямоугольника в список объектов документа. Событие нажатия кнопки мыши:

public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)


{
AddNewObject(drawArea, new DrawRectangle(e.X, e.Y, 1, 1));
}

Код файла с классом: [искомый код можно найти в приложении к данной


лабораторной работе в (описания можно того или иного приложения можно посмотреть
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6
(Файлы классов Tool...)].

ToolEllipse.cs (класс ToolEllipse). Это базовый класс для реализации


инструмента рисования «Эллипс». Передаёт параметры объекту рисования. Реализует
свойства курсора для инструмента, а также функцию добавления нового объекта
эллипса в список объектов документа. Событие нажатия кнопки мыши:

public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)


{
AddNewObject(drawArea, new DrawEllipse(e.X, e.Y, 1, 1));
}

Код файла с классом: [искомый код можно найти в приложении к данной


лабораторной работе в (описания можно того или иного приложения можно посмотреть
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6
(Файлы классов Tool...)].

ToolPolygon.cs (класс ToolPolygon). Это базовый класс для реализации


инструмента рисования «Карандаш». Передаёт параметры объекту рисования.
Реализует свойства курсора для инструмента, а также функцию добавления нового
объекта непрерывной линии в список объектов документа.

Код файла с классом: [искомый код можно найти в приложении к данной


лабораторной работе в (описания можно того или иного приложения можно посмотреть
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6
(Файлы классов Tool...)].

ToolPointer.cs (класс ToolPointer). Это базовый класс для реализации


инструмента рисования «Выделение». Передаёт параметры объекту рисования.
Реализует события связанные с выделение объектов в области рисования, работает с

484
перемещением объекта, изменением размеров объекта и чистым выделением объект(-
ов). Этот инструмент также управляет пунктирным прямоугольником выделения:

if (selectMode == SelectionMode.NetSelection)
{
// Удаляем прямоугольник предыдущего выделения
ControlPaint.DrawReversibleFrame(

drawArea.RectangleToScreen(DrawRectangle.GetNormalizedRectangle(startPoint, oldPoint)),
Color.Black,
FrameStyle.Dashed);
// Рисуем прямоугольник нового выделения
ControlPaint.DrawReversibleFrame(

drawArea.RectangleToScreen(DrawRectangle.GetNormalizedRectangle(startPoint, point)),
Color.Black,
FrameStyle.Dashed);
return;
}

Код файла с классом: [искомый код можно найти в приложении к данной


лабораторной работе в (описания можно того или иного приложения можно посмотреть
в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6
(Файлы классов Tool...)].

На этом формирование классов закончено. Теперь можно перейти к реализации


функциональности непосредственно для главной формы и элемента, в котором будем
рисовать (DrawArea).

Первым делом «добьём» форму LWP15About. Событие Load для формы будет
таким:

private void LWP15About_Load(object sender, EventArgs e)


{
this.Text = "О программе " + Application.ProductName;
L_About.Text =
"Программа: " + Application.ProductName + "\n" +
"Версия: " + Application.ProductVersion;
}

Событие Click кнопки B_OK:

private void B_OK_Click(object sender, EventArgs e)


{
this.Close();
}

Перейдём к DrawArea.cs. Это основной элемент, в котором происходит


«рисование». Любые действия мышью совершаемые в фокусе элемента

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

Откроем код элемента DrawArea.cs и заменим все директивы using в начале


файла следующим кодом:

#region Директивы Using


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using LWP15Tools; // Подключаем библиотеку классов
#endregion

Подключаем к проекту ссылку на нашу библиотеку LWP15tools: жмём правую


кнопку мыши на имени проекта LWP15Draw, затем Добавить ссылку...:

Рис. 6. 1. Добавить ссылку: вкладка Проекты

В открывшемся окне переходим на вкладку Проекты и выбираем LWP15Tools,


жмём ОК.

В коде находим:

namespace LWP15Draw
{
public partial class DrawArea : UserControl

486
{
public DrawArea()
{
InitializeComponent();
}
}

Заменяем на (последнюю фигурную скобку в файле не трогаем):

namespace LWP15Draw
{
partial class DrawArea : UserControl
{
#region Конструктор
public DrawArea()
{
InitializeComponent();
}
#endregion

#region Перечисления
public enum DrawToolType
{
Pointer, Rectangle, Ellipse, Line, Polygon, NumberOfDrawTools
};
#endregion

#region Члены
private GraphicsList graphicsList; // Список объектов рисования
private DrawToolType activeTool; // Активный инструмент рисования
private Tool[] tools; // Массив инструментов
private LWP15Main owner;
private DocManager docManager;
private ContextMenuStrip m_ContextMenu;
private UndoManager undoManager;
#endregion

#region Свойства
/// <summary>
/// Ссылка на владельца формы
/// </summary>
public LWP15Main Owner
{
get { return owner; }
set { owner = value; }
}

/// <summary>
/// Ссылка на DocManager
/// </summary>
public DocManager DocManager
{
get { return docManager; }
set
{
docManager = value;
}
}

/// <summary>
/// Активный инструмент рисования
/// </summary>
public DrawToolType ActiveTool
{

487
get { return activeTool; }
set { activeTool = value; }
}

/// <summary>
/// Список объектов рисования
/// </summary>
public GraphicsList GraphicsList
{
get { return graphicsList; }
set
{
graphicsList = value;
undoManager = new UndoManager(graphicsList);
}

/// <summary>
/// true - если операция отмены возможна
/// </summary>
public bool CanUndo
{
get
{
if (undoManager != null) { return undoManager.CanUndo; }
return false;
}
}

/// <summary>
/// true - если операция возврата возможна
/// </summary>
public bool CanRedo
{
get
{
if (undoManager != null) { return undoManager.CanRedo; }
return false;
}
}
#endregion

#region Прочие функции

/// <summary>
/// Инициализация
/// </summary>
/// <param name="owner"></param>
/// <param name="docManager"></param>
public void Initialize(LWP15Main owner, DocManager docManager)
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer, true);
// Сохраняем ссылку на владельца формы
this.Owner = owner;
this.DocManager = docManager;
// Устанавливаем инструмент по умолчанию
activeTool = DrawToolType.Pointer;
// Создаём список графических объектов
graphicsList = new GraphicsList();
// Создаём экземпляр UndoManager для текущего файла
undoManager = new UndoManager(graphicsList);
// Создаём массив инструментов рисования
tools = new Tool[(int)DrawToolType.NumberOfDrawTools];

488
tools[(int)DrawToolType.Pointer] = new ToolPointer();
tools[(int)DrawToolType.Rectangle] = new ToolRectangle();
tools[(int)DrawToolType.Ellipse] = new ToolEllipse();
tools[(int)DrawToolType.Line] = new ToolLine();
tools[(int)DrawToolType.Polygon] = new ToolPolygon();
}

/// <summary>
/// Добавления команды в историю
/// </summary>
public void AddCommandToHistory(Command command)
{
undoManager.AddCommandToHistory(command);
}

/// <summary>
/// Очистка истории
/// </summary>
public void ClearHistory()
{
undoManager.ClearHistory();
}

/// <summary>
/// Отменить
/// </summary>
public void Undo()
{
undoManager.Undo();
Refresh();
}

/// <summary>
/// Вернуть
/// </summary>
public void Redo()
{
undoManager.Redo();
Refresh();
}

/// <summary>
/// Устанавливаем флаг "грязный" (файл был изменён после последней операции
сохранения)
/// </summary>
public void SetDirty()
{
DocManager.Dirty = true;
}

/// <summary>
/// Обработчик нажатия правой кнопки мышки
/// </summary>
/// <param name="e"></param>
private void OnContextMenu(MouseEventArgs e)
{
// Измененяем текущий выбор при необходимости
Point point = new Point(e.X, e.Y);
int n = GraphicsList.Count;
DrawObject o = null;

for (int i = 0; i < n; i++)


{

if (GraphicsList[i].HitTest(point) == 0)

489
{
o = GraphicsList[i];
break;
}
}

if (o != null)
{

if (!o.Selected) GraphicsList.UnselectAll();
// Выбор объекта произведён
o.Selected = true;
}
else
{
GraphicsList.UnselectAll();
}
Refresh(); // В случае изменения выбора
// Выводин контекстное меню (всплывающее).
// Элементы меню вставлены из строки меня, главного элемента "Правка"
m_ContextMenu = new ContextMenuStrip();
int nItems = owner.ContextParent.DropDownItems.Count;
// Получаем элементы меню "Правка" и перемещаем их на контекстное-всплывающее
меню.
// Так как каждый шаг уменьшает количество элементов, читая их в обратном
порядке.
// Чтобы получить элементы в прямом порядке, вставим каждый из них в начало
for (int i = nItems - 1; i >= 0; i--)
{
m_ContextMenu.Items.Insert(0, owner.ContextParent.DropDownItems[i]);
}
// Выводит контекстное меню для владельца формы, а также обрабатывает
элементы выбора.
// Преобразует координаты точки в этом окне к координатам владельца
point.X += this.Left;
point.Y += this.Top;
m_ContextMenu.Show(owner, point);
Owner.SetStateOfControls(); // Включение/выключение элементов меню
// Контекстное меню вызвано, но меню "Правка" владельца теперь пусто.
// Подписываемся на событие закрытия контекстного меню и восстанавливаем там
элементы
m_ContextMenu.Closed += delegate(object sender,
ToolStripDropDownClosedEventArgs args)
{

if (m_ContextMenu != null)
{
nItems = m_ContextMenu.Items.Count;
for (int k = nItems - 1; k >= 0; k--)
{ owner.ContextParent.DropDownItems.Insert(0, m_ContextMenu.Items[k]); }
}
};
}
#endregion
}

По очереди инициализируем для элемента DrawArea следующие события с


кодом. Вначале идёт событие Paint:

#region Обработчики событий


/// <summary>
/// Рисование графического объекта и группировка прямоугольника выделения
(опционально)
/// </summary>

490
private void DrawArea_Paint(object sender, PaintEventArgs e)
{
SolidBrush brush = new SolidBrush(Color.FromArgb(255, 255, 255));
e.Graphics.FillRectangle(brush, this.ClientRectangle);
if (graphicsList != null) { graphicsList.Draw(e.Graphics); }
//DrawNetSelection(e.Graphics);
brush.Dispose();
}

Событие MouseDown, MouseMove и MouseUp для DrawArea:

/// <summary>
/// Событие MouseDown для DrawArea
/// ЛКМ: перехватывается активным инструментом
/// ПКМ: перехватывается и описано в данном классе
/// </summary>
private void DrawArea_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) tools[(int)activeTool].OnMouseDown(this,
e);
else if (e.Button == MouseButtons.Right) OnContextMenu(e);
}

/// <summary>
/// Событие MouseMove для DrawArea
/// Перемещение без нажатия кнопок или с нажатие левой кнопки мыши
перехватываетсмя активным инструментом
/// </summary>
private void DrawArea_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left || e.Button == MouseButtons.None)
tools[(int)activeTool].OnMouseMove(this, e);
else this.Cursor = Cursors.Default;
}

/// <summary>
/// Событие MouseUp для DrawArea
/// ЛКМ: перехватывается активным инструментом
/// </summary>
private void DrawArea_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) tools[(int)activeTool].OnMouseUp(this, e);
}
#endregion

Переходим к главной форме LWP15Main. Переписываем директивы using:

#region Директивы Using


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.Serialization;
using System.Diagnostics;
using System.Security;
using Microsoft.Win32;
using LWP15Tools;
#endregion

Находим:

491
public partial class LWP15Main : Form
{

Заменяем на:

partial class LWP15Main : Form


{
#region Члены
private DrawArea drawArea;
private DocManager docManager;
private DragDropManager dragDropManager;
private MruManager mruManager;
private PersistWindowState persistState;
private string argumentFile = ""; // Имя файла из командной строки
const string registryPath = "Software\\LWP15Draw";
#endregion

#region Свойства
/// <summary>
/// Имя файла из командной строки
/// </summary>
public string ArgumentFile
{
get { return argumentFile; }
set { argumentFile = value; }
}

/// <summary>
/// Получаем ссылку на элемент строки меню "Правка".
/// Используется при вызове контекстного-всплывающего меню в классе DrawArea
/// </summary>
/// <value></value>
public ToolStripMenuItem ContextParent
{
get { return правкаToolStripMenuItem; }
}
#endregion

#region Конструктор
public LWP15Main()
{
InitializeComponent();
persistState = new PersistWindowState(registryPath, this);
}
#endregion

#region Обработчики событий для DocManager


/// <summary>
/// Загрузка документа из потока поставляемая DocManager
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void docManager_LoadEvent(object sender, SerializationEventArgs e)
{
// DocManager просит загрузить документ в поставляемый поток
try
{
drawArea.GraphicsList =
(GraphicsList)e.Formatter.Deserialize(e.SerializationStream);
}
catch (ArgumentNullException ex) { HandleLoadException(ex, e); }
catch (SerializationException ex) { HandleLoadException(ex, e); }
catch (SecurityException ex) { HandleLoadException(ex, e); }

492
}

/// <summary>
/// Сохранение документа в поток поставляемый DocManager
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void docManager_SaveEvent(object sender, SerializationEventArgs e)
{
// DocManager просит сохранить документ в поставляемый поток
try
{
e.Formatter.Serialize(e.SerializationStream, drawArea.GraphicsList);
}
catch (ArgumentNullException ex) { HandleSaveException(ex, e); }
catch (SerializationException ex) { HandleSaveException(ex, e); }
catch (SecurityException ex) { HandleSaveException(ex, e); }
}
#endregion

Теперь поочерёдно создаём события строки меню (двойное нажатие на элементе


меню для инициализации события Click). В качестве кнопок будут выступать все
подпункты (дочерние пункты) меню кроме подпункта «Последние файлы» меню
«Файл». Блок с кодом событий Click всей строки меню будет таким:

#region Обработчики событий строки меню


private void создатьToolStripMenuItem_Click(object sender, EventArgs e)
{
CommandNew();
}

private void открытьToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandOpen();
}

private void сохранитьToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandSave();
}

private void сохранитькакToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandSaveAs();
}

private void выходToolStripMenuItem_Click(object sender, EventArgs e)


{
this.Close();
}

private void отменитьToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandUndo();
}

private void вернутьToolStripMenuItem1_Click(object sender, EventArgs e)


{
CommandRedo();
}

private void выделитьвсеToolStripMenuItem_Click(object sender, EventArgs e)


{

493
drawArea.GraphicsList.SelectAll();
drawArea.Refresh();
}

private void снятьВыделениеToolStripMenuItem_Click(object sender, EventArgs e)


{
drawArea.GraphicsList.UnselectAll();
drawArea.Refresh();
}

private void удалитьToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandDelete command = new CommandDelete(drawArea.GraphicsList);

if (drawArea.GraphicsList.DeleteSelection())
{
drawArea.SetDirty();
drawArea.Refresh();
drawArea.AddCommandToHistory(command);
}
}

private void удалитьВсеToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandDeleteAll command = new CommandDeleteAll(drawArea.GraphicsList);

if (drawArea.GraphicsList.Clear())
{
drawArea.SetDirty();
drawArea.Refresh();
drawArea.AddCommandToHistory(command);
}
}

private void переместитьНазадToolStripMenuItem_Click(object sender, EventArgs e)


{

if (drawArea.GraphicsList.MoveSelectionToBack())
{
drawArea.SetDirty();
drawArea.Refresh();
}
}

private void переместитьВпередToolStripMenuItem_Click(object sender, EventArgs e)


{

if (drawArea.GraphicsList.MoveSelectionToFront())
{
drawArea.SetDirty();
drawArea.Refresh();
}
}

private void параметрыToolStripMenuItem_Click(object sender, EventArgs e)


{

if (drawArea.GraphicsList.ShowPropertiesDialog(drawArea))
{
drawArea.SetDirty();
drawArea.Refresh();
}
}

private void выделениеToolStripMenuItem_Click(object sender, EventArgs e)

494
{
CommandPointer();
}

private void карандашToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandPolygon();
}

private void линияToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandLine();
}

private void эллипсToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandEllipse();
}

private void прямоугольникToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandRectnagle();
}

private void опрограммеToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandAbout();
}
#endregion

Тоже самое проделываем для строки с иконками (панелью инструментов).


Двойное нажатие на иконку инициализирует событие Click:

#region Обработчики событий панели инструментов


private void создатьToolStripButton_Click(object sender, EventArgs e)
{
CommandNew();
}

private void открытьToolStripButton_Click(object sender, EventArgs e)


{
CommandOpen();
}

private void сохранитьToolStripButton_Click(object sender, EventArgs e)


{
CommandSave();
}

private void выделениеToolStripButton_Click(object sender, EventArgs e)


{
CommandPointer();
}

private void карандашToolStripButton_Click(object sender, EventArgs e)


{
CommandPolygon();
}

private void линияToolStripButton_Click(object sender, EventArgs e)


{
CommandLine();
}

495
private void эллипсToolStripButton_Click(object sender, EventArgs e)
{
CommandEllipse();
}

private void прямоугольникToolStripButton_Click(object sender, EventArgs e)


{
CommandRectangle();
}

private void отменитьToolStripButton_Click(object sender, EventArgs e)


{
CommandUndo();
}

private void вернутьToolStripButton_Click(object sender, EventArgs e)


{
CommandRedo();
}

private void справкаToolStripButton_Click(object sender, EventArgs e)


{
CommandAbout();
}
#endregion

Инициализируем три события для главной формы. Событие Load формы


LWP15Main:

#region Обработчики событий


private void LWP15Main_Load(object sender, EventArgs e)
{
toolStripStatusLabel.Text = "Готов к работе";
// Создаём область рисования
drawArea = new DrawArea();
drawArea.Location = new System.Drawing.Point(0, 0);
drawArea.Size = new System.Drawing.Size(10, 10);
drawArea.Owner = this;
this.Controls.Add(drawArea);
// Вспомогательные объекты (DocManager и прочие)
InitializeHelperObjects();
drawArea.Initialize(this, docManager);
ResizeDrawArea();
LoadSettingsFromRegistry();
// Submit to Idle event to set controls state at idle time
Application.Idle += delegate(object o, EventArgs a)
{
SetStateOfControls();
};
// Открытый файл передаётся в командную строку
if (ArgumentFile.Length > 0) OpenDocument(ArgumentFile);
// Подписываемся на событие DropDownOpened для каждого всплывающего меню
foreach (ToolStripItem item in menuStrip1.Items)
{
if (item.GetType() == typeof(ToolStripMenuItem))
{ ((ToolStripMenuItem)item).DropDownOpened += LWP15Main_DropDownOpened; }
}
}

Событие Resize:

/// <summary>
/// Изменение размеров DrawArea, Когда меняются размеры формы

496
/// </summary>
private void LWP15Main_Resize(object sender, EventArgs e)
{
if (this.WindowState != FormWindowState.Minimized && drawArea != null)
{ ResizeDrawArea(); }
}

Событие FormClosing:

/// <summary>
/// Событие закрытия формы
/// </summary>
private void LWP15Main_FormClosing(object sender, FormClosingEventArgs e)
{

if (e.CloseReason == CloseReason.UserClosing)
{
if (!docManager.CloseDocument()) e.Cancel = true;
}
SaveSettingsToRegistry();
}

Последнее событие формы без инициализации (просто вставляем код после


последнего события):

/// <summary>
/// Всплывающая строка меню ("Файл", "Правка" и прочее) открыто
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void LWP15Main_DropDownOpened(object sender, EventArgs e)
{
// Устанавливаем активный инструмент на "Выделение".
// Педотвращает редкий сбой, когда выбран иной инструмент, пользователь
открывает главное менюuser opens
// и после нажатия в DrawArea событие MouseDown не вызывается и MouseMove
работает неправильно
drawArea.ActiveTool = DrawArea.DrawToolType.Pointer;
}
#endregion

Наконец добавляем последний участок кода сразу же после предыдущего:

#region Прочие функции


/// <summary>
/// Установка состояния элементов управления.
/// Функция вызывается при простое
/// </summary>
public void SetStateOfControls()
{
// Выбор активного инструмента
выделениеToolStripButton.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Pointer);
прямоугольникToolStripButton.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Rectangle);
эллипсToolStripButton.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Ellipse);
линияToolStripButton.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Line);
карандашToolStripButton.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Polygon);

497
выделениеToolStripMenuItem.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Pointer);
прямоугольникToolStripMenuItem.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Rectangle);
эллипсToolStripMenuItem.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Ellipse);
линияToolStripMenuItem.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Line);
карандашToolStripMenuItem.Checked = (drawArea.ActiveTool ==
DrawArea.DrawToolType.Polygon);

bool objects = (drawArea.GraphicsList.Count > 0);


bool selectedObjects = (drawArea.GraphicsList.SelectionCount > 0);
// Операции с файлом
сохранитьToolStripMenuItem.Enabled = objects;
сохранитьToolStripButton.Enabled = objects;
сохранитьКакToolStripMenuItem.Enabled = objects;
// Операции правки
удалитьToolStripMenuItem.Enabled = selectedObjects;
удалитьВсеToolStripMenuItem.Enabled = objects;
выделитьВсеToolStripMenuItem.Enabled = objects;
снятьВыделениеToolStripMenuItem.Enabled = objects;
переместитьВпередToolStripMenuItem.Enabled = selectedObjects;
переместитьНазадToolStripMenuItem.Enabled = selectedObjects;
параметрыToolStripMenuItem.Enabled = selectedObjects;
// "Отменить"
отменитьToolStripMenuItem.Enabled = drawArea.CanUndo;
отменитьToolStripButton.Enabled = drawArea.CanUndo;
// "Вернуть"
вернутьToolStripMenuItem.Enabled = drawArea.CanRedo;
вернутьToolStripButton.Enabled = drawArea.CanRedo;
}

/// <summary>
/// Установка области рисования формы на основе пространство свободной области,
/// за исключением панели инструментов, строки состояния и строки меню
/// </summary>
private void ResizeDrawArea()
{
Rectangle rect = this.ClientRectangle;
drawArea.Left = rect.Left;
drawArea.Top = rect.Top + menuStrip1.Height + toolStrip1.Height;
drawArea.Width = rect.Width;
drawArea.Height = rect.Height - menuStrip1.Height - toolStrip1.Height -
statusStrip1.Height;
}

/// <summary>
/// Инициализация вспомогательных объектов из библиотеки классов LWP15Tools.
/// </summary>
private void InitializeHelperObjects()
{
// DocManager
DocManagerData data = new DocManagerData();
data.FormOwner = this;
data.UpdateTitle = true;
data.FileDialogFilter = "Файлы LWP15Draw (*.lwp)|*.lwp|Все файлы (*.*)|*.*";
data.NewDocName = "New.lwp";
data.RegistryPath = registryPath;

docManager = new DocManager(data);


docManager.RegisterFileType("lwp", "lwpfile", "Файл LWP15Draw");
// Подписываемся на события класса DocManager
docManager.SaveEvent += docManager_SaveEvent;
docManager.LoadEvent += docManager_LoadEvent;

498
// Делаем "встроенные подписки" с помощью анонимных методов
docManager.OpenEvent += delegate(object sender, OpenFileEventArgs e)
{
// Обновляем список последних файлов
if (e.Succeeded) mruManager.Add(e.FileName);
else mruManager.Remove(e.FileName);
};

docManager.DocChangedEvent += delegate(object o, EventArgs e)


{

try
{
drawArea.Refresh();
drawArea.ClearHistory();
}
catch { }
};

docManager.ClearEvent += delegate(object o, EventArgs e)


{

if (drawArea.GraphicsList != null)
{
drawArea.GraphicsList.Clear();
drawArea.ClearHistory();
drawArea.Refresh();
}
};
docManager.NewDocument();
// DragDropManager
dragDropManager = new DragDropManager(this);
dragDropManager.FileDroppedEvent += delegate(object sender,
FileDroppedEventArgs e)
{
OpenDocument(e.FileArray.GetValue(0).ToString());
};
// MruManager
mruManager = new MruManager();
mruManager.Initialize(
this, // Владелец формы (this)
последниеФайлыToolStripMenuItem, // Элемент меню последних исользованных
файлов
файлToolStripMenuItem, // Родительский элемент ("Файл")
registryPath); // Путь в системном реестре для хранения
списка последних файлов
mruManager.MruOpenEvent += delegate(object sender, MruFileOpenEventArgs e)
{
OpenDocument(e.FileName);
};
}
/// <summary>
/// Обрабатываем исключение из функции docManager_LoadEvent
/// </summary>
/// <param name="ex"></param>
/// <param name="fileName"></param>
private void HandleLoadException(Exception ex, SerializationEventArgs e)
{
MessageBox.Show(this,
"Операция открытия файла завершилась неудачей. Имя файла: " + e.FileName
+ "\n" +
"Причина: " + ex.Message,
Application.ProductName);
e.Error = true;
}

499
/// <summary>
/// Обрабатываем исключение из функции docManager_SaveEvent
/// </summary>
/// <param name="ex"></param>
/// <param name="fileName"></param>
private void HandleSaveException(Exception ex, SerializationEventArgs e)
{
MessageBox.Show(this,
"Операция сохранения файла завершилась неудачей. Имя файла: " +
e.FileName + "\n" +
"Причина: " + ex.Message,
Application.ProductName);
e.Error = true;
}

/// <summary>
/// Открытие документа.
/// Используется для открытия файла переданного в командной строке или
"переброшенного" в окно
/// </summary>
/// <param name="file"></param>
public void OpenDocument(string file)
{
docManager.OpenDocument(file);
}

/// <summary>
/// Загрузка настроек приложения из системного реестра
/// </summary>
void LoadSettingsFromRegistry()
{

try
{
RegistryKey key = Registry.CurrentUser.CreateSubKey(registryPath);
DrawObject.LastUsedColor = Color.FromArgb((int)key.GetValue("Color",
Color.Black.ToArgb()));
DrawObject.LastUsedPenWidth = (int)key.GetValue("Width", 1);
}
catch (ArgumentNullException ex) { HandleRegistryException(ex); }
catch (SecurityException ex) { HandleRegistryException(ex); }
catch (ArgumentException ex) { HandleRegistryException(ex); }
catch (ObjectDisposedException ex) { HandleRegistryException(ex); }
catch (UnauthorizedAccessException ex) { HandleRegistryException(ex); }
}

/// <summary>
/// Сохранение настроек приложения в системный реестр
/// </summary>
void SaveSettingsToRegistry()
{

try
{
RegistryKey key = Registry.CurrentUser.CreateSubKey(registryPath);
key.SetValue("Color", DrawObject.LastUsedColor.ToArgb());
key.SetValue("Width", DrawObject.LastUsedPenWidth);
}
catch (SecurityException ex) { HandleRegistryException(ex); }
catch (ArgumentException ex) { HandleRegistryException(ex); }
catch (ObjectDisposedException ex) { HandleRegistryException(ex); }
catch (UnauthorizedAccessException ex) { HandleRegistryException(ex); }
}

500
private void HandleRegistryException(Exception ex)
{
Trace.WriteLine("Выполнение операции с системны реестром завершилась
неудачей: " + ex.Message);
}

/// <summary>
/// Выделение
/// </summary>
private void CommandPointer()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Pointer;
toolStripStatusLabel.Text = "Выбран инструмент \"Выделение\"";
}

/// <summary>
/// Прямоугольник
/// </summary>
private void CommandRectangle()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Rectangle;
toolStripStatusLabel.Text = "Выбран инструмент \"Прямоугольник\"";
}

/// <summary>
/// Эллипс
/// </summary>
private void CommandEllipse()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Ellipse;
toolStripStatusLabel.Text = "Выбран инструмент \"Эллипс\"";
}

/// <summary>
/// Линия
/// </summary>
private void CommandLine()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Line;
toolStripStatusLabel.Text = "Выбран инструмент \"Линия\"";
}

/// <summary>
/// Карандаш
/// </summary>
private void CommandPolygon()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Polygon;
toolStripStatusLabel.Text = "Выбран инструмент \"Карандаш\"";
}

/// <summary>
/// Диалог "О программе..."
/// </summary>
private void CommandAbout()
{
LWP15About frm = new LWP15About();
frm.ShowDialog(this);
toolStripStatusLabel.Text = "Вызвана форма \"О программе\"";
}

/// <summary>
/// Создать
/// </summary>
private void CommandNew()

501
{
docManager.NewDocument();
toolStripStatusLabel.Text = "Создан новый документ";
}

/// <summary>
/// Открыть
/// </summary>
private void CommandOpen()
{
docManager.OpenDocument("");
toolStripStatusLabel.Text = "Документ открыт";
}

/// <summary>
/// Сохранить
/// </summary>
private void CommandSave()
{
docManager.SaveDocument(DocManager.SaveType.Save);
toolStripStatusLabel.Text = "Документ сохранён";
}

/// <summary>
/// Сохранить как
/// </summary>
private void CommandSaveAs()
{
docManager.SaveDocument(DocManager.SaveType.SaveAs);
toolStripStatusLabel.Text = "Документ сохранён";
}

/// <summary>
/// Отменить
/// </summary>
private void CommandUndo()
{
drawArea.Undo();
toolStripStatusLabel.Text = "Произведена операция \"Отменить\"";
}

/// <summary>
/// Вернуть
/// </summary>
private void CommandRedo()
{
drawArea.Redo();
toolStripStatusLabel.Text = "Произведена операция \"Вернуть\"";
}
#endregion

Почти всё. Наше приложение способно создавать свои собственные документы


(файлы *.lwp). А как быть, если необходимо сохранить рисунок в нормальном
графическом формате? Всё достаточно просто. Нужно сохранить содержимое элемента
управления DrawArea в одно из желаемых форматов растрового изображения.
Для реализации этой функции создаём в меню «Файл» новую кнопку «Сохранить
как изображение...»:

502
Свойства такие:

(Name): сохранитьКакИзображениеToolStripMenuItem
Text: Сохранить как &изображение...
Image:
Импортируем иконку

Событие Click по кнопке меню «Сохранить как изображение...»:

#region Добавленная функция экспорта DrawArea в изображение


private Rectangle drawAreaRect;
private string fileName;
private string strFilExtn;

private void сохранитьКакИзображениеToolStripMenuItem_Click(object sender,


EventArgs e)
{
SaveFileDialog SFD_Picture = new SaveFileDialog();
SFD_Picture.Title = "Сохранить ка изображение";
SFD_Picture.OverwritePrompt = true;
SFD_Picture.CheckPathExists = true;
SFD_Picture.Filter = "Изображение в формате PNG|*.png|" + "Изображение в
формате JPEG|*.jpg|" + "Изображение в формате BMP|*.bmp|" + "Изображение в формате GIF|
*.gif|" + "Изображение в формате TIF|*.tif";
drawAreaRect = new Rectangle();
drawAreaRect.Width = drawArea.Size.Width;
drawAreaRect.Height = drawArea.Size.Height;
Bitmap drawAreaBitmap = new Bitmap(drawArea.Size.Width,
drawArea.Size.Height);
drawArea.DrawToBitmap(drawAreaBitmap, drawAreaRect);
// Если выбрали, сохраняем
if (SFD_Picture.ShowDialog() == DialogResult.OK)
{
// Получаем имя файла из диалога
fileName = SFD_Picture.FileName;
// Получаем расширения файла из диалога
strFilExtn = fileName.Remove(0, fileName.Length - 3);
// Сохраняем файл в выбранном расширении
switch (strFilExtn)
{
case "bmp":
drawAreaBitmap.Save(fileName,
System.Drawing.Imaging.ImageFormat.Bmp);
break;
case "jpg":
drawAreaBitmap.Save(fileName,
System.Drawing.Imaging.ImageFormat.Jpeg);

503
break;
case "gif":
drawAreaBitmap.Save(fileName,
System.Drawing.Imaging.ImageFormat.Gif);
break;
case "tif":
drawAreaBitmap.Save(fileName,
System.Drawing.Imaging.ImageFormat.Tiff);
break;
case "png":
drawAreaBitmap.Save(fileName,
System.Drawing.Imaging.ImageFormat.Png);
break;
default:
break;
}
toolStripStatusLabel.Text = "Изображение " +
System.IO.Path.GetFileName(SFD_Picture.FileName) + " успешно сохранено! Полный путь: " +
SFD_Picture.FileName;
}
SFD_Picture.Dispose();
drawAreaBitmap.Dispose();
}
#endregion

Хорошо, а если, например нам потребуется вставить изображение, да ещё и


изменить его размеры? Тоже не проблема. Выполним полноценное расширение
возможностей нашего приложения добавление нового полноценного объекта рисования
— изображения. Изображение будет добавлять на форму через диалог выбора файла.
По умолчанию у него будет чёрная прямоугольная рамка с толщиной пера в один
пиксель. После добавления изображения, цвет и толщину пера рамки можно будет
поменять (как и у любого другого объекта). Изображение будет полноценно
сериализовываться в файле наравне с остальными объектами. Во время рисования,
размер изображения будет автоматически подстраиваться под нарисованную рамку.
Вставленное изображение можно будет переместить и изменить его размер.

Первое что нужно сделать, это добавить новый класс для рисования объекта
изображения. Назовём его DrawImage (файл DrawImage.cs). За основу возьмём
класс DrawRectangle, так как он наиболее походит нам для реализации нового класса.
Код класс рисования будет таким:

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Globalization;
using System.Runtime.Serialization;
using System.Windows.Forms;

namespace LWP15Draw
{
/// <summary>
/// Рисование графического объекта "Изображение"
/// </summary>
class DrawImage : LWP15Draw.DrawObject
{
private Rectangle rectangle;
private Bitmap _image;
private Bitmap _originalImage; // Оригинальное изображение

public Bitmap TheImage

504
{
get { return _image; }
set
{
_originalImage = value;
ResizeImage(rectangle.Width, rectangle.Height);
}
}

private const string entryRectangle = "Rect";


private const string entryImage = "Image";
private const string entryImageOriginal = "OriginalImage";

/// <summary>
/// Клонирование данного экземпляра
/// </summary>
public override DrawObject Clone()
{
DrawImage drawImage = new DrawImage();
drawImage._image = _image;
drawImage._originalImage = _originalImage;
drawImage.rectangle = rectangle;
FillDrawObjectFields(drawImage);
return drawImage;
}

protected Rectangle Rectangle


{
get { return rectangle; }
set { rectangle = value; }
}

public DrawImage()
{
SetRectangle(0, 0, 1, 1);
Initialize();
}

public DrawImage(int x, int y)


{
rectangle.X = x;
rectangle.Y = y;
rectangle.Width = 1;
rectangle.Height = 1;
Initialize();
// Устанавливаем перо по умолчанию для каждого нового изображения
Color = Color.Black;
PenWidth = 0;
}

public DrawImage(int x, int y, Bitmap image)


{
rectangle.X = x;
rectangle.Y = y;
_image = (Bitmap)image.Clone();
SetRectangle(rectangle.X, rectangle.Y, image.Width, image.Height);
Initialize();
}

/// <summary>
/// лавная функция рисования изображения на форме
/// </summary>
/// <param name="g"></param>
public override void Draw(Graphics g)
{

505
Pen pen = new Pen(Color, PenWidth);

if (_image == null)
{
g.DrawRectangle(pen, rectangle);
}
else
{
g.DrawImage(_image, new Point(rectangle.X, rectangle.Y));
g.DrawRectangle(pen, rectangle);
}

protected void SetRectangle(int x, int y, int width, int height)


{
rectangle.X = x;
rectangle.Y = y;
rectangle.Width = width;
rectangle.Height = height;
}

public override int HandleCount


{
get { return 8; }
}

/// <summary>
/// Получение ключевых точек изображения
/// </summary>
/// <param name="handleNumber"></param>
/// <returns></returns>
public override Point GetHandle(int handleNumber)
{
int x, y, xCenter, yCenter;

xCenter = rectangle.X + rectangle.Width / 2;


yCenter = rectangle.Y + rectangle.Height / 2;
x = rectangle.X;
y = rectangle.Y;

switch (handleNumber)
{
case 1:
x = rectangle.X;
y = rectangle.Y;
break;
case 2:
x = xCenter;
y = rectangle.Y;
break;
case 3:
x = rectangle.Right;
y = rectangle.Y;
break;
case 4:
x = rectangle.Right;
y = yCenter;
break;
case 5:
x = rectangle.Right;
y = rectangle.Bottom;
break;
case 6:
x = xCenter;

506
y = rectangle.Bottom;
break;
case 7:
x = rectangle.X;
y = rectangle.Bottom;
break;
case 8:
x = rectangle.X;
y = yCenter;
break;
}
return new Point(x, y);
}

/// <summary>
/// Проверка нажатия.
/// Возвращаемые параметры: -1 - нет нажатия
/// 0 - нажатие где угодно
/// > 1 - обработка ключевой точки
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public override int HitTest(Point point)
{

if (Selected)
{

for (int i = 1; i <= HandleCount; i++)


{
if (GetHandleRectangle(i).Contains(point)) return i;
}
}
if (PointInObject(point)) return 0;
return -1;
}

protected override bool PointInObject(Point point)


{
return rectangle.Contains(point);
}

/// <summary>
/// Получение курсора для ключевых точек
/// </summary>
/// <param name="handleNumber"></param>
/// <returns></returns>
public override Cursor GetHandleCursor(int handleNumber)
{

switch (handleNumber)
{
case 1:
return Cursors.SizeNWSE;
case 2:
return Cursors.SizeNS;
case 3:
return Cursors.SizeNESW;
case 4:
return Cursors.SizeWE;
case 5:
return Cursors.SizeNWSE;
case 6:
return Cursors.SizeNS;
case 7:

507
return Cursors.SizeNESW;
case 8:
return Cursors.SizeWE;
default:
return Cursors.Default;
}
}

/// <summary>
/// Изменение размеров объекта изображения
/// </summary>
/// <param name="point"></param>
/// <param name="handleNumber"></param>
public override void MoveHandleTo(Point point, int handleNumber)
{
int left = Rectangle.Left;
int top = Rectangle.Top;
int right = Rectangle.Right;
int bottom = Rectangle.Bottom;

switch (handleNumber)
{
case 1:
left = point.X;
top = point.Y;
break;
case 2:
top = point.Y;
break;
case 3:
right = point.X;
top = point.Y;
break;
case 4:
right = point.X;
break;
case 5:
right = point.X;
bottom = point.Y;
break;
case 6:
bottom = point.Y;
break;
case 7:
left = point.X;
bottom = point.Y;
break;
case 8:
left = point.X;
break;
}
Dirty = true;
SetRectangle(left, top, right - left, bottom - top);
ResizeImage(rectangle.Width, rectangle.Height);
}

protected void ResizeImage(int width, int height)


{
if (_originalImage != null)
{
Bitmap b = new Bitmap(_originalImage, new Size(width, height));
_image = (Bitmap)b.Clone();
b.Dispose();
}
}

508
public override bool IntersectsWith(Rectangle rectangle)
{
return Rectangle.IntersectsWith(rectangle);
}

/// <summary>
/// Перемещение объекта изображения
/// </summary>
/// <param name="deltaX"></param>
/// <param name="deltaY"></param>
public override void Move(int deltaX, int deltaY)
{
rectangle.X += deltaX;
rectangle.Y += deltaY;
Dirty = true;
}

public override void Dump()


{
base.Dump();
Trace.WriteLine("rectangle.X = " +
rectangle.X.ToString(CultureInfo.InvariantCulture));
Trace.WriteLine("rectangle.Y = " +
rectangle.Y.ToString(CultureInfo.InvariantCulture));
Trace.WriteLine("rectangle.Width = " +
rectangle.Width.ToString(CultureInfo.InvariantCulture));
Trace.WriteLine("rectangle.Height = " +
rectangle.Height.ToString(CultureInfo.InvariantCulture));
}

/// <summary>
/// Нормализация объекта прямоугольника
/// </summary>
public override void Normalize()
{
rectangle = DrawRectangle.GetNormalizedRectangle(rectangle);
}

/// <summary>
/// Сохранение объекта изображения в потоке сериализации
/// </summary>
/// <param name="info"></param>
/// <param name="orderNumber"></param>
public override void SaveToStream(SerializationInfo info, int orderNumber)
{
info.AddValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}", entryRectangle, orderNumber), rectangle);
info.AddValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}",
entryImage, orderNumber), _image);
info.AddValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}",
entryImageOriginal, orderNumber), _originalImage);

base.SaveToStream(info, orderNumber);
}

/// <summary>
/// Загрузка объекта изображения из потока сериализации
/// </summary>
/// <param name="info"></param>

509
/// <param name="orderNumber"></param>
public override void LoadFromStream(SerializationInfo info, int orderNumber)
{
rectangle = (Rectangle)info.GetValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}", entryRectangle, orderNumber),
typeof(Rectangle));
_image = (Bitmap)info.GetValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}", entryImage, orderNumber),
typeof(Bitmap));
_originalImage = (Bitmap)info.GetValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}", entryImageOriginal, orderNumber),
typeof(Bitmap));
base.LoadFromStream(info, orderNumber);
}

#region Вспомогательные функции


public static Rectangle GetNormalizedRectangle(int x1, int y1, int x2, int y2)
{
if (x2 < x1)
{
int tmp = x2;
x2 = x1;
x1 = tmp;
}

if (y2 < y1)


{
int tmp = y2;
y2 = y1;
y1 = tmp;
}
return new Rectangle(x1, y1, x2 - x1, y2 - y1);
}

public static Rectangle GetNormalizedRectangle(Point p1, Point p2)


{
return GetNormalizedRectangle(p1.X, p1.Y, p2.X, p2.Y);
}

public static Rectangle GetNormalizedRectangle(Rectangle r)


{
return GetNormalizedRectangle(r.X, r.Y, r.X + r.Width, r.Y + r.Height);
}
#endregion
}
}

Второй класс нам понадобится для реализации инструмента для рисования


изображения. ToolImage (ToolImage.cs) выглядит так:

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

namespace LWP15Draw
{
/// <summary>
/// Изображение
/// </summary>
internal class ToolImage : ToolObject
{

510
public ToolImage()
{
Cursor = new Cursor(GetType(), "C_Image.cur");
}

public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)


{
AddNewObject(drawArea, new DrawImage(e.X, e.Y));
}

public override void OnMouseMove(DrawArea drawArea, MouseEventArgs e)


{
drawArea.Cursor = Cursor;

if (e.Button == MouseButtons.Left)
{
Point point = new Point(e.X, e.Y);
drawArea.GraphicsList[0].MoveHandleTo(point, 5);
drawArea.Refresh();
}
}

public override void OnMouseUp(DrawArea drawArea, MouseEventArgs e)


{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Title = "Вставка изображения";
ofd.Filter = "Изображение в формате PNG|*.png|" + "Изображение в формате
JPEG|*.jpg|" + "Изображение в формате BMP|*.bmp|" + "Изображение в формате GIF|*.gif|" +
"Изображение в формате TIF|*.tif|" + "Изображения в формате ICO|*.ico|" + "Все файлы|
*.*";
ofd.InitialDirectory = Environment.SpecialFolder.MyPictures.ToString();

if (ofd.ShowDialog() == DialogResult.OK)
{
((DrawImage)drawArea.GraphicsList[0]).TheImage =
(Bitmap)Bitmap.FromFile(ofd.FileName);
}
ofd.Dispose();
base.OnMouseUp(drawArea, e);
}
}
}

Курсор под инструмент такой:

Не забываем изменить свойство для файла курсора: «Действие при построении»


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

511
(Name): изображениеToolStripButton
Text: &Изображение
ToolTipText: Изображение

В меню «Рисование» добавим новый пункт «Изображение» со следующими


свойствами:

(Name): изображениеToolStripMenuItem
Text: &Изображение
Image: Импортируем иконку

В файле DrawObject.cs находим:

private const string entryPenWidth = "PenWidth";

Добавляем после:

// Прочее
private bool dirty;

В том же файле DrawObject.cs находим:

/// <summary>
/// ID объекта
/// </summary>
public int ID
{
get { return id; }
set { id = value; }
}

Добавляем после:

/// <summary>
/// true когда объект изменён
/// </summary>
public bool Dirty
{
get { return dirty; }
set { dirty = value; }
}

В файле DrawArea.cs находим:

public enum DrawToolType


{
Pointer, Polygon, Line, Ellipse, Rectangle, NumberOfDrawTools
};

Заменяем на:

public enum DrawToolType


{
Pointer, Polygon, Line, Ellipse, Rectangle, Image, NumberOfDrawTools
};

В том же файле DrawArea.cs находим:

512
tools[(int)DrawToolType.Ellipse] = new ToolEllipse();
tools[(int)DrawToolType.Rectangle] = new ToolRectangle();

Добавляем после:

tools[(int)DrawToolType.Image] = new ToolImage();

На главной форме инициализируем события двух кнопок «Изображение» в


строке меню и на панели инструментов с иконками, а также добавляем общую функцию
для двух обработчиков Click:

#region Добавленный инструмент вставки изображения


private void изображениеToolStripButton_Click(object sender, EventArgs e)
{
CommandImage();
}

private void изображениеToolStripMenuItem_Click(object sender, EventArgs e)


{
CommandImage();
}

/// <summary>
/// Изображение
/// </summary>
private void CommandImage()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Image;
toolStripStatusLabel.Text = "Выбран инструмент \"Изображение\"";
}
#endregion

Готово. Можно компилировать и проверять работоспособность.

7. Завершающая часть

Компилируем приложение (Release) и запускаем. Рисуем что хотим, при помощи


всех доступных инструментов, смотрим что получилось в окне приложения:

513
Рис. 7. 1. Модифицированное приложение Windows Forms: рисуем

Сохраняем документ («Файл» -> «Сохранить» или «Сохранить как») как


Test.lwp, закрываем приложение, вновь запускаем, далее открываем ранее
сохранённый файл и смотрим список последних файлов («Файл»):

514
Рис. 7. 2. Модифицированное приложение Windows Forms: открываем ранее
сохранённый файл и просматриваем список последних открытых файлов

Выполняем «Файл» -> «Сохранить как изображение....». Сохраняем документ в


формате *.gif (Рис. 7. 3):

515
Рис. 7. 3. Результат работы приложения: изображение Test.gif, открытое в стандартном
редакторе изображений Windows 7

Перетаскиваем объекты в документе Test.lwp, меняем их размеры и свойства:

516
Рис. 7. 4. Модифицированное приложение Windows Forms: меняем свойства объектов в
открытом документе Test.lwp

Рис. 7. 5. Модифицированное приложение Windows Forms: перетаскиваем файл


Test.lwp на форму из проводника Windows (в результате файл откроется)

517
8. О приложении к Лабораторной работе № 15

Получившуюся программу и библиотеку (LWP15Draw.exe и LWP15Tools.dll),


собранную из кусков кода приведённых в данной лабораторной работе, архив с
изображениями иконок для меню (Icons.zip), использованный в данной работе, можно
загрузить по ссылке в конце этого материала (сслыка доступна в программном
продукте).

Приложении № 1 (Библиотека классов LWP15Tools): Исходный код всех


файлов библиотеки классов приведён по ссылке в конце этого материала (сслыка
доступна в программном продукте).
Приложении № 2 (Файлы классов Draw...): Исходный код всех файлов
классов начинающихся на слово «Draw» приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).
Приложении № 3 (Файлы классов Graphics...): Исходный код всех файлов
классов начинающихся на слово «Graphics» приведён по ссылке в конце этого
материала (сслыка доступна в программном продукте).
Приложении № 4 (Файлы классов Command...): Исходный код всех
файлов классов начинающихся на слово «Command» приведён по ссылке в конце этого
материала (сслыка доступна в программном продукте)
Приложении № 5 (Прочие файлы): Исходный код файлов UndoManager.cs,
Program.cs и AssemblyInfo.cs приведён по ссылке в конце этого материала (сслыка
доступна в программном продукте).
Приложении № 6 (Файлы классов Tool...): Исходный код всех файлов
классов начинающихся на слово «Tools» приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).

Приложение № 7 (Все формы и новые классы): Исходный код всех форм


программы, пользовательского элемента DrawArea, а также новых классов DrawImage и
ToolImage приведён по ссылке в конце этого материала (сслыка доступна в
программном продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

16. Лабораторная работа № 16: Windows Communication Foundation

Лабораторная работа № 16: Windows Communication Foundation

Содержание

36.Вводная часть
37.Создание приложения WCF
38.Модификация приложения WCF: приложение-клиент для WCF-службы
39.Создание приложения Windows Forms: сервер чата на WCF
40.Создание приложения Windows Forms: клиент чата на WCF
41.Завершающая часть
42.О приложении к Лабораторной работе № 16

1. Вводная часть

В этой работе будет рассмотрена работа с подтипом приложений доступных для


создания в Visual Studio 2010, а именно Приложение службы WCF. Что такое WCF?

518
Windows Communication Foundation (WCF) — программный «фреймворк»,
используемый для обмена данными между приложениями входящими в состав .NET
Framework. До своего выпуска в декабре 2006 года в составе .NET Framework 3.0,
WCF был известен под кодовым именем Indigo.

WCF делает возможным построение безопасных и надёжных транзакционных


систем через упрощённую унифицированную программную модель межплатформенного
взаимодействия. Или проще: приложений для удалённого межплатформенного обмена
данными. Комбинируя функциональность существующих технологий .NET по
разработке распределённых приложений (ASP.NET XML Web Services — ASMX, WSE
3.0, .NET Remoting, .NET Enterprise Services и System.Messaging), WCF
предоставляет единую инфраструктуру разработки, при умелом применении
повышающую производительность и снижающую затраты на создание безопасных,
надёжных и транзакционных Web-служб. Заложенные в неё принципы
интероперабельности (способность к взаимодействию) позволяют организовать
работу с другими платформами, для чего используются технологии взаимодействия
платформ, например WSIT (Web Services Interoperability Technology)
разрабатываемые на базе открытого исходного кода.

«Хостинг» (расположение) приложений WCF:

Класс службы WCF не может существовать самостоятельно. Каждая служба WCF


должна находиться под управлением некоторого процесса Windows, называемого
хостовым (серверным) процессом. Существуют несколько вариантов хостинга:
 Автохостинг (то есть хост-процессом является, к примеру, консольное
приложение или графическое приложение Windows Forms)
 Хостинг в одной из служб Windows.
 Хостинг с использованием IIS (Internet Information Server) или WAS
(Windows Activation Services).

В данной работе будет рассмотрен простейший случай работы с WCF, а также будет
написан клиент и сервер для реализации возможностей чата на основе WCF (оба
приложения будут в виде Windows Forms).

2. Создание приложения WCF:

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

519
Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение службы WCF. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

Рис. 2. 2. Окно создания нового проекта

520
В поле Имя вводим LWP16WCF — это название программы (выбрано по
названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

521
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).
В данном типе проекта может быть два возможных вывода результата после
компиляции. Если в обозревателе решений выбран любой файл кроме Service1.svc,
будет открыто окно Internet Explorer с содержимым каталога проекта:

522
Рис. 2. 5. Запуск приложения WCF по конфигурации Debug (не выбран файл
Service1.svc)

Если выбрать вышеуказанный файл, то должен открыться Тестовый клиент


WCF который служит для проверки работоспособности сервиса и выполняет функции
простого клиента.

Тестовый клиент WCF (WcfTestClient.exe) представляет собой средство с


графическим интерфейсом пользователя, позволяющее вводить тестовые параметры,
отправлять их в службу и просматривать ответную реакцию службы. При совместном
использовании с узлом службы WCF это обеспечивает удобную практику тестирования
служб.
Тестовый клиент WCF (WcfTestClient.exe) находится в следующей папке (для
Visual Studio 2010): C:\Program Files (x86)\Microsoft Visual Studio 10.0\
Common7\IDE\

Рис. 2. 6. Запуск приложения WCF по конфигурации Debug (выбран файл Service1.svc)

523
Собственно тестирование происходит так: дважды нажмём на GetData(), затем в
поле Запрос -> строка value введём любое число и нажмём Вызвать. В окне
Предупреждение системы безопасности жмём ОК и наслаждается ответом службы
в поле Ответ -> return:

Рис. 2. 7. Запуск приложения WCF по конфигурации Debug (выбран файл


Service1.svc): тестирование службы

Здесь также можно проверить работу и второго доступного параметра


(контейнера с двумя разными типами переменных): GetDataUsingDataContract().

ПРИМЕЧАНИЕ № 1: Если по каким-либо причинам произошли ошибки при


компиляции, можно проверить наличие всех необходимых компонентов. Если на ПК
Visual Studio 2010 установлен правильно и со всеми необходимыми компонентами,
добавлять ничего не нужно и ошибок не должно возникать. Если же были установлены
не все компоненты (например, нет IIS), то можно воспользоваться специальным
установщиком (ищем его в на одном из верхним меню Visual Studio 2010): Установщик
веб-платформы 3.0:

После запуска установщика, выбираем неотступающие компоненты (при наличии


подключения к сети Интернет). Выбор компонента происходит нажатием кнопки
Добавить:

524
Рис. 2. 8. Установщик веб-платформы 3.0: выбор компонентов на вкладке Важный
сайт

Рис. 2. 9. Установщик веб-платформы 3.0: выбор компонентов на вкладке


Продукты -> Сервер

525
3. Модификация приложения WCF: приложение-клиент для WCF-службы

Для начала изменим имя сервиса. Выделим файл Service1.svc в обозревателе


решений и перейдём к его коду (правая кнопка мыши -> Перейти к коду либо F7).
Код файла содержит классы для работы с параметрами сервиса, теми самыми, что были
доступны в клиенте WCF. Найдём в коде строку:

// ПРИМЕЧАНИЕ. Команду "Переименовать" в меню "Рефакторинг" можно использовать для


одновременного изменения имени класса "Service1" в коде, SVC-файле и файле конфигурации.
public class Service1 : IService1
{

Выделим имя класса (слово) Service1 и далее ПКМ -> во всплывающем меню:
Рефакторинг -> Переименовать (или F2):

Рис. 3. 1. Переименование сервиса

Вводим в открывшемся окне имя LWP16Service, жмём ОК, в следующем окне


Применить:

Рис. 3. 2. Переименование сервиса: Переименовать

526
Рис. 3. 2. Переименование сервиса: Просмотр изменений - Переименование

То же самое проделаем для имени интерфейса (IService1). Введём новое имя


ILWP16Service. И переименуем имя файла сервиса в LWP16Service:

Слегка изменим сервис. Откроем код класса интерфейса сервиса (файл


ILWP16Service.cs) и найдём:

[OperationContract]
string GetData(int value);

Изменим так:

[OperationContract]

527
string GetInt(int value);

[OperationContract]
string GetString(string value);

Обратим внимание на атрибуты [OperationContract] в приведенном выше коде.


Эти атрибуты обязательны для любого метода, предоставляемого службой. Параметры
которым обладает «контракт»:

IsInitiating – если true тогда связь с сервисом и сессия начинается, создается


объект реализации на сервисе и запускается его конструктор. IsOneWay – если true
данный метод ничего не возвращает и только в одну сторону. IsTerminating –
значение true этого параметра приводит к тому, что по окончанию его обработки на
сервисе связь с клиентом прерывается и сессия закрывается, т.е. последующие
обращения к сервису приведут к ошибкам. Выглядит это так:

[OperationContract(IsInitiating = true, IsOneWay = false, IsTerminating = false)]

Для [ServiceContract]:

[ServiceContract(SessionMode = SessionMode.Required)]

Параметр SessionMode отвечатет за то, разрешены, запрещены или требуются


ли сеансы.

В файле LWP16Service.svc.cs найдём:

public class LWP16Service : ILWP16Service


{
public string GetData(int value)
{
return string.Format("You entered: {0}", value);
}

Заменим на:

public string GetInt(int value)


{
return string.Format("Вы ввели число: {0}", value);
}

public string GetString(string value)


{
return string.Format("Вы ввели строку: {0}", value);
}

Сервис можно компилировать по конфигурации Release (Построение ->


Построить решение, либо F6).

Осталось добавить клиентское приложение. Интегрируем такое приложение


непосредственно в текущее решение. Для этого выполним следующие действия: Файл -
> Создать -> Проект. В окне открывшемся окне «Создать проект», в поле Решение,
выберем: Добавить в решение. В качестве проекта выберем Приложение Windows
Forms. Имя будет: LWP16WCFClient. Жмём ОК. Получим следующее:

528
Рис. 3. 3. Обозреватель решений: содержимое решения, состоящего из двух проектов

Теперь интегрируем службу LWP16Service.svc в форму. Для этого в


обозревателе решений выделим имя проекта (LWP16WCFClient), далее жмём правую
кнопку мыши -> Добавить ссылку на службу...:

В открывшемся окне жмём на Найти, далее Службы решения. Наша служба


автоматически добавится в список. В поле Пространство имён вводим:
LWP16ServiceReference:

529
Рис. 3. 4. Добавить ссылку на службу: добавление службы из нашего решения в
другой проект этого же решения

Жмём ОК. И откроем конструктор формы клиентского проекта2.

ПРИМЕЧАНИЕ № 2: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

На форме клиентского проекта Form1 расставим два текстовых поля, под ними
одну кнопку и ещё ниже два статических поля Label. Все имена и свойства
добавленных элементов оставим без изменений. Задаём следующие параметры самой
формы на панели Свойства:

(Name) изменим с Form1.cs3 на LWP16Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Клиент для службы
WCF (C#)
^ Поменяем заголовок формы (то, что отображается в шапке приложения слева).
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.
Icon изменим изображение (иконку)

530
приложения
^ Необходим файл значка *.ico.
FormBorderStyle > изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.
Size изменим со значений 290; 290 на 350;
200
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 3: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Форма будет выглядеть примерно так:

Рис. 3. 5. Модифицированная форма приложения и расстановка необходимых


элементов управления

Кнопку и текстовые поля TextBox растянем до границ формы.

Дважды щёлкнем по кнопке на форме, тем самым создав событие Click. Впишем
код:

private void button1_Click(object sender, EventArgs e)


{
if (textBox1.Text == "Ведите число") { textBox1.Text = "0"; }
LWP16ServiceReference.LWP16ServiceClient client = new
LWP16ServiceReference.LWP16ServiceClient(); // Открываем сеанс связи со службой

returnString1 = client.GetInt(Convert.ToInt32(textBox1.Text));
label1.Text = returnString1;
returnString2 = client.GetString(textBox2.Text);
label2.Text = returnString2;
client.Close(); // Закрываем сеанс связи
}

В этом же файле найдём:

531
public partial class LWP16Main : Form
{

Вставим после:

String returnString1;
String returnString2;

Код метода LWP16Main() изменим так:

public LWP16Main()
{
InitializeComponent();
label1.Text = "";
label2.Text = "";
textBox1.Text = "Ведите число";
textBox2.Text = "Введите строку";
button1.Text = "Связать со службой";
}

И для первого (верхнего) TextBox объявим событие KeyPress, в котором


запретим вводить все символы кроме цифр. Если этого не сделать, поймаем
исключение. Служба на это поле будет передавать данные из метода GetInt(),
который в качестве параметра принимает только int. Можно обойтись и без события
для TextBox, тогда исключение можно обработать try-catch блоком. Код события:

private void textBox1_KeyPress(object sender, KeyPressEventArgs e)


{
// Введённые символы должны быть только цифрами, иначе ввода не будет (символ
не введётся)
if (!Char.IsDigit(e.KeyChar))
{
e.Handled = true;
}
}

Сделаем нашу форму автоматически запускаемой при компиляции. Откроем


свойства решения ( ) и в поле Запускаемый проект
выберем LWP16WCFClient:

Компилируем приложение (Release) и запускаем. Откроется форма клиента.


Вводим в первое поле цифры, во второе какую-нибудь строку и жмём на кнопку
Связаться со службой. Получаем ответ в виде двух статических строк внизу формы:

532
Рис. 3. 6. Модифицированное приложение Windows Forms: работа клиента для службы
WCF

4. Создание приложения Windows Forms: сервер чата на WCF

Чат. Наиболее простое и часто встречающее приложение на WCF для


демонстрации возможности технологии удалённого «общения». Так как WCF требует
нечто что должно выполнять функции «хоста», то самым простым решением станет
создание приложение-сервера, которое будет запускать и останавливать сервис для
работы множества (2 и более) приложений-клиентов. Каждое приложение-клиент будет
реализовывать удалённый клиент чата — попросту одного пользователя в чате. Средой
для связи (сервер-клиент) выступит localhost (локальная машина), но можно
использовать и адрес удалённого сервера, например в локальной сети. Ключевым
адресом является адрес сервера.

Теперь о нашем приложении:

Приложение будет простой формой с двумя кнопками, одним GroupBox и одним


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

Открываем снова Visual Studio и создаём пустое решение (Файл -> Создать ->
Новый проект). Выбираем Другие типы проектов -> Решение Visual Studio ->
Новое решение. Имя выбираем как LWP16:

533
Рис. 4. 1. Вводим данные нового пустого решения

Жмём ОК. Было добавлено решение, но пока без проектов. Создаём новый
проект в текущем решении. Для этого выполним следующие действия: Файл -> Создать
-> Проект. В окне открывшемся окне «Создать проект», в поле Решение, выберем:
Добавить в решение. В качестве проекта выберем Приложение Windows Forms. Имя
будет: LWP16-ChatServer. Жмём ОК. Получим следующее:

534
Рис. 4. 2. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Сразу же добавим необходимую библиотеку. Должен быть выбрать текущий


проект в обозревателе решений (LWP16-ChatServer). Выполним Проект -> Добавить
ссылку... -> в окне Добавить ссылку переходим на вкладку .NET и ищем
System.ServiceModel:

Рис. 4. 3. Добавить ссылку: добавляем новую ссылку на библиотеку


System.ServiceModel

535
Переименуем форму в обозревателе решений (ПКМ на иконке формы ->
Переименовать). Новое имя формы будет LWP16MainServer. После переименования,
автоматически изменится свойство (Name) формы.

Теперь изменим свойства формы LWP16MainServer:

Text изменим с Form1 на Чат на WCF (C#) ::


Сервер
^ Поменяем заголовок формы (то, что отображается в шапке приложения слева).
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
FormBorderStyle > изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.
Size изменим со значений 290; 290 на 300;
125
^ Поменяем размер формы.

Расставим элементы с панели инструментов как показано на рисунке ниже:

Рис. 4. 3. Расстановка элементов на форме приложения-сервера

Свойства элементов следующие:

Button:
(Name): B_Start
Text: Запуск
Button:
(Name): B_Stop
Text: Остановка
GroupBox:
(Name): GB_1
Text: Серверные операции
Label:
(Name): StatusLabel
Text: Состояние сервера

Событие Click кнопки Запуск:

536
private void B_Start_Click(object sender, EventArgs e)
{
try
{
cprs = new CustomPeerResolverService(); // Инициалазируем службу узлов
cprs.RefreshInterval = TimeSpan.FromSeconds(5); // Время в секундах
обновления таблицы записей распознователя одноранговых узлов
host = new ServiceHost(cprs); // Передаём серверу службу узлов
cprs.ControlShape = true; // Инициализируем совместное использование
ссылок
cprs.Open(); // Открываем службу
host.Open(TimeSpan.FromDays(1)); // Запускаем сервер на период одного дня
StatusLabel.Text = "Сервер WCF успешно запущен!";
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
finally
{
B_Start.Enabled = false;
B_Stop.Enabled = true;
}
}

Событие Click кнопки Остановка:

private void B_Stop_Click(object sender, EventArgs e)


{

try
{
cprs.Close();
host.Close();
StatusLabel.Text = "Сервер WCF успешно остановлен!";
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
finally
{
B_Start.Enabled = true;
B_Stop.Enabled = false;
}
}

Отредактируем код файла формы LWP16MainServer.cs. Подключим библиотеки


в начале файла:

using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.PeerResolvers;

Найдём:

public partial class LWP16MainServer : Form


{

Добавим после:

537
private CustomPeerResolverService cprs; // Объекта базовой реализации
настраиваемой службы распознователя одноранговых узлов
private ServiceHost host; // Объект сервера для службы

Метод LWP16MainServer():

public LWP16MainServer()
{
InitializeComponent();
B_Stop.Enabled = false;
}

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


App.Config. Так как при создании приложения Winfows Forms файл конфигурации не
создаётся, то нужно создать его самостоятельно. Должен быть выбрать текущий проект
в обозревателе решений (LWP16-ChatServer). Выполним Проект -> Добавить новый
элемент... (Ctrl+Shift+A). В открывшемся окне выберем Файл конфигурации
приложения. Имя оставим неизменным.

Откроем новый добавленный файл App.config и вставим в файл следующий XML-


код:

<?xml version="1.0"?>
<configuration>
<system.serviceModel>
<services>
<service name="System.ServiceModel.PeerResolvers.CustomPeerResolverService">
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:5433/LWP16_ChatServer"/>
</baseAddresses>

538
</host>
<endpoint address="net.tcp://localhost:5433/LWP16_ChatServer"
binding="netTcpBinding" bindingConfiguration="TcpConfig"
contract="System.ServiceModel.PeerResolvers.IPeerResolverContract">
</endpoint>
</service>
</services>

<bindings>
<netTcpBinding>
<binding name="TcpConfig">
<security mode="None"></security>
</binding>
</netTcpBinding>
</bindings>
</system.serviceModel>
</configuration>

Здесь основные значения содержатся в поле:

<add baseAddress="net.tcp://localhost:5433/LWP16-ChatServer"/>

Строчкой выше указывается локальный адрес и порт. Вместо localhost можно


использовать реальный IP-адрес. Следующая строка:

<endpoint address="net.tcp://localhost:5433/LWP16-ChatServer"
binding="netTcpBinding" bindingConfiguration="TcpConfig"
contract="System.ServiceModel.PeerResolvers.IPeerResolverContract">
</endpoint>

Обратим внимание на привязку netTcpBinding.

Эта привязка создаёт стек связи времени выполнения по умолчанию,


использующий режим безопасности транспорта, протокол TCP для доставки сообщений,
а также кодирование двоичных сообщений. Эта привязка является должным
предоставляемым системой выбором Windows Communication Foundation для
взаимодействия через интрасеть.

Конфигурация по умолчанию для привязки netTcpBinding быстрее, чем


конфигурация, предоставляемая привязкой wsHttpBinding, но она предназначена
только для взаимодействия WCF с WCF. Режим безопасности настраивается с помощью
дополнительного атрибута securityMode. Использование WS-ReliableMessaging
настраивается с использованием дополнительного атрибута reliableSessionEnabled.
Но по умолчанию надежный обмен сообщениями отключен. В общем случае системные
привязки по протоколу HTTP, такие как wsHttpBinding и basicHttpBinding,
настроены на включение основных возможностей по умолчанию, в то время как
привязка netTcpBinding по умолчанию отключает возможности, так что для получения
поддержки, например для спецификаций WS-*, необходимо специально их включить.
Это означает, что используемая по умолчанию конфигурация для TCP быстрее при
обмене сообщениями между конечными точками, чем конфигурация по умолчанию для
привязок HTTP.
Привязка задается в файлах конфигурации клиента и службы (App.config). Тип
привязки указывается в атрибуте binding элемента <endpoint>. Если необходимо
настроить привязку netTcpBinding и изменить некоторые из ее параметров, необходимо
определить конфигурацию привязки. Конечная точка должна ссылаться на

539
конфигурацию привязки с атрибутом bindingConfiguration. Для нашего случая
атрибут имеет привязку к имени TcpConfig:

<binding name="TcpConfig">
<security mode="None"></security>
</binding>

Компилируем приложение (Release) и запускаем. Запускаем и останавливаем


сервер. Не должно быть никаких ошибок при нажатии кнопок:

Рис. 4. 4. Модифицированное приложение Windows Forms: работа сервера чата после


запуска

5. Создание приложения Windows Forms: клиент чата на WCF

Для создания клиентского приложения выполним аналогичные пункту № 4


данной лабораторной работы действия.

Создаём новый проект в текущем решении (LWP16). Для этого выполним


следующие действия: Файл -> Создать -> Проект. В окне открывшемся окне «Создать
проект», в поле Решение, выберем: Добавить в решение. В качестве проекта выберем
Приложение Windows Forms. Имя будет: LWP16-ChatClient. Жмём ОК.

Сразу же добавим необходимые библиотеки. Должен быть выбрать текущий


проект в обозревателе решений (LWP16-ChatClient). Выполним Проект -> Добавить
ссылку... -> в окне Добавить ссылку переходим на вкладку .NET и ищем
System.ServiceModel. Если ОС на которой запущена среда разработки Windows 7,
добавим также ещё одну библиотеку: Microsoft Speech Object Library (вкладка
COM):

540
Рис. 5. 2. Добавить ссылку: добавляем новую ссылку на библиотеку Microsoft Speech
Object Library

Переименуем форму проекта клиентского приложений в обозревателе решений


(ПКМ на иконке формы -> Переименовать). Новое имя формы будет LWP16MainClient.
После переименования, автоматически изменится свойство (Name) формы.

Теперь изменим свойства формы LWP16MainClient:

Text изменим с Form1 на Чат на WCF (C#) ::


Клиент
^ Поменяем заголовок формы (то, что отображается в шапке приложения слева).
MaximizeBox изменим с True на False
^ Уберём кнопку Развернуть.
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
FormBorderStyle > изменим с Sizable на FixedDialog
^ Сделаем окно «неизменяем» по размерам.
Size изменим со значений 290; 290 на 300;
125
^ Поменяем размер формы.

Расставим элементы с панели инструментов как показано на рисунке ниже:

541
Рис. 5. 1. Расстановка элементов на форме приложения-клиента

Здесь у нас в первой группе (GroupBox) «Пользовательские данные»: один Label,


один TextBox и Button.

Свойства элементов следующие:

GroupBox:
(Name): GB_UserDetails
Text: Пользовательские данные
Label:
(Name): L_LoginInfo
Text: Введите имя для входа и нажмите на
«Войти в чат»
TextBox:
(Name): TB_UserName
Button:
(Name): B_Login
Text: Войти в чат

Во второй группе элементов один GroupBox и один ListBox:

GroupBox:
(Name): GB_UserList
Text: Пользователи в сети
ListBox:
(Name): LB_Users

Последняя группа содержит один RichTextBox, TextBox и две кнопки.

542
GroupBox:
(Name): GB_MessageWindow
Text: Сообщения чата
RichTextBox:
(Name): RTB_Messages
ReadOnly: True
Multiline True
TextBox:
(Name): TB_SendMessage
Button:
(Name): B_Send
Text: Отправить
Button:
(Name): B_WakeUp
Text: !

Перейдём к редактированию кода формы. В начале файла с кодом для формы


LWP16MainClient.cs объявим:

using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Runtime.InteropServices;
using SpeechLib; // Для работы имитации голоса (в Windows XP не использовать)

Найдём:

namespace LWP16_ChatClient
{

Добавим после:

[ServiceContract(CallbackContract = typeof(ILWP16Service))]
public interface ILWP16Service // Интерфейс подключения к службе сервера
{
[OperationContract(IsOneWay = true)] // Операция не возвращает ответовное
сообщение
void Join(string memberName); // Объявление метода присоединения к чату (по
имени)
[OperationContract(IsOneWay = true)]
void SendMessage(string memberName, string message); // Объявление метода отсылки
сообщения (по имени и тексту)
[OperationContract(IsOneWay = true)]
void WakeUp(string memberName, string wakeup); // Объявление метода отсылки
сообщения (по имени и тексту)
[OperationContract(IsOneWay = true)]
void Leave(string memberName); // Объявление метода выхода из чата (по имени)
[OperationContract(IsOneWay = true)]
void ImageFF(string memberName, Bitmap image); // Объявление метода выхода из
чата (по имени)
}

public interface ILWP16Channel : ILWP16Service, IClientChannel


{
}

Найдём:

543
public partial class LWP16MainClient : Form
{

Заменим на:

public partial class LWP16MainClient : Form, ILWP16Service


{
[DllImport("kernel32.dll")]
public static extern bool Beep(int BeepFreq, int BeepDuration);

private delegate void UserJoined(string name);


private delegate void UserSendMessage(string name, string message);
private delegate void UserWakeUp(string name, string wakeup);
private delegate void UserLeft(string name);

private static event UserJoined NewJoin; // Экземпляр события присоединения к


чату через делегат
private static event UserSendMessage MessageSent; // Экземпляр события отсылки
сообщения через делегат
private static event UserWakeUp NewWakeUp; // Экземпляр события сообщения типа:
"разбудить чат"
private static event UserLeft RemoveUser; // Экземпляр события выхода из чата
через делегат

private string userName; // Переменная имени пользователя в чате


private ILWP16Channel channel; // Экземпляр интерфейса чата для канала
// class ServiceModel.DuplexChannelFactory<TChannel>
private DuplexChannelFactory<ILWP16Channel> factory; // Объект средства приёма и
передаче "дуплексных" сообщений по каналам в обе стороны

Метод LWP16MainClient() измени так:

public LWP16MainClient()
{
InitializeComponent();
this.AcceptButton = B_Login; // Привязываем событие Нажатия Enter с кнопкой
"Войти в чат"
}

После добавим:

public LWP16MainClient(string userName)


{
this.userName = userName;
}
// Метод присоединения к чату (по имени)
void LWP16Client_NewJoin(string name)
{
RTB_Messages.AppendText("\r\n");
RTB_Messages.AppendText(name + " присоединился: [" + DateTime.Now.ToString()
+ "]"); // Добавляем в RichTextBox строчку c именем и датой входа пользователя
LB_Users.Items.Add(name); // Добавляем нового пользователя в ListBox
}
// Метод отсылки сообщения (по имени и тексту)
void LWP16Client_MessageSent(string name, string message)
{
if (!LB_Users.Items.Contains(name)) // Если имени нет в ListBox при получении
сообщения в RichTextBox
{
LB_Users.Items.Add(name); // Добавляет нового пользователя в ListBox
}

544
RTB_Messages.AppendText("\r\n");
RTB_Messages.AppendText(name + " говорит: " + message + " [" +
DateTime.Now.ToString() + "]"); // Добавляем в RichTextBox строчку с именем и сообщением
if (message == "WakeUp") { Beep(500, 100); }
}
// Метод присоединения к чату (по имени)
void LWP16Client_WakeUp(string name, string wakeup)
{
if (!LB_Users.Items.Contains(name)) // Если имени нет в ListBox при получении
сообщения в RichTextBox
{
LB_Users.Items.Add(name); // Добавляет нового пользователя в ListBox
}
RTB_Messages.AppendText("\r\n");
RTB_Messages.AppendText(name + " попытался разбудить чат: [" +
DateTime.Now.ToString() + "]"); // Добавляем в RichTextBox строчку c именем и датой входа
пользователя
if (wakeup == "WakeUp")
{
Beep(500, 100);
Beep(500, 100);
SpVoice voice = new SpVoice();
voice.Speak("Wake Up Mate", SpeechVoiceSpeakFlags.SVSFDefault);
}
}
// Метод выхода из чата (по имени)
void LWP16Client_RemoveUser(string name)
{

try
{
RTB_Messages.AppendText("\r\n");
RTB_Messages.AppendText(name + " вышел: [" + DateTime.Now.ToString() +
"]"); // Добавляем в RichTextbox строчку с именем и датой выхода пользователя
LB_Users.Items.Remove(name); // Удаляем по имени из ListBox
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.ToString());
}
}

void Online(object sender, EventArgs e)


{
RTB_Messages.AppendText("\r\nВ сети: " + this.userName);
}

void Offline(object sender, EventArgs e)


{
RTB_Messages.AppendText("\r\nНе в сети: " + this.userName);
}

#region ILWP16Service Основные методы

public void Join(string memberName)


{
if (NewJoin != null)
{
NewJoin(memberName);
}
}

public void SendMessage(string memberName, string message)


{
if (MessageSent != null)

545
{
MessageSent(memberName, message);
}
}

public void WakeUp(string memberName, string wakeup)


{
if (NewWakeUp != null)
{
NewWakeUp(memberName, wakeup);
}
}

public new void Leave(string memberName)


{
if (RemoveUser != null)
{
RemoveUser(memberName);
}
}

#endregion

Перейдём к событиям формы. Для кнопки «Войти в чат» событие Click будет
содержать код:

private void B_Login_Click(object sender, EventArgs e)


{
if (!string.IsNullOrEmpty(TB_UserName.Text.Trim()))
{

try
{
NewJoin += new UserJoined(LWP16Client_NewJoin); // Переопределяем
вызов метода подключения к чату через экзмепляр события
MessageSent += new UserSendMessage(LWP16Client_MessageSent); //
Переопределяем вызов метода отсылки сообщения в чат через экзмепляр события
NewWakeUp += new UserWakeUp(LWP16Client_WakeUp); // Переопределяем
вызов метода "разбудить чат" в чат через экзмепляр события
RemoveUser += new UserLeft(LWP16Client_RemoveUser); // Переопределяем
вызов метода выхода из чата через экзмепляр события

channel = null;
this.userName = TB_UserName.Text.Trim(); // Удаляем пробелы из имени
пользователя
// class ServiceModel.InstanceContext
InstanceContext context = new InstanceContext(new
LWP16MainClient(TB_UserName.Text.Trim()));
factory = new DuplexChannelFactory<ILWP16Channel>(context,
"ChatEndPoint"); // Получаем данные из app.config и передаём данные duplex-каналу
channel = factory.CreateChannel(); // Создаём канал и передаём его
экземпляру интерфейса чата
// class ServiceModel.IOnlineStatus
IOnlineStatus status = channel.GetProperty<IOnlineStatus>(); //
Определяем экземпляр для индикации доступности объекта по каналу
status.Offline += new EventHandler(Offline); // Вызов метода Offline
(если в чате больше никого)
status.Online += new EventHandler(Online); // Вызов метода Online
(если в чате больше одного пользователя)
channel.Open(); // Открываем канал
channel.Join(this.userName); // Вызываем метод Join() с текущим
именем пользователя введённым в TB_Username
GB_MessageWindow.Enabled = true; // Включаем группу "Сообщения чата"
GB_UserList.Enabled = true; // Включаем группу "Список пользователей"

546
GB_UserDetails.Enabled = false; // Гасим группу "Данные для входа"
this.AcceptButton = B_Send; // Enter = "Отослать"
RTB_Messages.AppendText("*****************************ДОБРО
ПОЖАЛОВАТЬ В ЧАТ*****************************\r\n");
TB_SendMessage.Select();
TB_SendMessage.Focus();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
}

События Click кнопок «Отправить» и «!»:

private void B_Send_Click(object sender, EventArgs e)


{
channel.SendMessage(this.userName, TB_SendMessage.Text.Trim()); // Вызываем
метод SendMessage() и отсылаем имя пользователя и сообщение
TB_SendMessage.Clear(); // Очищаем TB_SendMessage и далее передаём фокус на
элемент и делаем его активным
TB_SendMessage.Select();
TB_SendMessage.Focus();
}

private void B_WakeUp_Click(object sender, EventArgs e)


{
channel.WakeUp(this.userName, "WakeUp"); // Вызываем метод WakeUp() и
отсылаем имя пользователя и текст "WakeUp"
TB_SendMessage.Clear(); // Очищаем TB_SendMessage и далее передаём фокус на
элемент и делаем его активным
TB_SendMessage.Select();
TB_SendMessage.Focus();
}

Событие FormClosing формы LWP16MainClient:

private void LWP16MainClient_FormClosing(object sender, FormClosingEventArgs e)


{

try
{

if (channel != null)
{
channel.Leave(this.userName); // Если закрываем форму, вызваем метод
Leave() и удаляем пользователя
channel.Close(); // Закрываем канал между сервером и клиентом
}

if (factory != null)
{
factory.Close(); // Закрываем duplex-канал
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}

547
Теперь разъясним принцип работы всего того что было тут наворочено. После
запуска приложения-сервера, происходит инициализация канала по определённому
адресу. Был выбран локальный адрес и порт 5433, а также абсолютное имя
пространства имён LWP16_ChatServer для адресации по этому порту. Имя для
адресации в строке конфигурации клиента:

<endpoint name="ChatEndPoint" address="net.p2p://LWP16-Сhat/LWP16_ChatServer"


И...

<custom address="net.tcp://localhost:5433/LWP16_ChatServer"

Может быть любым, но! Главное чтобы оно совпадало с серверной строкой
адреса (в файле конфигурации приложения-сервера). То есть, запущенное
приложение-клиент должно содержать в файле конфигурации этот абсолютный адрес.
После ввода имени, становится доступной кнопка «Войти в чат». После нажатия на
кнопку, в приложении-клиенте происходит вызов (через делегат и событие) метода
интерфейса: Join(string memberName), содержащее переданное из текстового поля
строку с именем. И далее выполняется: LWP16Client_NewJoin(string name), где в
RichTextBox добавляется запись о присоединении пользователя с таким-то именем к
чату и временем присоединения. Имя также добавляется в ListBox. Если пользователь в
уже не один, срабатывает метод Online(), которые сразу же оповещает и пользователя
и другого пользователя о том, что они «в сети». Метод срабатывает в том случае, если
в сети больше одного пользователя, после нажатия кнопки «Войти в чат» пользователя
ещё не вошедшего в чат. Вызывает событие после получения по каналу «индикатора
события доступности»:

IOnlineStatus status = channel.GetProperty<IOnlineStatus>();

Подключение третьего пользователя не вызывает срабатывание метода у двух


предыдущих, но «в сети» отображается у нового пользователя, которому выдаётся
оповещение о том что в канале он не один. Также любое подключение вызывает у всех
клиентов срабатывание события NewJoin и следовательно Join(<имя
подключившегося>).
Сообщение отсылается по такому же событийному механизму, но содержит также
текст сообщения, которое пользователь набрал у себя в клиенте. Отправка идёт после
нажатия кнопки «Отправить». В ListBox новый пользователи чата добавляются лишь по
факту совершения события и появления записи с именем в RichTextBox, потом если в
сети один клиент, то об имени уже находящегося в канале пользователя новый клиент
узнает только после совершения тем пользователем действий.
Аналогичным образом работает событие WakeUp, которое передаёт чату имя
пользователя нажавшего кнопку «!» и текст «WakeUp», который не отображается в
чате. После получения такого сообщения, все клиенты издают системный звук (чтобы
разбудить тех кто находится в чате). А также, если ОС на которой запущен клиент это
Windows Vista или Windows 7, то приложение проговаривает заранее заготовленную
фразу. Также системный звук раздаётся, если написать в чат непосредственно слово
WakeUp.
Выход из чата (через закрытие формы и вызов события FormClosing) тоже
выдаёт оповещения, а также происходит удаление имени вышедшего из ListBox всех
клиентов.

Наше приложение-клиент готово. Можно компилировать и проверять


работоспособность.

6. Завершающая часть

Компилируем приложения (Release) и запускаем. В качестве основного


запускаемого объекта решения (Запускаемый проект) ставим приложение сервер.

548
Стартуем сервер нажатием кнопки «Запуск». Запускаем копию приложения клиента.
Вводим имя и жмём «Войти в чат»:

Рис. 6. 1. Результат работы приложения клиента после запуска сервера: вход первого
пользователя в чат и отсылка сообщения

Запускаем второй клиент, входим в чат и пишем сообщение:

549
Рис. 6. 2. Результат работы приложения клиента после запуска сервера: вход второго
пользователя в чат и его действия

Закрывает первый клиент, запускаем третий клиент и пишем со второго и


третьего клиентов:

550
Рис. 6. 3. Результат работы приложения клиента после запуска сервера: вход третьего
пользователя в чат и его действия

7. О приложении к Лабораторной работе № 16

Получившиеся программы (LWP16WCF.exe, LWP16WCFClient.exe, LWP16-


ChatServer.exe и LWP16-ChatClient.exe), собранные из кусков кода приведённых в
данной лабораторной работе, можно загрузить по ссылке в конце этого материала
(сслыка доступна в программном продукте).

Приложение № 1: Исходный код приложения WCF, а также клиента к нему и


всех сопровождающих файлов с кодом приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).
Приложение № 2: Исходный код программы-сервера чата и всех
сопровождающих файлов с кодом приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).
Приложение № 3: Исходный код программы-клиента чата и всех
сопровождающих файлов с кодом приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).

551
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

17. Лабораторная работа № 17: Знакомство с Silverlight

Лабораторная работа № 17: Знакомство с Silverlight

Содержание

43.Вводная часть
44.Создание приложения Silverlight
45.Модификация приложения Silverlight: первые шаги и полноэкранный
режим
46.Модификация приложения Silverlight: простой проигрыватель MP3-
файлов
47.Модификация приложения Silverlight: работа с анимацией
48.Завершающая часть
49.О приложении к Лабораторной работе № 17

1. Вводная часть

В этой работе будет рассмотрена работа с подтипом приложений доступных для


создания в Visual Studio 2010, а именно Приложение Silverlight. Что такое Silverlight?

Рис. 1. 1. Логотип технологии Microsoft Silverlight

Microsoft Silverlight — это программная платформа, включающая в себя плагин


(надстройку) для браузера, который позволяет запускать приложения, содержащие
анимацию, векторную графику и аудио-видео ролики, что характерно для RIA (Rich
Internet Application).
Silverlight предоставляет графическую систему, схожую с Windows
Presentation Foundation, и объединяет мультимедиа, графику, анимацию и
интерактивность в одной программной платформе. Он был разработан, чтобы работать
с XAML и с языками .NET.
Приложения Silverlight могут быть написаны на любом языке программирования,
включённом в платформу .NET. Как таковые, все инструменты разработки которые
могут быть использованы для разработки на .NET, также могут работать c Silverlight.
Как уже было сказано выше Silverlight это RIA-приложение.

Rich Internet Application ( «Насыщенное, «богатое» Интернет-приложение») —


это приложение, доступное через Интернет, насыщенное функциональностью
традиционных настольных приложений, которое предоставляется либо уникальной

552
спецификой браузера, либо через плагин, либо путём «песочницы» (виртуальной
машины).

Как правило, приложение RIA:


 передаёт веб-клиенту необходимую часть пользовательского интерфейса,
оставляя большую часть данных (ресурсы программы, данные и пр.) на сервере;
 запускается в браузере и не требует дополнительной установки ПО;
 запускается локально в среде безопасности, называемой «песочница»
(sandbox).

В настоящее время тремя наиболее распространенными подобными платформами


являются Adobe Flash, Java и Microsoft Silverlight.

Главное что нужно понять, Silverlight это веб-технология, позволяющая


разнообразить возможности веб-приложений. Silverlight в отличие от WPF более
простая в применении и более «массовая» технология.

Поддержка Silverlight реализована для всех популярных Windows-систем


(Windows XP, Vista и 7), а также для большинства веб-браузеров. То есть технология не
узкоспециализированная как, например WPF и достаточно распространена в настоящее
время.

Чтобы больше понять, что за «зверь» этот Silverlight, приведём таблицу


сравнения технических возможностей некоторых RIA:

HTML4, Adobe Native


Возможности HTML5 Silverlight JavaFX
XHTML Flash Client

Многопоточность Да Да Нет Да

Двумерная графика Да Да Да Да

Неофициально
Трёхмерная графика Да Да Да
и небезопасно

Поддержка сокетов Да Да Да Да

Асинхронные HTTP
Да Да Да Да Да
запросы

Синхронные HTTP
Да Да Да Нет
запросы

Модификация HTTP
Да Да Да Частично Да
заголовков запроса

Анализ HTTP заголовков


Да Да Да Нет Да
ответа

Постоянное HTTP/1.1
Нет Да Да Нет Нет
соединение

Хранилище данных Нет Да Да Да Да Нет

553
Поддержка «cookie» Да Да Да Да Да Нет

Таблицы стилей Да Да Да Да Да

Пользовательские
Нет Да Да Да Да
шрифты

Таймеры Да Да Да Да Да

Поддержка аудио Нет Да Да Да Да Нет

Поддержка видео Нет Да Да Да Да

Поддержка доступа к
Да Да Да Да Нет Нет
DOM браузера

Поддержка исполнения
Да Да Да Да Нет Нет
скриптов JavaScript

Поддержка вызова кода


- - Да Да Нет Нет
RIA из JavaScript

Поддержка
динамической Да Да Да Да Да
подгрузки кода

Доступ к файловой
Да Да Частично Да Нет
системе

Браузерная поддержка технологий:

Adobe Native
Браузер HTML5+JavaScript Silverlight JavaFX
Flash Client

Internet Explorer 6 Нет Да Да Да Нет

Internet Explorer 7 Нет Да Да Да Нет

Internet Explorer 8 Нет Да Да Да Нет

Internet Explorer 9 Да Да Да Да Нет

FireFox 5 (Windows) Нет Да Да Да Нет

FireFox 3.6 Нет Moonlight Да Да Нет

FireFox 4 Да Да Да Да Нет

Opera 9 Нет Да Да Да Нет

Opera 10\11 (Windows) Да Да Да Да Нет

Opera 10\11 (Linux) Да Moonlight Да Да Нет

554
Google Chrome (>9.0) Да Да Да Да Да

Safari (>4.0) Да Да Да Да Нет

Seamonkey 2.0 Нет Да Да Да Нет

Symbian Explorer Нет Да Частично Нет Нет

Первая версия Silverlight, выпущенная в мае 2007 года, была далека от


завершения и фактически не поддерживала .NET-код совсем. Вся основная разработка
велась на языке JavaScript, а также присутствовала поддержка небольшого
подмножества языка XAML, использовавшегося для описания интерфейса приложений
для Silverlight.

Рис. 1. 2. Архитектура приложения


Silverlight 1.0

555
Рис. 1. 3. Архитектура приложения Silverlight 2.0

Версией поставляемой «по умолчанию» вместе с Visual Studio 2010 стала


Silverlight 3.

18 марта 2009 года на конференции «MIX09» в Лас-Вегасе Microsoft


продемонстрировала бета-версию. 10 июля 2009 года в ходе мероприятия «See the
Light» Microsoft объявила об официальном выпуске RTM версии, а вскоре была
выпущена и окончательная версия (через девять месяцев после выхода второй).

В новую версию были добавлены новые элементы управления и различные


возможности, а уровень данных был расширен для обеспечения стабильной основы для
сценариев, более ориентированных на бизнес-процессы. В то же время, существующий
уровень медиа был расширен за счёт добавления поддержки новых форматов и
эффектов (более известных как «пиксельные шейдеры»). На уровне пользователя
стало возможным переносить двухмерные элементы в трёхмерное пространство (что
иногда называется «псевдо-3D» или «2,5D»). Анимация продвинулась ещё дальше,
обеспечивая более плавное и приближенное к реальному воспроизведение. Некоторые
шаги были предприняты и в области использования аппаратного ускорения (что само
по себе является настоящей проблемой на смешанных платформах, как например, на
поддерживаемых Silverlight).

Среди особенностей новой версии:


 Поддержка устройств с multitouch-интерфейсом (устройства, распознающие
множественные касания сенсора).
 Поддержка форматов H.264, AAC и MPEG-4.
 Поддержка аппаратного ускорения при работе с трехмерной графикой. Это
позволяет задействовать потенциал графического процессора и снизить
нагрузку на центральный процессор.

В целом Silverlight представляет собой урезанный вариант Windows Presentation


Foundation, не требующий наличия .NET Framework. При этом возможности
приложений Silverlight почти безграничны, весь недостающий функционал можно
реализовать в виде модулей, используя Silverlight 3 SDK.
Также в Silverlight 3 был впервые представлен функционал, получивший
название «вне браузера» («out-of-the-browser») (OOB). Реализация данного
функционала обладала серьезными недостатками: например, OOB-приложения всё ещё
не могли получать какие-либо дополнительные права, что весьма ограничивало их
действия. Также не было возможности настройки окна OOB-приложения.

Silverlight 4 (Silverlight 4 SDK) на данный момент являются последней


поддерживаемой версией Silverlight для Visual Studio 2010. Для установки можно
воспользоваться встроенным в среду разработки средством: Установщик веб-
платформы 3.0. Дополнительные инструменты для версии Silverlight 4, если таковые
не были установлены ранее можно найти на вкладке Продукты -> Средства:
Средства Silverlight 4 для Visual Studio 2010 (Англий) от 17.05.2011. Для версии
будущей «пятой» версии: Silverlight 5 Tools for Visual Studio 2010 (Английский) от
16.12.2011.

Для того, чтобы узнать какая на данный момент в ОС установлена версия


Silverlight можно воспользоваться средством Конфигурация Microsoft Silverlight («по
умолчанию»: Пуск -> Все программы -> Microsoft Silverlight):

556
Рис. 1. 4. Окно «Конфигурация Microsoft Silverlight»

Также это окно можно вызвать, если запустить Silverlight-приложение и нажать


правую кнопку мыши в области этого приложения.

Что будет представлять из себя приложение, разрабатываемое в данной


лабораторной работе?

Рассмотрим самые простые примеры работы с Silverlight-приложением. Разделим


данную работу на три части.
Перед началом работы, рассмотрим процесс создания простейшего приложения
Silverlight и основные особенности работы с данным типом приложений.
Во первой части будет разобрана возможность переключения страницы (для
браузера) с приложением в полноэкранный режим и обратно в оконный. Также,
посмотрим, нажатие каких клавиш клавиатуры (в фокусе страницы) будет
«перехватываться» приложением в этих режимах. В довесок будет определять
реальные размеры окна страницы (приложения на странице браузера).
Во второй части будет создан полноценный проигрыватель MP3-файлов. «Трэки»
будут заранее подготовлены и сохранены в маленькой базе данных. Проигрыватель
будет поддерживать возможность воспроизведения, остановки воспроизведения, паузы
на текущем месте воспроизведения, выключение и включение звука, изменения уровня
громкости и переключение баланса на левый или правый наушник (колонку) (если
звуковое устройство ОС настроено и поддерживает режим воспроизведения
стереозвука).
Третья часть работы будет демонстрировать работу с анимацией движения
объекта в приложении Silverlight. Будет продемонстрировано несколько простых
режимов обработки анимации простых объектов.

2. Создание приложения Silverlight:

557
Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Silverlight. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

558
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP17Silverlight — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

559
Рис. 2. 3. Вводим данные нового проекта приложения Silverlight

После нажатия клавиши ОК. Откроется ещё одно окно, в котором будет
предложено:

Рис. 2. 4. Создать приложение Silverlight: выбор настроек

560
Ввести имя нового веб-проекта на основе приложения (Имя нового веб-
проекта оставляем без изменений: LWP17Silverlight.Web), указать версию Silverlight
(в нашем случае «четвёрка»), а также Включить службы RIA WCF1 (не ставим
галочку).

ПРИМЕЧАНИЕ № 1: Включение в проект Службы RIA WCF упрощает


разработку многоуровневых решений класса RIA (полнофункциональных интернет-
приложений), таких как приложения Silverlight. Типичной проблемой при разработке
многоуровневого решения RIA является согласование логики среднего уровня и уровня
представления. Чтобы обеспечить максимальное взаимодействие с пользователем,
клиент «Службы RIA» должен учитывать логику приложения, которое находится на
сервере. Однако было бы нежелательным разрабатывать и поддерживать логику
приложения, как на уровне представления, так и на среднем уровне. «Службы RIA»
решает эту проблему, поскольку в составе платформы имеются компоненты,
инструменты и службы, обеспечивающие доступ клиента «Службы RIA» к логике
приложения на сервере без необходимости вручную дублировать эту программную
логику. Можно создать клиент «Службы RIA», учитывающий бизнес-правила, и при
каждой компиляции решения эта логика будет автоматически обновляться на клиенте.
На рисунке ниже показана упрощённая версия многоуровневого приложения.
«Службы RIA» посвящён в основном решению задач в прямоугольнике между уровнем
представления и уровнем доступа к данным (DAL) и позволяет упростить разработку
многоуровневого решения с клиентом «Службы RIA»:

Рис. 2. 5. Пример упрощённой структуры многоуровневого приложения

«Службы RIA» добавляет в среду Visual Studio 2010 инструменты (в частности


Ссылки), которые позволяют связать клиентский и серверный проекты в единое
решение и формировать код для клиентского проекта из кода среднего уровня.
Компоненты платформы поддерживают предписывающие шаблоны при написании
логики приложения, что позволяет повторно использовать ее на уровне представления.
Доступны также такие службы для типовых сценариев, как проверка подлинности и
управление параметрами пользователя, что позволяет сократить время разработки.
В «Службы RIA» можно предоставлять данные из серверного проекта
клиентскому проекту путем добавления служб домена. Платформа «Службы RIA»
реализует каждую службу домена в виде службы Windows Communication
Foundation (WCF).

Текст примечания выше является выдержкой из статьи Службы WCF RIA


(http://msdn.microsoft.com/ru-ru/library/ee707344(v=vs.91).aspx).

561
После нажатия кнопки ОК (рисунок 2. 4), среда разработки сформирует два (а
не один как раньше) проекта в одном решении, каждый со своим исходных кодом и
дополнительными файлами.

Рис. 2. 6. Обозреватель решений: состав проектов приложения Silverlight


сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально). Перед компиляцией обратим вниманием на
директорию ClientBin веб-проекта. Директория пока что пуста.

Рис. 2. 7. Запуск приложения Silvelight по конфигурации Debug

Как видим, проект пока что пуст. Единственное что можно сделать, это вызвать
окно конфигурации Silverlight:

562
Обратим также внимание на имя страницы в окне браузера и на
соответствующие имена в обозревателе решений веб-проекта. Обе страницы
(LWP17SilverlightTestPage.aspx и LWP17SilverlightTestPage.html) идентичны и
подцепляют «яваскрипт»-код Silvelight.js и главную страницу первого проекта в
составе решения (WPF-приложения). Код обеих веб-страниц автоматически
редактируется после каждой компиляции. Привязка объекта WPF-приложения к
страницах осуществляется следующим образом (автоматически, через обычный HTML-
код):

<form id="form1" runat="server" style="height:100%">


<div id="silverlightControlHost">
<object data="data:application/x-silverlight-2," type="application/x-silverlight-
2" width="100%" height="100%">
<param name="source" value="ClientBin/LWP17Silverlight.xap"/>
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="4.0.50826.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.50826.0"
style="text-decoration:none">
<img src="http://go.microsoft.com/fwlink/?LinkId=161376"
alt="Получить Microsoft Silverlight" style="border-style:none"/>
</a>
</object><iframe id="_sl_historyFrame"
style="visibility:hidden;height:0px;width:0px;border:0px"></iframe></div>
</form>

Ключевая строчка запускаемого объекта выглядит так:

<param name="source" value="ClientBin/LWP17Silverlight.xap"/>

Сам объект, на которые ссылается код, генерируется после первой компиляции и


помещается в директорию ClientBin:

В случае, если бы в окне на рисунке 2. 4. сняли галочку с пункта: Разместить


приложение Silverlight на новом веб-сайте, веб-проект бы не создавался, и
отладка и запуск производилась бы на динамически-создаваемых веб-страницах в
директории LWP17Silverlight\Bin\Debug или LWP17Silverlight\Bin\Release
(LWP17SilverlightTestPage.html).

В любом случае, для отладки и запуска среда разработки запускает ASP.NET


Development Server и «бросает» исполняемые страницы на локальный адрес и порт
(например, в нашем случае http://localhost:53510). Получить доступ к окну
сведений можно через иконку в «трее» панели «Пуск» (кнопка «Подробные
сведения»). Окно сведений выглядит так:

563
Рис. 2. 8. Запуск приложения Silverlight по конфигурации Debug: просмотр сведений о
сервере и локальном адресе страницы

Почему же понадобилось создавать новый веб-проект? Об этом будет упомянуто


немного позже.

3. Модификация приложения Silverlight: первые шаги и полноэкранный режим

Первым делом рассмотрим начальные возможности Silverlight. Как уже стало


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

Основное отличие данного WPF и «чистого» WPF-приложения состоит в


изменённом и расширенном коде файла App.xaml.cs для главного файла, а также
наличие файла AppManifest.xml в папке Properties:

Двойным нажатием открываем Конструктор для главной редактируемой


страницы проекта MainPage.xaml. Код XAML должен быть изначально таким:

<UserControl x:Class="LWP17Silverlight.MainPage"
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" d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White"></Grid>


</UserControl>
Ищем:

mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">

Данный код означает, что размеры страницы свободные и не фиксированные для


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

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

Либо впишем:

mc:Ignorable="d" Height="300" Width="400">

Итак, поменяем эту строчку следующим кодом (изменим размеры для дизайна):

mc:Ignorable="d" d:DesignHeight="480" d:DesignWidth="640">

Теперь инициализируем событие нажатия любой клавиши в фокусе страницы.


Для этого на панель Свойства и перейдём в вкладку События. Двойным нажатием по
событию KeyDown инициализируем метод UserControl_KeyDown (где UserControl —
имя формы):

Код XAML:

mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"


KeyDown="UserControl_KeyDown">

Отредактируем сетку Grid. Для этого найдём в XAML-коде конструктора код:

<Grid x:Name="LayoutRoot" Background="White">

Мышкой жмём на слово «Grid». На панели свойство ищем свойство Background


(группа свойств Кисти):

565
Вписываем Azure либо выбираем понравившийся цвет. Далее редактируем
свойство RowDefinitions, жмём «...»:

В окне Редактор коллекции: RowDefinition добавляем пять элементов


RowDefinition, свойство Height первого и последнего ставим как «2*»:

Рис. 3. 1. Добавляем разделители на строки для элемента сетки

Также поступаем для столбцов. Открываем свойство для элемента Grid:


ColumnDefinitons. Добавляем два новых ColumnDefinition в редакторе коллекции.
Свойство Width второго вписываем как «2*». Итоговый элемент Grid должен выглядеть
так:

566
Рис. 3. 2. Завершённый элемент Grid, разделённый на строки и столбцы

В первую клетку сетки (строка: 0, столбец: 0) вставляем в панели элементов

простой TextBlock ( ):

Выделим XAML-код добавленного элемента и заменим его следующим кодом:

<TextBlock HorizontalAlignment="Stretch" Name="textBlock1"


VerticalAlignment="Stretch" TextAlignment="Center">
Нажмите кнопку "Полноэкранный режим" для перехода в полноэкранный режим.
<LineBreak/>
Нажмите кнопку "Обычный режим" для перехода в режим окна браузера.
<LineBreak/>
<LineBreak/>
В обычном режиме опробуйте нажатие клавиш на клавиатуре в фокусе окна
браузера.
<LineBreak/>
Нажатая клавиша будет отображена на на странице.
<LineBreak/>
<LineBreak/>

567
В полноэкранном режим будут работать только следующие клавиши:
<LineBreak/>
Стрелки клавиатуры, пробел, Tab, Page Up, Page down, Home, End и
Enter.</TextBlock>

Данный помещает текст не внутрь свойства Text, а в границы открывающего и


закрывающего тэгов <TextBlock ...>Текст</TextBlock>. Размещённый текст
располагается по центру в элементе. Сам элемент растягивается до границ ячейки
сетки.

В ячейку ниже [1, 0] добавим кнопку (Button), и сразу же замени код кнопки
следующим XAML-кодом:

<Button Content="Полноэкранный режим" Grid.Row="1" Grid.Column="0"


x:Name="button1" Height="23" VerticalAlignment="Center" HorizontalAlignment="Center"
Width="150" Click="button1_Click" IsTabStop="False" />

Замену в этом случае можно не проводить. Свойства Grid.Row «1» и


Grid.Column «0» определяются в момент размещения элемента внутри сетки.
Остальные свойства кнопки: Width установлено в «150», а в свойствах
VerticalAligment и HorizontalAligment выбирано Center. IsTabStop: галочка снята.

В ячейки [2, 0] и [3, 0] вставляем ещё два TextBlock со следующим XAML-кодом:

<TextBlock x:Name="textBlock2" Grid.Row="2" Grid.Column="0"


HorizontalAlignment="Stretch" VerticalAlignment="Stretch" TextAlignment="Center"
FontSize="26" />
<TextBlock x:Name="textBlock3" Grid.Row="3" Grid.Column="0"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" TextAlignment="Center"
FontSize="36" />

Двойным нажатием по единственной кнопке в конструкторе инициализируем


событие Click. Код такой:

private void button1_Click(object sender, RoutedEventArgs e)


{
if (!App.Current.Host.Content.IsFullScreen)
button1.Content = "Оконный режим";
else button1.Content = "Полноэкранный режим";
// Переключение в полноэкранный или оконный режимы
App.Current.Host.Content.IsFullScreen = !
App.Current.Host.Content.IsFullScreen;
}

Код события нажатия клавиши KeyDown меняем так:

private void UserControl_KeyDown(object sender, KeyEventArgs e)


{
// Перехватываем нажатие клавиши в textBlock3
this.textBlock3.Text = "Нажата кнопка: " + e.Key.ToString();
}

В коде файла MainPage.xaml.cs находим:

public MainPage()
{
InitializeComponent();
}

568
Изменяем на:

public MainPage()
{
InitializeComponent();
// Подключаем событие Loaded чтобы подключить события на этапе загрузки
страницы
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}

void MainPage_Loaded(object sender, RoutedEventArgs e)


{
// Подписываемся на события из SilverlightHost и вызываем соответствующий
метод при срабатывании
App.Current.Host.Content.FullScreenChanged += new
EventHandler(Content_FullScreenChanged); // Событие состояния страницы:
окнонный/полноэкранный
App.Current.Host.Content.Resized += new EventHandler(Content_Resized); //
Событие изменения размеров окна
}

void Content_Resized(object sender, EventArgs e)


{
// Когда изменяются размеры страницы, обновляем данные об этом
// вызывом функцию вывода размеров Silverlight плагина
RefreshTextBlockShowSize();
}

void Content_FullScreenChanged(object sender, EventArgs e)


{
// Когда изменяются состояния страницы (оконный/полноэкранный), обновляем
данные об этом
// вызывом функцию вывода размеров Silverlight плагина
RefreshTextBlockShowSize();
}

private void RefreshTextBlockShowSize()


{
// Показывает размеры плагина Silverlight plug-in в textBlock2
this.textBlock2.Text = string.Format("Размер страницы: {0}*{1}",
App.Current.Host.Content.ActualWidth,
App.Current.Host.Content.ActualHeight);
}

Компилируем, проверяем работоспособность. Следуя инструкции, написанной на


странице, выполняем различные действия, и наблюдает за результатом.

569
Рис. 3. 3. Результат работы приложения Silverlight: нажата клавиша «пробел»

Рис. 3. 4. Результат работы приложения Silverlight: включен полноэкранный режим


страницы

4. Модификация приложения Silverlight: простой проигрыватель MP3-файлов

570
Простой проигрыватель MP3-файлов. Для начала добавим два простеньких
класса. Один класс будет отвечать за свойства MP3-файла: имя и путь. Добавим класс
для проекта WPF-приложения: выделим имя проекта (LWP17Silverlight) и выполним:
Проект -> Добавить класс... (Shift+Alt+C): в открывшемся окне в поле Имя
указываем DataItem.cs. Код файла будет таким:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace LWP17Silverlight
{
public class DataItem
{
private string nameItem;
private string pathItem;

public string NameItem


{
get { return nameItem; }
set { nameItem = value; }
}

public string PathItem


{
get { return pathItem; }
set { pathItem = value; }
}
}
}

Второй класс будет содержать всего две функции, которые будут работать со
временем воспроизведения файла. Класс назовём ProgressConverter (файл
ProgressConverter.cs) с кодом:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Data;

namespace LWP17Silverlight
{
public class ProgressConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return ((TimeSpan)value).TotalSeconds;

571
}

public object ConvertBack(object value, Type targetType, object parameter,


System.Globalization.CultureInfo culture)
{
return TimeSpan.FromSeconds((double)value);
}
}
}

В файле MainPage.xaml в коде XAML найдём:

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

Добавим после:

xmlns:lwp17="clr-namespace:LWP17Silverlight"

Найдём:

mc:Ignorable="d" d:DesignHeight="480" d:DesignWidth="640"


KeyDown="UserControl_KeyDown">

Добавим после:

<UserControl.Resources>
<lwp17:ProgressConverter x:Key="progress"></lwp17:ProgressConverter>
</UserControl.Resources>

Основным элементом, реализующим работу непосредственно «начинки»


проигрывателя станет MediaElement (Панель элементов:

). Добавляем его на страницу в ячейку [0, 1]


(строка: 0, столбец: 1). Код элемента будет таким:

<MediaElement x:Name="mediaElement" Grid.Column="1" Height="23"


HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Width="Auto" AutoPlay="True"
IsMuted="False" Stretch=”Fill” />

Установленные свойства:

XAML-код имени элемента: x:Name=”mediaElement”


AutoPlay: True
Stretch: Fill
HorizontalAligment: Stretch
VetricalAligment: Bottom
Height: 23
Width: Auto
IsMuted: Нет галочки

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


всех доступных композиций для прослушивания. Список мы поместим в DataGrid и
будем выводить в нём композиции по имени, записанному в специальном XML-файле
нашего «сайта».

572
Перетягиваем с панели элементов в самую правую внизу страницы [4, 1]

DataGrid ( ). Код, основанный на изменении


свойств элемента:

<sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" Grid.Column="1"


Grid.Row="4" Height="Auto" HorizontalAlignment="Stretch" Name="songList"
VerticalAlignment="Stretch" Width="Auto" DataContext="{Binding}" />

Не забудем привязать данные к свойствам DataContext и ItemSource:

Для этого выберем любое из свойств, нажмём на Привязка..., далее в пункте


Источник дважды щёлкнем по DataContext.

Далее редактируем свойство Columns (жмём «...»):

Рис. 4. 1. Редактор коллекции: Columns

573
В списке Выбрать элемент выбираем DataGridTextColumn, жмём дважды
добавить. Свойства первого столбца такие:

XAML-код поля привязки: Binding=”{Binding NameItem}”


ACanUserSort: Нет галочки
Header: Имя композиции
IsReadOnly: Галочка
Width: Auto

Для второго столбца:

XAML-код поля привязки: Binding=”{Binding PathItem}”


ACanUserSort: Нет галочки
Header: Путь к композиции
IsReadOnly: Галочка
Width: Star

Итоговый XAML-код:

<sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" Grid.Column="1"


Grid.Row="4" Height="Auto" HorizontalAlignment="Stretch" Name="songList"
VerticalAlignment="Stretch" Width="Auto" DataContext="{Binding}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Binding="{Binding NameItem}"
CanUserReorder="True" CanUserResize="True" CanUserSort="False" Header="Имя композиции"
IsReadOnly="True" Width="Auto" />
<sdk:DataGridTextColumn Binding="{Binding PathItem}"
CanUserReorder="True" CanUserResize="True" CanUserSort="False" Header="Путь к композиции"
IsReadOnly="True" Width="*" />
</sdk:DataGrid.Columns>
</sdk:DataGrid>

Дальше последовательно размещаем остальные элементы проигрывателя.


Выглядеть это будет следующим образом:

574
Рис. 4. 2. Расстановка элементов на странице MainPage.xaml проекта WPF приложения
Silverlight

Первым элемент будет являться шапкой нашего MP3-проигрывателя с простым


текстовым блоком. Расположим его в [0, 1] ячейке сетки. Код TextBlock будет таким:

<TextBlock FontSize="20" Height="34" Name="tbTitle" Text="Простой проигрыватель


MP3-файлов" VerticalAlignment="Center" Width="Auto" Grid.Column="1"
HorizontalAlignment="Center" />

Чуть ниже в ячейке [1, 1] расположим элемент, выполняющий функции


временной дорожки композиции (расположение ползунка будет отвечать за положение
на шкале воспроизведения файла), а также два TextBlock. Первый будет статическими,
второй же будет отражать текущий статус воспроизведений:

<TextBlock Grid.Row="1" Grid.Column="1" Height="23" HorizontalAlignment="Center"


Name="textBlock4" Text="Воспроизведение:" VerticalAlignment="Top" Width="106" />
<TextBlock Grid.Row="1" Grid.Column="1" Height="23" HorizontalAlignment="Center"
Name="textBlock5" Text="Статус:" VerticalAlignment="Bottom" Width="Auto" />

Временная дорожка: Slider ( ). Привязку


(свойство Value для элемента Slider) данных к ползунку (Position) осуществим через
класс ProgressConvetrter на основе данных элемента medaiElement:

575
XAML-код элемента будет таким:

<Slider x:Name="sliderProcess" Grid.Row="1" Grid.Column="1" Minimum="0"


Maximum="230" Value="{Binding ElementName=mediaElement, Path=Position, Mode=TwoWay,
Converter={StaticResource progress}}" IsEnabled="False" />

В ячейку [2, 1] вставим кнопки проигрывателя и два дополнительных слайдера.


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

Кнопка «Стоп»:

<Button x:Name="buttonStop" Content="Стоп" Grid.Row="2" Grid.Column="1"


Width="70" Height="23" HorizontalAlignment="Left" VerticalAlignment="Center"></Button>

Кнопка «Воспроизвести:

<Button x:Name="buttonPlay" Content="Воспроизвести" Grid.Row="2" Grid.Column="1"


Width="100" Height="23" HorizontalAlignment="Left" Margin="70,23,0,23"
VerticalAlignment="Center"></Button>

Кнопка «Пауза»:

<Button x:Name="buttonPause" Content="Пауза" Grid.Row="2" Grid.Column="1"


Width="70" Height="23" HorizontalAlignment="Left" Margin="170,23,0,23"
VerticalAlignment="Center"></Button>

Кнопка «Заглушить»:

<Button x:Name="buttonMuted" Content="Заглушить" Grid.Row="2" Grid.Column="1"


Width="70" Height="23" HorizontalAlignment="Left" Margin="240,23,0,23"
VerticalAlignment="Center"></Button>

TextBlock и Slider отвечающий за громкость:

576
<Slider LargeChange="0.1" Maximum="1" SmallChange="0.01" Value="{Binding Volume,
ElementName=mediaElement, Mode=TwoWay, UpdateSourceTrigger=Default}" Grid.Column="1"
Grid.Row="2" Height="25" VerticalAlignment="Top" HorizontalAlignment="Right"
Width="94" />
<TextBlock Height="23" HorizontalAlignment="Right" Margin="0,0,98,0"
Name="textBox6" Text="Громкость:" VerticalAlignment="Top" Grid.Column="1" Grid.Row="2" />

TextBlock и Slider отвечающий за баланс:

<Slider LargeChange="0.1" Maximum="1" Minimum="-1" Name="sliderBalance"


Value="{Binding Path=Balance, ElementName=mediaElement, Mode=TwoWay,
UpdateSourceTrigger=Default}" Grid.Column="1" Grid.Row="2" HorizontalAlignment="Right"
Width="94" Height="25" VerticalAlignment="Bottom" />
<TextBlock Height="23" HorizontalAlignment="Right" Margin="0,0,116,0"
Name="textBox7" Text="Баланс:" VerticalAlignment="Bottom" Grid.Column="1" Grid.Row="2" />

В ячейку [3, 1] вставляем единственный TextBlock:

<TextBlock FontSize="12" Height="23" HorizontalAlignment="Center"


Name="textBlock8" Text="Список доступных MP3-файлов:" VerticalAlignment="Center"
Width="Auto" Grid.Column="1" Grid.Row="3" />

Перед тем, как работать с событиями элементов и кодом приложения, добавим


необходимые ресурсы «сайта» (веб-проекта). Создаём последовательно две папки
(Resource -> MP3). Для создания папки выделяем имя веб-проекта, далее выполняем
Проект -> Создать папку:

Собственно веб-проект и импорт ресурсов MP3-файлов в конечную папку


проекта необходим для «эмуляции» работы приложения Silverlight с неким сайтом. В
конечном итоге, страница с Silverlight-приложением получает удалённо сами файлы
(путь, имя файла и содержимое).

В конечную папку импортируем несколько композиций, выполняем: Проект ->


Добавить существующий элемент... (Shift+Alt+A). Пусть в этой папке будут
храниться несколько MP3-файлов:

Теперь в папке Resource создаём XML-файл с именем MusicList.xml (Проект ->


Добавить новый элемент... (Ctrl+Shift+A), следующего содержания:

<?xml version="1.0" encoding="utf-8" ?>


<root>
<music open="1" path="Resource/MЗ3/Atrium_Sun_-_Abyss_(Original_Mix).mp3" name="Abyss
(Original Mix)">Atrium Sun</music>

577
<music open="1" path="Resource/MP3/Daniel_Barbosa_-
_Asian_Gardens_(Feat._Shen_Shen).mp3" name="Asian Gardens (Feat. Shen Shen)">Daniel
Barbosa</music>
<music open="1" path="Resource/MP3/Elena_-_Zombie_(Ambient_Vocal_Edit).mp3"
name="Zombie (Ambient Vocal Edit)">Elena</music>
</root>

Атрибуты для элемента music:


path: содержит полный путь до файла с указанием имени файла и расширения;
name: содержит отображаемое в списке имя (для страницы);
значение: содержит автора композиции.

Перейдём к функциональности кода главной страницы MainPage.xaml.cs: в


начало файла добавим следующий код:

// Для MP3-проигрывателя
using System.Xml;
using System.IO;
using System.Windows.Threading;
using System.Windows.Browser;

Найдём:

public MainPage()
{

Добавим до:

public List<DataItem> DataItems;


public bool boolIsMuted = false;
private TimeSpan timeDuration; // Интервал времени

Найдём:

// Подключаем событие Loaded чтобы подключить события на этапе загрузки


страницы
this.Loaded += new RoutedEventHandler(MainPage_Loaded);

Добавим после:

this.XmlProcessMethod();

Найдём:

// Переключение в полноэкранный или оконный режимы


App.Current.Host.Content.IsFullScreen = !
App.Current.Host.Content.IsFullScreen;
}

Добавим после:

/// <summary>
/// Функция использует асинхронную загрузку данных из XML-файла
/// и подписывается на событие для объекта WebClient DownLoadXmlComplete
/// и событие MeidaEnded объекта MediaElement
/// </summary>
private void XmlProcessMethod()
{

578
WebClient webClient = new WebClient();
webClient.DownloadStringAsync(new Uri(HtmlPage.Document.DocumentUri,
"Resource/MusicList.xml"));
webClient.DownloadStringCompleted += new
DownloadStringCompletedEventHandler(this.DownLoadXmlComplete);
mediaElement.MediaEnded += new RoutedEventHandler(mediaElement_MediaEnded);
}

/// <summary>
/// Загрузка музыки согласно данные XML-файла и конвертирование данных в список.
/// Привязка свойства ItemSource элемента DataGrid к данному списку.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DownLoadXmlComplete(object sender, DownloadStringCompletedEventArgs
e)
{
using (XmlReader reader = XmlReader.Create(new StringReader(e.Result)))
{
DataItems = new List<DataItem>();

while (reader.Read())
{

if (reader.IsStartElement() && reader.GetAttribute("open") == "1")


{
string pathMusic = reader.GetAttribute("path").ToString();
string nameMusic = reader.GetAttribute("name").ToString();
DataItem dataItem = new DataItem();
dataItem.NameItem = nameMusic;
dataItem.PathItem = pathMusic;
DataItems.Add(dataItem);
}
}
this.DataContext = DataItems;
}
}

Инициализируем события. Событие SelectionChanged элемента DataGrid:

private void songList_SelectionChanged(object sender, SelectionChangedEventArgs


e)
{
sliderProcess.IsEnabled = true;
mediaElement.Position = new TimeSpan(0); // Обнуляем слайдер воспроизведения
DataItem selectItem = (DataItem)songList.SelectedItem; // Получаем выбранный
в списке элемент
mediaElement.Source = new Uri(HtmlPage.Document.DocumentUri,
selectItem.PathItem); // Меняем источник
mediaElement.Play(); // Запускаем композицию
textBlock4.Text = "Воспроизведение: " + selectItem.NameItem.ToString();
}

Событие MediaEnded элемента MediaElement:

private void mediaElement_MediaEnded(object sender, RoutedEventArgs e)


{
textBlock5.Text = "Статус: Воспроизведение завершено";
mediaElement.Pause();
mediaElement.Position = TimeSpan.Zero;
}

Событие MediaOpened элемента MediaElement:

579
private void mediaElement_MediaOpened(object sender, RoutedEventArgs e)
{
textBlock5.Text = "Статус: Воспроизведение начато";
// Получаем общее время композиции
timeDuration = mediaElement.NaturalDuration.HasTimeSpan ?
mediaElement.NaturalDuration.TimeSpan : TimeSpan.FromMilliseconds(0);
sliderProcess.Maximum = timeDuration.TotalSeconds;
}

Событие CurrenyStateChanged элемента MediaElement:

private void mediaElement_CurrentStateChanged(object sender, RoutedEventArgs e)


{
textBlock5.Text = "Статус: " + mediaElement.CurrentState.ToString();
if (mediaElement.CurrentState != MediaElementState.Playing
&& mediaElement.CurrentState != MediaElementState.Paused)
{ this.sliderProcess.IsEnabled = false; }
else { this.sliderProcess.IsEnabled = true; }
}

События Click элемента всех кнопок поочерёдно:

private void buttonStop_Click(object sender, RoutedEventArgs e)


{
mediaElement.Position = TimeSpan.Zero;
mediaElement.Stop();
}

private void buttonPlay_Click(object sender, RoutedEventArgs e)


{
mediaElement.Play();
}

private void buttonPause_Click(object sender, RoutedEventArgs e)


{
mediaElement.Pause();
}

private void buttonMuted_Click(object sender, RoutedEventArgs e)


{

if (!boolIsMuted)
{
textBlock5.Text = "Статус: Звук выключен";
buttonMuted.Content = "Включить";
mediaElement.IsMuted = true;
boolIsMuted = true;
}
else
{
textBlock5.Text = "Статус: Звук включен";
buttonMuted.Content = "Заглушить";
mediaElement.IsMuted = false;
boolIsMuted = false;
}
}

Компилируем, проверяем работоспособность. Запускаем страницу, выбираем


любой MP3-файл в списке доступных файлов, слушаем (если конечно есть на чём
слушать ), перемещаем ползунок по композиции, жмём на кнопку «Пауза»,

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

Рис. 4. 3. Результат работы приложения Silverlight: воспроизведение выбранной


композиции

5. Модификация приложения Silverlight: работа с анимацией

Для работы с анимацией нужен объект анимации и объект, который будет


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

<Storyboard>Дочерние анимации</Storyboard>

Storyboard — класс, шкала времени контейнера, которая предоставляет объект


и свойство, содержащие сведения о дочерних анимациях.
Большая часть свойств шкалы времени могут иметь привязку данных или могут
быть анимированы; тем не менее, в связи с особенностями действия системы расчета
времени поведение привязанных к данным и анимированных шкал времени отличается
от поведения других привязанных к данным или анимированных объектов. Чтобы
понять их поведение, следует осознать, что означает активирование временной шкалы.
При активировании временной шкалы создаются копии временной шкалы и дочерних
временных шкал. Эти копии фиксируются (устанавливаются в режим "только для
чтения"), а на их базе создаются объекты Clock. Эти объекты используются для
выполнения фактических действий по анимации целевых свойств. Если шкала времени
является привязанной к данный или анимированной, при создании часов создается
моментальный снимок ее текущих значений. Хотя исходная временная шкала может
продолжить изменение, ее часы не будут меняться.
Для отражения в шкале времени привязки данных или изменений анимации
необходимо повторно создать ее часы. Часы не воссоздаются автоматически. Ниже
приведены несколько способов применения изменений шкалы времени:
 Если шкала времени принадлежит Storyboard, можно заставить её отражать
изменения посредством применения её раскадровки при помощи метода

581
BeginStoryboard или Begin. Это имеет побочный эффект в виде перезапуска
анимации. В коде можно использовать метод Seek для возврата раскадровки
обратно в предыдущее положение.
 Если анимация применяется непосредственно к свойству с помощью метода
BeginAnimation, снова вызываем метод BeginAnimation и передаём ему
измененную анимацию.
 Если ведется работа непосредственно на уровне часов, создаём и применяем
новый набор часов и используем их для замены предыдущего набора созданных
часов.

Текст выше является выдержкой из статьи Storyboard - класс


(http://msdn.microsoft.com/ru-ru/library/system.windows.media.animation.stor
yboard.aspx).

Подготовим «площадку» для реализации возможностей анимации. Откроем


конструктор главной страницы MainPage.xaml и установим в единственную незанятую
ячейку элемента Grid (строка: 4, столбец: 0) элемент TabControl (
) со следующими свойствами, уже
определёнными XAML-кодом:

<sdk:TabControl Grid.Row="4" HorizontalAlignment="Stretch" Name="tabControl1"


VerticalAlignment="Stretch">
...
</sdk:TabControl>

Элемент TabControl будет растянут по всей ячейки элемента Grid. Что же


касается кода на месте троеточия: там будет располагаться код определяющий
закладки TabItem, но каждая закладка будет являться полноценной страницей XAML.
Для первой закладки создаём эту страницу. Выделим имя WPF-проекта, далее
выполним Проект -> Добавить новый элемент...: в открывшемся окне в списке
«Установленные шаблоны» выберем «Silverlight», далее справа найдём «Страница
Silverlight»:

582
Рис. 5. 1. Добавление нового элемента – LWP17Silverlight: Страница Silverlight

Имя вводим как Page_BasicAnimation.xaml (приставка Page нужна для


группировки страниц в обозревателе решений). Жмём ОК. Страница добавлена.
Откроем её в конструкторе и в коде XAML найдём строчки:

d:DesignWidth="640" d:DesignHeight="480"
Title="Page1 Page">

Изменим эти строчки так (равносильно изменению свойств, как уже было
сказано):

d:DesignWidth="300" d:DesignHeight="300"
Title="Знакомство с Silverlight (C#) :: Основы анимации">

Теперь «запихнём» новую страницу в TabControl следующим образом. Откроем


MainPage.xaml в конструкторе, найдём код добавления элемента TabControl и между
тэгами добавим следующий XAML-код:

<lwp17:Page_BasicAnimation></lwp17:Page_BasicAnimation>

Напоминаем, слово lwp17 это доступ к пространству имён приложения (WPF-


проекта) определённый в шапке страницы MainPage.xaml:

xmlns:lwp17="clr-namespace:LWP17Silverlight"

Откроем XAML-код страницы Page_BasicAnimation.xaml, найдём:

<Grid x:Name="LayoutRoot">
...
</Grid>

За место троеточия вставим:

583
<Rectangle x:Name="rectangle1" Fill="Turquoise"
Canvas.Top="100" Canvas.Left="100"
Width="100" Height="100">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation RepeatBehavior="3x"
Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="Height"
To="200" Duration="0:0:5"
AutoReverse="True" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

Откомпилируем и увидим как ровный квадрат превращается в следующее:

Теперь немного теории:

BeginStoryboard («Начать раскадровку») — это действие триггера, содержащее


объект Storyboard («Раскадровка»). Объекты раскадровки содержат определения
анимации. При определении анимации эти объекты просто внедряются внутрь
определения EventTrigger:

<Rectangle x:Name="rect" Fill="Red"


Canvas.Top="100" Canvas.Left="100"
Width="100" Height="100">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

Теперь, когда инфраструктура для анимации установлена, можно указать, какую


анимацию следует выполнить. На самом базовом уровне, анимация определяет
изменение свойства с течением времени. Можно анимировать три различных типа
свойств. Каждый из этих типов свойств анимируется из значения, указанного в
атрибуте From («От») (или, если оно не установлено, из его текущего значения), к

584
значению, указанному в атрибуте To («К»), либо к значению, указанному в атрибуте
By («По»).
Двойные типы анимируются с помощью DoubleAnimation или
DoubleAnimationUsingKeyFrames. Этот метод используется для анимации свойств,
содержащих двойное значение — например, измерений, вроде Canvas.Left или
визуальных атрибутов, вроде Opacity.
Типы точек анимируются с помощью PointAnimation или типа
PointAnimationUsingKeyFrames. Этот конкретный метод используется для анимации
свойств, содержащих значение точки, таких, как сегментов строк или кривых,
определенных с использованием точек.
Типы цветов анимируются с помощью ColorAnimation или типа
ColorAnimationUsingKeyFrames. Этот метод используется для анимации свойств,
содержащих значение цвета — фона или штриха элемента, например.

Чтобы определить, к какому объекту следует применить анимацию, на этих


типах анимации используется свойство Storyboard.TargetName и ему необходимо
передать имя такого объекта, устанавливаемое на объекте с помощью свойства
x:Name. Вдобавок, свойство, которое будет анимировано, указывается с помощью
Storyboard.TargetProperty. Учтите, что при указании сложного или присоединенного
свойства (такого как Canvas.Left), его следует поместить в скобки. Так, для примера,
чтобы нацелить двойную анимацию на Height прямоугольника, именуемого
rectangle1, XAML должен выглядеть следующим образом:

<DoubleAnimation Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="Height" />

Чтобы определить, сколько времени займёт переход затрагиваемых свойств от


одного значения к другому, используется свойство Duration («Продолжительность»).
Отметьте, что оно определено в формате чч:мм:сс, где пятисекундная
продолжительность анимации указывается как 00:00:05, сокращенно 0:0:5.

<DoubleAnimation Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="Height" Duration="0:0:5" />

Если нежелательно, чтобы анимация началась немедленно, можно вставить


задержку, используя свойство BeginTime и тот же синтаксис:

<DoubleAnimation Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="Height" BeginTime="0:0:5" />

Можно также скорректировать поведение анимации, умножив


продолжительность на коэффициент скорости. Это проделывается с помощью свойства
SpeedRatio. Например, в предыдущем случае продолжительность была установлена на
5 секунд. Можно изменить коэффициент скорости, заставив анимацию длиться 10
секунд, установив SpeedRatio на 2, или, как вариант, можно ускорить анимацию до 1
секунды, установив SpeedRatio на 0.2.

<DoubleAnimation Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="Height" SpeedRatio="2" Duration="0:0:5" />

Анимация Silverlight предоставляет средство для отмены изменений, внесённых


как часть анимации. Например, если двойное значение переносится от 0 к 500 за
определенный промежуток времени, AutoReverse заставит анимацию перейти от 500
обратно к нулю.
Если анимация установлена на работу в течении 5 секунд, как показано выше и
AutoReverse установлен на true, то полная анимация займёт 10 секунд.

<DoubleAnimation Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="Height" SpeedRatio="2" Duration="0:0:5" AutoReverse="True"
/>

585
Когда работа анимации завершена, можно применить ряд параметров, чтобы
заставить её вести себя нужным образом. Они указываются с помощью свойства
RepeatBehavior. Это свойство может принимать три различных типа значений:
 Время, определённое в секундах. Временная шкала подождет это время и затем
начнёт анимацию снова.
 Установка RepeatBehavior на Forever («Постоянно») для постоянного
повторения.
 Определенное число повторений, установленное путем указанием числа, за
которым следует x. Например, если анимация должна произойти трижды,
указывается значение 3х.

Анимация значения с помощью DoubleAnimation:

Объект DoubleAnimation позволяет указать, как будет меняться двойное


значение на указанной временной шкале. Анимация вычисляется как линейная
интерполяция между значениями свойств с течением времени.
При анимации двойного значения, значение в начале анимации указывается с
помощью значения From и затем изменяется либо на значение To, являющееся
абсолютной точкой назначения, либо на значение By, являющееся относительной
точкой назначения. Например, если свойство Canvas.Left элемента перемещается от
100 (рядом с левой стороной экрана) к 500, можно установить From на 100 и To на 500,
либо By на 400. Если установить оба, свойство To будет иметь больший приоритет, а
свойство By игнорируется. Также, если прямоугольник уже размещён в желаемой
позиции From, указывать свойство From не нужно.

Анимация цвета с помощью ColorAnimation:

ColorAnimation работает как DoubleAnimation. Оно используется для определения


того, как значение цвета элемента будет меняться со временем. Анимация, вычисляется
как линейная интерполяция между значениями свойств цвета с течением указанного
времени.
При анимации цвета, его значение в начале анимации указывается с
использованием свойства From. Если не указать его, то используется текущий цвет.
Желательный конечный цвет указывается с использованием атрибута To. Можно также
указать атрибут By, который предоставит конечный цвет, являющийся продуктом
добавления значений цвета From (или начального цвета) к цвету By.
При анимации свойства, основанного на цвете, содержимое свойства не
анимируется напрямую, поскольку содержимым свойства обычно является кисть, а не
цвет. Так что если необходимо анимировать цвет заливки прямоугольника, в качестве
цели не следует использовать свойство Fill («Заливка»). Вместо этого, следует указать,
что собираетесь анимировать свойство Color («Цвет») используемой для выполнения
заливки SolidBrush.
Пример анимации цвета прямоугольника и изменения его с чёрного на белый в
течении пяти секунд (затем возвращение и вечное повторение), с помощью анимации
света. Как можно заметить в коде ниже, этот фрагмент XAML указывает свойство Color,
относящееся к заливающей фигуру SolidColorBrush, как свое целевое свойство.
Добавим этот кусок кода на страницу Page_BasicAnimation.xaml после предыдущего
фрагмента, закрывающего тэга прямоугольника:

<Rectangle x:Name="rectangle2"
Width="100" Height="100"
Fill="Black"
HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="rectangle2"
Storyboard.TargetProperty=

586
"(Shape.Fill).(SolidColorBrush.Color)"
To="#00000000" Duration="0:0:5"
AutoReverse="True" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

Анимация точки с помощью PointAnimation:

Чтобы изменять значение, определённое как точка, с течением времени,


используется тип PointAnimation. Анимация вычисляется как линейная интерполяция
между значениями свойств цвета с течением указанного времени.
Подобно анимации двойных значений и значений цвета, значение в начале
анимации указывается с помощью значения From, а конечное значение указывается
либо как относительное направление (с помощью By), либо как абсолютная точка (с
помощью To). Следующий код показывает пример того, как можно анимировать
конечную точку кривой Безье. В данном случае, кривая Безье определяется со
начальной точкой в (100,100), конечной точкой в (300,100) и контрольной точкой в
(200,0). Анимация устанавливается на запуск после загрузки пути и она анимирует
конечную точку кривой (Point2) от (300,100) до (300,200) на протяжении пяти секунд.
Код добавляем после закрывающего тэга предыдущего фрагмента (прямоугольника):

<Path Stroke="Black">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="100,100">
<QuadraticBezierSegment x:Name="seg"
Point1="200,0" Point2="300,100" />
</PathFigure>
</PathGeometry>
</Path.Data>
<Path.Triggers>
<EventTrigger RoutedEvent="Path.Loaded">
<BeginStoryboard>
<Storyboard>
<PointAnimation Storyboard.TargetName="seg"
Storyboard.TargetProperty="Point2"
From="300,100" To="300,200"
Duration="0:0:5" AutoReverse="True"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Path.Triggers>
</Path>

Использование опорных кадров:


Все три типа анимации, о которых только что было рассказано, ColorAnimation,
DoubleAnimation и PointAnimation работают путём изменения определённого свойства с
течением времени, используя линейную интерполяцию. Например, при переводе
двойного значения от 100 к 500 в течение пяти секунд, оно будет изменяться на 80
каждую секунду.
В каждом из этих трёх типов анимации данный перенос может быть определён
через набор «вех», именуемых опорными кадрами. Чтобы изменить линейное
поведение анимации от начального свойства к конечному свойству, следует просто
вставить один или несколько опорных кадров. Затем определяется желаемый стиль
анимации между этими различными точками.
Опорные кадры определяются с помощью «опорных моментов». Это моменты,
указываемые относительно момента начала анимации; они также указывают время
окончания опорного кадра. Так что если, для примера, необходима девятисекундная

587
анимация с тремя равномерно распределёнными опорными моментами, можно указать
окончание первого опорного момента в 0:0:3, второго в 0:0:6 и третьего в 0:0:9.
Длина опорного момента не указывается: вместо этого указывается конечное время для
каждого опорного кадра.
В качестве ещё одного примера, представим себе двойную анимацию, которая
должна охватывать половину диапазона от 100 до 500. Анимация должна двигаться
очень быстро в первой половине и очень медленно во второй. В целом, она будет
требовать шести секунд переноса. Поскольку 350 – это середина между 100 и 500,
опорный кадр следует определить как начинающийся в точке 350. Ему следует указать
продолжаться одну секунду между начальной точкой и средней точкой (опорное время
0:0:1) и затем установить продолжительность опорного времени между средней точкой
и конечной точкой в пять секунд, используя второе опорное время как 0:0:6. Теперь
элемент установлен так, чтобы пролететь по экрану к средней точке и медленно ползти
дальше.
В предыдущих примерах, оба сегмента анимации используют линейную
интерполяцию. Для обеспечения большей гибкости, предоставлены два других типа
опорных кадров: дискретный опорный кадр, который мгновенно перебрасывает
значение между двумя значениями и сплайновый опорный кадр, который перемещает
значение между начальной и конечной точками, используя квадратическую кривую для
определения интерполяции.
Для указания опорных кадров на анимации используется постфикс
UsingKeyFrames. То есть, для указания двойных анимаций и использования опорных
кадров, следует использовать DoubleAnimationUsingKeyFrame, на котором указывается
цель и свойство (тем же образом, что при использовании DoubleAnimation).
DoubleAnimationUsingKeyFrames содержит определения опорных кадров. И то же самое
относится к PointAnimationUsingKeyFrames или ColorAnimationUsingKeyFrames.

Использование линейных опорных кадров:

По умолчанию, методом анимации между двумя значениями свойств является


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

<Rectangle Fill="#FFFF0000" Stroke="#FF000000"


Width="40" Height="40" x:Name="rectangle3">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="rectangle3"
Storyboard.TargetProperty="Width" AutoReverse="True"
RepeatBehavior="5x" >
<LinearDoubleKeyFrame KeyTime="0:0:1" Value="100" />
<LinearDoubleKeyFrame KeyTime="0:0:9" Value="200" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

Использование дискретных опорных кадров:

Если необходимо изменить значение свойства, не используя линейную


интерполяцию, то можно использовать дискретный опорный кадр. Это заставляет

588
объект перескочить к значению на момент указанного опорного кадра. Добавляем
следующий код на страницу, результатом будет скачкообразное изменение размеров
фиолетового прямоугольника в течении трёх секунд:

<Rectangle Fill="#FFFF00FF" Stroke="#FF0000FF"


HorizontalAlignment="Center" VerticalAlignment="Bottom"
Width="20" Height="20" x:Name="rectangle4">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="rectangle4"
Storyboard.TargetProperty="Width" AutoReverse="True"
RepeatBehavior="5x">
<DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="100" />
<DiscreteDoubleKeyFrame KeyTime="0:0:2" Value="200" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

Использование опорных кадров сплайна:

Если необходимо изменить значение свойства, используя искривляющееся


значение, обеспечивающее ускорение и замедление, то используется дискретный
опорный кадр. Для этого сперва определяется квадратичная кривая Безье, а затем
скорость свойства, при его перемещении от одного значения к другому, определяется
параллельной проекцией этой кривой.
Это похоже на следующую ситуацию: солнце прямо над головой и вы только что
ударили по футбольному мячу. Вы смотрите на тень мяча. По мере того, как он
взлетает выше, движение тени кажется ускоряющимся. В высшей точке полёта мяча,
можно увидеть, что движение тени замедлилось. Когда мяч начинает падать, можно
увидеть, что тень ускоряется снова, пока мяч не отобьют или он не коснётся земли.
Анимация, в этом случае — тень мяча, а сплайн — траектория полёта меча. Эта
траектория, сплайн, определяется используя KeySpline. KeySpline определяет
контрольные точки для квадратичной кривой Безье. Он нормализован так, что первой
точкой кривой является 0, а второй 1. Для параболической дуги, по которой пролетел
бы мяч, KeySpline будет содержать два нормализованных значения, разделенных
запятыми.
Чтобы определить кривую, подобную полету мяча, можно определить сплайн,
используя KeySpline, такой как 0.3, 0 0.6, 1. Это определит первую точку кривой как
(0.3, 0), а вторую как (0.6, 1). Результатом этого будет быстрое ускорение анимации
примерно до завершения одной трети полета мяча; затем она будет двигаться с
единообразной скоростью, примерно до завершения второй трети; и наконец, будет
уменьшаться в течение оставшейся части полета анимированного мяча, когда анимация
имитирует падение мяча на землю. Код такой:

<Ellipse Fill="Aqua" Stroke="#FFFF4444" HorizontalAlignment="Right"


Width="20" Height="20" x:Name="ball1">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Ellipse.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="ball1"
Storyboard.TargetProperty="Height" AutoReverse="True"
RepeatBehavior="3x">
<SplineDoubleKeyFrame KeyTime="0:0:5"

589
KeySpline="0.3,0 0.6,1" Value="200" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>

Теперь без долгих объяснений создаём «событийную» анимацию, о которой


говорилось в начале данного пункта работы. Основу каждого последующего окна
составит сетка, разделённая в соотношении 1:9 размеров окна. В верхней части сетки
будет закреплена StackPanel с надписью, а в нижней будет создан объект Path с
градиентом. Основу Path составит простой эллипс закрашенный этим градиентом. Для
второго StackPanel будет инициализировано событие MouseLeftButtonDown для
получения координат нажатия.

Создаём новую страницу с именем Page_BasicPointAnimation.xaml и XAML-


кодом:

<navigation:Page x:Class="LWP17Silverlight.Page_BasicPointAnimation"
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"
xmlns:navigation="clr-
namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
d:DesignWidth="300" d:DesignHeight="300"
Title="Знакомство с Silverlight (C#) :: Базовая точечная анимация с
событиями">
<navigation:Page.Resources>
<Storyboard x:Name="MyStoryboard">
<PointAnimation x:Name="MyPointAnimation" Duration="0:0:2"
Storyboard.TargetProperty="Center"
Storyboard.TargetName="MyAnimatedEllipseGeometry">
</PointAnimation>
</Storyboard>
</navigation:Page.Resources>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="1*"></RowDefinition>
<RowDefinition Height="9*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<TextBlock Text="Щёлкните левой кнопкой мыши на любой позиции на сером фоне"
TextAlignment="Center"></TextBlock>
</StackPanel>
<StackPanel MouseLeftButtonDown="StackPanel_MouseLeftButtonDown"
x:Name="MyStackPanel" Background="Gray" Grid.Row="1">
<Path>
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF1F7FB" Offset="0"/>
<GradientStop Color="#FF3794E4" Offset="1"/>
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<!-- Рисуем эллипс -->
<EllipseGeometry x:Name="MyAnimatedEllipseGeometry" Center="50,80"
RadiusX="15" RadiusY="15" />
</Path.Data>
</Path>
</StackPanel>
</Grid>

590
</navigation:Page>

Код события MouseLeftButtonDown для файла


Page_BasicPointAnimation.xaml.cs будет таким:

/// <summary>
/// Обработчик события изменяет свойство To объекта PointAnimation,
/// и начинает проигрывание анимации Storyboard
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void StackPanel_MouseLeftButtonDown(object sender, MouseButtonEventArgs
e)
{
var targetpoint = e.GetPosition(this.MyStackPanel);
this.MyPointAnimation.To = targetpoint;
this.MyStoryboard.Begin();
}

Код новой вкладки для TabControl:

<sdk:TabItem Header="Базовыя точечная анимации с событиями"


Name="BasicPointAnimation" DataContext="{Binding}">
<lwp17:Page_BasicPointAnimation></lwp17:Page_BasicPointAnimation>
</sdk:TabItem>

Следующая вкладка выполняет похожую функцию, только «анимация» на этот


раз нужна не только для эллипса, но и для линии, которая проводится из центра
эллипса к точке, в которую эллипс должен быть перемещён. Для реализации помимо
новой страницы также нам понадобится ещё одна дополнительная страница с классом.
Создаём новую страницу с именем Page_AimateDependencyProperty.xaml с
XAML-кодом:

<navigation:Page x:Class="LWP17Silverlight.Page_AnimateDependencyProperty"
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:c="clr-namespace:LWP17Silverlight;assembly=LWP17Silverlight"
mc:Ignorable="d"
xmlns:navigation="clr-
namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
d:DesignWidth="300" d:DesignHeight="300"
Title="Знакомство с Silverlight (C#) :: Анимация свойства зависимостей">
<navigation:Page.Resources>
<Storyboard x:Name="MyAnimationStoryboard">
<PointAnimation x:Name="MyAnimation"
Duration="0:0:2"
Storyboard.TargetProperty="EllipseCenter"
Storyboard.TargetName="MyAnimatedEllipseGeometry">
</PointAnimation>
</Storyboard>
</navigation:Page.Resources>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="1*"></RowDefinition>
<RowDefinition Height="9*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<TextBlock Text="Щёлкните левой кнопкой мыши на любой позиции на чёрном фоне"
TextAlignment="Center"></TextBlock>
</StackPanel>

591
<StackPanel x:Name="MyStackPanel"
MouseLeftButtonDown="MyStackPanel_MouseLeftButtonDown" Background="Black" Grid.Row="1">
<Canvas>
<Line x:Name="MyLine" Fill="Red" Stroke="Red" Visibility="Collapsed"
StrokeThickness="5" Canvas.ZIndex="1"></Line>
<c:MyEllipse x:Name="MyAnimatedEllipseGeometry"
EllipseCenterChanged="MyAnimatedEllipseGeometry_EllipseCenterChanged"></c:MyEllipse>
</Canvas>
</StackPanel>
</Grid>
</navigation:Page>

Код новой вкладки для TabControl:

<sdk:TabItem Header="Анимация свойства зависимостей"


Name="AnimateDependencyProperty" DataContext="{Binding}">

<lwp17:Page_AimateDependencyProperty></lwp17:Page_AimateDependencyProperty>
</sdk:TabItem>

Код файла Page_AnimateDependencyProperty.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Navigation;

namespace LWP17Silverlight
{
public partial class Page_AnimateDependencyProperty : Page
{
Point _currenttargetpoint;

public Page_AnimateDependencyProperty()
{
InitializeComponent();
}

// Выполняется, когда пользователь переходит на эту страницу.


protected override void OnNavigatedTo(NavigationEventArgs e)
{
}

private void MyStackPanel_MouseLeftButtonDown(object sender, MouseButtonEventArgs


e)
{
_currenttargetpoint = e.GetPosition(this.MyStackPanel);
this.MyAnimation.To = _currenttargetpoint;
this.MyAnimationStoryboard.Begin();
}

/// <summary>
/// Сихнронизация конечной точки объекта MyLine c последней точкой
/// которую пользователь выбирает нажатием и текущей позицие объекта MyEllipse.
/// Анимация создаётся для MyLine
/// </summary>

592
/// <param name="sender"></param>
/// <param name="e"></param>
private void MyAnimatedEllipseGeometry_EllipseCenterChanged(DependencyObject
sender, DependencyPropertyChangedEventArgs e)
{
this.MyLine.Visibility = Visibility.Visible;
this.MyLine.X1 = this.MyAnimatedEllipseGeometry.EllipseCenter.X;
this.MyLine.Y1 = this.MyAnimatedEllipseGeometry.EllipseCenter.Y;
this.MyLine.X2 = this._currenttargetpoint.X;
this.MyLine.Y2 = this._currenttargetpoint.Y;
}
}
}

Теперь создаём ещё одну страницу с именем MyEllipse.xaml и XAML-кодом:

<navigation:Page x:Class="LWP17Silverlight.MyEllipse"
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:c="clr-namespace:LWP17Silverlight;assembly=LWP17Silverlight"
mc:Ignorable="d"
xmlns:navigation="clr-
namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
d:DesignWidth="300" d:DesignHeight="300">
<Grid x:Name="LayoutRoot" Background="Transparent" IsHitTestVisible="False">
<Path>
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF1F7FB" Offset="0"/>
<GradientStop Color="#FF3794E4" Offset="1"/>
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
Center="50,50" RadiusX="15" RadiusY="15" />
</Path.Data>
</Path>
</Grid>
</navigation:Page>

Код файла MyEllipse.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Navigation;

namespace LWP17Silverlight
{
public delegate void EllipseCenterChangedEventHandler(DependencyObject sender,
DependencyPropertyChangedEventArgs e);

public partial class MyEllipse : Page


{

593
public static readonly DependencyProperty EllipseCenterProperty =
DependencyProperty.Register("EllipseCenter", typeof(Point),
typeof(MyEllipse),
new PropertyMetadata(new PropertyChangedCallback((obj, e) => { MyHandler(obj,
e); })));
EllipseCenterChangedEventHandler _myDelegate;

public event EllipseCenterChangedEventHandler EllipseCenterChanged


{
add
{
if (_myDelegate == null) _myDelegate = value;
else { Delegate.Combine(_myDelegate, value); }
}
remove { Delegate.Remove(_myDelegate, value); }
}

public Point EllipseCenter


{
get { return (Point)GetValue(EllipseCenterProperty); }
set { SetValue(EllipseCenterProperty, value); }
}

public MyEllipse()
{
InitializeComponent();
EllipseCenter = this.MyAnimatedEllipseGeometry.Center;
}

/// <summary>
/// Это метод обратного вызова, который вызывает метод OnEllipseCenterChanged
/// объекта MyEllipse, свойство EllipseCenter которого было изменено
/// </summary>
/// <param name="obj"></param>
/// <param name="e"></param>
static void MyHandler(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
MyEllipse ellipse = obj as MyEllipse;
if (ellipse != null) { ellipse.OnEllipseCenterChanged(obj, e); }
}

/// <summary>
/// Этот метод вызывается методом свойства EllipseCenterProperty.
/// Это обновляет свойство Center объекта MyAnimatedEllipseGeometry интерфейса,
/// когда срабатывает событие EllipseCenterChanged
/// </summary>
/// <param name="obj"></param>
/// <param name="e"></param>
void OnEllipseCenterChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs e)
{
MyEllipse ellipse = obj as MyEllipse;
if (ellipse != null) { ellipse.MyAnimatedEllipseGeometry.Center =
ellipse.EllipseCenter; }
if (_myDelegate != null) _myDelegate(obj, e);
}
}
}

Применение специально функции EasingFunction, позволяющей «настроить»


анимацию различными способами:

<DoubleAnimation>
<DoubleAnimation.EasingFunction>

594
Функции
</DoubleAnimation.EasingFunction>
</DoubleAnimation>

Создаём последнюю страницу с именем Page_EasingFunction.xaml с XAML-


кодом:

<navigation:Page x:Class="LWP17Silverlight.Page_EasingFunction"
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:c="clr-namespace:LWP17Silverlight;assembly=LWP17Silverlight"
mc:Ignorable="d"
xmlns:navigation="clr-
namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
d:DesignWidth="300" d:DesignHeight="300"
Title="Знакомство с Silverlight (C#) :: Применение EasingFunction">
<navigation:Page.Resources>
<BackEase x:Name="BackEase" Amplitude="1" EasingMode="EaseIn"></BackEase>
<c:MyEase x:Name="MyEase"></c:MyEase>
<Storyboard x:Name="MyAnimationStoryboard">
<PointAnimation x:Name="MyAnimation"
Duration="0:0:2"
Storyboard.TargetProperty="Center"
Storyboard.TargetName="MyAnimatedEllipseGeometry"
EasingFunction="{StaticResource BackEase}">
</PointAnimation>
</Storyboard>
</navigation:Page.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="3*"></RowDefinition>
<RowDefinition Height="7*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<RadioButton GroupName="g1" Content="Применяем BackEase" IsChecked="true"
x:Name="BackEaseRadioButton" Click="BackEaseRadioButton_Click"></RadioButton>
<RadioButton GroupName="g1" Content="Применяем MyEase" IsChecked="false"
x:Name="MyEaseRadioButton" Click="MyEaseRadioButton_Click"></RadioButton>
<TextBlock Text="Щёлкните левой кнопкой мыши на любой позиции на розовом
фоне" TextAlignment="Center"></TextBlock>
</StackPanel>
<StackPanel MouseLeftButtonDown="MyStackPanel_MouseLeftButtonDown"
x:Name="MyStackPanel" Background="Pink" Grid.Row="1">
<Path>
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF1F7FB" Offset="0"/>
<GradientStop Color="#FF3794E4" Offset="1"/>
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
Center="50,50" RadiusX="15" RadiusY="15" />
</Path.Data>
</Path>
</StackPanel>
</Grid>
</navigation:Page>

Код события Click нажатия кнопки для файла Page_EasingFunction.xaml.cs,


нажатия на одну из RadioButton, а также специальный класс MyEase будет таким (в
одно файле):

595
private void MyStackPanel_MouseLeftButtonDown(object sender, MouseButtonEventArgs
e)
{
var targetpoint = e.GetPosition(this.MyStackPanel);
this.MyAnimation.To = targetpoint;
this.MyAnimationStoryboard.Begin();
}

private void MyEaseRadioButton_Click(object sender, RoutedEventArgs e)


{
this.MyAnimation.EasingFunction = this.Resources["MyEase"] as
IEasingFunction;
}

private void BackEaseRadioButton_Click(object sender, RoutedEventArgs e)


{
this.MyAnimation.EasingFunction = this.Resources["BackEase"] as
IEasingFunction;
}
}

/// <summary>
/// Класс MyEase
/// </summary>
public class MyEase : EasingFunctionBase
{
protected override double EaseInCore(double normalizedTime)
{
return normalizedTime / 5;
}
}

Код новой вкладки для TabControl:

<sdk:TabItem Header="Применение EasingFunction" Name="EasingFunction"


DataContext="{Binding}">
<lwp17:Page_EasingFunction></lwp17:Page_EasingFunction>
</sdk:TabItem>

Следующая последняя вкладка будет запускать по нажатию кнопки анимацию


«падения шарика на пол».
Создаём последнюю страницу с именем Page_UsingKeyFrames.xaml с XAML-
кодом:

<navigation:Page x:Class="LWP17Silverlight.Page_UsingKeyFrames"
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"
xmlns:navigation="clr-
namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
d:DesignWidth="300" d:DesignHeight="300"
Title="Знакомство с Silverlight (C#) :: Покадровая анимация">
<navigation:Page.Resources>
<Storyboard x:Name="MyAnimationStoryboard">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="path" Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[3].(TranslateTransform.X)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.8000000" Value="-4"
KeySpline="1,0,1,1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="-5" KeySpline="0,0,0,1"/>

596
<SplineDoubleKeyFrame KeyTime="00:00:01.2000000" Value="-4"
KeySpline="1,0,1,1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="path" Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[3].(TranslateTransform.Y)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.8000000" Value="127"
KeySpline="1,0,1,1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="69" KeySpline="0,0,0,1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01.2000000" Value="127"
KeySpline="1,0,1,1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</navigation:Page.Resources>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="1*"></RowDefinition>
<RowDefinition Height="9*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Button Content="Нажмите кнопку для начала анимации"
Click="Button_Click"></Button>
</StackPanel>
<StackPanel x:Name="MyStackPanel" Background="WhiteSmoke" Grid.Row="1">
<Path x:Name="path" RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Path.RenderTransform>
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF1F7FB" Offset="0"/>
<GradientStop Color="#FF3794E4" Offset="1"/>
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
Center="50,50" RadiusX="15" RadiusY="15" />
</Path.Data>
</Path>
</StackPanel>
</Grid>
</navigation:Page>

Код события Click нажатия кнопки для файла Page_UsingKeyFrames.xaml.cs


будет таким:

private void Button_Click(object sender, RoutedEventArgs e)


{
this.MyAnimationStoryboard.Begin();
}

Код новой вкладки для TabControl:

<sdk:TabItem Header="Покадровая анимация" Name="UsingKeyFrames"


DataContext="{Binding}">
<lwp17:Page_UsingKeyFrames></lwp17:Page_UsingKeyFrames>
</sdk:TabItem>

597
Готово. Компилируем, проверяем работоспособность. Если требуется поменять
заголовок страницы с приложением при запуске, открываем файлы
LWP17SilverlightTestPage.aspx или LWP17SilverlightTestPage.html, и ищем так строку:

<title>LWP17Silverlight</title>

6. Завершающая часть

Компилируем приложения (Release) и запускаем.

Рис. 6. 1. Результат работы приложения Silverlight: анимация на вкладке «Основы


анимации»

Рис. 6. 2. Результат работы приложения Silverlight: анимация на вкладке «Базовая


точечная анимация с событиями»

598
Рис. 6. 3. Результат работы приложения Silverlight: анимация на вкладке «Анимация
свойства зависимостей»

Рис. 6. 4. Результат работы приложения Silverlight: анимация на вкладке «Покадровая


анимация»

599
Рис. 6. 5. Результат работы приложения Silverlight: анимация на вкладке «Применение
EasingFunction»

7. О приложении к Лабораторной работе № 17

Получившуюся программу (LWP17Silverlight.xap, и


LWP17SilverlightTestPage.html), собранную из кусков кода приведённых в данной
лабораторной работе, можно загрузить по ссылке в конце этого материала (сслыка
доступна в программном продукте).

Приложение № 1: Исходный код WCF-проекта и всех сопровождающих


файлов с кодом приведён по ссылке в конце этого материала (сслыка доступна в
программном продукте).
Приложение № 2: Исходный код веб-проекта и всех сопровождающих
файлов с кодом приведён по ссылке в конце этого материала (сслыка доступна в
программном продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

18. Лабораторная работа № 18: Знакомство с ASP.NET

Лабораторная работа № 18: Знакомство с ASP.NET

Содержание

50.Вводная часть
51.Создание веб-приложения ASP.NET и новой страницы веб-приложения
52.Модификация веб-приложения ASP.NET: реализация различной
функциональности
53.Модификация веб-приложения ASP.NET: AJAX
54.Завершающая часть
55.О приложении к Лабораторной работе № 18

1. Вводная часть

600
В этой работе будет рассмотрена работа с подтипом приложений доступных для
создания в Visual Studio 2010, а именно: Веб-приложение ASP.NET.

ASP.NET — технология создания веб-приложений и веб-сервисов от корпорации


Microsoft. Она является составной частью платформы Microsoft .NET и развитием более
старой технологии Microsoft ASP. На данный момент последней версией этой
технологии является ASP.NET 4.0. Однако в текущих бета-версия следующей ОС
Windows 8 присутствует ASP.NET 4.5.
ASP.NET внешне во многом сохраняет схожесть с более старой технологией ASP,
что позволяет разработчикам относительно легко перейти на ASP.NET. В то же время
внутреннее устройство ASP.NET существенно отличается от ASP, поскольку она
основана на платформе .NET и, следовательно, использует все новые возможности,
предоставляемые этой платформой.
ASP.NET не является платформой.

ASP (Active Server Pages — «активные серверные страницы») — первая


технология Microsoft, позволяющая динамически создавать веб-страницы на стороне
сервера. ASP работает на операционных системах линейки Windows NT и на веб-
сервере Microsoft IIS.
ASP не является языком программирования — это лишь технология предварительной
обработки, позволяющая подключать программные модули во время процесса
формирования веб-страницы. Относительная популярность ASP основана на простоте
используемых языков сценариев (VBScript или JScript) и возможности использования
внешних COM-компонентов.

Итак: ASP.NET — это веб-платформа, предоставляющая все необходимые службы


для создания серверных веб-приложений корпоративного класса.ASP.NET создана на
основе платформы .NET Framework, поэтому все функции .NET Framework доступны
для приложений ASP.NET. Приложения могут быть написаны на любом языке,
совместимом со средой CLR, включая, конечно же, C#.
Средства и параметры Visual Studio, предназначенные для создания веб-
приложений, в совокупности именуются Visual Web Developer. Кроме того, имеется
бесплатный самостоятельный продукт Visual Web Developer Express, который
включает в себя базовый набор функций веб-разработки, реализованных в Visual
Studio.

С помощью Visual Studio 2010 можно создавать различные типы проектов


ASP.NET, в том числе веб-сайты, веб-приложения, веб-службы и серверные
элементы управления AJAX (Asynchronous JavaScript and XML — «асинхронный
JavaScript и XML»).
Между проектами веб-сайтов и проектами веб-приложений существует разница.
Некоторые функции, такие как MVC (Model-View-Controller — «Модель-
представление-поведение», «Модель-представление-контроллер») и некоторые
средства автоматизации веб-развёртывания, работают только с проектами веб-
приложений. Другие функции, такие как платформы динамических данных, работают
как с проектами веб-сайтов, так и проектами веб-приложений. В данной лабораторной
работе будет создан проект именно веб-приложения. Веб-приложение — (на взгляд
автора лабораторной работы) наиболее удобный тип проекта для создания сайтов.

Что же такое, «веб-приложение»?

601
С точки зрения пользователя, открывшего браузер и перешедшего на веб-сайт,
всё веб-приложение для него: веб-страница. То есть пользователя не нужно знать,
как и на чём написана страница в браузере. Главное для него, разумеет содержимое.
Итак, ASP.NET предоставляет огромные и мощные инструменты по созданию веб-
страниц с применением полноценных языков программирования и вытекающими
отсюда практически безграничными возможностями.

Требования к системе для просмотра веб-страниц ASP.NET:

1. Установленный .NET Framework.


2. IIS версии не ниже 6.0.

Преимущества ASP.NET:

1. ASP.NET скрипты компилируются, а не интерпретируются. Веб-приложения


написанные на .NET это набор скриптов, которые выполняются на виртуальной машине
.NET Framework, то есть для работы приложений на ASP.NET кроме самого веб сервера
необходим ещё и .NET Framework. Таким образом: при первом обращении к
приложению идёт компиляция скриптов в файлы *.dll, а далее и их исполнение
«фреймворком». При последующем обращении идет просто вызов DLL тем самым
увеличивается быстродействие по сравнению с другими скриптовыми языками, которые
интерпретируются интерпретатором каждый раз запросе.
Перекомпиляция происходит автоматически при перезагрузке приложения или
изменении существующих скриптов. Выполнение, таким образом, существенно
ускоряется.
2. Отделение кода. Благодаря тому что дизайн сайта находится (может
находиться) в файле отдельном от кода приложения, программисты и дизайнеры могу
работать одновременно над одним проектом и не сталкиваться между собой по
вопросам кода. Тем самым код становится чистым и понятным. Такая технология
называется Code Behind.
3. Настоящий отладчик. В отличие от других веб-языков, при разработке на
ASP.NET не придётся после каждой строчки делать вывод в журнал или на HTTP-запрос.
Достаточно только включить директиву трассировки и вся доступная информация будет
на экране. Используя Visual Studio возможно пошаговое выполнение приложения.
4. Как уже отмечалось выше, используя ASP.NET возможно писать приложения
на различных языках, благодаря тому что .NET является мультиязыковой технологией.
5. Развёртывание. Развёртывание приложения подразумевает лишь копирование
файлов на сервер. Причём все настройки приложения хранятся в XML файле и ничего
не придётся настраивать дополнительно.

Файлы ASP.NET и дополнительных файлов, что могут быть использованы в веб-


приложении данной лабораторной работы:

*.asax — файл обработчик событий приложения и сессий.


*.ascx — пользовательские элементы управления.
*.asmx — веб-сервисы.
*.aspx — сами страницы.
*.config — файлы настроек.
*.cs — файл с кодом C# для страниц ASP.NET.
*.js — файл скриптов JSrcipt и JavaScript.
*.css — файл стилей для HTML-представления веб-страниц.

602
Дополнительные сведения о ASP.NET можно получить в разделе ASP.NET и
Visual Web Developer (http://msdn.microsoft.com/ru-ru/library/dd566231.aspx)
на странице
ASP.NET 4 (http://msdn.microsoft.com/ru-ru/library/ee532866.aspx).

Что будет представлять из себя приложение, разрабатываемое в данной


лабораторной работе?

Рассмотрим самые простые примеры работы с веб-приложением ASP.NET.


Разделим данную работу на три части.
Перед началом работы, рассмотрим процесс создания веб-приложения и новой
страницы с элементами для него, а также рассмотрим основные особенности работы с
данным типом приложений.
В первой ...
Во второй ...
Третья часть работы рассмотрим совместное использование ASP.NET и AJAX на
некоторых простых примерах, таких как:

2. Создание веб-приложения ASP.NET

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


нажмём на Веб и найдём в списке справа Веб-приложение ASP.NET. Также здесь
можно выбрать какой использовать «фреймворк» (набора компонентов для написания
программ). В нашем случае выберем .NET Framework 4.

603
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP18ASPNET — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

604
Рис. 2. 3. Вводим данные нового проекта веб-приложения ASP.NET

После нажатия кнопки ОК (рисунок 2. 3), среда разработки сформирует один


полноценный проект веб-приложения (шаблон ASP.NET 4):

605
Рис. 2. 4. Обозреватель решений: состав проекта веб-приложения ASP.NET
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально). Перед компиляцией обратим вниманием на
директорию App_Data веб-проекта. Директория пока что пуста.

606
Рис. 2. 5. Запуск веб-приложения ASP.NET по конфигурации Debug

Жмём Войти, далее Регистрация.


Вводим в поле «Имя пользователя»: «Test» без кавычек.
В поле «Электронная почта»: «test@test.com».
В поля «Пароль» и «Подтвердите пароль», первоначально вписываем «123»,
жмём Создать пользователя и видим предупреждение о минимальном количестве
знаков и содержании пароля. Вводим любой простой пароль из шести знаков и снова
жмём на кнопку:

607
Рис. 2. 6. Запуск веб-приложения ASP.NET по конфигурации Debug: создание базы
данных пользователей

Ждём, пока система сформирует базу данных. Закрываем приложение (страницу


в браузере). Теперь, если снова запустить приложение и нажать кнопку «Войти», а
затем ввести данные пользователя «Test» + его пароль, то будет осуществлён вход в
систему веб-приложения. Данные будут подцепляться из базы. Сама база создаётся в
папке App_Data. Для просмотра записей таблиц базы сделаем следующее: нажмём в
обозревателе решений на кнопку Показать все файлы:

Далее, если на ПК не установлен SQL Server 2012 или ниже, будем работать в
«местном аналоге» предлагаемым по умолчанию студией. Откроем папку «App_Data»:

Дважды щёлкнем на ASPNETDB.MDF. Откроется новая панель слева:

608
Рис. 2. 7. Обозреватель серверов: ASPNETDB.MDF -> Таблицы ->
aspnet_Membership

«Обозреватель серверов» отображает все текущие подключенные серверы (в


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

609
Рис. 2. 8. aspnet_Membership: режим конструктора таблицы

Для открытия данных, выделим в обозревателе серверов эту же таблицу, далее


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

Для работы с базами данных (как SQL Server, SQL Express, так и Microsoft
Access) Visual Studio имеет достаточно средств для комфортной работы с базами
различных поставщиков. В среде разработки предусмотрело большинство средств
работы с базами данных. От «схемы данных» и «связи данных», до создания SQL-

610
запросов. Останавливаться на этом не будем. Куда интереснее посмотреть
непосредственно код подключения и работы с базой данных.

Для подключения к базе и выбора провайдера используется файл Web.config (в


корневой папке проекта). Код подключения очевиден:

<configuration>
<connectionStrings>
<add name="ApplicationServices"
connectionString="data source=.\SQLEXPRESS;Integrated
Security=SSPI;AttachDBFilename=|DataDirectory|\aspnetdb.mdf;User Instance=true"
providerName="System.Data.SqlClient" />
</connectionStrings>

За запись данных в базу отвечает файл в папке Account -> Register.aspx, и


непосредственно элемент управления CreateUserWizard (группа элементов Вход:

). Настройка вводимых данных на странице


регистрации (минимальная длина пароля, возможность сбора пароля для пользователя
и прочее), осуществляется следующим кодом «конфига»:

<membership>
<providers>
<clear/>
<add name="AspNetSqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="ApplicationServices"
enablePasswordRetrieval="false" enablePasswordReset="true"
requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>

За страницу «входа» в систему отвечает файл той же папки: Login.aspx.


За страницу смены пароля для текущего пользователя отвечает файл
ChangePassword.aspx. Страница, оповещающая об успешной смене пароля:
ChangePasswordSuccess.aspx.

Итак, откроем любую веб-страницу нашего приложения и посмотрим на код.


Например, для «начальной» страницы веб-приложения Default.aspx код такой:

<%@ Page Title="Домашняя страница" Language="C#" MasterPageFile="~/Site.master"


AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="LWP18ASPNET._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">


</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
<h2>
Добро пожаловать в ASP.NET!
</h2>
<p>
Для получения дополнительных сведений об ASP.NET посетите веб-сайт <a
href="http://www.asp.net" title="Веб-сайт ASP.NET">www.asp.net</a>.
</p>
<p>
Кроме того, <a href="http://go.microsoft.com/fwlink/?LinkID=152368"

611
title="Документация по ASP.NET на MSDN">документация по ASP.NET доступна на
MSDN</a>.
</p>
</asp:Content>

Это полноценная HTML-страница, которая является «встраиваемой в главную


мастер-страницу Site.Master. Встраивание страницы похоже на работу элемента
Windows Forms: TabControl. Мастер-страница (шаблон) отвечает за меню и внешнее
оформление страницы, содержимое же самой страницы Default.aspx подгружается
внутри рамки мастер-страницы. Код Site.Master для вложения содержимого
Default.aspx:

<div class="main">
<asp:ContentPlaceHolder ID="MainContent" runat="server"/>
</div>

Как видно, Default.aspx имеет «ссылку» MainContent:

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">


...
</asp:Content>

Информация о том, что страница использует шаблон храниться в заголовке:

... MasterPageFile="~/Site.Master" ...

Для чего нужны шаблоны (Site.Master)?

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

Разумеет, страницы могут быть любыми и содержать любой HTML-код.


Классический вариант веб-страниц это «пустая» страница, или так называемая Форма
Web Form, которую можно добавить вызван окно создания нового элемента для
проекта. Встраиваемая страница под выбираемый шаблон: Веб-форма,
использующая главную. Главный шаблон: Главная страница.
Создаём новый элемент: Проект -> Добавить новый элемент...
(Ctrl+Shift+A). Выбираем тип страницы «Веб-форма, использующая главную
страницу», Имя вводим как Page.aspx, жмём ОК.

612
Рис. 2. 9. Добавление нового элемента – LWP18ASPNET: добавление встраиваемой
страницы

В следующем окне, среда разработки поспросит выбрать главную страницу в


качестве основы. У нас она одна, это «Site.Master». Жмём ОК.
Любая веб-страница имеет свой «Конструктор». Как и для Windows Forms, так и
для WPF отображение страницы через конструктор достаточно наглядное. Для
переключения между режимами работы с файлами веб-страниц используются кнопки
«Конструктор», «Разделитель» и «Исходный код». Наиболее удобным (личное мнение
автора) является режим «Разделитель», подходящий идеально в случае наличия
монитора с большим экраном :

Представление «Конструктор» — это область редактирования, близкая к режиму


точного отображения (WYSIWYG).Представление «Исходный код» — это HTML-редактор
страницы. В представлении с Разделитель отображаются оба представления
документа — Конструктор и Исходный код.

Добавленная страница теперь выглядит так:

Напишем что-нибудь внутри:

613
Тут же изменится HTML-код (тэг p будет вставлен автоматически):

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">


<p>
Это новая страница нашего сайта!</p>
</asp:Content>

Выполним последовательно следующие действия:

1. Поместите точку (курсор) вставки в конце текста «Это новая страница нашего
сайта!» и нажмём Enter один раз, чтобы создать дополнительное пространство в
поле элемента div. Вручную перепишем тэг <p></p> на тэг <div><div>:

<p>
Это новая страница нашего сайта!</p>
<div>
</div>

2. Развернём группу Стандартные на панели элементов.


3. Перетащим элемент управления TextBox на страницу и расположим его в
центре поля элемента div. После текстового поля нажмём пробел.
4. Перетащим элемент управления Button на страницу, и расположите его справа
от элемента управления TextBox.
5. Изменим код div’а для вставки над TextBox строчки «Введите свое имя:»:

<div>
<p>Введите ваше имя:<br />
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>&nbsp;<asp:Button
ID="Button1" runat="server" Text="Button" />
</p>
</div>

6. Переставим курсор в конструкторе после кнопки и нажмём клавишу Enter,


чтобы расширить место на странице. Перетащим элемент управления Label на
страницу, и расположим его на отдельной строке под элементом управления
Button.
7. Выберем элемент управления Button, после чего в окне Свойства задаём для
свойства Text значение «Отображаемое имя». Введённый текст появится на
кнопке в конструкторе. Получим:

614
Теперь выполним следующее:

1. Перейдём в представление «Исходный код».


В представлении Исходный код отображается HTML-код страницы, включая
элементы, автоматически создаваемые в Visual Studio для серверных элементов
управления. Для объявления элементов управления используется схожий с HTML
синтаксис. Отличие заключается в использовании префикса asp: и атрибута
runat="server".
Свойства элемента управления объявляются как атрибуты. Например, при
задании свойства Text элемента управления Button фактически задаётся атрибут Text
разметки элемента управления.
Обратим внимание, что все элементы управления находятся внутри элемента
Content, который также имеет атрибут runat="server".Атрибут runat="server" и
префикс asp: тегов элемента управления отмечают элементы управления так, что они
обрабатываются в ASP.NET на сервере при запуске страницы. Код вне элементов
<form runat="server"> и <script runat="server"> передаётся в браузер в
неизменённом виде, поэтому код в ASP.NET должен быть внутри элемента, в
открывающем теге которого содержится атрибут runat="server".
2. Установим точку вставки после asp:Label в теге <asp:Label> и нажмём на
пробел. Появится раскрывающийся список, в котором отображается список
свойств, которые можно задавать для элемента управления Label. Эта
стандартный IntelliSense1, помогает работать в представлении «Исходный код»
с синтаксисом серверных элементов управления, HTML-элементами и другими
элементами на странице.
3. Выберем свойство ForeColor и введём знак равенства и знак кавычек «="».
Отобразится список возможных цветов.
4. Выберите цвет текста элемента управления Label. Выберем достаточно тёмный
цвет, который будет хорошо смотреться на белом фоне.
5. Дважды щёлкнем элемент управления Button в конструкторе. Visual Studio
переключится в представление Исходный код и создаст каркас обработчика для
события по умолчанию элемента управления Button и события Click. Код
события Click, а также слегка изменённое событие возникающие при вызове
страницы Page_Load:

protected void Page_Load(object sender, EventArgs e)


{
Label1.Text = "";
}

protected void Button1_Click(object sender, EventArgs e)


{

615
Label1.Text = TextBox1.Text + ", добро пожаловать на страницу!";
}

Прокрутим HTML-код страницы вниз до элемента <asp:Button>.Обратим


внимание, что элемент <asp:Button> теперь имеет атрибут onclick="Button1_Click".

ПРИМЕЧАНИЕ № 1: Раскрывающийся список IntelliSense можно отобразить в


любой момент путем нажатия сочетания клавиш Ctrl+J.

Теперь для данной страницы выполним последние действия (добавление


элемента управления «Календарь»):

1. В Visual Studio перейдём в представление конструктора.


2. Из раздела Стандартные на панели элементов перетащим элемент управления
Calendar на страницу, и разместите его под элементом Label (Enter после
элемента создаст новый тэг «p»). Отобразится панель смарт-тега календаря.
Панель отображает команды, которые позволяют выполнять самые часто
используемые задачи выбранного элемента управления.
3. В области смарт-тега выберем Автоформат....
4. В окне Автоформат в списке «Выберите схему», выбираем Простой. Жмём ОК.
5. В представлении «Конструктор» дважды щёлкнем на элементе управления
«Calendar». Исходный код события SelectionChanged:

protected void Calendar1_SelectionChanged(object sender, EventArgs e)


{
Label1.Text = Calendar1.SelectedDate.ToLongDateString();
}

Страницу можно запускать через отладку, но если текущий выбор в среде


разработки выставлен на иную страницу (не на Page.aspx), то для доступа к странице
придётся вводить имя в адресную строку браузера.
Для того, чтобы добавить страницу в меню навигации веб-приложения
(запускать не только через отладку), откроем файл Site.Master и перейдём на
представление «Исходный код». Найдём:

<div class="clear hideSkiplink">


<asp:Menu ID="NavigationMenu" runat="server" CssClass="menu"
EnableViewState="false" IncludeStyleBlock="false" Orientation="Horizontal">
<Items>
<asp:MenuItem NavigateUrl="~/Default.aspx" Text="Домашняя"/>
<asp:MenuItem NavigateUrl="~/About.aspx" Text="О программе"/>
</Items>
</asp:Menu>
</div>

Здесь главным является элемент Menu. Дочерние элементы расположены в тэге


Items. Для добавления новых можно скопировать код и вписать нужный адрес, либо
работать через визуальный конструктор (смарт-тэг Правка элементов меню...):

616
Редактируем код:

<div class="clear hideSkiplink">


<asp:Menu ID="NavigationMenu" runat="server" CssClass="menu"
EnableViewState="false" IncludeStyleBlock="false" Orientation="Horizontal">
<Items>
<asp:MenuItem NavigateUrl="~/Default.aspx" Text="Домашняя"/>
<asp:MenuItem NavigateUrl="~/Page.aspx" Text="Страница"/>
<asp:MenuItem NavigateUrl="~/About.aspx" Text="О программе"/>
</Items>
</asp:Menu>
</div>

И изменим заголовок (для браузера) Page.aspx:

<%@ Page Title="Новая страница" Language="C#" MasterPageFile="~/Site.Master"


AutoEventWireup="true" CodeBehind="Page.aspx.cs" Inherits="LWP18ASPNET.Page" %>

Компилируем, проверяем работоспособность. Вводим имя, жмём кнопку и


выделяем дату в календаре на странице.

617
Рис. 2. 10. Результат работы веб-приложения ASP.NET: выбрана дата в календаре

3. Модификация веб-приложения ASP.NET: реализация различной


функциональности

На этот раз будем работать на странице Default.aspx. Перейдём к конструктору


страницы. Нажмём Enter после последней строчки с текстом. Добавиться новый тэг «p».
С панели элементов перетащим TextBox и изменим свойства:

Width: 300px
ReadOnly: True

Откроем событие Page_Load для этой страницы (файл Default.aspx.cs) и


впишем:

TextBox1.Text = "Московское время: " + DateTime.Now.TimeOfDay.ToString();


// Response.Write("Сейчас: " + DateTime.Now); // Функция выводит строку в
левом верхнем углу страницы

Ещё несколько слов о ViewState и серверных элемента управления:


В Visual Studio есть два раздела с похожими компонентами Например такие как
TextBox (текстовое поле для редактирования). Один элемент находится в разделе
«Стандартные» (TextBox) другой находится в разделе HTML «Input (Text)». В чём же их
отличие? Для начала добавим на страницу два этих элемента управления:

<p>
<asp:TextBox ID="TextBox2" runat="server"></asp:TextBox>
<input id="Text1" type="text" />
</p>

618
Кроме того, что они объявляются по-разному, в TextBox добавлен параметр
runat="server", который он говорит компилятору о том, что исполнение данного
элемента будет происходить на сервере. Похоже на TextBox в Windows Forms, то есть
элемент имеет свойства, методы и события.

Второй же элемент, не что иное, как известный всем HTML тэг и компилятором он
никак обрабатываться не будет, а будет использоваться как обычный HTML.
В случае PHP или HTML: у пользователя всегда была нужная переменная на
каждой странице, и её надо все время передавать через запросы (речь не о «cookie»),
например через «input type=hidden».

В ASP.NET есть механизм, который называется ViewState. На самом деле


ViewState это тот-же input type=hidden в который добавляются все свойства
серверных элементов управления. По умолчанию все свойства элементов
«серелизуются» в ViewState.
Таким образом, задав например цвет элемента красным — он будет красным на
всех страницах после каждого «постбэка» (посылки запроса) пользователя.
При инициализации страницы все доступные свойства из ViewState
подставляются в наш серверный элемент.
Для HTML-элементов такое разумеется не выполняется.
При всех своих плюсах ViewState имеет минус — размер. При большом
количестве элементов на странице и большом изменении их свойств ViewState может
сильно раздуваться. Необходимо позаботиться о пользователях с низкими скоростями
соединения с интернетом.

Можно удалить ViewState двумя способами:

1. Удалять для всей странице целиком, указав директиву


EnableViewState="false".

<%@ Page Title="Домашняя страница" Language="C#" MasterPageFile="~/Site.master"


AutoEventWireup="true" EnableSessionState="False"
CodeBehind="Default.aspx.cs" Inherits="LWP18ASPNET._Default" %>

2. Выключить для отдельных элементов:

<asp:TextBox ID="TextBox2" runat="server" EnableViewState="false"></asp:TextBox>

Global.asax:

Иногда, нужно обработать события, которые возникают в момент первого


посещения сайта пользователем. Например, хотим знать когда пользователь зашёл или
когда вышел или просто факт того что он был на сайте и прочее. Как же это сделать?
Но начнём не с пользователя, а с приложения.
В совокупности все страницы ASP.NET входят в приложение. Оно загружается,
как только к нему происходит обращение. Для того чтобы об этом первом обращении, а
также о других события используется файл Global.asax. Откроем этот файл с кодом.

1. Application_Start отрабатывается, когда загружается приложение, допишем


туда код (одну строку после комментария):

void Application_Start(object sender, EventArgs e)


{

619
// Код, выполняемый при запуске приложения
Application["users"] = 0;
}

2. Application_End — приложение завершается.


3. Application_Error — ошибка в приложении.
4. Session_Start — начинается сессия для нового пользователя. Код наш
выполняет увеличение счётчика на единицу:

void Session_Start(object sender, EventArgs e)


{
// Код, выполняемый при запуске нового сеанса
Application["users"] = Convert.ToInt32(Application["users"]) + 1;
}

5. Session_End — завершается сессия. Событие Session_End вызывается только


в том случае, если для режима sessionstate задано значение «InProc» в файле
Web.config. Если для режима сеанса задано значение StateServer или SQLServer,
событие не порождается. Код выполняет уменьшение счётчика на едениу при
завершении сессии:

void Session_End(object sender, EventArgs e)


{
// Код, выполняемый при запуске приложения.
// Примечание: Событие Session_End вызывается только в том случае, если для
режима sessionstate
// задано значение InProc в файле Web.config. Если для режима сеанса задано
значение StateServer
// или SQLServer, событие не порождается.
Application["users"] = Convert.ToInt32(Application["users"]) - 1;
}

Добавим на страницу Default.aspx новый элемент TextBox с HTML-кодом:

<p>
<asp:TextBox ID="TextBox3" runat="server" ReadOnly="True"
Width="300px"></asp:TextBox>
</p>

Код для события Page_Load файла Defualt.aspx.cs:

TextBox3.Text = "Число пользователей на сайте: " +


Application["users"].ToString();

Button:

В веб-приложениях кнопка делает «PostBack» (в HTML это называется submit)


на какую-либо страницу (чаще всего на свою же). Основное отличие ASP.NET
заключается в том, что по кнопке идёт не только постбэк (передача POST и GET
параметров), но так же ещё отрабатывает событие Click которое обрабатывается каким
либо методом, то есть после нажатия кнопки не только перезагрузиться страница, но и
отработает метод, подвязанный на событие Click.

У кнопки имеются следующие интересные свойства:

1. CssClass — указывает имя класса стилей из CSS-файла.


2. Text — надпись на кнопке.

620
3. Enabled — когда true кнопка активна. То есть её возможно нажать.
4. EnableViewState — хранить или нет состояние элемента в ViewState.
5. OnClientClick — указывается инструкция JavaScript, которая выполнится при
нажатии по кнопке.
6. PostBackUrl — страница на которую будет выполнен постбэк (передача
параметров). Аналог action в тэге form.
7. ToolTip — всплывающая подсказка.
8. ValidationGroup — группа для проверки значений перед постбэком.
9. Visible — когда false кнопка не отображается.

Практически все перечисленные выше параметры являются общими для всех


элементов.

Посмотрев на генерируемый ASP.NET HTML-код видно как DropDownList


превращается в select, или как TextBox становится «input type="text”», но также
можно заметить и как ASP.NET формирует имена HTML элементов. То-есть
формируемые имена имеют другой ID (а не тот, что указан в коде при написании
приложения). И при задаче проверки или выполнения какого либо JavaScript кода
можно зайти в тупик, какое же будет имя элемента управления. Но об этом
позаботились. У каждого элемента есть свойство ClientID которое и содержит
генерируемое имя.
Посмотрим пример, который на JavaScript будет выводить HTML имя кнопки.
Добавим на страницу Default.aspx кнопку со следующим кодом (свойства кнопки уже
указаны в коде):

<p>
<asp:Button ID="Button1" runat="server" Text="Узнать имя этой кнопки"
OnClientClick="alert(this.name);" />
</p>

Нажатие этой кнопки будет выводить сгенерированное имя:

Создаём TabControl:

В Windows Forms приложениях есть элемент управления TabControl который


представляет собой панель с закладками. К сожалению в веб-приложениях такого нет,
но можно сделать. Для этого будем использовать элементы: Menu (панель элементов,
группа Переходы), MultiView, View. Вначале на новой строке в конструкторе
разместим Menu, после добавления нажмём на смарт-тэг «Правка элементов меню...».
Создаём три вкладки:

621
Рис. 3. 1. Редактор элементов меню: «Вкладка № 1»

Последовательно добавим три меню элемента с разными свойствами Text и


Value:

Первый элемент меню:


Text: Вкладка № 1
Value: 0
Второй элемент меню:
Text: Вкладка № 2
Value: 1
Третий элемент меню:
Text: Вкладка № 3
Value: 2

Применим какой-нибудь шаблон для меню. Например, Профессиональный


(смарт-тэг для элемента Menu — «Автоформат...»):

622
Меню разместим горизонтально. Нажмём мышкой на код:

<asp:Menu ... >

И на панели свойств изменим свойство:

Orientation: Horizontal

После меню добавим элемент MultiView и внутри размести последователь (три


раза) три элемента View. В каждом view что-нибудь напишем. Получим HTML-код:

<p>
<asp:MultiView ID="MultiView1" runat="server">
<asp:View ID="View1" runat="server">
Вкладка № 1
</asp:View>
<asp:View ID="View2" runat="server">
Вкладка № 2
</asp:View>
<asp:View ID="View3" runat="server">
Вкладка № 3
</asp:View>
</asp:MultiView>
</p>

Теперь проинициализируем событие нажатия на элемент меню. Для этого


дважды щёлкнем на представлении «Конструктор» на элементе управления Menu
нашей страницы. Будет создан обработчик события MenuItemClick. Запишем туда код:

protected void Menu1_MenuItemClick(object sender, MenuEventArgs e)


{
MultiView1.ActiveViewIndex = Convert.ToInt32(Menu1.SelectedValue);
}

Теперь при выборе любого меню «Вкладка № ...» будет отображена


соответствующая вкладка:

DropDownList:

Аналогом ComboBox В ASP.NET является DropDownList В коде HTML он будет


выглядеть как «select». Добавим его на страницу и свяжем с базой данных сайта.
Добавим следующий HTML-код после последнего закрывающего элемента </p>:

<p align="right">
Зарегистрированные пользователи:
</p>

Переместим курсор выделения после двоеточие и перетащим DropDownList.


После добавления нажмём на смарт-тэг «Выбрать источник данных...». В открывшемся
окне в поле Выберите источник данных выберем <Новый источник данных>. В
следующем окне выберем База данных (иконка с надписью «SQL»), в единственном
поле редактирования впишем «ASPNETDB»:

623
Рис. 3. 2. Мастер конфигурации источника данных: выбор истопника и ввод
префикса

Жмём ОК. В следующем окне в выпадающем списке выбираем


ApplicationServices, жмём «+» чтобы посмотреть строку соединения с базой:

624
Рис. 3. 3. Настроить источник данных – ASPNETDB: выбор соединения данных

Жмём Далее. В следующем окне в поле имя выбираем из списка таблицу


vw_aspnet_Users. В поле Столбцы щёлкаем по UserName:

625
Рис. 3. 4. Настроить источник данных – ASPNETDB: запрос из базы

Жмём Далее. В следующем окне можно протестировать запрос на выполнение


(получим все имена зарегистрированных пользователей). Жмём Готово, это вернёт нас
к первому, открывшемуся по цепочке окну:

626
Там уже автоматически настроятся оставшиеся поля для DropDownList (как на
кусочке рисунка выше). Жмём ОК. Теперь элемент «списка» будет отображать на
странице сайта всех зарегистрированных пользователей:

Panel:

Panel используется в качестве контейнера для нескольких элементов и может


выполнять групповые операции, например скрытия объектов. Добавим две панели и
разместим в первой текстовое поле и кнопку, а во второй панели создадим Label и
будем выводить в него текст введенный в текстовое поле После нажатия на кнопку на
странице должна быть только введённая надпись Первоначально установим свойство
Visible для второй панели как false. Всё это реализует простым HTML-кодом (элементы
были добавлены переносом, свойства изменены на панели свойств):

<p>
&nbsp;<asp:Panel ID="Panel1" runat="server" Height="50px" Width="125px">
<asp:TextBox ID="TextBox4" runat="server"></asp:TextBox>
<asp:Button ID="Button2" runat="server" OnClick="Button2_Click" Text="Нажми
меня!" /></asp:Panel>
<asp:Panel ID="Panel2" runat="server" Height="50px" Visible="False"
Width="125px">
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label></asp:Panel>
</p>

Событие Click для кнопки с Button3:

protected void Button2_Click(object sender, EventArgs e)


{
Label1.Text = TextBox4.Text;
Panel1.Visible = false;
Panel2.Visible = true;
}

Literal:

Literal используется в качестве контейнера для HTML. Иногда бывает ситуация


при которой нет возможности использовать ASP.NET элементы управления или просто
такого элемента нет. Добавляем в то место, куда хотим вывести HTML блок элемент
Literal и в свойство Text записываем HTML-код. В новый блок «p» перетаскиваем этот
элемент:

<p>
<asp:Literal ID="Literal1" runat="server"></asp:Literal>
</p>

Добавляем следующие строчки к событию Page_Load страницы:

String table = "<table border=\"1\"><tr>";


for (int i = 0; i < 10; i++) table += "<td>" + i + "</td>";
table += "</tr></table>";
Literal1.Text = table;

Этот код нарисует табличку:

627
Wizard:
При реализации многостраничной регистрации, или списка вопросов, которые не
умещались на одну страницу и прочее, возникает задача разнести вопросы по
нескольким последовательно связанным страницам. Можно делать через множество
страниц и передавть все предыдущие ответы через поля «hidden», можно через GET,
возможно, объединять элементы в Panel и затем их скрывать. В ASP.NET 2.0 появился
элемент управления Wizard. В Wizard настраивается: количество шагов, формат
кнопок, надписи на них, внешний вид и прочее. Всё что необходимо, это добавить
нужные элементы внутрь шага Wizard.
Перетащим Wizard на новую сточку на странице (новый тэг «p»). Это создаст
HTML-код:

<p>
<asp:Wizard ID="Wizard1" runat="server">
<WizardSteps>
<asp:WizardStep ID="WizardStep1" runat="server" Title="Step 1">
</asp:WizardStep>
<asp:WizardStep ID="WizardStep2" runat="server" Title="Step 2">
</asp:WizardStep>
</WizardSteps>
</asp:Wizard>
</p>

Для визуального редактирования элементов можно воспользоваться смарт-тэгом


Добавить/удалить шаги WizardStep...:

Рис. 3. 5. Редактор коллекции WizardStep: добавляем три шага и указываем Title

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

<p>
<asp:Wizard ID="Wizard1" runat="server" ActiveStepIndex="0" BackColor="#F7F6F3"
BorderColor="#CCCCCC" BorderStyle="Solid" BorderWidth="1px"
Font-Names="Verdana" Font-Size="0.8em" >
<HeaderStyle BackColor="#5D7B9D" BorderStyle="Solid" Font-Bold="True"
Font-Size="0.9em" ForeColor="White" HorizontalAlign="Left" />
<NavigationButtonStyle BackColor="#FFFBFF" BorderColor="#CCCCCC"
BorderStyle="Solid" BorderWidth="1px" Font-Names="Verdana" Font-
Size="0.8em"
ForeColor="#284775" />
<SideBarButtonStyle BorderWidth="0px" Font-Names="Verdana" ForeColor="White"
/>
<SideBarStyle BackColor="#7C6F57" BorderWidth="0px" Font-Size="0.9em"
VerticalAlign="Top" />
<StepStyle BorderWidth="0px" ForeColor="#5D7B9D" />
<WizardSteps>
<asp:WizardStep ID="WizardStep1" runat="server" Title="Шаг № 1">
<asp:TextBox ID="TextBox5" runat="server"></asp:TextBox>
</asp:WizardStep>
<asp:WizardStep ID="WizardStep2" runat="server" Title="Шаг № 2">
<asp:CheckBox ID="CheckBox1" runat="server" Text="Выдели меня!" />
</asp:WizardStep>
<asp:WizardStep ID="WizardStep3" runat="server" Title="Шаг № 3">
Готово!
</asp:WizardStep>
</WizardSteps>
</asp:Wizard>
</p>

Это создаст следующее:

Application (глобальные переменные веб-приложения):

Наверное, задач хранения глобальных переменных в ASP.NET да и вообще в веб


технологиях не так и много, но они все таки есть. Есть они в том числе и в ASP.NET
Хранятся переменные через HttpApplicationState. Итак, данные в Application
хранятся на протяжении жизни приложения ASP.NET и доступны из любого места
приложения. Об использовании Application уже было рассказано ранее в данной
лабораторной работе.

Session (глобальные переменные сессии):

Наряду с хранением переменных в Application, также можно их хранить и в


сессии пользователя. Отличие будет в том, что эти переменные будет видеть только
пользователь-владелец сессии (браузер одного ПК). Воспользуемся одним из TextBox
уже установленных на странице и добавим только код в событие Page_Load страницы:

Session["myBrowser"] = Request.Browser.Browser + " " +


Request.Browser.Version;

629
if (Session["myBrowser"] != null) { TextBox2.Text = "Ваш браузер: " +
Session["myBrowser"].ToString(); }

Организовать выход пользователя с сайта, а, следовательно, удалить все


переменные в его сессии можно с помощью статического метода Abandon. Добавим в
файл Global.asax.cs в метод Session_End следующий код:

Session.Abandon();

При запуске страницы из Internet Explorer версии 9.0 получим:

FileUpload (загрузка файлов на сервер):

FileUpload позволяет пользователю загрузить файл на сервер. Разместим этот


элемент на страница, а также рядом поместим кнопку и текстовое поле:

<asp:FileUpload ID="FileUpload1" runat="server" />


&nbsp;<asp:Button ID="Button4" runat="server" Text="Загрузить файл"
onclick="Button4_Click" />
&nbsp;<asp:Label ID="Label3" runat="server" Text="Label" Visible="False"></asp:Label>

Событие Click кнопки Button4:

String SavePath = @"D:\";


// Метод MapPath возвращает полный физический путь для виртуального пути,
который можно передать в метод.
// Например, следующий код возвращает путь к файлу с корневого каталога веб-
узла:
// String SavePath = Server.MapPath("~"); //

if (FileUpload1.HasFile)
{
SavePath += FileUpload1.FileName;
FileUpload1.SaveAs(SavePath);
Label3.Visible = true;
Label3.Text = "Файл сохранён в, путь: " + SavePath + ". Размер файла: " +
(FileUpload1.FileBytes.Length/(1024)).ToString() + "Кб";
}

В итоге, жмём кнопку обзор, выбираем любой файл и жмём Загрузить файл.
Файл копируется на диск D (это может быть корневой каталог сервера и соответственно
абсолютный путь до директории сайта на сервере). Пример работы загрузки в корневой
каталог «веб-сайта» (метод MapPath):

Файл был загружен в каталог запуска веб-приложения.

ServerVariables (узнаём параметры клиента):

Часто встречается задача определения некоторых параметров клиента,


например таких как его IP адрес, клиентское приложение, ссылка с которой клиент
пришёл на данную страницу и прочее. Вся эта информация хранится в ServerRequest

630
HTTP запроса Для получения всех параметров клиента сделаем следующее. Добавим
DropDowList на «Вкладку № 1» (изменим код вкладки View1):

<asp:View ID="View1" runat="server">


Сведения о клиенте:<br />
<asp:DropDownList ID="DropDownList2" runat="server">
</asp:DropDownList>
</asp:View>

А также перепишем событие нажатия на элемент Menu:

protected void Menu1_MenuItemClick(object sender, MenuEventArgs e)


{
MultiView1.ActiveViewIndex = Convert.ToInt32(Menu1.SelectedValue);
foreach (string key in Request.ServerVariables.AllKeys)
{
if (key == "ALL_HTTP" || key == "ALL_RAW") { }
else { DropDownList2.Items.Add(key + " = " +
Request.ServerVariables[key]); }
}
}

Данный код выводит все параметры клиента, кроме самых длинных


(записываются в одну строку):

Рис. 3. 4. Результат работы веб-приложения ASP.NET: получений сведений о клиенте на


стороне клиента

Кэширование:

Для увеличения производительности имеет смысл кэшировать некоторые


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

631
В ASP.NET кэширование добавляется с использованием директивы страницы
OutputCache. Поместим в HTML-код нашей страницы в самом верху после первой
строчки следующий код:

<%@ OutputCache Duration="10" Location="server" VaryByParam="*" %>

Duration указывает, на какой интервал времени (в секундах) кэшировать


страницу.
Location указывает, где будет храниться кэш.
VaryByParam позволяет кэшировать используя определенные алгоритмы
различия запросов. то есть ASP.NET может различать страница с данными параметрами
была сохранена ранее или на страницу поступают новые параметры которых нет в
кэше.

Откомпилируем страницу, и попробуем «побегать» по меню навигации и


нажимать F5. Время не будет менять в текстовом поле в течение 10 минут (на
серверные кнопки не распространяется).

Создаем пользовательский элемент:

При многократном использовании каких либо элементов управления, можно


создать свой собственный и использовать его в любом месте веб-приложения. Создаём.
Выполним Проект -> Добавить новый элемент...: ищем в открывшемся окне
Пользовательский веб-элемент управления. Вводим Имя: MyControl.ascx, жмём
ОК.

Открываем HTML-код элемента, перемещаем курсор на пустое место внутри кода


и последовательно перетаскиваем DropDownList, Button и TreeView (группа Переходы
панели элементов). Настраиваем «дерево» элементов TreeView (смарт-тэг «Правка
узлов...»):

632
Рис. 3. 5. Редактор узла TreeView: создаём «дерево» по своему вкусу

Для создания дочерних элементов выбираем последний элемент для которого


необходимо создать дочерний элемент и жмём Добавить дочерний узел. Для
созданий нескольких дочерних элементов в одном узле, выделенным узлом должен
быть родительский узел.
Также, изменяем стиль «дерева» (смарт-тэг «Автоформат...») на стиль «MSDN».
Основные свойства элемента TreeView которые будут также изменены (показываем
линии «дерева» и ставим CheckBox около каждого элемента:

ShowLines: True
ShowCheckBoxes: All

HTML-код пользовательского элемента будет таким:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="MyControl.ascx.cs"


Inherits="LWP18ASPNET.MyControl" %>
<asp:DropDownList ID="DropDownList1" runat="server">
</asp:DropDownList>
<asp:Button ID="Button1" runat="server" Text="Показать выделенные узлы"
onclick="Button1_Click" />
<asp:TreeView ID="TreeView1" runat="server" ImageSet="Msdn" NodeIndent="10"
ShowCheckBoxes="All" ShowLines="True">
<HoverNodeStyle BackColor="#CCCCCC" BorderColor="#888888" BorderStyle="Solid"
Font-Underline="True" />
<Nodes>
<asp:TreeNode Text="Корень № 1" Value="Корень № 1">
<asp:TreeNode Text="1, 1" Value="1, 1"></asp:TreeNode>
</asp:TreeNode>
<asp:TreeNode Text="Корень № 2" Value="Корень № 2">
<asp:TreeNode Text="2, 2" Value="2, 2"></asp:TreeNode>

633
<asp:TreeNode Text="2, 1" Value="2, 1"></asp:TreeNode>
</asp:TreeNode>
<asp:TreeNode Text="Корень № 3" Value="Корень № 3">
<asp:TreeNode Text="3, 1" Value="3, 1">
<asp:TreeNode Text="3, 1, 1" Value="3, 1, 1"></asp:TreeNode>
</asp:TreeNode>
</asp:TreeNode>
<asp:TreeNode Text="Корень № 4" Value="Корень № 4">
<asp:TreeNode Text="4, 1" Value="4, 1">
<asp:TreeNode Text="4, 1, 1" Value="4, 1, 1">
<asp:TreeNode Text="4, 1, 1, 1" Value="4, 1, 1, 1"></asp:TreeNode>
</asp:TreeNode>
</asp:TreeNode>
</asp:TreeNode>
<asp:TreeNode Text="Корень № 5" Value="Корень № 5">
<asp:TreeNode Text="5, 1" Value="5, 1">
<asp:TreeNode Text="5, 1, 1" Value="5, 1, 1">
<asp:TreeNode Text="5, 1, 1, 1" Value="5, 1, 1, 1">
<asp:TreeNode Text="5, 1, 1, 1, 1" Value="5, 1, 1, 1,
1"></asp:TreeNode>
</asp:TreeNode>
</asp:TreeNode>
</asp:TreeNode>
</asp:TreeNode>
</Nodes>
<NodeStyle Font-Names="Verdana" Font-Size="8pt" ForeColor="Black"
HorizontalPadding="5px" NodeSpacing="1px" VerticalPadding="2px" />
<ParentNodeStyle Font-Bold="False" />
<SelectedNodeStyle BackColor="White" BorderColor="#888888" BorderStyle="Solid"
BorderWidth="1px" Font-Underline="False" HorizontalPadding="3px"
VerticalPadding="1px" />
</asp:TreeView>

Событие Click для кнопки Button1 пользовательского элемента будет добавлять


в DropDownList значения всех выделенных элементов в TreeView:

protected void Button1_Click(object sender, EventArgs e)


{
DropDownList1.Items.Clear();
for (int i = 0; i < TreeView1.Nodes.Count; i++) Check(TreeView1.Nodes[i]);
}

private void Check(TreeNode tn)


{
if (tn.Checked) DropDownList1.Items.Add("[" + tn.Value + "] ");
if (tn.ChildNodes.Count > 0)
{
for (int i = 0; i < tn.ChildNodes.Count; i++) Check(tn.ChildNodes[i]);
}
}

Зарегистрируем элемент управления, используя директиву Register


(регистрировать необходимо на страницах, на которых будем использовать наш
элемент) на странице Default.aspx:

<%@ Register Src="~/MyControl.ascx" TagPrefix="My" TagName="SpecialControl" %>

И используем элемент управления на странице:

<p><my:specialcontrol ID="My1" runat="server" /></p>

634
Что получилось в итоге:

Cookie:

Мы уже рассматривали как можно хранить переменные отдельного пользователя


в сессии, но чаще возникает желание хранить нечто, что сразу будет ассоциироваться с
пользователем ранее посещавшим сайт. За дело берутся «cookie». «Куки» сохраняются
не в сессии, а на компьютере пользователя. При первом обращении к сайту мы
посылаем «кук» на машину клиента; при следующем заходе (или когда нам
потребуется) мы можем запросить кук и идентифицировать пользователя или
установить на сайте настройки, которые пользователь выбирал в прошлом и прочее.
Итак, пользователь вводит в текстовое поле свои данныеи передаёт информацию на
сервер. В ответ на это сервер отправляем ему кук с этой информацией. Воспользуемся
кнопкой Button2 и полем TextBox4 (которые были созданы для работы с Panel):

Текст события Click кнопки Button2 перепишем, добавив строчку кода, которая
отправляет написанное в TextBox4 в cookie-файл пользователя:

protected void Button2_Click(object sender, EventArgs e)


{
Panel1.Visible = false;
Panel2.Visible = true;
Response.Cookies["TextC"].Value = TextBox4.Text;
Label1.Text = TextBox4.Text;
}

Добавим следующий код в событие Page_Load:

Response.Cookies["Text"].Expires = DateTime.Now.AddHours(1); // Время жизни


cookie

if (Request.Cookies["TextC"] != null) // Получаем значение cookie


{
Panel2.Visible = true;
Label1.Text = Request.Cookies["TextC"].Value;
TextBox4.Text = Request.Cookies["TextC"].Value;
}

Код при загрузке страницы проверяет наличие cookie с полем TextC, если не
пустое и существует, заполняем TextBox4. Также этот код устанавливает «время

635
жизни» cookie с нашего сайта (1 час). В результате, вводим данные в текстовое поле,
открываем ещё одну страницу браузера и видим введённое нами число в нужном поле:

Для проверки можно переключиться между страницами сайта и убедиться, что


другие элементы управления сбрасываются после обновления страницы.

Web.config:

В ASP.NET реализован интересный механизм, который позволяет перекрывать


практически все настройки веб сервера: файл Web.config. В данном файле
описываются все свойства веб приложения. Благодаря этому файлу больше не
потребуется обращаться, например, к хостинг провайдеру, где лежат файла сервера
для изменения параметров веб-сервера, всё можно описать в этом файле. Файл
представляет собой XML документ, рассмотрим некоторые секции.

Код ниже, позволяет указать выводить или нет отладочную информацию на


страницу при возникновении ошибки (секция <system.web> ... </system.web>):

<compilation debug="true" targetFramework="4.0" />

Код ниже устанавливает способ авторизации:

<authentication mode="Forms">
<forms loginUrl="~/Account/Login.aspx" timeout="2880" />
</authentication>

Код ниже устанавливает режим хранения сессии:

<sessionState mode="StateServer"></sessionState>

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

<globalization
fileEncoding="utf-8"
requestEncoding="utf-8"
responseEncoding="utf-8"
culture="ru-RU"
uiCulture="ru-RU" />

Код ниже позволяет настраивать стандартные ошибки HTTP:

<customErrors mode="RemoteOnly" defaultRedirect="GenericErrorPage.htm">


<error statusCode="403" redirect="NoAccess.htm" />
<error statusCode="404" redirect="FileNotFound.htm" />
</customErrors>

И последнее в данной части работы. Контроль ошибок ввода на стороне


пользователя:

Зачем? Можно проверять все введённые пользователем данные и на сервере, но


лучше этого избегать: во-первых затрачиваются лишние ресурсы сервера, во-вторых

636
пользователю лучше сразу показать, где ошибка, экономя его время. Итак, на стороне
клиента правильность заполнения кода можно проверить на JavaScript. В ASP.NET есть
несколько элементов управления, которые генерируют JavaScript с целью проверки
правильного заполнения формы. Находятся в группе Проверка:

Начнём:

RequiredFieldValidator проверяет, заполнено поле или нет. Свойства элемента


таковы:

 ControlToValidtae — имя элемента, который необходимо проверить


 Text — сообщение, об ошибке которое, покажется, в случае если поле не
заполнено.
 ErrorMessage — сообщение, об ошибке которое, покажется, в списке всех
ошибок (summary).
 ValidationGroup — задаёт имя группы проверки. Актуально если на странице
две кнопки, которые могут делать постбэк. И две группы элементов, которые
хотим проверять.

Добавим на страницу следующий HTML-код (элемент проверки расположен после


текстового поля, шрифт ошибки изменён):

<p>
<asp:TextBox ID="TextBox6" runat="server"></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server"
ErrorMessage="RequiredFieldValidator" ControlToValidate="TextBox6"
Text="[ пожалуйста, заполните текстовое поле ]" Font-Size="Large"
ForeColor="#FF3300" Font-Bold="True"></asp:RequiredFieldValidator><br />
<asp:Button ID="Button5" runat="server" Text="Выполнить" />
</p>

Запустим, нажмём серверную кнопку «Выполнить», не заполняя текстовое поле:

Второй элемент проверки: RangeValidator проверяет, входит ли введенное


значение в диапазон. Ключевые свойства:

 MaximumValue — максимальное значение.


 MinimumValue — минимально значение.

Изменим добавленный HTML-код:

<p>
<asp:TextBox ID="TextBox6" runat="server"></asp:TextBox>

637
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server"
ErrorMessage="RequiredFieldValidator" ControlToValidate="TextBox6"
Text="[ пожалуйста, заполните текстовое поле ]" Font-Size="Large"
ForeColor="#FF3300" Font-Bold="True">
</asp:RequiredFieldValidator>
<br />
<asp:RangeValidator ID="RangeValidator1" runat="server"
ErrorMessage="RangeValidator" ControlToValidate="TextBox6"
Text="[ значение поле не в диапазоне от 0 до 100 ]" Font-Size="Large"
ForeColor="#FF3300" Font-Bold="True" MaximumValue="100" MinimumValue="0">
</asp:RangeValidator>
<br />
<asp:Button ID="Button5" runat="server" Text="Выполнить" />
</p>

RegularExpressionValidator проверяет заполнение поля по шаблону


(регулярному выражению). Очень удобно, например, проверять правильность
введенного E-mail адреса, тем более что в Visual Studio уже есть шаблон «e-mail».
Свойства:

 ValidationExpression — регулярное выражение проверки.

Рис. 3. 6. Редактор регулярных выражений: редактируем свойство


ValidationExpression и выбираем выражение

Добавим новый код на страницу:

<p>
<asp:TextBox ID="TextBox7" runat="server"></asp:TextBox>
<asp:RegularExpressionValidator ID="RegularExpressionValidator1" runat="server"
ErrorMessage="RegularExpressionValidator" ControlToValidate="TextBox7"
Text="[ пожалуйста, введите корректный адрес E-mail ]" Font-Size="Large"
Font-Bold="True" ForeColor="Red"
ValidationExpression="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"></
asp:RegularExpressionValidator>
<br />
<asp:Button ID="Button6" runat="server" Text="Выполнить" />
</p>

638
CompareValidator сравнивает значения введённые в разные элементы
управления. Таким образом, можно проверить правильность ввода пароля в два поля.
Свойства:

 ControlToCompare — элемент для сравнения.

<p>
<asp:TextBox ID="TextBox8" runat="server"></asp:TextBox>
<asp:TextBox ID="TextBox9" runat="server"></asp:TextBox>
<asp:CompareValidator ID="CompareValidator1" runat="server"
ErrorMessage="CompareValidator" ControlToValidate="TextBox8"
ControlToCompare="TextBox9" Font-Size="Large" Font-Bold="True"
Text="[ значения в текстовых полях не совпадают ]"
ForeColor="Red"></asp:CompareValidator>
<br />
<asp:Button ID="Button7" runat="server" Text="Выполнить" />
</p>

CustomValidator позволяет проверить то, что не смог ни один предыдущий


компонент. Для этого нужно написать стороннюю функцию роверки на стороне
клиента. Свойства:

 ClientValidationFunction — скрипт на стороне клиента.

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

 DisplayMode — определяет стиль отображения списка.


 HeaderText — определяет заголовок списка.
 ShowMessageBox — показывает сообщение об ошибке в JavaScript-окне.

<asp:ValidationSummary ID="ValidationSummary1" runat="server"


Font-Bold="True" Font-Size="Large" ForeColor="Red"
HeaderText="Ошибки ввода:" ShowMessageBox="True" />

Также «облагородим» свойство ErrorMessage остальных «валидаторов» (именно


этот текст ловит ValidationSummary, например для CompareValidator:

<asp:CompareValidator ID="CompareValidator1" runat="server"


ErrorMessage="Значения в текстовых полях не совпадают."
ControlToValidate="TextBox8"
ControlToCompare="TextBox9" Font-Size="Large" Font-Bold="True"
Text="[ значения в текстовых полях не совпадают ]"
ForeColor="Red"></asp:CompareValidator>

639
И всплывающее окно:

С этой частью работы закончили.

4. Модификация веб-приложения ASP.NET: AJAX

Введение в AJAX:

AJAX (Asynchronous JavaScript And XML — асинхронный JavaScript «плюс» XML),


обозначает подход к созданию веб-приложений, при котором после первичного вызова
веб-страницы она получает возможность обмена данных с сервером и отображения
данных без необходимости своей перегрузки.
Это обеспечивают ряд механизмов, основной из которых — движок AJAX. Это
посредник между браузером (загруженной в него страницей) и сервером, способный
передавать данные серверу и принимать их от него. Окончательно этот механизм
сформировался в 1998 году, когда в Internet Explorer 5.0 были внедрены новые
ActiveX-объекты, и, в частности, XMLHttpRequest-объект. Именно XMLHttpRequest
приобрел наибольшую популярность и поддержку в других браузерах (Mozilla Firefox,
начиная с версии 1.0, Opera, начиная с версии 8.0, Safari...).
Движок AJAX (XMLHttpRequest) производит необходимые запросы асинхронно,
обычно при помощи XML, не прерывая взаимодействия пользователя с приложением.
Ответ сервера — это либо текстовая строка, либо XML документ.

XMLHttpRequest-объект создаётся для страницы, именно как объект, в момент её


загрузки или при необходимости обмена данных с сервером. Созданный объект имеет
свои методы и свойства, доступные коду JavaScript-скриптов страницы. Вот основные:

640
open('Тип запроса «GET» или «POST»', 'URL страницы", 'исполнение запроса —
«True» — асинхронное исполнение', 'username', 'pasword') — создаёт запрос к серверу.

send('content') — отправляет запрос на сервер. В качестве значений может быть


null или данные для запроса «POST» или пустая строка.
onreadystatechange — событие, возникающее при смене состояния объекта:
 0 — (uninitialized) — запрос не отправлен;
 1 — (loading) — объект инициализирован;
 2 — (loaded) — получен ответ от сервера;
 3 — (interactive) — соединение с сервером активно;
 4 — (complete) — объект завершил работу;
responseText — взять данные от сервера в виде строки.
responseXML — взять данные от сервера в виде XML;
status — свойство статус HTTP-ответа (например, 404 — страница не найдена на
сервере).
Все свойства и методы объекта, доступны из JavaScript скриптов. Таким образом, AJAX,
как её трактуют сами разработчики, это несколько технологий, объединённых в новое
направление:
 стандартизованное представление данных с использованием XHTML и CSS;
 динамическое отображение и взаимодействие при помощи DOM (Document
Object Model);
 обмен и управление данными через XML и XSLT;
 асинхронные получение данных с использованием XMLHttpRequest;
 JavaScript, как язык, связывающий все компоненты.

Подготовка к реализации функциональности AJAX:

Создаём новую страницу («Веб-форма, использующая главную страницу») с


именем Ajax.aspx. В качестве основного шаблона выбираем Site.Master. HTML-код
добавленной страницы будет таким:

<%@ Page Title="Страница с AJAX" Language="C#" MasterPageFile="~/Site.Master"


AutoEventWireup="true" CodeBehind="Ajax.aspx.cs" Inherits="LWP18ASPNET.Axaj" %>
<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>
Работа с технологией AJAX!
</h2>
<p align="center">
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>&nbsp;
<asp:Button ID="Button1" runat="server" Text="Серверная кнопка" />
</p>
</asp:Content>

Страница содержит одну кнопку, один Label (в качестве базовых элементов


управления).
Добавим страницу в меню (файл Site.Master). Найдём там:

<asp:MenuItem NavigateUrl="~/Page.aspx" Text="Страница"/>

Добавим после:

<asp:MenuItem NavigateUrl="~/Ajax.aspx" Text="AJAX"/>

Теперь добавим код для события Page_Load файла Ajax.aspx.cs:

if (!IsPostBack) { Label1.Text = "Текст при первой загрузке страницы


Default.aspx"; }
else { Label1.Text = "Текст при перегрузке страницы"; }

641
Этот код будет сигнализировать о том, что страница не перегружается при
использовании AJAX-вызовов. Загрузим веб-сайт, текст в Label1 будет «Текст при
первой загрузке страницы Default.aspx», нажимаем серверную кнопку — «Текст при
перегрузке страницы». По смене текста будем судить о том, перегружалась или нет
страница:

Рис. 4. 1. Результат работы веб-приложения ASP.NET: загружена «Страница с AJAX»

Среда разработки по умолчанию умеет работать со следующими компонентами


AJAX:

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


пользователя, а именно AJAX-часы.

Реализации таких часов: вначале добавим простой Label. Затем, на странице,


которая будет использовать эту технологию должен быть обязательно элемент
управления ScriptManager (группа AJAX-расширения панели элементов). Добавляем
его на страницу. Добавляем компонент UpdatePanel. При постбэке из этого элемента
будет обновляться только эта часть, то есть элементы внутри UpdatePanel. Внутрь
UpdatePanel необходимо добавить тэг ContentTemplate и затем в него уже добавлять
компоненты (добавим один TextBox). Добавим на форму компонент UpdateProgress,
отображаться который будет, если запрос на обновление UpdatePanel будет
происходить дольше, чем указано в свойстве DisplayAfter. Ну и для полноты
компонент Timer, который будет перезагружать UpdatePanel через интервал указанный
в свойстве Interval (в нашем случае это будет одна секунда):

</p>
<p>
<asp:Label ID="Label2" runat="server" Text="Label"></asp:Label>
</p>
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<div>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate >
<asp:Timer ID="Timer1" runat="server" Interval="1000">
</asp:Timer>
<asp:TextBox ID="TextBox1" runat="server" ReadOnly="True"></asp:TextBox>
</ContentTemplate>
</asp:UpdatePanel>
<asp:UpdateProgress ID="UpdateProgress1" runat="server">

642
<ProgressTemplate>
Обновление...
</ProgressTemplate>
</asp:UpdateProgress>
</div>

Так это будет выглядеть в представлении «Конструктор»:

Теперь код события Page_Load, который будет реализовывать функции часов, и


обновлять TextBox через каждую секунду, посредством всех добавленных элементов
(Label2 же будет неизменным и будет содержать время старта страницы, так как
находится вне UpdatePanel):

Label2.Text = DateTime.Now.ToString();
TextBox1.Text = DateTime.Now.ToString();

Запускаем, следим за текстом в TextBox (временем) и за текстом в Label1 (не


получили ли мы «PostBack» случаем):

Рассмотрим несколько простых примеров с действиями. Начнём с самого


базового и часто-описываемого: загрузка данных из текстового файла. Будем
применять отдельный класс.

Загрузка данных из одного файла:

Создаём в веб-проекте новый текстовый файл AJAX_Text1.txt и заносим туда


любой текс, например: «Здесь расположен текст первого текстового файла
AJAX_Text1.txt!». Для создания файла выполняем Проект -> Добавить новый
элемент...: в открывшемся окне ищем группу Данные и далее Текстовый файл:

643
Рис. 4. 2. Добавление нового элемента – LWP18ASPNET: Текстовый файл

Сохраняем текст в файле:

Теперь, необходимо создать дополнительный класс для работы AJAX и


исполнения загрузки из этого файла. Будем использовать HttpHandler.
ASP.NET обрабатывает запросы HTTP с помощью обработчика HttpHandler,
который доступен по умолчанию для всех запрашиваемых страниц c расширением
*.aspx и служб (*.asmx). HttpHandlers — это классы, реализующие интерфейсы
IHttpHandler и IHttpAsyncHandler и, по существу, служат ISAPI-фильтром, обработки
HTTP-запросов. Запросы могут обрабатываться как синхронно (интерфейс
System.Web.IHttpHandler) — HttpHandler возвращает управление по завершению
обработки запроса или асинхронно (интерфейс System.Web.IHttpAsyncHandler) —
путём запуска процессов обработки и возврата управления. Иначе — HttpHandler’ы
могут передавать выполнение запроса другим классам или же сами обрабатывают
запрос и возвращают результат клиенту.
Важной особенностью ASP.NET является то, что HttpHandler может быть создан
разработчиком для выполнения своих конкретных задач.
Для того, чтобы любой класс мог выполнять функции HttpHandler’а, необходимо
реализовать в нём интерфейс System.Web.IHttpHandler или
System.Web.IHttpAsyncHandler. Можно также создавать экземпляр обработчика
HttpHandler с помощью класса, реализующего интерфейс IHttpHandlerFactory.
Интерфейсы System.Web.IHttpAsyncHandler и System.Web.IHttpHandler должны
включать методы ProcessRequest (обработчик запроса), свойства IsReusable
(поддержка организация пулов). Для интерфейса System.Web.IHttpAsyncHandler
требуются дополнительно методы BeginProcessRequest и EndProcessRequest
(инициализация и завершение асинхронных вызовов).

Выполним: Проект -> Добавить класс... (Shift+Alt+C). Имя вводим как


AJAX.cs. Вставляем туда код:

644
using System;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using System.Drawing;
using System.IO;
using System.Drawing.Imaging;

public class AJAX : IHttpHandler


{
#region Члены IHttpHandler
public bool IsReusable
{
get { return true; }
}

public void ProcessRequest(HttpContext context)


{
HttpRequest Request = context.Request;
HttpResponse Response = context.Response;
// Здесь будет код обработки запроса
}
#endregion
}

Подготовим «почву» для работы AJAX. Откроем файл Site.Master и найдём


строчки:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>


<title></title>

Добавим после:

<script language = "javascript" type="text/javascript">


function getDataFromServer(dataSource, targetControl)
{
}
</script>

Добавим на страницу Ajax.aspx ещё один серверный Label и Input (Buton). Для
события onclick кнопки запишем вызов некоторой функции
getDataFromServer('AJAX_Text1.txt', 'Label3') при нажатии кнопки. Задача
функции — вывести данные в Label3 из файла без перегрузки страницы. HTML-код
будет таким:

<p align="center">
<span class="bold" style="color:Green">[ реализация технологии AJAX: загрузка
текста из файла ]</span><br />
<asp:Label ID="Label3" runat="server" Text="Здесь будет выводимый
текст"></asp:Label><br />
<input type="button" id="Button2" value="Загрузить текст из AJAX_Text1.txt"
onclick="getDataFromServer('AJAX_Text1.txt', 'MainContent_Label3')"/>
</p>

Заполняем код скрипта:

645
var myAjaxObject = false;
if (window.XMLHttpRequest) {
myAjaxObject = new XMLHttpRequest();
}
else {
if (window.ActiveXObject) {
myAjaxObject = new ActiveXObject("Microsoft.XMLHTTP");
}
}

if (myAjaxObject) {
myAjaxObject.open("GET", dataSource);
myAjaxObject.onreadystatechange =
function () {

if (myAjaxObject.readyState == 4 && myAjaxObject.status == 200) {


var targetObj = document.getElementById(targetControl);
targetObj.innerHTML = myAjaxObject.responseText;
delete myAjaxObject;
myAjaxObject = null;
}
}
myAjaxObject.send(null);
}

Поясним. Первое, что добавляем в скрипт: создание XMLHttpRequest-объекта.


Создание объектов различно для браузеров Mozilla Firefox, Opera, Safari и
некоторых других (new XMLHttpRequest()) и Internet Explorer (new
ActiveXObject("Microsoft.XMLHTTP")), что и отражает данный код. Далее, после
проверки того, что объект создан, мы создаём запрос к серверу (метод «open»). К
событию, возникающему при смене состояния объекта (onreadystatechange),
добавляем безымянную функцию, задачей которой будет слежение за состоянием
объекта (нас интересует завершение получения данных):

if (myAjaxObject) {
myAjaxObject.open("GET", dataSource);
myAjaxObject.onreadystatechange =
function () {
}
}

В теле функции определяем, что загрузка данных завершена (readyState == 4)


и обмен прошёл без ошибок (status == 200). Осталось получить текст
(myAjaxObject.responseText) и присвоить его свойству innerHTML элемента Label3:

if (myAjaxObject.readyState == 4 && myAjaxObject.status == 200) {


var targetObj = document.getElementById(targetControl);
targetObj.innerHTML = myAjaxObject.responseText;
delete myAjaxObject;
myAjaxObject = null;

И последнее: отсылка запроса на сервер:

myAjaxObject.send(null);

В результате имеем работающий код, выполняющий динамическое чтение


данных из текстового файла:

646
После нажатия кнопки «Загрузить текст из AJAX_Text1.txt» видим, что страница
не обновлялась (сигнализатор: текст в Label1 на странице).

Используем класс AJAX.cs:

Расширим текст кода файла класса AJAX.cs. Найдём:

HttpResponse Response = context.Response;


// Здесь будет код обработки запроса

Добавим после:

// Чтение файла
String s = AppDomain.CurrentDomain.BaseDirectory + (@"AJAX_Text1.txt");
byte[] bText;
using (FileStream filestream = new FileStream(s, FileMode.Open, FileAccess.Read))
{
int bufSize = (int)filestream.Length;
bText = new byte[bufSize];
filestream.Read(bText, 0, (int)filestream.Length);
}
// Так мы вновь потеряем кириллицу
// Response.BinaryWrite(bText);
// А так нет
s = System.Text.Encoding.UTF8.GetString(bText);
Response.Write(s);

Теперь нужно создать файл содержащий скрипт из предыдущего примера.


Добавим в наш проект одну пустую форму (Форма Web Forms) с именем
Ajax_Script.aspx и следующим простым кодом (заменим код созданного файла):

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Ajax_Script.aspx.cs"


Inherits="LWP18ASPNET.Ajax_Script" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"


"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script language = "javascript" type="text/javascript">
function getDataFromServer(dataSource, targetControl) {
var myAjaxObject = false;
if (window.XMLHttpRequest) {
myAjaxObject = new XMLHttpRequest();
}
else {
if (window.ActiveXObject) {
myAjaxObject = new ActiveXObject("Microsoft.XMLHTTP");
}
}

647
if (myAjaxObject) {
myAjaxObject.open("GET", dataSource);
myAjaxObject.onreadystatechange =
function () {

if (myAjaxObject.readyState == 4 && myAjaxObject.status == 200) {


var targetObj = document.getElementById(targetControl);
targetObj.innerHTML = myAjaxObject.responseText;
delete myAjaxObject;
myAjaxObject = null;
}
}
myAjaxObject.send(null);
}
}
</script>
</head>
</html>

Теперь организуем «регистрацию» нашего класса и привязку его к скрипту.


Откроем корневой Web.config, найдём там

<system.web>

Добавим после:

<httpHandlers>
<add verb="GET" path="Ajax_Script.aspx" type="AJAX" />
</httpHandlers>

И наконец, создаём «интерфейс» на странице Ajax.aspx:

<p align="center">
<span class="bold" style="color:Green">[ реализация технологии AJAX: загрузка
текста из файла, используя класс ]</span><br />
<asp:Label ID="Label4" runat="server" Text="Здесь будет выводимый
текст"></asp:Label><br />
<input type="button" id="Button3" value="Загрузить текст из AJAX_Text1.txt"
onclick="getDataFromServer('Ajax_Script.aspx', 'MainContent_Label4')"/>
</p>

Выбираем загружаемую информацию:

Создаём ещё один текстовый файл с именем AJAX_Text2.txt и текстом «Здесь


расположен текст первого текстового файла AJAX_Text2.txt!».

Отредактируем ради новой функциональности код класс AJAX.cs, изменим всю


функцию ProcessRequest следующим образом:

public void ProcessRequest(HttpContext context)


{
HttpRequest Request = context.Request;
HttpResponse Response = context.Response;
// Здесь будет код обработки запроса
String id = string.Empty;
// Извлечение параметров

648
id = Request.QueryString.Get(0);

if (!string.IsNullOrEmpty(id))
{
if (id == "1") { id = AppDomain.CurrentDomain.BaseDirectory +
(@"AJAX_Text1.txt"); }
else { id = AppDomain.CurrentDomain.BaseDirectory + (@"AJAX_Text2.txt"); }
}
else { return; }
byte[] bTextid;
using (FileStream filestream = new FileStream(id, FileMode.Open,
FileAccess.Read))
{
int bufSize = (int)filestream.Length;
bTextid = new byte[bufSize];
filestream.Read(bTextid, 0, (int)filestream.Length);
}
id = System.Text.Encoding.UTF8.GetString(bTextid);
Response.Write(id);
}

Также подправим код для предыдущего примера, найдём в файле Ajax.aspx:

onclick="getDataFromServer('Ajax_Script.aspx', 'MainContent_Label4')"/>

Изменим на:

onclick="getDataFromServer('Ajax_Script.aspx?id=1', 'MainContent_Label4')"/>

И добавим новый HTML-код (в тэге «p»):

<p align="center">
<span class="bold" style="color:Green">[ реализация технологии AJAX: загрузка
текста из файла, используя класс с выбором ]</span><br />
<asp:Label ID="Label5" runat="server" Text="Здесь будет выводимый
текст"></asp:Label><br />
<input type="button" id="Button4" value="Загрузить текст из AJAX_Text1.txt"
onclick="getDataFromServer('Ajax_Script.aspx?id=1', 'MainContent_Label5')"/>
<input type="button" id="Button5" value="Загрузить текст из AJAX_Text2.txt"
onclick="getDataFromServer('Ajax_Script.aspx?id=2', 'MainContent_Label5')"/>
</p>

Выбор и отображение изображений:

Создаём новый класс AJAX_Image.cs с кодом:

using System;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using System.Drawing;

649
using System.IO;
using System.Drawing.Imaging;

public class AJAX_Image : IHttpHandler


{
#region Члены IHttpHandler
public bool IsReusable
{
get { return true; }
}

void IHttpHandler.ProcessRequest(HttpContext context)


{
HttpRequest Request = context.Request;
HttpResponse Response = context.Response;
string s = string.Empty;
s = Request.QueryString.Get(0);
if (!string.IsNullOrEmpty(s))
{
if (s == "1") { s = AppDomain.CurrentDomain.BaseDirectory +
(@"Image1.jpg"); }
else{ s = AppDomain.CurrentDomain.BaseDirectory + (@"Image2.jpg"); }
}
else { s = AppDomain.CurrentDomain.BaseDirectory + (@"Image1.jpg"); }
using (MemoryStream memorystream = new MemoryStream())
{
Bitmap bitmap = null;

try
{
bitmap = new Bitmap(s);
bitmap.Save(memorystream, ImageFormat.Png);
byte[] b = memorystream.GetBuffer();
// Формат файла рисунка может быть отличен от исходного файла
Response.ContentType = "image/png";
Response.BinaryWrite(b);
bitmap.Dispose();
}
catch (Exception) { }
memorystream.Dispose();
}
}
#endregion
}

Регистрируем класс в Web.config (в блоке httpHandlers):

<add verb="GET" path="Ajax_Script_Image.aspx" type="AJAX_Image" />

Создаём копию страницы со скриптом Ajax_Script_Image.aspx с кодом


(изменена одна строка):

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Ajax_Script_Image.aspx.cs"


Inherits="LWP18ASPNET.Ajax_Script_Image" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"


"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script language = "javascript" type="text/javascript">
function getImageFromServer(dataSource, targetControl) {
var myAjaxObject = false;

650
if (window.XMLHttpRequest) {
myAjaxObject = new XMLHttpRequest();
}
else {
if (window.ActiveXObject) {
myAjaxObject = new ActiveXObject("Microsoft.XMLHTTP");
}
}

if (myAjaxObject) {
myAjaxObject.open("GET", dataSource);
myAjaxObject.onreadystatechange =
function () {

if (myAjaxObject.readyState == 4 && myAjaxObject.status == 200) {


var targetObj = document.getElementById(targetControl);
targetObj.innerHTML = "<img src= " + dataSource + ">";
delete myAjaxObject;
myAjaxObject = null;
}
}
myAjaxObject.send(null);
}
}
</script>
</head>
</html>

Тот же скрипт помещаем в Site.Master после предыдущего добавленного в этот


файл скрипта. Не забудем это сделать, иначе функция JavaScript будет не определена!

И наконец, создаём интерфейс для реализации выбора и отображения


изображения в файле Ajax.aspx:

<div align="center" id="iddivforimage"></div>


<div align="center">
<select id="Select1"
onchange="getImageFromServer('Ajax_Script_Image.aspx?
='+this.options[this.selectedIndex].value,
'iddivforimage')">
<option value="1">Рисунок № 1</option>
<option value="2">Рисунок № 2</option>
</select>
</div>

Добавим в корень проекта два изображения с именами Image1.jpg и


Image2.jpg (импортируем извне). Например, в данном случае были использованы два
изображения:

Первое:

Второе:

651
«AJAXML»:

Перейдём непосредственно к использованию XML-данных. Как видно из


рассмотренных свойств и методов объекта XMLHttpRequest, он имеет специальное
свойство для трактовки данных, полученных от сервера, как XML-данные. Это свойство,
доступное для чтения — responseXML. Далее, JavaScript имеет методы для разбора
XML-файлов.
Следующий пример демонстрирует простейшее использование совместной
работы AJAX и JavaScript при получении и разборке XML-данных с сервера без
перегрузки страницы.

Создаём простой XML-файл (XML-файл — в окне добавления нового объекта)с


именем Data.xml и поместим его в директорию веб-проекта. Структура файла будет
такой:

<?xml version="1.0" standalone="yes" ?>


<sites>
<site comments="World Wide Web Consortium (W3C)">http://www.w3.org/</site>
<site comments="Сайт корпорации Microsoft">http://www.microsoft.com</site>
<site comments="Russian on the Net">http://www.ru</site>
</sites>

Создаём новый файл скрипта Ajax_Script_XML.aspx с кодом:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Ajax_Script_XML.aspx.cs"


Inherits="LWP18ASPNET.Ajax_Script_XML" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"


"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script language="javascript" type="text/javascript">
function getXMLFromServer(dataSource, targetControl) {
var myAjaxObject = false;

if (window.XMLHttpRequest) { myAjaxObject = new XMLHttpRequest();}


else { if (window.ActiveXObject) { myAjaxObject = new
ActiveXObject("Microsoft.XMLHTTP"); } }

if(myAjaxObject)
{
myAjaxObject.open("GET", dataSource);
myAjaxObject.onreadystatechange =
function()
{

if (myAjaxObject.readyState == 4 && myAjaxObject.status == 200)


{
var xmlDocument = myAjaxObject.responseXML;

652
var i;
var points = xmlDocument.getElementsByTagName("site");
var html='';

for (i = 0; i < points.length; i++)


{
html=html+'<a href="'+points[i].firstChild.data+'"
target="blank">'+
points[i].getAttribute ('comments')
+'</a><br/>';
}
var targetObj = document.getElementById(targetControl);
targetObj.innerHTML = html + "";
}
}
myAjaxObject.send(null);
}
}
</script>
</head>
</html>

Не забываем поместить код скрипта (<script> ... </script>) в тэг head файла
Site.Master.

Создаём новый класс AJAX_XML.cs с кодом:

using System;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using System.Drawing;
using System.IO;
using System.Drawing.Imaging;

public class AJAX_XML : IHttpHandler


{
#region Члены IHttpHandler
bool IHttpHandler.IsReusable
{
get { return true; }
}

void IHttpHandler.ProcessRequest(HttpContext context)


{
HttpRequest Request = context.Request;
HttpResponse Response = context.Response;
string s = string.Empty;
s = AppDomain.CurrentDomain.BaseDirectory + (@"Data.xml");

using (FileStream filestream = new FileStream(s, FileMode.Open, FileAccess.Read))


{
// Чтение символов из файла
using (TextReader streamreader = new StreamReader(s,
System.Text.Encoding.Default))
{
s = streamreader.ReadToEnd();

653
Response.ContentType = "text/xml"; // Ключевая строка для передачи XML
Response.BinaryWrite(System.Text.Encoding.UTF8.GetBytes(s));
}
}
}
#endregion
}

Регистрируем класс в Web.config:

<add verb="GET" path="Ajax_Script_XML.aspx" type="AJAX_XML" />

Код элементов управления для файла Ajax.aspx:

<p align="center">
<span class="bold" style="color:Green">[ реализация технологии AJAX: загрузка
текста из XML-файла, используя класс ]</span><br />
<input type="button" id="Button7" value="Загрузить XML-файл"
onclick="getXMLFromServer('Ajax_Script_XML.aspx', 'iddivforxml')" /><br />
<asp:Label ID="Label7" runat="server" Text="Ниже будет выводимое
содержимое:"></asp:Label>
</p>
<div align="center" id="iddivforxml"></div>

Готово. Компилируем, проверяем работоспособность. Перед компиляцией


перемещаемся на страницу Default.aspx (должна быть активной вкладкой в среде
разработки). Перед компиляцией изменим заголовок на странице. Найдём в файле
Site.Master строчки:

<div class="title">
<h1>
Мое приложение ASP.NET (C#)
</h1>

И изменим на:

<div class="title">
<h1>
Знакомство с ASP.NET (C#)
</h1>

5. Завершающая часть

Компилируем приложение (Release) и запускаем. Оказываемся на выбранной


ранее в среде разработки странице («Домашняя страница»). Переключаемся между
страницами при помощи меню навигации.

654
Рис. 5. 1. Результат работы веб-приложения ASP.NET: общий вид страницы «Домашняя
страница»

Рис. 5. 2. Результат работы веб-приложения ASP.NET: общий вид страницы «Домашняя


страница» (продолжение)

655
Рис. 5. 3. Результат работы веб-приложения ASP.NET: общий вид страницы «Новая
страница»

Рис. 5. 4. Результат работы веб-приложения ASP.NET: общий вид страницы «AJAX»

Проверяем работоспособность всех созданных в данной лабораторной работе


элементов на всех страницах.

6. О приложении к Лабораторной работе № 18

Ресурсы (архив с рисунками Images.zip, а также XML-файл Data.xml), , а также


получившуюся программу (откомпилированный DLL-компонент LWP18ASPNET.dll для

656
веб-сервера IIS), собранную из кусков кода приведённых в данной лабораторной
работе, можно загрузить по ссылке в конце этого материала (сслыка доступна в
программном продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


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

19. Лабораторная работа № 19: Расширенная работа с GDI+

Лабораторная работа № 19: Расширенная работа с GDI+

Содержание

1. Вводная часть
2. Создание приложения Windows Forms
3. Модификация приложения Windows Forms: подготовка интерфейса и
добавление TabControl
4. Модификация приложения Windows Forms: вкладка «Просто фон!»
5. Модификация приложения Windows Forms: вкладка «Объекты и
градиент»
6. Модификация приложения Windows Forms: вкладка «Мой монитор
сломался?!»
7. Модификация приложения Windows Forms: вкладка «Векторные часы»
8. Модификация приложения Windows Forms: вкладка «Огонь!!!»
9. Модификация приложения Windows Forms: вкладка «Дождик»
10.Завершающая часть
11.О приложении к Лабораторной работе № 19

1. Вводная часть

В этой работе будет рассмотрена расширенная работа с графикой GDI+ в C#.


Фактически это «демонстрационная» лабораторная работа, призванная показать
богатство возможностей «обычной» системной графики на примере обычного Windows
Forms. А этих возможностей на поверку оказывается очень много...

Непосредственно о том, что такое GDI+ уже было рассказано ранее в


предыдущей работе практикуму, но повторимся...

Что же такое GDI+? Официальная документация скромно называет её Class-


based API, то есть основанным на классах интерфейсе прикладных программ для ОС
Windows поколения «не GDI». Так как она встроена в Windows XP и все последующие
ОС вплоть до Windows 7, её называют частью этих операционных систем. Часто
встречается также определение «библиотека» или «библиотека классов». В
действительности, предоставляемый GDI+ набор классов является тонкой оболочкой
над множеством обычных функций, реализованных в одной динамической библиотеке
GdiPlus.dll.
Итак, GDI+ — это библиотека, призванная заменить существующий уже больше
11 (или 18, в зависимости от того, как считать) лет интерфейс GDI, являющийся
графическим ядром предыдущих версий Windows. Она сочетает в себе (по крайней

657
мере, по замыслу) все достоинства своего предшественника и предоставляет множество
новых мощных возможностей. Кроме того, при её проектировании заранее ставилась
цель наименее болезненного переноса приложений на 64-битные платформы.

Само слово GDI: Graphics Device Interface, Graphical Device Interface.


«Устройство(-а) графического интерфейса» или «интерфейс графического устройства»,
но проще: «пользовательский интерфейс».

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


каждой вкладке будет демонстрация возможностей по работе с GDI+. Всё просто.

2. Создание приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

658
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP19GDIPlus — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

659
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК. мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms


сформированного средой разработки

660
Теперь, можно откомпилировать созданную программу, нажав клавишу F5

(Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms: подготовка интерфейса и


добавление TabControl

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP19Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Расширенная
работа с GDI+ (C#)

661
^ Поменяем заголовок формы (то, что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 800;
600
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Разместим на форме единственный элемент: TabControl (


), находится в группе Контейнеры панели
элементов.
Растянем этот элемент до краёв формы и измени имя элемента:

TabControl:
(Name): TB_Main
Anchor: Top, Bottom, Left, Right

Изменение последнего свойства потребуется, чтобы весь элемент менял размер


при изменении размеров формы.

То, что должно получиться в итоге, показано на рисунке ниже:

662
Рис. 3. 1. Модифицированная форма приложения и расстановка необходимых
элементов управления

4. Модификация приложения Windows Forms: вкладка «Просто фон!»

Первая вкладка TabControl будет показана первой при старте приложения. На


неё просто необходимо установить что-нибудь простое.

Для начала изменим текст самой вкладки. Выделим элемент управления и


перейдём к свойствам (панель Свойства) элемента. Нам нужно свойство TabPages:

Жмём «...» и открываем окно редактирования вкладок:

663
Рис. 4. 1. Редактор коллекции TabPage: меняем Text и Cursor

Для вкладки tabPage1 (имя не будем менять), изменим свойства:

TabPage:
Text: Просто фон!
Cursor: No

В качестве фона вкладки будет выбрано некое изображение. Пусть это будет
изображение с именем Background_Image.jpg (использованное в данной работе
изображение можно получить в ZIP-архиве по ссылке в конец этого материала). Для
добавления выполним действия: Проект -> Существующий элемент...
(Shift+Alt+A), в открывшемся окне находим изображение и жмём Добавить.

Теперь приступаем к коду вкладки. Для начальной вкладки tabPage1


проинициализируем событие Paint (панель свойств, вкладка События):

Код события такой:

private void tabPage1_Paint(object sender, PaintEventArgs e)


{
// При создании вкладки и её прорисовки, вызываем метод DoPaint

664
// Используем аргумент e для получение параметров рисования (устройста и
прямоугольной области)
DoPaint(e.Graphics, e.ClipRectangle);
}

Добавим и сам метод DoPaint в код под событие Paint:

protected void DoPaint(Graphics g, Rectangle clipBox)


{
RectangleF bounds = clipBox;
String welcome = "Расширенная работа с GDI+ (C#) :: Вкладка \"Просто фон\"";
Bitmap bg = new Bitmap("/Background_Image.gif"); // Абсолютный путь до
изображения
g.DrawImage(bg, bounds); // Рисуем и растягиваем изображение на весь фон
вкладки
// Создаём градентную заливку для текста
LinearGradientBrush brush = new LinearGradientBrush(bounds,
Color.FromArgb(130, 255, 0, 0), Color.FromArgb(255, 0, 0, 255),
LinearGradientMode.BackwardDiagonal);
// Создаём форматирование текста и помещаем его в центре вкладки (по
вертикали и горизонтали)
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
// Задаём параметры шрифта
Font font = new Font("Verdana", 48, FontStyle.Bold);
g.DrawString(welcome, font, brush, bounds, format);
}

В самом начале файла формы LWP19Main.cs добавим следующую строчку:

using System.Drawing.Drawing2D;

Всё предельно просто. Комментарии расставлены. Собственно этим кодом был


реализован простейший пример работы с GDI+.
Откомпилируем и запустим приложение (Debug). Увидим, что текст
отрисовывается без сглаживания. Это связано с тем, что в Windows Forms по
умолчанию отключается улучшенный режим отрисовки шрифтов. Включить можно,
например, так (размещать код перед кодом рисования объекта):

g.SmoothingMode = SmoothingMode.AntiAlias;

В объекте Graphics имеется также соответствующее свойство


InterpolationMode. Выигрыш в качестве в данном случае приводит к проигрышу в
скорости, поэтому при использовании режима с наивысшим качеством
InterpolationModeHighQualityBicubic медленные компьютеры могут выводить
изображения больших размеров в течение нескольких секунд (речь идет об экранном
выводе, то есть умеренно больших, так как бикубическая интерполяция изображений
полиграфического разрешения может длиться минутами и на самых современных
компьютерах)! Но только этот метод способен адекватно отображать, например
картинку при уменьшении её до 25 процентов (и менее) от оригинала. Этот режим
очень поможет различным автоматическим генераторам иконок (thumbnail)
изображений в ASP.NET.
На качество (и скорость) вывода растров также влияют некоторые другие
установки объектов Graphics. Перечислим их:

Метод Назначение

665
Как уже говорилось, позволяет указать метод устранения
SmoothingMode ступенчатости (Anti Aliasing) при выводе примитивов – линий и
геометрических фигур.

Устанавливает или отключает учет прозрачности при наложении


CompositingMode
растровых изображений.

Управляет качеством расчета цветовых компонентов при


CompositingQuality
наложении растров.

Задает метод учета смещения пикселов при интерполяции. Грубо


PixelOffsetMode говоря, определяет, являются ли координаты пикселов (или их
центров) целыми числами при расчетах.

Устанавливает позицию начальной точки при псевдосмешении


RenderingOrigin
(dithering) цветов в 8- и 16-битных режимах.

5. Модификация приложения Windows Forms: вкладка «Объекты и градиент»

Теперь поработаем с градиентами более с более близкого расстояния.


Редактируем вторую вкладку tabPage2 следующим образом:

TabPage:
Text: Объекты и градиент
Cursor: Hand

Рис. 5. 1. Редактор коллекции TabPage: меняем Text и Cursor

666
На второй вкладке разместим сначала элемент Panel (

, группа «Контейнеры» панели элементов), а


затем три кнопки. Свойства всего этого дела (имена элементов не трогаем):

Panel:
(Name): panel1
BackColor: Black
AutoScroll: True
Button:
(Name): button1
Text: Прямоугольник
Button:
(Name): Button2
Text: Треугольник
Button:
(Name): Button3
Text: Круг

Расстановка элементов выглядит примерно так:

Рис. 5. 2. Модифицированная форма приложения и расстановка необходимых


элементов управления на второй вкладке

Кнопки находятся внутри элемента Panel (поверх).

667
Открываем код нашей формы LWP19Main (файл LWP19Main.cs). Ищем:

public LWP19Main()
{
InitializeComponent();

Добавляем после:

DoubleBuffered = true;

Ищем:

public partial class LWP19Main : Form


{

Добавляем после:

// Начало: вкладка "Объекты и градиент"


// Для работы с появлением/скрытием фигур
bool bDrawFigure1 = false;
bool bDrawFigure2 = false;
bool bDrawFigure3 = false;
// Создаём цвета
Color c1 = Color.FromArgb(51, 204, 51);
Color c2 = Color.FromName("Green");
Color c3 = Color.FromArgb(0, 0, 0);
Color c4 = Color.FromArgb(51, 104, 51);
// Конец: вкладка "Объекты и градиент"

Инициализируем событие Paint для элемента Panel второй вкладки:

private void panel1_Paint(object sender, PaintEventArgs e)


{
Graphics g = panel1.CreateGraphics();
// Задаём визуализацию со сглаживанием для объекта Graphics
g.SmoothingMode = SmoothingMode.HighQuality;
// Массив точек треугольника
Point[] triangle = { new Point(80, 150), new Point(200, 50), new Point(320,
150) };
// Задаём перья с цветами c1 и c2 и толщиной в 1 пиксель
Pen pen1 = new Pen(c1, 1);
Pen pen2 = new Pen(c2, 1);
// Заливка и цвета градиента
Brush brush1 = new LinearGradientBrush(triangle[0], triangle[2], c1, c4);
Brush brush2 = new LinearGradientBrush(triangle[0], triangle[2], c3, c2);

if (bDrawFigure1)
{
// Заливаем цветом прямоугольник
g.FillRectangle(brush2, new Rectangle(100, 150, 200, 100));
}

if (bDrawFigure2)
{
// Заливаем цветом треугольник
g.FillPolygon(brush2, triangle);
}

if (bDrawFigure3)
{

668
// Заливаем цветом эллипс
g.FillEllipse(brush1, new Rectangle(175, 175, 50, 50));
}
}

И последовательно инициализируем двойным нажатием события каждой кнопки.


Событие Click для каждой из кнопок будет таким:

private void button1_Click(object sender, EventArgs e)


{
if (bDrawFigure1 == true) { bDrawFigure1 = false; }
else bDrawFigure1 = true;
panel1.Invalidate();
}

private void button2_Click(object sender, EventArgs e)


{
if (bDrawFigure2 == true) { bDrawFigure2 = false; }
else bDrawFigure2 = true;
panel1.Invalidate();
}

private void button3_Click(object sender, EventArgs e)


{
if (bDrawFigure3 == true) { bDrawFigure3 = false; }
else bDrawFigure3 = true;
panel1.Invalidate();
}

Компилируем (Debug) и запускаем. Переходим на закладку «Объекты и


градиент» и нажимаем на кнопочки... Любуемся, как появляется «домик» с круглым
окном...

6. Модификация приложения Windows Forms: вкладка «Мой монитор


сломался!»

Следующая вкладка будет реализовывать вот что: откроем её, нажмём где-
нибудь на свободном участке вкладки левую кнопку мыши и потащить курсор.
Выделенный участок инвертируется (инвертируются его цветовая палитра) даже за
пределами формы — это такое взаимодействие высокоуровневой GDI+ и
низкоуровневой GDI...

Создаём новую вкладку tabPage3. Для создания вкладки выделяем мышкой


TabControl и на панели свойств в области команд жмём «Добавить вкладку». Свойства
новой вкладки:

TabPage:
Text: Объекты и градиент
Cursor: Cross

Накидаем на вкладку различных элементов, каких не важно. В нашем случае на


вкладку было помещено две кнопки, два TextBox и один Label. Изменим свойства
элементов управления как угодно? Изменим цвет фона элемента, параметры шрифты и
прочее.

669
Рис. 6. 1. Модифицированная форма приложения и расстановка необходимых
элементов управления на третьей вкладке

Теперь, последовательно, для вкладки tabPage3 (нужно переключиться на неё в


конструкторе и выделить область самой вкладки) инициализируем три события:
MouseDown, MouseMove и MouseUp:

Код дополнительной переменной, всех событий и двух дополнительных методов


будет таким:

Rectangle frameRect; // Вкладка "Мой монитор сломался?!"

private void tabPage3_MouseDown(object sender, MouseEventArgs e)


{

if (e.Button == MouseButtons.Left)
{
// Фиксируем стартовую точку
frameRect.Location = new Point(e.X, e.Y);
frameRect.Size = new Size(0, 0);
}
base.OnMouseDown(e); // Вызываем базовый метод
}

private void tabPage3_MouseMove(object sender, MouseEventArgs e)


{

if (e.Button == MouseButtons.Left)
{
DrawFrame(); // Стираем старый прямоугольник

670
frameRect.Width = e.X - frameRect.Left;
frameRect.Height = e.Y - frameRect.Top;
DrawFrame(); // Рисуем новый прямоугольник
}
base.OnMouseMove(e); // Вызываем базовый метод
}

private void tabPage3_MouseUp(object sender, MouseEventArgs e)


{

if (e.Button == MouseButtons.Left)
{
DrawFrame(); // Стираем старый прямоугольник
frameRect.Size = new Size(0, 0);
}
base.OnMouseUp(e); // Вызываем базовый метод
}

protected void DrawFrame()


{

if (!frameRect.Size.IsEmpty)
{
Rectangle r = RectangleToScreen(frameRect);
// Собственно, инвертируем цвета
ControlPaint.FillReversibleRectangle(r, Color.FromArgb(40, 0, 0, 160));
}
}

protected override void OnLoad(EventArgs e)


{
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
base.OnLoad(e);
}

Обратим внимание на метод OnLoad: здесь идёт включение режима двойной


буферизации. Для его включения необходимо в окне, в которое производится
отрисовка (например, элементе управления или форме) установить флаги UserPaint,
AllPaintingInWmPaint и DoubleBuffer перечисления
System.Windows.Forms.ControlStyles.
Вывод графики довольно заметно ускоряется (несмотря на необходимость
дополнительного переноса на экран).

Компилируем (Debug) и запускаем. Переходим на третью вкладку и инвертируем


цвета у всего, до чего дотягивается мышка...

7. Модификация приложения Windows Forms: вкладка «Векторные часы»

Данная вкладка отображает текущее время на стилизованном «аналоговом»


циферблате (без использования изображений и сторонних ресурсов). В ней
использован большой набор примитивов: прямоугольники, эллипсы и отрезки прямых.
Для расчёта их координат используются не геометрические преобразования GDI+, а
простые вычисления. Что действительно заслуживает внимания, так это отображение
стрелок часов. Каждая стрелка рисуется всего одним вызовом метода DrawLine!

Для часиков создаём четвёртую вкладку tabPage4 со свойствами:

TabPage:

671
Text: Векторные часы
Cursor: AppStarting

Добавляем на форму таймер (Timer) со свойствами:

Timer:
(Name): timer1
Interval: 1000

Инициализируем событие первого таймера Tick:

private void timer1_Tick(object sender, EventArgs e)


{
tabPage4.Text = "Векторные часы: " + DateTime.Now.ToLongTimeString();
Invalidate();
}

Инициализируем событие Selected для главного элемента TabControl со


следующим кодом:

private void TB_Main_Selected(object sender, TabControlEventArgs e)


{
if (e.TabPage.Name == tabPage4.Name) timer1.Enabled = true;
else timer1.Enabled = false;
}

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

Инициализируем событие загрузки формы Load:

private void LWP19Main_Load(object sender, EventArgs e)


{
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
SetStyle(ControlStyles.UserPaint, true);
}

И, наконец, инициализируем событие Paint вкладки tabPage4, а также


дополнительный метод:

private Point RadialPoint(int radius, int seconds)


{
Point ptCenter = new Point(tabPage4.ClientRectangle.Width / 2,
tabPage4.ClientRectangle.Height / 2);
double angle = -((seconds - 15) % 60) * Math.PI / 30; // Вычисляем угол на
"окружности" (шаг)
Point ret = new Point(
ptCenter.X + (int)(radius * Math.Cos(angle)),
ptCenter.Y - (int)(radius * Math.Sin(angle)));
return ret;
}

private void tabPage4_Paint(object sender, PaintEventArgs e)


{
// Создаём объект часов
DateTime dt = DateTime.Now;
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
// Вычисляем центральную точку для циферблата

672
Point ptCenter = new Point(tabPage4.ClientRectangle.Width / 2,
tabPage4.ClientRectangle.Height / 2);
// Вычисляем радиус циферблата на основе размеров вкладки
int radius = Math.Min(tabPage4.ClientRectangle.Width,
tabPage4.ClientRectangle.Height) / 2;
using (LinearGradientBrush br = new
LinearGradientBrush(tabPage4.ClientRectangle, Color.White, Color.Azure,
LinearGradientMode.BackwardDiagonal))
{
//br.SetSigmaBellShape(.5f, 1.0f);
// Заполняем циферблат градиентом
g.FillEllipse(br, ptCenter.X - radius, ptCenter.Y - radius, radius * 2,
radius * 2);
}
// Рисуем эллипс цифербалата
using (Pen pen = new Pen(Color.Black))
g.DrawEllipse(pen, ptCenter.X - radius, ptCenter.Y - radius, radius * 2,
radius * 2);
// Цикл минуты
for (int minute = 0; minute < 60; minute++)
{
Point pt = RadialPoint(radius - 10, minute);
using (SolidBrush br = new SolidBrush(Color.Black))
{
if ((minute % 5) == 0)
g.FillRectangle(br, pt.X - 3, pt.Y - 3, 6, 6);
else
g.FillRectangle(br, pt.X - 1, pt.Y - 1, 2, 2);
}
}

// Рисуем стрелку часов (от центра до позиции на циферблате)


using (Pen pen = new Pen(Color.Black, 8))
{
pen.StartCap = LineCap.Flat;
pen.EndCap = LineCap.DiamondAnchor;
float[] compVals = new float[] { 0.0f, 0.2f, 0.5f, 0.7f, 0.9f, 1.0f };
pen.CompoundArray = compVals;
g.DrawLine(pen, RadialPoint(15, 30 + dt.Hour * 5 + dt.Minute / 12),
RadialPoint((int)(radius * 0.75), dt.Hour * 5 + dt.Minute / 12));
}

// Рисуем минутную стрелку


using (Pen pen = new Pen(Color.FromArgb(100, 0, 0, 0), 6))
{
pen.StartCap = LineCap.RoundAnchor;
pen.EndCap = LineCap.Round;
g.DrawLine(pen, RadialPoint(15, 30 + dt.Minute), RadialPoint((int)(radius
* 0.8), dt.Minute));
}

// Русуем секундную стрелку


using (Pen pen = new Pen(Color.FromArgb(50, 150, 50, 25), 4))
{
pen.CustomEndCap = new AdjustableArrowCap(4, 6, true);
g.DrawLine(pen, RadialPoint(20, dt.Second + 30), RadialPoint(radius - 2,
dt.Second));
}
using (SolidBrush br = new SolidBrush(Color.FromArgb(100, Color.Wheat)))
g.FillEllipse(br, ptCenter.X - 5, ptCenter.Y - 5, 10, 10);
}

673
Компилируем (Debug) и запускаем. Переходим на закладку «Векторные часы»,
чем запускаем таймер, заголовок вкладки будет отображать текущее время, а на самой
вкладке видим круглые стрелочные часы собранные из примитивов. Мило!

8. Модификация приложения Windows Forms: вкладка «Огонь!!!»

На этот раз будем имитировать горение огня (стена огня). Для создания эффекта огня
будем использовать генерацию случайных чисел каждую миллисекунду. Однако,
отображать сам огонь будем не на вкладке TabControl, а в новой форме. Это связано с
«некрасивым» обновлением вкладки при рисовании огня (возникает заметное
мерцание).

Создаём новую вкладку со свойствами:

TabPage:
Text: Огонь!!!
Cursor: No

Добавляем новую форму с именем LWP19Fire и не меняем какие-либо свойства


формы (ну, разве что значок). Добавить форму можно так: Проект -> Добавить
форму Windows... (а затем переименовать класс формы); либо использовать
стандартное добавление: Проект -> Добавить новый элемент... (Ctrl+Shift+A) и
далее указать Имя: LWP19Fire).

Редактируем код события Selected для TabControl. Добавляем туда строчки:

if (e.TabPage.Name == tabPage5.Name)
{
tabPage5.BackColor = Color.Black;
LWP19Fire Fire = new LWP19Fire();
Fire.ShowDialog();
}

Выбор данной вкладки («Огонь!!!») вызовет экземпляр модальной формы


LWP19Fire.

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


директивы using):

using System.Drawing.Imaging;

Теперь, найдём в коде формы строчки:

public partial class LWP19Fire : Form


{

Добавим после:

// Начало: вкладка "Огонь!!!"


private Bitmap buf; // Буфер для
графики
private Random random = new Random(DateTime.Now.Millisecond); // Истоник данных
(Random)
private const int width = 640;
private const int height = 320;

674
// Конец: вкладка "Огонь!!!"

Метод LWP19Fire() редактируем так:

public LWP19Fire()
{
InitializeComponent();
// Инициализируем источник для последующего использования
random = new Random();
// Устанавливаем начальные параметры вкладки
this.Text = "Расширенная работа с GDI+ (C#) :: форма для
вкладки \"Огонь!!!\"";
this.ClientSize = new Size(width, height);
this.MaximizeBox = false;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.BackColor = Color.Black;
SetStyle(ControlStyles.Opaque, true);
// Вызываем событие рисования (для метода DoFire)
this.Paint += new PaintEventHandler(this.DoFire);
// Генерируем палитру 640x480 (на основе Bimtap с 256 цветами на пиксель)
buf = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
ColorPalette pal = buf.Palette;
// Заполняем палитру, используя 64-цветные блоки:
// чёрный с красным, красный с желтым, желтый с белым, белый.
// Так как каждый диапазон составляет 64 цветов и RGB охватывает 256
значений,
// использовуем левый сдвиг (<<) по массиву
for (int i = 0; i < 64; i++)
{
pal.Entries[i] = Color.FromArgb(i << 2, 0, 0);
pal.Entries[i + 64] = Color.FromArgb(255, i << 2, 0);
pal.Entries[i + 128] = Color.FromArgb(255, 255, i << 2);
pal.Entries[i + 192] = Color.FromArgb(255, 255, 255);
}
buf.Palette = pal;
}

И добавим метод для переопределённого события Paint формы:

// Метод рисования "огня"


private void DoFire(object src, PaintEventArgs e)
{
// Блокируем растр, чтобы мы смогли записывать в него напрямую
BitmapData buflock = buf.LockBits(
new Rectangle(Point.Empty, buf.Size),
ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
// Записываем "огонь"
// Используем указатели, потому блок "unsafe"
unsafe
{
// Извлекает указатель на верхнюю строку развёртки изображения
Byte* bufdata = (Byte*)buflock.Scan0;
Byte* bufbottom = bufdata + ((height - 1) * width);
Byte* i; int v;
// Используем случайную нижней линию в качестве источника "огня"
for (int x = 0; x < width; x++)
{
*(bufbottom + x) = (Byte)random.Next(100, 255);
}
// Для каждого пикселя в изображении,
// среднее значение пикселей, один справа, один снизу
// и один в левом нижнем углу. Достигаем порог 0,
// и пишем на текущей позиции

675
for (i = bufdata; i < bufbottom; i++)
{
v = *i + *(i + 1) + *(i + height) + *(i + height - 1);
v /= 4;
if (v < 0) v = 0;
*i = (Byte)v;
}
}
// Разблокируем изображение и рисуем на его на форме
buf.UnlockBits(buflock);
e.Graphics.DrawImageUnscaled(buf, 0, 0);
this.Invalidate();
}

Компилируем (Debug) и запускаем. Переходим на закладку «Огонь!!!», любуемся


на огонь (можно любоваться вечно, не правда ли?).

9. Модификация приложения Windows Forms: вкладка «Дождик»

Последняя вкладка будет вызывать дождь. Внутри вкладки. Опять же, как и в
случае с часами и огнём, не будем использовать сторонних ресурсов. Разве что можно
добавить звуки дождя.

Для этого нам понадобиться новая вкладка со свойствами:

TabPage:
Text: Дождик
Cursor: PanSouth

Один новый таймер со свойствами:

TabPage:
(Name): timer2
Interval: 60

Один PictureBox, растянутый на всю новую вкладку со свойствами:

PictureBox:
(Name): pictureBox1
Anchor: Top, Bottom, Left, Right

Код для события Selected элемента TabControl:

if (e.TabPage.Name == tabPage6.Name) timer2.Enabled = true;


else timer2.Enabled = false;

После закрывающей скобки предыдущего события в файле LWP19Main.cs


добавим:

// Начало: вкладка "Дождик"


private static Image _raindropImage;
private readonly List<Raindrop> _raindrops;
// Конец: вкладка "Дождик"

#region Класс Raindrop


private class Raindrop
{
private static readonly Random random = new Random(DateTime.Now.Millisecond);

676
// Удаление от экрана
private float _depth;
// Положение
private PointF _location;
// Прямоугольник для отрисовки
private Rectangle _rectangleToDraw;
// Скорость
private PointF _speed;

public Raindrop(Rectangle rectangleToDraw)


{
_rectangleToDraw = rectangleToDraw;
SetRandomLoc();
}

public void UpdatePosition()


{
// Вертикальная скрость
_location.Y += _speed.Y;
// Горизонтальная скорость
_location.X += _speed.X;
if (_location.Y > _rectangleToDraw.Height)
SetRandomLoc();
}

private void SetRandomLoc()


{
// Максимально удаление
const int MAX_DEPTH = 10;
_depth = random.Next(MAX_DEPTH) + 1;
const int MIN_SPEED = 100;
const int SPEED_DISPERSION = 20;
_speed.X = 5;
// Чем дальше капля от экрана, тем она медленнее падает
_speed.Y = (MIN_SPEED + random.Next(SPEED_DISPERSION)) / _depth;
// Необходимо расширить область, в которой появляются капли
const int DY = 200;
int locX = random.Next(_rectangleToDraw.Width + DY) - DY;
int locY = -random.Next(_rectangleToDraw.Height) - _raindropImage.Height;
// Точка возникновения капли
_location = new PointF(locX, locY);
}

public void Draw(Graphics g)


{
// Масштаб текстуры
float scale = 2 / _depth;
g.DrawImage(_raindropImage, _location.X, _location.Y,
_raindropImage.Width * scale,
_raindropImage.Height * scale);
}
}
#endregion

Инициализируем событие второго таймера Tick:

private void timer2_Tick(object sender, EventArgs e)


{
// Обновляем положение капель
foreach (Raindrop raindrop in _raindrops)
raindrop.UpdatePosition();
// Перерисовываем картинку
pictureBox1.Refresh();
}

677
И событие Paint для PictureBox:

private void pictureBox1_Paint(object sender, PaintEventArgs e)


{
// Отрисовываем капли
foreach (Raindrop raindrop in _raindrops)
raindrop.Draw(e.Graphics);
}

Готово. Можно компилировать и проверять работоспособность.

10. Завершающая часть

Компилируем приложение (Release) и запускаем. После запуска на первой


вкладке видим (можно менять размер формы и наблюдать за изменениями при
рисовании):

Рис. 10. 1. Модифицированное приложение Windows Forms: вкладка «Просто фон!»

Переходим на следующую вкладку и нажимаем на кнопки:

678
Рис. 10. 2. Модифицированное приложение Windows Forms: вкладка «Объекты и
градиент»

Переходим на следующую вкладку и жмём левую кнопку мыши на пусто месте.


Ведем курсор в любой угол экрана:

679
Рис. 10. 3. Модифицированное приложение Windows Forms: вкладка «Мой монитор
сломался?!»

На следующей вкладке запускаются часы:

680
Рис. 10. 4. Модифицированное приложение Windows Forms: вкладка «Векторные часы»

А на следующий горит огонь:

681
Рис. 10. 5. Модифицированное приложение Windows Forms: вкладка «Огонь!!!»

Завершаем всю дождём:

682
Рис. 10. 6. Модифицированное приложение Windows Forms: вкладка «Дождик»

Если требуется добавить звуки дождя (есть на чём услышать):

Отредактируем код события Selected для последней вкладки:

if (e.TabPage.Name == tabPage6.Name)
{
timer2.Enabled = true;
player.SoundLocation = "/Raindrop.wav";
player.Play();
}
else
{
timer2.Enabled = false;
player.Stop();
}

И добавим директиву using в начало файла с кодом:

using System.Media;

И добавим до метода Selected строчку с кодом:

SoundPlayer player = new SoundPlayer();

11. О приложении к Лабораторной работе № 19

Получившуюся программу (LWP19GDIPlus.exe), собранную из кусков кода


приведённых в данной лабораторной работе, а также архив с фоновым изображением
для первой вкладки (Background_Image.zip), использованный в данной работе,

683
можно загрузить по ссылке в конце этого материала (сслыка доступна в программном
продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).
Варианты заданий: Варианты для выполнения самостоятельных заданий с
использованием материала данной работы приведены по ссылке в конце этого
материала (сслыка доступна в программном продукте).

20. Лабораторная работа № 20: Inventor ― работа с Inventor

Лабораторная работа № 20: Inventor — работа с Inventor

Содержание

56.Вводная часть
57.Создание приложения Windows Forms
58.Модификация приложения Windows Forms: запуск и завершение
приложения Inventor
59.Модификация приложения Windows Forms: создание новых документов
60.Модификация приложения Windows Forms: доступ к элементам
документов Inventor
61.Завершающая часть
62.О приложении к Лабораторной работе № 20

1. Вводная часть

В этой работе будет рассмотрены некоторые вводные особенности работы с


программным комплексом САПР: Inventor (Professional, версии 2012 с
установленным пакетом обновлений Service Pack 1.0).

Рис. 1. 1. Логотип разработчика Autodesk, Inc

Autodesk, Inc. — компания, крупнейший в мире поставщик программного


обеспечения (САПР) для промышленного и гражданского строительства,
машиностроения, рынка средств информации и развлечений.
Компанией разработан широкий спектр решений для архитекторов, инженеров,
конструкторов, позволяющих им создавать цифровые модели. Технологии Autodesk
используются для визуализации, моделирования и анализа поведения
разрабатываемых конструкций на ранних стадиях проектирования и позволяют не
просто увидеть модель на экране, но и испытать её. Сейчас насчитывается более 9 млн.
пользователей Autodesk по всему миру.
Имеет представительства в СНГ.

Autodesk Inventor — система трехмерного твердотельного и поверхностного


проектирования (САПР) компании Autodesk, предназначенная для создания цифровых
прототипов промышленных изделий. Инструменты Inventor обеспечивают полный цикл
проектирования и создания конструкторской документации:

684
 2D/3D моделирование;
 создание изделий из листового материала и получение их разверток;
 разработка электрических и трубопроводных систем;
 проектирование оснастки для литья пластмассовых изделий;
 динамическое моделирование;
 параметрический расчет напряженно-деформированного состояния деталей и
сборок;
 визуализация изделий;
 автоматическое получение и обновление конструкторской документации
(оформление по ЕСКД).

Функциональные возможности:

Компоновочные схемы совмещают отдельные детали и узлы. Пользователи


могут проверить возможность сборки объекта, добавить и позиционировать новые
части, а также устранить помехи между частями проекта.
Литьевые формы и оснастка. Программа автоматизирует ключевые аспекты
процесса проектирования литьевых форм под давлением. Пользователи могут быстро
создавать и проверять конструкции форм, а затем экспортировать их в Autodesk
Moldflow.
Детали из листового материала. Специальная среда проектирования изделий
из листового материала автоматизирует многие аспекты работы. Пользователи могут
создавать детали развертки, гнутые профили, формировать фланцы путем 3D-
моделирования и вставлять в детали специализированные крепежные элементы.
Генератор рам служит для проектирования каркасов (рам) на основе
стандартных профилей. Рамы создаются путем размещения стандартных стальных
профилей на каркасе. Формирование конечных условий упрощается благодаря
наличию стандартных опций для угловых соединений и соединений встык.
Пользователи могут создавать собственные профили и добавлять их в библиотеку.
Кабельные и трубопроводные системы. Среда для создания трубопроводов
помогает проектировать их таким образом, чтобы вписать в сложную сборку или
ограниченное пространство. Она включает библиотеку стандартных фитингов, труб и
шлангов, и обеспечивает создание сборочных чертежей, которые обновляются по мере
изменений исходной 3D-модели.
Полное описание возможностей программы доступно на сайте Autodesk в
документе «Autodesk Inventor: технология цифровых прототипов для
машиностроения и промышленного производства»
(http://images.autodesk.com/emea_apac_main/files/inv10_techwhatsnew_us00
.pdf).

685
Рис. 1. 1. Анимация, полученная с помощью Autodesk Inventor

Студенческие лицензии:
Студенческие версии Autodesk Inventor, предназначенные исключительно для
использования студентами и преподавателями в образовательных целях, доступны для
бесплатной загрузки с сайта Образовательного сообщества Autodesk . Функционально
такая версия Autodesk Inventor ничем не отличается от полной, за одним исключением:
все файлы, созданные или отредактированные в ней, имеют специальную пометку (так
называемый educational flag), которая будет размещена на всех видах.

2. Создание приложения Windows Forms

Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение Windows Forms. Также здесь можно выбрать какой

686
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим LWP20Inventor — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

687
Рис. 2. 3. Вводим данные нового проекта приложения Windows Forms

После нажатия клавиши ОК, мы увидим сформированный проект и исходный код


приложения Windows Forms (не пустого изначально).

688
Рис. 2. 4. Обозреватель решений: состав проекта приложения Windows Forms
сформированного средой разработки

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

689
Рис. 2. 5. Запуск приложения Windows Forms по конфигурации Debug

3. Модификация приложения Windows Forms: запуск и завершение


приложения Inventor

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

ПРИМЕЧАНИЕ № 1: Для перехода на визуальное представление формы,


необходимо двойным нажатием в обозревателе решений нажать на значок формы (

) или выбрать вкладку на панели вкладок с именем <имя формы>.cs


[Конструктор].

Задаём следующие параметры формы на панели Свойства:

(Name) изменим с Form1.cs2 на LWP20Main


^ Поменяем внутреннее имя формы.
Text изменим с Form1 на Работа с Inventor
(C#)
^ Поменяем заголовок формы (то что отображается в шапке приложения слева).
Icon изменим изображение (иконку)
приложения
^ Необходим файл значка *.ico.
Size изменим со значений 300; 300 на 400;
300

690
^ Поменяем размер формы.

ПРИМЕЧАНИЕ № 2: Для того, чтобы поменять имя файла нашей формы,


необходимо выполнить следующее: выделить в обозревателе решений значок формы (

) и нажать правую кнопку мыши, затем выбрать Переименовать. Ввести


необходимое новое имя СОХРАНЯЯ расширение *.cs. После смены имени,
автоматически поменяются имена проассоциированных непосредственно с формой
файлов:

Получим нечто подобное:

Рис. 3. 1. Модифицированная форма приложения

Установим на форме в левом верхнем углу две кнопки Button. Параметры


кнопки будут следующими:

(Name): B_Open
Text: Запустить AIP 2012
Size: 1550; 23

(Name): B_Close
Text: Завершить работу AIP 2012
Size: 155; 23

Установим также GroupBox для объединения элементов управления в группу.


Параметры такие:

(Name): GB_Open
Text: Запуск и завершение приложения Inventor

691
Расставим эти элементы как показано на рисунке ниже:

Рис. 3. 2. Расстановка элементов управления первой группы элементов

Теперь необходимо добавить ссылку на основную библиотеку компонентов для


Autodesk Inventor Professional 2012 (COM-библиотека: Autodesk Inventor
Object Library, версии 1.0)

Для добавления последовательно выполним: Проект -> Добавить ссылку... -


>в открывшемся окне перейти на вкладку COM-компоненты:

Рис. 3. 3. Добавление нового элемента Autodesk

Двойным щелчком по кнопке B_Open и B_Close в конструкторе форм, создаём


код для события нажатия (событие Click) со следующим кодом для двух кнопок
(B_Open и B_Close соответственно):

private void B_Open_Click(object sender, EventArgs e)

692
{
LaunchInventor(true);
}

private void B_Close_Click(object sender, EventArgs e)


{
CloseInventor();
B_Close.Enabled = false;
B_Open.Enabled = true;
}

А ниже после этого кода добавим код вышеупомянутых методов открытия и


закрытия экземпляра приложения Inventor.exe:

private bool LaunchInventor(bool bVisible)


{
// Inventor уже был запущен?
Process[] aInventor = Process.GetProcessesByName("Inventor");

if (aInventor.Length == 0)
{
// Запускаем экземпляр приложения Inventor
Process InventorProcess = System.Diagnostics.Process.Start("C:\\Program
Files\\Autodesk\\Inventor 2012\\Bin\\Inventor.exe");
B_Close.Enabled = true;
B_Open.Enabled = false;
Thread.Sleep(20000);
}
// Подключаемся к запущенному экземпляру Inventor
oApp1 =
(Inventor.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Inventor.Ap
plication");
if (oApp1 == null)
{
return false;
}
// Предотвращаем выскакивание сообщений об ошибках
oApp1.SilentOperation = true;
// Делаем экземпляр приложения видимым
oApp1.Visible = bVisible;
return true;
}
// Закрываем экземпляр Inventor
private bool CloseInventor()
{
// quit Inventor - we're done with it
if (oApp1 != null)
{
oApp1.Quit();
oApp1 = null;
}
return true;
}

В самом начале файла формы LWP20Main.cs добавим следующие объявления


для функционирования кода формы:

using System.Threading;
using System.Diagnostics;
using Inventor;
using System.Reflection;

693
Найдём в этом же файле:

public partial class Form1 : Form


{

И добавим после:

// Объявляем экземпляр переменной oApp1 для доступа к объектам


Inventor.Application
// Таким образом, будет получена возможность в работы в этом классе
private Inventor.Application oApp1;

Текст метода LWP20Main() изменим так:

public LWP20Main()
{
InitializeComponent();
B_Close.Enabled = false;
}

Компилируем приложение (Debug) и запускаем. Жмём первую кнопку и


запускаем Inventor (Рис. 3. 4):

Рис. 3. 4. Запуск приложения Inventor после нажатия кнопки «Запустить AIP 2012»

Закрываем экземпляр приложения нажатием другой кнопки.

4. Модификация приложения Windows Forms: создание новых документов

Следующая часть данной работы объясняет, как создать новый документ


(например, обычную деталь, тип файла Обычный.ipt) и получить общее число
открытых документов в открытом приложении Inventor. Строго говоря, работы с COM-
Сервером Inventor очень похожа и мало чем отличается от работы с COM-сервером
SolidWorks (классы приложения, документа, эскиза и т.п.). Поэтому основы работы
такие же. Необходимо лишь хорошо знать структуру применяемых классов COM-
сервера и документа для успешного применения этих навыков.

694
Единственно «но». Популярность разработки приложений на C# «под и для»
Inventor достаточно невысокая по сравнению с тем же «родным» для корпорации
Autodesk Inventor Visual Basic for Application и Visual Basic .NET.. Но найти
необходимую документацию всё же можно.

И так, размещаем вторую группу элементов на форме следующим образом:

Рис. 4. 1. Расстановка элементов управления второй группы элементов

И код для единственной кнопки B_Create (событие Click):

private void B_Create_Click(object sender, EventArgs e)


{
Inventor.Application oApp2;
oApp2 =
(Inventor.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Inventor.Ap
plication");
Inventor.PartDocument oPart2;
oPart2 =
(Inventor.PartDocument)oApp2.Documents.Add(Inventor.DocumentTypeEnum.kPartDocumentObject)
;
// Получае число открытых документов экземпляра приложения Inventor
int number_int = oApp2.Documents.Count;
string number_string = Convert.ToString(number_int);
MessageBox.Show("Текущее число открытых документов: " + number_string,
"Работа с Inventor (C#) :: Получение числа открытых страниц в Inventor");
}

Подкорректируем код LWP20Main():

public LWP20Main()
{
InitializeComponent();
B_Close.Enabled = false;
B_Create.Enabled = false;
}

Найдём:

// Запускаем экземпляр приложения Inventor


Process InventorProcess = System.Diagnostics.Process.Start("C:\\Program
Files\\Autodesk\\Inventor 2012\\Bin\\Inventor.exe");
B_Close.Enabled = true;
B_Open.Enabled = false;

Добавим после:

B_Create.Enabled = true;

И изменим событие нажатия кнопки B_Close:

private void B_Close_Click(object sender, EventArgs e)


{
CloseInventor();
B_Close.Enabled = false;

695
B_Open.Enabled = true;
B_Create.Enabled = false;
}

Компилируем приложение (Debug) и запускаем. Запускаем Inventor основной


кнопкой, и жмём несколько раз на Создать и посчитать (Рис. 4. 2):

Рис. 4. 2. Создание двух документов деталей в Inventor и отображение получения


общего числа открытых документов

5. Модификация приложения Windows Forms: доступ к элементам документов


Inventor

Допустим, есть документ сборка (тип файла Обычный.iam) с двумя простенькими


деталями:

Рис. 5. 1. Сборка1.iam в среде Inventor

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

Размести элементы третьей группы следующим образом:

Рис. 5. 2. Расстановка элементов управления третьей группы элементов

Где три кнопки и одно текстовое поле TextBox по порядку:

(Name): B_Start
Text: Инициализировать сборку
Size: 155; 23

(Name): B_Hide
Text: Скрыть
Size: 100; 23

(Name): B_Show
Text: Показать
Size: 100; 23

(Name): TB_Name
Text: Имя элемента
Size: 100; 23

Также добавим на форму элемент OpenFileDialog со свойствами:

(Name): OFD_Name
Filter: Сборка *.iam|*.iam

И один Label (по первой кнопкой группы).

Найдём в файле LWP20Main.cs строчки:

private Inventor.Application oApp1;

Добавим после:

// Делаем это ещё раз для другой части нашего приложения


private Inventor.Application oApp3;
// Объявляем экземпляр переменную oAsmDoc3, чтобы получить возможности в работы в
этом классе
private Inventor.AssemblyDocument oAsmDoc3;

697
Событие Click кнопки B_Start:

private void B_Start_Click(object sender, EventArgs e)


{
OFD_Name.ShowDialog();
oApp3 =
(Inventor.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Inventor.Ap
plication");
// Открываем файл сборки
oAsmDoc3 =
(Inventor._AssemblyDocument)oApp3.Documents.Open(OFD_Name.FileName, true);
// Проверяем, что хотя бы один документ был открыт
if (oApp3.Documents.Count == 0)
{
MessageBox.Show("Должен быть открыт хотя бы один документ в данный
момент");
}
// Проверяем что хотя бы один документ Сборка (*.iam) был активным
if (oApp3.ActiveDocument.DocumentType !=
Inventor.DocumentTypeEnum.kAssemblyDocumentObject)
{
MessageBox.Show("Должна быть открыт хотя бы одна сборка
(документ: \"Сборка\", тип файла: *.iam) в данный момент");
}
// Переопределяем тип активного документа ActiveDocument в AssemblyDocument
(документ сборки)
// Устанавливаем ссылку на активный документ
oAsmDoc3 = (Inventor.AssemblyDocument)oApp3.ActiveDocument;
B_Show.Enabled = true;
B_Hide.Enabled = true;
}

Событие MouseClick текстового поля TB_Name:

private void TB_Name_MouseClick(object sender, MouseEventArgs e)


{
TB_Name.Text = "";
}

Изменим метод LWP20Main():

public LWP20Main()
{
InitializeComponent();
B_Close.Enabled = false;
B_Create.Enabled = false;
B_Start.Enabled = false;
B_Show.Enabled = false;
B_Hide.Enabled = false;
}

И код закрытия Кнопки для приложения Inventor:

private void B_Close_Click(object sender, EventArgs e)


{
CloseInventor();
B_Close.Enabled = false;
B_Open.Enabled = true;
B_Create.Enabled = false;
B_Start.Enabled = false;
B_Show.Enabled = false;

698
B_Hide.Enabled = false;
}

Найдём:

Thread.Sleep(20000);

Добавим до:

B_Start.Enabled = true;

Последним штрихом инициализируем события кнопок Скрыть и Показать, а


также методу, выполняющему изменения статуса видимости объекта:

private void B_Hide_Click(object sender, EventArgs e)


{
// Вызываем функция, которая работает со сборкой и устанавливает видимость
объектов
SetVisibility(oAsmDoc3.ComponentDefinition.Occurrences, TB_Name.Text, false);
// Обновляем вид
oApp3.ActiveView.Update();
}

private void B_Show_Click(object sender, EventArgs e)


{
// Вызываем функция, которая работает со сборкой и устанавливает видимость
объектов
SetVisibility(oAsmDoc3.ComponentDefinition.Occurrences, TB_Name.Text, true);
// Обновляем вид
oApp3.ActiveView.Update();
}

private static void SetVisibility(Inventor.ComponentOccurrences Occurences,


string SearchName, bool VisibilityOn)
{
// Итерация по каждому из "упоминаний" в коллекции условий
foreach (Inventor.ComponentOccurrence oOccurence in Occurences)
{ // Проверяем, если имя "упоминания" совпадает с введённым на форме именем
// Все символы строки делаем строчными чтобы избежать чувствительности к
регистру
if (oOccurence.Name.ToLower().Contains(SearchName.ToLower()))
{ // Проверяем, если параметр "Видимость" отличает от введённого при
нажатии на кнопки формы
if (oOccurence.Visible != VisibilityOn)
{ // Устанавливаем видимость для "упоминания" (например, детали)
oOccurence.Visible = VisibilityOn;
}
}
}
}

Готово. Можно компилировать и проверять работоспособность.

6. Завершающая часть

Компилируем приложение (Release) и запускаем. Жмём на кнопку «Запустить


AIP 2012», далее «Инициализировать сборку» и выбираем любой доступный файл
сборки (файл *.iam). После чего загрузки сборки вводим в текстовое поля имя любого
объекта сборки и меняем видимость. Результат работы показан ниже (Рис. 6. 1):

699
Рис. 6. 1. Работа кнопки Скрыть: скрытая Деталь1 файла сборки Сборка1.iam

7. О приложении к Лабораторной работе № 20

Получившуюся программу (LWP20Inventor.exe), собранную из кусков кода


приведённых в данной лабораторной работе и сборку (в архиве Assembly1.zip) можно
загрузить по ссылке в конце этого материала (сслыка доступна в программном
продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).

21. Лабораторная работа № 21: Знакомство с языком F# (Эф-шарп) на примере


простого приложения для командной строки

Лабораторная работа № 21: Знакомство с языком F# (Эф-шарп) на


примере простого приложения для командной строки

Содержание

1. Вводная часть
2. Основные положения при работе с F#
3. Создание приложения «Учебник по F#»
4. Создание консольного приложения
5. О приложении к Лабораторной работе № 21

1. Вводная часть

Функциона́льное программи́рование — раздел дискретной математики и


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

700
предполагает явного хранения состояния программы. Соответственно, не предполагает
оно и изменяемость этого состояния (в отличие от императивного, где одной из базовых
концепций является переменная, хранящая своё значение и позволяющая менять его
по мере выполнения алгоритма). F# — язык функционального программирования для
платформы .NET.

F# — это мультипарадигменный язык программирования, разработанный в


подразделении Microsoft Research и предназначенный для исполнения на платформе
Microsoft .NET. Он сочетает в себе выразительность функциональных языков, таких
как OCaml и Haskell с возможностями и объектной моделью .NET. Язык обеспечивает
безопасность относительно типов, неплохую производительность, а также способен
выступать в роли языка сценариев.
Поскольку все понятия функционального программирования проникают в
основные языки (C#, Visual Basic) через такие технологии, как обобщенные типы .NET
или LINQ, язык F# обязан своим успехом прежде всего сообществу .NET — причем
успешен он был настолько, что в ноябре 2007 года корпорация Microsoft объявила о
том, что F# будет переведён в разряд поддерживаемых языков программирования .NET.

F# выступает строго против использования значений null и активно


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

Программы на F# обычно получаются лаконичнее. С какой стороны ни посмотри,


«типографии» получается меньше: сокращается не только количество символов, но и
количество мест, где компилятор требует указать тип переменной, аргумента или
возвращаемых значений. Значит, и обслуживать приходится меньший объем кода.

По производительности F# сравним с C#, однако если сравнивать с языками,


которые настолько же лаконичны, как F#, в частности с динамическими языками и
языками сценариев, он имеет гораздо лучшие показатели. И так же как во многих
динамических языках, в F# имеются средства, позволяющие проверить данные:
написать фрагмент кода и запустить его в интерактивном режиме.

Первая версия языка появилась в 2005 году. С тех пор вокруг F# стало
формироваться сообщество. За счёт поддержки функциональной парадигмы язык
оказался востребован в научной сфере и финансовых организациях. Во многом
благодаря этому Microsoft решила перевести F# из статуса исследовательских проектов
в статус поддерживаемых продуктов и поставить его в один ряд с основными языками
платформы .NET. И это несмотря на то, что в последнее время всё большую активность
проявляют динамические языки, поддержка которых также присутствует в .NET
Framework. 12 апреля 2010 года свет увидела новая версия флагманского продукта
для разработчиков — Microsoft Visual Studio 2010, которая поддерживает разработку
на F# прямо из коробки, то есть можно создавать приложения сразу после установки
среды разработки.

2. Основные положения при работе с F#

Исполняемый файл Fsi.exe, входящий в комплект поставки F#, представляет


собой интерактивную консоль, в которой можно быстро проверить работоспособность
отдельных фрагментов кода на F#. После установки среды разработки расположен по
адресу (для Windows 7 64-бит): C:\Program Files (x86)\Microsoft F#\v4.0\Fsi.exe

701
Рис. 2. 1. Fsi.exe

В состав современных инсталляционных пакетов F# входят также модули


интеграции в Visual Studio 2008 и свободно распространяемую Visual Studio 2008
Shell, которые позволяют компилировать участки кода прямо из редактора кода.
Открыв текст программы во встроенном редакторе кода, можно отправлять выделенные
участки на исполнение простым нажатием комбинации клавиш Alt+Enter.

Исполняемый файл Fsc.exe — непосредственно компилятор исходного кода F#,


который можно использовать совместно со своим любимым текстовым редактором.
Расположен в той же директории что и Fsi.exe.

Утилиты fsc.exe и fsi.exe отлично работают и под Mono, открытой


реализацией .NET Framework.

Файлы, содержащие код на F#, обычно имеют следующие расширения:

*.fs — обычный файл с кодом, который может быть скомпилирован;


*.fsi — файл описания публичного интерфейса модуля. Обычно генерируется
компилятором на основе кода, а затем редактируется вручную;
*.fsx — исполняемый скрипт. Может быть запущен прямо из проводника
Windows при помощи соответствующего пункта всплывающего меню или передан на
исполнение в интерактивную консоль Fsi.exe.

Иногда можно встретить в начале F# кода директиву #light on. Эта директива
отключает режим совместимости синтаксиса с OCaml, делая отступы в коде значимыми
(как, например в Python или Haskell). В последних версиях языка облегчённый режим
включен по умолчанию, поэтому необходимости в указании директивы #light больше
нет.

Также для F# не поддерживается режим конструктора страниц ASP.NET. Это не


означает, что F# нельзя использовать вместе с ASP.NET — отнюдь. Просто в Visual
Studio, работая с F#, нельзя без дополнительных средств перетаскивать элементы
управления, как при работе с C# или Visual Basic.

702
3. Создание приложения «Учебник по F#»

После запуска Visual Studio 2010, откроется Начальная страница:

Рис. 3. 1. Начальная страница Visual Studio 2010 Professional (русская версия)

Для начала, надо создать пустой проект, для этого выполним последовательно:
Файл -> Создать -> Проект… (также можно просто нажать сочетание клавиш
Ctrl+Shift+N или пункт «Создать проект…» на Начальной странице):

703
Рис. 3. 2. Создание нового проекта

Откроется окно создания проекта и выбора необходимых нам параметров.

Выберем слева в пункте Установленные шаблоны -> Другие языки ->


Visual F#, далее найдём в списке Учебник по F#. Также здесь можно выбрать, какой
использовать «фреймворк» (набора компонентов для написания программ). В нашем
случае выберем .NET Framework 4.

Рис. 3. 3. Окно создания нового проекта

В поле Имя вводим LWP21-Tutorial — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект
(значение «по умолчанию» можно поменять, выполнив действия: Сервис ->
Параметры… -> Проекты и решения -> меняем путь в поле Размещение
проектов). Выберем расположение удобное для быстрого поиска. В поле Имя
решения вводится либо название программы «по умолчанию» из поля Имя
автоматически, либо можно ввести своё собственное. Под этим именем будет создана
конечная папка проекта (если Имя и Имя решения разные).

704
Рис. 3. 4. Вводим данные нового проекта «Учебник по F#»

После нажатия клавиши ОК мы увидим сформированный проект и исходный код


консольного приложения (не пустого изначально).

705
Рис. 3. 5. Исходный код консольного приложения сформированного средой разработки

Как видим, среда разработки сформировала один файл Tutorial.fs с исходным


кодом. В самом конце кода вставим следующую строчку:

let c = Console.ReadKey()

Без точки с запятой как в C#... Код служит «паузой» для окна консоли после
компиляции.

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 или


Отладка -> Начать отладку. Если программист хочет запустить программу без
отладки и уверен что программа не нуждается в поиске ошибок и оптимизации кода, то
можно нажать Отладка -> Запуск без отладки.
По умолчанию клавиша отладки вынесена на панель инструментов вверху.
Запускаем приложение в режиме отладки (и компиляции debug-версии программы)

нажав на иконку (Debug выбрано изначально).

706
Рис. 3. 5. Запуск приложения «Учебник по F#» по конфигурации Debug

Но компиляция в случае данного приложения не нужна. Весь файл содержит


примеры для изучения F#. В файле собрано достаточное количество различных
примеров с использование оператора let. Более подробно о том, что делает та или иная
строчка кода можно прочитать из русскоязычных комментариев (если их нет, взять код
с комментариями из Приложения № 1 к данной лабораторной работе).
Для получения промежуточных результатов работы кода можно воспользоваться
комбинацией: выделение участка кода + Alt+Enter.

Найдём и выделим мышкой следующий код:

/// Очень простое константное целое


let int1 = 1

/// Другое очень простое константное целое


let int2 = 2

/// Добавление двух целых


let int3 = int1 + int2

После выделения жмём «альт» + «энтер» и...:

707
Рис 3. 6. Результат выполнения выделенного участка кода в окне F# Interactive

Как видим, здесь сработала встроенная интерактивная консоль Fsi.exe.


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

Наполним наш «учебник» новыми «уроками».

Перед вызовом функции «паузы» окна консоли добавим код:

let results = [ for i in 0 .. 100 -> (i, i*i) ]


printfn "\n\tРезультаты работы цикла и операции с шагом цикла i: i*i = \n\n%A" results

На первый взгляд этот цикл for кажется совершенно безобидным, но в


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

Список — это примитив, часто встречающийся в функциональных языках. Во


многих отношениях список схож с массивом. Разница состоит в том, что список не
позволяет получать доступ к отдельному элементу на основании его позиции в списке
(как традиционное выражение a[i] в C#). В функциональном программировании списки
можно встретить в самых различных ситуациях. По большей части их можно считать
эквивалентом List<T> в .NET Framework с несколько расширенными возможностями.

Список всегда относится к какому-то определенному типу. В нашем примере


идентификатор порождает список кортежей, а точнее кортежей, относимых языком F#
к типу (int * int). Список кортежей можно представить в виде пары столбцов,
возвращаемых оператором SELECT в SQL. Наш код создаёт список, содержащий 100
пар целых чисел.

708
Ни одно введение в язык программирования не обходится без программы «Hello,
World». F# не станет исключением.

После цикла добавим код:

printf "Hello World!"


open System.Windows.Forms
let mb = MessageBox.Show("Привет мир и F#!", "Учебник по F# (F#) :: Работа с MessageBox")

F# полностью поддерживает связь и взаимодействие с нижележащей


платформой CLR — в том числе и библиотеки Windows Forms. Но для работы кода
выше потребуется добавить в приложение библиотеку. В обозревателе решений
выделим Ссылки и нажмём правую кнопку мыши, далее в открывшемся окне на
вкладке .NET найдём System.Windows.Forms, далее ОК.

Рис. 3. 7. Добавление ссылки: System.Windows.Forms

Компилируем приложение (Debug) и запускаем. Видим следующее:

709
Рис. 3. 8. Результат выполнения новых участков кода в приложении «Учебник по F#»

Оператор let. Это вообще самый важный оператор. Формально let присваивает
идентификатору значение. «Определяет переменную», но это было бы неверно.
Идентификаторы в F# имеют природу двойственную. Во-первых, однажды
определенный идентификатор может так никогда и не измениться. Именно этот момент
позволяет писать программы, не нарушающие параллельность обработки: изменение
состояния язык F# не приветствует. Во-вторых, идентификатор может относиться не
только к примитивному или объектному типу, как в C# и Visual Basic, но и к
функциональному типу, подобному тем, что встречаются в LINQ.
Обратим внимание ещё и на то, что явно тип идентификатора никогда не
указывается. Идентификатор результата, к примеру, никогда не определяется — он
выводится из правой части выражения, следующего за ним. Эта функция известна как
вывод типа. Она свидетельствует о способности компилятора проанализировать код,
определить возвращаемое значение и автоматически его использовать. Аналогичным
образом действуют новые выражения с выводимым типом в C#, содержащие ключевое
слово var.

Оператор let работает не только с данными. Его можно применять для


определения функций, которые в F# являются понятиями первостепенными:

let add a b =
a + b

Программа выше делает именно то, что от неё ожидается: складывает числа a и
b и неявно возвращает результат вызывающей стороне. Технически, каждая функция
F# возвращает значение, даже если оно по природе своей является не значением, а
особой единицей под названием unit.

710
Бывают ситуации, в которых функция должна игнорировать передаваемые ей
параметры. Тогда в F# используется знак нижнего подчеркивания — в качестве
заполнителя для параметра. Добавим следующий код в наш «учебник»:

let add a b = a + b
let return10 _ = add 5 5
let ten = return10 12
printf "\n\nДесять = %d\n" ten

Результат:

Как и в большинстве функциональных языков, в F# функцию можно определить


частично — в расчёте на то, что недостающие параметры будут переданы при вызове:

let add5 a =
add a 5

В определённой степени это напоминает создание перегруженного метода,


который получает другой набор параметров и вызывает ещё один метод (код C#):

public class Adders {


public static int add(int a, int b) { return a + b; }
public static int add5(int a) { return add(a, 5); }
}

Но есть небольшая разница. Обратим внимание на то, что в F# типы явным


образом не определяются. Это означает, что компилятор проделывает всю работу по
выводу типов, определяет, совместим ли параметр функции add5 по типу с
возможностью сложения с целочисленным литералом 5, и либо проводит компиляцию,
либо сообщает об ошибке. В действительности в F# чаще всего имеет место неявная
параметризация по типам (то есть используются общие типы).

Если в Visual Studio навести указатель мыши на определение числа десять,


приведенное выше, мы увидим, что его тип объявляется следующим образом:

На языке F# это означает, что десять — это значение, функция, принимающая


один параметр любого типа, и возвращающая целочисленный результат. Синатксис с
«галочкой» — это грубый эквивалент синтаксиса <T> в C#, так что в наилучшем

711
переводе на язык функций C# можно было бы сказать, что десять — это экземпляр
делегата метода с параметризацией типов, тип которого в действительности лучше
просто игнорировать (только по правилам C# это невозможно):

delegate int Transformer<T>(T ignored);

public class App


{
public static int return10(object ignored) { return 5 + 5; }

static void Main()


{
Transformer<object> ten = return10;
System.Console.WriteLine("\n\nДесять = {0}", return10(0));
}
}

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


месте, в котором может находиться и сам код. То есть, если нужно расширить
предыдущий пример с циклом, можно написать так (уменьшим число итераций до 10):

let compute2 x = (x, x*x)


let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 10 -> compute2 i ]
let results3 = [ for i in 0 .. 10 -> compute3 i ]
printfn "\n\tРезультаты работы цикла и операции с шагом цикла i: i*i = \n\n%A" results2
printfn "\n\tРезультаты работы цикла и операции с шагом цикла i: i*i, i*i*i = \n\n%A"
results3

Поэлементный просмотр списка (или массива, или любой другой схожей


конструкции) настолько распространен в функциональных языках, что его свели к
единому базовому методу List.iter. Он просто вызывает функцию для каждого элемента
списка. Весьма полезны и другие схожие библиотечные функции. Например, метод
List.map принимает в качестве аргумента функцию и применяет её к каждому
элементу списка, создавая, таким образом, новый список.

Немного об асинхронном выполнении функций. Вставим следующий код:

open System.Threading
printf "\n"
let printWithThread str =
printfn "[ID потока = %d] %s" Thread.CurrentThread.ManagedThreadId str

let evals =
let z = 1.0
[ async { do printWithThread "Вычисляем z*z\n"
return z + z };
async { do printWithThread "Вычисляем sin(z)\n"
return (sin z) };
async { do printWithThread "Вычисляем log(z)\n"
return (log z) } ]

let awr =
async { let! vs = Async.Parallel evals
do printWithThread "Вычисляем v1 + v2 + v3\n"
return (Array.fold (fun a b -> a + b) 0.0 vs) }

let R = Async.RunSynchronously awr

printf "Результат вычислений = %f\n" R

712
В нём показана работа асинхронных рабочих потоков в упрощенном,
удобоваримом виде. Если не вдаваться в подробности, evals является массивом
функций, которые должны быть выполнены. Каждая из них помещается в очередь на
выполнение путем вызова Async.Parallel. При выполнении становится ясно, что
функции, входящие в массив evals, фактически находятся в отдельных потоках, идущих
от функции в awr. Хотя, по причине природы пула потоков .NET, часть функций из
массива evals или даже все функции могут выполняться в одном потоке, что заметно
при выводе результата: части некоторых строк при печати окажутся не в том месте.

Факт выполнения функций в пуле потоков .NET ещё раз подтверждает отличную
приспособленность языка F# ко взаимодействию с нижележащей средой выполнения.
То, что он опирается на библиотеку классов .NET там, где другие функциональные
языки используют специальные методы (например, многопоточность), означает, что в
программах на C# можно использовать библиотеки и модули F# — и наоборот.

4. Создание консольного приложения

Теперь продемонстрируем возможности языка F# на примере работы


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

Для начала нужно создать новый проект. Основу на этот раз станет тип проекта
Visual F#: Приложение F#. Имя будет LWP21Console.

713
Рис. 4. 1. Вводим данные нового проекта «Приложение F#»

Но на самом деле, после создания такого проекта и ввода имени, мы увидим


пустую страницу с кодом и один единственный файл в обозревателе решений
(Program.fs):

Рис. 4. 2. Обозреватель решений: состав проекта приложения F# сформированного


средой разработки

Пустота обусловлена отсутствием нормальных шаблонов в версии среды


разработки Visual Studio 2010. Шаблоны приложений (в том числе и консольного)
можно получить, например на официальном блоге разработчик F# (Visual Studio F#
Team Blog).

Рис. 4. 3. Блог команды разработчиков F#: шаблон консольного приложения

Загруженный с сайта шаблон устанавливается в «один клик» (специальное


расширение файла, запускает встроенный установщик Visual Studio):

714
Рис. 4. 4. Пример установленного шаблона консольного приложения для F#

Либо можно поискать шаблоны в сети Интернет (окно Создать проект ->
Шаблоны в Интернете):

715
Рис. 4. 5. Шаблоны в Интернете: доступные шаблоны

Вернёмся к нашему пустому приложению. Вставим туда следующий код игры:

// Open - объявление импорта указывает модуль или пространство имён,


// на элементы которого можно ссылаться без использования полного имени
open System
open System.Threading
// System.Console - System можно не писать
Console.Title <- "Консольное приложение (F#)"

let ввод() = Console.ReadLine()


let ожидание() = Console.ReadKey()
let преобразование = Int32.TryParse // Преобразуем ввод ответа в Int32
let использовать цвет f =
Console.ForegroundColor <- цвет
f()
Console.ResetColor()
let красный = ConsoleColor.Red
let зелёный = ConsoleColor.Green
let тёмножёлтый = ConsoleColor.DarkYellow
let сигнал() = Console.Beep()
let ждать(n:int) = Threading.Thread.Sleep n // Число секунд (n000)
let сейчас() = DateTime.Now
let случайно = Random()
let сообщение = ждать (1000)
использовать тёмножёлтый (fun () -> printfn "Добро пожаловать в игру \"Умножение на
время\"!"
printfn "Пожалйста, ввидете число заданий для
игры \"Умножение на вермя\""
printf "Число заданий = ")
let задания = Convert.ToInt32(ввод()) // Вводим число заданий

let игра время =

716
for сообщение in ["\nПриготовиться!"; "3"; "2"; "1"; "Поехали!\n"] // Выводим
сообщения цикла последовательно, через одну секунду
do
printfn "%s" сообщение
ждать 1000
let начало = сейчас() // Фиксируем время старта
[1..время] |> Seq.sumBy (fun i -> // Фиксируем сумму правильных результатов (1), 0 -
неправильные результаты
let a, b = случайно.Next 13, случайно.Next 13
printf "%d x %d = " a b
let введено = ввод()
match преобразование введено with
| true, ответ when ответ = a * b -> // Истина, если ответ верен (a * b)
использовать зелёный (fun () -> printfn "Правильно!")
1
| _, _ ->
сигнал()
использовать красный (fun () -> printfn "%d x %d = %d" a b (a*b))
0
) |> (fun счёт ->
let получить = (сейчас() - начало).ToString("c") // Получаем итоговое время и
преобразуем в строку
// Подстановка параметров: сначала подставляем результат работы блока "[1.."
(число верных ответов),
// потом общее число заданий "..время]" (число срабатывания блока),
// потом время на ответы
printfn "\nИгра завершена!\n%d правильных ответов из %d заданий, за время: %s.\n"
счёт время получить
)

while true do
игра (задания) // Число заданий
ждать 5000
printfn "Играть снова?\nДля продолжения игры нажмите любую клавишу . . ."
ожидание() |> ignore

Код содержит все необходимые комментарии и достаточно понятен. После


запуска и инициализации пространства имён (директива open), меняем заголовок
приложения, затем инициализируем все необходимые функции (в данном случае с
русскоязычными именами), выводим приветственные сообщения и вводим число
заданий для игры. После ввода числа, запускается цикл, который посекундно выводит
куски сообщения (...«2», «1» и «Поехали!»). printf выводит строку без добавления
новой строки. Далее в том же цикле на основе числа заданий выводятся выражения
типа «a умножить на b» со случайными a и b (диапазон от 0 до 12) и программа ждёт
ввода ответа пользователя, этот ответ проверяется и выводится результат.
Неправильный ответ вызывает также звуковой сигнал.

Сама игра продолжается по «вине» следующего цикла:

while true do
игра (задания) // Число заданий

После окончания игры выводится результат и сообщение о продолжении.

Компилируем приложение (Release) и запускаем.

717
Рис. 4. 6. Результат работы консольного приложения игры «Умножение на время»

5. О приложении к Лабораторной работе № 21

Получившиеся программы (LWP21-Tutorial.exe и LWP21-Console.exe),


собранные из кусков кода приведённых в данной лабораторной работе, а также
установочный файл шаблона консольного приложения для F# (FSharp.Console.vsix),
можно загрузить по ссылке в конце этого материала (сслыка доступна в программном
продукте).

Приложение № 1: Исходный код приложения «Учебник по F#» и всех


сопровождающих файлов с кодом приведён по ссылке в конце этого материала
(сслыка доступна в программном продукте).
Приложение № 2: Исходный код «Приложение F#» и всех сопровождающих
файлов с кодом приведён по ссылке в конце этого материала (сслыка доступна в
программном продукте).

22. Лабораторная работа № 22: Различные примеры на F#

Лабораторная работа № 22: Различные примеры на F#

Содержание

1. Вводная часть
2. Создание приложения «Приложение F#»
3. Модификация приложения F#: match
4. Модификация приложения F#: создание форм и рисование объектов на
форме
5. Модификация приложения F#: работа с базой данных Microsoft Access
6. О приложении к Лабораторной работе № 22

1. Вводная часть

718
В предыдущей работе было рассказано, что F# — это язык функционального
программирования для платформы .NET. В данной работе будет продолжена работа с
этим языком на новых, более объёмных и содержательных примерах.

Повторимся и приведём выдержку из предыдущей лабораторной работы:

F# — это мультипарадигменный язык программирования, разработанный в


подразделении Microsoft Research и предназначенный для исполнения на платформе
Microsoft .NET. Он сочетает в себе выразительность функциональных языков, таких
как OCaml и Haskell с возможностями и объектной моделью .NET. Язык обеспечивает
безопасность относительно типов, неплохую производительность, а также способен
выступать в роли языка сценариев.
Поскольку все понятия функционального программирования проникают в
основные языки (C#, Visual Basic) через такие технологии, как обобщённые типы .NET
или LINQ, язык F# обязан своим успехом, прежде всего сообществу .NET — причём
успешен он был настолько, что в ноябре 2007 года корпорация Microsoft объявила о
том, что F# будет переведён в разряд поддерживаемых языков программирования .NET.

Что будет реализовывать в данной работе?

2. Создание приложения «Приложение F#»

После запуска Visual Studio 2010, откроется Начальная страница:

Рис. 2. 1. Начальная страница Visual Studio 2010 Professional (русская версия)

719
Для начала, надо создать пустой проект, для этого выполним последовательно:
Файл -> Создать -> Проект… (также можно просто нажать сочетание клавиш
Ctrl+Shift+N или пункт «Создать проект…» на Начальной странице):

Рис. 2. 2. Создание нового проекта

Откроется окно создания проекта и выбора необходимых нам параметров.

Выберем слева в пункте Установленные шаблоны -> Другие языки ->


Visual F#, далее найдём в списке Приложение F#. Также здесь можно выбрать,
какой использовать «фреймворк» (набора компонентов для написания программ). В
нашем случае выберем .NET Framework 4.

720
Рис. 2. 3. Окно создания нового проекта

В поле Имя вводим LWP22Samples — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект
(значение «по умолчанию» можно поменять, выполнив действия: Сервис ->
Параметры… -> Проекты и решения -> меняем путь в поле Размещение
проектов). Выберем расположение удобное для быстрого поиска. В поле Имя
решения вводится либо название программы «по умолчанию» из поля Имя
автоматически, либо можно ввести своё собственное. Под этим именем будет создана
конечная папка проекта (если Имя и Имя решения разные).

721
Рис. 2. 4. Вводим данные нового проекта «Приложение F#»

После нажатия клавиши ОК, мы увидим сформированный проект и исходный код


консольного приложения.

Но на самом деле, после создания такого проекта и ввода имени, мы увидим


пустую страницу с кодом и один единственный файл в обозревателе решений
(Program.fs):

722
Рис. 2. 5. Обозреватель решений: состав проекта приложения F# сформированного
средой разработки

Пустота обусловлена отсутствием нормальных шаблонов в версии среды


разработки Visual Studio 2010. Шаблоны приложений (в том числе и консольного)
можно получить, например на официальном блоге разработчик F# (Visual Studio F#
Team Blog).

Рис. 2. 6. Блог команды разработчиков F#: шаблон консольного приложения

Загруженный с сайта шаблон устанавливается в «один клик» (специальное


расширение файла, запускает встроенный установщик Visual Studio):

723
Рис. 2. 7. Пример установленного шаблона консольного приложения для F#

Либо можно поискать шаблоны в сети Интернет (окно Создать проект, группа
Шаблоны в Интернете):

724
Рис. 2. 8. Шаблоны в Интернете: доступные шаблоны

3. Модификация приложения F#: match

Полностью рабочее приложение будет вовсю использовать директивы open и


ссылаться на библиотеки компонентов .NET. Нам понадобятся Ссылки; для добавления
ссылок выполним: Проект -> Добавить ссылку...:

В открывшемся окне переходим на вкладку .NET:

725
Рис. 3. 1. Добавление ссылки: добавляем ссылку на компоненты Windows Forms

Нам понадобятся:

System.Windows.Forms;
System.Data;
System.Drawing;

Так будет выглядеть папка «Ссылки» проекта в итоге:

Остальное (библиотеки) будет добавляться использованием «open».

Для начала воспользуемся консолью для реализации следующей


функциональности. В F# существует ключевое слово match. Выражение match
позволяет управлять ветвлением на основе сравнения выражения с набором шаблонов.
Пример:

// Выражение match
match test-expression with
| pattern1 [ when condition ] -> result-expression1
| pattern2 [ when condition ] -> result-expression2
| ...

// Шаблон функции
function

726
| pattern1 [ when condition ] -> result-expression1
| pattern2 [ when condition ] -> result-expression2
| ...

И так, реализуем начальную функциональность приложения. Пользователю


предложат ввести символы, и на основе ввода будет выдан результат. Добавляем
следующий код в файл Program.cs:

// Ключевое слово match: вывод армейских наименований взводов


// Используем библиотеку F#
open System
// Меняем заголовок окна консольного приложения
Console.Title <- "Различные примеры (F#) :: Вывод армейских наименований взводов"
// Меняем цвет отображения символов
Console.ForegroundColor <- ConsoleColor.Blue
Console.BackgroundColor <- ConsoleColor.Gray
// Выводим запрос
printfn "Введите букву латинского алфавита (от a до z):"
// Конвертируем введённые символы с заглавные символы
let chrletter = Char.ToUpper(Convert.ToChar(System.Console.ReadLine()))
// match использует переменную chrletter
match chrletter with
// Если значение chrletter "a" или "A", выводим "Alpha"
| 'A' -> printfn "Вам подходит: Alpha"
| 'B' -> printfn "Вам подходит: Bravo"
| 'C' -> printfn "Вам подходит: Charlie"
| 'D' -> printfn "Вам подходит: Delta"
| 'E' -> printfn "Вам подходит: Echo"
| 'F' -> printfn "Вам подходит: FoxTrot"
| 'G' -> printfn "Вам подходит: Golf"
| 'H' -> printfn "Вам подходит: Hotel"
| 'I' -> printfn "Вам подходит: India"
| 'J' -> printfn "Вам подходит: Juliet"
| 'K' -> printfn "Вам подходит: Kilo"
| 'L' -> printfn "Вам подходит: Lima"
| 'M' -> printfn "Вам подходит: Mike"
| 'N' -> printfn "Вам подходит: November"
| 'O' -> printfn "Вам подходит: Oscar"
| 'P' -> printfn "Вам подходит: Papa"
| 'Q' -> printfn "Вам подходит: Quebec"
| 'R' -> printfn "Вам подходит: Romeo"
| 'S' -> printfn "Вам подходит: Sierra"
| 'T' -> printfn "Вам подходит: Tango"
| 'U' -> printfn "Вам подходит: Uniform"
| 'V' -> printfn "Вам подходит: Victor"
| 'W' -> printfn "Вам подходит: Whiskey"
| 'X' -> printfn "Вам подходит: X-Ray"
| 'Y' -> printfn "Вам подходит: Yankee"
| 'Z' -> printfn "Вам подходит: Zulu"
// Если не буква
| _ -> printfn "Вы ввели символ не латинского алфавита"
printfn "\t\t\tНажмите клавишу Enter для продолжения..."
// Пауза
Console.ReadKey()
// Очищаем экран
Console.Clear()

Соответствующие шаблону выражения позволяют выполнять сложное ветвление


на основе сравнения тестового выражения с набором шаблонов. В выражении match
выражение test-expression по очереди сравнивается с каждым шаблоном, и когда
соответствие найдено, вычисляется соответствующее выражение result-expression, а
полученное значение возвращается в качестве значения выражения match.

727
Функция сопоставления шаблонов, продемонстрированная в предыдущей
синтаксической конструкции, представляет собой лямбда-выражение (ключевое
слово fun), в котором сопоставление шаблонов выполняется непосредственно в
аргументе. Функция сопоставления шаблонов, продемонстрированная в предыдущей
синтаксической конструкции, эквивалентна следующему:

arg функции ->


сопоставить arg с
| pattern1 [ если condition ] -> result-expression1
| pattern2 [ если condition ]-> result-expression2
| ...

Дополнительные сведения о лямбда-выражениях можно посмотреть здесь:


Лямбда-выражения: ключевое слово fun (F#).

Полный набор шаблонов должен включать все возможные сочетания входной


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

Компилируем приложение (Debug) и запускаем. Вводим любой символ с


латинской раскладки клавиатуры и получаем результат:

Рис. 3. 1. Результат работы приложения F#: работа match и вывод результата

Добавим следующий код ещё одного примера работы с match:

// Ключевое слово match: вывод календарного месяца


Console.Title <- "Различные примеры (F#) :: Вывод календарного месяца"
Console.ForegroundColor <- ConsoleColor.Blue
Console.BackgroundColor <- ConsoleColor.White
printfn "Введите числа в диапазоне от 1 до 12:"
let intmonth = Convert.ToInt32(System.Console.ReadLine())
// match использует переменную intmonth
match intmonth with
// Если значение intmonth = 1, выводим "Январь"

728
| 1 -> printfn "Вам подходит: Январь (31 день)"
| 2 -> printfn "Вам подходит: Февраль (28/29 дней)"
| 3 -> printfn "Вам подходит: Март (31 день)"
| 4 -> printfn "Вам подходит: Апрель (30 день)"
| 5 -> printfn "Вам подходит: Май (31 день)"
| 6 -> printfn "Вам подходит: Июнь (30 дней)"
| 7 -> printfn "Вам подходит: Июль (31 день)"
| 8 -> printfn "Вам подходит: Август (31 день)"
| 9 -> printfn "Вам подходит: Сетябрь (30 дней)"
| 10 -> printfn "Вам подходит: Октябрь (31 дней)"
| 11 -> printfn "Вам подходит: Ноябрь (30 дней)"
| 12 -> printfn "Вам подходит: Декабрь (31 день)"
| _ -> printfn "Вы ввели число не из диапазона"
printfn "\t\t\tНажмите клавишу Enter для продолжения..."
Console.ReadKey()
Console.Clear()

Рис. 3. 2. Результат работы приложения F#: работа match и вывод результата

4. Модификация приложения F#: создание форм и рисование объектов на


форме

F# является .NET-языком и ему доступно всё то что можно реализовать и на


C#. Например, попробуем поработать с графикой GDI+.

Следующий блок кода будет создавать простую форму Windows Forms и


рисовать в ней два объекта. Для выхода с формы будет предусмотрена одна кнопка
«Выход» (приложение не будет закрыто).

// Создание формы Windows Forms: Рисование объектов на форме


Console.Title <- "Различные примеры (F#) :: Рисование объектов на форме"
open System.Collections.Generic
open System.Windows.Forms
open System.ComponentModel
open System.Drawing
let graphicForm = new Form(Text = "Различные примеры (F#) :: Рисование объектов на
форме", AutoScaleDimensions = new System.Drawing.SizeF(60.0F, 13.0F), ClientSize = new
System.Drawing.Size(500, 200), StartPosition = FormStartPosition.CenterScreen)
// Создаём элементы управления

729
let exitButton = new Button(Text = "Выход", Location = new System.Drawing.Point(415,
167), Size = new Size(75, 23))
// Событие нажатия на кнопку "Выход"
exitButton.Click.Add(fun quit -> graphicForm.Close())
// Событие прорисовки элементов формы
graphicForm.Paint.Add(fun draw ->
let array = [|new Point(100, 150);new Point(250, 10);new Point(400, 150)|]
// Перо рисования (цвет: синий, толщина 10 пикселей)
let pen = new Pen(Color.Blue, Width = 10.0f)
// Создаём заливку
let brush = new SolidBrush(Color.LightGreen)
// Рисуем треугольник на основе массива точек
draw.Graphics.DrawPolygon(pen, array)
// Заполняем треугольник
draw.Graphics.FillPolygon(brush, array)
// Рисуем эллипс
draw.Graphics.DrawEllipse(pen, 10.0f, 10.0f, 100.0f, 100.0f)
// Заполняем эллипс
draw.Graphics.FillEllipse(brush, 10.0f, 10.0f, 100.0f, 100.0f))
// Добавляем на форму кнопку "Выход"
graphicForm.Controls.Add(exitButton)
// Запускаем новую форму
Application.Run(graphicForm)
printfn "\t\t\tНажмите клавишу Enter для продолжения..."
Console.ReadKey()
Console.Clear()

Компилируем приложение (Debug) и запускаем. Как только доберёмся до этой


итерации в приложении, увидим открывшуюся форму и два нарисованных на форме
объекта. Для выхода с формы жмём кнопку «Выход»:

Рис. 4. 1. Результат работы приложения F#: форма «Рисование объектов на форме»

Следующий пример немного содержательнее. На форме будут создано три


кнопки. «Выход», «Загрузить» и «Печать». Вторая кнопка: вызывает стандартный
диалог открытия файлов (изображений двух типов: JPEG и GIF). После выбора файла,
изображения, оно будет отмасштабировано и помещено в PictureBox на форме.
Нажатие на кнопку «Печать» отправит рисунок печататься на первом же возможном
принтере (если тот подключен к ПК). Также, если на ПК установлено Microsoft Office

730
2010 и OneNote 2010 в комплекте с «офисом», нажатие кнопки «Печать» вызовет это
приложение. Также, код изменяет курсор на форме на один из стандартных (крест), но
можно использовать и свой (закомментированный код). Весь код такой:

// Создание формы Windows Forms: Печать выбранного рисунка


Console.Title <- "Различные примеры (F#) :: Печать выбранного рисунка"
open System.Drawing.Printing
open System.Drawing.Imaging
let imageForm = new Form(Text = "Различные примеры (F#) :: Печать выбранного рисунка",
AutoScaleDimensions = new System.Drawing.SizeF(60.0F, 13.0F), ClientSize = new
System.Drawing.Size(500, 250), StartPosition = FormStartPosition.CenterScreen)
// Создаём элементы управления
let exitButton2 = new Button(Text = "Выход", Location=new System.Drawing.Point(400, 210))
let loadButton2 = new Button(Text = "Загрузить", Location=new System.Drawing.Point(320,
210))
let printButton2 = new Button(Text = "Печать", Location=new System.Drawing.Point(240,
210))
let pictueBox2 = new PictureBox(SizeMode = PictureBoxSizeMode.StretchImage, Location =
new System.Drawing.Point(20, 20), BorderStyle = BorderStyle.FixedSingle, Size = new
System.Drawing.Size(100, 100))
let label2 = new Label(AutoSize = true, Location = new System.Drawing.Point(0, 120))
let openFileDialog2 = new OpenFileDialog()
let draw2 = imageForm.CreateGraphics()
let printDocument2 = new System.Drawing.Printing.PrintDocument()
imageForm.Controls.Add(pictueBox2)
imageForm.Controls.Add(loadButton2)
imageForm.Controls.Add(label2)
imageForm.Controls.Add(printButton2)
imageForm.Controls.Add(exitButton2)
// События нажатия кнопок
printButton2.Click.Add(fun startprint2 -> printDocument2.Print())

loadButton2.Click.Add(fun load2 ->


openFileDialog2.Filter <- "Изображения JPEG (*.jpg,*.jpeg)|*.jpg;*.jpeg|Изображения GIF
(*.gif)|*.gif"
openFileDialog2.Title <- "Различные примеры (F#) :: Выбор файла изображения"
if (openFileDialog2.ShowDialog() = DialogResult.OK) then
// Создаём растровый рисунок и снимаем поворот и отражение (если есть)
let bitmap2 = new System.Drawing.Bitmap(openFileDialog2.FileName)
bitmap2.RotateFlip(RotateFlipType.RotateNoneFlipNone)
// Помещаем выбранное изображение в PictureBox
pictueBox2.Image <- bitmap2
// Отображаем в Label асболютный путь до изображения
label2.Text <- "\tИмя файла изображения:\n" + Convert.ToString(Convert.ToChar(32)) +
(openFileDialog2.FileName))
// Вызываем страницу печати
printDocument2.PrintPage.Add(fun printData2 -> draw2.DrawImage(pictueBox2.Image, 10, 10))

exitButton2.Click.Add(fun quit2 -> imageForm.Close())


// Создание своего курсора (добавленного как ресурс в корень проекта)
// let customCursor2 = new System.Windows.Forms.Cursor("/Pointer.cur")
// imageForm.MouseHover.Add(fun custom2 -> imageForm.Cursor <- customCursor2)
imageForm.MouseHover.Add(fun Cross2 -> imageForm.Cursor <- Cursors.Cross)
[<STAThreadAttribute>]
Application.Run(imageForm)
printfn "\t\t\tНажмите клавишу Enter для продолжения..."
Console.ReadKey()
Console.Clear()

Компилируем приложение (Debug) и запускаем. Жмём «Загрузить», выбираем


любое изображение с нужным расширением, затем, если есть принтер жмём на
«Печать»:

731
Рис. 4. 2. Результат работы приложения F#: форма «Печать выбранного рисунка»

Наконец добавим последний код, реализующий рисование. На этот раз будем


рисовать курсором мыши на форме (аналог инструмент «карандаша»). Толщина пера
будет неизменной, а вот цвет будем выбирать через стандартный диалог выбора цвета.
Также на форме будет кнопка «Стереть»:

// Создание формы Windows Forms: Рисование мышкой


Console.Title <- "Различные примеры (F#) :: Рисование мышкой"
open System.Drawing.Drawing2D
let drawingForm = new Form(Text = "Различные примеры (F#) :: Рисование мышкой",
AutoScaleDimensions = new System.Drawing.SizeF(60.0F, 13.0F), ClientSize = new
System.Drawing.Size(500, 250), StartPosition = FormStartPosition.CenterScreen)
let exitButton3 = new Button(Text = "Выход", Location = new System.Drawing.Point(400,
210))
let eraseButton3 = new Button(Text = "Очистить", Location = new System.Drawing.Point(320,
210))
let colorButton3 = new Button(Text = "Цвет пера", Location = new
System.Drawing.Point(240, 210))
drawingForm.Controls.Add(exitButton3)
drawingForm.Controls.Add(eraseButton3)
drawingForm.Controls.Add(colorButton3)
// Создаём диалог выбора цвета
let colorDialog3 = new ColorDialog()
// Создаём объект ColorBlend
let mutable color3 = new ColorBlend()
// Инициализируем объекты рисования
let createGraphics3 = drawingForm.CreateGraphics()
createGraphics3.SmoothingMode<-SmoothingMode.HighQuality
drawingForm.Load.Add(fun background3 ->
// Устанавливаем цвет заливки по умолчанию: Indigo
color3.Colors <- [|Color.Indigo|])
// Событие перемещения мыши на форме
drawingForm.MouseMove.Add(fun trail->
// Когда мышка двигается и левая кнопка нажата
if (trail.Button = System.Windows.Forms.MouseButtons.Left) then
//Рисуем объект с выбранным цветом из диалога выбора цвета (или цвета по умолчанию)
createGraphics3.FillRectangle(new SolidBrush(color3.Colors.[0]),new
Rectangle(trail.X,trail.Y,5,5)))
// Событие нажатия кнопки "Очистить", возвращаем цвет формы и стираем всё на форме
eraseButton3.Click.Add(fun erase3-> createGraphics3.Clear(drawingForm.BackColor))

732
exitButton3.Click.Add(fun quit3 -> drawingForm.Close())
colorButton3.Click.Add(fun colors3 ->
// Вызываем диалог выбора цвета
if colorDialog3.ShowDialog() = DialogResult.OK then
// Сохраняем выбранный цвет
color3.Colors<-[|colorDialog3.Color|])
Application.Run(drawingForm)
printfn "\t\t\tНажмите клавишу Enter для продолжения..."
Console.ReadKey()
Console.Clear()

Компилируем приложение (Debug) и запускаем. Зажимаем левую кнопку мыши


на форме, рисуем, выбираем другой текст, рисуем, стираем, выходим:

Рис. 4. 3. Результат работы приложения F#: форма «Рисование мышкой»

5. Модификация приложения F#: работа с базой данных Microsoft Access

Для следующего примера понадобится простенькая база данных в формате


*.mdb. Запускаем Microsoft Access любой имеющейся версии и создаём простую
таблицу с именем Главная таблица. Ключевое поле (Счётчик): «Номер». Далее идут
два текстовых столбца: «Имя» и «Фамилия». Заполняем данными (добавляем три-
четыре записи в базу данных). Базу назовём, например Работники:

733
Рис. 5. 1. Содержимое таблицы базы данных Работники.mdb

Импортируем эту базу данных в сам проект. Для этого выполним: Проект ->
Существующий элемент... (Shift+Alt+A). После вставки базы в проект приложения
F#, перейдём на панель Свойства для файла базы и изменим значение свойства
«Копировать в выходной каталог» на «Всегда копировать».

Приготовления завершены. Теперь о коде. Код будет создавать форму с


кнопками «Выход» и «Поиск». В качестве поля для поиска будет выступать столбец
Номер. Также на форме будет DataGridView для отображения всех записей из базы.
Нажатие на «Поиск» отобразить в Label’ах другие параметры запроса. Код всего этого
дела такой:

// Создание формы Windows Forms: Работа с базой данных Microsoft Access


Console.Title <- "Различные примеры (F#) :: Работа с базой данных Microsoft Access"
open System.Data
open System.Data.OleDb
// Создаём шрифт
let ffont = new Font("Verdana", 9.75F,FontStyle.Regular, GraphicsUnit.Point)
// Создаём объект соединения с базой данных
let oleconn = new System.Data.OleDb.OleDbConnection("Provider = Microsoft.Jet.OLEDB.4.0;
Data Source = Работники.mdb")
// СоздаёмOleDbDataAdapter
let dataAdpter4 = new System.Data.OleDb.OleDbDataAdapter("Select * from [Главная
таблица]", oleconn)
// Генерируем DataSet
let dataSet4 = new DataSet()
//fills the dataset with recod values
dataAdpter4.Fill(dataSet4,"[Главная таблица]") |> ignore
// Создаём форму и элементы управления
let dataForm = new Form(Text = "Различные примеры (F#) :: Работа с базой данных Microsoft
Access", AutoScaleDimensions = new System.Drawing.SizeF(60.0F, 13.0F), ClientSize = new
System.Drawing.Size(500, 360), StartPosition = FormStartPosition.CenterScreen)
let exitButton4 = new Button(Text = "Выход", Location = new System.Drawing.Point(300,
320))
let searchButton4 = new Button(Text = "Поиск", Location = new System.Drawing.Point(220,
320))
let label14 = new Label(Text = "Введите \"Номер\":", Location = new
System.Drawing.Point(0, 10), AutoSize = true)
let label24 = new Label(Text = "Номер:", Location = new System.Drawing.Point(0, 50),
AutoSize = true)
let label34 = new Label(Text = "Имя:", Location = new System.Drawing.Point(0,100),
AutoSize = true)
let label44 = new Label(Text = "Фамилия:", Location = new System.Drawing.Point(0,150),
AutoSize = true)
let textBoxNumber4 = new TextBox(Location = new System.Drawing.Point(200,10))
let labelNumber4 = new Label(Location = new System.Drawing.Point(100,50), BorderStyle =
BorderStyle.FixedSingle)
let labelFirstName4 = new Label(Location = new System.Drawing.Point(100,100), BorderStyle
= BorderStyle.FixedSingle)
let labelLastName4 = new Label(Location = new System.Drawing.Point(100,150), BorderStyle
= BorderStyle.FixedSingle)
// Создаём DataGridView
let dataGridView4 = new DataGridView(ColumnHeadersHeightSizeMode =
DataGridViewColumnHeadersHeightSizeMode.AutoSize,Size = new System.Drawing.Size(480,
120), Location = new System.Drawing.Point(10, 180))
// Создаём столбцы DataGridView
let chrnumbercol = new DataGridViewTextBoxColumn()
let chrfnamecol = new DataGridViewTextBoxColumn()
let chrlnamecol = new DataGridViewTextBoxColumn()
// Добавляем данные в столбцы
dataGridView4.Columns.Add(chrnumbercol) |> ignore

734
dataGridView4.Columns.Add(chrfnamecol) |> ignore
dataGridView4.Columns.Add(chrlnamecol) |> ignore
dataGridView4.DataSource <- dataSet4.Tables.["[Главная таблица]"]
// Применяем шрифт для формы
dataForm.Font <- ffont
// Применям связь данных базы с DataGridView (имя стоблца в базе / имя столбца в
элементе)
chrnumbercol.DataPropertyName <- "Номер"
chrnumbercol.HeaderText <- "Номер"
chrfnamecol.DataPropertyName<-"Имя"
chrfnamecol.HeaderText<-"Имя работника"
chrlnamecol.DataPropertyName<-"Фамилия"
chrlnamecol.HeaderText<-"Фамилия работника"
// Добавляем элементы на форму
dataForm.Controls.Add(dataGridView4)
dataForm.Controls.Add(exitButton4)
dataForm.Controls.Add(searchButton4)
dataForm.Controls.Add(label14)
dataForm.Controls.Add(label24)
dataForm.Controls.Add(label34)
dataForm.Controls.Add(label44)
dataForm.Controls.Add(textBoxNumber4)
dataForm.Controls.Add(labelNumber4)
dataForm.Controls.Add(labelFirstName4)
dataForm.Controls.Add(labelLastName4)
// Связываем Label'ы со столбцами базы данных
labelNumber4.Text <- Convert.ToString(dataSet4.Tables.["[Главная
таблица]"].Rows.Item(0).Item(0))
labelFirstName4.Text <- Convert.ToString(dataSet4.Tables.["[Главная
таблица]"].Rows.Item(0).Item(1))
labelLastName4.Text <- Convert.ToString(dataSet4.Tables.["[Главная
таблица]"].Rows.Item(0).Item(2))
searchButton4.Click.Add(fun search->
// Обрабатываем номер строки индекса
let mutable introws = 0
// Определяем, была найдена запись или нет
let mutable blnfound = false
// Обрабатываем общее количество записей
let mutable inttotrec = Convert.ToInt32(dataSet4.Tables.["[Главная
таблица]"].Rows.Count)
// Обрабатываем данные вводимые пользователем
let strtext = Convert.ToString(textBoxNumber4.Text)
// До тех пор, пока совпадений не найдено и конец записей не
достигнут
while((blnfound = false) && (introws <= inttotrec-1)) do
let strempnum = Convert.ToString(dataSet4.Tables.["[Главная
таблица]"].Rows.Item(introws).Item(0))
// Сравниваем данные введённые в TextBox пользователем с нашей
таблицей ("Номер")
// Если есть совпадений, отображаем результат запроса
if strtext.ToUpper() = strempnum.ToUpper() then
blnfound<-true
labelNumber4.Text <- Convert.ToString(dataSet4.Tables.
["[Главная таблица]"].Rows.Item(introws).Item(0))
labelFirstName4.Text <- Convert.ToString(dataSet4.Tables.
["[Главная таблица]"].Rows.Item(introws).Item(1))
labelLastName4.Text <- Convert.ToString(dataSet4.Tables.
["[Главная таблица]"].Rows.Item(introws).Item(2))
// Сравниваем со следующей запись до появления совпадений
introws<-introws + 1
// Если совпадения не найдены
if blnfound = false then
MessageBox.Show("Запись не найдена!", "Работа с базой данных
Microsoft Access :: Сообщение об ошибке", MessageBoxButtons.OK,
MessageBoxIcon.Information) |> ignore)

735
exitButton4.Click.Add(fun exit->
dataForm.Close()
oleconn.Close())
Application.Run(dataForm)
printfn "\t\t\tНажмите клавишу Enter для продолжения..."
Console.ReadKey()
Console.Clear()
Application.Exit() // Завершаем приложение

Компилируем приложение (Debug) и запускаем. Вводим «номер работника» из


базы данных (смотрим все доступные записи в DataGridView на форме) в текстовое
поле и жмём «Поиск»:

Рис. 5. 1. Результат работы приложения F#: форма «Работа с базой данных Microsoft
Access»

Компилируем приложение (Release) и запускаем.

6. О приложении к Лабораторной работе № 22

Получившуюся программу (LWP22Samples.exe), собранную из кусков кода


приведённых в данной лабораторной работе, а также установочный файл шаблона
консольного приложения для F# (FSharp.Console.vsix) и архив с базой данных
Работники.mdb (DB.zip), можно загрузить по ссылке в конце этого материала (сслыка
доступна в программном продукте).

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).

736
23. Дополнительная лабораторная работа № 1

C# — Подготовка системы к написанию программ на языке C#

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


наша система готова написанию программ на «шарпах». А именно нам нужен
минимальный набор для программирования на С# и разработки приложений в .NET. В
него входит следующее:

 Microsoft Windows 2000 или выше — обязательно.


 Microsoft .NET Framework SDK — необходимо (ставиться вместе со средой
разработки).
 Microsoft Visual Studio .NET или выше — обязательно.

Ниже на рисунке показан процесс установка SDK (версии 1.1), а именно опции,
включённые перед установкой:

Рис. 1. Microsoft .NET Framework SDK — Setup (под Windows 2000)

Установлен SDK (версии 1.1) будет в папку с нашей ОС: WINNT (Windows
2000), а именно в папку Microsoft.NET.

Для Windows 7 содержимое этой папки выглядит примерно так (Рис. 2):

737
Рис. 2. Содержимое папки Microsoft.NET (Microsoft Windows 7 Professional)

Нас интересует папка Framework. Содержимое этой директории представлено


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

Рис. 3. Содержимое директории Microsoft.NET/Framework (Microsoft Windows 7


Professional)

В данном случае, папки, начинающиеся с буквы v обозначают версии


установленных Framework’ов (последний как видно из рисунка: 4.030319). Если
необходимости ставить громоздкие среды разработки приложений нет в системе не
установлен свежий Framework, то можно скачать с официального сайта пакет
обновлений: Microsoft Windows SDK for Windows 7 and .NET Framework 4 (ISO),
содержащий три образа диска (пакет бесплатный). .NET Framework 4 — на
сегодняшний день самый последний и самый полный из доступных широкой массе
пользователей сети Интернет.

Также, можно загрузить сам Microsoft .NET Framework 4 (веб-установщик)


(http://msdn.microsoft.com/ru-ru/netframework).

738
Рис. 4. Установка Windows SDK for Windows 7 and .NET Framework 4 (Web Setup)

Окончательная версия .NET Framework 4.0 была выпущена 12 апреля 2010 года
вместе с версией среды разработки Visual Studio 2010 и обновления для языка C#
(C# 4.0).

В практических работах лабораторного практикума данного курса для написания


программы были использовано следующее:

Операционная система: Microsoft Windows 7 Professional.


Установлено: .NET Framework 4.0.
То, что в дальнейшем будет использоваться: Microsoft Visual Studio 2010
Professional, Microsoft SQL Server 2008 и Microsoft Silverlight 4 SDK.

Всё выше перечисленные программные продукты, кроме ОС Windows 7,


являются бесплатными («условно» бесплатными) и их можно получить с официального
сайта корпорации Microsoft (на данный момент). Получить одну из доступных версий
среды разработки Visual Studio (2008/2010/11 Beta) также можно бесплатно по
программе Microsoft DreamSpark.
DreamSpark — программа корпорации Microsoft, предоставляющая школьникам,
студентам и аспирантам бесплатный доступ к инструментам Microsoft для разработки и
дизайна. Изначально программа распространялась на учащихся вузов, в настоящее
время она расширена также на учащихся старших классов. Программа была
анонсирована Биллом Гейтсом 18 февраля 2008 г. в Пало-Альто, США.

ПРИМЕЧАНИЕ: Установка Microsoft Visual Studio 2008, Microsoft Visual


Studio 2010 или свежего Microsoft Visual Studio 11 Beta «по умолчанию»
автоматически добавляет все необходимые компоненты для разработки приложений,
библиотек и т.п. Поэтому в данном практическом курсе предполагается, что в системе
уже установлены все необходимые для работы в среде разработки Visual Studio
компоненты (то есть сама среда, желательно версий 2008 или 2010).

24. Дополнительная лабораторная работа № 2

C# — Создание программ на языке C# без среды разработки

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

Цель данного материала это показать некоторые особенности языка Си-шарп и


то, как может быть устроена простейшая программа.

Нам необходимо всего два файла: csc.exe и cscui.dll. Оба файла можно найти в
папке Windows/Microsoft.NET/Framework (Рис. 1).

csc.exe ищем в папке v4.0.30319 (последние цифры могут изменяться в


зависимости версии Framewokr’а):

Рис. 1. Содержимое папки Microsoft.NET/Framework

Нужный нам файл это: Visual C# Command Line Compiler – попросту


компилятор приложений.

cscui.dll, называвшийся ранее cscompui.dll и работавший в паре с cscomp.dll


лежит в папке v4.0.30319/1033 (Рис. 2):

Рис. 2. Содержимое папки v4.0.30319/1033

Это библиотека необходима для компилятора: Visual C# Compiler


Error/Warning Messages.

740
Ссылки для загрузки этих файлов приведены в конце этого материала.

Копируем оба файла в папку, в которой хотим скомпилировать нашу ещё


ненаписанную программу. И далее создаём в той же папке пустой текстовый документ
(.txt) и напишем туда следующий код (пояснения будут даны чуть ниже):

using System;
using System.Globalization;

class MainApp
{
public static void Main()
{
Console.Title = "This is My First App!";
//
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.BackgroundColor = ConsoleColor.DarkBlue;
//
Console.WriteLine("Hello World!\nИ по-русски можно? Конечно, привет мир!\nThis is
my first application using C#!\nP.S. C# and F# FTW?\n");
//
Calendar MyCalendar = new GregorianCalendar();
CultureInfo MyCulture = new CultureInfo("ru-RU");
DateTime MyDate = new DateTime(2012, 12, 21, 18, 30, 0, 0);
//
Console.WriteLine("Время записанное в календаре: " +
MyCalendar.ToDateTime(MyDate.Year,
MyDate.Month,
MyDate.Day,
MyDate.Hour,
MyDate.Minute, 0, 0) + "\n");
//
DateTime now = DateTime.Now;
Console.WriteLine("Время запуска приложения: {0}\n", now);
//
Random rand = new Random();
int a = 0, b = 0;

for (int i = 0; i < 5; i++)


{
a = rand.Next() % 100;
b = rand.Next() % 100;
Console.WriteLine("a = {0}, b = {1}, больше: {2}", a, b, a > b ? 'a' : 'b');
}
//
Console.ReadKey();
}
}

Далее сохраним как Test.cs (в Блокноте: Сохранить как… -> Тип файла: Все
файлы). Такое расширение (.cs) имеют файлы кода для программ на Си-шарп (Рис. 3):

741
Рис. 3. Создание файла для компиляции

После этого, создаём в этой же папке ещё один текстовый файл, внутри
вписываем:

csc.exe Test.cs
pause

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

Рис. 4. Создание командного файла

ПРИМЕЧАНИЕ: Мы можем и не копировать эти файлы (зачем делать лишнюю


работу?), а воспользоваться возможностями .bat файлов и запустить компилятор прямо
из той же папки, где он находится (но для этого его нужно всё равно найти), поместив
затем откомпилированную им программу туда, куда нам нужно.

Способ № 1: модифицируем Compile.bat:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe Test.cs

742
pause

В итоге, скомпилируется программа Test.exe в той же папке, где находится


файл Test.cs (Compile.bat запускаем из этой же папки, где лежит наш код).

Способ № 2: модифицируем Compile.bat:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /out:D:\VS\Test.exe D:\


VS\Test.cs
pause

Здесь, параметр csc.exe /out:<file> указывает, куда именно записать


скомпилированную программу и какое дать ей имя и расширение. Второй параметр
указывает место, где хранится наш Test.cs. Этот способ предпочтительнее, так как не
имеет значения, где лежит Compile.bat, где находится файл с исходным кодом и куда
компилировать код. Единственное, чего необходимо избегать, это написания
командных файлов в Блокноте с русскими символами в пути или названиях
(предпочтительнее для этого использовать другой текстовый редактор, например: Bred
3).

Способ № 3: также мы можем воспользоваться стандартной командной строкой


(Рис. 5) Visual Studio (Пуск -> Все программы -> Microsoft Visual Studio 2010 ->
Visual Studio Tools -> Командная строка Visual Studio (2010)):

Рис. 5. Открываем командную строку через меню Пуск

В общем случае, путь прописываем как во втором способе (Рис. 6):

743
Рис. 6. Компиляция при помощи командной строки Visual Studio (2010), русские
символы допускаются

Всё. Запускаем Compile.bat. Видим следующее (в окне командной строки):

Рис. 7. Компиляция программы… Успешно!

В нашей папке появился новый файл Test.exe. Собственно это и есть наша
программа, скомпилированная только что без использования какой-либо среды
программирования. Это удобно лишь в том случае, если под рукой нет среды
разработки. Результат работы программы представлен на рисунке ниже:

744
Рис. 8. Рабочая программа (консольное приложение)

Теперь немного поясним код (ключевые слова подсвечены синим):

using System;

Конструкция выше — объявление использования пространства имён. В данном


случае использовано системное пространство имён: System (но можно использовать и
своё). Тоесть используя using, мы обращаемся к имени. В System содержится набор
основных классов, и мы здесь создаём свой класс. А вот using — это
зарезервированное слово в языке Cи-шарп. Что же это?

Директива using используется в двух случаях:

 разрешает использование типов в пространстве имён, поэтому уточнение


использования типа в этом пространстве имён не требуется: using System.Text;
 позволяет создавать псевдонимы пространства имен или типа. Это называется
директива using alias: using Project = PC.MyCompany.Project;

Ключевое слово using также используется для создания операторов using,


которые обеспечивают правильную обработку объектов IDisposable, например файлов
и шрифтов.

Как видим, также как и в C и C++ язык унаследовал символ «;» как символ
разделения операторов, однако можно писать код и без этого разделителя (частично).

Вернёмся к нашему коду:

class MainApp {

В Visual C#, весь код должен содержаться в методах класса. Таким образом, к
началу точки входа (в данном случае: в программу), необходимо сначала создать
класс. Имя класса, здесь не имеет значения. Далее, указывается точка входа:

public static void Main() {

745
Компилятор требует, что-то, что можно назвать Main(). Точка входа должна быть
маркирована как public, так и static. Кроме того, точка входа не имеет аргументов и
ничего не возвращает (но это как в и C++ зависит от сложности программ, например:
static void Main(string[] args)). Конструкция Main() должна начинаться с заглавной
буквы, но само слово – не зарезервировано (не подсвечено синим в среде Visual
Studio):

Console.Title = "This is My First App!";


//
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.BackgroundColor = ConsoleColor.DarkBlue;
//
Console.WriteLine("Hello World!\nИ по-руcски можно? Конечно, привет мир!\nThis is
my first application using C#!\nP.S. C# and F# FTW?\n");
//

Здесь всё просто. Двойной правый слеш — комментарии (зелёным). Дальше идут
функции реализующие работу с консольным приложением. А именно класс
System.Console предоставляющий стандартные потоки (входной и выходной, а также
сообщения об ошибках) консольным приложениям. Первая строчка отвечает за
заголовок окна консольного приложения (Console.Title), вторая за фон под текстом, а
третья за цвет букв. Четвёртая же выводит строку в скобках без кавычек. \n
определяется как перевод курсора на следующую строку. Если System — это
пространство имён, Console — класс, определённый в этом пространстве, то метод
WriteLine — это метод определённый в этом классе (метод аналогичен процедуре,
функции или подпрограмме).

Идём дальше по коду:

Calendar MyCalendar = new GregorianCalendar();


CultureInfo MyCulture = new CultureInfo("ru-RU");
DateTime MyDate = new DateTime(2012, 12, 21, 18, 30, 0, 0);
//
Console.WriteLine("Время записанное в календаре: " +
MyCalendar.ToDateTime(MyDate.Year,
MyDate.Month,
MyDate.Day,
MyDate.Hour,
MyDate.Minute, 0, 0) + "\n");
//

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

using System.Globalization;

В самом же коде мы видим объявление объектов класса Calendar (первая строка


– создаётся экземпляр MyCalendar с параметрами грегорианского календаря). Дальше
объявляется класс CultureInfo. Он служит для предоставления сведений об
определенном языке и региональных параметрах (которые в совокупности называются
«языковым стандартом» для разработки неуправляемого программного кода). В этих
сведениях содержатся имена языков и региональных параметров, система языка,
используемый календарь и форматы дат, а также разделители строк. Дальше идёт
DataTime с новым экземпляром MyDate которому ставится в соответствие
фиксированная дата. И наконец, мы выводим всё на экран консоли в формате
грегорианского календаря и региональными параметрами под ru-RU.

DateTime now = DateTime.Now;


Console.WriteLine("Время запуска приложения: {0}\n", now);

746
//
Random rand = new Random();
int a = 0, b = 0;

for (int i = 0; i < 5; i++)


{
a = rand.Next() % 100;
b = rand.Next() % 100;
Console.WriteLine("a = {0}, b = {1}, больше: {2}", a, b, a > b ? 'a' : 'b');
}
//

В первой строчке создаём экземпляр класса DateTime — now хранящий


текущее время. И выводим на экран вместо символов {0}. Далее по коды, мы создаём
генератор случайных чисел (rand), объявляем переменные типа int и загоняем всё это
в цикл, основанный на int i. Цикл проводит 5 итераций. За каждую итерацию
происходит генерация числа и операция % над этим числом: оператор модуля (%)
вычисляет остаток после деления первого операнда на второй. Все числовые
типы имеют предопределённые операторы модуля. И в этой же итерации выводится
строка: вместо {0} и {1} подставляются a и b, а вместо {2} результат их сравнения
(сравниваются остатки от деления).

Модифицируем приложение и посмотрим, что именно генерирует Random:

Рис. 9. Операция % не проведена над числами сгенерированными в Random

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

Во-первых, Си-шарп позволяет писать код свободно, но каждый оператор


должен заканчивать точкой с запятой*:

public static void Main() {


Console.Title =
"This is My First App!"
;//
Console.ForegroundColor = ConsoleColor.DarkYellow;Console.BackgroundColor =
ConsoleColor.DarkBlue;
//

Также в куске кода выше, показано, что можно написать код приложения даже в
одну строчку.

Во-вторых, Си-шарп чувствителен к регистру. А это значит что ConsolE, Static и


Public могут вызвать ошибки. Также важно понимать, что это касается и переменных и
операторов.

ПРИМЕЧАНИЕ (*): Однако, не всегда точка с запятой обязана быть. Например,


конструкция без точек с запятой ниже, выдаст сообщение с трёмя кнопками (Рис. 10):

class MainApp
{

747
public static void Main()
{
while (System.Windows.Forms.MessageBox.Show("Hello World without ';'! Привет мир
без точек с запятой!", "This is My First Message Box!",
System.Windows.Forms.MessageBoxButtons.YesNoCancel) !=
System.Windows.Forms.DialogResult.Yes)
{ } // До тех пор, пока не будет нажата кнопка «Да», окно не закроется
}
}

Рис. 10. Результат работы кода без точек с запятой (нужно нажать «Да»)

Получившуюся программу (Test.exe), собранную из кусков кода приведённых в


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

25. Дополнительная лабораторная работа № 3

C# — Солнечная система в ListBox (WPF)

Содержание

1. Вводная часть
2. Создание приложения Windows Foundation Presentation
3. Модификация приложения Windows Foundation Presentation:
добавление ресурсов
4. Модификация приложения Windows Foundation Presentation:
добавление исходного кода
5. Модификация приложения Windows Foundation Presentation:
оформление элемента ListBox
6. Завершающая часть
7. О приложении к данной работе

1. Вводная часть

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


для элемента управления ListBox (списка) средствами Windows Foundation Presentation.
В качестве конечной цели выберем отображение орбит и планет в элементе и
полностью перерисуем оформление. Сделаем фон списка чёрным и изменим
отображение каждого элемента. Элементы будут являться изображениями, при
наведении мышки на элементы будут всплывать подсказки. Также до границ ListBox от
каждого элемента будет отходить окружности (орбиты планет). Элементы будут
располагаться на этих окружностях.

2. Создание приложения Windows Foundation Presentation

748
Запускаем Visual Studio 2010, откроется Начальная страница:

Для начала, надо создать проект, для этого выполним последовательно: Файл -
> Создать -> Проект… (также можно просто нажать сочетание клавиш Ctrl+Shift+N
или пункт «Создать проект…» на Начальной странице):

Рис. 2. 1. Создание нового проекта

Выберем слева в пункте Установленные шаблоны язык Visual C#, далее


найдём в списке Приложение WPF. Также здесь можно выбрать какой использовать
«фреймворк» (набора компонентов для написания программ, конечную платформу). В
нашем случае выберем .NET Framework 4.

749
Рис. 2. 2. Окно создания нового проекта

В поле Имя вводим NewListBox — это название программы (выбрано по


названию лабораторного практикума, номеру и названию работы). В поле
Расположение указана конечная директория, где будет находиться весь проект.
Выберем расположение удобное для быстрого поиска. В поле Имя решения вводится
либо название программы «по умолчанию» из поля Имя автоматически, либо можно
ввести своё собственное. Под этим именем будет создана конечная папка проекта (если
Имя и Имя решения разные).

750
Рис. 2. 3. Вводим данные нового проекта приложений WPF

После нажатия кнопки ОК мы увидим сформированный проект и исходный код


приложения WPF (не пустого изначально). Среда разработки сформировала исходный
код в двух файлах (не считая стандартного AssemblyInfo.cs). Один из файлов
отвечает за программу в целом (это файл App.xaml.cs), а второй за формирование
окна приложения — инициализацию конструктора формы и всех её элементов (это
файл MainWindow.xaml.cs). Как видим, исходный код, например файла App.xaml.cs
не отличается от шаблонного:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;

namespace NewListBox
{
/// <summary>
/// Логика взаимодействия для App.xaml
/// </summary>
public partial class App : Application
{
}
}

XAML-код App.xaml выглядит так:

<Application x:Class="NewListBox.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

751
StartupUri="MainWindow.xaml">
<Application.Resources>

</Application.Resources>
</Application>

Файл MainWindows.xaml.cs содержит следующий исходный код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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 NewListBox
{
/// <summary>
/// Логика взаимодействия для MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

XAML-код MainWindows.xaml выглядит так:

<Window x:Class="NewListBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>

</Grid>
</Window>

752
Рис. 2. 4. Обозреватель решений: состав проекта приложения WPF сформированного
средой разработки

Теперь посмотрим текущий макет нашей главной формы, которая должна


запуститься в качестве главного окна:

753
Рис. 2. 5. Макет формы MainWindows.xaml: отображение конструктора формы (сверху)
и представление этой формы в качестве рамзетки XAML (внизу)

Теперь, можно откомпилировать созданную программу, нажав клавишу F5 (

Отладка -> Начать отладку или нажав на иконку . Тем самым мы


запускаем приложение в режиме отладки (и производим компиляцию debug-версии
программы) (Debug выбрано изначально).

Рис. 2. 6. Запуск приложения WPF по конфигурации Debug

3. Модификация приложения Windows Foundation Presentation: добавление


ресурсов

Для начала добавим все необходимые ресурсы, ими станут картинки


планет.

Выделим в обозревателе решений название проекта ( ),


нажнём на нём правую кнопку мыши и далее во всплывающем меню выберем
Добавить -> Создать папку:

754
Рис. 3. 1. Создание новой директории для файлов внутри проекта

Теперь введём Planets в названии и получим новую директорию в


обозревателе решений:

Теперь добавим в эту директорию изображения планет. Выполним для


иконки проекта в обозревателе решений Добавить -> Существующий элемент…
или в верхнем меню Проект -> Существующий элемент… (Shift+Alt+A) для
всех следующих изображений:

Солнце (Sun.jpg):

Меркурий (Mercury.gif):

Венера (Venus.gif):

755
Земля (Earth.gif):

Марс (Mars.gif):

Юпитер (Jupiter.gif):

Сатурн (Saturn.gif):

Уран (Uranus.gif):

Нептун (Neptune.gif):

756
Плутон (Pluto.gif):

Получим список изображений в обозревателе решений:

4. Модификация приложения Windows Foundation Presentation:


добавление исходного кода

Добавим новый класс для работы с будущим элементом управления. Для


начала поработаем с ресурсами изображений. Добавим новый файл класса C# с
коллекцией элементов с именем SolarSystem.cs:

Исходный код добавленного файла изначально такой:

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

namespace NewListBox.Planets
{
class SolarSystem
{
}
}

Модифицируем файл следующим образом:

using System;

757
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Collections.ObjectModel;

namespace NewListBox
{
public class SolarSystem
{
// объявление коллекции динамических данных
ObservableCollection<SolarSystemObject> и заполнение коллекции элементами
private ObservableCollection<SolarSystemObject> solarSystemObjects;

public ObservableCollection<SolarSystemObject> SolarSystemObjects // Свойство для


работы с коллекцией элементов
{
get { return solarSystemObjects; }
}

public SolarSystem()
{
this.solarSystemObjects = new ObservableCollection<SolarSystemObject>();
/* Добавляем новые объекты типа SolarSystemObject(Название, число: удаление
от солнца [а. е.], число: средний диаметр [км], ссылка на изображение внутри проекта,
описание) */
this.solarSystemObjects.Add(new SolarSystemObject("Солнце", 0, 1380000, new
Uri(@"Planets\Sun.jpg", UriKind.Relative), "Со́лнце — единственная звезда Солнечной
системы, вокруг которой обращаются другие объекты этой системы: планеты и их спутники,
карликовые планеты и их спутники, астероиды, метеороиды, кометы и космическая пыль."));
this.solarSystemObjects.Add(new SolarSystemObject("Меркурий", 0.38, 4880, new
Uri(@"Planets\Mercury.gif", UriKind.Relative), "Мерку́рий — самая близкая к Солнцу планета
Солнечной системы, обращающаяся вокруг Солнца за 88 земных суток."));
this.solarSystemObjects.Add(new SolarSystemObject("Венера", 0.72, 12103.6,
new Uri(@"Planets\Venus.gif", UriKind.Relative), "Вене́ра — вторая внутренняя планета
Солнечной системы с периодом обращения в 224,7 земных суток. Планета получила своё
название в честь Венеры, богини любви из римского пантеона. Венера — третий по яркости
объект на небе Земли после Солнца и Луны и достигает видимой звёздной величины в
−4,6."));
this.solarSystemObjects.Add(new SolarSystemObject("Земля", 1, 12756.3, new
Uri(@"Planets\Earth.gif", UriKind.Relative), "Земля́ — третья от Солнца планета Солнечной
системы, крупнейшая по диаметру, массе и плотности среди планет земной группы. Наша
родная планета."));
this.solarSystemObjects.Add(new SolarSystemObject("Марс", 1.52, 6794, new
Uri(@"Planets\Mars.gif", UriKind.Relative), "Марс — четвёртая по удалённости от Солнца и
седьмая по размерам планета Солнечной системы; масса планеты составляет 10,7 % массы
Земли."));
this.solarSystemObjects.Add(new SolarSystemObject("Юпитер", 5.20, 142984, new
Uri(@"Planets\Jupiter.gif", UriKind.Relative), "Юпи́тер — пятая планета от Солнца,
крупнейшая в Солнечной системе. Наряду с Сатурном, Ураном и Нептуном Юпитер
классифицируется как газовый гигант. Планета была известна людям с глубокой древности,
что нашло своё отражение в мифологии и религиозных верованиях различных культур:
месопотамской, вавилонской, греческой и других. Современное название Юпитера происходит
от имени древнеримского верховного бога-громовержца."));
this.solarSystemObjects.Add(new SolarSystemObject("Сатурн", 9.54, 120536, new
Uri(@"Planets\Saturn.gif", UriKind.Relative), "Сату́рн — шестая планета от Солнца и вторая
по размерам планета в Солнечной системе после Юпитера. Сатурн, а также Юпитер, Уран и
Нептун, классифицируются как газовые гиганты. Сатурн назван в честь римского бога
земледелия."));
this.solarSystemObjects.Add(new SolarSystemObject("Уран", 19.218, 51118, new
Uri(@"Planets\Uranus.gif", UriKind.Relative), "Ура́н — седьмая по удалённости от Солнца,
третья по диаметру и четвёртая по массе планета Солнечной системы. Была открыта в 1781
году английским астрономом Уильямом Гершелем и названа в честь греческого бога неба
Урана, отца Кроноса (в римской мифологии Сатурна) и, соответственно, деда Зевса."));

758
this.solarSystemObjects.Add(new SolarSystemObject("Нептун", 30.06, 49532, new
Uri(@"Planets\Neptune.gif", UriKind.Relative), "Непту́н — восьмая и самая дальняя планета
Солнечной системы. Нептун также является четвёртой по диаметру и третьей по массе
планетой. Масса Нептуна в 17,2 раза, а диаметр экватора в 3,9 раза больше таковых у
Земли. Планета была названа в честь римского бога морей."));
this.solarSystemObjects.Add(new SolarSystemObject("Плутон", 39.5, 2274, new
Uri(@"Planets\Pluto.gif", UriKind.Relative), "Плуто́н (134340 Pluto) — крупнейшая по
размерам, наряду с Эридой, карликовая планета Солнечной системы, транснептуновый объект
(ТНО) и девятое/десятое по величине небесное тело, обращающееся вокруг Солнца.
Первоначально Плутон классифицировался как планета, однако сейчас он считается одним из
крупнейших объектов (возможно, самым крупным) в поясе Койпера."));
}
}
}

Теперь, добавим сам класс, обеспечивающий обработку и получение


данных из коллекции элементов. Создаём новый класс с именем
SolarSystemObject.cs:

Исходный код сделаем таким:

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

namespace NewListBox
{
public class SolarSystemObject
{
// Название планеты
private string name;

public string Name


{
get { return name; }
set { name = value; }
}
// Орбита, в астрономических единицах (а. е)
private double orbit;

public double Orbit


{
get { return orbit; }
set { orbit = value; }
}
// Средний диаметр, в км
private double diameter;

public double Diameter


{
get { return diameter; }
set { diameter = value; }
}
// Относительная ссылка на изображение
private Uri image;

public Uri Image


{
get { return image; }
set { image = value; }

759
}
// Описание
private string details;

public string Details


{
get { return details; }
set { details = value; }
}
// Метод для работы с коллекцией элементов
public SolarSystemObject(string name, double orbit, double diameter, Uri
image, string details)
{
this.name = name;
this.orbit = orbit;
this.diameter = diameter;
this.image = image;
this.details = details;
}
// Вместо new использован virtual-override
public override string ToString()
{
return this.name;
}
}
}

Немного о ключевом слове override в самом конце кода:

Рис. 4. 1. Разница между new и override (наследование и переопределение


методов)

При использовании new метод класса Foo не перекрывает реализацию


базового класса, а подменяет её своей (прячет её). Поэтому когда мы
сохраняем переменную типа в переменной базового типа от класса Bar, мы не
получим вызова override метода из класса Foo , так как не переопределяется, а
подменяется метод используя new. При использовании virtual-override
происходит виртуальный вызов и будет вызван переопределённый (overriden)
метод из класса Bar.

760
Добавим последний необходимый класс для обработки положения
окружностей орбит в элементе управления. Назовём его ConvertOrbit.cs:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.Globalization;

namespace NewListBox
{
public class ConvertOrbit : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
double orbit = (double)value;
double factor = System.Convert.ToDouble(parameter, culture.NumberFormat); //
Получаем форматирование для единиц измерения (зависит от настроек операционной системы по
умолчанию, а именно от региона и региональных стандартов), parameter - получаем извне
return Math.Pow(orbit / 40, 0.4) * 700 * factor; // Возводим выражение за
запятой в степень 0.4, необходимо для определения положения в элементе управления в
пикселях? определяет значения для положения по X и Y, 700 - размер окна, factor -
получаем извне (parameter)
}

public object ConvertBack(object value, Type targetType, object parameter,


CultureInfo culture)
{
throw new NotSupportedException("Этот метод не должен быть вызван");
}
}
}

5. Модификация приложения Windows Foundation Presentation: оформление


элемента ListBox

Теперь осталось нарисовать новый элемент для главного окна


MainWindow.xaml. Отредактируем код следующим образом:

<Window x:Class="NewListBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NewListBox"
Title="WPF :: Солнечная система в ListBox (C#)" Height="700" Width="700">
<!-- Ресурсы окна -->
<Window.Resources>

</Window.Resources>

<Grid HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="True">

</Grid>
</Window>

Для начала мы объявили привязку окна в класс NewListBox при помощи


строчки в свойства кона:

xmlns:local="clr-namespace:NewListBox"

Поменяли свойство Title, высоту (700) и ширину (700) окна. Для сетки
(Grid) установили положение внутренних элементов по вертикали и по

761
горизонтали (по центру), а также задали свойство отсечения лишнего у
элемента, чтобы сетка помещалась в окне. И в конце объявили пространство
объектов для ресурсов окна.

Перетащим с панели инструментов элемент ListBox и очистим его


содержимое. Изменим для этого элемента свойство Focusable на False, и для
свойство ItemsSource пропишем Binding:

<Grid HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="True">


<!-- Устанавливаем сам элемент ListBox по сетке Grid -->
<ListBox ItemsSource="{Binding Source={StaticResource solarSystem},
Path=SolarSystemObjects}" Focusable="False" />
</Grid>

Для ресурсов окна сделаем следующее:

<!-- Ресурсы окна -->


<Window.Resources>
<!-- Переопределяем класс и подключаем объекты класса -->
<local:SolarSystem x:Key="solarSystem" />
<local:ConvertOrbit x:Key="convertOrbit" />
<!-- Устанавливаем шаблон для данных -->
<DataTemplate DataType="{x:Type local:SolarSystemObject}">
<Canvas Width="20" Height="20">

</Canvas>
</DataTemplate>
</Window.Resources>

Внутри поставлен элемент Canvas с размерами высоты и ширины в 20.


Заполним элемент Canvas, элементом окружности и элементом изображения,
тем самым создаём шаблон данных для элемента внутри ListBox (элемента
списка):

<Canvas Width="20" Height="20">


<!-- Окружность орбиты (элемент Ellipse) -->
<!-- Через Binging конвертируем число типа Double Orbit из класса
SolarSystemObject? Left для отступа по левому края элемента Ellipse; Top для отступа
сверху -->
<Ellipse
Canvas.Left="{Binding Path=Orbit, Converter={StaticResource
convertOrbit}, ConverterParameter=-1.707}"
Canvas.Top="{Binding Path=Orbit, Converter={StaticResource
convertOrbit}, ConverterParameter=-0.3}"
Width="{Binding Path=Orbit, Converter={StaticResource convertOrbit},
ConverterParameter=2}"
Height="{Binding Path=Orbit, Converter={StaticResource convertOrbit},
ConverterParameter=2}"
Stroke="AliceBlue"
StrokeThickness="1" />
<!-- Через Binding получаем путь до изображения для элемента Image -->
<Image Source="{Binding Path=Image}" Width="20" Height="20">
<!-- Вставка элемента изображения для планет и размер изображения для
вывода -->
<Image.ToolTip>
<!-- Подсказка при наведении мыши на элемент в ListBox -->
<StackPanel Width="250" TextBlock.FontSize="12">
<!-- Устанавливаем размер подсказки по ширине и размер шрифта
-->
<!-- Используем элемент типа StackPanel для организации одной
строки и вставки TextBlock -->
<TextBlock FontWeight="Bold" Text="{Binding Path=Name}" />

762
<StackPanel Orientation="Horizontal">
<TextBlock Text="Орбита: " />
<TextBlock Text="{Binding Path=Orbit}" />
<TextBlock Text=" а. е." />
</StackPanel>
<TextBlock Text="{Binding Path=Details}"
TextWrapping="Wrap"/>
</StackPanel>
</Image.ToolTip>
</Image>
</Canvas>

Каждый элемент внутри списка будет представлен изображением,


всплывающей подсказкой при наведении на изображении (название планеты,
орбита, описание планеты), а также пустой окружностью проходящей позади
изображения (параметры окружности определим как зависимость положение
для левой верхней точки окружности внутри Canvas: слева и сверху).
Класс ConvertOrbit организовывает связь положения со значением
орбиты.

Теперь установим связь шаблона данных и стиля одного элемента, а


также добавим событие выделения элемента в списке через Trigger, для этого
добавим новый стиль одного элемента (после закрывающего тэга
</DataTemplate> и до закрывающего тэга </Window.Resources>):

</DataTemplate>
<!-- Стиль одного элемента в ListBox -->
<Style TargetType="ListBoxItem">
<Setter Property="Canvas.Left" Value="{Binding Path=Orbit,
Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
<!-- Отступ для элемента слева -->
<Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit,
Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
<!-- Отступ для элемента снизу -->
<Setter Property="Template">
<!-- Меняем шаблон -->
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<!-- Вставляем в элемент ListBoxItem сетку -->
<!-- Вставляем окружность выделения -->
<Ellipse x:Name="selectedPlanet" Margin="-10"
StrokeThickness="4"/>
<!-- Устанавливаем радиус окружности (-10, -10, -10, -10) и
толщину пера (4) -->
<!-- ContentPresenter сообщает что окружность должна
отрисовывать не поточечное, с горизонтальным положением и вертикальным положением в
зависимости от шаблона первоначального элемента -->
<ContentPresenter SnapsToDevicePixels="{TemplateBinding
SnapsToDevicePixels}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>
<ControlTemplate.Triggers>
<!-- Событие "если выделен элемент" для ListBoxItem -->
<Trigger Property="IsSelected" Value="true">
<!-- Устанавливаем цвет и тип пера "штрих" -->
<Setter Property="Stroke" TargetName="selectedPlanet"
Value="Green"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>

763
</Setter>
</Style>
</Window.Resources>

В окне конструктора для MainWindow.xaml отобразится следующее:

Рис. 5. 1. Незаконченное изменения стиля элемента управления ListBox

Попробуем откомпилировать и запустить. Увидим что элементы (планеты)


выделяются, появляется зелёное выделение и подсказка с полным описанием.

Наконец, изменим главный стиль ListBox (добавим XAML-код после


закрывающего тэга </Style>:

</Style>
<!-- Стиль элемента управления ListBox -->
<Style TargetType="ListBox">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<!-- Конечный размер ListBox, фон -->
<Canvas Width="590" Height="590" Background="Black" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>

Готово. В конструкторе изменится вид представления окна…

6. Завершающая часть

764
Компилируем приложение (Release) и запускаем. Результат работы показан
ниже (Рис. 6. 1):

Рис. 6. 1. Модифицированное приложение Windows Foundation Presentation (Выделена


планета и высвечена подсказка)

7. О приложении к данной работе

Получившуюся программу (NewListBox.exe), собранную из кусков кода


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

Приложение: Исходный код программы и всех сопровождающих файлов с


кодом приведён по ссылке в конце этого материала (сслыка доступна в программном
продукте).

765

Вам также может понравиться