Настройка нестандартных окон

28

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

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

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

В данном случае в качестве контейнера макета служит элемент StackPanel, но в принципе это может быть и какой-то другой элемент (например, еще один Grid или Canvas для абсолютного позиционирования на основе координат). В этом элементе StackPanel находится кнопка закрытия (со знакомым значком X) и текст:

<Grid>
		<Path Stroke="DarkGray" StrokeThickness="1" SnapsToDevicePixels="True">

      <Path.Fill>
        <LinearGradientBrush StartPoint="0.2,0" EndPoint="0.8,1" >
          <LinearGradientBrush.GradientStops>
            <GradientStop Color="White"  Offset="0"></GradientStop>
            <GradientStop Color="White"  Offset="0.45"></GradientStop>
            <GradientStop Color="LightBlue" Offset="0.9"></GradientStop>
            <GradientStop Color="Gray" Offset="1"></GradientStop>
          </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>
      </Path.Fill>

      <Path.Data>
        <PathGeometry>
          <PathGeometry.Figures>
            <PathFigure StartPoint="20,0" IsClosed="True">
              <LineSegment Point="140,0"></LineSegment>
              <ArcSegment Point="160,20" Size="20,20" SweepDirection="Clockwise"></ArcSegment>
              <LineSegment Point="160,60"></LineSegment>
              <ArcSegment Point="140,80" Size="20,20" SweepDirection="Clockwise"></ArcSegment>
              <LineSegment Point="70,80"></LineSegment>
              <LineSegment Point="70,130"></LineSegment>
              <LineSegment Point="40,80"></LineSegment>
              <LineSegment Point="20,80"></LineSegment>
              <ArcSegment Point="0,60" Size="20,20" SweepDirection="Clockwise"></ArcSegment>
              <LineSegment Point="0,20"></LineSegment>
              <ArcSegment Point="20,0" Size="20,20" SweepDirection="Clockwise"></ArcSegment>
            </PathFigure>
          </PathGeometry.Figures>
        </PathGeometry>
      </Path.Data>
      <Path.RenderTransform>
        <ScaleTransform ScaleX="1.3" ScaleY="1.3"></ScaleTransform>
      </Path.RenderTransform>

    </Path>

    <StackPanel Margin="5">
      <!--Close button-->
      <Button HorizontalAlignment="Right" Click="cmdClose_Click" Margin="0,5,10,0">
        x
      </Button>

      <!--<Label  BorderBrush="LightGray" BorderThickness="1" MouseLeftButtonDown="window_MouseLeftButtonDown" FontSize="15" HorizontalAlignment="Center">Drag Here</Label>-->
      <TextBlock TextWrapping="Wrap"  MouseLeftButtonDown="window_MouseLeftButtonDown" FontSize="15" HorizontalAlignment="Center">This is a balloon-shaped window.</TextBlock>
    </StackPanel>


  </Grid>
Окно нестандартной формы использующее элемент Path

Ключевым компонентом в данном примере является элемент Path, который создает фон. Он представляет собой простую векторную фигуру, которая состоит из ряда линий и дуг.

В текущий момент элемент Path имеет фиксированный размер (так же, как и окно), однако его размер несложно сделать изменяемым, поместив элемент в контейнер Viewbox. Еще улучшить этот пример можно, придав кнопке для закрытия окна более убедительный внешний вид — например, с помощью векторного значка X, прорисовываемого на красной поверхности. Хотя для представления кнопки и обработки связанных с ней событий мыши можно было бы воспользоваться и отдельным элементом Path, лучше поступить следующим образом: изменить стандартный элемент управления Button путем применения шаблона и затем сделать элемент Path, прорисовывающий значок X, частью этой измененной кнопки.

Перемещение окон нестандартной формы

Одним из ограничений окон нестандартной формы является то, что в них отсутствует неклиентская область со строкой заголовка, позволяющая пользователю легко перетаскивать окно по рабочему столу. В Windows Forms это причиняло определенные неудобства — приходилось либо обеспечивать реакцию на события мыши вроде MouseDown, MouseUp и MouseMove и перемещать окно вручную при выполнении пользователем щелчка и перетаскивания, либо переопределять метод WndProc() и обрабатывать низкоуровневое сообщение WM_NCHITTEST.

В WPF эта задача решается гораздо легче. Здесь в любое время можно инициировать режим перетаскивания окна путем вызова метода Window.DragMove().

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

private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
   this.DragMove();
}

Теперь окно будет следовать за курсором мыши по экрану до тех пор, пока пользователь не отпустит кнопку мыши.

Изменение размеров окон нестандартной формы

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

Например, в случае создания окна с эффектом скругленных краев за счет использования объекта Border, такой прием может и сработать. Элемент захвата и изменения размера появится в правом нижнем углу и, в зависимости от того, насколько скругленным был сделан этот угол, разместится в пределах поверхности окна, которому принадлежит. Но в случае создания окна более экзотической формы с применением, например, элемента Path, такой подход точно не сработает — элемент захвата и изменения размера "зависнет" в пустой области рядом с окном.

Если добавление элемента захвата и изменения размера не подходит для окна данной формы или если требуется разрешить пользователю изменять размеры окна путем перетаскивания его краев, придется приложить немного дополнительных усилий. В принципе в таком случае существует два основных подхода. Первый — использовать .NET-функцию вызова платформы (P/Invoke) для отправки сообщения Win32, изменяющего размер окна, а второй — просто отслеживать позицию курсора мыши при перетаскивании пользователем окна в одну сторону и изменять размер вручную установкой свойства Width. Ниже рассматривается пример применения второго подхода.

Прежде чем воспользоваться любым из этих подходов, нужно придумать способ для определения момента наведения пользователем курсора мыши на край окна. В WPF это легче всего сделать, разместив вдоль каждого края окна специальный элемент. Быть видимым этому элементу вовсе необязательно — на самом деле он даже может быть полностью прозрачным и позволять окну проглядывать сквозь него. Его единственная задачей будет перехват событий мыши.

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

<Rectangle Grid.RowSpan="3" Width="5" VerticalAlignment="Stretch" HorizontalAlignment="Right"
            Cursor="SizeWE" Fill="Transparent"
            MouseLeftButtonDown="Rectangle_MouseLeftButtonDown" MouseLeftButtonUp="Rectangle_MouseLeftButtonUp"
            MouseMove="Rectangle_MouseMove"></Rectangle>

В данном случае элемент Rectangle размещается в верхней строке, но для свойства RowSpan получает значение 3. Благодаря этому он растягивается на все три строки и занимает всю правую сторону окна. В свойстве Cursor указывается тот курсор мыши, который должен появляться при наведении мыши на этот элемент. В данном случае это курсор изменения размера "запад-восток" — он имеет знакомую всем форму двухконечной стрелки, которая указывает вправо и влево.

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

private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
      isWiden = true;
}

private void Rectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
      isWiden = false;
      Rectangle rect = (Rectangle)sender;
      rect.ReleaseMouseCapture();
}

private void Rectangle_MouseMove(object sender, MouseEventArgs e)
{
      Rectangle rect = (Rectangle)sender;
      if (isWiden)
     {
           rect.CaptureMouse();
           double newWidth = e.GetPosition(this).X + 5;
           if (newWidth > 0) this.Width = newWidth;
     }
}
Изменение размеров окна нестандартной формы
Пройди тесты
Лучший чат для C# программистов