WPF自定义Bevel效果实现

在WPF的早期,控件效果丰富,颜色渐变,其中Bitmap-EFFECTS尤为突出。然而,随着时间的推移,这些效果逐渐被标记为技术上的过时,并被更通用的Effect类所取代。尽管如此,在某些情况下,仍然可能会需要这样的特性。

由于Bevel-Effect既方便又简单,无论是在设计上还是在性能上,决定构建一个简单且易于实现的Bevel效果。在实现过程中,采用了以下WPF中级技术:

  • 行为(Behaviors)
  • 装饰器(Adorners)
  • 依赖属性(Dependency-Properties)

由于Bevel-Effect简单、清晰的特性,并且它除了简单的装饰特性之外还具有一定的设计价值,所以时不时地会使用它。这也是选择这种效果最简单的视觉形式的原因。

在本文中,省略了所有与主题不直接相关的代码,因为相信它会掩盖想要表达的核心思想和简单性。

使用代码

当开始寻找实现这种效果的最佳方案时,目标是找到一个既简单又优雅、健壮且通用的实现方式。最终,找到了一个满足目标的解决方案,那就是这里展示的方案。

在基础层面上,它是一个“协同”的行为和装饰器的混合体:

  • 为什么选择装饰器?因为它本质上是一个覆盖在视觉元素上的视觉效果,用来增强元素的初始特性。这正是想要在现有视觉元素上添加Bevel-Effect时所追求的,以增强其“视觉外观”到3D风格,但同时又保持清晰。
  • 为什么选择行为?经过多次实验,使用行为被证明是将装饰器注入XAML*的最优雅方式。

也考虑过基于XAML的装饰器是使用装饰器的首选方式(与基于代码的替代方案相比)。

以下是实现方式:

《Button》 《i:Interaction.Behaviors》 《local:BevelBehavior BevelThickness="30" /》 《/i:Interaction.Behaviors》 《/Button》

行为的工作方式如下:

一旦“待Beveled/Adorned”的元素被加载,它就有一个“Adorner-Layer”。然后,BevelBehavior实例化一个BevEffAdor装饰器,并用来自XAML*的数据填充其属性。

BevelBehavior充当效果用户(在XAML中使用Bevel相关术语)和处理不同“实现相关”属性集的装饰器之间的中介:

行为中的属性:

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"));

*行为中的属性是依赖属性,这暗示了在未来/进一步开发中对更改属性值做出反应的可能性。

由BevelBehavior填充的BevEffAdor中的属性:

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;

正如所说,尝试了很多不同的方法来解决这个问题,无论是在技术上还是在视觉上(它的外观)。不能一一解释为什么其他方法被排除了,但最终选择了“OnRender-override, Adorner-Painting”的方法。

在'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}");

实际的绘画是在OnRender覆盖中完成的(作为Geometry-Path字符串):

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); 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(); };
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485