Кнопки как в супербаре Windows 7


Ещё начиная с момента выхода Windows 7, мне хотелось сделать на WPF-е стиль для кнопок, такой же как в супербаре. И наконец-то у меня дошли до этого руки.
Кнопки как в супербаре Windows 7

Итак, я начал с того, что создал простой стиль для кнопки, визуально похожий на то, как кнопка в обычном состоянии выглядит в супербаре:
<Style x:Key="SuperBarButtonStyle" TargetType="Button">
  <Setter Property="Width" Value="60"/>
  <Setter Property="Height" Value="40"/>
  <Setter Property="Margin" Value="1"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="Button">
        <Border BorderBrush="#BB000000" BorderThickness="1" CornerRadius="1" Background="Transparent">
            <Grid>
              <Rectangle x:Name="rectangle" Margin="0" Stroke="#99FFFFFF" RadiusY="1" RadiusX="1" OpacityMask="{x:Null}" Fill="#00000000"/>
              <Rectangle Margin="0" Stroke="{x:Null}" RadiusY="1" RadiusX="1">
                <Rectangle.OpacityMask>
                  <RadialGradientBrush Center="1,1" GradientOrigin="1,1" RadiusY="1" RadiusX="1">
                    <GradientStop Offset="0" Color="#19FFFFFF"/>
                    <GradientStop Color="#BFFFFFFF" Offset="1"/>
                    <GradientStop Offset="0.99"/>
                  </RadialGradientBrush>
                </Rectangle.OpacityMask>
                <Rectangle.Fill>
                  <RadialGradientBrush Center="0.0,0.0" GradientOrigin="0.0,0.0" RadiusY="0.9" RadiusX="0.9">
                    <GradientStop Color="#7FFFFFFF"/>
                    <GradientStop Offset="0.9"/>
                  </RadialGradientBrush>
                </Rectangle.Fill>
            </Rectangle>
            <Image x:Name="image" HorizontalAlignment="Center" VerticalAlignment="Center" Width="32" Height="32" Stretch="Uniform" Margin="0" Source="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}" />
          </Grid>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
В данном стиле Border задаёт чёрную рамку, первый Rectangle задаёт белую рамку, второй Rectangle задаёт хитрый блик, и в Image содержится иконка, путь в которой задаётся в Content-е кнопки.
Дальше, используя VisualState-ы, я добавил нажатое состояние, для этого я сделал фон первого Rectangle-а полупрозрачным белым и модифицировал Margin у Image-а:
<VisualState x:Name="Pressed">
  <Storyboard>
    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="rectangle">
      <EasingColorKeyFrame KeyTime="0" Value="#72FFFFFF"/>
    </ColorAnimationUsingKeyFrames>
    <ThicknessAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Margin)" Storyboard.TargetName="image">
      <EasingThicknessKeyFrame KeyTime="0" Value="1,1,0,0"/>
    </ThicknessAnimationUsingKeyFrames>
  </Storyboard>
</VisualState>
Теперь я реализую подсветку при наведении, для этого не понадобилось получить основной цвет из иконки. Я сделал это с помощью конвертера (IValueConverter) самым простейшим способом: уменьшил иконку до размера в один пиксель и посчитал цвет этого пикселя средним:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    Image img = value as Image;
    if (img != null)
    {
        BitmapSource src = img.Source as BitmapSource;
        if (src != null)
        {
            if (src.Format != PixelFormats.Rgb24)
            {
                // Если изображение не в формате RGB24, то переводим его в этот формат
                src = new FormatConvertedBitmap(src, PixelFormats.Rgb24, null, 1);
            }
            // Ужимаем иконку до размера 1x1
            TransformedBitmap bmp = new TransformedBitmap(src, new ScaleTransform(1.0/src.Width, 1.0/src.PixelHeight));
            // Достаём цвет пиксела
            byte[] pixels = new byte[3];
            bmp.CopyPixels(pixels, 3, 0);
            // Возвращаём цвет полученного пиксела
            return Color.FromArgb(255, pixels[0], pixels[1], pixels[2]);
        }
    }
    return Colors.Yellow;
}

Теперь в стиле я создаю ещё один Rectangle, с радиальным градиентом, использующий цвет, полученный из Image-а, с помощью данного конвертера:
<Rectangle x:Name="rectangle1" Margin="1" RadiusY="1" RadiusX="1" Stroke="{x:Null}" OpacityMask="{x:Null}" Opacity="0" StrokeThickness="0">
  <Rectangle.Fill>
    <RadialGradientBrush Center="0.5, 1" GradientOrigin="0.5, 1">
      <GradientStop Color="White" Offset="0"/>
      <GradientStop Color="{Binding ElementName=image, Converter={StaticResource ImageToColorConvert}}" Offset="1"/>
    </RadialGradientBrush>
  </Rectangle.Fill>
</Rectangle>
И добавляю в VisualState-ы, появление этого Rectangle-а при наведении мыши:
<VisualState x:Name="MouseOver">
  <Storyboard>
    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="rectangle1">
      <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
  </Storyboard>
</VisualState>
Осталось сделать, чтоб центр радиального градиента перемещался в зависимости от положения мышки. Для этого я создал два AttachedProperty. Первое свойство - UseColorOffset, используется для того, чтобы включить подписку на события мыши для кнопки. А во втором - ColorOffset, сохраняется точка центра градиента:
public static class ControlExtender
{
    public static bool GetUseColorOffset(DependencyObject obj)
    {
        return (bool)obj.GetValue(UseColorOffsetProperty);
    }

    public static void SetUseColorOffset(DependencyObject obj, bool value)
    {
        obj.SetValue(UseColorOffsetProperty, value);
    }

