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

WPF 的深度缩放

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (54投票s)

2010年11月24日

Ms-PL

13分钟阅读

viewsIcon

415156

downloadIcon

9190

WPF 的 MultiScaleImage(Deep Zoom)实现,兼容 Deep Zoom Composer 和 Zoom.it。

Deep Zoom for WPF Screenshot

目录

引言

Silverlight 最具“魔力”的功能之一是 Deep Zoom [^]。通过巧妙地分割图像,Deep Zoom 允许用户以出色的性能流畅地平移和缩放巨大的(甚至可能是无限的)图像。

自 Silverlight 2 发布 Deep Zoom 以来,它一直是 WPF 中最受请求的功能之一——截至撰写本文时,它在 WPF 功能建议的 UserVoice 网站上排名前 10 [^]。

Deep Zoom for WPF requests on UserVoice

好了,你们的请求得到了回应!本文介绍了一个功能齐全的 WPF Deep Zoom 实现,包括 Deep Zoom Composer 和 Zoom.it 支持、多点触控支持等。第一部分,我们将了解如何使用该控件及其当前限制;之后,我们将详细了解其实现方式。让我们开始吧!

使用控件

Deep Zoom for WPF 项目是一个在 CodePlex [^] 上以 MS-PL [^] 许可证提供的开源项目。您可以在 http://deepzoom.codeplex.com/releases [^] 下载最新版本。

要使用该控件,您只需添加对 DeepZoom.dll 的引用,并像普通图像一样将其添加到 XAML 文件中。

[MainWindow.xaml]:(已修改)
<Window x:Class="DeepZoom.TestApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:dz="clr-namespace:DeepZoom.Controls;assembly=DeepZoom"
        Title="MainWindow" Height="800" Width="1280">
    <Border Background="Black">
        <dz:MultiScaleImage 
            Source="http://static.seadragon.com/content/misc/color-flower.dzi" />
    </Border>
</Window>

再简单不过了 :)

该控件还支持使用 Deep Zoom Composer [^](**集合除外**)和 Zoom.it [^] 生成的图像,以及自定义图块源。

限制和已知问题

该控件的当前实现是 Silverlight MultiScaleImage 的简化版本,仅支持其最基本的功能。其限制和已知问题如下:

  • 该控件仅支持 Deep Zoom 图像(包括稀疏图像),**尚不支持 Deep Zoom 集合**。
  • 不支持的 Silverlight 成员
    • 属性:
      • AllowDownloading
      • BlurFactor
      • IsDownloading
      • IsIdle
      • SkipLevels
      • SubImages
      • UseSprings
      • ViewportOrigin
      • ViewportWidth
    • 事件
      • ImageFailed
      • ImageOpenFailed
      • ImageOpenSucceeded
      • MotionFinished
      • ViewportChanged
  • 该控件仅支持通过鼠标拖动/滚轮和多点触控进行平移和缩放,**没有额外的导航按钮**(但可以轻松添加)。
  • 图块的最大数量(总计)为 **int.MaxValue(约 21 亿)**。这是因为使用了 IList/ItemsContainerGenerator 进行虚拟化(稍后详述)。因此,最大缩放级别将为约 24(取决于图像的纵横比)。
  • 在级别约 21 之后,平移和缩放性能会变差。如果您有任何修复此问题的想法,请告诉我!

致谢

本项目使用了以下库的代码:

特别感谢 IntuiLab,我项目诞生时的雇主,允许我将此项目和文章作为开源发布!

看看它是如何实现的

Deep Zoom 是如何工作的?

构建 Deep Zoom 的第一步是理解它的工作原理。摘自 Jaime Rodriguez 的 Deep Zoom Primer [^]

Deep Zoom 通过将图像(或图像组合)分割成图块来实现其目标。在对图像进行分块的同时,组合器还会为原始组合创建低分辨率图块的层级结构。

Image pyramid used by Deep Zoom

图片来自 MSDN Library

上图展示了层级结构的样子;原始图像位于层级结构的底部,请注意它如何被分割成更小的图像;另请注意,层级结构会创建分辨率更低的图像(也已分块)。(...) 所有这些分块操作都在设计时完成,并通过 Deep Zoom Composer 实现。

