标签:
文章虽小,内容还好,且看且珍惜。
当界面上使用数千个矢量图形,例如实时统计图,粒子碰撞,比如超级玛丽游戏,图像一直在绘,过量的使用WPF的元素系统和Shape类会使用程序变慢,所以我们需要使用Visual类手动进行渲染。
Visual类是很多WPF元素的父类。所以掌握它当然很重要了。
Visual的开销小于Geometry小于Path
Visual作为抽象类,有UIElement这个子类,也有Viewport3DVisual类(3D知识中的)
放置Visual的对象的容器:ContainerVisual类,继承该类的子类有DrawingVisual类。
DrawingVisual.RenderOpen()会返回个可用于定义可视化内容的DrawingContext对象。
DrawingContext类使用完要Close,类似文件流读写完要close,所以你可以使用using关键字了,使用完自动释放对象。
有了DrawingContext后就可以很轻松的开始绘制你想要的图形了。
除了基本的DrawLine,DrawRectangle,DrawRoundedRectangle,DrawEllipse,当然还有上一篇博客说到的DrawGeometry,还有DrawDrawing来放置Geometry,Drawing对象。
同样的类似winform的验证码绘制代码,DrawText,DrawImage(例如做水印),DrawVedio。
还有些方法可能第一次接触,例如Pop()撤销上一次Push操作,PushClip(),PushEffect(),PushOpacity(),PushOpacityMask(),PushTransform()等
例如代码:
private void Window_Loaded(object sender, RoutedEventArgs e) { DrawingVisual dv=new DrawingVisual(); using (DrawingContext dc=dv.RenderOpen()) { Pen pen = new Pen(new SolidColorBrush(Colors.Red), 2); //Pen pen = new Pen(Brushes.DarkRed, 2); dc.DrawEllipse(Brushes.DarkRed, null, new Point(20, 20), 20, 20); dc.DrawLine(pen, new Point(10, 20), new Point(100, 200)); dc.DrawLine(pen, new Point(0, 10), new Point(80, 150)); } }
DrawingVisual对象一旦关闭,可视化对象不可修改。
以上代码都还算简单,接着我们需要自定义一个Visual放置的面板用于显示元素。除了2个父类的操作,我们还添加了新增和删除,还有一个获取
public class DrawingCanvas : Panel { protected override Visual GetVisualChild(int index) { return base.GetVisualChild(index); } protected override int VisualChildrenCount { get { return base.VisualChildrenCount; } } public void AddVisual(Visual visual) { } public void DeleteVisual(Visual visual) { } public DrawingVisual GetVisual(Point point) { return null; } }
我们规定了约定,接下来新增具体代码
public class DrawingCanvas : Panel { private List<Visual> visuals = new List<Visual>(); protected override Visual GetVisualChild(int index) { return visuals[index]; } protected override int VisualChildrenCount { get { return visuals.Count; } } public void AddVisual(Visual visual) { visuals.Add(visual); base.AddVisualChild(visual); base.AddLogicalChild(visual); } public void DeleteVisual(Visual visual) { visuals.Remove(visual); base.RemoveVisualChild(visual); base.RemoveLogicalChild(visual); } public DrawingVisual GetVisual(Point point) { return null; } }
为了让用户单击面板,我们可以拿到单击时候的点下面的visual,我们使用"命中测试"的wpf技术去做
public DrawingVisual GetVisual(Point point) { HitTestResult hitResult = VisualTreeHelper.HitTest(this, point); return hitResult.VisualHit as DrawingVisual; }
能够单击的都是可视化的,我们也在AddVisual中将visual对象加入了可视化树。
由于单击的地方,或者一块区域,可能有多个Visual,所以我们在返回一个List<DrawingVisual>
到目前为止,这些代码都还是挺好理解的。这里很好的使用了 矩形命中测试和点命中测试
private List<DrawingVisual> hits = new List<DrawingVisual>(); public List<DrawingVisual> GetVisuals(Geometry region) { hits.Clear(); GeometryHitTestParameters parameters = new GeometryHitTestParameters(region); HitTestResultCallback callback = new HitTestResultCallback(this.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; }
接下来新建一个窗体
<Window x:Class="ay3dDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ay3dDemo" Title="Window1" Height="600" Width="800" Loaded="Window_Loaded"> <Grid x:Name="lay"> <Grid.RowDefinitions> <RowDefinition Height="51*"/> <RowDefinition Height="518*"/> </Grid.RowDefinitions> <local:DrawingCanvas Grid.Row="1" x:Name="dcanvas" Background="White" ClipToBounds="True" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> </local:DrawingCanvas> </Grid> </Window>
接下来后台增加drawingvisual到这个面板上
private void Window_Loaded(object sender, RoutedEventArgs e) { DrawingVisual dv = new DrawingVisual(); using (DrawingContext dc = dv.RenderOpen()) { Pen pen = new Pen(new SolidColorBrush(Colors.Red), 2); //Pen pen = new Pen(Brushes.DarkRed, 2); dc.DrawEllipse(Brushes.DarkRed, null, new Point(20, 20), 20, 20); dc.DrawLine(pen, new Point(10, 20), new Point(100, 200)); dc.DrawLine(pen, new Point(0, 10), new Point(80, 150)); } dcanvas.AddVisual(dv); }
效果图:
为什么显示呢,因为我们加入到了可视化树上面。
接下来我们增加面板的左键单击事件
<Grid x:Name="lay"> <Grid.RowDefinitions> <RowDefinition Height="51*"/> <RowDefinition Height="518*"/> </Grid.RowDefinitions> <local:DrawingCanvas Grid.Row="1" x:Name="dcanvas" Background="White" ClipToBounds="True" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MouseLeftButtonDown="dcanvas_MouseLeftButtonDown" > </local:DrawingCanvas> <RadioButton Content="圆形" HorizontalAlignment="Left" Margin="77,10,0,0" VerticalAlignment="Top" Name="rdoEllipse"/> <RadioButton Content="正方形" HorizontalAlignment="Left" Margin="158,10,0,0" VerticalAlignment="Top" Name="rdosquare"/> <RadioButton Content="删除" HorizontalAlignment="Left" Margin="269,10,0,0" VerticalAlignment="Top" Name="rdodelete"/> <RadioButton Content="移动" HorizontalAlignment="Left" Margin="393,10,0,0" VerticalAlignment="Top" Name="rdomove"/> </Grid>
接下来,后台左键事件中,使用点命中测试
private void dcanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Point pointClicked = e.GetPosition(dcanvas); if (rdodelete.IsChecked.Value) { DrawingVisual dv = dcanvas.GetVisual(pointClicked); if (dv != null) { dcanvas.DeleteVisual(dv); } } }
接下来我们再次绘制矩形和圆形
private void dcanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Point pointClicked = e.GetPosition(dcanvas); if (rdodelete.IsChecked.Value) { DrawingVisual dv = dcanvas.GetVisual(pointClicked); if (dv != null) { dcanvas.DeleteVisual(dv); } } else if (rdoEllipse.IsChecked.Value) { DrawingVisual dv = new DrawingVisual(); using (DrawingContext dc = dv.RenderOpen()) { Pen pen = new Pen(new SolidColorBrush(Colors.Red), 2); dc.DrawEllipse(Brushes.DarkRed, pen, pointClicked, 20, 20); dcanvas.AddVisual(dv); } } else if (rdosquare.IsChecked.Value) { DrawingVisual dv = new DrawingVisual(); using (DrawingContext dc = dv.RenderOpen()) { Pen pen = new Pen(new SolidColorBrush(Colors.Red), 5); dc.DrawRectangle(Brushes.YellowGreen, pen, new Rect(pointClicked, new Size(50, 30))); dcanvas.AddVisual(dv); } } }
代码简单易懂,不讲了,知识让你对Visual有认识,不会太害怕这个知识点了。
所以到目前位置你都可以自定义Geometry了,如果你想保存最后的工程图,你可以记住位置和图形就ok了。
关于移动也就不写了,因为可以拿到Visual对象就行了,想怎么操作就怎么操作了。
Pen是边框,Brush是填充画刷
接下来讲一下矩形区域命中测试,首先我们就需要创建一个矩形
思路:
鼠标左键按下,标记状态是多选状态,公用一个pen,brush,drawingvisual,共享起始点的位置。
1.左键按下,创建一个DrawingVisual,并放入面板,标记起始点,设置当前鼠标移动时候的状态,捕获鼠标 dcanvas.CaptureMouse();
public bool isMultiSelecting = false; private DrawingVisual selectionSquare; private Brush selectionSquareBrush = Brushes.Transparent; private Pen selectionSquarePen = new Pen(Brushes.Black, 1); private Point selectionSquareTopLeft; private void dcanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Point pointClicked = e.GetPosition(dcanvas); if (rdoMouseMul.IsChecked.Value) { selectionSquare = new DrawingVisual(); dcanvas.AddVisual(selectionSquare); selectionSquareTopLeft = pointClicked; isMultiSelecting = true; dcanvas.CaptureMouse(); }
.....
}
2.鼠标移动时候,如果是多选时候就触发代码,设置pen的风格是虚线,并开始绘制,这里我们可以得到起点和中点,所以可以创建一个虚线的矩形 Visual。
private void dcanvas_MouseMove(object sender, MouseEventArgs e) { if (isMultiSelecting) { Point pointDragged = e.GetPosition(dcanvas); selectionSquarePen.DashStyle = DashStyles.Dash; using (DrawingContext dc = selectionSquare.RenderOpen()) { dc.DrawRectangle(selectionSquareBrush, selectionSquarePen, new Rect(selectionSquareTopLeft, pointDragged)); } } }
鼠标抬起时候,我们根据起点和终点创建一个RectangleGeometry,用于做区域命中测试
private void dcanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (isMultiSelecting) { RectangleGeometry geometry = new RectangleGeometry( new Rect(selectionSquareTopLeft, e.GetPosition(dcanvas))); List<DrawingVisual> visualsInRegion = dcanvas.GetVisuals(geometry); lblmsg.Content = String.Format("你选中了{0}个visual", visualsInRegion.Count); isMultiSelecting = false; dcanvas.DeleteVisual(selectionSquare); dcanvas.ReleaseMouseCapture(); } }
虚线矩形在鼠标抬起时候,从面板中移除,我们并且释放鼠标捕获。
效果图演示
更高性能的WriteableBitmap对象,这里我也不太会,所以不讲了。
讲了这么多基础图形的东西,感觉本篇文章实用的一些东西还挺少,其实了解了Visual可以做出很多高级效果。这里后面我们再讲。
为了让文章充实,我暴露一个我的AyImage4Button按钮的制作方法
图片的区域使用,我找了一张4种按钮状态的图片,我们使用按钮去操作
下面看下我的代码
<Style x:Key="{x:Type control:AyImage4Button}" TargetType="{x:Type control:AyImage4Button}"> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="Background" Value="Red"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true" > <Border.Background> <ImageBrush ImageSource="{Binding Path=Icon, RelativeSource={RelativeSource TemplatedParent}}" Stretch="Uniform" Viewbox="0,0 0.25,1"/> </Border.Background> <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsDefaulted" Value="true"> <Setter Property="Background" TargetName="border"> <Setter.Value> <ImageBrush ImageSource="{Binding Path=Icon, RelativeSource={RelativeSource TemplatedParent}}" Stretch="Uniform" Viewbox="0,0 0.25,1"/> </Setter.Value> </Setter> </Trigger> <Trigger Property="IsMouseOver" Value="true"> <Setter Property="Background" TargetName="border"> <Setter.Value> <ImageBrush ImageSource="{Binding Path=Icon, RelativeSource={RelativeSource TemplatedParent}}" Stretch="Uniform" Viewbox="0.25,0 0.25,1"/> </Setter.Value> </Setter> </Trigger> <Trigger Property="IsPressed" Value="true"> <Setter Property="Background" TargetName="border"> <Setter.Value> <ImageBrush ImageSource="{Binding Path=Icon, RelativeSource={RelativeSource TemplatedParent}}" Stretch="Uniform" Viewbox="0.5,0 0.25,1"/> </Setter.Value> </Setter> </Trigger> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Background" TargetName="border"> <Setter.Value> <ImageBrush ImageSource="{Binding Path=Icon, RelativeSource={RelativeSource TemplatedParent}}" Stretch="Uniform" Viewbox="0.75,0 0.25,1"/> </Setter.Value> </Setter> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
using System; using System.Collections.Generic; using System.Text; using System.Windows.Controls; using System.Windows.Media; using System.ComponentModel; using System.Windows; namespace Ay.Framework.WPF.Controls { public class AyImage4Button:Button { /// <summary> /// Image4Button /// </summary> public ImageSource Icon { get { return (ImageSource)GetValue(IconProperty); } set { SetValue(IconProperty, value); } } // Using a DependencyProperty as the backing store for Icon. This enables animation, styling, binding, etc... public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(ImageSource), typeof(AyImage4Button), new PropertyMetadata(null)); } }
这里我使用了ImageBrush的Viewbox截取图片的区域部分
ok,我们来使用下
首先引入:
xmlns:local2="clr-namespace:Ay.Framework.WPF.Controls"
上方引入样式后
<local2:AyImage4Button x:Name="CloseBtn" Width="32" Height="24" Margin="605,8,0,17" HorizontalAlignment="Left" VerticalAlignment="Center" Icon="close.png" />
ImageBrush还有TileMode就是平铺方式,除了百分比的区域截图,还有绝对尺寸的区域截图ViewportUnits
还有个VisualBrush,做复制样子的。比如应用,做倒影,指定Visual的属性就可以了,倒影,然后你再旋转图形,然后设置OpacityMask
<local2:AyImage4Button x:Name="CloseBtn" Width="32" Height="24" Margin="605,8,0,17" HorizontalAlignment="Left" VerticalAlignment="Center" Icon="close.png" /> <Rectangle Margin="605,48,155,5" Width="32" Height="24"> <Rectangle.Fill> <VisualBrush Visual="{Binding ElementName=CloseBtn}"/> </Rectangle.Fill> </Rectangle>
还有个画刷叫BitmapCacheBrush,用法类似VisualBrush,设置Rectangle的Fill的Brush为BitmapCacheBrush,然后使用Target替代Visual,然后指定BitmapCache为,例如BitmapCache使用位图缓存,当然第一次缓存就会有一些延迟了。这里暂时不讲了。比如一个小球在运动,背景是死的,球在背景上动,那么如果背景不是缓存的,那么背景将会一直刷新,就会很费内存,此时的画刷就可以使用BitmapCacheBrush了,它的好处不言而喻了。
接下来,我需要讲一下RenderTransform和LayoutTransform的区别。
LayoutTransform是在转换操作之前就计算好布局了。而RenderTransform是在呈现时候,例如,两个旋转的按钮
<StackPanel Margin="25" Background="LightYellow"> <Button Padding="5" HorizontalAlignment="Left"> <Button.RenderTransform> <RotateTransform Angle="35" CenterX="45" CenterY="5" /> </Button.RenderTransform> <Button.Content>I‘m rotated 35 degrees</Button.Content> </Button> <Button Padding="5" HorizontalAlignment="Left">I‘m not</Button> </StackPanel> <StackPanel Margin="25" Background="LightYellow"> <Button Padding="5" HorizontalAlignment="Left"> <Button.LayoutTransform> <RotateTransform Angle="35" CenterX="45" CenterY="5" /> </Button.LayoutTransform> <Button.Content>I‘m rotated 35 degrees</Button.Content> </Button> <Button Padding="5" HorizontalAlignment="Left">I‘m not</Button> </StackPanel>
可以看出区别了吧。有时候如果变化的时候发现不对,你就换一下转换布局的方式 o(∩_∩)o 哈哈
关于动画一章,我为什么还不讲,原因是,Blend中基本的太容易做了,建议大家还是试试,前台代码转后台代码的写法试试。练习,因为我的一些想要的动画都是先用blend写好动画草稿,然后自己在后台转换为后台代码去是实现的,例如那个环状图AyArcChart。
好了,从下篇开始,我就要一直讲3d的知识了,当然也会有很炫的3d的窗口动画。
=============潇洒的版权线==========www.ayjs.net===== Aaronyang ========= AY =========== 安徽 六安 杨洋 ========== 未经允许不许转载 =========
-------------------小小的推荐,作者的肯定,读者的支持。推不推荐不重要,重要的是希望大家能把WPF推广出去,别让这么好的技术消失了,求求了,让我们为WPF技术做一份贡献。-----------------
[Aaronyang] 写给自己的WPF4.5 笔记19[Visual类图文并茂讲解]
标签:
原文地址:http://www.cnblogs.com/AaronYang/p/4335570.html