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

WPF 缩放和平移自定义控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (127投票s)

2010年6月4日

MIT

31分钟阅读

viewsIcon

882894

downloadIcon

47486

检查一个自定义内容控件,该控件可用于缩放和平移其内容。

示例代码

引言

本文探讨了一个可重用的WPF自定义控件的用法和实现,该控件用于缩放和移动其内容。本文和示例代码展示了如何从XAMLC#代码中使用该控件。

主类ZoomAndPanControl派生自WPF的ContentControl类。这意味着该控件的主要用途是显示内容。在XAML中,内容控件会包裹其他UI元素。例如,内容可以是图像、地图或图表。在本文中,我使用Canvas作为内容。这个Canvas包含一些用户可以拖动的彩色矩形。

我将把本文分为两部分。第一部分展示了如何使用ZoomAndPanControl,并对三个示例项目进行了演练。如果您只想使用代码或进行试用,这应该足够了。本文的第二部分详细介绍了该控件的实现方式。如果您想对ZoomAndPanControl进行自己的修改,或者想了解如何开发一个非简单的自定义控件,这部分会很有用。

屏幕截图

此屏幕截图显示了数据绑定示例。

带有滚动条的大窗口是内容视图。工具栏包含一些按钮和一个用于更改缩放级别的滑块。它还以百分比显示当前缩放级别。如前所述,内容是一个带有彩色矩形的Canvas

左下角的小概览窗口显示了整个内容的概览。透明的黄色矩形显示了当前在视图中可见的内容部分。

假设知识

假定您已经了解C#,并对使用WPF和XAML有基本了解。

背景

在我上一篇文章中,我曾暗示我一直在开发一个流程图控件。显示和编辑流程图的工作区域可能比包含它的窗口大得多。通常,这是使用ScrollViewer来包装内容的理想场所。ScrollViewer是一个相当容易使用的类。它通过提供内容视图来处理大于自身的内容。视图可以选择性地被滚动条包围,允许用户查看内容的任何部分。

然而,我希望用户能够放大查看更多细节,或缩小查看概览。通过缩放内容来实现缩放可以通过WPF的2D变换相对容易完成。尽管让它与ScrollViewer协调工作是另一回事!

编写这种自定义控件比你想象的要难。我的第一个实现有点糟糕(好吧,不是完全糟糕——它启发了我重写代码,然后写了这篇文章)。缩放和移动代码与显示和编辑流程图的代码纠缠在一起。我当时还在学习WPF,这可能没有帮助。互联网上有一些关于如何做这类事情的例子,但我发现它们要么不够完整,没有做到我想要的所有事情,要么带有我不需要的额外代码包袱。可以说,在我为本文编写代码之前,我所拥有的就是一团糟,它不断出错,并且很难修改。

ZoomAndPanControl:它是什么,它不是什么

我写这篇文章的主要目的是尽量减少代码的复杂性。为此,ZoomAndPanControl只尝试做我需要它做的事情。特别是,我没有尝试实现任何UI虚拟化

此外,可重用控件中没有输入处理逻辑。我认为这类代码是应用程序特定的,并且很可能会改变。通用实现会增加复杂性,因此我将输入处理委托给了应用程序代码。在示例代码中,输入处理代码可以在MainWindow类(派生自Window)中找到。

我发现将缩放和移动逻辑移到一个自定义控件中,让我能够清晰地将代码从应用程序的其余部分中分离出来。结果是,两套代码都更简单、更清晰、更易于理解。

第一部分 - 示例项目演练

我包含了三个示例项目来演示ZoomAndPanControl的用法。每个示例基本上都有相同的内容:一小组用户可以拖动的彩色矩形。

  • SimpleZoomAndPanSample.zip演示了ZoomAndPanControl最简单的用法。该项目展示了如何实现左键拖动平移、简单的鼠标滚轮缩放以及使用加/减键进行缩放。

  • AdvancedZoomAndPanSample.zip在简单示例的基础上增加了更多高级功能。该项目演示了使用动画缩放来缩放到用户拖出的矩形。它具有类似Google地图的鼠标滚轮缩放功能,按退格键可以返回到前一个缩放级别。它还展示了如何使用其他UI控件(标签、按钮和滑块)来控制缩放功能。

  • DataBindingZoomAndPanSample.zip则更高级。该项目演示了如何使用简单的数据模型和数据绑定来在主窗口和概览窗口之间共享数据(彩色矩形)。概览窗口以类似Photoshop的方式显示全部内容,并有一个透明的黄色矩形,显示了视图中显示的内容范围。

  • InfiniteWorkspaceZoomAndPanSample.zip是本文发布后添加的一个新示例项目。它展示了如何使用ZoomAndPanControl构建无限工作区的概念。由于这是新增内容,我不会在文章中再次提及,但会在此处简要描述。内容画布最初设置为大小为0,0,并自动扩展以包含初始内容。当用户拖动彩色矩形时,画布会自动扩展或收缩以包含修改后的内容。在这个示例中,概览窗口已更改,以便在画布扩展/收缩时调整其缩放级别,使画布始终填充视图。在此示例中,滚动条已被移除,仅使用左键拖动平移和概览窗口来导航内容。

基础知识

ZoomAndPanControl在XAML中的用法与常规的ContentControl基本相同。它包裹了要显示的内容。内容视图显示了该内容的一部分。内容可以被缩放,也就是说,比视图大或小。用户可以通过鼠标平移或使用滚动条来移动视图。

这里快速看一下主要类及其关系(感谢StarUML)。还展示了主要的ZoomAndPanControl依赖属性和一些方法的示例。实线表示继承,虚线表示依赖。

下一个图试图说明内容视图如何映射到缩放后的内容(请原谅我业余的Photoshop技巧)

前面的图展示了本文中将要引用的各种坐标系之间的关系。相对于内容(本文中是Canvas)的坐标称为“内容坐标”。相对于视图的坐标称为“视图坐标”。“视图坐标”可以帮助您理解,但实际上并非如此,因为WPF是分辨率无关的。

要从内容坐标转换为视图坐标,XY点会通过“内容偏移量”然后“内容缩放比例”进行转换,如下所示

与任何WPF控件一样,ZoomAndPanControl具有依赖属性,用于在运行时设置和检索控件的值。

最重要的三个属性是

  • ContentScale - 这指定了缩放级别,或者更准确地说,是正在查看的内容的比例。当设置为默认值1.0时,内容以100%查看。我将此属性称为“内容缩放比例”。
  • ContentOffsetXContentOffsetY - 这些值构成了内容视图的XY偏移量。这些值以内容坐标指定。我将这两个属性统称为“内容偏移量”。

简单示例演练

为了跟随演练,您应该在Visual Studio(我使用VS 2008)中加载简单示例,然后生成并运行应用程序。

首先,让我们尝试一下用于与ZoomAndPanControl交互的输入控件。只需按加号和减号键即可放大和缩小内容。按住Shift键并单击鼠标左键或右键,或使用鼠标滚轮也可以放大和缩小内容。在空白区域单击鼠标左键并拖动即可平移内容视图。左键拖动也可用于移动彩色矩形。

现在让我们看一下在MainWindow.xamlZoomAndPanControl的用法。首先需要做的是引用包含ZoomAndPanControl的命名空间和程序集。

<Window x:Class="ZoomAndPanSample.MainWindow"
    ...
    xmlns:ZoomAndPan="clr-namespace:ZoomAndPan;assembly=ZoomAndPan"
    ...
    >

    ... main window content ...

</Window>

下一个片段显示了XAML中ZoomAndPanControl最基本的定义。

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    ...
    >

    ... content to be zoomed and panned is defined here ...

</ZoomAndPan:ZoomAndPanControl>

要显示的内容嵌入在XAML的ZoomAndPanControl内。在本文中,使用Canvas作为内容。

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    ...
    >
    <Canvas
        x:Name="content"
        ...
        >

        ... contents of the canvas defined here ...

    </Canvas>
</ZoomAndPan:ZoomAndPanControl>

如果您已经熟悉WPF,那么您会知道,在前面的片段中为Canvas分配名称“content”会生成一个MainWindow成员变量,该变量引用Canvas的实例。同样,也会生成一个“zoomAndPanControl”成员变量。稍后,我们将在C#代码中使用这些生成的成员变量。

Canvas添加WidthHeight可以设置内容的大小(以内容坐标表示)。

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    >
    <Canvas
        x:Name="content"
        Width="2000"
        Height="2000"
        ...
        >

        ... contents of the canvas defined here ...

    </Canvas>
</ZoomAndPan:ZoomAndPanControl>

请注意,前面的片段中未明确指定ContentScale的值。ContentScale的默认值为1.0,这意味着内容坐标和视图坐标的比例相同。例如,当ContentScale为1.0时,我们的内容大小在内容和视图坐标中均为2000x2000。但是,如果ContentScale设置为0.5,则视图中的大小将按比例缩小一半(50%),即1000x1000。同样,如果设置为2.0,则视图中的大小将是原来的两倍(200%),即4000x4000。

通过将ZoomAndPanControl包装在ScrollViewer中,我们可以免费获得滚动条。

<ScrollViewer
    ...
    CanContentScroll="True"
    VerticalScrollBarVisibility="Visible"
    HorizontalScrollBarVisibility="Visible"
    >
    <ZoomAndPan:ZoomAndPanControl
        x:Name="zoomAndPanControl"
        >

            ... content ...

    </ZoomAndPan:ZoomAndPanControl>
</ScrollViewer>

ZoomAndPanControl类实现了IScrollInfo接口。该接口允许控件与ScrollViewer进行紧密的关联。请注意,CanContentScroll设置为“True”。这是必需的,以指示ScrollViewer通过IScrollInfoZoomAndPanControl通信,以确定水平和垂直滚动条的偏移量及其内容的范围。我将在第二部分中更多地讨论IScrollInfo

如前所述,ZoomAndPanControl本身不处理任何用户输入。用户输入的实现委托给了MainWindow。为ZoomAndPanControl定义了所有常见鼠标操作的事件处理程序。

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    Background="LightGray"
    MouseDown="zoomAndPanControl_MouseDown"
    MouseUp="zoomAndPanControl_MouseUp"
    MouseMove="zoomAndPanControl_MouseMove"
    MouseWheel="zoomAndPanControl_MouseWheel"
    >
    <Canvas
        x:Name="content"
        Width="2000"
        Height="2000
        Background="White"
        >

        ... contents of the canvas defined here ...

    </Canvas>
</ZoomAndPan:ZoomAndPanControl>

请注意,为ZoomAndPanControlCanvas都设置了Background。主要是因为Background必须设置才能接收ZoomAndPanControl上的鼠标事件。当Background未设置时,命中测试会失败,并且不会触发鼠标事件。Background也用于突出显示内容(白色)与内容后面的背景(浅灰色)之间的区别。当您从内容中缩小视图时,您就会明白我的意思。

MainWindow.xaml.cs中的鼠标事件处理程序通过直接设置ZoomAndPanControl属性来执行缩放和平移。下一个片段说明了左键拖动如何更新内容偏移量。

private void zoomAndPanControl_MouseMove(object sender, MouseEventArgs e) 
{ 
    if (mouseHandlingMode == MouseHandlingMode.Panning) 
    { 
        Point curContentMousePoint = e.GetPosition(content); 
        Vector dragOffset = curContentMousePoint - origContentMousePoint; 
        zoomAndPanControl.ContentOffsetX -= dragOffset.X; 
        zoomAndPanControl.ContentOffsetY -= dragOffset.Y; 
        e.Handled = true; 
    } // ... other mouse input handling ... 
} 

计算鼠标拖动的距离并将其分配给dragOffset。然后使用此值计算新的内容偏移量。请注意,dragOffset是在内容坐标中计算的,因为我们正在处理相对于内容的点。如果您注意到了,您可能会想知道前面的片段中的代码实际上是如何工作的?当然,如果origContentMousePoint是在zoomAndPanControl_MouseDown中初始化的,那么当鼠标从原始点进一步拖动时,dragOffset会越来越大,导致平移越来越快。乍一看,情况似乎是这样,但您必须考虑到内容本身正在被移动(实际上是通过WPF 2D变换系统进行平移)。作为平移的一部分,内容会随着鼠标光标一起移动,因此鼠标悬停的内容点永远不会远离原始点。这是因为当光标移动得足够远以至于调用zoomAndPanControl_MouseMove时,内容会移动,将原始点拉回到当前鼠标光标的位置。

