Click here to Skip to main content
15,885,985 members
Articles / Desktop Programming / WPF

Simple Adorner-Based Bevel Effect

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
4 Sep 2015CPOL3 min read 9.9K   198   2  
A simple and elegant way to apply Bevel-Effect in WPF

  

* 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:

  1. Behaviors
  2. Adorners
  3. 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:

C++
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:

C#
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':

C#
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):

C#
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.

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) self employed
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --