WPF自定义控件 —— 自绘篇
本文示例源代码或素材下载
首先我们来说最简单的画面的呈现。
一.在自定义控件上画矩形
增加一个自定义控件类:
打开创建的类重载OnRender函数并注释掉静态构造函数,代码如下
public class CustomerRender : Control
{
//static CustomerRender()
//{
// DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomerRender), new FrameworkPropertyMetadata(typeof(CustomerRender)));
//}
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(Brushes.Bisque, null, new Rect(0, 0, 50, 50));
}
}
在Window1这个类中修改如下
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
CustomerRender customerRender = new CustomerRender();
customerRender.Width = 200;
customerRender.Height = 300;
this.Content = customerRender;
}
}
本文示例源代码或素材下载
首先我们来说最简单的画面的呈现。
一.在自定义控件上画矩形
增加一个自定义控件类:

打开创建的类重载OnRender函数并注释掉静态构造函数,代码如下
public class CustomerRender : Control
{
//static CustomerRender()
//{
// DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomerRender), new FrameworkPropertyMetadata(typeof(CustomerRender)));
//}
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(Brushes.Bisque, null, new Rect(0, 0, 50, 50));
}
}
在Window1这个类中修改如下
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
CustomerRender customerRender = new CustomerRender();
customerRender.Width = 200;
customerRender.Height = 300;
this.Content = customerRender;
}
}
运行程序我们可以看到:
当然你还可以通过drawingContext来画圆圈、不规则矩形、圆角矩形和写文字等等。具体可以看DrawingContext 中的API介绍和MSDN文档。

当然你还可以通过drawingContext来画圆圈、不规则矩形、圆角矩形和写文字等等。具体可以看DrawingContext 中的API介绍和MSDN文档。
二.拖动矩形
一般来说UI是数据的反应,UI的变化其实是后段数据的变化。我们一般在移动到方块上方后,按住鼠标开始拖动,那么在鼠标移动上去之后,我们首先判断此位置是否有矩形,点击鼠标的时候,我们记录点下的这个坐标为A,鼠标移动的时候我们根据移动时候鼠标所在坐标和原来的A坐标比较,然后把这个差值和矩形的开始坐标相加,通过InvalidateVisual()方法不断重画就达到了移动的效果。

如果你需要鼠标移动出窗口还可以移动该矩形请加CaptureMouse()来锁定当前鼠标,但不要忘记在鼠标放开的时候把它给释放掉ReleaseMouseCapture()。
我们在window类中为我们的控件赋上底色
InitializeComponent();
CustomerRender customerRender = new CustomerRender();
customerRender.Width = 200;
customerRender.Height = 300;
customerRender.Background = Brushes.Green;
this.Content = customerRender;
并在控件的OnRender函数中加入
drawingContext.DrawRectangle(Background, null,new Rect(new Point(0,0),this.RenderSize));
效果如图:
这时你拖动方块可能会发现方块可以被移出背景,这在WPF下是允许的,但有时候这又不符合情理,我们需要把多余的部分裁减掉。
protected override void OnRender(DrawingContext drawingContext)
{
Rect rect = new Rect(new Point(0, 0), this.RenderSize);
drawingContext.DrawRectangle(Background, null, rect);
//创建一个填充色为Bisque边框色为Orange,横坐标为2,纵坐标为3,宽度为80,长度为50的矩形
Pen pen = new Pen(Brushes.Orange, 1);
pen.Freeze();//冻结可以加快呈现速度
RectangleGeometry rectangleGeometry = new RectangleGeometry(rect);
rectangleGeometry.Freeze();
drawingContext.PushClip(rectangleGeometry);
drawingContext.DrawRectangle(Brushes.Bisque, pen, _rectangle);
drawingContext.Pop();
}

这时你拖动方块可能会发现方块可以被移出背景,这在WPF下是允许的,但有时候这又不符合情理,我们需要把多余的部分裁减掉。