缩放是通过更新ContentScale属性来实现的。在MainWindow.xaml.cs中,ZoomOutZoomIn方法会响应各种输入事件(加/减键、鼠标滚轮和Shift-左键/右键单击)。这些方法只是将ContentScale增加或减少一个小的量。例如,ZoomOut如下所示。

private void ZoomOut()
{
    zoomAndPanControl.ContentScale -= 0.1;
}

在这些代码示例中,鼠标滚轮仅用于放大和缩小。执行工作的事件处理程序是zoomAndPanControl_MouseWheel。还有另一种处理鼠标滚轮输入的方法——即使用它来平移视图,就像标准的ScrollViewer一样。要使鼠标滚轮输入以这种方式工作,请将ZoomAndPanControlIsMouseWheelScrollingEnabled属性设置为true。此外,您不应处理ZoomAndPanControlMouseWheel事件,也就是说,您不需要zoomAndPanControl_MouseWheel,它存在于简单示例中。

简单示例仅具有有限的功能。它将自己限制于直接操作内容偏移量和内容缩放比例。由于这些是依赖属性,因此WPF动画系统可用于对它们进行动画处理。但是,为了方便起见,ZoomAndPanControl提供了一些执行动画缩放和平移操作的方法。我们将在接下来的几个部分中介绍这些方法。

高级示例演练

在本节中,您应该在Visual Studio中加载高级示例,然后生成并运行应用程序。

高级示例具有我们在简单示例中看到的相同功能。此外,它还使用方便的ZoomAndPanControl方法来执行动画缩放和平移。

总结一下,新功能包括:

  • 一个显示当前缩放级别(以百分比显示)的标签
  • 一个用于选择当前缩放级别的滑块
  • 一个包含各种缩放操作按钮的工具栏
  • 按退格键可跳回前一个缩放级别
  • 双击可将点击位置居中
  • 拖动缩放;以及
  • 类似Google地图的鼠标滚轮缩放

我将先简要介绍滑块和按钮等较简单的功能,然后再讨论更复杂的功能:拖动缩放和类似Google地图的缩放。

工具栏中的标签以百分比显示当前缩放级别。在MainWindow.xaml中,您将看到ScaleToPercentage转换器用于将ContentScale中的缩放值转换为标签中显示的百分比值。

工具栏中的Slider用于更改缩放级别,它还使用ScaleToPercentage转换器。滑块的Value数据绑定ContentScale

<Slider
    ...
    Value="{Binding ElementName=zoomAndPanControl, Path=ContentScale,
	Converter={StaticResource scaleToPercentConverter}}"
    />

我可以跳过工具栏中的放大/缩小按钮——它们只是调用了简单示例演练中已经讨论过的ZoomInZoomOut方法。

“填充”和“100%”按钮显示了动画缩放的第一个示例。例如,Fill_Executed是用户单击“填充”按钮时调用的方法。它调用AnimatedScaleToFit

private void Fill_Executed(object sender, ExecutedRoutedEventArgs e)
{
    SavePrevZoomRect();

    zoomAndPanControl.AnimatedScaleToFit();
}

AnimatedScaleToFit启动一个动画,该动画会放大或缩小,以便整个内容完全适合视图。

请注意,Fill_Executed还会调用SavePrevZoomRect。此方法保存当前的视图矩形和内容缩放比例。

private void SavePrevZoomRect()
{
    prevZoomRect = new Rect(zoomAndPanControl.ContentOffsetX,
	zoomAndPanControl.ContentOffsetY, zoomAndPanControl.ContentViewportWidth,
	zoomAndPanControl.ContentViewportHeight);
    prevZoomScale = zoomAndPanControl.ContentScale;
    prevZoomRectSet = true;
}

当用户按下退格键时,将调用JumpBackToPrevZoom,该函数通过调用AnimatedZoomTo来跳回到前一个缩放级别。将先前保存的视图矩形和内容缩放比例作为参数传递。

private void JumpBackToPrevZoom()
{
    zoomAndPanControl.AnimatedZoomTo(prevZoomScale, prevZoomRect);

    ClearPrevZoomRect();
}

双击内容会在视图中将点击位置居中。这是另一个利用动画方法的功能,在本例中是通过调用AnimatedSnapTo

现在我已经讨论了某些较简单功能的后端功能,接下来我将介绍最有趣的功能:拖动缩放和类似Google地图的缩放。

拖动缩放功能允许用户按住Shift键并左键拖出一个矩形。然后ZoomAndPanControl会放大,直到该矩形填满整个视图。该矩形的视觉表示是一个Border,它嵌套在其自己的Canvas中,位于内容内。默认情况下,此Border是隐藏的。

<Canvas
    x:Name="dragZoomCanvas"
    Visibility="Collapsed"
    >
    <Border
        x:Name="dragZoomBorder"
        BorderBrush="Black"
        BorderThickness="1"
        Background="Silver"
        CornerRadius="1"
        Opacity="0"
        />
</Canvas>

当用户开始拖出矩形时,Border变为可见。

public void InitDragZoomRect(Point pt1, Point pt2)
{
    SetDragZoomRect(pt1, pt2);

    dragZoomCanvas.Visibility = Visibility.Visible;
    dragZoomBorder.Opacity = 0.5;
}

SetDragZoomRect的调用根据参数pt1pt2设置Border的位置和大小。当用户继续拖出矩形时,SetDragZoomRect会被反复调用。

private void zoomAndPanControl_MouseMove(object sender, MouseEventArgs e)
{
    ... handle other mouse handling modes ...

    else if (mouseHandlingMode == MouseHandlingMode.DragZooming)
    {
        Point curContentMousePoint = e.GetPosition(content);
            SetDragZoomRect(origContentMouseDownPoint, curContentMousePoint);
        e.Handled = true;
        }
    }

如果您查看SetDragZoomRect的代码,您会发现如果用户从左侧或向上拖出矩形而不是从右侧或向下拖动,它负责反转pt1pt2

当用户完成拖出矩形时,将调用AnimatedZoomTo

