Практикум прикладного программирования на C# в среде VS.NET 2008

Графика в WPF

Разбить на страницы
Показывать лекцию целиком

Двумерная графика в WPF

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

Вся двумерная графика WPF реализуется классами, производными от абстрактных классов Drawing и Shape. Классы, наследующие Drawing, являются более простыми конструкциями, чем классы, произведенные от Shape, и поэтому требуют меньших системных ресурсов.

Абстрактный класс Drawing находится в пространстве имен System.Windows.Media.Drawing и содержит общее описание того, что должно быть нарисовано: фигура, точечный рисунок, строка текста или видео. Производные от него классы описывают конкретные типы содержимого:

  • GeometryDrawing - готовит внутри себя данные фигуры для рисования
  • ImageDrawing - готовит внутри себя данные изображения (точечный рисунок)
  • GlyphRunDrawing - готовит внутри себя текстовые данные
  • VideoDrawing - готовит внутри себя аудио- или видеофайл
  • DrawingGroup - послойно накапливает коллекцию данных предыдущих четырех объектов Drawing как один составной объект рисования для последующего вывода через класс отображения ( DrawingBrush, DrawingImage или Visual )
  • Все эти производные классы являются запечатанными ( sealed ) и не могут продолжать цепочку наследования.

    Нужно понимать, что перечисленные объекты содержат только описание того, что нужно нарисовать. Но чтобы воспроизвести содержимое любого из них, нужно передать этот объект в конструктор экземпляра класса-контейнера DrawingImage, а сам контейнер присоединить к свойству Source объекта-представления Image.

    Упражнение 1. Применение объекта GeometryDrawing для построения графиков

    Объект GeometryDrawing позволяет создать фигуру с заливкой и контуром путем совместного использования объектов Geometry, Pen и Brush, адресуемых его одноименными свойствами-ссылками, где

  • объект Geometry описывает структуру самой геометрии фигуры
  • объект Pen описывает контур фигуры
  • объект Brush описывает заливку фигуры
  • Свойство-ссылка типа Geometry адресует экземпляр одного из классов, производных от абстрактного класса Geometry. Таковыми являются следующие запечатанные ( sealed ) классы:

    Класс GeometryGroup может накапливать описание геометрических примитивов своих родственников за счет свойства-коллекции Children типа GeometryCollection, а затем передавать их как единый составной объект свойству-ссылке Geometry объекта GeometryDrawing. В дополнение к такому описанию геометрии объект GeometryDrawing добавляет свое описание свойств Pen и Brush. Затем можно передать все это содержимое в коллекцию DrawingGroup как очередной готовый слой для последующего отображения. Такой механизм определяет векторную (в отличие от точечной) графику, которую можно произвольно масштабировать без потери качества рисунка на устройстве отображения с любым разрешением.

    Продемонстрируем применение описанного на примере построения графиков (см. по ссылке http://vitiy.info/?p=5). Построим два графика: sin() и cos() для одного периода.

    При выборе места размещения решения следите, чтобы путь к нему и проектам не содержал пробелов и кириллицы, иначе возможны сбои в работе!!!
  • Создайте новое решение WpfGraphics с новым проектом WpfApp1 типа WPF Application
  • Заполните декларативную часть Window1.xaml проекта следующей разметкой
  • <Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
        <Grid Margin="20">
            <Image Name="image1" />
        </Grid>
    </Window>
  • Реализуйте бизнес-логику окна в застраничном файле 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.Globalization;
        
    namespace WpfApp1
    {
        public partial class Window1 : Window
        {
            //
            // Поля
            //
            const int countDot = 30;// Количество отрезков
            // Список для хранения данных
            List<double[]> dataList = new List<double[]>();
            // Или можно DoubleCollection data = new DoubleCollection();
            // Контейнер слоев рисунков
            DrawingGroup drawingGroup = new DrawingGroup();
        
            public Window1()
            {
                InitializeComponent();
        
                DataFill();// Заполнение списка данными
                Execute(); // Заполнение слоев
        
                // Отображение на экране
                image1.Source = new DrawingImage(drawingGroup);
            }
        
            // Генерация точек графиков
            void DataFill()
            {
                double[] sin = new double[countDot + 1];
                double[] cos = new double[countDot + 1];
        
                for (int i = 0; i < sin.Length; i++)
                {
                    double angle = Math.PI * 2 / countDot * i;
                    sin[i] = Math.Sin(angle);
                    cos[i] = Math.Cos(angle);
                }
        
                dataList.Add(sin);
                dataList.Add(cos);
            }
        
            // Послойное формирование рисунка в Z-последовательности
            void Execute()
            {
                BackgroundFun();    // Фон
                GridFun();          // Мелкая сетка
                SinFun();           // Строим синус линией
                CosFun();           // Строим косинус точками
                MarkerFun();        // Надписи
            }
        
            // Фон
            private void BackgroundFun()
            {
                // Создаем объект для описания геометрической фигуры
                GeometryDrawing geometryDrawing = new GeometryDrawing();
        
                // Описываем и сохраняем геометрию квадрата
                RectangleGeometry rectGeometry = new RectangleGeometry();
                rectGeometry.Rect = new Rect(0, 0, 1, 1);
                geometryDrawing.Geometry = rectGeometry;
        
                // Настраиваем перо и кисть
                geometryDrawing.Pen = new Pen(Brushes.Red, 0.005);// Перо рамки
                geometryDrawing.Brush = Brushes.Beige;// Кисть закраски
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
        
            // Горизонтальная сетка
            private void GridFun()
            {
                // Создаем коллекцию для описания геометрических фигур
                GeometryGroup geometryGroup = new GeometryGroup();
        
                // Создаем и добавляем в коллекцию десять параллельных линий 
                for (int i = 1; i < 10; i++)
                {
                    LineGeometry line = new LineGeometry(new Point(1.0, i * 0.1), 
                        new Point(-0.1, i * 0.1));
                    geometryGroup.Children.Add(line);
                }
        
                // Сохраняем описание геометрии
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                // Настраиваем перо
                geometryDrawing.Pen = new Pen(Brushes.Gray, 0.003);
                double[] dashes = { 1, 1, 1, 1, 1 };// Образец штриха
                geometryDrawing.Pen.DashStyle = new DashStyle(dashes, -.1);
        
                // Настраиваем кисть 
                geometryDrawing.Brush = Brushes.Beige;
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
        
            // Строим синус линией
            private void SinFun()
            {
                // Строим описание синусоиды
                GeometryGroup geometryGroup = new GeometryGroup();
                for (int i = 0; i < dataList[0].Length - 1; i++)
                {
                    LineGeometry line = new LineGeometry(
                        new Point((double)i / (double)countDot, 
                            .5 - (dataList[0][i] / 2.0)),
                        new Point((double)(i + 1) / (double)countDot, 
                            .5 - (dataList[0][i + 1] / 2.0)));
                    geometryGroup.Children.Add(line);
                }
        
                // Сохраняем описание геометрии
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                // Настраиваем перо
                geometryDrawing.Pen = new Pen(Brushes.Blue, 0.005);
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
        
            // Строим косинус точками
            private void CosFun()
            {
                // Строим описание косинусоиды
                GeometryGroup geometryGroup = new GeometryGroup();
                for (int i = 0; i < dataList[1].Length; i++)
                {
                    EllipseGeometry ellips = new EllipseGeometry(
                        new Point((double)i / (double)countDot, 
                        .5 - (dataList[1][i] / 2.0)), 0.01, 0.01);
                    geometryGroup.Children.Add(ellips);
                }
        
                // Сохраняем описание геометрии
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                // Настраиваем перо
                geometryDrawing.Pen = new Pen(Brushes.Green, 0.005);
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
            
            // Надписи
            private void MarkerFun()
            {
                GeometryGroup geometryGroup = new GeometryGroup();
                for (int i = 0; i <= 10; i++)
                {
                    FormattedText formattedText = new FormattedText(
                    String.Format("{0,7:F}", 1 - i * 0.2),
                    CultureInfo.InvariantCulture,
                    FlowDirection.LeftToRight,
                    new Typeface("Verdana"),
                    0.05,
                    Brushes.Black);
        
                    formattedText.SetFontWeight(FontWeights.Bold);
        
                    Geometry geometry = formattedText.BuildGeometry(new Point(-0.2, i * 0.1 - 0.03));
                    geometryGroup.Children.Add(geometry);
                }
        
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                geometryDrawing.Brush = Brushes.LightGray;
                geometryDrawing.Pen = new Pen(Brushes.Gray, 0.003);
        
                drawingGroup.Children.Add(geometryDrawing);
            }
        }
    }
  • Разберитесь с кодом и запустите приложение - результат будет таким
  • Поизменяйте размер окна - рисунок автоматически масштабируется с сохранением качества отображения
  • Упражнение 2. Имитация полноэкранного режима с отключением системных клавиш

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

    window.WindowStyle = WindowStyle.None;
    window.ResizeMode = ResizeMode.NoResize;

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

    Есть более сложный способ, приведенный в "http://www.swart.ws/2009/03/kiosk-full-screen-wpf-applications.html", но он имеет те же недостатки.

    Для достижения полной иллюзии режима FullScreen следует отключить системные клавиши, вызывающие панель задач и меню Пуск, и самый эффективный (на мой взгляд) способ приведен в "http://www.sql.ru/Forum/actualthread.aspx?tid=632552", которым мы и воспользуемся в данном упражнении.

    Из приведенного в статье http://support.microsoft.com/kb/126449/ru сочетания клавиш мы воспользуемся только следующими вариантами:

  • CTRL + ESC: открытие меню Пуск.
  • Клавиша WIN: открытие меню Пуск.
  • ALT + ESC: показать панель задач и переключать развернутые окна.
  • ALT + TAB: переключение между программами.
  • Этого будет достаточно, чтобы поддерживать полноэкранный режим и одновременно не блокировать остальную функциональность клавиатуры.

  • Добавьте к решению командой File/Add/New Project новый проект с именем FullScreen и назначьте его стартовым
  • В панели Solution Explorer добавьте к корню проекта FullScreen новую папку Images командой контекстного меню Add/New Folder
  • В панели Solution Explorer командой Add/Existing Item контекстного меню для папки Images скопируйте в нее файл flower2.jpg из прилагаемого каталога Source
  • Определите в интерфейсной части окна следующий код
  • <Window x:Class="FullScreen.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300"
        Background="Green"
        Loaded="Window_Loaded"
        KeyDown="Window_KeyDown"
        KeyUp="Window_KeyDown"
            >
        <Window.ContextMenu>
            <ContextMenu>
                <MenuItem Name="changeScreen" Click="MenuItem_Click" />
            </ContextMenu>
        </Window.ContextMenu>
        <DockPanel>
            <Menu Name="menu" DockPanel.Dock="Top">
                <MenuItem Header="File">
                    <MenuItem Header="Open" />
                    <MenuItem Header="Save" />
                    <MenuItem Header="SaveAs" />
                    <MenuItem Header="Exit" />
                </MenuItem>
                <MenuItem Header="Edit">
                    <MenuItem Header="Cut" />
                    <MenuItem Header="Copy" />
                    <MenuItem Header="Paste" />
                </MenuItem>
            </Menu>
            <Image Source="Images/flower2.jpg" Stretch="Uniform" /> 
        </DockPanel>
    </Window>

    Для настройки окна и его элементов мы применили синтаксис атрибута, а для создания каркаса контекстного меню - синтаксис тега свойства ( свойства зависимости, присоединенные свойства ). Атрибут Stretch элемента Image может принимать следующие значения:

  • None - изображение масштабируется до естественного размера и в области просмотра справа внизу видно столько, сколько поместилось
  • Fill - изображение масштабируется по всей области просмотра без соблюдения пропорций
  • Uniform - изображение масштабируется с соблюдением пропорций, чтобы полностью поместиться в область просмотра
  • UniformToFill - изображение масштабируется с соблюдением пропорций так, чтобы в область просмотра полностью вместился хотя бы один размер, а для другой стороны видно столько, сколько поместилось
  • Теперь необходимо создать обработчики для событий, выделеных в листинке, в файле присоединенного кода Window1.xaml.cs. Для этого:

  • Найдите в разметке файла Window1.xaml атрибуты событий
  • Loaded="Window_Loaded"
  • KeyDown="Window_KeyDown"
  • KeyUp="Window_KeyDown"
  • Click="MenuItem_Click"
  • В любом месте каждого из этих атрибутов щелкните правой кнопкой мыши и выполните команду Navigate to Event Handler.
  • Оболочка создаст обработчики с указанными именами, а если обработчик уже существует, то перейдет к нему.

  • Заполните файл поддержки разметки 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;
        
    namespace FullScreen
    {
        public partial class Window1 : Window
        {
            // Объявляем как поля для видимости в функциях
            bool fullScreen = false;// Состояние экрана
            WindowStyle windowStyle;
            WindowState windowState;
            ResizeMode resizeMode;
        
            public Window1()
            {
                InitializeComponent();
            }
        
            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                // Запоминаем начальные параметры окна
                windowState = this.WindowState;
                windowStyle = this.WindowStyle;
                resizeMode = System.Windows.ResizeMode.CanResizeWithGrip;
                this.ResizeMode = resizeMode;
                changeScreen.Header = "FullScreen";
            }
        
            private void MenuItem_Click(object sender, RoutedEventArgs e)
            {
                FullScreen();
            }
        
            private void FullScreen()
            {
                if (!fullScreen)// Переходим в полноэкранный режим 
                {
                    if (this.WindowState == WindowState.Maximized)
                    {
                        // Чтобы скрыть панель задач и не было мерцания
                        this.Hide();
                        this.WindowState = WindowState.Normal;
                    }
        
                    App.Current.MainWindow.WindowStyle = WindowStyle.None;// Без заголовка
                    App.Current.MainWindow.Topmost = true;      // На передний план
                    App.Current.MainWindow.WindowState = WindowState.Maximized;// Развернуть
                    App.Current.MainWindow.ResizeMode = ResizeMode.NoResize;// Неизменяемое
        
                    menu.Visibility = Visibility.Collapsed;// Скрываем меню
                    HookSystemKeys.FunHook();// Запрещаем системные клавиши
                    this.Visibility = Visibility.Visible;
        
                    fullScreen = true;
                    changeScreen.Header = "WindowScreen";
                }
                else // Восстанавливаем оконный режим
                {
                    this.WindowStyle = windowStyle;
                    this.Topmost = false;
                    this.WindowState = windowState;
                    this.ResizeMode = resizeMode;
        
                    menu.Visibility = Visibility.Visible;// Показываем меню
                    HookSystemKeys.FunUnHook();// Освобождаем системные клавиши
        
                    fullScreen = false;
                    changeScreen.Header = "FullScreen";
                }
            }
        
            private void Window_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.Key == Key.Escape)
                    this.Close();
            }
        }
    }

    В приведенном коде используется класс HookSystemKeys, который нам нужно подключить к приложению и который взят из "http://www.sql.ru/Forum/actualthread.aspx?tid=632552" (там же и описан).

  • Добавьте к текущему проекту FullScreen командой Project/Add New Item новый файл с именем HookSystemKeys.cs
  • Заполните файл HookSystemKeys.cs так
  • using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
        
    // Для перечисления Keys 
    using System.Windows.Forms; 
        
    namespace FullScreen
    {
        // Отключение системной клавиши
        // http://www.sql.ru/Forum/actualthread.aspx?tid=632552
        class HookSystemKeys
        {
            private const int WH_KEYBOARD_LL = 13;
            private const int WM_KEYUP = 257;// Отпускание любой клавиши
        
            private static LowLevelKeyboardProc _proc = HookCallback;
            private static IntPtr _hookID = IntPtr.Zero;
        
            // Поддержка флагов состояния системных клавиш 
            static bool CtrlKey, AltKey, WinKey;
            private static void StateKey(int vkCode, IntPtr wParam)
            {
                switch ((Keys)vkCode)
                {
                    case Keys.LControlKey:
                    case Keys.RControlKey:
                        if (wParam == (IntPtr)WM_KEYUP)
                            CtrlKey = false;
                        else
                            CtrlKey = true;
                        break;
                    case Keys.LMenu:
                    case Keys.RMenu:
                        if (wParam == (IntPtr)WM_KEYUP)
                            AltKey = false;
                        else
                            AltKey = true;
                        break;
                    case Keys.LWin:
                    case Keys.RWin:
                        if (wParam == (IntPtr)WM_KEYUP)
                            WinKey = false;
                        else
                            WinKey = true;
                        break;
                }
            }
        
            // Общедоступная упаковка оригинала
            public static void FunHook()
            {
                _hookID = SetHook(_proc);
            }
            public static void FunUnHook()
            {
                UnhookWindowsHookEx(_hookID);
            }
        
            private static IntPtr SetHook(LowLevelKeyboardProc proc)
            {
                using (Process curProcess = Process.GetCurrentProcess())
                using (ProcessModule curModule = curProcess.MainModule)
                {
                    return SetWindowsHookEx(WH_KEYBOARD_LL, proc, 
                        GetModuleHandle(curModule.ModuleName), 0);
                }
            }
        
            private delegate IntPtr LowLevelKeyboardProc(int nCode, 
                IntPtr wParam, IntPtr lParam);
        
            private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
            {
                int vkCode = Marshal.ReadInt32(lParam);
                StateKey(vkCode, wParam);
                if (WinKey                                      // Системная
                    || CtrlKey  (Keys)vkCode == Keys.Escape   // Ctrl+Esc
                    || AltKey  (Keys)vkCode == Keys.Escape    // Alt+Esc
                    || AltKey  (Keys)vkCode == Keys.Tab)      // Alt+Tab
                {
                    return (IntPtr)1;
                }
                else
                    return CallNextHookEx(_hookID, nCode, wParam, lParam);
            }
        
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern IntPtr SetWindowsHookEx(int idHook,
                LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
        
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern bool UnhookWindowsHookEx(IntPtr hhk);
        
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, 
                IntPtr wParam, IntPtr lParam);
        
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern IntPtr GetModuleHandle(string lpModuleName);
        }
    }
  • Командой Project/Add Reference добавьте к проекту ссылку на библиотеку System.Windows.Forms.dll, в которой находится перечисление Keys кодов клавиш
  • Запустите проект FullScreen и испытайте построенную нами имитацию полноэкранного режима
  • Оконный и полноэкранный (уменьшенный масштаб) режимы будут выглядеть так

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

  • CTRL + ESC: открытие меню Пуск.
  • Клавиша WIN: открытие меню Пуск.
  • ALT + ESC: показать панель задач и переключать развернутые окна.
  • ALT + TAB: переключение между программами.
  • В оконном режиме функциональность системных клавиш восстанавливается.

    Для полного сходства можно было бы отключить мышь, но тогда все управление окном нужно будет возложить на клавиатуру. В нашем приложении мы для закрытия окна использовали клавишу Esc. Закрыть окно можно и системной комбинацией Alt+F4, которую мы не отключали. Осталась активной комбинация Alt+Space, вызывающая системное меню приложения.

    Мы использовали в классе HookSystemKeys перечисление System.Windows.Forms.Keys библиотечной сборки System.Windows.Forms, а в обработчике Window_KeyDown - перечисление System.Windows.Input.Key библиотечной сборки WindowsBase.dll. Это разные перечисления и их нельзя путать.

  • Попробуйте разобраться с кодом
  • Упражнение 3. Применение объекта ImageDrawing

    Объект ImageDrawing принимает графические данные в виде битовой карты (точечного рисунка). Для создания изображения необходимо создать экземпляр ImageDrawing и установить значения его свойств ImageDrawing.ImageSource и ImageDrawing.Rect. Свойство ImageDrawing.ImageSource задает изображение для рисования, а свойство ImageDrawing.Rect задает положение и размер каждого изображения.

    Если нужно отобразить один рисунок, то его сразу можно присоединить к объекту отображения Image, как мы это делали в предыдущем упражнении. Если же требуется нарисовать несколько рисунков сразу, то необходимо организовать конвейер формирования рисунков. Вначале создаются отдельные слои с помощью ImageDrawing, затем они передаются в накопитель DrawingGroup, где располагаются в Z -последовательности в порядке их добавления. В таком порядке каждый новый слой будет перекрывать все предыдущие и располагаться ближе к пользователю. Затем этот слоеный объект передается в рисовальщик DrawingImage, который и присоединяется, в конечном итоге, к элементу отображения Image.

    Объект DrawingGroup как составной объект рисования, может принимать не только точечные рисунки. В первом упражнении мы использовали его для накопления векторных рисунков геометрии, порожденных объектом GeometryDrawing. Он также способен принимать текстовые данные от GlyphRunDrawing, или медийные данные звука и видео от объекта VideoDrawing. Объект DrawingGroup является единственным типом базового объекта Drawing, который позволяет определять свою собственную область отсечения. Но об этом чуть позже, а сейчас приступим к рассмотрению объекта ImageDrawing.

    Класс ImageDrawing легко спутать с важным классом DrawingImage, названия которых так похожи. Но это нас не должно путать, если мы представим изготовление готового рисунка как последовательность операций на конвейере. Это будет выглядеть примерно так:

    GeometryDrawing -> DrawingGroup -> DrawingImage -> Image
    ImageDrawing
    GlyphRunDrawing
    VideoDrawing
  • Добавьте к решению проект типа WPF Application с именем WpfApp3 и назначьте его стартовым
  • Добавьте к проекту командой Project/New Folder папку с именем Images
  • В панели Solution Explorer вызовите контекстное меню для папки Images и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source пять файлов с рисунками (не забудьте изменить фильтр диалогового окна Add Existing Item на All Files ):
  • market 031.jpg
  • market 032.jpg
  • market 034.jpg
  • market 039.jpg
  • market 040.jpg
  • В панели Solution Explorer выделите все пять рисунков одновременно и в панели Properties установите для них свойства
  • Build Action=Content
  • Copy to Output Directory=Copy if newer
  • В этом упражнении мы рассмотрим два способа использования объекта ImageDrawing - с помощью разметки на XAML и с помощью кода C#. Оба способа работают совершенно одинаково и для того, чтобы это подчеркнуть, мы с помощью них реализуем одну и ту же задачу. Попутно рассмотрим решение некоторых мелких вопросов, таких, как предотвращение повторного запуска созданного окна приложения, назначение главного окна и формирование всплывающей подсказки.

    Вначале создадим главное окно приложения Window1, в котором примененим объект ImageDrawing в кодовой части Window1.xaml.cs. Затем построим дочернее окно Window2, в котором используем объект ImageDrawing в дискрипторной части Window2.xaml.

  • Заполните файл разметки Window1.xaml следующим дескрипторным кодом
  • <Window x:Class="WpfApp3.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Главное окно Window1: Работа объекта ImageDrawing через процедурный код"
        Loaded="Window_Loaded"
        MouseDoubleClick="Show_Window2"
        SizeToContent="WidthAndHeight"
        ResizeMode="NoResize">
        <Window.ContextMenu>
            <ContextMenu>
                <MenuItem Header="Создать дочернее окно" Click="Create_Window2" />
            </ContextMenu>
        </Window.ContextMenu>
    </Window>

    Эта разметка настраивает окно и создает контекстное меню с одним пунктом. Вызов дочернего окна, которое будет иллюстрировать разметочный способ применения объекта ImageDrawing, предусмотрим в обработчиках событий MouseDoubleClick окна и Click контекстного меню. Такое дублирование одной и той же задачи создания дочернего окна выбрано потому, чтобы испытать разные способы предотвращения повторного запуска уже существующего окна.

    Событие Loaded срабатывает после загрузки окна в оперативную память, поэтому в его обработчик удобно поместить код создания объектов содержимого окна. Атрибуты SizeToContent и ResizeMode дескриптора <Window> делают окно подстраиваемым под содержимое (при первом появлении) и неизменяемое в размерах пользователем, соответственно. Контекстное меню прикрепляется к объекту окна и будет вызываться в любой его точке, кроме заголовка.

  • Щелкните правой кнопкой мыши на каждом из событий Loaded, MouseDoubleClick, Click и выполните команду Navigate to Event Handler, чтобы создать в кодовой части обработчики с уже заготовленными именами
  • В файле Window1.xaml.cs заполните обработчик события Loaded следующим кодом
  • private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        // Размеры всех рисунков одинаковы
        const int WIDTH = 348, HEIGHT = 232;
    
        // Создаем накопитель рисунков DrawingGroup 
        DrawingGroup drawingGroup = new DrawingGroup();
    
        // Левый верхний 
        ImageDrawing pict1 = new ImageDrawing();
        pict1.Rect = new Rect(0, 0, WIDTH, HEIGHT);
        pict1.ImageSource = new BitmapImage(
            new Uri(@"Images\market 040.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict1);
    
        // Правый верхний 
        ImageDrawing pict2 = new ImageDrawing();
        pict2.Rect = new Rect(350, 0, WIDTH, HEIGHT);
        pict2.ImageSource = new BitmapImage(
            new Uri(@"Images\market 039.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict2);
    
        // Левый нижний
        ImageDrawing pict3 = new ImageDrawing();
        pict3.Rect = new Rect(0, 234, WIDTH, HEIGHT);
        pict3.ImageSource = new BitmapImage(
            new Uri(@"Images\market 034.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict3);
    
        // Правый нижний
        ImageDrawing pict4 = new ImageDrawing();
        pict4.Rect = new Rect(350, 234, WIDTH, HEIGHT);
        pict4.ImageSource = new BitmapImage(
            new Uri(@"Images\market 032.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict4);
    
        // Передать рисовальщику
        DrawingImage drawingImageSource = new DrawingImage(drawingGroup);
    
        // Заморозить DrawingImage для лучшей производительности
        drawingImageSource.Freeze();
    
        // Передать элементу отображения
        Image image = new Image();
        image.Stretch = Stretch.None;
        image.Source = drawingImageSource;
    
        // Контейнер Border для присоединения к содержимому окна
        Border border = new Border();
        border.Background = Brushes.White;
        border.BorderBrush = Brushes.White;
        border.BorderThickness = new Thickness(2);  // Толщина рамки
        border.Margin = new Thickness(10);          // Внешний отступ-поле
        border.Child = image;                       // Отдать родителю
    
        this.Background = Brushes.Blue;
        this.Content = border;
    }
  • Дополните конструктор окна Window1 следующим кодом
  • public Window1()
    {
        // Инициализация разметочной части
        InitializeComponent();
    
        // Корректировка заголовка окна
        this.Title += "=\"Так голодают буржуины!\"";
    
        // Всплывающая подсказка
        this.ToolTip = "Вызывайте дочернее окно\n"
            + "двойным щелчком мыши\n"
            + "или контекстным меню...";
    }

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

  • Запустите приложение - первое окно будет выглядеть так
  • Обратите внимание, что окно не имеет системных кнопок минимизации и максимизации, а только кнопку закрытия. Это поведение обеспечивается настройкой в разметке окна с помощью атрибута ResizeMode="NoResize".

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

  • Добавьте к текущему (выделенному в панели Solution Explorer ) проекту WpfApp3 командой Project/Add Window новое окно WPF с именем Window2.xaml
  • Наполните файл Window2.xaml следующей разметкой
  • <Window x:Class="WpfApp3.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Дочернее окно Window2: Работа объекта ImageDrawing через разметку"
        SizeToContent="WidthAndHeight"
        ResizeMode="NoResize"
        Background="Green"
            >
        <Border BorderBrush="White" BorderThickness="2" 
                Margin="10"
                Background="White"
            >
            <Image Stretch="None">
                <Image.Source>
                    <DrawingImage>
                        <DrawingImage.Drawing>
                            <DrawingGroup>
        
                                <!-- Рис.1 - левый верхний (x, y, width, height) -->
                                <ImageDrawing Rect="0,0,348,232" ImageSource="Images\market 031.jpg"/>
        
                                <!-- Рис.2 - правый верхний (x, y, width, height) -->
                                <ImageDrawing Rect="350,0,348,232" ImageSource="Images\market 034.jpg"/>
        
                                <!-- Рис.3 - левый нижний (x, y, width, height) -->
                                <ImageDrawing Rect="0,234,348,232" ImageSource="Images\market 039.jpg"/>
        
                                <!-- Рис.4 - правый нижний (x, y, width, height) -->
                                <ImageDrawing Rect="350,234,348,232" ImageSource="Images\market 040.jpg"/>
        
                            </DrawingGroup>
                        </DrawingImage.Drawing>
                    </DrawingImage>
                </Image.Source>
            </Image>
        </Border>
    </Window>
  • Дополните конструктор класса Window2 следующим кодом
  • public Window2()
    {
        // Инициализация разметочной части
        InitializeComponent();
    
        // Корректировка заголовка окна
        this.Title += "=\"Так голодают буржуины!\"";
    
        // Дочернее окно не отображать в панели задач ОС 
        this.ShowInTaskbar = false;
    }
  • Заполните обработчики щелчка и контекстного меню в классе Window1 следующим кодом
  • Window wnd2;
    private void Show_Window2(object sender, MouseButtonEventArgs e)
    {
        wnd2 = new Window2();
        wnd2.Show();
    }
    
    private void Create_Window2(object sender, RoutedEventArgs e)
    {
        wnd2 = new Window2();
        wnd2.Show();
    }
  • Запустите приложение и вызовите окно Window2 через окно Window1, как напоминает всплывающая подсказка
  • Получится следующий результат
  • Здесь есть несколько существенных недостатков, которые следует устранить:

  • Второе окно можно создавать во многих экземплярах
  • Первое окно со вторым никак не связано и каждое из них закрывается самостоятельно
  • Пиктограмма вторичных окон не отображается в панели задач (мы специально ввели в конструктор окна настройку this.ShowInTaskbar = false; )
  • Во первых, зачем пользователю много экземпляров одного и того же окна. Во вторых, хоть окна не имеют системных кнопок минимизации, все равно их можно минимизировать все сразу через операционную систему командой "Показать рабочий стол". А в этом случае вновь сделать видимыми вторичные окна не удасться и придется их закрывать через Диспетчер задач (либо через оболочку Visual Studio останавливать процесс - работу приложения).

    Для устранения указанных недостатков нужно выполнить следующее:

  • Назначить первое окно главным, чтобы при его закрытии закрывалось приложение в целом
  • Блокировать создание вторичных окон, если одно из них уже существует, и попутно активировать вторичное окно, если оно заслонено другими окнами или скрыто
  • Для назначения окна Window1 главным добавьте в его конструктор следующий код
  • public Window1()
    {
        // Инициализация разметочной части
        InitializeComponent();
    
        // Корректировка заголовка окна
        this.Title += "=\"Так голодают буржуины!\"";
    
        // Всплывающая подсказка
        this.ToolTip = "Вызывайте дочернее окно\n"
            + "двойным щелчком мыши\n"
            + "или контекстным меню...";
    
        // Сделать главным окном приложения
        Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
        Application.Current.MainWindow = this;
    }
  • Запустите приложение и убедитесь, что теперь все дочерние окна при закрытии первичного окна тоже закрываются вместе с приложением
  • Для блокирования повторного создания дочерних окон применим два способа:

  • В первом способе обернем в классе Window1 ссылку wnd2 на дочернее окно в свойство Wnd2 и будем ее обнулять в обработчике Closed окна Window2. В первом окне перед созданием дочернего будем эту ссылку проверять - если ненулевая, значит окно существует и новое создавать нельзя
  • Второй способ намного проще, так как не требует жесткой зависимости кода между окнами введением дополнительного общего поля или свойства. Можно просто воспользоваться свойством Application.Current.Windows типа WindowCollection приложения и в первом окне перед созданием дочернего проверять, отсутствует ли в коллекции существующих окон приложения интересующее нас окно. Любое созданное окно приложения сразу заносится в эту коллекцию, а при закрытии между событиями Closing и Closed немедленно удаляется из нее. Единственное, что потребуется определить в дочернем окне, так это свойство Name, по которому мы будем искать это окно в коллекции приложения
  • Итак, реализуем сказанное...

    Способ 1

  • В классе Window1 найдите объявление поля wnd2 (строка: Window wnd2; ), щелкните на поле wnd2 правой кнопкой мыши и выполните команду контекстного меню Refactor/Encapsulate Field
  • Появится окно Encapsulate Field, предлагающее обернуть поле в общедоступное свойство с именем Wnd2

  • Скажите OK и оболочка создаст следующий код свойства
  • public static Window Wnd2
    {
        get { return Window1.wnd2; }
        set { Window1.wnd2 = value; }
    }

    Аксессор get нам не нужен, поскольку в клиенте Window2 при закрытии окна мы будем поле wnd2 только обнулять через свойство Wnd2, поэтому get следует удалить.

  • Внесите в класс Window1 следующий окончательный код реализации первого способа
  • // Предотвращение повторного открытия окна: Способ 1
    static Window wnd2;
    public static Window Wnd2
    {
        set { Window1.wnd2 = value; }
    }
    // Обработчик двойного щелчка
    private void Show_Window2(object sender, MouseButtonEventArgs e)
    {
        if (wnd2 == null)
        {
            wnd2 = new Window2();
            wnd2.Show();
        }
        else
            wnd2.Activate();// Сдвинуть на передний план
    }
  • Модифицируйте класс Window2 следующим образом
  • public partial class Window2 : Window
    {
        public Window2()
        {
            // Инициализация разметочной части
            InitializeComponent();
    
            // Корректировка заголовка окна
            this.Title += "=\"Так голодают буржуины!\"";
    
            // Дочернее окно не отображать в панели задач ОС 
            this.ShowInTaskbar = false;
    
            // Регистрация обработчика
            this.Closed += new EventHandler(Window2_Closed);
        }
    
        private void Window2_Closed(object sender, EventArgs e)
        {
            Window1.Wnd2 = null;        // Для предотвращения повторного запуска
            GC.WaitForFullGCComplete(); // Ждать завершения сборки мусора
            GC.Collect();               // Начать сборку мусора
        }
    }

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

  • Запустите приложение и убедитесь, что обработчик создания дочернего окна по двойному щелчку теперь работает правильно
  • Способ 2

  • В разметку файла Window2.xaml добавьте определение свойства Name окна Window2 для его идентификации в коллекции приложения
  • <Window x:Class="WpfApp3.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Name="Window_2"
        Title="Дочернее окно Window2: Работа объекта ImageDrawing через разметку"
        SizeToContent="WidthAndHeight"
        ResizeMode="NoResize"
        Background="Green"
            >
        ...................................
    </Window>
  • Модифицируйте обработчик контекстного меню в файле Window1.xaml.cs следующим образом
  • // Предотвращение повторного открытия окна: Способ 2
    // Обработчик контекстного меню
    private void Create_Window2(object sender, RoutedEventArgs e)
    {
        bool windowExists = false;
        foreach (Window window in Application.Current.Windows)
        {
            if (window.Name == "Window_2")
            {
                windowExists = true;
                window.Activate();// Сдвинуть на передний план
                break;
            }
        }
    
        if (!windowExists)
        {
            wnd2 = new Window2();
            wnd2.Show();
        }
    }
  • Запустите приложение и убедитесь, что и этот обработчик блокирования повторного запуска теперь тоже работает правильно
  • Разберитесь с кодом
  • В будущих упражнениях мы продолжим знакомство с графическими возможностями WPF.

    Страницы:

    Двумерная графика в WPF

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

    Вся двумерная графика WPF реализуется классами, производными от абстрактных классов Drawing и Shape. Классы, наследующие Drawing, являются более простыми конструкциями, чем классы, произведенные от Shape, и поэтому требуют меньших системных ресурсов.

    Абстрактный класс Drawing находится в пространстве имен System.Windows.Media.Drawing и содержит общее описание того, что должно быть нарисовано: фигура, точечный рисунок, строка текста или видео. Производные от него классы описывают конкретные типы содержимого:

  • GeometryDrawing - готовит внутри себя данные фигуры для рисования
  • ImageDrawing - готовит внутри себя данные изображения (точечный рисунок)
  • GlyphRunDrawing - готовит внутри себя текстовые данные
  • VideoDrawing - готовит внутри себя аудио- или видеофайл
  • DrawingGroup - послойно накапливает коллекцию данных предыдущих четырех объектов Drawing как один составной объект рисования для последующего вывода через класс отображения ( DrawingBrush, DrawingImage или Visual )
  • Все эти производные классы являются запечатанными ( sealed ) и не могут продолжать цепочку наследования.

    Нужно понимать, что перечисленные объекты содержат только описание того, что нужно нарисовать. Но чтобы воспроизвести содержимое любого из них, нужно передать этот объект в конструктор экземпляра класса-контейнера DrawingImage, а сам контейнер присоединить к свойству Source объекта-представления Image.

    Упражнение 1. Применение объекта GeometryDrawing для построения графиков

    Объект GeometryDrawing позволяет создать фигуру с заливкой и контуром путем совместного использования объектов Geometry, Pen и Brush, адресуемых его одноименными свойствами-ссылками, где

  • объект Geometry описывает структуру самой геометрии фигуры
  • объект Pen описывает контур фигуры
  • объект Brush описывает заливку фигуры
  • Свойство-ссылка типа Geometry адресует экземпляр одного из классов, производных от абстрактного класса Geometry. Таковыми являются следующие запечатанные ( sealed ) классы:

    Класс GeometryGroup может накапливать описание геометрических примитивов своих родственников за счет свойства-коллекции Children типа GeometryCollection, а затем передавать их как единый составной объект свойству-ссылке Geometry объекта GeometryDrawing. В дополнение к такому описанию геометрии объект GeometryDrawing добавляет свое описание свойств Pen и Brush. Затем можно передать все это содержимое в коллекцию DrawingGroup как очередной готовый слой для последующего отображения. Такой механизм определяет векторную (в отличие от точечной) графику, которую можно произвольно масштабировать без потери качества рисунка на устройстве отображения с любым разрешением.

    Продемонстрируем применение описанного на примере построения графиков (см. по ссылке http://vitiy.info/?p=5). Построим два графика: sin() и cos() для одного периода.

    При выборе места размещения решения следите, чтобы путь к нему и проектам не содержал пробелов и кириллицы, иначе возможны сбои в работе!!!
  • Создайте новое решение WpfGraphics с новым проектом WpfApp1 типа WPF Application
  • Заполните декларативную часть Window1.xaml проекта следующей разметкой
  • <Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
        <Grid Margin="20">
            <Image Name="image1" />
        </Grid>
    </Window>
  • Реализуйте бизнес-логику окна в застраничном файле 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.Globalization;
        
    namespace WpfApp1
    {
        public partial class Window1 : Window
        {
            //
            // Поля
            //
            const int countDot = 30;// Количество отрезков
            // Список для хранения данных
            List<double[]> dataList = new List<double[]>();
            // Или можно DoubleCollection data = new DoubleCollection();
            // Контейнер слоев рисунков
            DrawingGroup drawingGroup = new DrawingGroup();
        
            public Window1()
            {
                InitializeComponent();
        
                DataFill();// Заполнение списка данными
                Execute(); // Заполнение слоев
        
                // Отображение на экране
                image1.Source = new DrawingImage(drawingGroup);
            }
        
            // Генерация точек графиков
            void DataFill()
            {
                double[] sin = new double[countDot + 1];
                double[] cos = new double[countDot + 1];
        
                for (int i = 0; i < sin.Length; i++)
                {
                    double angle = Math.PI * 2 / countDot * i;
                    sin[i] = Math.Sin(angle);
                    cos[i] = Math.Cos(angle);
                }
        
                dataList.Add(sin);
                dataList.Add(cos);
            }
        
            // Послойное формирование рисунка в Z-последовательности
            void Execute()
            {
                BackgroundFun();    // Фон
                GridFun();          // Мелкая сетка
                SinFun();           // Строим синус линией
                CosFun();           // Строим косинус точками
                MarkerFun();        // Надписи
            }
        
            // Фон
            private void BackgroundFun()
            {
                // Создаем объект для описания геометрической фигуры
                GeometryDrawing geometryDrawing = new GeometryDrawing();
        
                // Описываем и сохраняем геометрию квадрата
                RectangleGeometry rectGeometry = new RectangleGeometry();
                rectGeometry.Rect = new Rect(0, 0, 1, 1);
                geometryDrawing.Geometry = rectGeometry;
        
                // Настраиваем перо и кисть
                geometryDrawing.Pen = new Pen(Brushes.Red, 0.005);// Перо рамки
                geometryDrawing.Brush = Brushes.Beige;// Кисть закраски
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
        
            // Горизонтальная сетка
            private void GridFun()
            {
                // Создаем коллекцию для описания геометрических фигур
                GeometryGroup geometryGroup = new GeometryGroup();
        
                // Создаем и добавляем в коллекцию десять параллельных линий 
                for (int i = 1; i < 10; i++)
                {
                    LineGeometry line = new LineGeometry(new Point(1.0, i * 0.1), 
                        new Point(-0.1, i * 0.1));
                    geometryGroup.Children.Add(line);
                }
        
                // Сохраняем описание геометрии
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                // Настраиваем перо
                geometryDrawing.Pen = new Pen(Brushes.Gray, 0.003);
                double[] dashes = { 1, 1, 1, 1, 1 };// Образец штриха
                geometryDrawing.Pen.DashStyle = new DashStyle(dashes, -.1);
        
                // Настраиваем кисть 
                geometryDrawing.Brush = Brushes.Beige;
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
        
            // Строим синус линией
            private void SinFun()
            {
                // Строим описание синусоиды
                GeometryGroup geometryGroup = new GeometryGroup();
                for (int i = 0; i < dataList[0].Length - 1; i++)
                {
                    LineGeometry line = new LineGeometry(
                        new Point((double)i / (double)countDot, 
                            .5 - (dataList[0][i] / 2.0)),
                        new Point((double)(i + 1) / (double)countDot, 
                            .5 - (dataList[0][i + 1] / 2.0)));
                    geometryGroup.Children.Add(line);
                }
        
                // Сохраняем описание геометрии
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                // Настраиваем перо
                geometryDrawing.Pen = new Pen(Brushes.Blue, 0.005);
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
        
            // Строим косинус точками
            private void CosFun()
            {
                // Строим описание косинусоиды
                GeometryGroup geometryGroup = new GeometryGroup();
                for (int i = 0; i < dataList[1].Length; i++)
                {
                    EllipseGeometry ellips = new EllipseGeometry(
                        new Point((double)i / (double)countDot, 
                        .5 - (dataList[1][i] / 2.0)), 0.01, 0.01);
                    geometryGroup.Children.Add(ellips);
                }
        
                // Сохраняем описание геометрии
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                // Настраиваем перо
                geometryDrawing.Pen = new Pen(Brushes.Green, 0.005);
        
                // Добавляем готовый слой в контейнер отображения
                drawingGroup.Children.Add(geometryDrawing);
            }
            
            // Надписи
            private void MarkerFun()
            {
                GeometryGroup geometryGroup = new GeometryGroup();
                for (int i = 0; i <= 10; i++)
                {
                    FormattedText formattedText = new FormattedText(
                    String.Format("{0,7:F}", 1 - i * 0.2),
                    CultureInfo.InvariantCulture,
                    FlowDirection.LeftToRight,
                    new Typeface("Verdana"),
                    0.05,
                    Brushes.Black);
        
                    formattedText.SetFontWeight(FontWeights.Bold);
        
                    Geometry geometry = formattedText.BuildGeometry(new Point(-0.2, i * 0.1 - 0.03));
                    geometryGroup.Children.Add(geometry);
                }
        
                GeometryDrawing geometryDrawing = new GeometryDrawing();
                geometryDrawing.Geometry = geometryGroup;
        
                geometryDrawing.Brush = Brushes.LightGray;
                geometryDrawing.Pen = new Pen(Brushes.Gray, 0.003);
        
                drawingGroup.Children.Add(geometryDrawing);
            }
        }
    }
  • Разберитесь с кодом и запустите приложение - результат будет таким
  • Поизменяйте размер окна - рисунок автоматически масштабируется с сохранением качества отображения
  • Упражнение 2. Имитация полноэкранного режима с отключением системных клавиш

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

    window.WindowStyle = WindowStyle.None;
    window.ResizeMode = ResizeMode.NoResize;

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

    Есть более сложный способ, приведенный в "http://www.swart.ws/2009/03/kiosk-full-screen-wpf-applications.html", но он имеет те же недостатки.

    Для достижения полной иллюзии режима FullScreen следует отключить системные клавиши, вызывающие панель задач и меню Пуск, и самый эффективный (на мой взгляд) способ приведен в "http://www.sql.ru/Forum/actualthread.aspx?tid=632552", которым мы и воспользуемся в данном упражнении.

    Из приведенного в статье http://support.microsoft.com/kb/126449/ru сочетания клавиш мы воспользуемся только следующими вариантами:

  • CTRL + ESC: открытие меню Пуск.
  • Клавиша WIN: открытие меню Пуск.
  • ALT + ESC: показать панель задач и переключать развернутые окна.
  • ALT + TAB: переключение между программами.
  • Этого будет достаточно, чтобы поддерживать полноэкранный режим и одновременно не блокировать остальную функциональность клавиатуры.

  • Добавьте к решению командой File/Add/New Project новый проект с именем FullScreen и назначьте его стартовым
  • В панели Solution Explorer добавьте к корню проекта FullScreen новую папку Images командой контекстного меню Add/New Folder
  • В панели Solution Explorer командой Add/Existing Item контекстного меню для папки Images скопируйте в нее файл flower2.jpg из прилагаемого каталога Source
  • Определите в интерфейсной части окна следующий код
  • <Window x:Class="FullScreen.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300"
        Background="Green"
        Loaded="Window_Loaded"
        KeyDown="Window_KeyDown"
        KeyUp="Window_KeyDown"
            >
        <Window.ContextMenu>
            <ContextMenu>
                <MenuItem Name="changeScreen" Click="MenuItem_Click" />
            </ContextMenu>
        </Window.ContextMenu>
        <DockPanel>
            <Menu Name="menu" DockPanel.Dock="Top">
                <MenuItem Header="File">
                    <MenuItem Header="Open" />
                    <MenuItem Header="Save" />
                    <MenuItem Header="SaveAs" />
                    <MenuItem Header="Exit" />
                </MenuItem>
                <MenuItem Header="Edit">
                    <MenuItem Header="Cut" />
                    <MenuItem Header="Copy" />
                    <MenuItem Header="Paste" />
                </MenuItem>
            </Menu>
            <Image Source="Images/flower2.jpg" Stretch="Uniform" /> 
        </DockPanel>
    </Window>

    Для настройки окна и его элементов мы применили синтаксис атрибута, а для создания каркаса контекстного меню - синтаксис тега свойства ( свойства зависимости, присоединенные свойства ). Атрибут Stretch элемента Image может принимать следующие значения:

  • None - изображение масштабируется до естественного размера и в области просмотра справа внизу видно столько, сколько поместилось
  • Fill - изображение масштабируется по всей области просмотра без соблюдения пропорций
  • Uniform - изображение масштабируется с соблюдением пропорций, чтобы полностью поместиться в область просмотра
  • UniformToFill - изображение масштабируется с соблюдением пропорций так, чтобы в область просмотра полностью вместился хотя бы один размер, а для другой стороны видно столько, сколько поместилось
  • Теперь необходимо создать обработчики для событий, выделеных в листинке, в файле присоединенного кода Window1.xaml.cs. Для этого:

  • Найдите в разметке файла Window1.xaml атрибуты событий
  • Loaded="Window_Loaded"
  • KeyDown="Window_KeyDown"
  • KeyUp="Window_KeyDown"
  • Click="MenuItem_Click"
  • В любом месте каждого из этих атрибутов щелкните правой кнопкой мыши и выполните команду Navigate to Event Handler.
  • Оболочка создаст обработчики с указанными именами, а если обработчик уже существует, то перейдет к нему.

  • Заполните файл поддержки разметки 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;
        
    namespace FullScreen
    {
        public partial class Window1 : Window
        {
            // Объявляем как поля для видимости в функциях
            bool fullScreen = false;// Состояние экрана
            WindowStyle windowStyle;
            WindowState windowState;
            ResizeMode resizeMode;
        
            public Window1()
            {
                InitializeComponent();
            }
        
            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                // Запоминаем начальные параметры окна
                windowState = this.WindowState;
                windowStyle = this.WindowStyle;
                resizeMode = System.Windows.ResizeMode.CanResizeWithGrip;
                this.ResizeMode = resizeMode;
                changeScreen.Header = "FullScreen";
            }
        
            private void MenuItem_Click(object sender, RoutedEventArgs e)
            {
                FullScreen();
            }
        
            private void FullScreen()
            {
                if (!fullScreen)// Переходим в полноэкранный режим 
                {
                    if (this.WindowState == WindowState.Maximized)
                    {
                        // Чтобы скрыть панель задач и не было мерцания
                        this.Hide();
                        this.WindowState = WindowState.Normal;
                    }
        
                    App.Current.MainWindow.WindowStyle = WindowStyle.None;// Без заголовка
                    App.Current.MainWindow.Topmost = true;      // На передний план
                    App.Current.MainWindow.WindowState = WindowState.Maximized;// Развернуть
                    App.Current.MainWindow.ResizeMode = ResizeMode.NoResize;// Неизменяемое
        
                    menu.Visibility = Visibility.Collapsed;// Скрываем меню
                    HookSystemKeys.FunHook();// Запрещаем системные клавиши
                    this.Visibility = Visibility.Visible;
        
                    fullScreen = true;
                    changeScreen.Header = "WindowScreen";
                }
                else // Восстанавливаем оконный режим
                {
                    this.WindowStyle = windowStyle;
                    this.Topmost = false;
                    this.WindowState = windowState;
                    this.ResizeMode = resizeMode;
        
                    menu.Visibility = Visibility.Visible;// Показываем меню
                    HookSystemKeys.FunUnHook();// Освобождаем системные клавиши
        
                    fullScreen = false;
                    changeScreen.Header = "FullScreen";
                }
            }
        
            private void Window_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.Key == Key.Escape)
                    this.Close();
            }
        }
    }

    В приведенном коде используется класс HookSystemKeys, который нам нужно подключить к приложению и который взят из "http://www.sql.ru/Forum/actualthread.aspx?tid=632552" (там же и описан).

  • Добавьте к текущему проекту FullScreen командой Project/Add New Item новый файл с именем HookSystemKeys.cs
  • Заполните файл HookSystemKeys.cs так
  • using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
        
    // Для перечисления Keys 
    using System.Windows.Forms; 
        
    namespace FullScreen
    {
        // Отключение системной клавиши
        // http://www.sql.ru/Forum/actualthread.aspx?tid=632552
        class HookSystemKeys
        {
            private const int WH_KEYBOARD_LL = 13;
            private const int WM_KEYUP = 257;// Отпускание любой клавиши
        
            private static LowLevelKeyboardProc _proc = HookCallback;
            private static IntPtr _hookID = IntPtr.Zero;
        
            // Поддержка флагов состояния системных клавиш 
            static bool CtrlKey, AltKey, WinKey;
            private static void StateKey(int vkCode, IntPtr wParam)
            {
                switch ((Keys)vkCode)
                {
                    case Keys.LControlKey:
                    case Keys.RControlKey:
                        if (wParam == (IntPtr)WM_KEYUP)
                            CtrlKey = false;
                        else
                            CtrlKey = true;
                        break;
                    case Keys.LMenu:
                    case Keys.RMenu:
                        if (wParam == (IntPtr)WM_KEYUP)
                            AltKey = false;
                        else
                            AltKey = true;
                        break;
                    case Keys.LWin:
                    case Keys.RWin:
                        if (wParam == (IntPtr)WM_KEYUP)
                            WinKey = false;
                        else
                            WinKey = true;
                        break;
                }
            }
        
            // Общедоступная упаковка оригинала
            public static void FunHook()
            {
                _hookID = SetHook(_proc);
            }
            public static void FunUnHook()
            {
                UnhookWindowsHookEx(_hookID);
            }
        
            private static IntPtr SetHook(LowLevelKeyboardProc proc)
            {
                using (Process curProcess = Process.GetCurrentProcess())
                using (ProcessModule curModule = curProcess.MainModule)
                {
                    return SetWindowsHookEx(WH_KEYBOARD_LL, proc, 
                        GetModuleHandle(curModule.ModuleName), 0);
                }
            }
        
            private delegate IntPtr LowLevelKeyboardProc(int nCode, 
                IntPtr wParam, IntPtr lParam);
        
            private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
            {
                int vkCode = Marshal.ReadInt32(lParam);
                StateKey(vkCode, wParam);
                if (WinKey                                      // Системная
                    || CtrlKey  (Keys)vkCode == Keys.Escape   // Ctrl+Esc
                    || AltKey  (Keys)vkCode == Keys.Escape    // Alt+Esc
                    || AltKey  (Keys)vkCode == Keys.Tab)      // Alt+Tab
                {
                    return (IntPtr)1;
                }
                else
                    return CallNextHookEx(_hookID, nCode, wParam, lParam);
            }
        
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern IntPtr SetWindowsHookEx(int idHook,
                LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
        
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern bool UnhookWindowsHookEx(IntPtr hhk);
        
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, 
                IntPtr wParam, IntPtr lParam);
        
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern IntPtr GetModuleHandle(string lpModuleName);
        }
    }
  • Командой Project/Add Reference добавьте к проекту ссылку на библиотеку System.Windows.Forms.dll, в которой находится перечисление Keys кодов клавиш
  • Запустите проект FullScreen и испытайте построенную нами имитацию полноэкранного режима
  • Оконный и полноэкранный (уменьшенный масштаб) режимы будут выглядеть так

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

  • CTRL + ESC: открытие меню Пуск.
  • Клавиша WIN: открытие меню Пуск.
  • ALT + ESC: показать панель задач и переключать развернутые окна.
  • ALT + TAB: переключение между программами.
  • В оконном режиме функциональность системных клавиш восстанавливается.

    Для полного сходства можно было бы отключить мышь, но тогда все управление окном нужно будет возложить на клавиатуру. В нашем приложении мы для закрытия окна использовали клавишу Esc. Закрыть окно можно и системной комбинацией Alt+F4, которую мы не отключали. Осталась активной комбинация Alt+Space, вызывающая системное меню приложения.

    Мы использовали в классе HookSystemKeys перечисление System.Windows.Forms.Keys библиотечной сборки System.Windows.Forms, а в обработчике Window_KeyDown - перечисление System.Windows.Input.Key библиотечной сборки WindowsBase.dll. Это разные перечисления и их нельзя путать.

  • Попробуйте разобраться с кодом
  • Упражнение 3. Применение объекта ImageDrawing

    Объект ImageDrawing принимает графические данные в виде битовой карты (точечного рисунка). Для создания изображения необходимо создать экземпляр ImageDrawing и установить значения его свойств ImageDrawing.ImageSource и ImageDrawing.Rect. Свойство ImageDrawing.ImageSource задает изображение для рисования, а свойство ImageDrawing.Rect задает положение и размер каждого изображения.

    Если нужно отобразить один рисунок, то его сразу можно присоединить к объекту отображения Image, как мы это делали в предыдущем упражнении. Если же требуется нарисовать несколько рисунков сразу, то необходимо организовать конвейер формирования рисунков. Вначале создаются отдельные слои с помощью ImageDrawing, затем они передаются в накопитель DrawingGroup, где располагаются в Z -последовательности в порядке их добавления. В таком порядке каждый новый слой будет перекрывать все предыдущие и располагаться ближе к пользователю. Затем этот слоеный объект передается в рисовальщик DrawingImage, который и присоединяется, в конечном итоге, к элементу отображения Image.

    Объект DrawingGroup как составной объект рисования, может принимать не только точечные рисунки. В первом упражнении мы использовали его для накопления векторных рисунков геометрии, порожденных объектом GeometryDrawing. Он также способен принимать текстовые данные от GlyphRunDrawing, или медийные данные звука и видео от объекта VideoDrawing. Объект DrawingGroup является единственным типом базового объекта Drawing, который позволяет определять свою собственную область отсечения. Но об этом чуть позже, а сейчас приступим к рассмотрению объекта ImageDrawing.

    Класс ImageDrawing легко спутать с важным классом DrawingImage, названия которых так похожи. Но это нас не должно путать, если мы представим изготовление готового рисунка как последовательность операций на конвейере. Это будет выглядеть примерно так:

    GeometryDrawing -> DrawingGroup -> DrawingImage -> Image
    ImageDrawing
    GlyphRunDrawing
    VideoDrawing
  • Добавьте к решению проект типа WPF Application с именем WpfApp3 и назначьте его стартовым
  • Добавьте к проекту командой Project/New Folder папку с именем Images
  • В панели Solution Explorer вызовите контекстное меню для папки Images и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source пять файлов с рисунками (не забудьте изменить фильтр диалогового окна Add Existing Item на All Files ):
  • market 031.jpg
  • market 032.jpg
  • market 034.jpg
  • market 039.jpg
  • market 040.jpg
  • В панели Solution Explorer выделите все пять рисунков одновременно и в панели Properties установите для них свойства
  • Build Action=Content
  • Copy to Output Directory=Copy if newer
  • В этом упражнении мы рассмотрим два способа использования объекта ImageDrawing - с помощью разметки на XAML и с помощью кода C#. Оба способа работают совершенно одинаково и для того, чтобы это подчеркнуть, мы с помощью них реализуем одну и ту же задачу. Попутно рассмотрим решение некоторых мелких вопросов, таких, как предотвращение повторного запуска созданного окна приложения, назначение главного окна и формирование всплывающей подсказки.

    Вначале создадим главное окно приложения Window1, в котором примененим объект ImageDrawing в кодовой части Window1.xaml.cs. Затем построим дочернее окно Window2, в котором используем объект ImageDrawing в дискрипторной части Window2.xaml.

  • Заполните файл разметки Window1.xaml следующим дескрипторным кодом
  • <Window x:Class="WpfApp3.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Главное окно Window1: Работа объекта ImageDrawing через процедурный код"
        Loaded="Window_Loaded"
        MouseDoubleClick="Show_Window2"
        SizeToContent="WidthAndHeight"
        ResizeMode="NoResize">
        <Window.ContextMenu>
            <ContextMenu>
                <MenuItem Header="Создать дочернее окно" Click="Create_Window2" />
            </ContextMenu>
        </Window.ContextMenu>
    </Window>

    Эта разметка настраивает окно и создает контекстное меню с одним пунктом. Вызов дочернего окна, которое будет иллюстрировать разметочный способ применения объекта ImageDrawing, предусмотрим в обработчиках событий MouseDoubleClick окна и Click контекстного меню. Такое дублирование одной и той же задачи создания дочернего окна выбрано потому, чтобы испытать разные способы предотвращения повторного запуска уже существующего окна.

    Событие Loaded срабатывает после загрузки окна в оперативную память, поэтому в его обработчик удобно поместить код создания объектов содержимого окна. Атрибуты SizeToContent и ResizeMode дескриптора <Window> делают окно подстраиваемым под содержимое (при первом появлении) и неизменяемое в размерах пользователем, соответственно. Контекстное меню прикрепляется к объекту окна и будет вызываться в любой его точке, кроме заголовка.

  • Щелкните правой кнопкой мыши на каждом из событий Loaded, MouseDoubleClick, Click и выполните команду Navigate to Event Handler, чтобы создать в кодовой части обработчики с уже заготовленными именами
  • В файле Window1.xaml.cs заполните обработчик события Loaded следующим кодом
  • private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        // Размеры всех рисунков одинаковы
        const int WIDTH = 348, HEIGHT = 232;
    
        // Создаем накопитель рисунков DrawingGroup 
        DrawingGroup drawingGroup = new DrawingGroup();
    
        // Левый верхний 
        ImageDrawing pict1 = new ImageDrawing();
        pict1.Rect = new Rect(0, 0, WIDTH, HEIGHT);
        pict1.ImageSource = new BitmapImage(
            new Uri(@"Images\market 040.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict1);
    
        // Правый верхний 
        ImageDrawing pict2 = new ImageDrawing();
        pict2.Rect = new Rect(350, 0, WIDTH, HEIGHT);
        pict2.ImageSource = new BitmapImage(
            new Uri(@"Images\market 039.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict2);
    
        // Левый нижний
        ImageDrawing pict3 = new ImageDrawing();
        pict3.Rect = new Rect(0, 234, WIDTH, HEIGHT);
        pict3.ImageSource = new BitmapImage(
            new Uri(@"Images\market 034.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict3);
    
        // Правый нижний
        ImageDrawing pict4 = new ImageDrawing();
        pict4.Rect = new Rect(350, 234, WIDTH, HEIGHT);
        pict4.ImageSource = new BitmapImage(
            new Uri(@"Images\market 032.jpg", UriKind.Relative));
        drawingGroup.Children.Add(pict4);
    
        // Передать рисовальщику
        DrawingImage drawingImageSource = new DrawingImage(drawingGroup);
    
        // Заморозить DrawingImage для лучшей производительности
        drawingImageSource.Freeze();
    
        // Передать элементу отображения
        Image image = new Image();
        image.Stretch = Stretch.None;
        image.Source = drawingImageSource;
    
        // Контейнер Border для присоединения к содержимому окна
        Border border = new Border();
        border.Background = Brushes.White;
        border.BorderBrush = Brushes.White;
        border.BorderThickness = new Thickness(2);  // Толщина рамки
        border.Margin = new Thickness(10);          // Внешний отступ-поле
        border.Child = image;                       // Отдать родителю
    
        this.Background = Brushes.Blue;
        this.Content = border;
    }
  • Дополните конструктор окна Window1 следующим кодом
  • public Window1()
    {
        // Инициализация разметочной части
        InitializeComponent();
    
        // Корректировка заголовка окна
        this.Title += "=\"Так голодают буржуины!\"";
    
        // Всплывающая подсказка
        this.ToolTip = "Вызывайте дочернее окно\n"
            + "двойным щелчком мыши\n"
            + "или контекстным меню...";
    }

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

  • Запустите приложение - первое окно будет выглядеть так
  • Обратите внимание, что окно не имеет системных кнопок минимизации и максимизации, а только кнопку закрытия. Это поведение обеспечивается настройкой в разметке окна с помощью атрибута ResizeMode="NoResize".

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

  • Добавьте к текущему (выделенному в панели Solution Explorer ) проекту WpfApp3 командой Project/Add Window новое окно WPF с именем Window2.xaml
  • Наполните файл Window2.xaml следующей разметкой
  • <Window x:Class="WpfApp3.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Дочернее окно Window2: Работа объекта ImageDrawing через разметку"
        SizeToContent="WidthAndHeight"
        ResizeMode="NoResize"
        Background="Green"
            >
        <Border BorderBrush="White" BorderThickness="2" 
                Margin="10"
                Background="White"
            >
            <Image Stretch="None">
                <Image.Source>
                    <DrawingImage>
                        <DrawingImage.Drawing>
                            <DrawingGroup>
        
                                <!-- Рис.1 - левый верхний (x, y, width, height) -->
                                <ImageDrawing Rect="0,0,348,232" ImageSource="Images\market 031.jpg"/>
        
                                <!-- Рис.2 - правый верхний (x, y, width, height) -->
                                <ImageDrawing Rect="350,0,348,232" ImageSource="Images\market 034.jpg"/>
        
                                <!-- Рис.3 - левый нижний (x, y, width, height) -->
                                <ImageDrawing Rect="0,234,348,232" ImageSource="Images\market 039.jpg"/>
        
                                <!-- Рис.4 - правый нижний (x, y, width, height) -->
                                <ImageDrawing Rect="350,234,348,232" ImageSource="Images\market 040.jpg"/>
        
                            </DrawingGroup>
                        </DrawingImage.Drawing>
                    </DrawingImage>
                </Image.Source>
            </Image>
        </Border>
    </Window>
  • Дополните конструктор класса Window2 следующим кодом
  • public Window2()
    {
        // Инициализация разметочной части
        InitializeComponent();
    
        // Корректировка заголовка окна
        this.Title += "=\"Так голодают буржуины!\"";
    
        // Дочернее окно не отображать в панели задач ОС 
        this.ShowInTaskbar = false;
    }
  • Заполните обработчики щелчка и контекстного меню в классе Window1 следующим кодом
  • Window wnd2;
    private void Show_Window2(object sender, MouseButtonEventArgs e)
    {
        wnd2 = new Window2();
        wnd2.Show();
    }
    
    private void Create_Window2(object sender, RoutedEventArgs e)
    {
        wnd2 = new Window2();
        wnd2.Show();
    }
  • Запустите приложение и вызовите окно Window2 через окно Window1, как напоминает всплывающая подсказка
  • Получится следующий результат
  • Здесь есть несколько существенных недостатков, которые следует устранить:

  • Второе окно можно создавать во многих экземплярах
  • Первое окно со вторым никак не связано и каждое из них закрывается самостоятельно
  • Пиктограмма вторичных окон не отображается в панели задач (мы специально ввели в конструктор окна настройку this.ShowInTaskbar = false; )
  • Во первых, зачем пользователю много экземпляров одного и того же окна. Во вторых, хоть окна не имеют системных кнопок минимизации, все равно их можно минимизировать все сразу через операционную систему командой "Показать рабочий стол". А в этом случае вновь сделать видимыми вторичные окна не удасться и придется их закрывать через Диспетчер задач (либо через оболочку Visual Studio останавливать процесс - работу приложения).

    Для устранения указанных недостатков нужно выполнить следующее:

  • Назначить первое окно главным, чтобы при его закрытии закрывалось приложение в целом
  • Блокировать создание вторичных окон, если одно из них уже существует, и попутно активировать вторичное окно, если оно заслонено другими окнами или скрыто
  • Для назначения окна Window1 главным добавьте в его конструктор следующий код
  • public Window1()
    {
        // Инициализация разметочной части
        InitializeComponent();
    
        // Корректировка заголовка окна
        this.Title += "=\"Так голодают буржуины!\"";
    
        // Всплывающая подсказка
        this.ToolTip = "Вызывайте дочернее окно\n"
            + "двойным щелчком мыши\n"
            + "или контекстным меню...";
    
        // Сделать главным окном приложения
        Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
        Application.Current.MainWindow = this;
    }
  • Запустите приложение и убедитесь, что теперь все дочерние окна при закрытии первичного окна тоже закрываются вместе с приложением
  • Для блокирования повторного создания дочерних окон применим два способа:

  • В первом способе обернем в классе Window1 ссылку wnd2 на дочернее окно в свойство Wnd2 и будем ее обнулять в обработчике Closed окна Window2. В первом окне перед созданием дочернего будем эту ссылку проверять - если ненулевая, значит окно существует и новое создавать нельзя
  • Второй способ намного проще, так как не требует жесткой зависимости кода между окнами введением дополнительного общего поля или свойства. Можно просто воспользоваться свойством Application.Current.Windows типа WindowCollection приложения и в первом окне перед созданием дочернего проверять, отсутствует ли в коллекции существующих окон приложения интересующее нас окно. Любое созданное окно приложения сразу заносится в эту коллекцию, а при закрытии между событиями Closing и Closed немедленно удаляется из нее. Единственное, что потребуется определить в дочернем окне, так это свойство Name, по которому мы будем искать это окно в коллекции приложения
  • Итак, реализуем сказанное...

    Способ 1

  • В классе Window1 найдите объявление поля wnd2 (строка: Window wnd2; ), щелкните на поле wnd2 правой кнопкой мыши и выполните команду контекстного меню Refactor/Encapsulate Field
  • Появится окно Encapsulate Field, предлагающее обернуть поле в общедоступное свойство с именем Wnd2

  • Скажите OK и оболочка создаст следующий код свойства
  • public static Window Wnd2
    {
        get { return Window1.wnd2; }
        set { Window1.wnd2 = value; }
    }

    Аксессор get нам не нужен, поскольку в клиенте Window2 при закрытии окна мы будем поле wnd2 только обнулять через свойство Wnd2, поэтому get следует удалить.

  • Внесите в класс Window1 следующий окончательный код реализации первого способа
  • // Предотвращение повторного открытия окна: Способ 1
    static Window wnd2;
    public static Window Wnd2
    {
        set { Window1.wnd2 = value; }
    }
    // Обработчик двойного щелчка
    private void Show_Window2(object sender, MouseButtonEventArgs e)
    {
        if (wnd2 == null)
        {
            wnd2 = new Window2();
            wnd2.Show();
        }
        else
            wnd2.Activate();// Сдвинуть на передний план
    }
  • Модифицируйте класс Window2 следующим образом
  • public partial class Window2 : Window
    {
        public Window2()
        {
            // Инициализация разметочной части
            InitializeComponent();
    
            // Корректировка заголовка окна
            this.Title += "=\"Так голодают буржуины!\"";
    
            // Дочернее окно не отображать в панели задач ОС 
            this.ShowInTaskbar = false;
    
            // Регистрация обработчика
            this.Closed += new EventHandler(Window2_Closed);
        }
    
        private void Window2_Closed(object sender, EventArgs e)
        {
            Window1.Wnd2 = null;        // Для предотвращения повторного запуска
            GC.WaitForFullGCComplete(); // Ждать завершения сборки мусора
            GC.Collect();               // Начать сборку мусора
        }
    }

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

  • Запустите приложение и убедитесь, что обработчик создания дочернего окна по двойному щелчку теперь работает правильно
  • Способ 2

  • В разметку файла Window2.xaml добавьте определение свойства Name окна Window2 для его идентификации в коллекции приложения
  • <Window x:Class="WpfApp3.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Name="Window_2"
        Title="Дочернее окно Window2: Работа объекта ImageDrawing через разметку"
        SizeToContent="WidthAndHeight"
        ResizeMode="NoResize"
        Background="Green"
            >
        ...................................
    </Window>
  • Модифицируйте обработчик контекстного меню в файле Window1.xaml.cs следующим образом
  • // Предотвращение повторного открытия окна: Способ 2
    // Обработчик контекстного меню
    private void Create_Window2(object sender, RoutedEventArgs e)
    {
        bool windowExists = false;
        foreach (Window window in Application.Current.Windows)
        {
            if (window.Name == "Window_2")
            {
                windowExists = true;
                window.Activate();// Сдвинуть на передний план
                break;
            }
        }
    
        if (!windowExists)
        {
            wnd2 = new Window2();
            wnd2.Show();
        }
    }
  • Запустите приложение и убедитесь, что и этот обработчик блокирования повторного запуска теперь тоже работает правильно
  • Разберитесь с кодом
  • В будущих упражнениях мы продолжим знакомство с графическими возможностями WPF.

    Вернуться к учебному плану