Введение в WPF. Часть 2. Анатомия Window

Часть 1

Пришла очередь препарировать окна. Вспомним минимальный код, из прошлой статьи, который позволил нам с Вами, отобразить программно окно с использованием WPF:

using System;
using System.Windows;
 
namespace Juice.HelloWpf
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            Application app = new Application();
            Window windows = new Window();
            windows.Show();
            app.Run();
        }
    }
}

Что происходит в момент создания окна? Начнем с конструктора. Конструируя окно WPF, поступает примерно следующим образом. Перво-наперво устанавливает ряду полей значения по умолчанию. Во вторых проверяет, существование Application. Зачем? Ну просто для того, что бы не заставлять нас с Вами, выполнять лишние телодвижения. Если мы посмотрим еще раз на код приведенный Выше и вспомним о том, что приложение имеет главное окно, то можем задать вопрос, где мы его установили? Попробуем провести эксперимент. Перепишем код так:

using System;
using System.Windows;
 
namespace Juice.HelloWpf
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            Application app = new Application();
            Window windows = new Window();
            app.MainWindow.Show();
            app.Run();
        }
    }
}

Код также отлично отработал. Следствие этого не сложного эксперимента, следующие: окно само сделало себя главным и добавило в коллекцию Windows класса Application. А теперь попробуем поменять местами две строчки кода:

using System;
using System.Windows;
 
namespace Juice.HelloWpf
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            Window windows = new Window();
            Application app = new Application();
            app.MainWindow.Show();
            app.Run();
        }
    }
}

И убедимся, что от перестановки слагаемых, в нашем случае, сумма изменяется. На момент создание окна, не существовало экземпляра Application, окно выяснив это не «смогло» себя добавить в соответствующее свойство. А объект Application сможет установить корректные значения только в методе Run(). Именно по этому app.MainWindow вырнуло null в программу и попытка вызвать метод Show() привела к исключению.

Каждый экземпляр окна имеет внутреннее свойство App, которое позволяет ему получить ссылку на экземпляр приложения. Окно пользуется им в двух ситуациях. Первую мы уже рассмотрели, вторая ситуация возникает когда окно закрывается, оно удаляет себя из коллекции Windows экземпляра приложения и обнуляет MainWindow, только в том случае если окно было главным окном приложения. Именно поэтому коллекция Windows у Application всегда поддерживается в актуальном состоянии.

Разберемся с позиционированием окном. В отличие от WindowsForms где у Form было свойство Location, Window такого свойства не имеет. Зато присутствуют известные нам по WindwosForm свойства Left и Top.

Left - отступ от левого, края рабочего стола. Top - отступ от верхнего, края рабочего стола.

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

Первое, что вызывает удивление это использование типа double для хранения этих значений. Но удивление быстро проходит. Оказывается в WPF, размеры исчисляются не в обычных пикселах, а в логических пикселах. Которые, на самом деле, не зависят от разрешения монитора. В англоязычной литературе часто встречается сокращение DIP (Device Independent Pixel). 96 DIP = 1 дюйму = 2.54 сантиметра. А так как разрешающая способность для большинства современных мониторов это 96 DPI, то в подавляющем большинстве случаев 1 DIP = 1 физическому пикселу.

По этой ссылке находится один интересный проект с открытым исходным кодом и в нем есть класс DipHelper который позволяет легко оперировать DIP в метрической и дюймовой системах счисления.

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

Для управления размером окон мы можем использовать свойства Width и Height. Тем не менее их поведение отличается от привычного нам по модели WindowsForms. Поставим простой эксперимент.

using System;
using System.Windows;
 
namespace HelloWPFNew
{
    public class HeloWPF
    {
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            Window window = new Window();
            window.Title = String.Format("Width: {0}; Height: {1};", window.Width, window.Height);
            app.Run(window);
        }
    }
}

