65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中的附加行为示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (8投票s)

2015年5月19日

CPOL

13分钟阅读

viewsIcon

31506

downloadIcon

1181

以下是使用附加属性实现的功能:将枚举绑定到 ComboBox,创建可搜索的 TreeView,为 WPF 创建 WinForm 风格的 TreeView 以及为 WPF 创建炫酷的像素着色器放大镜。

引言

在本文中,我将向您展示三个使用附加属性的实现的示例,这些属性可以非常简单地在一行 XAML 代码中为现有控件(或类)添加便捷的行为,而无需通过继承控件到自定义类来仅用属性进行扩展。附加属性的使用非常广泛,即使您从未听说过它们。我能想到最常见的例子是 Grid.Row 和 Grid.Column 属性。这些是附加行为应该如何工作的示例:可重用的代码,设置时间很短,并且以一种优雅的方式解决了反复出现的问题。

背景

附加属性非常类似于现有类的扩展,下面通过函数Reverse为例,该函数现在可以在应用程序中的每个String上调用。

Module Extensions
    <System.Runtime.CompilerServices.Extension>
    Public Function Reverse(ByVal OriginalString As String) As String
        Dim Result As New Text.StringBuilder
        Dim chars As Char() = OriginalString.ToCharArray

        For i As Integer = chars.Count - 1 To 0 Step -1
            Result.Append(chars(i))
        Next

        Return Result.ToString
    End Function
End Module

虽然这在任何地方都有效,但附加属性更有用,因为您可以决定要扩展哪个类的实例。它们也是 DependencyProperties,支持与其他元素绑定。

要创建附加依赖属性,您需要创建一个新类(此处称为MyNewClass),并如下声明:

    Public Shared ReadOnly SearchTextProperty As DependencyProperty =
        DependencyProperty.RegisterAttached("SearchText",
                                            GetType(String),
                                            GetType(MyNewClass),
                                            New FrameworkPropertyMetadata(
                                                Nothing,
                                                FrameworkPropertyMetadataOptions.AffectsRender,
                                                New PropertyChangedCallback(AddressOf OnSearchTextChanged)))

普通依赖属性和附加属性之间的区别在于附加属性的此调用:

DependencyProperty.RegisterAttached

以下适用于普通依赖属性:

DependencyProperty.Register

附加属性非常有用,以至于创建了一个抽象层来将其封装在 Blend 中,称为 Behavior。您可以在Brian Noyes的博客上阅读更多相关信息。要瞥一眼如何自己创建一个,可以查看 Jason Kemp 的博客文章。他的代码允许您以一种类似于 Behavior 类的方式添加元素,并且 VB.NET 版本已添加到 VS2013 项目的 Unused 文件夹中。

要在项目中使用它,您可以键入以下内容:

     <TreeView>
            <local:Behavior.ContentPresenter>
                <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
                    <TextBlock Height="23">
                        <Run Text="Show some text" FontSize="18"/>
                    </TextBlock>
                </StackPanel>
            </local:Behavior.ContentPresenter>
            ....

        </TreeView>

将枚举绑定到 ComboBox

假设您想将Enum绑定到ComboBox,以便显示当前值并允许您通过更改ComboBox中的SelectedItem来更改它。这个问题似乎是一个反复出现的主题,有许多不同的解决方案,甚至一些带有附加行为的解决方案。

我假设您只想将Enum显示为带有可读字符串的ComboBox,并且我还假设我们都知道通过为 Enum 添加可读描述的技巧。为了完整起见,我将再次介绍它,您只需这样做:

    Public Enum TheComboBoxShow
        <System.ComponentModel.DescriptionAttribute("The show is on")>
        [On]
        <System.ComponentModel.DescriptionAttribute("The show is off")>
        [Off]
    End Enum

现在,可以使用描述属性从Enum属性收集信息,方法是使用一个辅助函数。这个特定的函数我从OriginalGriff那里窃取的,但许多其他人也根据MSDN 示例创建了类似的函数。

    Public Shared Function GetDescription(value As [Enum]) As String
        ' Get information on the enum element
        Dim fi As FieldInfo = value.[GetType]().GetField(value.ToString())
        ' Get description for elum element
        Dim attributes As DescriptionAttribute() = DirectCast(fi.GetCustomAttributes(GetType(DescriptionAttribute), False), DescriptionAttribute())
        If attributes.Length > 0 Then
            ' DescriptionAttribute exists - return that
            Return attributes(0).Description
        End If
        ' No Description set - return enum element name
        Return value.ToString()
    End Function

我们议程上的下一个项目(作为程序员,您必须喜欢这个文字游戏!不?好的,我们继续)是在ComboBox中显示所有Enum。似乎主要的方法,至少通过大量的 Google 搜索,是使用ObjectDataProvider

<UserControl.Resources>
    <ObjectDataProvider MethodName="GetValues" 
    ObjectType="{x:Type sys:Enum}" x:Key="Options">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="local:EnumOptions" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</UserControl.Resources>

然后像这样绑定到 ComboBox:

<ComboBox x:Name="cmbOptions"  
    ItemsSource="{Binding Source={StaticResource Options}}"
    ....
    ....
</ComboBox>

我的天,要显示Enum需要写这么多代码。除非您决定成为一名程序员是因为您非常喜欢向计算机输入命令,否则这有点太多了。我一直希望它是这样的:您在类中声明了一个Enum属性:

Class TheComboBoxShowCase
    Implements System.ComponentModel.INotifyPropertyChanged

  ...

    Private pTheEnumProperty As TheComboBoxShow = TheComboBoxShow.On
    Public Property TheEnumProperty() As TheComboBoxShow
        Get
            Return pTheEnumProperty
        End Get
        Set(ByVal value As TheComboBoxShow)
            pTheEnumProperty = value
            OnPropertyChanged("TheEnumProperty")
        End Set
    End Property

    Public Enum TheComboBoxShow
        <DescriptionAttribute("The show is on")>
        [On]
        <DescriptionAttribute("The Show is off")>
        [Off]
    End Enum

End Class

然后在 XAML 中,您只需使用以下命令将其连接到ComboBox

        <ComboBox Name="cmbEnum" ItemsSource="{Binding TheEnumProperty}"  />

然后,如果您在ComboBox中更改值,您的类中的属性就会更新;如果您在代码隐藏中更改值,它将更新选择。好了,这可以通过附加属性的帮助来实现:

Imports System.Reflection
Imports System.ComponentModel

Public Class EnumToComboBoxBinding

    Private Shared Combo As ComboBox
    Private Shared ComboNameList As List(Of String)
    Private Shared ComboEnumList As List(Of [Enum])

    Public Shared ReadOnly EnumItemsSourceProperty As DependencyProperty =
        DependencyProperty.RegisterAttached("EnumItemsSource",
                                            GetType([Enum]),
                                            GetType(EnumToComboBoxBinding),
                                            New FrameworkPropertyMetadata(Nothing,
                                                                          FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                          New PropertyChangedCallback(AddressOf OnEnumItemsSourceChanged)))

    ...

    Public Shared Sub OnEnumItemsSourceChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
        'Store the set enum locally
        Dim TempEnum As [Enum] = sender.GetValue(EnumItemsSourceProperty)

        ' First time run trough or the binding source has changed (last one not very likely or impossible?)
        If ComboEnumList Is Nothing OrElse Not ComboEnumList.Contains(TempEnum) Then

            ' Remove any previously handlers
            If Combo IsNot Nothing Then
                RemoveHandler Combo.SelectionChanged, AddressOf EnumValueChanged
            End If

            Combo = DirectCast(sender, ComboBox)

            'Clear the lists
            ComboNameList = New List(Of String)
            ComboEnumList = New List(Of [Enum])

            'Get all possible values for the enum type
            Dim Values = [Enum].GetValues(TempEnum.GetType)

            ' Loop trough them and store the description 
            ' and the Enum type in two separate lists
            For Each Value In Values
                ComboNameList.Add(GetDescription(Value))
                ComboEnumList.Add(Value)
            Next

            'Add a handler if you change the selected value of the ComboBox
            AddHandler Combo.SelectionChanged, AddressOf EnumValueChanged

            ' Set the ComboBox's ItemsSource to the DescriptionAttribute
            Combo.ItemsSource = ComboNameList
        End If

        ' Sync the selected value with the Property 
        Combo.SelectedIndex = ComboEnumList.IndexOf(TempEnum)
    End Sub

    Private Shared Sub EnumValueChanged(sender As Object, e As EventArgs)
        ' Selected item in the ComboBox has changes, so updates the  Enum DependencyProperty
        SetEnumItemsSource(Combo, ComboEnumList(Combo.SelectedIndex))
    End Sub

    ...

