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

时间:2015-11-27 14:49:36

标签: c# wpf user-interface scrollviewer

我需要一台相机"它能够同时显示来自不同视口的画布 。 我的第一个想法是简单地使用2个不同的滚动查看器,并为它们提供与内容相同的画布,并简单地改变它们中的滚动量。

不幸的是,只有一个scrollview显示内容,另一个是空的。这里奇怪的是,将scrollview添加到根元素(在这种情况下也是一个画布)的顺序决定了哪一个获取内容,而不是将内容添加到滚动查看器的顺序。< / p>

那么有可能以某种方式将滚动查看器用于我的目的吗?如果现在,您对如何在同一个画布上实现一个能够拥有2个不同视口的简单相机有任何建议吗?

提前致谢。

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

 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);

    }
}

1 个答案:

答案 0 :(得分:1)

注意:我同意评论者Sinatr的意见,如果可能的话,最好只使用视图模型进行数据模板化。您可以使用单个视图模型,该模型用作两个或多个ContentControl对象的上下文,这些对象只使用为其定义的DataTemplate来呈现该视图模型。这将允许完全的用户交互,最高质量的渲染和最灵活的方法(即,根据您的需要,您的不同“相机”甚至可以为相同的数据呈现截然不同的视觉效果)。

以下是一个示例:

<强> 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,并且由于某种原因(可能是性能问题),实际上明确地为每个“相机”创建单独的对象图是不可取的(但是,我希望WPF能够优化性能以及你或我可以)。但是我没有解决房间里的大象的问题,相对于正常的WPF习语能够比实际尝试构建同一视觉的两个不同“相机”更优雅地解决这个场景的能力。我希望上面的备选方案能为您提供一些评估选项的背景。

据说......


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

这是一个“非常糟糕的代码”示例(即基于您上面的原文: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对象不是可视树的一部分,因此您必须通过调用Measure()Arrange()来自己充当主机,以便正确初始化它的孩子用于渲染。

或者,您可以将Canvas对象作为一个 Content的{​​{1}},然后在其他对象中使用ScrollViewer对象。在这种情况下,您不需要自己调用RenderTargetBitmapMeasure(),但需要确保在框架之前不尝试渲染位图做到了。例如,不要像上面那样在构造函数中调用Arrange(),而是在bitmap.Render(canvas);事件的处理程序中调用它:

Loaded

在任何一种情况下,您都需要检测何时需要重新渲染位图。这可能涉及相当多的工作,具体取决于渲染的复杂程度。如果您只是添加/删除子项,则对呈现的 Loaded += (sender, e) => { bitmap.Render(canvas); }; 对象上的LayoutUpdated事件进行响应可能就足够了。如果您需要响应较小的更改,例如子元素的颜色更改,您可能需要实际上子类Canvas并挂钩到适当的事件;例如覆盖Canvas方法,并在OnRender()返回后调用位图的Render()方法。