2012年9月2日星期日

从WPF到Silverlight到WinRT: 自定义控件从此没有Visual和OnRender

从WPF到Silverlight到WinRT: 自定义控件从此没有Visual和OnRender

目录

  • 更精简的UIElement继承树
  • WPF中使用VisualCollection来自定义控件
  • Visual Tree的”Visual”已从Visual变成UIElement
  • Silverlight和WinRT的唯一自定义控件方法
  • 最后的OnRender方法

 

这个话题早就想写写了,一直没时间,后来索性忘了,今天在写WP7控件又遇到类似话题,把他写出来。

 

注意:

由于WinRT的XAML衍生自Silverlight,两者XAML相关类型的执行虽有差异,但整体相似,不影响本文话题,因此将Silverlight和WinRT合并起来说,并以WinRT代码做示例。

 

 

返回目录

更精简的UIElement继承树

在WPF中,UIElement上是Visual –> DependencyObject –> DispatcherObject –> Object

image

 

而在Silverlight/WinRT中,UIElement上仅仅是:DependencyObject –> Object

下图是WinRT的UIElement继承树:

image

 

没有了Visual和DispatcherObject。

 

首先在Silverlight/WinRT中,DispatcherObject被整合到了DependencyObject中了,这个DispatcherObject的Dispatcher属性返回一个Dispatcher对象,同时CheckAccess和VerifyAccess方法其实就是调用Dispatcher对象的相应方法。那么在Silverlight和WinRT中,只需要DependencyObject返回一个Dispatcher就可以了。(在Silverlight中Dispatcher只有CheckAccess方法。在WinRT中,该类型的名称是CoreDispatcher,在Windows.UI.Core命名空间内,它有一个HasThreadAccess属性有类似的功能)

 

接下来就是少了Visual,也是本文的主题。下面将更具体的阐述Visual的离去所带来的影响。

 

 

 

返回目录

WPF中使用VisualCollection来自定义控件

WPF中可以使用VisualCollection来自定义控件直接在代码中控制Visual Tree的数据,VisualCollection的初始化需要另一个Visual对象,创建好VisualCollection后,该集合的改变将同时改变目标控件的Visual Tree数据,同时还有一个UIElementCollection,不仅会改变Visual Tree,同时还会改变Logical Tree的数据。更多可以参考我的另一篇文章:

WPF中神奇的容器:VisualCollection,UIElementCollection和ItemCollection

 

对于自定义控件,同时还需改写Visual.GetVisualChild方法和Visual.VisualChildrenCount属性,把需要返回的值和VisualCollection联系起来。这样WPF系统可以正确获取Visual Tree中数据的信息。

最后则是通过改写MeasureOverride和ArrangeOverride方法来把Visual Tree中的数据正确显示出来(相关可以参考:WPF:为什么需要Measure和Arrange两步?)。

 

那么我们自定义一个非常简单的控件,一个只显示一个Button的控件,如下WPF代码:

class MyControl : Control

{

    //需要显示的控件

    Button button;

    //用于Visual Tree的VisualCollection

    VisualCollection visualCollection;

 

    //初始化VisualCollection并添加Visual

    public MyControl()

    {

        visualCollection = new VisualCollection(this);

        button = new Button() { Content = "Mgen" };

        visualCollection.Add(button);

    }

 

    //改写Visual.GetVisualChild

    protected override Visual GetVisualChild(int index)

    {

        return visualCollection[index];

    }

    //改写Visual.VisualChildrenCount

    protected override int VisualChildrenCount

    {

        get

        {

            return visualCollection.Count;

        }

    }

 

    //改写MeasureOverride和ArrangeOverride

    protected override Size MeasureOverride(Size constraint)

    {

        button.Measure(constraint);

        return button.DesiredSize;

    }

 

    protected override Size ArrangeOverride(Size arrangeBounds)

    {

        button.Arrange(new Rect(arrangeBounds));

        return arrangeBounds;

    }

}

 

OK,把该控件声明在XAML中,运行,一个只显示Button的控件:

image

 

 

返回目录

Visual Tree的”Visual”已从Visual变成UIElement

上面我们通过VisualCollection操作Visual Tree创建了一个简单的WPF控件,但是在Silverlight/WinRT中已经没有了Visual,那么,Visual Tree也没了?显然不可能,WinRT和Silverlight的Visual Tree和Logical Tree我都亲自解析过,可以参考以前的文章:

WinRT/Metro:解析元素在Visual Tree和Logical Tree中的位置

Silverlight以及Windows Phone:解析元素在Visual Tree和Logical Tree中的位置

 

那么Visual Tree的”Visual”到底代表谁?这个可以从VisualTreeHelper中验证,在WPF中,显然就是Visual类型!

 

下面WPF中VisualTreeHelper.GetChild方法代码截图:

image