End Class

该类非常直接,我只删除了“我们现在都知道”的函数。所有Enum值都来自该属性,结果存储在两个列表中,一个用于ComboBox显示,一个用于属性中可用的实际Enum值。显示已连接,并且已将事件附加到ComboBox的选择更改。这就是全部内容,现在只需在 XAML 中键入代码即可工作!

        <ComboBox Name="cmbEnum" 
                  local:EnumToComboBoxBinding.EnumItemsSource="{Binding TheEnumProperty}"
                  ...
  />
我使用了一个小技巧来仅通过设置ComboBoxDataContext来测试单个类:
    Dim TheShowCaseClass As New TheComboBoxShowCase

    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)

         ...

        cmbEnum.DataContext = TheShowCaseClass

这个小技巧应该可以轻松地在所有情况下重用它,或者只需添加您需要的其他功能来扩展它。

可搜索的 TreeView

我浏览了一些关于TreeView的文章,偶然发现了这篇名为

我非常喜欢他提供的基于过滤器的搜索功能,我的第一反应是:我也想在我的项目中添加这个。但那并不那么容易(好吧,这并非完全正确,但如果您从一开始就考虑过,在您的情况下要实现它仍然需要做很多工作)。我决定放弃他那个花哨的搜索框,我不需要它。我只需要一个基于TextBox更改的搜索,而且如果搜索字段已经启动并运行,其他东西就可以很容易地作为事后想法来实现。

于是我开始着手为我想创建的附加属性的辅助类编写代码。我清楚地知道我需要存储TreeView中的每个项目及其子元素。我还需要绑定到每个TreeViewItems的底层类,最后是 Fredrik 提供的搜索功能。

该类还必须包含基于属性字符串的反射搜索。辅助类最终看起来像这样,并包含指向实际值的指针:

