通常,当基本性能成为问题或需要访问单个像素时,将使用这些低级功能。
可视化对象(Visual
) :如果希望构建用于绘制矢量图形的程序,或计划创建包含数千个形状并可以分别操作这些形状的画布,那么使用WPF的元素系统和形状类会使速度过慢,不能满足要求。相反,需要更简洁的方法,使用低级的Visual
类手动进行执行渲染
效果(Effect
) :如果希望为元素应用复杂的可视化效果(如模糊和颜色调整),最简便的方法是使用像素着色器(pixel shader)这个专用工具修改单个像素。为提高性能,像素着色器是硬件加速的,并且有许多已经制作好的效果,您付出很少的努力就可以将这些效果应用到自己的应用程序中
WriteableBitmap :虽然需要做很多工作,但通过 WriteableBitmap 类可以完全拥有幅位图–这意味着可以设置并检査位图的任何像素。对于复杂的数据可视化情形(例如当图形化科学计算数据时)可以使用该特性,也可以使用该特性从头开始实现一个赏心悦目的效果。
可视化对象
通过使用几何图形、图画和路径,可以降低2D图形的开销,即使正在使用复杂的具有分层效果的组合形状和渐变画刷,这种方法也仍然能够工作得很好。然而,这种设置不适合需要渲染大量图形元素的绘图密集型应用程序。
WPF针对此类问题的解决方案时,**使用低级的可视化层(visual layer
)模型。基本思想时将每个元素定义为一个Visual
对象,Visual
对象是轻量级的要素,比Geometry
对象或Path
对象需要的开销更小。然后可使用单个元素在窗口中渲染所有可视化对象
绘制可视化对象
Visual
类是抽象类,所有不能创建该类的实例。相反,需要使用继承自Visual
类的某个类,包括UIElement
类(该类是WPF元素模型的根)、Viewport3DVisual
类(通过该类可显示3D内容)以及ContainerVisual
(包含其他可视化对象的基本容器)。当最有用的派生类时DrawingVisual
类,该类继承自ContainerVisual
类,并添加了支持“绘制”希望放置到可视化对象中的图形内容的功能。
为使用DrawingVIsual
类绘制内容,需要调用DrawingVisual.RenderOpen()
方法。该方法返回一个可用于定义可视化内容的DrawingContext
对象。绘制完毕后,需要调用DrawingContext.Close()
方法。下面时绘制图形的完整过程:
1 2 3 4 DrawingVisual visual = new DrawingVisual(); DrawingContext dc = visual.RenderOpen(); dc.Close();
本质上,DrawingContext类由各种为可视化对象增加了一些图形细节的方法构成 。可调用这些方法来绘制各种图形、应用变换以及改变不透明度等。
DrawingContext类的方法
|名称|说明|
|DrawLine()
DrawRectangle()
DrawRoundedRectangle()
DrawEllipse()
|在指定的位置,使用指定的填充和轮廓绘制特定的形状。|
|DrawGeometry()
DrawDrawing()
|绘制更复杂的Geometry
对象和Drawing
对象|
|DrawText()
|在指定的位置绘制文本。通过为该方法传递FormattedText
对象,可指定文本、字体、填充以及其他细节。如果设置了FormattedText.MaxTextWidth
属性,可使用该方法绘制换行的文本|
|DrawImage()
|在指定的区域(由Rect对象定义)绘制一幅位图图像|
|DrawVideo()
|在特定区域绘制视频内容(封装在MediaPlayer对象中)|
|Pop()
|封装最后调用的PushXxx()方法,可使用PushXxx()方法暂时应用一个或多个效果并且Pop()方法会翻转它们|
|PushClip()
|将绘图限制在特定剪裁区域中。这个区域外的内容不被绘制|
|PushEffect()
|为随后的绘图操作应用BitmapEffect
对象|
|PushOpacity()
PushOpacityMask()
|为了使后续的绘图操作部分透明,应用新的不透明设置或不透明掩码|
|PushTransform()
|设置将应用于后续绘制操作的Transform
对象。可使用变换来缩放、移动、旋转或扭曲内容|
下面的示例创建了一个可视化对象,该可视化对象包含没有填充的基本的黑色三角形:
1 2 3 4 5 6 7 8 DrawingVisual visual = new DrawingVisual(); using (DrawingContext dc = visual.RenderOpen()){ Pen drawingPen = new Pen(Brushes.Black, 3 ); dc.DrawLine(drawingPen, new Point(0 , 50 ), new Point(50 , 0 )); dc.DrawLine(drawingPen, new Point(50 , 0 ), new Point(100 , 50 )); dc.DrawLine(drawingPen, new Point(0 , 50 ), new Point(100 , 50 )); }
当调用 DrawingContext方法时,没有实际绘制可视化对象–而只是定义了可视化外观。当通过调用Cose()方法结束绘制时,完成的图画被存储在可视化对象中,并通过只读的DrawingVisual.Drawing 属性提供这些图画。WPF 会保存 Drawing对象,从而当需要时可以重新绘制窗口。
绘图代码的顺序很重要。后面的绘图操作可在已经存在的图形上绘制内容。Pusbxcx0)方法应用的设置会被应用到后续的绘图操作中。例如,可使用PushOpacity()方法改变不透明级别,该设置会影响所有的后续绘图操作。可使用Pop()方法恢复最近的Pushxxx()方法。如果多次调用 Pushxxx()方法,可一次使用一系列 Pop( )方法调用关闭它们。
一旦关闭 DrawingContext对象,就不能再修改可视化对象。但可以使用 DrawingVisual 类的 Transfomm 和 Opacity 属性应用变换或改变整个可视化对象的透明度。如果希望提供全新的内容,可以再次调用 RenderOpen()方法并重复绘制过程。
许多绘图方法都使用 Pen 和 Brush 对象。如果计划使用相同的笔画和填充绘制许多可视化对象,或者如果希望多次渲染同一个可视化对象(为了改变其内容),就值得事先创建所需的 Pen和 Brush对象,并在窗口的整个生命周期中保存它们。
在元素中封装可视化对象
在可视化层中编写程序时,最重要的一步是定义可视化对象,但为了在屏幕上实际显示可视内容,这还不够。为显示可视化对象,还需要借助于功能完备的 WPF 元素,WPF 元素将可视化对象添加到可视化树中。乍一看,这好像降低了可视化层编程的优点–毕竟,避免使用元素并避免它们的巨大开销不正是使用可视化层的全部目的吗?然而,单个元素具有显示任意数量可视化对象的能力。因此,可以很容易地创建只包含一两个元素,但却驻留了几千个可视化对象的窗口。
为在元素中驻留可视化对象,需要执行以下任务:
为元素调用AddVisualChild()
和AddLogicalChild()
方法来注册可视化对象。从技术角度看,为了显示可视化对象,不需要执行这些任务,但为了确保正确跟踪可视化对象。从技术角度看,为了显示可视化对象,不需要执行这些人物,但为了确保正确跟踪可视化对象、在可视化树和逻辑树中显示可视化对象以及使用其他WPF特性(如命中测试),需要执行这些操作。
重写VisualChildrenCount
属性并返回已经增加了的可视化对象的数量
重写GetVisualChild()
方法,当通过索引号请求可视化对象时,添加返回可视化对象所需的代码
当重写 VisualChildrenCount属性和 GetVisualChild()方法时,本质上是劫持了那个元素。如果使用的是能够包含嵌套元素的内容控件、装饰元素或面板,这些元素将不再被渲染。例如,如果在自定义窗口中重写了这两个方法,就看不到窗口的其他内容。只会看到添加的可视化对象。
因此,通常创建专用的自定义类来封装希望显示的可视化对象。例如,下图显示的窗口。该窗口允许用户为自定义的 Canvas 面板添加正方形(每个正方形是可视化对象)。
DrawingCanvas MainWidnow.xaml MainWindow.xaml.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public class DrawingCanvas : Canvas { private List<DrawingVisual> _hits = new List<DrawingVisual>(); private List<Visual> _visuals = new List<Visual>(); protected override int VisualChildrenCount => _visuals.Count; protected override Visual GetVisualChild (int index ) { return _visuals[index]; } public void AddVisual (Visual visual ) { _visuals.Add(visual); base .AddVisualChild(visual); base .AddLogicalChild(visual); } public void RemoveVisual (Visual visual ) { _visuals.Remove(visual); base .RemoveVisualChild(visual); base .RemoveLogicalChild(visual); } public DrawingVisual? GetVisual(Point point) { HitTestResult hitResult = VisualTreeHelper.HitTest(this , point); return hitResult.VisualHit as DrawingVisual; } public List<DrawingVisual> GetVisuals (Geometry region ) { _hits.Clear(); GeometryHitTestParameters parameters = new GeometryHitTestParameters(region); HitTestResultCallback callback = new HitTestResultCallback(HitTestCallback); VisualTreeHelper.HitTest(this , null , callback, parameters); return _hits; } private HitTestResultBehavior HitTestCallback (HitTestResult result ) { GeometryHitTestResult geometryResult = (GeometryHitTestResult)result; DrawingVisual? visual = result.VisualHit as DrawingVisual; if (visual != null && geometryResult.IntersectionDetail == IntersectionDetail.FullyInside) { _hits.Add(visual); } return HitTestResultBehavior.Continue; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <Window x:Class ="WpfApp.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:WpfApp" xmlns:res ="clr-namespace:ResourceLibrary;assembly=ResourceLibrary" xmlns:i ="http://schemas.microsoft.com/xaml/behaviors" xmlns:t ="http://bootree.cn" mc:Ignorable ="d" Title ="MainWindow" Height ="450" Width ="800" > <Grid > <Grid.ColumnDefinitions > <ColumnDefinition Width ="150" /> <ColumnDefinition Width ="*" /> </Grid.ColumnDefinitions > <StackPanel VerticalAlignment ="Center" Margin ="10 0" > <RadioButton x:Name ="cmdSelectMove" > Select/Move</RadioButton > <RadioButton x:Name ="cmdAdd" IsChecked ="True" Margin ="0 10" > Add Square</RadioButton > <RadioButton x:Name ="cmdDelete" > Delete Square</RadioButton > <RadioButton x:Name ="cmdSelectMultiple" Margin ="0 10" > Select Multiple</RadioButton > </StackPanel > <t:DrawingCanvas x:Name ="drawingSurface" Grid.Column ="1" Background ="White" ClipToBounds ="True" MouseLeftButtonDown ="drawingSurface_MouseLeftButtonDown" MouseLeftButtonUp ="drawingSurface_MouseLeftButtonUp" MouseMove ="drawingSurface_MouseMove" /> </Grid > </Window >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 public partial class MainWindow : Window { public MainWindow () { InitializeComponent(); } private Brush drawingBrush = Brushes.AliceBlue; private Brush selectedDrawingBrush = Brushes.LightGoldenrodYellow; private Pen drawingPen = new Pen(Brushes.SteelBlue, 3 ); private Size squareSize = new Size(30 , 30 ); private void DrawSquare (DrawingVisual visual, Point topLeftCorner, bool isSelected ) { using (DrawingContext dc = visual.RenderOpen()) { Brush brush = drawingBrush; if (isSelected) brush = selectedDrawingBrush; dc.DrawRectangle(brush, drawingPen, new Rect(topLeftCorner, squareSize)); } } private bool _isDragging = false ; private DrawingVisual? _selectedVisual; private Vector _clickOffset; private DrawingVisual _selectionSquare; private bool _isMultiSelecting = false ; private Point _selectionSquareTopLeft; private void drawingSurface_MouseLeftButtonDown (object sender, MouseButtonEventArgs e ) { Point pointClicked = e.GetPosition(drawingSurface); if (cmdAdd.IsChecked == true ) { DrawingVisual visual = new DrawingVisual(); DrawSquare(visual, pointClicked, false ); drawingSurface.AddVisual(visual); } else if (cmdDelete.IsChecked == true ) { DrawingVisual? visual = drawingSurface.GetVisual(pointClicked); if (visual != null ) { drawingSurface.RemoveVisual(visual); } } else if (cmdSelectMove.IsChecked == true ) { DrawingVisual? visual = drawingSurface.GetVisual(pointClicked); if (visual != null ) { Point topLeftCorner = new Point( visual.ContentBounds.TopLeft.X + drawingPen.Thickness / 2 , visual.ContentBounds.TopLeft.Y + drawingPen.Thickness / 2 ); DrawSquare(visual, topLeftCorner, true ); _clickOffset = topLeftCorner - pointClicked; _isDragging = true ; if (_selectedVisual != null && _selectedVisual != visual) { ClearSelection(); } _selectedVisual = visual; } } else if (cmdSelectMultiple.IsChecked == true ) { _selectionSquare = new DrawingVisual(); drawingSurface.AddVisual(_selectionSquare); _isMultiSelecting = true ; drawingSurface.CaptureMouse(); } else if (_isMultiSelecting) { Point pointDragged = e.GetPosition(drawingSurface); DrawSelectionSquare(_selectionSquareTopLeft, pointDragged); } } private Brush _selectionSquareBrush = Brushes.Transparent; private Pen _selectionSquarePen = new Pen(Brushes.Black, 2 ); private void DrawSelectionSquare (Point point1, Point point2 ) { _selectionSquarePen.DashStyle = DashStyles.Dash; using (DrawingContext dc = _selectionSquare.RenderOpen()) { dc.DrawRectangle(_selectionSquareBrush, _selectionSquarePen, new Rect(point1, point2)); } } private void ClearSelection () { if (_selectedVisual != null ) { Point topLeftCorner = new Point( _selectedVisual.ContentBounds.TopLeft.X + drawingPen.Thickness / 2 , _selectedVisual.ContentBounds.TopLeft.Y + drawingPen.Thickness / 2 ); DrawSquare(_selectedVisual, topLeftCorner, false ); _selectedVisual = null ; } } private void drawingSurface_MouseLeftButtonUp (object sender, MouseButtonEventArgs e ) { _isDragging = false ; if (_isMultiSelecting) { RectangleGeometry geometry = new RectangleGeometry( new Rect(_selectionSquareTopLeft, e.GetPosition(drawingSurface))); List<DrawingVisual> visualsInRegion = drawingSurface.GetVisuals(geometry); MessageBox.Show(String.Format("You selected {0} squer(s)." , visualsInRegion.Count)); _isMultiSelecting = false ; drawingSurface.RemoveVisual(_selectionSquare); drawingSurface.ReleaseMouseCapture(); } } private void drawingSurface_MouseMove (object sender, MouseEventArgs e ) { if (_isDragging && _selectedVisual != null ) { Point pointDragged = e.GetPosition(drawingSurface) + _clickOffset; DrawSquare(_selectedVisual, pointDragged, true ); } } }
命中测试
绘制正方形的应用程序不仅允许用户绘制正方形,还允许用户移动和删除已经绘制的正方形。为了执行这些任务,需要编写代码以截获鼠标单击,并查找位于可单击位置的可视化对象。该任务被称为命中测试(hit testing)
为支持命中测试,最好为 DrawingCanvas类添加GetVisual()方法。该方法使用一个点作为参数并返回匹配的 DrawingVisual对象。为此使用了 VisualTreeHelper.HitTest( )静态方法。
1 2 3 4 5 6 public DrawingVisual? GetVisual(Point point){ HitTestResult hitResult = VisualTreeHelper.HitTest(this , point); return hitResult.VisualHit as DrawingVisual; }
复杂的命中测试
在上面的示例中,命中测试代码始终返回最上面的可视化对象(如果单击空自处,就返回null 引用)。然而,VisualTreeHelper 类提供了 HitTest()方法的两个重载版本,从而可以执行更加复杂的命中测试。使用这些方法,可以检索位于特定点的所有可视化对象,即使它们被其他元素隐藏在后面也同样如此。还可找到位于给定几何图形中的所有可视化对象。
为了使用这个更高级的命中测试行为,需要创建回调函数。之后 VisualTreeHelper 类自上而下遍历所有可视化对象(与创建它们的顺序相反)。每当发现匹配的对象时,就会调用回调函数并传递相关细节。然后可以选择停止查找(如果已经查找到足够的层次),或继续查找直到遍历完所有的可视化对象为止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private List<DrawingVisual> _hits = new List<DrawingVisual>();public List<DrawingVisual> GetVisuals (Geometry region ){ _hits.Clear(); GeometryHitTestParameters parameters = new GeometryHitTestParameters(region); HitTestResultCallback callback = new HitTestResultCallback(HitTestCallback); VisualTreeHelper.HitTest(this , null , callback, parameters); return _hits; } private HitTestResultBehavior HitTestCallback (HitTestResult result ){ GeometryHitTestResult geometryResult = (GeometryHitTestResult)result; DrawingVisual? visual = result.VisualHit as DrawingVisual; if (visual != null && geometryResult.IntersectionDetail == IntersectionDetail.FullyInside) { _hits.Add(visual); } return HitTestResultBehavior.Continue; }
回调方法实现了命中测试行为。通常,HitTestResult对象只提供一个属性(VisualHit),但可以根据执行命中测试的类型,将它转换成两个派生类型中的任意一个。
如果使用一个点进行命中测试,可将 HitTestResult 对象转换为 PointHitTestResult 对象,该类提供了一个不起眼的 PointHit 属性,该属性返回用于执行命中测试的原始点。但如果使用 Geometry对象进行命中测试,如本例那样,可将HitTestResult对象转换为 GeometryHitTestResult 对象,并访问 IntersectionDetail 属性。IntersectionDetail 属性告知您几何图形是否完全封装了可视化对象(Fullyinside),几何图形是否与可视化元素只是相互重叠(lmtersets),或者用于命中测试的几何图形是杏落在可视化元素的内部(FulyContains)。在该例中,只有当可视化对象完全位于命中测试区域时,才会对命中数量计数。最后,在回调函数的末尾,可返回两个HitTestResultBehavior 枚举值中的一个:返回 Continue 表示继续查找命中,返回Stop 则表示结束查找过程。
效果
WPF 提供了可应用于任何元素的可视化效果。效果的目标是提供一种简便的声明式方法从而改进文本、图像、按钮以及其他控件的外观。不是编写自己的绘图代码,而是使用某个继承自 Effect 的类(位于 System.Windows.Media.Effects名称空间中)以立即获得诸如模糊、辉光以及阴影等效果。
效果类
名称
说明
属性
BlurEffect
模糊元素中的内容
Radius
、KernelType
、RenderingBias
DropShadowEffect
在元素背后添加矩形阴影
BlurRadius
、Color
、Direction
、Opacity
、ShadowDepth
、RenderingBias
ShaderEffect
应用像索着色器,像索着色器是使用高级着色语言(High Level Shading Language,HLSL)事先制作好的并且已经编译过的效果
PixelShader
上表列出的Effect
类的派生类和位图效果类相混淆,位图效果类派生自BitmapEffect类,该类和Effect类位于相同的名称空间中。尽管位图效果具有类似的编程模型,但它们存在几个严重的局限性:
位图效果不支持像素着色器,像素着色器是创建可重用效果的最强大、最灵活的方式。
位图效果是用非托管的代码实现的,从而需要完全信任的应用程序。所以,在基于浏览器的 XBAP 应用程序中不能使用位图效果。
位图效果总使用软件进行渲染,不使用显卡资源。这使得它们的速度较慢,当处理大量元素或具有较大可视化表面的元素时尤其如此。
BitmapEffect类是在 WPF 的第一个版本中引入的,该版本没有提供 Efect类。为了向后兼容,仍保留了位图效果。
BlurEffect类
最简单的WPF效果时BlurEffect
类。该类模糊元素的内容,就像通过失焦透镜观察到的效果。通过增加Radius
属性的值(默认值是5)可添加模糊程度。
1 2 3 4 5 6 7 <Button Content ="Blurred (Radius=2)" Padding ="5" Margin ="3" > <Button.Effect > <BlurEffect Radius ="2" /> </Button.Effect > </Button >
DropShaowEffect类
DropShadowEffect
类在元素背后添加了轻微的偏移阴影。可使用该类的几个属性:
DropShadowEffect
名称
说明
Color
设置阴影的颜色(默认黑色)
ShadowDepth
确定阴影离开内容多远,单位为像素(默认为5)。将该属性设置为0会创建外侧辉光(outer-glow)效果,该效果会在内容周围添加晕菜(halo of color)
BlurRadius
模糊阴影,该属性和BlurEffect类的Radius
属性非常类似
Opacity
使用0(完全透明)~1(完全不透明,默认值)之间的小数,使阴影部分透明
Direction
使用从0到360之间的角度值指定阴影相对于内容的位置。将该属性设置为0会将阴影放置到右边,增加该属性的值时会逆时针移动阴影。默认值是315,该值会将阴影放置到元素的右下方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <StackPanel > <TextBlock FontSize ="20" Margin ="5" Text ="Basic dropshadow" > <TextBlock.Effect > <DropShadowEffect /> </TextBlock.Effect > </TextBlock > <TextBlock FontSize ="20" Margin ="5" Text ="BLight blue dropshadow" > <TextBlock.Effect > <DropShadowEffect Color ="SlateBlue" /> </TextBlock.Effect > </TextBlock > <TextBlock FontSize ="20" Margin ="5" Text ="BLight blue dropshadow" > <TextBlock.Effect > <DropShadowEffect Color ="SlateBlue" ShadowDepth ="15" /> </TextBlock.Effect > </TextBlock > <TextBlock FontSize ="20" Margin ="5" Text ="BLight blue dropshadow" > <TextBlock.Effect > <DropShadowEffect Color ="SlateBlue" ShadowDepth ="25" /> </TextBlock.Effect > </TextBlock > <TextBlock FontSize ="20" Margin ="5" Text ="BLight blue dropshadow" > <TextBlock.Effect > <DropShadowEffect Color ="SlateBlue" ShadowDepth ="0" /> </TextBlock.Effect > </TextBlock > </StackPanel >
ShaderEffect类
ShaderEffect 类没有提供就绪的效果。相反,它是一个抽象类,可继承该类以创建自己的自定义像素着色器。通过使用 ShaderEfect类(或从该类派生的自定义效果),可实现更多的效果,而不仅局限于模糊和阴影
可能与您所期望的相反,实现像素着色器的逻辑不是直接在效果类中使用C#代码编写的。相反,像素着色器是用高级着色语言(High Level Shader Language
,HLSL)编写的,该语言是Microsoft DirectX 的一部分(使用这种语言的优点是很明显的—因为 DirectX和 HLSL 已经存在许多年了,图形开发人员已经创建了许多可在代码中使用的像素着色器例程)。
如果正在使用自定义像素着色器(已经编译到名为Effect.ps
的文件中),可使用以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class CustomEffect : ShaderEffect { public CustomEffect () { Uri pixelShaderUri = new Uri("Effect.ps" , UriKind.Relative); PixelShader = new PixelShader(); PixelShader.UriSource = pixelShaderUri; } }
1 2 3 4 5 6 7 <TextBlock FontSize ="20" Margin ="5" Text ="Basic dropshadow" > <TextBlock.Effect > <t:CustomEffect /> </TextBlock.Effect > </TextBlock >
如果使用采用特定输入参数的像素着色器,需要做的工作比上面的示例要更复杂一点。对于这种情况,需要通过调用 RegisterPixelShaderSamplerProperty()静态方法创建相应的依赖项属性。
灵活的像素着色器就像在诸如 Adobe Photoshop 这样的图形软件中使用的插件一样强大。它可以执行任何工作,从添加基本的阴影乃至更富有挑战性的效果,如模糊、辉光、水波、浮雕和锐化等。当结合使用动画实时改变像素着色器的参数时,像素着色器还可创建赏心悦目的效果
WriteableBitmap类
WPF 允许使用 Image 元素显示位图。然而,按这种方法显示图片的方法完全是单向的。应用程序使用现成的位图,读取位图,并在窗口中显示位图。就其本身而言,Image元素没有提供创建或编辑位图信息的方法。
这正是WriteableBitmap
类的用武之地。该类继承自BitmapSource
,BitmapSource
类是当设置Image.Source
属性时使用的类(不管是在代码中直接设置图像,还是XAML中隐式地设置图像)。但BitmapSource
是只读的位图数据映射,而WriteableBitmap
类是个修改的像素数组,为实现需要有趣的效果提供了可能。
生成位图
为使用 WriteableBitmap 类生成一幅位图,必须提供几部分重要信息:以像素为单位的宽度和高度、两个方向上的 DPI分辨率以及图像格式。下面是创建一幅与当前窗口尺寸相同的位图的示例:
1 2 3 4 5 6 7 8 WriteableBitmap wb = new WriteableBitmap( (int )this .ActualWidth, (int )this .ActualHeight, 96 , 96 , PixelFormats.Bgra32, null );
PixelFormats枚举提供了许多像素格式,但只有一半格式被认为是可写入的并且得到了WriteableBitmap类的支持。下面是可供使用的像素格式:
Bgra32 ,使用32位的sRGB颜色。这意味每个像素由32位(或4字节)表示
Bgr32 ,这种格式为每个像素使用4字节,就像Bgra32格式一样。区别是忽略了alpha通道。当不需要透明度时可使用这种格式
Pbgra32 ,就像Bgra32格式一样,该格式为每个像素使用4可字节。区别在于处理半透明像素的方式
BlackWhite、Gray2、Gray4、Gray8 ,这种格式是黑白和灰度格式
Indexed1、Indexed2、Indexed4、Indexed8 ,这些是索引格式,这意味着每个像素指向颜色调色板终点额一个值
写入WiteableBitmap对象
开始时,WriteableBitmap对象中所有字节的值都是0。本质上,就是一个大的黑色矩形。
为使用内容填充 WriteableBitmap 对象,需要使用 WritePixels()方法。WritePixels()方法将字节数组复制到指定位置的位图中。可调用 WitePixels()方法设置单个像素、整幅位图或选择的某块矩形区域。为从 WriteableBitmnap对象中获取像素,需要使用 CopyPixels()方法,该方法将您希望获取的多个字节转换成字节数组。总之,WritePixels()和 CopyPixels()方法没有为您提供可供使用的最方便编程模型,但这是低级像素访问需要付出的代价。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 WriteableBitmap wb = new WriteableBitmap(200 , 200 , 96 , 96 , PixelFormats.Bgra32, null ); for (int x = 0 ; x < wb.PixelWidth; x++){ for (int y = 0 ; y < wb.PixelHeight; y++) { byte blue = (byte )Random.Shared.Next(0 , 256 ); byte green = (byte )Random.Shared.Next(0 , 256 ); byte red = (byte )Random.Shared.Next(0 , 256 ); byte alpha = (byte )Random.Shared.Next(0 , 256 ); byte [] colorData = { blue, green, red, alpha }; Int32Rect rect = new Int32Rect(x, y, 1 , 1 ); wb.WritePixels(rect, colorData, 4 , 0 ); } } img.Source = wb;
更高效的像素写入
尽管在上一节中显示的代码可以工作,但并非最佳方法。如果需要一次性写入大量像素数据–甚至是整幅图像–最好使用更大的块,因为调用 WritePixels()方法需要一定的开销,并且调用该方法越频警,应用程序的运行速度就越慢。
为一次更新更多个像素,需要理解像素被打包进字节数组的方式。无论使用哪种格式,更新缓冲区都将包括一维字节数组。这个数组提供了用于图像矩形区域中像素的数值,成左向右延伸填充没放,然后自上而下延伸。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 WriteableBitmap wb = new WriteableBitmap((int )img.Width, (int )img.Height, 96 , 96 , PixelFormats.Bgra32, null ); Int32Rect rect = new Int32Rect(0 , 0 , (int )img.Width, (int )img.Height); byte [] pixels = new byte [(int )img.Width * (int )img.Height * wb.Format.BitsPerPixel / 8 ];Random rand = new Random(); for (int y = 0 ; y < wb.PixelHeight; y++){ for (int x = 0 ; x < wb.PixelWidth; x++) { int alpha, red, green, blue; if ((x % 5 == 0 ) || (y % 7 == 0 )) { red = (int )(((double )y) / wb.PixelHeight * 255 ); green = rand.Next(100 , 255 ); blue = (int )((double )x / wb.PixelWidth * 255 ); alpha = 255 ; } else { red = (int )((double )x / wb.PixelWidth * 255 ); green = rand.Next(100 , 255 ); blue = (int )((double )y / wb.PixelHeight * 255 ); alpha = 50 ; } int pixelOffset = (x + y * wb.PixelWidth) * wb.Format.BitsPerPixel / 8 ; pixels[pixelOffset] = (byte )blue; pixels[pixelOffset + 1 ] = (byte )green; pixels[pixelOffset + 2 ] = (byte )red; pixels[pixelOffset + 3 ] = (byte )alpha; } int stride = (wb.PixelWidth * wb.Format.BitsPerPixel) / 8 ; wb.WritePixels(rect, pixels, stride, 0 ); } img.Source = wb;
如果需要频繁更新 WriteableBitmap 对象中的图像数据,并希望在另一个线程中执行这些更新,可以使用 WriteableBitmap 后台缓冲区以进一步优化代码。基本过程是:使用 Lock( )方法预订后台缓冲区,获得指向后台缓冲区的指针,更新后台缓冲区,调用 AddDirtyRect()方法指示已经改变的区域,然后通过调用 UnLock()方法释放后台缓冲区。这个过程需要不安全的代码