При выполнении в лабораторной работе упражнения №1 , а именно при выполнении нижеследующего кода: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Microsoft.Xna.Framework.Graphics;
namespace Application1 { public partial class MainForm : Form { // Объявим поле графического устройства для видимости в методах GraphicsDevice device;
public MainForm() { InitializeComponent();
// Подпишемся на событие Load формы this.Load += new EventHandler(MainForm_Load);
// Попишемся на событие FormClosed формы this.FormClosed += new FormClosedEventHandler(MainForm_FormClosed); }
void MainForm_FormClosed(object sender, FormClosedEventArgs e) { // Удаляем (освобождаем) устройство device.Dispose(); // На всякий случай присваиваем ссылке на устройство значение null device = null; }
void MainForm_Load(object sender, EventArgs e) { // Создаем объект представления для настройки графического устройства PresentationParameters presentParams = new PresentationParameters(); // Настраиваем объект представления через его свойства presentParams.IsFullScreen = false; // Включаем оконный режим presentParams.BackBufferCount = 1; // Включаем задний буфер // для двойной буферизации // Переключение переднего и заднего буферов // должно осуществляться с максимальной эффективностью presentParams.SwapEffect = SwapEffect.Discard; // Устанавливаем размеры заднего буфера по клиентской области окна формы presentParams.BackBufferWidth = this.ClientSize.Width; presentParams.BackBufferHeight = this.ClientSize.Height;
// Создадим графическое устройство с заданными настройками device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, this.Handle, presentParams); }
protected override void OnPaint(PaintEventArgs e) { device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);
base.OnPaint(e); } } } Выбрасывается исключение: Невозможно загрузить файл или сборку "Microsoft.Xna.Framework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d" или один из зависимых от них компонентов. Не удается найти указанный файл. Делаю все пунктуально. В чем может быть проблема? |
Работа с потоками данных
Упражнение 7. Байтовый поток MemoryStream
Для определения MemoryStream приведем выдержку из MSDN, поскольку точнее и не скажешь: " Класс MemoryStream создает потоки, которые в качестве резервного хранилища имеют память, а не подключение к сети или диску. Класс MemoryStream инкапсулирует данные, хранимые в виде массива байтов без знака, инициализированного при создании объекта MemoryStream . Массив может быть создан пустым. К инкапсулированным данным в памяти можно получить доступ напрямую. Потоки памяти могут снижать потребность во временных буферах и файлах в приложении ".
Поток MemoryStream, как и FileStream, тоже относится к рабочим потокам и допускает использование как напрямую, так и с помощью надстроек-оболочек. Для создания потока MemoryStream имеется несколько перегрузок конструктора и необходимые члены, приведенные в таблице
Упражнение построим в виде нескольких примеров, которые программно реализуем в методах графического приложения с консольным окном. Эти примеры позволят всесторонне проиллюстрировать возможности применения MemoryStream. В интерфейсном окне разместим список со ссылками для выбора примера, а результат выполнения будем выводить на консоль. Для разнообразия интерфейсное окно создадим на основе технологии WPF.
- Командой File/Add/New Project добавьте к решению новый проект с именем App7, заполнив окно мастера так
- При открытом на редактирование файле Window1.xaml выполните команду Project/Set as StartUp Project, чтобы назначить новый проект стартовым
- В панели Solution Explorer раскройте узел App7, двойным щелчком на папке Properties вызовите панель конструктора проектов и для вкладки Application установите выпадающий список Output type в значение Console Application, чтобы включить консольное окно
Вначале сделаем заготовку для выбора примеров.
- Отредактируйте разметку файла Window1.xaml следующим образом
<Window x:Class="App7.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Упражнение 7. Поток MemoryStream" Height="300" Width="300" MinHeight="300" MinWidth="300" ResizeMode="CanResizeWithGrip" WindowStartupLocation="CenterScreen" Closed="Window_Closed" > <ListBox SelectionChanged="ListBox_SelectionChanged" ScrollViewer.VerticalScrollBarVisibility="Auto"> <ListBox.Resources> <!--Selected color when the ListBox is selected--> <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Aqua" /> </ListBox.Resources> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10397.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 1</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10398.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 2</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10402.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 3</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10423.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 4</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10418.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 5</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10440.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 6</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10434.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 7</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10436.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 8</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10438.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 9</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10424.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 10</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="Smiles/10408.jpg" Width="24" Height="24" Margin="2" /> <Label VerticalContentAlignment="Center">Пример 11</Label> </StackPanel> </ListBox> </Window>
- В панели Solution Explorer вызовите контекстное меню для узла App7 и командой Add/New Folder добавьте к корню проекта папку Smiles для хранения рисунков
- В панели Solution Explorer вызовите контекстное меню для папки Smiles и командой Add/Existing Item скопируйте в нее все рисунки из папки Source/Smiles, прилагаемой к данной работе (предварительно установите фильтр диалогового окна в значение All Files )
- Заполните файл процедурного кода Window1.xaml.cs заготовкой, чтобы в итоге он стал таким
using System; using System.Collections.Generic; 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; namespace App7 { public partial class Window1 : Window { // Объект для сохранения параметров приложения Properties.Settings settings = new App7.Properties.Settings(); public Window1() { // Настройка консоли Console.ForegroundColor= ConsoleColor.White; Console.CursorVisible = false; Console.Title = "Упражнение 7. Поток MemoryStream"; InitializeComponent(); } private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Очистим консоль перед новым примером //Console.Clear(); switch (((ListBox)sender).SelectedIndex + 1) { case 1: // Пример 1 Console.WriteLine("Пример 1"); Example1(); break; case 2: // Пример 2 Console.WriteLine("Пример 2"); Example2(); break; case 3: // Пример 3 Console.WriteLine("Пример 3"); Example3(); break; case 4: // Пример 4 Console.WriteLine("Пример 4"); Example4(); break; case 5: // Пример 5 Console.WriteLine("Пример 5"); Example5(); break; case 6: // Пример 6 Console.WriteLine("Пример 6"); Example6(); break; case 7: // Пример 7 Console.WriteLine("Пример 7"); Example7(); break; case 8: // Пример 8 Console.WriteLine("Пример 8"); Example8(); break; case 9: // Пример 9 Console.WriteLine("Пример 9"); Example9(); break; case 10: // Пример 10 Console.WriteLine("Пример 10"); Example10(); break; case 11: // Пример 11 Console.WriteLine("Пример 11"); Example11(); break; } } void Example1() { } void Example2() { } void Example3() { } void Example4() { } void Example5() { } void Example6() { } void Example7() { } void Example8() { } void Example9() { } void Example10() { } void Example11() { } private void Window_Closed(object sender, EventArgs e) { // Сохраняем параметры приложения } } }
- Запустите приложение и испытайте работу заготовок вариантов, должно получиться примерно следующее
Конечно, в данном упражнении столько примеров у нас не наберется, но эта заготовка нам еще пригодится для выполнения других упражнений. Теперь можно приступать непосредственно к рассмотрению примеров использования потока MemoryStream, заключая код каждого из них в свою функцию ExampleX().
Пример 1. Из MSDN
Приведем пример из MSDN (http://msdn.microsoft.com/ru-ru/library/system.io.memorystream%28v=VS.90%29.aspx), который иллюстрирует некоторые аспекты применения класса MemoryStream.
- В файле Window1.xaml измените заголовок списка для ' Пример 1 ' на ' Пример 1. Код из MSDN '
- Заполните функцию Example1() следующим кодом
void Example1() { int count; byte[] byteArray;// Объявление итогового массива байтов, считанного из потока char[] charArray;// Объявление итогового массива символов, считанного из потока UnicodeEncoding unicodeEncoding = new UnicodeEncoding();// Двухбайтный UTF-16 // Генерируем исходные данные byte[] bytes1 = // Байтовый массив с допустимыми символами unicodeEncoding.GetBytes("Недопустимые символы в пути файла:\r\n"); byte[] bytes2 = // Байтовый массив с недопустимыми в путях символами unicodeEncoding.GetBytes(System.IO.Path.GetInvalidPathChars()); // Создает и открываем поток для чтения/записи с внутренним // байтовым буфером изменяемой длины и нулевой начальной емкостью using (MemoryStream memoryStream = new MemoryStream()) { // Пишем в поток допустимые символы блоком memoryStream.Write(bytes1, 0, bytes1.Length); // Пишем в поток недопустимые символы побайтно count = 0; while (count < bytes2.Length) { memoryStream.WriteByte(bytes2[count++]); } // Сбрасываем буфер memoryStream.Flush(); // Необязательно, данных немного // Печатаем свойства потока на консоль Console.WriteLine("\nСвойства потока MemoryStream:"); Console.WriteLine( "Capacity = {0}, Length = {1}, Position = {2}\n", memoryStream.Capacity.ToString(), // Длина внутреннего буфера memoryStream.Length.ToString(), // Количество байт содержимого memoryStream.Position.ToString() // Текущая позиция указателя ); // Печатаем исходные данные Console.WriteLine("Исходные данные перед записью в MemoryStream:"); Console.Write(unicodeEncoding.GetString(bytes1)); // Здесь Write() charArray = new char[unicodeEncoding.GetCharCount( bytes2, 0, bytes2.Length)]; unicodeEncoding.GetDecoder().GetChars( bytes2, 0, bytes2.Length, charArray, 0); Console.WriteLine(charArray); // Читаем содержимое потока byteArray = new byte[memoryStream.Length]; // Создаем массив под все символы // Устанавливаем указатель в начало потока memoryStream.Seek(0, SeekOrigin.Begin); // Читаем блоком первые 20 байт count = memoryStream.Read(byteArray, 0, 20); // Читаем остальное побайтно, наращивая счетчик while (count < memoryStream.Length) { byteArray[count++] = Convert.ToByte(memoryStream.ReadByte()); } // Преобразуем батовый массив в символьный и печатаем Console.WriteLine("\nДанные, прочитанные из MemoryStream:"); charArray = new char[unicodeEncoding.GetCharCount( byteArray, 0, count)]; unicodeEncoding.GetDecoder().GetChars( byteArray, 0, count, charArray, 0); Console.WriteLine(charArray); } }
Приведенный код совсем несложный, подробно прокомментирован и в дальнейших пояснениях вряд ли нуждается.
- Исполните Пример 1 - должен получиться такой результат
Обратите внимание на звуковой сигнал, который издает компьютер при выводе на консоль недопустимых управляющих символов. Когда для потока памяти не определен внешний неизменяемый байтовый массив, а работает внутренний буфер, то при необходимости его длина увеличивается дискретными блоками по 128 байт, удобными для тактовой обработки в регистрах процессора целиком. Емкость потока, которая автоматически увеличивается должным образом посредством перераспределения, может быть уменьшена с помощью явного задания свойства Capacity.
Пример 2. Создание файловой библиотеки рисунков
Сюжет предыдущего примера состоял в том, что приложение генерировало какую-то информацию, которую мы записывали в некоторый буфер потока MemoryStream, а затем извлекали ее для проверки, выводя на консоль. Более полезным местом применения MemoryStream будет избирательное извлечение информации из хранилища данных (файл или база данных), обработка ее и, либо представление пользователю, либо отправка на дальнейшее хранение.
Рассмотрим задачу упаковки набора рисунков определенного формата в библиотечный файл с целью дальнейшего избирательного их извлечения для представления пользователю. В этом примере мы создадим утилиту, которая в выбранном пользователем каталоге собирает по шаблону соответствующие рисуночные файлы, создает библиотечный файл, присоединяет к нему поток FileStream и копирует в него содержимое этих файлов. Библиотечный файл будет состоять из служебного заголовка, позволяющего быстро определить местонахождение рисунка в теле, и самого тела с рисунками.
Поскольку объем кода задуманного примера предполагается большим, то вынесем его в отдельный файл частичного класса Window1.
- В панели Solution Explorer вызовите контекстное меню для узла App7 и командой Add/Class создайте новый файл с именем Example2.cs, заготовку которого заполните так
using System; using System.Collections.Generic; using System.Text; // Дополнительные пространства имен using System.IO; namespace App7 { partial class Window1 { void Example2() { Console.WriteLine("Вынесенный в отдельный файл код функции Example2()"); } } }
- Удалите из файла Window1.xaml.cs каркас функции Example2(), поскольку он теперь в другом месте, и запустите приложение - заготовка Примера 2 работает нормально
В этой функции мы создадим код, упаковывающий рисунки в файловую библиотеку, поэтому
- В файле Window1.xaml измените заголовок списка для ' Пример 2 ' на ' Пример 2. Создание файловой библиотеки '
Прежде всего добавим диалог смены каталога для перехода в папку с рисунками. Для этого воспользуемся библиотечным классом FolderBrowserDialog библиотеки System.Windows.Forms.dll. Поскольку мастер при создании заготовки проекта WPF не подключает ссылку на эту библиотеку автоматически, выполним это вручную.
- В панели Solution Explorer вызовите контекстное меню для папки App7/References и командой Add Reference подключите к проекту библиотеку System.Windows.Forms.dll
Формат библиотечного файла выберем таким, как показано на рисунке
Он определит и код создания библиотеки, который выполнит задачи в следующей последовательности:
- В выбранном каталоге извлечем полные имена файлов рисунков и измерим их количество
- Обработаем массив с именами файлов, удалив из них пути и расширения
- Отсортируем массив имен файлов для быстрого их поиска в будущей библиотеке
- Откроем файловый поток, запишем в него число рисунков ( block1 ) и отсортированные имена файлов рисунков ( block2 )
- По отсортированным именам будем считывать рисунки и добавлять их в файловый поток ( block3 ). Одновременно в памяти будем формировать поток с информацией о локализации рисунков в файловом потоке ( block4 )
- После завершения записи всех рисунков допишем в поток памяти позицию ( block5 ), с которой в библиотеке начнет располагаться block4, и сбросим этот поток памяти в файловый поток библиотеки
Размещение блока с рисунками ( block3 ) перед информацией о их локализации в библиотеке ( block4 ) обусловлена тем, что рисунки могут быть большими и их удобнее писать сразу в файл. А информацию о их месте расположения в файле постепенно сформировать в потоке памяти и добавить последней. Изложенная схема позволит в будущем быстро определить место нахождения нужного рисунка и извлечь его из библиотеки. Разумеется, подобных схем можно придумать множество, главное, чтобы они взаимно согласовывались при записи и чтении библиотечных данных.
- В соответствии с вышесказанным модифицируйте файл Example2.cs следующим кодом (код файла приводится полностью, является окончательным и обжалованию не подлежит!)
using System; using System.Collections.Generic; using System.Text; // Дополнительные пространства имен using System.IO; namespace App7 { using FORMS = System.Windows.Forms; // Псевдоним partial class Window1 { FORMS.FolderBrowserDialog dialogFolder = new FORMS.FolderBrowserDialog(); String nameLibrary = "Pictures.my.lib"; // Будет находиться вместе с рисунками String extensionPictures = "jpg"; // Расширения файлов рисунков void Example2() { // Диалог смены каталога String currentDirectory; // Полный путь dialogFolder.Description = "Выберите каталог, где находятся файлы с рисунками"; dialogFolder.ShowNewFolderButton = false;// Отключить кнопку создания новой папки dialogFolder.RootFolder = Environment.SpecialFolder.MyComputer;// Компьютер FORMS.DialogResult result = dialogFolder.ShowDialog(); // Запускаем диалог выбора папки // Если был отказ или диалог закрыт системной кнопкой if (result == FORMS.DialogResult.Cancel) return; currentDirectory = dialogFolder.SelectedPath;// Извлекаем новый каталог // Принудительная перерисовка окна WPF this.Height = this.ActualHeight + 1; this.Height = this.ActualHeight - 1; // Читаем полные имена всех файлов текущего каталога по заданному шаблону String[] files = Directory.GetFiles(currentDirectory, "*." + extensionPictures); if (files.Length == 0) { FORMS.MessageBox.Show( "Нет в выбранном каталоге файлов для упаковки", "Предупреждение", FORMS.MessageBoxButtons.OK, FORMS.MessageBoxIcon.Information); return; } // Получаем сокращенные имена файлов без пути и расширения for (int i = 0; i < files.Length; i++) { String str = files[i].Substring(files[i].LastIndexOf('\\') + 1); int len = str.LastIndexOf('.'); files[i] = str.Substring(0, len); } // Сортируем массив для красоты и порядка Array.Sort<String>(files); // Array.Sort<String>(files, new ComparerNames());//Альтернативный компаратор!!!!!! // Создаем поток MemoryStream переменной длины для имен файлов MemoryStream memoryStream = new MemoryStream(); if (!memoryStream.CanWrite) // Проверять необязательно! { FORMS.MessageBox.Show("Не могу писать в MemoryStream"); return; } // Создаем писатель-оболочку для записи в MemoryStream элементарных типов BinaryWriter binaryWriter = new BinaryWriter(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); // Необязательно! // Первым записываем block1 для количества файлов binaryWriter.Write((Int32)files.Length);// Приводить тип необязательно, распознает сама //!!!=4 Console.WriteLine(binaryWriter.BaseStream.Position); // Закачиваем все имена файлов (block2) из отсортированного массива for (int i = 0; i < files.Length; i++) binaryWriter.Write((String)files[i]);// Приводить тип необязательно, распознает сама memoryStream.Flush(); // Сливаем буфер // Создаем файловый поток и сбрасываем в него накопленное FileStream fs = null; try { // Создаем или усекаем файл в том же каталоге fs = new FileStream( Path.Combine(currentDirectory, nameLibrary), FileMode.Create); memoryStream.WriteTo(fs);// Сбрасываем все из памяти в файл fs.Flush(); // Слить кэш (необязательно!) } catch { FORMS.MessageBox.Show("Не могу записать в файл " + nameLibrary); fs.Close(); return; } // Закрываем поток-оболочку, которая закроет и базовый поток binaryWriter.Close(); // Создаем поток памяти для записи block4 и block5 MemoryStream dimension = new MemoryStream( files.Length * (sizeof(long) + sizeof(int)) + sizeof(long)); dimension.Seek(0, SeekOrigin.Begin); // Необязательно! // Создаем поток-оболочку для записи пар "позиция-длина" в MemoryStream BinaryWriter dimensionWriter = new BinaryWriter(dimension); // Читаем отдельные рисунки и пишем сразу в библиотеку MemoryStream pictureStream; for (int i = 0; i < files.Length; i++) { // Читаем файл с рисунком byte[] bytes = File.ReadAllBytes(Path.Combine( currentDirectory, files[i] + "." + extensionPictures)); // Формируем в памяти block4 для локализации рисунков dimensionWriter.Write(fs.Position); // Позиция рисунка long в потоке dimensionWriter.Write(bytes.Length);// Длина рисунка int // Добавляем очередной рисунок в библиотеку (block3) pictureStream = new MemoryStream(bytes);// Создаем на базе массива pictureStream.WriteTo(fs); // Добавляем рисунок в библиотечный файл pictureStream.Close(); } fs.Flush(); // Сбрасываем буфер dimensionWriter.Write(fs.Position); // Дописываем в память block5 dimensionWriter.Flush(); // Дописываем в библиотеку последними block4 и block5 dimension.WriteTo(fs); dimensionWriter.Close(); // Надстройка закрывает и базовый поток fs.Close(); // При закрытии автоматически выполнится и Flush() // Оповещаем пользователя FORMS.MessageBox.Show("Библиотека " + nameLibrary + " создана в каталоге:\n" + currentDirectory); } } // Альтернативный компаратор!!!!!!! /************************************************** class ComparerNames : IComparer<String> { public int Compare(String x, String y) { int X, Y, XX, YY; int posX = x.IndexOf('_'); string str1 = posX != -1 ? x.Substring(0, posX) : ""; bool isNumber1 = int.TryParse(str1, out X); int posY = y.IndexOf('_'); string str2 = posY != -1 ? y.Substring(0, posY) : ""; bool isNumber2 = int.TryParse(str2, out Y); if (!isNumber1 || !isNumber2) return x.CompareTo(y); if (X < Y) return -1; else if (X == Y) { str1 = x.LastIndexOf('d') != -1 ? x.Substring(0, x.Length - 1) : x; str1 = str1.Substring(posX + 1); isNumber1 = int.TryParse(str1, out XX); str2 = y.LastIndexOf('d') != -1 ? y.Substring(0, y.Length - 1) : y; str2 = str2.Substring(posY + 1); isNumber2 = int.TryParse(str2, out YY); if (!isNumber1 || !isNumber2) return str1.CompareTo(str2); if (XX < YY) return -1; else if (XX == YY) if (x.LastIndexOf('d') != -1 && y.LastIndexOf('d') != -1) return 0; else if (x.LastIndexOf('d') != -1) return 1; else return -1; else return 1; } else return 1; } } //************************************************/ }
Для сохранения параметров диалогового окна FolderBrowserDialog выполним следующее:
- В файле Window1.xaml.cs добавьте код, выделенный в листинге
public partial class Window1 : Window { // Объект для сохранения параметров приложения Properties.Settings settings = new App7.Properties.Settings(); public Window1() { // Настройка консоли Console.ForegroundColor= ConsoleColor.White; Console.CursorVisible = false; Console.Title = "Упражнение 7. Поток MemoryStream"; InitializeComponent(); // Читаем параметры, сохраненные ранее dialogFolder.SelectedPath = settings.dialogFolder; } ....................................................... private void Window_Closed(object sender, EventArgs e) { // Сохраняем параметры приложения settings.dialogFolder = dialogFolder.SelectedPath; settings.Save(); } }
- В панели Solution Explorer для проекта App7 раскройте узел Properties и двойным щелчком на файле Settings.settings откройте окно установки параметров , которое заполните как показано на рисунке
Обратите внимание, что оболочка добавила к проекту файл конфигурации app.config, без которого механизм сохранения параметров работать не будет.
- Запустите приложение, выберите в прилагаемом к работе каталоге Source подкаталог Images с рисунками *.jpg и сверните их в библиотеку Pictures.my.lib, которая разместится в том же подкаталоге
- Разберитесь с приведенным кодом файла Example2.cs, который содержит подробные комментарии