我如何使用 Scrollviewers / 类似的东西为 canvas 实现相机

How do I implement a camera for a canvas using Scrollviewers / something similar

我需要一个 "camera",它能够同时从不同视口显示 canvas 。 我的第一个想法是简单地使用 2 个不同的 scrollviewers 并为它们提供相同的 canvas 作为内容,并简单地改变它们中的滚动量。

遗憾的是只有一个滚动视图显示内容,另一个是空的。这里奇怪的是,您将滚动视图添加到根元素(在本例中也是 canvas )的顺序决定了哪个获取内容,而不是您将内容添加到的顺序滚动查看器。

那么是否可以以某种方式使用滚动查看器来达到我的目的?如果现在,你对如何实现一个能够在同一个视口上有 2 个不同视口的简单相机有什么建议吗?Canvas?

提前致谢。

这是我为测试编写的一些非常糟糕的代码:

 public partial class MainWindow : Window
 {
    Canvas _root = new Canvas();
    public MainWindow()
    {
        InitializeComponent();

        _root = new Canvas();
        AddChild(_root);

        //ScrollViewer 1
        ScrollViewer sv = new ScrollViewer();
        sv.Height = 400;
        sv.Width = 600;

        //ScrollerViewer 2
        ScrollViewer sv2 = new ScrollViewer();
        sv2.Height = 400;
        sv2.Width = 200;

        // Will be set later as Content of both Scrollviewers
        Canvas svc = new Canvas();
        svc.Width = Width;
        svc.Height = Height;
        svc.Background = new SolidColorBrush(Color.FromRgb(255, 255, 0));

        // rectangle to be displayed on the canvas 
        Canvas rect = new Canvas();
        rect.Height = 100;
        rect.Width = 100;
        rect.Background = new SolidColorBrush(Color.FromRgb(255, 0, 0));


        sv2.Content = svc;
        sv.Content = svc;

        // Add the scrollviews to the root canvas.
        // !!! The order you add them decides (somehow?) which scrollview gets the content.
        _root.Children.Add(sv);
        _root.Children.Add(sv2);

        svc.Children.Add(rect);


        Canvas.SetLeft(sv, 0);
        Canvas.SetLeft(sv2, 900);

    }
}

注意: 我同意评论者 Sinatr 的观点,如果可能的话,最好只对视图模型使用数据模板。您可以有一个视图模型用作两个或多个 ContentControl object 的上下文,这些视图模型使用为其定义的任何 DataTemplate 简单地呈现该视图模型。这将允许完整的用户交互、最高质量的渲染和最灵活的方法(即您的不同 "cameras" 甚至可以根据您的需要为相同的数据呈现完全不同的视觉效果)。

这是一个外观示例:

XAML:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApplication2"
        x:Name="mainWindow1"
        Title="MainWindow" Height="350" Width="525">

  <Window.DataContext>
    <l:ViewModel Text="Some Text"/>
  </Window.DataContext>

  <Window.Resources>
    <DataTemplate DataType="{x:Type l:ViewModel}">
      <Canvas Width="{Binding Width, ElementName=mainWindow1}"
              Height="{Binding Height, ElementName=mainWindow1}"
              Background="Yellow">
        <Canvas Width="100" Height="100" Background="Red"/>
        <!--
            I added text and a button, so that the view model actually
            _does_ something, but you could use an empty view model class
            and leave out the Grid here and it would work just as well.
        -->
        <Grid Width="{Binding Width, ElementName=mainWindow1}"
              Height="{Binding Height, ElementName=mainWindow1}">
          <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock Text="{Binding Text}" FontSize="32"/>
            <Button Content="Reverse" Command="{Binding Command}" FontSize="24"/>
          </StackPanel>
        </Grid>
      </Canvas>
    </DataTemplate>
  </Window.Resources>

  <Canvas>
    <ScrollViewer Width="600" Height="400"
                  HorizontalScrollBarVisibility="Auto"
                  VerticalScrollBarVisibility="Auto">
      <ContentControl Content="{Binding}"/>
    </ScrollViewer>
    <ScrollViewer Width="200" Height="400" Canvas.Left="900"
                  HorizontalScrollBarVisibility="Auto"
                  VerticalScrollBarVisibility="Auto">
      <ContentControl Content="{Binding}"/>
    </ScrollViewer>
  </Canvas>
</Window>

C#:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

class ViewModel : INotifyPropertyChanged
{
    private readonly ICommand _command;
    private string _text = string.Empty;

    public ICommand Command { get { return _command; } }

    public string Text
    {
        get { return _text; }
        set
        {
            if (_text != value)
            {
                _text = value;
                OnPropertyChanged();
            }
        }
    }

