Кнопки как в супербаре Windows 7
6 апреля 2011 - 18:54
Ещё начиная с момента выхода Windows 7, мне хотелось сделать на WPF-е стиль для кнопок, такой же как в супербаре. И наконец-то у меня дошли до этого руки.
Итак, я начал с того, что создал простой стиль для кнопки, визуально похожий на то, как кнопка в обычном состоянии выглядит в супербаре:
В данном стиле Border задаёт чёрную рамку, первый Rectangle задаёт белую рамку, второй Rectangle задаёт хитрый блик, и в Image содержится иконка, путь в которой задаётся в Content-е кнопки.
Дальше, используя VisualState-ы, я добавил нажатое состояние, для этого я сделал фон первого Rectangle-а полупрозрачным белым и модифицировал Margin у Image-а:
Теперь я реализую подсветку при наведении, для этого не понадобилось получить основной цвет из иконки. Я сделал это с помощью конвертера (IValueConverter) самым простейшим способом: уменьшил иконку до размера в один пиксель и посчитал цвет этого пикселя средним:
Теперь в стиле я создаю ещё один Rectangle, с радиальным градиентом, использующий цвет, полученный из Image-а, с помощью данного конвертера:
И добавляю в VisualState-ы, появление этого Rectangle-а при наведении мыши:
Осталось сделать, чтоб центр радиального градиента перемещался в зависимости от положения мышки. Для этого я создал два AttachedProperty. Первое свойство - UseColorOffset, используется для того, чтобы включить подписку на события мыши для кнопки. А во втором - ColorOffset, сохраняется точка центра градиента:
Теперь в стиль я добавлю установку UseColorOffset в True:
И поменяю параметры RadialGradientBrush-а так, чтоб они использовали свойство ColorOffset:
В результате мы получили кнопки, очень похожие на кнопки в супербаре Windows 7.
Для большего сходства я расширил Aero на всю форму:
Скачать исходники:
PS. Данный пример написан исключительно для демонстрации возможностей WPF-а, и я не думаю, что существует приложение, в котором бы было разумно использовать кнопки с таким эффектом.
Итак, я начал с того, что создал простой стиль для кнопки, визуально похожий на то, как кнопка в обычном состоянии выглядит в супербаре:
<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>
Дальше, используя 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>
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 x:Name="MouseOver"> <Storyboard> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="rectangle1"> <EasingDoubleKeyFrame KeyTime="0" Value="1"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </VisualState>
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 && 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"/>
<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>
Для большего сходства я расширил 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-а, и я не думаю, что существует приложение, в котором бы было разумно использовать кнопки с таким эффектом.