在运行时,MultiScaleImage 首先下载图像的低分辨率图块,然后在需要时(在平移或缩放时)按需下载其他图像;Deep Zoom 可确保从低分辨率到高分辨率图像的过渡平滑无缝。

在 Deep Zoom 中,所有图块都是正方形且大小相同(默认 256x256 像素)。尽管如此,如果您查看计算机上 Deep Zoom 项目的输出文件夹,您会发现图块的大小不同,因为它们还会相互重叠(默认情况下,每边重叠 1 像素)。

本文将不深入探讨 Deep Zoom 的数学细节 - 请参考 Daniel Gasienica 博客上的 “Inside Deep Zoom - Part II: Mathematical Analysis” [^],以从数学角度对多尺度图像进行非常有趣的解释。

Deep Zoom 对象模型

为了尽可能贴近 Silverlight 的 Deep Zoom 对象模型,本项目提供了一个模仿 Silverlight 的类库。公共对象模型表示如下:

Deep Zoom object model

唯一的区别(除了未实现的成员)是 GetTileLayers 方法已被简化。在原始 Silverlight 版本中,该方法具有以下签名:

protected abstract void GetTileLayers(int tileLevel, 
                                      int tilePositionX, 
                                      int tilePositionY, 
                                      IList<object> tileImageLayerSources);

实现此方法的类必须添加新图块的 UritileImageLayerSources 列表中。由于该 URI 列表在此项目中未使用,因此该方法具有以下签名:

protected abstract object GetTileLayers(int tileLevel, 
                                        int tilePositionX, 
                                        int tilePositionY);

该方法现在支持 UriStream 作为有效返回值,允许自定义图块源在内存中动态生成图块。

进入 ZoomableCanvas

由于多尺度图像可能包含数十亿个图块,Deep Zoom 的主要挑战在于虚拟化。我们需要从两个方面进行虚拟化:

  1. **数据**必须虚拟化,以便只将正在使用的图块存储在内存中。
  2. **UI** 必须虚拟化,以便我们只在需要时加载和渲染图像。

为了实现这两种虚拟化,我使用了 Kael Rowan 的 ZoomableCanvas [^]。这个巧妙的控件允许我们在平移和缩放时进行项目虚拟化。基本思想是,在任何时刻,ZoomableCanvas 都必须能够知道当前视图框中哪些元素可见,以便它可以适当地加载和“实现”这些元素。

当您创建一个实现名为 ISpatialItemsSource 的接口的类时,就会发生奇迹:

public interface ISpatialItemsSource
{
    Rect Extent { get; }

    event EventHandler ExtentChanged;
    event EventHandler QueryInvalidated;

    IEnumerable<int> Query(Rect rectangle);
}

上述 Query 方法应返回在作为参数传入的矩形内的可见元素的索引列表,而 Extent 属性表示画布的边界。此外,您的类应使用 ExtentChangedQueryInvalidated 事件来通知画布它应重新查询或更改其范围(通常在项目集合更改时)。

当此接口实现后,ZoomableCanvas 通过在视图更改时(通过平移或缩放)查询可见项目来管理要添加到屏幕和从屏幕移除的元素。然后,如果 ZoomableCanvas 用作 ItemsControlItemsPanel(这意味着 ItemsSource 同时实现了 ISpatialItemsSourceIList),画布将按索引查询 List 以获取项目并将其显示在屏幕上。

由于 IList 的这种依赖关系,ZoomableCanvas 仅限于 int.MaxValue(约 21 亿)个图块,这似乎是一个很大的数字,但一些大型 Deep Zoom 稀疏图像可能会超过这个数量。

本项目中使用的实现位于 MultiScaleImageSpatialItemsSource 类中,该类基于 Kael Rowan 的名为 “ZoomableApplication2 - A million items” [^] 的演示。在此演示中,Kael 使用了一个简单的算法来确定均匀网格中可见的图块,并按从可见区域中心到边缘的顺序排列。本项目中的实现如下:

来自 [MultiScaleTileSource.cs]
private IEnumerable<Tile> VisibleTiles(Rect rectangle, int level)
{
    rectangle.Intersect(new Rect(ImageSize));

    var top = Math.Floor(rectangle.Top / TileSize);
    var left = Math.Floor(rectangle.Left / TileSize);
    var right = Math.Ceiling(rectangle.Right / TileSize);
    var bottom = Math.Ceiling(rectangle.Bottom / TileSize);

    right = right.AtMost(ColumnsAtLevel(level));
    bottom = bottom.AtMost(RowsAtLevel(level));

    var width = (right - left).AtLeast(0);
    var height = (bottom - top).AtLeast(0);

    if (top == 0.0 && left == 0.0 && width == 1.0 && height == 1.0) 
        // This level only has one tile
        yield return new Tile(level, 0, 0);
    else
    {
        foreach (var pt in Quadivide(new Rect(left, top, width, height)))
            yield return new Tile(level, (int)pt.X, (int)pt.Y);
    }
}

private static IEnumerable<Point> Quadivide(Rect area)
{
    if (area.Width > 0 && area.Height > 0)
    {
        var center = area.GetCenter();

        var x = Math.Floor(center.X);
        var y = Math.Floor(center.Y);

        yield return new Point(x, y);

        var quad1 = new Rect(area.TopLeft, new Point(x, y + 1));
        var quad2 = new Rect(area.TopRight, new Point(x, y));
        var quad3 = new Rect(area.BottomLeft, new Point(x + 1, y + 1));
        var quad4 = new Rect(area.BottomRight, new Point(x + 1, y));

        var quads = new Queue<IEnumerator<Point>>();
        quads.Enqueue(Quadivide(quad1).GetEnumerator());
        quads.Enqueue(Quadivide(quad2).GetEnumerator());
        quads.Enqueue(Quadivide(quad3).GetEnumerator());
        quads.Enqueue(Quadivide(quad4).GetEnumerator());
        while (quads.Count > 0)
        {
            var quad = quads.Dequeue();
            if (quad.MoveNext())
            {
                yield return quad.Current;
                quads.Enqueue(quad);
            }
        }
    }
}

一些需要注意的点:

  • Tile 结构通过其级别(Level)、列(Column)和行(Row)(均为 int 类型)来标识一个图块。
  • Quadivide 算法只是将一个矩形分割成 1x1 的“单元格”,并按从中心到边缘的顺序排列。
  • 此代码使用 Kael Rowan 的一些 MathExtensions [^] 扩展方法,例如 AtMostAtLeast,使代码更具可读性。

需要注意的一个重要点是,此算法必须在每个级别(从 0 到当前级别)上运行,才能实现 Deep Zoom 的“渐进加载”效果。执行此操作的代码如下:

来自 [MultiScaleTileSource.cs]:(已修改)
internal IEnumerable<Tile> VisibleTilesUntilFill(Rect rectangle, int startingLevel)
{
    var levels = Enumerable.Range(0, startingLevel + 1);
    return levels.SelectMany(level =>
    {
        var levelScale = ScaleAtLevel(level);
        var scaledBounds = new Rect(rectangle.X * levelScale,
                                    rectangle.Y * levelScale,
                                    rectangle.Width * levelScale,
                                    rectangle.Height * levelScale);
        return VisibleTiles(scaledBounds, level);
    });
}

此 LINQ 查询采用坐标系为完整图像的可见矩形(由 ISpatialItemsSource Query 方法传入),将其缩放到每个级别,并通过上述算法计算其中的单元格。请注意,随着 startingLevel 增加(用户正在深入缩放图像),此算法会变慢。根据 Daniel Gasienica 的文章 [^],此算法下载的数据量比仅显示最大层所需的数据量多 33%。

显示图块

知道了需要显示哪些图块后,我们就需要按需下载它们。为此,MultiScaleImageSpatialItemsSourceIListthis[int index] 实现必须能够从其索引获取图块,从 MultiScaleTileSource 获取图像源,并下载图像。在此实现中,下载使用 .NET 并行扩展异步完成。该类还管理图块的本地缓存。

下载后,我们必须以合理的性能将图块显示在屏幕上。由于屏幕上最多会显示 200 个图块,并且图块不需要交互,因此不建议使用 Image 对象或其他复杂的交互式 FrameworkElement

本项目中实现的解决方案是一个继承自 FrameworkElement 的简单类,它在 Visual 对象中渲染图像:

[TileHost.cs]:(已修改)
public class TileHost : FrameworkElement
{
    private DrawingVisual _visual;
    private static readonly AnimationTimeline _opacityAnimation = 
        new DoubleAnimation(1, TimeSpan.FromMilliseconds(500)) 
            { EasingFunction = new ExponentialEase() };

    public TileHost()
    {
        IsHitTestVisible = false;
    }
    
    // Both dependency properties trigger the RefreshTile callback when changed

    public static readonly DependencyProperty SourceProperty =
        DependencyProperty.Register("Source", 
                                    typeof(ImageSource), 
                                    typeof(TileHost),
                                    new FrameworkPropertyMetadata(null,
                                      new PropertyChangedCallback(RefreshTile)));

    public static readonly DependencyProperty ScaleProperty =
        DependencyProperty.Register("Scale", 
                                    typeof(double), 
                                    typeof(TileHost),
                                    new FrameworkPropertyMetadata(1.0,
                                      new PropertyChangedCallback(RefreshTile)));

    private static void RefreshTile(DependencyObject d,
                                    DependencyPropertyChangedEventArgs e)
    {
        var tileHost = d as TileHost;
        if (tileHost != null && tileHost.Source != null && tileHost.Scale > 0)
            tileHost.RenderTile();
    }

    private void RenderTile()
    {
        _visual = new DrawingVisual();
        Width = Source.Width * Scale;
        Height = Source.Height * Scale;
        var dc = _visual.RenderOpen();
        dc.DrawImage(Source, new Rect(0, 0, Width, Height));
        dc.Close();

        CacheMode = new BitmapCache(1 / Scale);

        // Animate opacity
        Opacity = 0;
        BeginAnimation(OpacityProperty, _opacityAnimation);
    }

    // Provide a required override for the VisualChildrenCount property.
    protected override int VisualChildrenCount
    {
        get { return _visual == null ? 0 : 1; }
    }

    // Provide a required override for the GetVisualChild method.
    protected override Visual GetVisualChild(int index)
    {
        return _visual;
    }
}

一些有趣的点:

  • TileHost 可以从其 Scale 属性计算其大小。
  • 使用 DrawingVisualDrawingContext 是在 FrameworkElement 上打印图像的有效方法,如果不需要交互的话。
  • “混合”动画在图块完成渲染时执行。

整合在一起

现在我们知道如何下载和在屏幕上绘制单个图块。将所有内容绑定在一起:

  • 一个以 MultiScaleImageSpatialItemsSource 作为 ItemsSourceItemsControl 将显示图块。
  • ItemsControlItemsPanel 是一个 ZoomableCanvas,由于实现了 ISpatialItemsSource,因此它能够虚拟化 UI 和数据。
  • 上一节定义的 TileHost 将用作 ItemTemplate,在屏幕上显示图像。

实现这一切的类是 MultiScaleImage,它是开发者的入口点。该类的默认模板如下所示:

来自 [Themes/Generic.xaml]
<ControlTemplate TargetType="{x:Type local:MultiScaleImage}">
    <ItemsControl x:Name="PART_ItemsControl" 
                     Background="Transparent" ClipToBounds="True">
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Top" Value="{Binding Top}"/>
                <Setter Property="Canvas.Left" Value="{Binding Left}"/>
                <Setter Property="Panel.ZIndex" Value="{Binding ZIndex}"/>
            </Style>
        </ItemsControl.ItemContainerStyle>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <local:TileHost Source="{Binding Source}" Scale="{Binding Scale}"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ControlTemplate>

您可能会问,ZoomableCanvas 在哪里?由于我们需要从类中访问它,如果我们将其设置为 ItemsControl.ItemsPanel,我们将无法访问它的命名空间。为了解决这个问题,MultiScaleImage 类注入了 ZoomableCanvas 并保留了对其的引用:

来自 [MultiScaleImage.cs]:(已修改)
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    _itemsControl = GetTemplateChild("PART_ItemsControl") as ItemsControl;
    if (_itemsControl == null) return;

    _itemsControl.ApplyTemplate();

    var factoryPanel = new FrameworkElementFactory(typeof(ZoomableCanvas));
    factoryPanel.AddHandler(LoadedEvent, new RoutedEventHandler(ZoomableCanvasLoaded));
    _itemsControl.ItemsPanel = new ItemsPanelTemplate(factoryPanel);
    
    // (...)
}

private void ZoomableCanvasLoaded(object sender, RoutedEventArgs e)
{
    // Got the reference to the actual instance of ZoomableCanvas!
    _zoomableCanvas = sender as ZoomableCanvas;
    
    // (...)
}

平移和缩放

Silverlight 中的 MultiScaleImage 默认不包含任何交互性。开发人员必须实现平移和缩放支持,以便用户可以操作图像。在此版本中,我在控件中包含了鼠标和多点触控的平移和缩放功能,以便更轻松地直接使用。

为此,我重写了 OnManipulationDelta 方法,并添加了多点触控平移和缩放支持。还重写了 OnManipulationInertiaStarting 以使动画更流畅。

问题在于,仅使用操作事件,控件将不支持鼠标。为了克服这个问题,我使用了 Josh Blake 的 Blake.NUI [^] 库中的 MouseTouchDevice。该类使鼠标能够充当单点触控设备,从而更容易通过单一入口点创建鼠标和多点触控交互。

鼠标支持的最后一步是鼠标滚轮。通过重写 OnPreviewMouseWheel,我们可以捕获鼠标滚轮并为缩放级别设置动画。

操作代码中的一个有趣点是使用了缩放事件的**节流(throttling)**。由于用户可能会非常快速地放大和缩小,级别会快速变化,迫使 ZoomableCanvas 为不必要的目的多次重新计算图块,而用户正在从一个级别移动到另一个级别。为了解决这个问题,我们使用 DispatcherTimer 将级别更改限制在预定义的间隔内:

摘自 [MultiScaleImage.cs]
private int _desiredLevel;
private readonly DispatcherTimer _levelChangeThrottle;

public MultiScaleImage()
{
    //(...)
    
    _levelChangeThrottle = new DispatcherTimer 
    { 
        Interval = TimeSpan.FromMilliseconds(ThrottleIntervalMilliseconds),
        IsEnabled = false 
    };
    _levelChangeThrottle.Tick += (s, e) =>
    {
        _spatialSource.CurrentLevel = _desiredLevel;
        _levelChangeThrottle.IsEnabled = false;
    };
}
        
private void ScaleCanvas(/*...*/)
{
    // (...)
    
    if (newLevel != level)
    {
        // If it's zooming in, throttle
        if (newLevel > level)
            ThrottleChangeLevel(newLevel);
        else
            _spatialSource.CurrentLevel = newLevel;
    }
    
    // (...)
}

private void ThrottleChangeLevel(int newLevel)
{
    _desiredLevel = newLevel;

    if (_levelChangeThrottle.IsEnabled)
        _levelChangeThrottle.Stop();

    _levelChangeThrottle.Start();
}

添加 TypeConverter

使开发人员更容易使用此控件的最后一步是使他们能够直接在 XAML 中使用它。如果没有 TypeConverter,代码将如下所示:

<dz:MultiScaleImage>
    <dz:MultiScaleImage.Source>
        <dz:DeepZoomImageTileSource 
            UriSource="http://static.seadragon.com/content/misc/color-flower.dzi" />
    </dz:MultiScaleImage.Source>
</dz:MultiScaleImage>

通过使用 TypeConverter,我们可以直接将字符串转换为其他类型,从而实现如下代码:

<dz:MultiScaleImage Source="http://static.seadragon.com/content/misc/color-flower.dzi" />

秘密是什么?要做到这一点,我们必须首先创建一个派生自 TypeConverter 的类,如下所示:

[DeepZoomImageTileSourceConverter.cs]
public class DeepZoomImageTileSourceConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, 
                                        Type sourceType)
    {
        if (sourceType == typeof(string))
            return true;
        return base.CanConvertFrom(context, sourceType);
    }

    public override bool CanConvertTo(ITypeDescriptorContext context, 
                                      Type destinationType)
    {
        if (destinationType == typeof(string))
            return true;

        return base.CanConvertTo(context, destinationType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, 
                                       CultureInfo culture, object value)
    {
        var inputString = value as string;
        if (inputString != null)
        {
            try
            {
                // This is the only important line of code in this file :)
                return new DeepZoomImageTileSource(
                    new Uri(inputString, UriKind.RelativeOrAbsolute)
                );
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format(
                    "Cannot convert '{0}' ({1}) - {2}",
                    value, 
                    value.GetType(),
                    ex.Message
                ), ex);
            }
        }

        return base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context, 
                                     CultureInfo culture, object value, 
                                     Type destinationType)
    {
        if (destinationType == null)
            throw new ArgumentNullException("destinationType");

        var tileSource = value as DeepZoomImageTileSource;

        if (tileSource != null)
            if (CanConvertTo(context, destinationType))
            {
                var uri = tileSource.UriSource;
                return uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.OriginalString;
            }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}

最后,我们必须声明此转换器可用于将字符串转换为 MultiScaleTileSource 类型的类:

[MultiScaleTileSource.cs]
[TypeConverter(typeof(DeepZoomImageTileSourceConverter))]
public abstract class MultiScaleTileSource : DependencyObject
{ 
    /*(...)*/ 
}

就是这样!

奖励:创建自定义图块源

最后,我们将看到实现自定义 Deep Zoom 图块源有多么容易。在此示例中,我们将为 **OpenStreetMap** 创建一个图块源。

OpenStreetMap Deep Zoom in WPF

**注意**:在生产环境中不建议直接访问 OSM 图块。请勿滥用此服务!

为了实现自定义 MultiScaleTileSource,我们只需要知道如何根据图块级别、X 和 Y 坐标生成 Uri。根据 OpenStreetMap wiki,Osmarender 地图的 URL 格式为 http://tah.openstreetmap.org/Tiles/tile/{zoom}/{x}/{y}.png

另外请注意,OSM 的缩放级别与 Deep Zoom 的缩放级别不同;OSM 的缩放级别 0 对应一个可见的 256x256 图块,这对应于 Deep Zoom 中的级别 log2256 = 8

因此,我们需要的全部代码是:

[OpenStreetMapTileSource.cs]
public class OpenStreetMapTileSource : MultiScaleTileSource
{
    public OpenStreetMapTileSource() : base(0x8000000, 0x8000000, 256, 0) { }

    protected override object GetTileLayers(int tileLevel, 
                       int tilePositionX, int tilePositionY)
    {
        var zoom = tileLevel - 8;
        if (zoom >= 0)
            return new Uri(string.Format(
                "http://tah.openstreetmap.org/Tiles/tile/{0}/{1}/{2}.png",
                zoom, 
                tilePositionX, 
                tilePositionY));
        else
            return null;
    }
}

在 XAML 文件中使用它的代码如下所示:

<dz:MultiScaleImage>
    <dz:MultiScaleImage.Source>
        <osm:OpenStreetMapTileSource />
    </dz:MultiScaleImage.Source>
</dz:MultiScaleImage>

总结

在本文中,我们利用了许多不同来源的想法,开发了 elusive 的 WPF Deep Zoom 控件。该控件仍处于起步阶段,因此代码也可在 CodePlex [^] 上找到。请帮助我们调整和改进此控件,以实现最佳的 Deep Zoom 体验!

我希望本文能够展示一些关于 WPF 中高性能界面的想法。如果您使用此控件开发了任何内容,请在评论部分发布链接!

未来展望

此应用程序的一些有趣后续步骤可能是

  • 添加对 Deep Zoom 集合的支持。
  • 提高性能,尤其是在更深的缩放级别(> 22)。
  • 使用统一的(可能是原生的)渲染表面,而不是多个 TileHost
  • 移除图块数量的 int.MaxValue 限制。这可能需要重新实现 ZoomableCanvas 以使用不同的虚拟化系统。
  • 实现一个更优化的图块下载/缓存和取消下载系统。
  • 将交互性与 MultiScaleImage 分离,以与 Silverlight 保持一致。这个新控件将包装 MultiScaleImage,添加交互性、导航等。

您怎么看?

这个项目有用吗?您会在 WPF 中如何实现 Deep Zoom?有什么需要进一步解释的地方吗?

请留下您的评论和建议,如果本文让您满意,请投票支持。谢谢!

其他链接和参考

历史

  • v1.0 (2010/11/22) - 初始发布。
© . All rights reserved.