Public Class TreeViewHelperClass

    Private pCurrentTreeViewItem As TreeViewItem
    Public Property CurrentTreeViewItem() As TreeViewItem
        Get
            Return pCurrentTreeViewItem
        End Get
        Set(ByVal value As TreeViewItem)
            pCurrentTreeViewItem = value
        End Set
    End Property

    Private pBindedClass As Object
    Public Property BindedClass() As Object
        Get
            Return pBindedClass
        End Get
        Set(ByVal value As Object)
            pBindedClass = value
        End Set
    End Property

    Private pChildren As New List(Of TreeViewHelperClass)
    Public Property Children() As List(Of TreeViewHelperClass)
        Get
            Return pChildren
        End Get
        Set(ByVal value As List(Of TreeViewHelperClass))
            pChildren = value
        End Set
    End Property

    Private Function FindString(obj As Object, ByVal SearchString As String) As Boolean

        If String.IsNullOrEmpty(SearchString) Then
            Return True
        End If

        If obj Is Nothing Then Return True

        For Each p As System.Reflection.PropertyInfo In obj.GetType().GetProperties()
            If p.PropertyType = GetType(String) Then
                Dim value As String = p.GetValue(obj)
                If value.ToLower.Contains(SearchString.ToLower) Then
                    Return True
                End If
            End If
        Next

        Return False
    End Function

    Private expanded As Boolean
    Private match As Boolean = True

    Private Function IsCriteriaMatched(criteria As String) As Boolean
        Return FindString(BindedClass, criteria)
    End Function

    Public Sub ApplyCriteria(criteria As String, ancestors As Stack(Of TreeViewHelperClass))
        If IsCriteriaMatched(criteria) Then
            IsMatch = True
            For Each ancestor In ancestors
                ancestor.IsMatch = True
            Next
        Else
            IsMatch = False
        End If

        ancestors.Push(Me) ' and then just touch me
        For Each child In Children
            child.ApplyCriteria(criteria, ancestors)
        Next
        ancestors.Pop()
    End Sub

    Public Property IsMatch() As Boolean
        Get
            Return match
        End Get
        Set(value As Boolean)
            If match = value Then Return

            match = value
            If CurrentTreeViewItem IsNot Nothing Then
                If match Then
                    CurrentTreeViewItem.Visibility = Visibility.Visible
                Else
                    CurrentTreeViewItem.Visibility = Visibility.Collapsed
                End If
            End If

            OnPropertyChanged("IsMatch")
        End Set
    End Property

    Public ReadOnly Property IsLeaf() As Boolean
        Get
            Return Not Children.Any()
        End Get
    End Property
End Class

从类中可以看出,过滤器使用Visibility更改来清除不匹配的项目。它也是一种自上而下的搜索,其中所有父TreeViewItems都通过迭代先前元素的堆栈来找到。

匹配的标准现在仅通过查看绑定类中的String元素来匹配,但可以通过简单地修改搜索函数来轻松扩展它,以搜索特定属性的值。假设您正在寻找一个名为id且值为45的项,那么您可以简单地在搜索字符串中键入id==45

        Dim PropertyName As String
        Dim ValueOfProperty As String

        PropertyName = SearchString.Split("==")(0)
        ValueOfProperty = SearchString.Split("==")(1)
   

        For Each p As System.Reflection.PropertyInfo In obj.GetType().GetProperties()
            If p.CanRead Then

                    If p.Name.ToLower = PropertyName.ToLower Then

                        Dim t As Type = If(Nullable.GetUnderlyingType(p.PropertyType), p.PropertyType)
                        Dim safeValue As Object = If((ValueOfProperty Is Nothing), Nothing, Convert.ChangeType(ValueOfProperty, t))

                        'Get the value
                        Dim f = p.GetValue(obj)

                        'Its the same type
                        If safeValue IsNot Nothing Then
                            If f = safeValue Then
                                Return True
                            Else
                                Return False
                            End If
                        Else
                            ' If you end up here you have entered the wrong element type of the property
                        End If
                    End If
                Next
            End If
        Next

这是可搜索TreeView的简单部分,现在我们需要编写附加依赖属性并获取所有TreeViewItems和底层类。很明显,我们需要SearchString作为附加依赖属性,并且我们需要在属性更改时进行新的搜索。

这里更困难的问题是确保TreeView中的所有元素都可见,并且在我们尝试将项目填充到辅助类时它们都已绘制。在这里,Bea Stollniz 的博客提供了很大的帮助,所以我实现了下面的函数,现在我可以相当确定所有元素都是可见和展开的。

            ApplyActionToAllTreeViewItems(Sub(itemsControl)
                                              itemsControl.IsExpanded = True
                                              itemsControl.Visibility = Visibility.Visible
                                              DispatcherHelper.WaitForPriority(DispatcherPriority.ContextIdle)
                                          End Sub, TreeViewControl)

有关在 TreeView 中查找项的MSDN文章也(间接?)解释了如何确保元素已填充。