    // Using a DependencyProperty as the backing store for UseColorOffset.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty UseColorOffsetProperty = DependencyProperty.RegisterAttached("UseColorOffset", typeof(bool), typeof(ControlExtender), new UIPropertyMetadata(false, OnUseColorOffsetPropertyChanged));

    private static void OnUseColorOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        bool newValue = (bool)e.NewValue;
        FrameworkElement element = d as FrameworkElement;
        if (element == null) return;

        if (newValue)
        {
            // Подписываемся на события мыши
            element.MouseMove += OnMousePositionChanged;
            element.MouseLeave += OnMousePositionChanged;
        }
        else
        {
            // Отписываемся от событий мыши
            element.MouseMove -= OnMousePositionChanged;
            element.MouseLeave -= OnMousePositionChanged;
        }
    }

    private static void OnMousePositionChanged(object sender, MouseEventArgs e)
    {
        FrameworkElement element = sender as FrameworkElement;
        if (element != null &amp;&amp; element.ActualWidth > 0)
        {
            Point mousePos = Mouse.GetPosition(element as IInputElement);
            if (mousePos.X < 0) mousePos.X = 0;
            else if (mousePos.X > element.ActualWidth) mousePos.X = element.ActualWidth;
            // В зависимости от положения мыши выставляем X координату точкм в диапазоне от 0 до 1
            SetColorOffset(sender as DependencyObject, new Point(mousePos.X/element.ActualWidth, 1));
        }
    }

    public static Point GetColorOffset(DependencyObject obj)
    {
        return (Point)obj.GetValue(ColorOffsetProperty);
    }

    public static void SetColorOffset(DependencyObject obj, Point value)
    {
        obj.SetValue(ColorOffsetProperty, value);
    }

    // Using a DependencyProperty as the backing store for ColorOffset.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ColorOffsetProperty =
        DependencyProperty.RegisterAttached("ColorOffset", typeof(Point), typeof(ControlExtender), new UIPropertyMetadata(new Point(0.5, 1)));        
}

Теперь в стиль я добавлю установку UseColorOffset в True:
<Setter Property="SuperbarButton:ControlExtender.UseColorOffset" Value="True"/>
И поменяю параметры RadialGradientBrush-а так, чтоб они использовали свойство ColorOffset:
<Rectangle x:Name="rectangle1" Margin="1" RadiusY="1" RadiusX="1" Stroke="{x:Null}" OpacityMask="{x:Null}" Opacity="0" StrokeThickness="0">
  <Rectangle.Fill>
    <RadialGradientBrush Center="{Binding Path=(SuperbarButton:ControlExtender.ColorOffset), RelativeSource={RelativeSource TemplatedParent}}" GradientOrigin="{Binding Path=(SuperbarButton:ControlExtender.ColorOffset), RelativeSource={RelativeSource TemplatedParent}}">
      <GradientStop Color="White" Offset="0"/>
      <GradientStop Color="{Binding ElementName=image, Converter={StaticResource ImageToColorConvert}}" Offset="1"/>
    </RadialGradientBrush>
  </Rectangle.Fill>
</Rectangle>
В результате мы получили кнопки, очень похожие на кнопки в супербаре Windows 7.
Для большего сходства я расширил Aero на всю форму:
public MainWindow()
{
    InitializeComponent();
    this.Loaded += (sender, e) =>
                        {
                            CreateGlass();
                        };
}

private void CreateGlass()
{
    try
    {
        IntPtr mainWindowPtr = new WindowInteropHelper(this).Handle;
        HwndSource mainWindowSrc = HwndSource.FromHwnd(mainWindowPtr);
        mainWindowSrc.CompositionTarget.BackgroundColor = Color.FromArgb(0, 0, 0, 0);

        Margins margins = new Margins();

        margins.cxLeftWidth = -1;
        margins.cxRightWidth = -1;
        margins.cyTopHeight = -1;
        margins.cyBottomHeight = -1;

        int hr = DwmExtendFrameIntoClientArea(mainWindowSrc.Handle, ref margins);

        if (hr < 0)
        {
            Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFB9D1EA"));
        }
    }

    catch (DllNotFoundException)
    {
        Background = Brushes.LightSkyBlue;
    }
}

#region Interop

[StructLayout(LayoutKind.Sequential)]
private struct Margins
{
    public int cxLeftWidth; 
    public int cxRightWidth;
    public int cyTopHeight; 
    public int cyBottomHeight;
};

[DllImport("DwmApi.dll")]
private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref Margins pMarInset);

#endregion

Замечания:

  • Я не ставил перед собой цели добиться стопроцентного соответствия кнопкам супербара Windows 7, поэтому стеклянный блик и эффект нажатия не полностью повторяют оные на кнопках супербара.
  • Алгоритм получения среднего цвета сделан абсолютно примитивно и возможно реализация более корректного способа, позволила бы получить более красивый эффект.
  • Свойства UseColorOffset и ColorOffset имеют названия не соответствующие своей функциональности, поэтому их было бы неплохо переименовать более корректно.
  • Реализация расширения Aero на всё окно не достаточно корректно, поскольку не учитывает возможность включения и отключения Aero в процессе работы приложения.
  • Иконки используемые в примере были найдены где-то в интернете, поэтому кому принадлежит их авторство, я не знаю.

Скачать исходники:


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

КОММЕНТАРИИ


НОВЫЙ КОММЕНТАРИЙ


*жирный*
_курсив_
+подчеркнутый+
! заголовок 1
!! заголовок 2
* список
** список 2
# нумерованый список
## нумерованый список 2
[url:http://www.example.com]
{"без форматирования"}
Полное описание синтаксиса