Результатом работы программы есть вывод в заголовок окна, значений его ширины и высоты. Для задания заголовка используется свойство Title. Результат мягко говоря удивительный. Но оказывается Width и Height не инициализируются окном при создании, вернее инициализируются значением NaN. Размерами окна по умолчанию заведует операционная система, а мы с помощью этих свойств лишь можем попросить ее сделать окно нужного нам размера.

Например так:

using System;
using System.Windows;
 
namespace HelloWPFNew
{
    public class HeloWPF
    {
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            Window window = new Window();
            window.Width = 320;
            window.Height = 240;
            window.Show();
            app.Run();
        }
    }
}

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

Для того, что бы узнать действительные размеры окна мы можем воспользоваться свойствами ActualWidth и ActualHeight, которые мы можем использовать после того как окно уже было отображено.

Чтоб посмотреть на эти свойства в действии посмотрите на следующую, не сложную, программу:

using System;
using System.Windows;
 
namespace HelloWPFNew
{
    public class HeloWPF
    {
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            Window window = new Window();
            Console.WriteLine("ActualWidth: {0}; ActualHeight: {1};", window.ActualWidth, window.ActualHeight);
            window.Show();
            Console.WriteLine("ActualWidth: {0}; ActualHeight: {1};", window.ActualWidth, window.ActualHeight);
            app.Run();
        }
    }
}

Результат работы:

ActualWidth: 0; ActualHeight: 0;
ActualWidth: 960; ActualHeight: 559;

Еще одним способом позиционирования окон является свойство WindowsStartupLocation, класса Window, которое может принимать значения:

WindowStartupLocation.Manual (по умолчанию) – выбирается операционной системой, или переопределяется программистом.

WindowStartupLocation.CenterScreen – выравнивание по центру рабочей области

WindowStartupLocation.CenterOwner – используется при выравнивании относительно родительского окна.

Для того, что бы закрепить сегодняшний материал, напишем простую программу, когда созданное нами окно, будет по нажатию соответствующих клавиш, «прикреплять» к краям рабочей области. При этом окно будет занимать ровно половину рабочей области. Отлавливать мы будем нажатия следующих клавиш T (Top), B(Bottom), L(Left), R(Right).

using System;
using System.Windows;
using System.Windows.Input;
 
namespace HelloWPFNew
{
    public class HeloWPF
    {
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
 
            // создаем окно
            Window window = new Window();
            window.Width = 320;
            window.Height = 240;
 
            // позиционируем по центру рабочей области
            window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
 
            // подписываемся на уведомление о нажатии клавиш
            window.KeyUp += new KeyEventHandler(window_KeyUp);
 
            // отображаем окно
            window.Show();
            app.Run();
        }
 
        static void window_KeyUp(object sender, KeyEventArgs e)
        {
            // выясняем текущий размер рабочей области
            Rect workArea = SystemParameters.WorkArea;
 
            // получаем ссылку на окно
            Window window = sender as Window;
 
            // позиционируем окно взависимости от пользовательского ввода
            switch (e.Key)
            {
                case Key.T:
                    {
                        window.Left = 0;                        
                        window.Top = 0;
                        window.Width = workArea.Width;
                        window.Height = workArea.Height / 2;
                        break;
                    }
                case Key.B:
                    {                        
                        window.Width = workArea.Width;
                        window.Height = workArea.Height / 2;
                        window.Left = 0;
                        window.Top = workArea.Height - window.Height;
 
                        break;
                    }
                case Key.L:
                    {
                        window.Left = 0;
                        window.Top = 0;
                        window.Width = workArea.Width / 2;
                        window.Height = workArea.Height;
                        break;
                    }
                case Key.R:
                    {
                        window.Width = workArea.Width / 2;
                        window.Height = workArea.Height;
                        window.Left = workArea.Width - window.Width;
                        window.Top = 0;
                        break;
                    }
            }
        }
    }
}

В следующий раз, попробуем разобраться с тем, как WPF «красит» окна. А может окна сами красят себя? :-)