为了将项目填充到辅助类中,我在MSDN FAQ中找到了一个更直接地获取元素的方法。

    Private Shared Sub CreateInternalViewModelFilter(parentContainer As ItemsControl, ByRef ParentTreeItem As TreeViewHelperClass)

        For Each item As [Object] In parentContainer.Items
            Dim TreeViewItemHelperContainer As New TreeViewHelperClass()

            TreeViewItemHelperContainer.BindedClass = item
            Dim currentContainer As TreeViewItem = TryCast(parentContainer.ItemContainerGenerator.ContainerFromItem(item), TreeViewItem)
            TreeViewItemHelperContainer.CurrentTreeViewItem = currentContainer
            ParentTreeItem.Children.Add(TreeViewItemHelperContainer)

            If currentContainer IsNot Nothing AndAlso currentContainer.Items.Count > 0 Then

                If currentContainer.ItemContainerGenerator.Status <> GeneratorStatus.ContainersGenerated Then

                    ' This indicates that the TreeView isn't fully created yet. 
                    ' That means that the code should not have reached this point 

                    ' If the sub containers of current item is not ready, we need to wait until 
                    ' they are generated. 
                    AddHandler currentContainer.ItemContainerGenerator.StatusChanged, Sub()
                                                                                          CreateInternalViewModelFilter(currentContainer, TreeViewItemHelperContainer)
                                                                                      End Sub
                Else
                    ' If the sub containers of current item is ready, we can directly go to the next 
                    ' iteration to expand them. 
                    CreateInternalViewModelFilter(currentContainer, TreeViewItemHelperContainer)
                End If

            End If
        Next
    End Sub

剩下的唯一一件事就是运行实际的过滤器(或搜索):

            'The first instance is a dummy that is not connected to the TreeView, but can initiate the Search
            TreeViewHelper.Item(0).ApplyCriteria(TempSearchString, New Stack(Of TreeViewHelperClass))

用于 WPF 的 Windows 窗体风格 TreeView

这个 TreeView 起初是一个由微软的 Niel Kronlage 生成的样式,它绘制了线条并添加了一个 ToggleButton 来展开和折叠子 TreeViewItems。他还实现了一个ValueConverter来获取最后一个项目,以此来停止绘制线条。

这对于一个不添加新项目的静态TreeView来说效果很好。由于 TreeVeiw 没有办法更新其渲染,Alex P.(在同一线程中)创建了一个附加属性并在构造函数中添加了一个事件处理程序,以便集合中的更改会强制 UI 更新。

然后我们有了最后一个(在我之前)的添加,它是由 TuyenTk 完成的,并在此处的 CodeProject 上作为提示发布:“带 WinForms 风格格式的 WPF TreeView”。他做了一些样式更改,但忽略了在添加新的 TreeViewItems 后更新 UI 的附加属性。

在我实现了其他人提出的所有不同部分之后,我在过滤/搜索我的TreeView时使用了它。结果并不理想,因为 UI 没有意识到折叠的项目。我需要附加一个事件来处理 Visibility 更改。

我还稍微调整了一下布局,以提高可重用性。现在,您只需将包含样式的目录合并到 Application 中即可:

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/WinFormStyleTreeView/ExpandCollapseToggleStyle.xaml"/>
                <ResourceDictionary Source="/WinFormStyleTreeView/WinFormStyle.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>

并确保先添加ExpandCollapseToggleStyle,因为它被WinFormStyle使用。如果更改它,您将得到一个听起来相当奇怪的错误。现在可以通过以下方式单独在您的任何TreeView上实现该样式:

        <TreeView>
            <TreeView.Resources>
                <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource WinFormTreeView}"/>

                ...

            </TreeView.Resources>
        </TreeView>

在附加类中,我们的附加属性就位于此处,我们需要有一个值来指示它下方是否有任何项。如果有,则将属性IsLast设置为 false,否则设置为 true。如果不这样做,它将一直绘制线条直到到达控件中的最后一个TreeViewItem。因此,设置了IsLast依赖属性:

    Public Shared IsLastOneProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsLastOne", GetType(Boolean), GetType(TVIExtender))

    Public Shared Function GetIsLastOne(sender As DependencyObject) As Boolean
        Return CBool(sender.GetValue(IsLastOneProperty))
    End Function
    Public Shared Sub SetIsLastOne(sender As DependencyObject, isLastOne As Boolean)
        sender.SetValue(IsLastOneProperty, isLastOne)
    End Sub