private void ApplyDragZoomRect()
{
    SavePrevZoomRect();

    double contentX = Canvas.GetLeft(dragZoomBorder);
    double contentY = Canvas.GetTop(dragZoomBorder);
    double contentWidth = dragZoomBorder.Width;
    double contentHeight = dragZoomBorder.Height;
    zoomAndPanControl.AnimatedZoomTo(new Rect
		(contentX, contentY, contentWidth, contentHeight));

    FadeOutDragZoomRect();
}

AnimatedZoomTo执行动画缩放,使拖出的矩形填满视图。另请注意对FadeOutDragZoomRect的调用。这会启动一个动画,该动画会淡出Border并将其恢复到默认的隐藏状态。

另一个值得一提的高级功能是类似Google地图的缩放。这是通过ZoomAboutPoint方法实现的。此方法在缩放时会保持“缩放焦点”锁定在视图中的同一位置。

private void ZoomOut(Point contentZoomCenter)
{
    zoomAndPanControl.ZoomAboutPoint
	(zoomAndPanControl.ContentScale - 0.1, contentZoomCenter);
}

当Shift左键/右键单击和鼠标滚轮滚动时,会调用此相同方法。在这两种情况下,传递的contentZoomCenter参数都设置为鼠标光标下的位置。锁定缩放焦点意味着我们可以放大和缩小,鼠标光标下的点在缩放过程中保持在鼠标光标下。

现在我们完成了对高级示例的查看。我介绍了许多动画缩放方法,完整的列表请参见ZoomAndPanControl 方法部分。现在,让我们继续讨论数据绑定示例。

数据绑定示例演练

此项目的目的是展示如何使用数据模型数据绑定在主窗口和概览窗口之间共享内容。主窗口与高级示例中的相同。它显示了我们可以缩放和平移的内容视图。概览窗口是该项目的新增内容,它显示了全部内容的整体视图。它显示一个透明的黄色矩形,该矩形显示了主窗口对内容的视图的位置和大小。

简单和高级项目都使用一个简单的Canvas作为我们内容的容器。内容,即彩色矩形,是静态嵌入在XAML中的。既然我们正在使用数据模型在视图之间共享内容,我们就需要用一个支持数据绑定的控件替换Canvas。我选择使用ListBox,部分原因在于其良好的数据绑定支持,也因为我想演示如何将其选择逻辑重用于也可以缩放和平移的内容。例如,如果您单击其中一个彩色矩形,它将被选中,并显示一个蓝色边框。然而,仍然使用Canvas,但它现在嵌套在ListBox中。ListBox绑定到数据源,并使用从数据模板生成的UI元素填充Canvas

在Visual Studio中加载数据绑定示例。数据模型位于DataModel.cs中。为了方便起见,DataModel是一个单例类。它有一个Rectangles属性,这是一个RectangleData对象列表。此属性是将在主窗口和概览窗口中的列表框填充的数据源。

让我们看一下概览窗口的代码。打开OverviewWindow.xaml,您会看到它包含一个ZoomAndPanControl。请注意,SizeChanged事件已处理。

<ZoomAndPan:ZoomAndPanControl
    x:Name="overview"
    SizeChanged="overview_SizeChanged"
    >

    ... overview content ...

</ZoomAndPan:ZoomAndPanControl>

OverviewWindow.xaml.csoverview_SizeChanged的实现调用了ZoomAndPanControl上的ScaleToFit。每当用户调整概览窗口大小时,内容都会被重新缩放以适应其整体。

private void overview_SizeChanged(object sender, SizeChangedEventArgs e)
{
    overview.ScaleToFit();
}

接下来,请看MainWindow.xaml以及如何使用ListBox作为内容。ListBoxItemsSource属性已数据绑定到数据模型的Rectangles属性。

<ListBox
    x:Name="content"
    ...
    ItemsSource="{Binding Source={x:Static local:DataModel.Instance},
	Path=Rectangles}"
    ...
    />

ListBox被重新样式化以提供一个新的视觉模板

<ListBox
    x:Name="content"
        ...
    ItemsSource="{Binding Source={x:Static local:DataModel.Instance},
	Path=Rectangles}"
    Style="{StaticResource noScrollViewerListBoxStyle}"
    ...
    />

noScrollViewerListBoxStyle指定的替代视觉模板是没有嵌入ScrollViewer的模板。

<Style x:Key="noScrollViewerListBoxStyle" TargetType="ListBox">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBox">
                <Canvas
                    Background="{TemplateBinding Background}"
                    IsItemsHost="True"
                    />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

默认视觉模板中的ScrollViewer是多余的,因为我们在MainWindow.xaml中的ZoomAndPanControl周围已经有一个ScrollViewer。请注意,替代视觉模板定义了Canvas作为ListBox的面板,这就是为什么IsItemsHost设置为True的原因。

还通过设置ItemContainerStyle为每个ListBoxItem指定了一个样式。

<ListBox
    x:Name="content"
        ...
    ItemsSource="{Binding Source={x:Static local:DataModel.Instance},
	Path=Rectangles}"
    Style="{StaticResource noScrollViewerListBoxStyle}"
    ItemContainerStyle="{StaticResource listBoxItemStyle}"
    />

listBoxItemStyle包含用于将每个列表框项定位在Canvas内的数据绑定。

<Style
    x:Key="listBoxItemStyle"
    TargetType="ListBoxItem"
    >
    <Setter
        Property="Canvas.Left"
        Value="{Binding X}"
        />
    <Setter
        Property="Canvas.Top"
        Value="{Binding Y}"
        />

    ...

</Style> 

样式还定义了一个Border,用于显示项目何时被选中。通常,Border是透明的,因此不可见。但是,当IsSelected设置为true时,触发器会将Border更改为蓝色。

<Style
    x:Key="listBoxItemStyle"
    TargetType="ListBoxItem"
    >

    ...

    <Setter
        Property="IsSelected"
        Value="{Binding IsSelected}"
        />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
                <Border
                    Name="Border"
                    BorderThickness="1"
                    Padding="2"
                    >
                    <ContentPresenter />
                </Border>
                <ControlTemplate.Triggers>
                    <!--
                    When the ListBoxItem is selected draw a
				simple blue border around it.
                    -->
                    <Trigger Property="IsSelected" Value="true">
                        <Setter
                            TargetName="Border"
                            Property="BorderBrush"
                            Value="Blue"
                            />
                    </Trigger>
              </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

现在我们来检查概览窗口中那个透明的黄色矩形的实现,该矩形显示了内容视图的位置和范围。再次查看OverviewWindow.xaml,我们可以看到它是一个Thumb,其视觉模板设置为透明的黄色Border

<Canvas>
    <Thumb
        x:Name="overviewZoomRectThumb"
        ...
        Opacity="0.5"
        >
        <Thumb.Template>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border
                    ...
                    Background="Yellow"
                    />
            </ControlTemplate>
        </Thumb.Template>
    </Thumb>
</Canvas>                           

使用Thumb是因为它方便地提供了DragDelta事件。DragDelta允许我们响应用户拖动Thumb

<Canvas>
    <Thumb
        x:Name="overviewZoomRectThumb"
            ...
        DragDelta="overviewZoomRectThumb_DragDelta"
        Opacity="0.5"
        >
        <Thumb.Template>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border
                        ...
                    Background="Yellow"
                    />
            </ControlTemplate>
        </Thumb.Template>
    </Thumb>
</Canvas>

事件处理程序在用户拖动黄色矩形时平移视图。它通过简单地更新ThumbCanvas位置来实现。

private void overviewZoomRectThumb_DragDelta
		(object sender, DragDeltaEventArgs e)
{
    double newContentOffsetX = Math.Min(Math.Max(0.0, Canvas.GetLeft
	(overviewZoomRectThumb) + e.HorizontalChange), DataModel.Instance.ContentWidth -
	DataModel.Instance.ContentViewportWidth);
    Canvas.SetLeft(overviewZoomRectThumb, newContentOffsetX);

    double newContentOffsetY = Math.Min(Math.Max(0.0, Canvas.GetTop
	(overviewZoomRectThumb) + e.VerticalChange),
	DataModel.Instance.ContentHeight - DataModel.Instance.ContentViewportHeight);
    Canvas.SetTop(overviewZoomRectThumb, newContentOffsetY);
}

您应该注意到,在前面的片段中,内容偏移量被限制在有效范围内。对于X偏移量,范围是从0.0(ContentWidth - ContentViewportWidth)(都是DataModel的成员),Y偏移量也使用类似的公式。

通过更新ThumbCanvas位置来平移视图是怎么实现的?因为Thumb的位置和大小都绑定到数据模型。

<Canvas>
    <Thumb
        x:Name="overviewZoomRectThumb"
        Canvas.Left="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentOffsetX, Mode=TwoWay}"
            Canvas.Top="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentOffsetY, Mode=TwoWay}"
            Width="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentViewportWidth}"
            Height="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentViewportHeight}"
        DragDelta="overviewZoomRectThumb_DragDelta"
        Opacity="0.5"
        >
        <Thumb.Template>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border
                    ...
                    Background="Yellow"
                    />
            </ControlTemplate>
        </Thumb.Template>
    </Thumb>
</Canvas>

回到MainWindow.xaml,我们可以看到主窗口中内容视图的位置和范围也绑定到了数据模型。

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    ContentScale="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentScale, Mode=TwoWay}"
    ContentOffsetX="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentOffsetX, Mode=TwoWay}"
    ContentOffsetY="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentOffsetY, Mode=TwoWay}"
    ContentViewportWidth="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentViewportWidth, Mode=OneWayToSource}"
    ContentViewportHeight="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentViewportHeight, Mode=OneWayToSource}"
    ...
    >

    ... content defined here ...

</ZoomAndPan:ZoomAndPanControl>

几段文字之前,我提到了DataModelContentWidth属性。还有一个匹配的ContentHeight属性。这些属性定义了内容的大小。在MainWindow.xaml中,我们可以看到WidthHeight属性已绑定到数据模型属性。

<ZoomAndPan:ZoomAndPanControl
    ...
    >

    <Grid
        Width="{Binding Source={x:Static local:DataModel.Instance}, Path=ContentWidth}"
        Height="{Binding Source={x:Static local:DataModel.Instance}, Path=ContentHeight}"
        >

        ... content defined here ...

    </Grid>
</ZoomAndPan:ZoomAndPanControl>

这结束了示例项目的演练。数据绑定,如果您还不熟悉,可以说是很难掌握的主题,希望您能坚持到这里!我想要展示的主要内容是,数据绑定是将主窗口视图和概览窗口保持同步的好方法。

接下来的两个部分是对ZoomAndPanControl属性和方法的总结。之后,我们将进入第二部分,讨论ZoomAndPanControl的实现。

ZoomAndPanControl 属性

本节总结了ZoomAndPanControl依赖属性

名称 描述
ContentScale 缩放级别,或者更准确地说,是正在查看的内容的比例。
MinContentScale, MaxContentScale ContentScale的有效值范围。
ContentOffsetX, ContentOffsetY 内容视图的XY偏移量(以内容坐标表示)。
AnimationDuration 通过调用AnimatedZoomTo和其他动画方法启动的缩放和平移动画的持续时间(以秒为单位)。
ContentZoomFocusX, ContentZoomFocusY 当前具有缩放焦点的(以内容坐标表示的)内容中的偏移量。每当视图平移以及调用AnimatedZoomTo或其他动画方法时,它都会自动更新。
ViewportZoomFocusX, ViewportZoomFocusY 当前具有缩放焦点的(以视图坐标表示的)视图中的偏移量。它通常设置为视图的中心,但在调用AnimatedZoomTo或其他动画方法时会自动更新。
ContentViewportWidth, ContentViewportHeight 视图的宽度和高度,但以内容坐标指定。每当视图调整大小时,它们都会自动更新。
IsMouseWheelScrollingEnabled 设置为true以启用控件响应鼠标滚轮输入来平移视图。默认设置为false

实现了以下IScrollInfo属性(尽管它们不是依赖属性)

名称 描述
HorizontalOffset, VerticalOffset 视图的XY偏移量(以缩放后的内容坐标表示)。
ViewportWidth, ViewportHeight 视图的宽度和高度(以视图坐标表示)。
ExtentWidth, ExtentHeight 内容的宽度和高度(以缩放后的内容坐标表示)。

ZoomAndPanControl 方法

ZoomAndPanControl包含许多执行动画和非动画缩放和平移的方法。其中一些方法已经讨论过,而另一些方法则没有。本节总结了所有这些方法。

注意:动画持续时间可以通过设置AnimationDuration属性的值来设置。

所有方法的RectanglePoint参数均以内容坐标指定。

名称 描述
AnimatedSnapTo(Point contentPoint)
SnapTo(Point contentPoint) 
将视图的位置调整到居中于特定点(不改变内容缩放比例)。
AnimatedZoomTo(double contentScale)
ZoomTo(double contentScale) 
缩放到指定的内容缩放比例。
AnimatedZoomTo(Rect contentRect)
ZoomTo(Rect contentRect) 
缩放以使指定矩形适合视图。
AnimatedZoomTo(double newScale, Rect contentRect) 
这是AnimatedZoomTo的一个特殊版本,它指定了要缩放到的内容矩形,并额外指定了缩放动画完成后最终的内容缩放比例。这用于跳回到先前已知的确切内容缩放比例,以避免在缩放动画期间更新内容偏移量时出现舍入误差。
AnimatedZoomAboutPoint
(double newContentScale, Point contentZoomFocus)
ZoomAboutPoint
(double newContentScale, Point contentZoomFocus) 
缩放到指定的内容缩放比例。内容缩放焦点保持锁定在其在视图中的当前位置。此方法用于实现类似Google地图的缩放。
AnimatedScaleToFit()
ScaleToFit() 
缩放内容,使其完全适合视图。

第二部分 - ZoomAndPanControl 内部

本文的这一部分将介绍ZoomAndPanControl的实现方式。

主类ZoomAndPanControl定义在ZoomAndPanControl.cs中,并派生自ContentControl

public class ZoomAndPanControl : ContentControl, IScrollInfo
{
    // ...
}

如您所见,ZoomAndPanControl还实现了IScrollInfo,但我直到第二部分末尾才会再次提及。

作为一个自定义控件,它可以被重新样式化以定制或替换默认UI。ZoomAndPanControl的默认视觉模板ZoomAndPan\Themes\Generic.xaml中找到的WPF样式定义。XAML定义很简单,只包含一个名为PART_Content命名部分

    <Style
        TargetType="{x:Type local:ZoomAndPanControl}"
        >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ZoomAndPanControl}">
                    <Border
                        ...
                        >
                        <ContentPresenter x:Name="PART_Content"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

PART_Content定义为一个ContentPresenter。正是这个UI元素显示(或呈现)内容。其RenderTransform用于缩放和翻译内容。

与所有自定义控件一样,ZoomAndPanControl通过在静态类构造函数中调用OverrideMetadata将其与Style关联起来。

public class ZoomAndPanControl : ContentControl, IScrollInfo
{
    ...

    static ZoomAndPanControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ZoomAndPanControl), 
		new FrameworkPropertyMetadata(typeof(ZoomAndPanControl)));
    }

    ...
}

开发自定义控件时,请记住在Assembly.cs中添加以下内容:

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None,
    ResourceDictionaryLocation.SourceAssembly
)]

这对于在将视觉模板应用于控件时找到Generic.xaml是必需的。创建新自定义控件时,我经常忘记添加此内容!

ZoomAndPanControl中的OnApplyTemplate方法在视觉模板应用到控件后被调用。在这里,PART_Content被检索并缓存以备后用。

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    content = this.Template.FindName("PART_Content", this) as FrameworkElement;

    ...
}

请注意,PART_Content仅作为FrameworkElement被引用。我们实际上不需要使用任何ContentPresenter的属性或方法,因此使用基类来引用内容。

WPF的2D变换用于缩放和翻译内容。实例化的ScaleTransformTranslateTransform被添加到TransformGroup中。然后将其分配给RenderTransform

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    content = this.Template.FindName("PART_Content", this) as FrameworkElement;
    if (content != null)
    {
        this.contentScaleTransform = 
		new ScaleTransform(this.ContentScale, this.ContentScale);
        this.contentOffsetTransform = new TranslateTransform();
        UpdateTranslationX();
        UpdateTranslationY();

        TransformGroup transformGroup = new TransformGroup();
        transformGroup.Children.Add(this.contentOffsetTransform);
        transformGroup.Children.Add(this.contentScaleTransform);
        content.RenderTransform = transformGroup;
    }
}

这些变换与ContentScaleContentOffsetXContentOffsetY的当前值保持同步。每当这些属性的值发生变化时,“属性更改”事件处理程序就会执行代码来更新缓存的变换。例如,ContentOffsetX_PropertyChanged调用UpdateTranslationX,该函数根据ContentOffsetX的当前值更新contentOffsetTransform的X坐标。

private static void ContentOffsetX_PropertyChanged
		(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    c.UpdateTranslationX();

    ...
}

UpdateTranslationX实际上以两种方式之一重新计算contentOffsetTransform.X。当内容完全适合视图时(在此例中是水平轴),X平移的计算方式是将内容居中于视图。否则,当内容不适合视图时,X平移的计算仅来自ContentOffsetX

private void UpdateTranslationX()
{
    if (this.contentOffsetTransform != null)
    {
        double scaledContentWidth = this.unScaledExtent.Width * this.ContentScale;
        if (scaledContentWidth < this.ViewportWidth)
        {
            //
            // 1st case: When the content can fit entirely within the viewport, 
            // center it.
            //
            this.contentOffsetTransform.X = -this.ContentOffsetX + 
		((this.ContentViewportWidth - this.unScaledExtent.Width) / 2);
        }
        else
        {
            //
            // 2nd case: When the content doesn't fit within the viewport.
            //
            this.contentOffsetTransform.X = -this.ContentOffsetX;
        }
    }
}

ContentOffsetY_PropertyChanged的代码类似。而ContentScale_PropertyChanged则做了更多的工作。

首先,它根据ContentScale更新contentScaleTransform

private static void ContentScale_PropertyChanged
	(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    if (c.contentScaleTransform != null)
    {
        c.contentScaleTransform.ScaleX = c.ContentScale;
        c.contentScaleTransform.ScaleY = c.ContentScale;
    }

    ...
}

然后它调用UpdateContentViewportSize

private static void ContentScale_PropertyChanged
	(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    if (c.contentScaleTransform != null)
    {
        c.contentScaleTransform.ScaleX = c.ContentScale;
        c.contentScaleTransform.ScaleY = c.ContentScale;
    }

    c.UpdateContentViewportSize();

    ...
}

UpdateContentViewportSize首先计算内容坐标下的视图大小。

private void UpdateContentViewportSize()
{
    ContentViewportWidth = ViewportWidth / ContentScale;
    ContentViewportHeight = ViewportHeight / ContentScale;

    ...
}

UpdateContentViewportSize然后计算并缓存一些值,这些值表示“受限内容视图大小”。它们被设置为内容坐标下的视图大小,但实际上它们最多达到内容的大小。

private void UpdateContentViewportSize()
{
    ContentViewportWidth = ViewportWidth / ContentScale;
    ContentViewportHeight = ViewportHeight / ContentScale;

    constrainedContentViewportWidth = 
		Math.Min(ContentViewportWidth, unScaledExtent.Width);
    constrainedContentViewportHeight = 
		Math.Min(ContentViewportHeight, unScaledExtent.Height);

    ...
}

ContentOffsetXContentOffsetY“coerce”回调使用缓存的“受限内容视图大小”来将这些属性的值保持在内容的可见区域的有效范围内。例如,ContentOffsetX_Coerce如下所示。

    private static object ContentOffsetX_Coerce(DependencyObject d, object baseValue)
    {
        ZoomAndPanControl c = (ZoomAndPanControl)d;
        double value = (double)baseValue;
        double minOffsetX = 0.0;
        double maxOffsetX = Math.Max(0.0, c.unScaledExtent.Width - 
			c.constrainedContentViewportWidth);
        value = Math.Min(Math.Max(value, minOffsetX), maxOffsetX);
        return value;
    }

UpdateContentViewportSize中的最后两行代码更新contentOffsetTransform。当内容适合视图时,这会导致内容在视图大小变化时居中于视图。

private void UpdateContentViewportSize()
{
    ContentViewportWidth = ViewportWidth / ContentScale;
    ContentViewportHeight = ViewportHeight / ContentScale;

    constrainedContentViewportWidth = 
		Math.Min(ContentViewportWidth, unScaledExtent.Width);
    constrainedContentViewportHeight = 
		Math.Min(ContentViewportHeight, unScaledExtent.Height);
		
    UpdateTranslationX();    
        UpdateTranslationY();
    }

现在,最后回到ContentScale_PropertyChanged,内容偏移量被有条件地重新计算。enableContentOffsetUpdateFromScale仅在缩放动画正在进行时(例如,围绕点缩放(类似Google地图的缩放)或缩放到用户拖出的特定矩形)才设置为true。计算涉及视图缩放焦点和内容缩放焦点。启用时,此代码将两个焦点点锁定在一起。

private static void ContentScale_PropertyChanged
		(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    if (c.contentScaleTransform != null)
    {
        c.contentScaleTransform.ScaleX = c.ContentScale;
        c.contentScaleTransform.ScaleY = c.ContentScale;
    }

    c.UpdateContentViewportSize();

    if (c.enableContentOffsetUpdateFromScale)
    {
        ...

        double viewportOffsetX = c.ViewportZoomFocusX - (c.ViewportWidth / 2);
        double viewportOffsetY = c.ViewportZoomFocusY - (c.ViewportHeight / 2);
        double contentOffsetX = viewportOffsetX / c.ContentScale;
        double contentOffsetY = viewportOffsetY / c.ContentScale;
        c.ContentOffsetX = (c.ContentZoomFocusX - 
		(c.ContentViewportWidth / 2)) - contentOffsetX;
        c.ContentOffsetY = (c.ContentZoomFocusY - 
		(c.ContentViewportHeight / 2)) - contentOffsetY;

        ...
    }

    ...
}

高级和数据绑定示例中的拖动缩放功能使用AnimatedZoomTo方法。现在我们将看看这个方法,以研究动画缩放是如何实现的。

首先,让我们看看MainWindow.xaml.csAnimatedZoomTo是如何被调用的。

private void ApplyDragZoomRect()
{
    ...
    
    double contentX = ...
    double contentY = ...
    double contentWidth = ...
    double contentHeight = ...
    zoomAndPanControl.AnimatedZoomTo(new Rect
		(contentX, contentY, contentWidth, contentHeight));

        ...
}

AnimatedZoomTo被传递一个矩形,该矩形指定要缩放到的内容区域。运行动画,使内容缩放比例和内容偏移量发生变化,以便矩形适合视图。

内部,AnimatedZoomTo计算一个新的内容缩放比例,该比例源自传入的矩形。随后调用内部辅助方法AnimatedZoomPointToViewportCenter。此方法被传递新的内容缩放比例和矩形中心,这是缩放将聚焦的内容位置。

public void AnimatedZoomTo(Rect contentRect)
{
    double scaleX = this.ContentViewportWidth / contentRect.Width;
    double scaleY = this.ContentViewportHeight / contentRect.Height;
    double newScale = this.ContentScale * Math.Min(scaleX, scaleY);

    AnimatedZoomPointToViewportCenter(newScale, 
	new Point(contentRect.X + (contentRect.Width / 2), contentRect.Y + 
	(contentRect.Height / 2)), null);
}

AnimatedZoomPointToViewportCenter首先取消任何当前动画。这是通过对可能已有动画的每个依赖属性调用CancelAnimation来实现的。

private void AnimatedZoomPointToViewportCenter
	(double newContentScale, Point contentZoomFocus, EventHandler callback)
{
    ...

    AnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);

    ...
}

CancelAnimation是我AnimationHelper类中的一个方法。该类包含一些简单的包装器,使WPF动画系统更容易使用。

接下来确定缩放焦点点。指定内容缩放焦点的点作为参数传递。然而,视图缩放焦点是通过将内容缩放焦点转换为视图坐标来计算的。

private void AnimatedZoomPointToViewportCenter
	(double newContentScale, Point contentZoomFocus, EventHandler callback)
{
    ...

    AnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);

    ContentZoomFocusX = contentZoomFocus.X;
    ContentZoomFocusY = contentZoomFocus.Y;
    ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale;
    ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale;

    ...
}

最后,执行依赖属性动画以执行缩放。这是通过调用StartAnimation来实现的。在动画进行过程中,enableContentOffsetUpdateFromScale被设置为true,以便启用ContentScale_PropertyChanged中的锁定焦点点的代码。传递给StartAnimation的匿名函数在动画完成后被调用,并将enableContentOffsetUpdateFromScale重置为false

private void AnimatedZoomPointToViewportCenter
	(double newContentScale, Point contentZoomFocus, EventHandler callback)
{
    ...

    AnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);

    ContentZoomFocusX = contentZoomFocus.X;
    ContentZoomFocusY = contentZoomFocus.Y;
    ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale;
    ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale;

    enableContentOffsetUpdateFromScale = true;

    AnimationHelper.StartAnimation(this, ContentScaleProperty, 
			newContentScale, AnimationDuration,
            delegate(object sender, EventArgs e)
            {
                enableContentOffsetUpdateFromScale = false;

                if (callback != null)
                {
                    callback(this, EventArgs.Empty);
                }
            });

    AnimationHelper.StartAnimation(this, ViewportZoomFocusXProperty, 
			ViewportWidth / 2, AnimationDuration);
    AnimationHelper.StartAnimation(this, ViewportZoomFocusYProperty, 
			ViewportHeight / 2, AnimationDuration);
}

这似乎是一种迂回且不直观的动画缩放实现方式,我将尝试解释我的想法。从代码中可以看出,ContentScale从当前值动画到新值。可能不清楚为什么视图缩放焦点被动画化。ViewportZoomFocusXViewportZoomFocusY跟踪视图中当前正在缩放的点。通常,它被设置为视图的中心,这意味着当我们按下加号或减号按钮时,我们会以视图中心为焦点进行放大或缩小。然而,在使用类似Google地图的缩放时,视图缩放焦点被设置为鼠标光标的位置。如前所述,ContentScale_PropertyChanged中的代码在缩放进行过程中将视图缩放焦点锁定在内容缩放焦点上,这就是类似Google地图的缩放工作方式。

事实证明,缩放焦点锁定对于实现拖动缩放也很有效。视图缩放焦点从当前值动画到视图中心。由于视图缩放焦点和内容缩放焦点被锁定在一起,因此此动画的效果是移动内容缩放焦点。由于ContentScale_PropertyChanged根据内容缩放焦点计算内容偏移量,因此这会在内容缩放比例发生变化时平移内容视图。我尝试了多种方法来实现动画以缩放到矩形,但这种实现与类似Google地图的缩放很好地契合,并且可以实现更流畅的动画缩放。

最后要提的是IScrollInfo。此接口允许嵌入ScrollViewer中的控件与该ScrollViewer进行通信。这是ScrollViewer确定滚动条位置和范围的方式。我的IScrollInfo方法的实现可以在ZoomAndPanControl_IScrollInfo.cs中找到。该文件包含ZoomAndPanControl部分实现,其中仅包含IScrollInfo所需的方法和属性。要理解IScrollInfo的工作原理,我将引导您参考以下已有文章:WPF教程 - 实现IScrollInfoAvalon中的IScrollInfo 第一部分第二部分第三部分第四部分

结论

本示例解释了一个可重用的WPF自定义控件,该控件可以对通用内容进行缩放和平移。在此过程中,我们涉及了WPF的许多非简单领域,如动画、2D变换、自定义控件和IScrollInfo接口的实现。开发本文提出的想法和代码花费了很多时间,我希望它能对他人有所帮助。

如开头所述,我没有尝试实现任何UI虚拟化。也许这将是未来文章的主题。我欢迎对代码的反馈和改进。感谢您阅读本文。

更新

  • 08/06/2010 
    • 根据Paul Selormey的反馈,我修改了代码,将内容偏移量限制在内容的可视区域内。文章已相应更新。
  • 09/06/2010 
    • 根据Paul Selormey的更多反馈,我在ZoomAndPanControl中添加了IsMouseWheelScrollingEnabled属性。将其设置为true即可启用控件响应鼠标滚轮输入来平移视图。这与标准ScrollViewer的鼠标滚轮输入的工作方式非常相似。我还添加了一个ZoomAndPanControl属性列表,以配合现有的方法列表。
  • 18/06/2010
    • 设置ContentScale的值以前会导致ContentOffsetXContentOffsetY自动更新。现在不再是这样了。我发现当您将所有三个依赖属性绑定到数据源,然后又想通过更改数据源一次性更新所有三个时,可能会发生一个不良后果。绑定到ContentScale会更新它,进而错误地更新您数据源中的ContentOffsetXContentOffsetY!现在不会发生这种情况了,ContentOffsetXContentOffsetY的自动更新现在仅限于需要的地方,即在类似Google地图的缩放和拖动缩放的动画进行过程中。
  • 22/06/2010 
    • 当视图大小调整时(由于窗口大小调整),ContentOffsetXContentOffsetY现在被限制在有效范围内。这是对Patrick Walz报告的一个问题的修复。当滚动条位于底部或右侧边界时,并且您调整了窗口的相应边缘(例如窗口的底部或右侧),内容偏移量现在将被限制在内容边缘,而不是允许显示内容之外的区域。
  • 29/06/2010
    • 修复了内容视图大于内容时居中内容的显示问题。当内容缩小缩放时,这本来是正常工作的,但当内容未缩放且窗口最大化,导致视图大于内容时,居中显示就不太正常了。我不得不为Generic.xaml中声明的ContentPresenter添加显式对齐,并修改了确定居中内容偏移量的计算。
  • 19/11/2010
    • 添加了SnapContentOffsetTo函数。这使得ZoomAndPanControlContentOffsetX和Y捕捉到指定点。
    • 修复了MakeVisible函数(实现IScrollInfo)。该函数现在如您所期望的那样工作。
    • 修复了tmsife和Member 7483521报告的一个问题。现在可以从代码后端正常设置内容的大小。
  • 09/12/2010
    • 添加了一个新的示例项目,演示了如何使用ZoomAndPanControl来创建“无限工作区”的概念。在第一部分的开头附近添加了一个小节,描述了这个新的示例项目。
  • 21/03/2011
    • 修复了Skybluecodeflier报告的高级示例中的一个问题。ZoomAndPanControl内容的尺寸未设置,因此平移和滚动无法正常工作。
© . All rights reserved.