protected override void OnRender(DrawingContext drawingContext)
{
Rect rect = new Rect(new Point(0, 0), this.RenderSize);
drawingContext.DrawRectangle(Background, null, rect);
//创建一个填充色为Bisque边框色为Orange,横坐标为2,纵坐标为3,宽度为80,长度为50的矩形
Pen pen = new Pen(Brushes.Orange, 1);
pen.Freeze();//冻结可以加快呈现速度
RectangleGeometry rectangleGeometry = new RectangleGeometry(rect);
rectangleGeometry.Freeze();
drawingContext.PushClip(rectangleGeometry);
drawingContext.DrawRectangle(Brushes.Bisque, pen, _rectangle);
drawingContext.Pop();
}
主要注意drawingContext.PushClip(rectangleGeometry);到drawingContext.Pop();之间是剪切的有效范围。 Pop()结束符不仅仅用于剪切,还适用于遮幕,Effect效果等有Push开头的那些玩艺。(关于GuidelineSet推荐一个网址:
http://www.wpftutorial.net/DrawOnPhysicalDevicePixels.html)
剪切后的效果如下:
剪切后的效果如下:

三.动画效果
当然我们也可以做些拖动预览效果如下:

实现的原理无非是拖动的时候画一个假矩形,并把该矩形呈现的代码放在剪切的效果之外,拖动结束后把假的位置给真的调用重画一遍就实现了。
我们能不能用动画效果使得矩形慢慢移动到指定位置,答案当然是肯定的。这里可以用System.Windows.Media.Animation命名空间下的一些方法来实现
RectAnimation rectAnimation = new RectAnimation
(_rectangle,_preivewRectangle,new Duration(TimeSpan.FromMilliseconds(600)));
drawingContext.DrawRectangle(Brushes.Bisque, pen, _rectangle,rectAnimation.CreateClock());
你也可以用来做颜色的变换,大小的变化等等。
四.多个矩形
有多个矩形可以把矩形放到一个**中依次判断,层叠的话就是后段数据存放的先后顺序问题,UI显示的快慢和后段数据查找有直接的联系,这个也是算法的价值体现。
小结
这里讲的做法也是winform里的大多数实现方式,只不过WPF使得我们对像素处理关心的更少,自绘在WPF的应用中相对winform要少了很多,除非用在大数据量的地方,WPF复合控件能完成大部分的需求——只要你能熟练定义样式,并了解可视树,逻辑树,以及各个控件的用法。
WPF自定义控件 —— 布局
本篇是上一篇自绘的补充,但需要一定的WPF相关知识,感谢Clingingboy 通宵达旦的帮助。 本文示例源代码或素材下载
本篇是上一篇自绘的补充,但需要一定的WPF相关知识,感谢Clingingboy 通宵达旦的帮助。 本文示例源代码或素材下载
一.ScrollViewer
在WPF自定义控件 —— 自绘篇我们做了一个可拖动的矩形,但你是否发现当矩形拖出背景后就不见了,一般来说对于不可见区域需要有ScrollBar来呈现,如图:

对于这一应用在WPF中最常用的应该在控件外面包个ScrollViewer,那么如何使得我们的控件支持ScrollViewer呢?
首先我们来了解一下ScrollViewer基本原理

通过上图我们可以看到ScrollViewer是以Grid为容器组成的控件,其中主要包括ScrollContentPresenter,和两个ScrollBar,其中ScrollBar就是我们第一张图中看到那两条,它也是一个由多个控件组成的复合控件,在这里先略过ScrollBar;来看红色边框内的ScrollContentPresenter,可以看到我们的控件CustomerRender在ScrollContentPresenter内,那么我们控件呈现的位置必定是由它来控制的。
这个神秘的ScrollContentPresenter到底做了什么能让我们看到一部分内容呢?看下这张图就清楚了

我们可以知道ScrollContentPresenter实际对我们玩了一个遮罩效果,把我们的控件当作一个背景图,用ScrollBar来移动背景位置,在ScrollViewer外的控件可视部分统统被裁减掉了。只要继承UIElement的控件就可以重载GetLayoutClip方法来剪切区域。
protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
return new RectangleGeometry(new Rect(base.RenderSize));
}
二.ArrangeOverride
ScrollContentPresenter又是如何控制子元素的坐标呢?重载ArrangeOverride函数便可,具体看代码注释
protected override Size ArrangeOverride(Size arrangeBounds)
{
//得到**中的第一个元素
UIElement visualChild = this.GetVisualChild(0) as UIElement;
//把子元素的左上角坐标定义到容器之外
Point point = new Point(-40, -50);
if (visualChild != null)
{
Rect finalRect = new Rect(point, visualChild.DesiredSize);
//设置元素坐标和大小
visualChild.Arrange(finalRect);
}
return arrangeBounds;
}
这段代码中参数arrangeBounds是父容器传进的值,一般表示你可以有多大的利用空间,这个函数的返回值一般指的是你控件RenderSize的大小.
RenderSize有什么用?(欢迎大家补充)
在onRender里可以用,比如画背景。
在MeasureOverride函数中当参数值为无限大时用来得知可用空间的大小。
{
return new RectangleGeometry(new Rect(base.RenderSize));
}
二.ArrangeOverride
ScrollContentPresenter又是如何控制子元素的坐标呢?重载ArrangeOverride函数便可,具体看代码注释
protected override Size ArrangeOverride(Size arrangeBounds)
{
//得到**中的第一个元素
UIElement visualChild = this.GetVisualChild(0) as UIElement;
//把子元素的左上角坐标定义到容器之外
Point point = new Point(-40, -50);
if (visualChild != null)
{
Rect finalRect = new Rect(point, visualChild.DesiredSize);
//设置元素坐标和大小
visualChild.Arrange(finalRect);
}
return arrangeBounds;
}
这段代码中参数arrangeBounds是父容器传进的值,一般表示你可以有多大的利用空间,这个函数的返回值一般指的是你控件RenderSize的大小.
RenderSize有什么用?(欢迎大家补充)
在onRender里可以用,比如画背景。
在MeasureOverride函数中当参数值为无限大时用来得知可用空间的大小。
三.MeasureOverride
visualChild.DesiredSize的值实际就是我们常用的ActualHeight和ActualWidth的源头,也就是控件的实际大小,我们可以重载MeasureOverride产生。下面是我们的自定义控件用的。
protected override Size MeasureOverride(Size constraint)
{
Size size = new Size
(
//判断形参constraint中传的值大,还是我们的Rectangle的值大,以最大的那个作为控件的长宽
Math.Max(double.IsInfinity(constraint.Width) ? this.RenderSize.Width : constraint.Width, _preivewRectangle.Right),
Math.Max(double.IsInfinity(constraint.Height) ? this.RenderSize.Height : constraint.Height, _preivewRectangle.Bottom));
return size;
}
要说明下的是外容器的大小并不会触发MeasureOverride(如把窗体拖大),只会触发ArrangeOverride,如果你要重新为DesiredSize赋值并通知父容器请使用Measure函数,它会调用父控件的OnChildDesiredSizeChanged方法来通知,同理父控件要监听子控件的大小变化只要重载该方法即可,这个方法可以一直沿着可视树向上引发InvalidateMeasure函数,InvalidateMeasure通过DispatcherPriority为Render来异步调用Measure。
ArrangeOverride中尽量不要调用本身的Measure,Measure函数会再次调用InvalidateArrange方法从而引起循环。控件容器放生变化时可以重载OnRenderSizeChanged实现。
Measure和Arrange的具体关系如下图:

四.补充
如果想要自定义的ScrollBar你可以能要根绝ScrollBar的值实时进行重绘图,这个好处是数据量大,你只需呈现当前画面中的图形,缺点是动一动就要重绘。通过例如ScrollViewer裁减的方式,遮罩得时候不会重绘,不过刚开始呈现的时候数据量大会慢。
另外在复合控件中配合Transform中的各种类来进行布局,使用CompositionTarget和动画类可以产生很多效果。
来源:博客园 作者:Curry