但是,我们需要在集合更改时触发此事件,并且最好在构造函数Sub New()中挂接事件。通常的做法是附加一个名为 IsUsed 或类似的布尔依赖属性。这应该只在拥有附加依赖属性的对象初始化时设置一次,并且您可以有一个CallBackFunction来设置项创建时的初始绑定。

Alex P. 通过响应UseExtenderProperty的变化来实现这一点,并使用TreeViewItem作为其参数来初始化一个新的TVIExtender

    Private _item As TreeViewItem


    Public Sub New(item As TreeViewItem)
        _item = item

        Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
        AddHandler ic.ItemContainerGenerator.ItemsChanged, AddressOf OnItemsChangedItemContainerGenerator

        _item.SetValue(IsLastOneProperty, ic.ItemContainerGenerator.IndexFromContainer(_item) = ic.Items.Count - 1)
    End Sub

代码的工作原理是获取包含新构造的子项(也是TreeViewItem)的 TreeViewItem,使用ItemsContol.ItemsControlFromItemContainer。然后,它会向项更改添加一个处理程序,该处理程序在每次集合更改时触发,如果当前项是集合中的最后一项,则将IsLastproperty设置为 true。如果集合已更改,我们将回到相同的设置:

    Private Sub OnItemsChangedItemContainerGenerator(sender As Object, e As ItemsChangedEventArgs)
        Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)

        If ic IsNot Nothing Then
            _item.SetValue(IsLastOneProperty, ic.ItemContainerGenerator.IndexFromContainer(_item) = ic.Items.Count - 1)
        End If
    End Sub

到目前为止,我只解释了 Alex P. 在其附加依赖属性实现中所做的工作,并且按原样,它在更改TreeViewItemVisiblility时不会正确响应。如果可见性值从VisibleHidden(它们将以相同的方式渲染 TreeView)变为Collapsed,我们必须重新计算IsLast属性。要将事件附加到依赖属性的更改,我使用了DependencyPropertyDescriptor类。

   Private Shared VisibilityDescriptor As DependencyPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(TreeViewItem.VisibilityProperty, GetType(TreeViewItem))

我在构造函数中添加了将 VisibilityChange 绑定到一个子项的代码:

    Public Sub New(item As TreeViewItem)
         ...

        VisibilityDescriptor.AddValueChanged(_item, AddressOf VisibilityChanged)

        ...       

    End Sub

这将每次运行VisibilityChanged子项,但请注意。每次添加事件或描述符时,都不要忘记在完成后进行清理。

    Private Sub Detach()
        If _item IsNot Nothing Then
            Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
            If ic IsNot Nothing Then
                RemoveHandler ic.ItemContainerGenerator.ItemsChanged, AddressOf OnItemsChangedItemContainerGenerator
            End If

            VisibilityDescriptor.RemoveValueChanged(_item, AddressOf VisibilityChanged)
        End If
    End Sub

 现在我们有了一个每次可见性更改时都会运行的子项,我们开始编写代码:

    Private Sub VisibilityChanged(sender As Object, e As EventArgs)
        If TypeOf (sender) Is TreeView Then
            Exit Sub
        End If

        If DirectCast(_item, ItemsControl).Visibility = Visibility.Collapsed Then
            Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
            Dim Index As Integer = ic.ItemContainerGenerator.IndexFromContainer(_item)

            If Index <> 0 And _item.GetValue(IsLastOneProperty) Then
                DirectCast(ic.ItemContainerGenerator.ContainerFromIndex(Index - 1), TreeViewItem).SetValue(IsLastOneProperty, True)
            End If
        Else
            Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
            Dim Index As Integer = ic.ItemContainerGenerator.IndexFromContainer(_item)

            If Index <> 0 Then
                DirectCast(ic.ItemContainerGenerator.ContainerFromIndex(Index - 1), TreeViewItem).SetValue(IsLastOneProperty, False)
            End If
        End If
    End Sub

