* Remark -
Sample code was written using VS2015, C#6, NET 4.5.2
Introduction
Back on the early days of WPF when controls were flashy and colors where gradient there where the Bitmap-EFFECTS!!! Nowadays they are almost not in use, and mostly, for good reasons. They are marked as technically obsolete and replaced by the more versatile Effect
class. However, sometimes one might come across the need for such a feature.
Since the Bevel-Effect is quit handy and is simple in terms of design and performance, I decided to build one that will be simple in its inner workings and its ('outer') implementation.
Background
After (not so) few attempts and refinements I came up with a solution I was pleased with.
It involved the use of the following intermediate-level WPF technologies:
- Behaviors
- Adorners
- Dependency-Properties
If you're not familiar with any of those, I suggest doing a quick overview of them before proceeding.
Also, as I've mentioned earlier, I keep using the Bevel-Effect from time to time (once every couple of years or so) due to its simple and clean 'nature' and that it has some added design value other than simple, ornamente features. That is also why I've picked the simplest visual-form of this effect.
As in most of my articles, I've omitted any 'not-directly-relevant' code from the attached sample, as I believe it masks the idea itself and the simplicity I was aiming for.
Using the Code
When I started experimenting on a solution for this effect I was looking for the best implementation in terms of simplicity, elegance, robustness and versatility. The solution that finally met my goals is the one presented here.
At its base, it is a 'Synergetic' mix of Behavior and Adorner:
1. Why Adorner? In its basic nature it's a OVERLAYED visual attribute/s place ON TOP of a visual element in order to ENHANCE the element's initial features.
That's exactly what I was looking for when I wanted a Bevel-Effect on an existing visual-element; to enhance it's 'visual look' into a 3D'ish yet clean representation.
2. Why Behavior? After many experiments, using Behaviors proved to yield the most elegant way to 'INJECT' Adorner into XAML*
*I also considered XAML-based Adorner to be the prefered way of using Adorners (compared to the code-based alternative)
So this is how it is implemented:
<Button>
...
<i:Interaction.Behaviors>
<local:BevelBehavior BevelThickness="30"/>
</i:Interaction.Behaviors>
Here is how the Behavior
works:
Once the 'to be Beveled/Adorned' element is loaded, it has an - 'Adorner-Layer'. Then, the BevelBehavior
instantiates a BevEffAdor
Adorner and fills its properties with data came from the XAML*
The BevelBehavior
acts as a mediator between the effect's user (in the XAML that uses Bevel-related terms) and the Adorner itself that handles different set of 'implementation-related' properties:
Properties in the Behavior:
public double BevelThickness { get; set; } = 30.0;
public Brush Lighted { get; set; } = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#30000000"));
public Brush Shadowed { get; set; } = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#60000000"));
public Brush Darkened { get; set; } = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#90000000"));
public Brush FaceShadowedTransp { get; set; } = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#50000000"));
*Properties inside the BevelBehavior
are DependencyProperties
comes to hint the option of reacting to changed props' values in a future/further development.
Properties in the BevEffAdor
filled by the BevelBehavior
:
bevador.NotIsPressedN = Lighted.Clone();
bevador.NotIsPressedW = Shadowed.Clone();
bevador.NotIsPressedSE = Darkened.Clone();
bevador.IsPressedNW = Darkened.Clone();
bevador.IsPressedNW.Opacity = 0;
bevador.IsPressedE = Shadowed.Clone();
bevador.IsPressedE.Opacity = 0;
bevador.IsPressedS = Lighted.Clone();
bevador.IsPressedS.Opacity = 0;
bevador.IsPressedFace = FaceShadowedTransp.Clone();
bevador.IsPressedFace.Opacity = 0;
As I've said, I've experimented quit a lot with different approaches getting this issue right both technically and visually (how it looks). I cannot get into all the reasons why each of the other approaches were ruled out, but I finally went with the 'OnRender-override, Adorner-Painting' approach.
Where Size-related variables calc's are made in the 'MeasureOverride
':
var pNWi = $"{BevelThickness},{BevelThickness}";
var pNEo = $"{AdornedElement.ActualWidth},0";
var pNEi = $"{AdornedElement.ActualWidth - BevelThickness},{BevelThickness}";
var pSEo = $"{AdornedElement.ActualWidth},{AdornedElement.ActualHeight}";
var pSEi = $"{AdornedElement.ActualWidth - BevelThickness},{AdornedElement.ActualHeight - BevelThickness}";
var pSWo = $"0,{AdornedElement.ActualHeight}";
var pSWi = $"{BevelThickness},{AdornedElement.ActualHeight - BevelThickness}";
geoInnerRect = Geometry.Parse($"M {pNWi} {pNEi} {pSEi} {pSWi}");
geoN = Geometry.Parse($"M {pNWo} {pNWi} {pNEi} {pNEo}");
geoSE = Geometry.Parse($"M {pNEo} {pNEi} {pSEi} {pSWi} {pSWo} {pSEo}");
geoW = Geometry.Parse($"M {pNWo} {pNWi} {pSWi} {pSWo}");
geoNW = Geometry.Parse($"M {pNWo} {pNEo} {pNEi} {pNWi} {pSWi} {pSWo}");
geoS = Geometry.Parse($"M {pSWo} {pSWi} {pSEi} {pSEo}");
geoE = Geometry.Parse($"M {pNEo} {pNEi} {pSEi} {pSEo}");
And the actual painting is made in the OnRender
override (as Geometry-Path strings):
drawingContext.DrawGeometry(NotIsPressedN, null, geoN);
drawingContext.DrawGeometry(NotIsPressedSE, null, geoSE);
drawingContext.DrawGeometry(NotIsPressedW, null, geoW);
drawingContext.DrawGeometry(IsPressedNW, null, geoNW);
drawingContext.DrawGeometry(IsPressedS, null, geoS);
drawingContext.DrawGeometry(IsPressedE, null, geoE);
drawingContext.DrawGeometry(IsPressedFace, null, geoInnerRect);
The Adorner itself marked as HitTestVisible=False
, so it wouldn't 'interfere' with the Adorned-Element 'Normal' behavior. The Adorner registers to MouseLeftButtonDown/Up
events so it will paint itself differently (stands out/immersed) through animated transition.
var durAnim = new Duration(TimeSpan.FromSeconds(0.2));
AdornedElement.PreviewMouseLeftButtonDown += (s, e) =>
{
var daHide = new DoubleAnimation(0, durAnim);
NotIsPressedN.BeginAnimation(Brush.OpacityProperty, daHide);
NotIsPressedW.BeginAnimation(Brush.OpacityProperty, daHide);
NotIsPressedSE.BeginAnimation(Brush.OpacityProperty, daHide);
var daShow = new DoubleAnimation(1, durAnim);
IsPressedNW.BeginAnimation(Brush.OpacityProperty, daShow);
IsPressedE.BeginAnimation(Brush.OpacityProperty, daShow);
IsPressedS.BeginAnimation(Brush.OpacityProperty, daShow);
IsPressedFace.BeginAnimation(Brush.OpacityProperty, daShow);
InvalidateVisual();
};
AdornedElement.PreviewMouseLeftButtonUp += (s, e) =>
{
var daShow = new DoubleAnimation(1, durAnim);
NotIsPressedN.BeginAnimation(Brush.OpacityProperty, daShow);
NotIsPressedW.BeginAnimation(Brush.OpacityProperty, daShow);
NotIsPressedSE.BeginAnimation(Brush.OpacityProperty, daShow);
var daHide = new DoubleAnimation(0, durAnim);
IsPressedNW.BeginAnimation(Brush.OpacityProperty, daHide);
IsPressedE.BeginAnimation(Brush.OpacityProperty, daHide);
IsPressedS.BeginAnimation(Brush.OpacityProperty, daHide);
IsPressedFace.BeginAnimation(Brush.OpacityProperty, daHide);
InvalidateVisual();
};
Points of Interest
The technique of using the Behavior as a Mediator/Injector of Adorner might be useful in other effects.