    public ViewModel()
    {
        _command = new DelegateCommand<object>(ExecuteCommand);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName]string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private void ExecuteCommand(object parameter)
    {
        Text = new string(Text.Reverse().ToArray());
    }
}

class DelegateCommand<T> : ICommand
{
    private readonly Action<T> _handler;
    private readonly Func<T, bool> _canExecute;

    public DelegateCommand(Action<T> handler) : this(handler, null) { }

    public DelegateCommand(Action<T> handler, Func<T, bool> canExecute)
    {
        _handler = handler;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute == null || _canExecute((T)parameter);
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _handler((T)parameter);
    }

    public void OnCanExecuteChanged()
    {
        EventHandler handler = CanExecuteChanged;

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

我在下面的回答旨在根据所提供的上下文解决您提出的具体问题。它假定您有充分的理由以这种方式构建 UI,并且出于某种原因(也许是性能问题?不过,从字面上明确地为每个 "camera" 创建单独的 object 图是不可取的我希望 WPF 能够像你或我一样优化性能)。但是我没有解决房间里的大象是我的疏忽,相对于普通 WPF 习语能够比实际尝试构建相同视觉效果的两个不同 "cameras" 更优雅地解决这种情况的能力。我希望上述替代方案能为您提供一些背景信息来评估您的选择。

话虽如此……


您可以对多个 Image 元素使用相同的 RenderTargetBitmap。因此,一种明显的方法是让您的 "shared Canvas" 根本不在可视化图表中;相反,独立维护它,当它的视觉外观发生变化时,将其渲染到用于您的视口的RenderTargetBitmap

这是一个 "really bad code" 示例(即基于您上面的原始内容 :p),它说明了我的意思:

public partial class MainWindow : Window
{
    Canvas _root = new Canvas();

    public MainWindow()
    {
        InitializeComponent();

        _root = new Canvas();
        AddChild(_root);

        //ScrollViewer 1
        ScrollViewer sv = new ScrollViewer();
        sv.Height = 400;
        sv.Width = 600;

        //ScrollerViewer 2
        ScrollViewer sv2 = new ScrollViewer();
        sv2.Height = 400;
        sv2.Width = 200;

        // Will be set later as Content of both Scrollviewers
        Canvas canvas = new Canvas();
        canvas.Width = Width;
        canvas.Height = Height;
        canvas.Background = new SolidColorBrush(Color.FromRgb(255, 255, 0));

        // rectangle to be displayed on the canvas 
        Canvas rect = new Canvas();
        rect.Height = 100;
        rect.Width = 100;
        rect.Background = new SolidColorBrush(Color.FromRgb(255, 0, 0));
        canvas.Children.Add(rect);
        canvas.Measure(new Size(Width, Height));
        canvas.Arrange(new Rect(0, 0, Width, Height));

        RenderTargetBitmap bitmap = new RenderTargetBitmap((int)Width, (int)Height, 96, 96, PixelFormats.Pbgra32);

        bitmap.Render(canvas);

        sv.Content = new Image { Source = bitmap };
        sv2.Content = new Image { Source = bitmap };
        sv.HorizontalScrollBarVisibility = sv.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
        sv2.HorizontalScrollBarVisibility = sv.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;

        _root.Children.Add(sv);
        _root.Children.Add(sv2);

        Canvas.SetLeft(sv, 0);
        Canvas.SetLeft(sv2, 900);
    }
}

请注意,由于 Canvas object 不是可视化树的一部分,您必须通过调用 Measure() 和 [=22= 自己充当它的宿主] 以便它正确初始化其 children 以进行渲染。

或者,您可以提供 Canvas object 作为 Content for one ScrollViewer,然后使用RenderTargetBitmap object 在其他人中。在这种情况下,您不需要自己调用 Measure()Arrange(),但是您 需要确保您不尝试渲染位图直到框架完成为止。例如,不是像上面那样在构造函数中调用 bitmap.Render(canvas);,而是在 Loaded 事件的处理程序中调用它:

        Loaded += (sender, e) =>
        {
            bitmap.Render(canvas);
        };

无论哪种情况,都由您来检测位图何时需要 re-rendered。这可能涉及大量工作,具体取决于渲染的复杂程度。如果您所做的只是 add/remove children,那么在呈现的 Canvas object 上响应 LayoutUpdated 事件可能就足够了。如果您需要响应较小的变化,例如 sub-element 的颜色变化,您可能需要实际 sub-class Canvas 并挂接到适当的事件;例如覆盖 OnRender() 方法并在 base.OnRender() 返回时调用位图的 Render() 方法。