代码本身实际上非常简单,如果属性 IsLast 为 true,则将前一个元素的 IsLast 设置为 true,除非当前元素是集合中的唯一一个。反之,您可以将前一个元素设置为 false,而不管其值如何。这就是拥有一个可折叠项的可正常工作的TreeView所需的全部。

使用DependencyPropertyDescriptor在分离事件方面还有一个问题。我找到了Andrew Smith 2008 年的博客文章,并将其代码翻译成了 VB,它在 Unused 文件夹中。然而,2008 年是很久以前的事了,在那之后发布的MSDN上的文章仍然使用了原始方法,所以我将其保留原样。如果有人知道它是否已修复,或者它是否仍然存在问题,我真的很想知道。

添加像素着色器放大镜

几年前,我记得看到过一篇 Silverlight 文章,其中有一个我见过的最酷的放大镜,它是用像素着色器创建的。我当时没有时间深入研究代码,所以只是将该网站添加了书签并忘记了它。然后,当我为这篇文章做研究时,我遇到了这篇文章:《使用像素着色器进行 WPF 父窗口着色》,我开始思考。我还有那篇关于酷放大镜的文章的链接吗?我找到了:《Silverlight 中的行为和触发器》,所以现在就开始实现它吧。

源代码,你知道的 .fx 文件,DirectX 编译器将其编译为 .ps 文件(您可以在这里阅读有关这些内容编译的更多信息)。我将只介绍最基本的内容,以便您开始了解为了使其正常工作需要考虑哪些因素,有关更详细的审查和一些很酷的工具和程序,请参阅以下链接:

(完整!)的文件看起来像这样:

float2 center : register(C0);
float inner_radius: register(C2);
float magnification : register(c3);
float outer_radius : register(c4);

SamplerState  Input : register(S0);

float4 main( float2 uv : TEXCOORD) : COLOR
{
    float2 center_to_pixel = uv - center; // vector from center to pixel  
    float distance = length(center_to_pixel);
    float4 color;
    float2 sample_point;
    
    if(distance < outer_radius)
    {
      if( distance < inner_radius )
      {
         sample_point = center + (center_to_pixel / magnification);
      }
      else
      {
          float radius_diff = outer_radius - inner_radius;
          float ratio = (distance - inner_radius ) / radius_diff; // 0 == inner radius, 1 == outer_radius
          ratio = ratio * 3.14159; //  -pi/2 .. pi/2          
          float adjusted_ratio = cos( ratio );  // -1 .. 1
          adjusted_ratio = adjusted_ratio + 1;   // 0 .. 2
          adjusted_ratio = adjusted_ratio / 2;   // 0 .. 1
       
          sample_point = ( (center + (center_to_pixel / magnification) ) * (  adjusted_ratio)) + ( uv * ( 1 - adjusted_ratio) );
      }
    }
    else
    {
       sample_point = uv;
    }

    return tex2D( Input, sample_point );    
}

一开始,您会看到代码中有 4 个输入参数,分别名为 C0、C2、C3 和 C4,它们将链接到它们各自的依赖属性。标记为 Input 的变量是一个“图像寄存器”,它也链接到依赖属性。依赖属性如下所示:

    Public Shared ReadOnly CenterProperty As DependencyProperty =
        DependencyProperty.Register("Center",
                                    GetType(Point),
                                    GetType(Magnifier),
                                    New PropertyMetadata(New Point(0.5, 0.5),
                                                         PixelShaderConstantCallback(0)))

    Public Shared ReadOnly InnerRadiusProperty As DependencyProperty =
        DependencyProperty.Register("InnerRadius",
                                    GetType(Double),
                                    GetType(Magnifier),
                                    New PropertyMetadata(0.2, PixelShaderConstantCallback(2)))

您可以看到

PixelShaderConstantCallback