不需要多说明了,显然它在找Visual,还有Visual3D(WPF中3D对象的Visual,也是直接继承自DependencyObject,并且派生UIElement3D)

 

在Silverlight和WinRT中,由于WinRT是用C++写的,轻易看不到Source,这里通过看Silverlight的VisualTreeHelper的GetChild方法:

image

是UIElement!

 

 

 

 

 

返回目录

Silverlight和WinRT的唯一自定义控件方法

尽管MeasureOverride和ArrangeOverride仍然在Silverlight和WinRT中存在,但是他们仅仅会用在自定义Panel中了,因为Panel控件用户是可以设置其Visual Tree成员的。因此自定义控件的样子完全依靠默认Style了,也就是在Themes/Generic.xaml中定义的控件默认Style了。

 

如果背后代码非要需要某种控件的话,那么在控件定义时最好加上TemplatePart特性,然后再改写OnApplyTemplate方法通过GetTemplateChild方法获取对于控件就可以了。

 

那么可以定义一个简单的WinRT自定义控件,需要模板中必须有一个Button控件,然后在背后控件代码中操作Button。

 

另外WinRT中的自定义控件又改名叫Templated Control了,其实就是Custom Control:

image

 

控件代码:

[TemplatePart(Name = "PART_Button", Type = typeof(Button))]

public sealed class MyControl : Control

{

    public MyControl()

    {

        this.DefaultStyleKey = typeof(MyControl);

    }

 

    protected override void OnApplyTemplate()

    {

        base.OnApplyTemplate();

        //可以在OnApplyTemplate中通过GetTemplateChild获取相应控件

        var button = GetTemplateChild("PART_Button") as Button;

 

        //判断控件不为空后进行操作

        if (button != null)

        {

            button.Content = "Mgen";

        }

    }

 

}

 

接着在Generic.xaml中定义好控件XAML就可以了:

<Style TargetType="local:MyControl">

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="local:MyControl">

                <Border

                   Background="{TemplateBinding Background}"

                   BorderBrush="{TemplateBinding BorderBrush}"

                   BorderThickness="{TemplateBinding BorderThickness}">

                    <Button x:Name="PART_Button"/>

                </Border>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

 

运行后,同样会有一个叫Mgen的Button出现在屏幕上:

image

 

当然,上面的这一切WPF早就支持了,只不过WPF还提供更多的选择!VisualCollection还有OnRender都可以。OnRender后面会说,当然最传统的方式还是上述这种方式的,这个在WPF/Silverlight/WinRT中都是被很好地支持的,可以说是XAML的标准创建自定义控件方法吧。当然对于Silverlight/WinRT这种对性能和大小都必须要求更高的框架,把一些可有可无的东西剪掉也是在情理之中的。

对于WPF的这种创建控件方法,跟上方类似,可以参考这篇专门为WPF写的文章:

WPF:详解创建Lookless自定义控件——文件选择控件

 

 

 

返回目录

最后的OnRender方法

WPF还有一种Silverlight和WinRT都没有的自定义控件方法,那就是UIElement的OnRender方法。这种方法的存在或多或少是保留另一个早期UI框架Windows Forms的方式。也就是Windows Forms中经典的Control.OnPaint方法。OnRender和OnPaint完全类似,DrawingContext对象就相当于Windows Forms中的Graphics对象,这种经典的绘图和刷新机制的控件显示完全可以追溯到更原始的Win32 API控件的创建。

 

当然,由于OnRender方法实在是太底层了,因此无法绘制任何可交互的控件(把它想象成画图),因此一般除了从底层创建控件或者做一些对性能要求较高控件你是很少需要他的。

 

这里,通过FormattedText和DrawText方法来创建一个最简单的显示文字的控件,注意依赖属性的定义需要加入FrameworkPropertyMetadataOptions.AffectsRender选项来触发OnRender。

代码:

class MyControl : Control

{

    #region Text

 

    public static readonly DependencyProperty TextProperty =

        DependencyProperty.Register("Text", typeof(string), typeof(MyControl),

            new FrameworkPropertyMetadata((string)null,

                FrameworkPropertyMetadataOptions.AffectsRender));

 

    public string Text

    {

        get { return (string)GetValue(TextProperty); }

        set { SetValue(TextProperty, value); }

    }

 

    #endregion

 

    protected override void OnRender(DrawingContext drawingContext)

    {

        if (Text == null)

            return;

        //创建FormattedText

        var format = new FormattedText(Text,

            System.Globalization.CultureInfo.InvariantCulture,

            FlowDirection.LeftToRight,

            new Typeface("Consolas"), 20, Brushes.Red);

        //设置可视范围

        format.MaxTextHeight = RenderSize.Height;

        format.MaxTextWidth = RenderSize.Width;

        //Draw!

        drawingContext.DrawText(format, new Point(10, 10));

    }

 

}

 

在XAML中设置属性:

<loc:MyControl Text="hello, mgen!"/>

 

结果:

image


TAG: