平移和缩放图像
我想在WPF中创build一个简单的图像查看器,使用户能够:
- 平移(通过鼠标拖动图像)。
- 缩放(使用滑块)。
- 显示重叠(例如矩形select)。
- 显示原始图像(如果需要,使用滚动条)。
你能解释一下怎么做吗?
我没有在网上find一个好的样本。 我应该使用ViewBox吗? 还是ImageBrush? 我需要ScrollViewer吗?
谢谢!
我解决这个问题的方法是将图像放置在边框中,并将其ClipToBounds属性设置为True。 然后将图像上的RenderTransformOrigin设置为0.5,0.5,这样图像将开始在图像的中心放大。 RenderTransform也被设置为包含ScaleTransform和TranslateTransform的TransformGroup。
然后,我处理图像上的MouseWheel事件来实现缩放
private void image_MouseWheel(object sender, MouseWheelEventArgs e) { var st = (ScaleTransform)image.RenderTransform; double zoom = e.Delta > 0 ? .2 : -.2; st.ScaleX += zoom; st.ScaleY += zoom; }
为了处理平移,我做的第一件事就是处理图像上的MouseLeftButtonDown事件,捕获鼠标并logging它的位置,同时我也存储TranslateTransform的当前值,这个值被更新以实现平移。
Point start; Point origin; private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { image.CaptureMouse(); var tt = (TranslateTransform)((TransformGroup)image.RenderTransform) .Children.First(tr => tr is TranslateTransform); start = e.GetPosition(border); origin = new Point(tt.X, tt.Y); }
然后我处理了MouseMove事件来更新TranslateTransform。
private void image_MouseMove(object sender, MouseEventArgs e) { if (image.IsMouseCaptured) { var tt = (TranslateTransform)((TransformGroup)image.RenderTransform) .Children.First(tr => tr is TranslateTransform); Vector v = start - e.GetPosition(border); tt.X = origin.X - vX; tt.Y = origin.Y - vY; } }
最后不要忘记释放鼠标捕获。
private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { image.ReleaseMouseCapture(); }
至于selectresize的手柄可以使用装饰者完成,请参阅这篇文章的更多信息。
使用这个问题的样本后,我已经完成了相对于鼠标指针缩放适当的平移和缩放应用程序的版本。 所有的平移和缩放代码已被移动到独立的类叫ZoomBorder。
ZoomBorder.cs
using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace PanAndZoom { public class ZoomBorder : Border { private UIElement child = null; private Point origin; private Point start; private TranslateTransform GetTranslateTransform(UIElement element) { return (TranslateTransform)((TransformGroup)element.RenderTransform) .Children.First(tr => tr is TranslateTransform); } private ScaleTransform GetScaleTransform(UIElement element) { return (ScaleTransform)((TransformGroup)element.RenderTransform) .Children.First(tr => tr is ScaleTransform); } public override UIElement Child { get { return base.Child; } set { if (value != null && value != this.Child) this.Initialize(value); base.Child = value; } } public void Initialize(UIElement element) { this.child = element; if (child != null) { TransformGroup group = new TransformGroup(); ScaleTransform st = new ScaleTransform(); group.Children.Add(st); TranslateTransform tt = new TranslateTransform(); group.Children.Add(tt); child.RenderTransform = group; child.RenderTransformOrigin = new Point(0.0, 0.0); this.MouseWheel += child_MouseWheel; this.MouseLeftButtonDown += child_MouseLeftButtonDown; this.MouseLeftButtonUp += child_MouseLeftButtonUp; this.MouseMove += child_MouseMove; this.PreviewMouseRightButtonDown += new MouseButtonEventHandler( child_PreviewMouseRightButtonDown); } } public void Reset() { if (child != null) { // reset zoom var st = GetScaleTransform(child); st.ScaleX = 1.0; st.ScaleY = 1.0; // reset pan var tt = GetTranslateTransform(child); tt.X = 0.0; tt.Y = 0.0; } } #region Child Events private void child_MouseWheel(object sender, MouseWheelEventArgs e) { if (child != null) { var st = GetScaleTransform(child); var tt = GetTranslateTransform(child); double zoom = e.Delta > 0 ? .2 : -.2; if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4)) return; Point relative = e.GetPosition(child); double abosuluteX; double abosuluteY; abosuluteX = relative.X * st.ScaleX + tt.X; abosuluteY = relative.Y * st.ScaleY + tt.Y; st.ScaleX += zoom; st.ScaleY += zoom; tt.X = abosuluteX - relative.X * st.ScaleX; tt.Y = abosuluteY - relative.Y * st.ScaleY; } } private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (child != null) { var tt = GetTranslateTransform(child); start = e.GetPosition(this); origin = new Point(tt.X, tt.Y); this.Cursor = Cursors.Hand; child.CaptureMouse(); } } private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (child != null) { child.ReleaseMouseCapture(); this.Cursor = Cursors.Arrow; } } void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e) { this.Reset(); } private void child_MouseMove(object sender, MouseEventArgs e) { if (child != null) { if (child.IsMouseCaptured) { var tt = GetTranslateTransform(child); Vector v = start - e.GetPosition(this); tt.X = origin.X - vX; tt.Y = origin.Y - vY; } } } #endregion } }
MainWindow.xaml
<Window x:Class="PanAndZoom.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PanAndZoom" Title="PanAndZoom" Height="600" Width="900" WindowStartupLocation="CenterScreen"> <Grid> <local:ZoomBorder x:Name="border" ClipToBounds="True" Background="Gray"> <Image Source="image.jpg"/> </local:ZoomBorder> </Grid> </Window>
MainWindow.xaml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace PanAndZoom { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }
答案是张贴在上面,但没有完成。 这里是完整的版本:
XAML
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="MapTest.Window1" x:Name="Window" Title="Window1" Width="1950" Height="1546" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Controls="clr-namespace:WPFExtensions.Controls;assembly=WPFExtensions" mc:Ignorable="d" Background="#FF000000"> <Grid x:Name="LayoutRoot"> <Grid.RowDefinitions> <RowDefinition Height="52.92"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Border Grid.Row="1" Name="border"> <Image Name="image" Source="map3-2.png" Opacity="1" RenderTransformOrigin="0.5,0.5" /> </Border> </Grid>
代码在后面
using System.Linq; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace MapTest { public partial class Window1 : Window { private Point origin; private Point start; public Window1() { InitializeComponent(); TransformGroup group = new TransformGroup(); ScaleTransform xform = new ScaleTransform(); group.Children.Add(xform); TranslateTransform tt = new TranslateTransform(); group.Children.Add(tt); image.RenderTransform = group; image.MouseWheel += image_MouseWheel; image.MouseLeftButtonDown += image_MouseLeftButtonDown; image.MouseLeftButtonUp += image_MouseLeftButtonUp; image.MouseMove += image_MouseMove; } private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { image.ReleaseMouseCapture(); } private void image_MouseMove(object sender, MouseEventArgs e) { if (!image.IsMouseCaptured) return; var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); Vector v = start - e.GetPosition(border); tt.X = origin.X - vX; tt.Y = origin.Y - vY; } private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { image.CaptureMouse(); var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); start = e.GetPosition(border); origin = new Point(tt.X, tt.Y); } private void image_MouseWheel(object sender, MouseWheelEventArgs e) { TransformGroup transformGroup = (TransformGroup) image.RenderTransform; ScaleTransform transform = (ScaleTransform) transformGroup.Children[0]; double zoom = e.Delta > 0 ? .2 : -.2; transform.ScaleX += zoom; transform.ScaleY += zoom; } } }
我有一个完整的wpf项目在我的网站上使用这个代码的例子: logging粘滞便笺应用程序 。
试试这个缩放控制: http : //wpfextensions.codeplex.com
该控件的用法很简单,引用wpfextensions程序集比:
<wpfext:ZoomControl> <Image Source="..."/> </wpfext:ZoomControl>
目前不支持滚动条。 (这将在下一个版本,将在一个或两个星期内提供)。
- 潘:将图像放在canvas内。 实现鼠标向上,向下和移动事件来移动Canvas.Top,Canvas.Left属性。 当下来时,你把一个isDraggingFlag标记为true,当你把标志设置为false时。 在移动时,检查是否设置了标志,是否在canvas上偏移了图像上的Canvas.Top和Canvas.Left属性。
- 缩放:将滑块绑定到canvas的缩放变换
- 显示叠加层:在包含图像的canvas上添加没有背景的其他canvas。
- 显示原始图像:ViewBox内部的图像控制
@Anothen和@ Number8 – Vector类在Silverlight中不可用,所以为了使它工作,我们只需要logging上次调用MouseMove事件的最后一个位置,并比较两个点以找出差异; 然后调整变换。
XAML:
<Border Name="viewboxBackground" Background="Black"> <Viewbox Name="viewboxMain"> <!--contents go here--> </Viewbox> </Border>
代码隐藏:
public Point _mouseClickPos; public bool bMoving; public MainPage() { InitializeComponent(); viewboxMain.RenderTransform = new CompositeTransform(); } void MouseMoveHandler(object sender, MouseEventArgs e) { if (bMoving) { //get current transform CompositeTransform transform = viewboxMain.RenderTransform as CompositeTransform; Point currentPos = e.GetPosition(viewboxBackground); transform.TranslateX += (currentPos.X - _mouseClickPos.X) ; transform.TranslateY += (currentPos.Y - _mouseClickPos.Y) ; viewboxMain.RenderTransform = transform; _mouseClickPos = currentPos; } } void MouseClickHandler(object sender, MouseButtonEventArgs e) { _mouseClickPos = e.GetPosition(viewboxBackground); bMoving = true; } void MouseReleaseHandler(object sender, MouseButtonEventArgs e) { bMoving = false; }
另外请注意,您不需要一个TransformGroup或集合来实现平移和缩放; 相反,一个CompositeTransform将会减less麻烦。
我很确定这在资源使用方面真的是效率低下,但至less它工作:)
要相对于鼠标位置缩放,您只需要:
var position = e.GetPosition(image1); image1.RenderTransformOrigin = new Point(position.X / image1.ActualWidth, position.Y / image1.ActualHeight);
@ Merk
对于你使用lambdaexpression式的解决scheme,你可以使用下面的代码:
//var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform); TranslateTransform tt = null; TransformGroup transformGroup = (TransformGroup)grid.RenderTransform; for (int i = 0; i < transformGroup.Children.Count; i++) { if (transformGroup.Children[i] is TranslateTransform) tt = (TranslateTransform)transformGroup.Children[i]; }
此代码可以使用.Net框架工作3.0或2.0
希望它可以帮助你:-)
我的伦敦地下样本做到这一点,虽然有一张地图而不是一张静态图片。
又一个版本的同一种控制。 它具有与其他function相似的function,但它增加了:
- 触摸支持(拖/捏)
- 该图像可以被删除(通常,图像控件locking磁盘上的图像,所以你不能删除它)。
- 内边框的子项,所以平移的图像不会与边框重叠。 如果边界是圆angular矩形,请查找ClippedBorder类。
用法很简单:
<Controls:ImageViewControl ImagePath="{Binding ...}" />
和代码:
public class ImageViewControl : Border { private Point origin; private Point start; private Image image; public ImageViewControl() { ClipToBounds = true; Loaded += OnLoaded; } #region ImagePath /// <summary> /// ImagePath Dependency Property /// </summary> public static readonly DependencyProperty ImagePathProperty = DependencyProperty.Register("ImagePath", typeof (string), typeof (ImageViewControl), new FrameworkPropertyMetadata(string.Empty, OnImagePathChanged)); /// <summary> /// Gets or sets the ImagePath property. This dependency property /// indicates the path to the image file. /// </summary> public string ImagePath { get { return (string) GetValue(ImagePathProperty); } set { SetValue(ImagePathProperty, value); } } /// <summary> /// Handles changes to the ImagePath property. /// </summary> private static void OnImagePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var target = (ImageViewControl) d; var oldImagePath = (string) e.OldValue; var newImagePath = target.ImagePath; target.ReloadImage(newImagePath); target.OnImagePathChanged(oldImagePath, newImagePath); } /// <summary> /// Provides derived classes an opportunity to handle changes to the ImagePath property. /// </summary> protected virtual void OnImagePathChanged(string oldImagePath, string newImagePath) { } #endregion private void OnLoaded(object sender, RoutedEventArgs routedEventArgs) { image = new Image { //IsManipulationEnabled = true, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new TransformGroup { Children = new TransformCollection { new ScaleTransform(), new TranslateTransform() } } }; // NOTE I use a border as the first child, to which I add the image. I do this so the panned image doesn't partly obscure the control's border. // In case you are going to use rounder corner's on this control, you may to update your clipping, as in this example: // http://wpfspark.wordpress.com/2011/06/08/clipborder-a-wpf-border-that-clips/ var border = new Border { IsManipulationEnabled = true, ClipToBounds = true, Child = image }; Child = border; image.MouseWheel += (s, e) => { var zoom = e.Delta > 0 ? .2 : -.2; var position = e.GetPosition(image); image.RenderTransformOrigin = new Point(position.X / image.ActualWidth, position.Y / image.ActualHeight); var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform); st.ScaleX += zoom; st.ScaleY += zoom; e.Handled = true; }; image.MouseLeftButtonDown += (s, e) => { if (e.ClickCount == 2) ResetPanZoom(); else { image.CaptureMouse(); var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); start = e.GetPosition(this); origin = new Point(tt.X, tt.Y); } e.Handled = true; }; image.MouseMove += (s, e) => { if (!image.IsMouseCaptured) return; var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); var v = start - e.GetPosition(this); tt.X = origin.X - vX; tt.Y = origin.Y - vY; e.Handled = true; }; image.MouseLeftButtonUp += (s, e) => image.ReleaseMouseCapture(); //NOTE I apply the manipulation to the border, and not to the image itself (which caused stability issues when translating)! border.ManipulationDelta += (o, e) => { var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform); var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform); st.ScaleX *= e.DeltaManipulation.Scale.X; st.ScaleY *= e.DeltaManipulation.Scale.X; tt.X += e.DeltaManipulation.Translation.X; tt.Y += e.DeltaManipulation.Translation.Y; e.Handled = true; }; } private void ResetPanZoom() { var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform); var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform); st.ScaleX = st.ScaleY = 1; tt.X = tt.Y = 0; image.RenderTransformOrigin = new Point(0.5, 0.5); } /// <summary> /// Load the image (and do not keep a hold on it, so we can delete the image without problems) /// </summary> /// <see cref="http://blogs.vertigo.com/personal/ralph/Blog/Lists/Posts/Post.aspx?ID=18"/> /// <param name="path"></param> private void ReloadImage(string path) { try { ResetPanZoom(); // load the image, specify CacheOption so the file is not locked var bitmapImage = new BitmapImage(); bitmapImage.BeginInit(); bitmapImage.CacheOption = BitmapCacheOption.OnLoad; bitmapImage.UriSource = new Uri(path, UriKind.RelativeOrAbsolute); bitmapImage.EndInit(); image.Source = bitmapImage; } catch (SystemException e) { Console.WriteLine(e.Message); } } }
要获得专业的缩放控制WPF检查出ZoomPanel 。
它不是免费的,但很容易使用,并具有许多function – animation缩放和平移,支持ScrollViewer,鼠标滚轮支持,包括ZoomController(移动,放大,缩小,矩形缩放,重置button)。 它还附带了许多代码示例。
做同样的事情的另一种方式。 这将放大和缩小以及平移,但将图像保持在容器的边界内。 作为控件书写,所以直接添加样式到App.xaml
,或者通过Themes/Generic.xaml
添加到不同的项目中。 已添加...
为了清晰起见代码被删除。
Viewer.cs:
public class Viewer : ContentControl { public static readonly DependencyProperty ZoomIncrementProperty = DependencyProperty.Register("ZoomIncrement", typeof(double), typeof(Viewer), new FrameworkPropertyMetadata(0.2, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register("Zoom", typeof(double), typeof(Viewer), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty MinZoomProperty = DependencyProperty.Register("MinZoom", typeof(double), typeof(Viewer), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty MaxZoomProperty = DependencyProperty.Register("MaxZoom", typeof(double), typeof(Viewer), new FrameworkPropertyMetadata(5d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty TranslateProperty = DependencyProperty.Register("Translate", typeof(Point), typeof(Viewer), new FrameworkPropertyMetadata(new Point(0, 0), FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); /// <summary> /// Capture mouse movements /// </summary> private bool _capture; /// <summary> /// The origin of the transform at the start of capture /// </summary> private Point _origin; /// <summary> /// The mouse position at the start of capture /// </summary> private Point _start; static Viewer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(Viewer), new FrameworkPropertyMetadata(typeof(Viewer))); } public double MaxZoom { get => (double) GetValue(MaxZoomProperty); set => SetValue(MaxZoomProperty, value); } public double MinZoom { get => (double) GetValue(MinZoomProperty); set => SetValue(MinZoomProperty, value); } public Point Translate { get => (Point) GetValue(TranslateProperty); set { var width = ActualWidth * Zoom; var height = ActualHeight * Zoom; if (value.X > 0) { value.X = 0; } if (value.X <= ActualWidth - width) { value.X = ActualWidth - width; } if (value.Y > 0) { value.Y = 0; } if (value.Y <= ActualHeight - height) { value.Y = ActualHeight - height; } SetValue(TranslateProperty, value); } } public double Zoom { get => (double) GetValue(ZoomProperty); set { if (value <= MinZoom) { value = MinZoom; } if (value >= MaxZoom) { value = MaxZoom; } SetValue(ZoomProperty, value); } } public double ZoomIncrement { get => (double) GetValue(ZoomIncrementProperty); set => SetValue(ZoomIncrementProperty, value); } public void Reset() { Zoom = 1; Translate = new Point(); } protected override void OnMouseLeave(MouseEventArgs e) { base.OnMouseLeave(e); Cursor = Cursors.Arrow; _capture = false; } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); _start = e.GetPosition(this); _origin = new Point { X = Translate.X, Y = Translate.Y }; Cursor = Cursors.Hand; _capture = true; e.Handled = true; } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { base.OnMouseLeftButtonUp(e); Cursor = Cursors.Arrow; _capture = false; } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (_capture) { var position = e.GetPosition(this); var v = _start - position; Translate = new Point { X = _origin.X - vX, Y = _origin.Y - vY }; } } protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); Reset(); } protected override void OnPreviewMouseWheel(MouseWheelEventArgs e) { base.OnPreviewMouseWheel(e); var position = e.GetPosition(this); var abosuluteX = position.X * Zoom + Translate.X; var abosuluteY = position.Y * Zoom + Translate.Y; Zoom += e.Delta > 0 ? ZoomIncrement : -ZoomIncrement; Translate = new Point { X = abosuluteX - position.X * Zoom, Y = abosuluteY - position.Y * Zoom }; e.Handled = true; } }
Viewer.xaml:
<ResourceDictionary ... > <Style TargetType="{x:Type local:Viewer}" BasedOn="{StaticResource {x:Type ContentControl}}"> <Setter Property="Background" Value="Transparent"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:Viewer}"> <!-- Container --> <Border Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Cursor="{TemplateBinding Cursor}"> <Grid ClipToBounds="True"> <!-- Viewbox --> <Viewbox> <!-- Content --> <ContentPresenter Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" /> <!-- Render Transform --> <Viewbox.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="{Binding Zoom, RelativeSource={RelativeSource TemplatedParent}}" ScaleY="{Binding Zoom, RelativeSource={RelativeSource TemplatedParent}}" /> <TranslateTransform X="{Binding Translate.X, RelativeSource={RelativeSource TemplatedParent}}" Y="{Binding Translate.Y, RelativeSource={RelativeSource TemplatedParent}}" /> </TransformGroup> </Viewbox.RenderTransform> </Viewbox> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
App.xaml中
<Application ... > <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="... /Themes/Generic.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
用法:
<viewers:Viewer> <Image Source="{Binding ...}"/> </viewers:Viewer>