在 fx 文件中,C 变量具有相同的值。在同一个文件(Magnifier)中,我们还重置了 PixelShader 属性(它继承自 ShaderEffectBase 中的 ShaderEffect 类)。

Public MustInherit Class ShaderEffectBase
    Inherits ShaderEffect

shadereffects.PixelShader 只是一个空指针,因此它需要一个 PixelShader 的新实例:

    Sub New()
        PixelShader = New PixelShader
        PixelShader.UriSource = New Uri(AppDomain.CurrentDomain.BaseDirectory & "\ShaderSourceFiles\Magnifier.ps")

        Me.UpdateShaderValue(CenterProperty)
        Me.UpdateShaderValue(InnerRadiusProperty)
        Me.UpdateShaderValue(OuterRadiusProperty)
        Me.UpdateShaderValue(MagnificationProperty)
    End Sub

UriSource 是一个真正的麻烦,它需要找到 *.ps 的编译文件在那个特定的位置,否则您的程序将崩溃。

现在只剩下带附加依赖属性的类了,我将(照常)用一个设置为 true 的布尔值来初始化它:

    Public Shared MagnifyProperty As DependencyProperty =
        DependencyProperty.RegisterAttached("Magnify",
                                            GetType(Boolean),
                                            GetType(MagnifierOverBehavior),
                                            New FrameworkPropertyMetadata(AddressOf MagnifiedChanged))

而 MagnifiedChanged 的回调函数则附加和分离使用的处理程序:

    Public Shared Sub MagnifiedChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
        AssociatedObject = TryCast(sender, FrameworkElement)
        If AssociatedObject IsNot Nothing Then
            If e.NewValue Then
                OnAttached()
            Else
                OnDetaching()
            End If
        End If
    End Sub

这两个类正好相反:

    Private Shared Sub OnAttached()
        AddHandler AssociatedObject.MouseEnter, AddressOf AssociatedObject_MouseEnter
        AddHandler AssociatedObject.MouseLeave, AddressOf AssociatedObject_MouseLeave
        AddHandler AssociatedObject.MouseMove, AddressOf AssociatedObject_MouseMove
        AssociatedObject.Effect = magnifier
    End Sub

    Private Shared Sub OnDetaching()
        RemoveHandler AssociatedObject.MouseEnter, AddressOf AssociatedObject_MouseEnter
        RemoveHandler AssociatedObject.MouseLeave, AddressOf AssociatedObject_MouseLeave
        RemoveHandler AssociatedObject.MouseMove, AddressOf AssociatedObject_MouseMove
        AssociatedObject.Effect = Nothing
    End Sub

这加起来会导致鼠标移动部分的反应,从而启动一个 StoryBoard 来移动放大镜:

    Private Shared Sub AssociatedObject_MouseMove(sender As Object, e As MouseEventArgs)

        TryCast(AssociatedObject.Effect, Magnifier).Center = e.GetPosition(AssociatedObject)

        Dim mousePosition As Point = e.GetPosition(AssociatedObject)
        mousePosition.X /= AssociatedObject.ActualWidth
        mousePosition.Y /= AssociatedObject.ActualHeight
        magnifier.Center = mousePosition

        Dim zoomInStoryboard As New Storyboard()
        Dim zoomInAnimation As New DoubleAnimation()
        zoomInAnimation.[To] = magnifier.Magnification
        zoomInAnimation.Duration = TimeSpan.FromSeconds(0.5)
        Storyboard.SetTarget(zoomInAnimation, AssociatedObject.Effect)
        Storyboard.SetTargetProperty(zoomInAnimation, New PropertyPath(magnifier.MagnificationProperty))
        zoomInAnimation.FillBehavior = FillBehavior.HoldEnd
        zoomInStoryboard.Children.Add(zoomInAnimation)
        zoomInStoryboard.Begin()
    End Sub

放大镜可以放大任何FrameworkElement元素,并且可以通过布尔值打开和关闭。

© . All rights reserved.