Reputation: 330982
I have a data grid control where I am using a custom effect called FastShadow which is like a glow.
I want the effect to glow outside its bounds, so that's fine but when I draw another shape on top, I don't want this shape to be affected. In this case, it's the Green/Blue Rectangle shapes.
As you can see these rectangle shapes get discolored by the glow effect. The ones without the glow effect behind them looks fine.
Here is the XAML:
<Window x:Class="test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:test"
mc:Ignorable="d"
Name="myMainWindow"
SizeToContent="Width"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Profit Tracker"
WindowStyle="None"
Topmost="True"
Height="426">
<Window.Resources>
<Style x:Key="DataGridColumnSeparatorStyle" TargetType="DataGridCell">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Rectangle VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Fill="#1e90ff"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DataGridColumnAlarmStyle" TargetType="DataGridCell">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Rectangle VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Fill="#000000"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type DataGrid}">
<Setter Property="Background" Value="#FFF" />
<Setter Property="AlternationCount" Value="2" />
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style x:Key="RowStyleWithAlternation" TargetType="DataGridRow">
<Setter Property="Background" Value="#141414"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Normal"/>
<Style.Triggers>
<Trigger Property="AlternationIndex" Value="0">
<Setter Property="Background" Value="#141414"/>
</Trigger>
<Trigger Property="AlternationIndex" Value="1">
<Setter Property="Background" Value="#282828"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Margin" Value="-1,0,0,0" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<!--<Setter Property="BorderBrush" Value="#2eff00" />
<Setter Property="BorderThickness" Value="1" />-->
<Setter Property="Background" Value="Orange"/>
<!--<Setter Property="Margin" Value="-1,0,0,0" />-->
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="TextBlock.TextAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Grid Background="{TemplateBinding Background}">
<ContentPresenter VerticalAlignment="Stretch"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="BorderBrush" Value="#1e90ff" />
<Setter Property="BorderThickness" Value="1" />
<!--<Setter Property="Background" Value="Red"/>-->
</Trigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Column.DisplayIndex, RelativeSource={RelativeSource Self}}" Value="4"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="VerticalAlignment" Value="Stretch"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="BorderBrush" Value="{x:Null}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style TargetType="{x:Type ProgressBar}">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Border BorderThickness="1" Background="#006400" CornerRadius="0" Padding="0">
<Grid x:Name="PART_Track">
<Rectangle x:Name="PART_Indicator" HorizontalAlignment="Left" Fill="#75001D" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<CollectionViewSource Source="{Binding Coins}" IsLiveSortingRequested="True" x:Key="MyKey" />
</Window.Resources>
<Grid>
<Grid.LayoutTransform>
<ScaleTransform ScaleX="{Binding ViewScale, ElementName=myMainWindow}" ScaleY="{Binding ViewScale, ElementName=myMainWindow}" />
</Grid.LayoutTransform>
<DataGrid Name="dataGrid" ItemsSource="{Binding Source={StaticResource MyKey}}" SelectionMode="Single" GridLinesVisibility="None" HorizontalScrollBarVisibility="Hidden" RowHeaderWidth="0" IsReadOnly="True" CanUserAddRows="False" CanUserResizeColumns="False" CanUserResizeRows="False" AutoGenerateColumns="False" RowStyle="{StaticResource RowStyleWithAlternation}">
<DataGrid.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="" MaxWidth="40">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid Visibility="{Binding IsVisible}">
<Border Margin="1" VerticalAlignment="Stretch" BorderThickness="0" Background="#ffff00" HorizontalAlignment="Left" Width="7">
<local:FastShadow Color="Yellow" ShadowDepth="0" Direction="0" BlurRadius="80" Opacity="0.2">
</local:FastShadow>
</Border>
<Border Margin="1" VerticalAlignment="Stretch" BorderThickness="0" Background="#ffff00" HorizontalAlignment="Left" Width="7">
<local:FastShadow Color="#ff8d0a" ShadowDepth="0" Direction="0" BlurRadius="80" Opacity="0.4">
</local:FastShadow>
</Border>
<Border BorderBrush="#ff8d0a" BorderThickness="1" CornerRadius="3" Background="#ffff00">
<Border Margin="1" BorderBrush="#fff533" BorderThickness="4" CornerRadius="3" ClipToBounds="False" HorizontalAlignment="Stretch">
<Border.Effect>
<BlurEffect Radius="50"/>
</Border.Effect>
</Border>
</Border>
</Grid>
<Grid Height="5" VerticalAlignment="Bottom">
<Grid Background="GreenYellow">
<Rectangle Fill="DarkGreen" HorizontalAlignment="Right" Width="50">
</Rectangle>
</Grid>
<Grid >
<Rectangle Fill="DarkBlue" HorizontalAlignment="Right" Width="20">
</Rectangle>
</Grid>
</Grid>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="0" Width="2" CellStyle="{StaticResource DataGridColumnSeparatorStyle}"/>
<DataGridTemplateColumn Header="" Width="24">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Ellipse Width="8" Height="8" Fill="Blue" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="" Width="60"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Code:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 test
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void SetField<T> ( ref T field, T value, string propertyName )
{
if ( !EqualityComparer<T>.Default.Equals ( field, value ) )
{
field = value;
PropertyChanged?.Invoke ( this, new PropertyChangedEventArgs ( propertyName ) );
}
}
decimal viewScale = 1;
public decimal ViewScale
{
get => this.viewScale;
set => SetField ( ref this.viewScale, value,
"ViewScale"
);
}
ObservableCollection<Coin> _coins;
public ObservableCollection<Coin> Coins { get => _coins; set => SetField ( ref _coins, value, nameof ( _coins ) ); }
public ICollectionView CollectionView;
public MainWindow ( )
{
this.Coins = new ObservableCollection<Coin> ( );
for ( int i = 0; i < 25; ++i )
this.Coins.Add ( new Coin ( "Coin 1", i ) );
this.Coins [ 1 ].IsVisible = Visibility.Visible;
this.Coins [ 4 ].IsVisible = Visibility.Visible;
this.Coins [ 7 ].IsVisible = Visibility.Visible;
this.DataContext = this;
InitializeComponent ( );
this.PreviewKeyDown += MainWindow_PreviewKeyDown;
this.MouseLeftButtonDown += MainWindow_MouseLeftButtonDown;
}
private void MainWindow_MouseLeftButtonDown ( object sender, MouseButtonEventArgs e )
{
if ( ( Keyboard.Modifiers & ModifierKeys.Alt ) == ModifierKeys.Alt )
DragMove ( );
}
void MainWindow_PreviewKeyDown ( object sender, KeyEventArgs e )
{
if ( e.Key == Key.Home )
{
this.dataGrid.ScrollIntoView ( this.dataGrid.Items [ 0 ] );
}
if ( e.Key == Key.End )
{
this.dataGrid.ScrollIntoView ( this.dataGrid.Items [ this.dataGrid.Items.Count - 1 ] );
this.dataGrid.UpdateLayout ( );
this.dataGrid.ScrollIntoView ( this.dataGrid.Items [ 0 ] );
}
else if ( e.Key == Key.F12 )
{
this.ViewScale += 0.1m;
}
}
}
public class Coin
{
public string Symbol { get; set; }
public int PNL { get; set; }
public SolidColorBrush Color2 { get; set; }
public Visibility IsVisible { get; set; }
public Coin ( string symbol, int pnl )
{
this.Symbol = symbol;
this.PNL = pnl;
Random rnd = new Random ( );
Color c = Color.FromRgb ( ( byte ) rnd.Next ( 256 ), ( byte ) rnd.Next ( 256 ), ( byte ) rnd.Next ( 256 ) );
this.Color2 = new SolidColorBrush ( c );
this.IsVisible = Visibility.Hidden;
}
}
}
class FastShadow
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace test
{
using System.Windows.Shapes;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System;
using System.ComponentModel;
/// <summary>
/// Emulates the System.Windows.Media.Effects.DropShadowEffect using
/// rectangles and gradients, which performs a million times better
/// and won't randomly crash a good percentage of your end-user's
/// video drivers.
/// </summary>
public class FastShadow : Decorator
{
#region Dynamic Properties
public static readonly DependencyProperty ColorProperty =
DependencyProperty.Register (
"Color",
typeof ( Color ),
typeof ( FastShadow ),
new FrameworkPropertyMetadata (
Color.FromArgb ( 0x71, 0x00, 0x00, 0x00 ),
FrameworkPropertyMetadataOptions.AffectsRender ) );
/// <summary>
/// The Color property defines the Color used to fill the shadow region.
/// </summary>
[Category ( "Common Properties" )]
public Color Color
{
get { return ( Color ) GetValue ( ColorProperty ); }
set { SetValue ( ColorProperty, value ); }
}
/// <summary>
/// Distance from centre, why MS don't call this "distance" beats
/// me.. Kept same as other Effects for consistency.
/// </summary>
[Category ( "Common Properties" ), Description ( "Distance from centre" )]
public double ShadowDepth
{
get { return ( double ) GetValue ( ShadowDepthProperty ); }
set { SetValue ( ShadowDepthProperty, value ); }
}
// Using a DependencyProperty as the backing store for ShadowDepth. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ShadowDepthProperty =
DependencyProperty.Register ( "ShadowDepth", typeof ( double ), typeof ( FastShadow ),
new FrameworkPropertyMetadata (
5.0, FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback ( ( o, e ) =>
{
FastShadow f = o as FastShadow;
if ( ( double ) e.NewValue < 0 )
f.ShadowDepth = 0;
} ) ) );
/// <summary>
/// Size of the shadow
/// </summary>
[Category ( "Common Properties" ), Description ( "Size of the drop shadow" )]
public double BlurRadius
{
get { return ( double ) GetValue ( BlurRadiusProperty ); }
set { SetValue ( BlurRadiusProperty, value ); }
}
// Using a DependencyProperty as the backing store for BlurRadius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BlurRadiusProperty =
DependencyProperty.Register ( "BlurRadius", typeof ( double ), typeof ( FastShadow ),
new FrameworkPropertyMetadata ( 10.0,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback ( ( o, e ) =>
{
FastShadow f = o as FastShadow;
if ( ( double ) e.NewValue < 0 )
f.BlurRadius = 0;
} ) ) );
/// <summary>
/// Angle of the shadow
/// </summary>
[Category ( "Common Properties" ), Description ( "Angle of the shadow" )]
public int Direction
{
get { return ( int ) GetValue ( DirectionProperty ); }
set { SetValue ( DirectionProperty, value ); }
}
// Using a DependencyProperty as the backing store for Direction. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DirectionProperty =
DependencyProperty.Register ( "Direction", typeof ( int ), typeof ( FastShadow ),
new FrameworkPropertyMetadata ( 315, FrameworkPropertyMetadataOptions.AffectsRender ) );
#endregion Dynamic Properties
#region Protected Methods
protected override void OnRender ( DrawingContext drawingContext )
{
double distance = Math.Max ( 0, ShadowDepth );
double blurRadius = Math.Max ( BlurRadius, 0 );
double angle = Direction + 45; // Make it behave the same as DropShadowEffect
Rect shadowBounds = new Rect ( new Point ( 0, 0 ),
new Size ( RenderSize.Width, RenderSize.Height ) );
shadowBounds.Inflate ( blurRadius, blurRadius );
Color color = Color;
// Transform angle for "Direction"
double angleRad = angle * Math.PI / 180.0;
double xDispl = distance;
double yDispl = distance;
double newX = xDispl * Math.Cos ( angleRad ) - yDispl * Math.Sin ( angleRad );
double newY = yDispl * Math.Cos ( angleRad ) + xDispl * Math.Sin ( angleRad );
TranslateTransform translate = new TranslateTransform ( newX, newY );
Rect transformed = translate.TransformBounds ( shadowBounds );
// Hint: you can make the blur radius consume more "centre"
// region of the bounding box by doubling this here
// blurRadius = blurRadius * 2;
// Build a set of rectangles for the shadow box
Rect [ ] edges = new Rect [ ] {
new Rect(new Point(transformed.X,transformed.Y), new Size(blurRadius,blurRadius)), // TL
new Rect(new Point(transformed.X+blurRadius,transformed.Y), new Size(Math.Max(transformed.Width-(blurRadius*2),0),blurRadius)), // T
new Rect(new Point(transformed.Right-blurRadius,transformed.Y), new Size(blurRadius,blurRadius)), // TR
new Rect(new Point(transformed.Right-blurRadius,transformed.Y+blurRadius), new Size(blurRadius,Math.Max(transformed.Height-(blurRadius*2),0))), // R
new Rect(new Point(transformed.Right-blurRadius,transformed.Bottom-blurRadius), new Size(blurRadius,blurRadius)), // BR
new Rect(new Point(transformed.X+blurRadius,transformed.Bottom-blurRadius), new Size(Math.Max(transformed.Width-(blurRadius*2),0),blurRadius)), // B
new Rect(new Point(transformed.X,transformed.Bottom-blurRadius), new Size(blurRadius,blurRadius)), // BL
new Rect(new Point(transformed.X,transformed.Y+blurRadius), new Size(blurRadius,Math.Max(transformed.Height-(blurRadius*2),0))), // L
new Rect(new Point(transformed.X+blurRadius,transformed.Y+blurRadius), new Size(Math.Max(transformed.Width-(blurRadius*2),0),Math.Max(transformed.Height-(blurRadius*2),0))), // C
};
// Gradient stops look a lot prettier than
// a perfectly linear gradient..
GradientStopCollection gsc = new GradientStopCollection ( );
Color stopColor = color;
stopColor.A = ( byte ) ( color.A );
gsc.Add ( new GradientStop ( color, 0.0 ) );
stopColor.A = ( byte ) ( .74336 * color.A );
gsc.Add ( new GradientStop ( stopColor, 0.1 ) );
stopColor.A = ( byte ) ( .38053 * color.A );
gsc.Add ( new GradientStop ( stopColor, 0.3 ) );
stopColor.A = ( byte ) ( .12389 * color.A );
gsc.Add ( new GradientStop ( stopColor, 0.5 ) );
stopColor.A = ( byte ) ( .02654 * color.A );
gsc.Add ( new GradientStop ( stopColor, 0.7 ) );
stopColor.A = ( byte ) ( 0 );
gsc.Add ( new GradientStop ( stopColor, 0.9 ) );
gsc.Freeze ( );
Brush [ ] colors = new Brush [ ]{
// TL
new RadialGradientBrush(gsc){ Center = new Point(1, 1), GradientOrigin = new Point(1, 1), RadiusX=1, RadiusY=1},
// T
new LinearGradientBrush(gsc, 0){ StartPoint = new Point(0,1), EndPoint=new Point(0,0)},
// TR
new RadialGradientBrush(gsc){ Center = new Point(0, 1), GradientOrigin = new Point(0, 1), RadiusX=1, RadiusY=1},
// R
new LinearGradientBrush(gsc, 0){ StartPoint = new Point(0,0), EndPoint=new Point(1,0)},
// BR
new RadialGradientBrush(gsc){ Center = new Point(0, 0), GradientOrigin = new Point(0, 0), RadiusX=1, RadiusY=1},
// B
new LinearGradientBrush(gsc, 0){ StartPoint = new Point(0,0), EndPoint=new Point(0,1)},
// BL
new RadialGradientBrush(gsc){ Center = new Point(1, 0), GradientOrigin = new Point(1, 0), RadiusX=1, RadiusY=1},
// L
new LinearGradientBrush(gsc, 0){ StartPoint = new Point(1,0), EndPoint=new Point(0,0)},
// C
new SolidColorBrush(color),
};
// This is a test pattern, uncomment to see how I'm drawing this
//Brush[] colors = new Brush[]{
// Brushes.Red,
// Brushes.Green,
// Brushes.Blue,
// Brushes.Fuchsia,
// Brushes.Gainsboro,
// Brushes.LimeGreen,
// Brushes.Navy,
// Brushes.Orange,
// Brushes.White,
//};
double [ ] guidelineSetX = new double [ ] { transformed.X,
transformed.X+blurRadius,
transformed.Right-blurRadius,
transformed.Right};
double [ ] guidelineSetY = new double [ ] { transformed.Y,
transformed.Y+blurRadius,
transformed.Bottom-blurRadius,
transformed.Bottom};
drawingContext.PushGuidelineSet ( new GuidelineSet ( guidelineSetX, guidelineSetY ) );
for ( int i = 0; i < edges.Length; i++ )
{
drawingContext.DrawRoundedRectangle ( colors [ i ], null, edges [ i ], 0.0, 0.0 );
}
drawingContext.Pop ( );
}
#endregion
}
}
Upvotes: 3
Views: 552
Reputation: 10193
I'm not sure if you've noticed (or if it's intentional), but the "glow" only affects higher up rows, and not rows further down. It's too subtle to see in your screenshot, but it becomes clear if you increase the intensity of the glow effect. For example the "coin" on row 7 results in a glow that overlaps the two or three rows above, but the glow doesn't appear at all in row 8 or below. This seems to have something to do with each successive row having a higher z-index than the last, so row 8 gets renders over the already-rendered glow effect, so is another reason why your current solution may be difficult to resolve!
I suspect the datagrid's odd visual tree is going to prevent you from achieving what you want with your current solution: you can't have a glow effect that renders across multiple grid rows and appears behind the rows' content (the green & blue rectangles), so you might need to approach this from a different angle. While I don't have a concrete solution to this, I would suggest investigating a way to draw the glow effects on a layer behind the datagrid (e.g. using a canvas).
This means the data grid rows would need to have a transparent b/g colour of course, so you might need to rethink how you achieve the alternating row colours.
When you add a glow effect to the canvas (which could be as simple as a Border control with a linear gradient b/g), you'll need to know where to place it (i.e. Canvas.Top) to ensure it appears behind the correct datagrid row. If all rows have the same fixed height then this should be a trivial calculation.
The canvas would have to scroll "in sync" with the datagrid, as the user scrolls the latter, ensuring that the glow effects (Border controls) on the canvas remain in place behind their respective rows. I think you'd need to place the canvas inside a ScrollViewer, then manipulate its scroll position from within scroll event handlers on the datagrid. You may find other SO articles detailing how to detect scrolling (and by how much).
It goes without saying that you'd have to do all this in code-behind, not with XAML alone!
Upvotes: 1
Reputation: 7908
Decorating is based on the Decorator class. And it sits in the AdornerLayer above all content elements.
Change the base class for FastShadow: use Effect (or a derivative from it) instead of Decorator.
Assign an instance of the FastShadow class to the Effect property.
Upvotes: 1