繁体   English   中英

如何使用Scrollviewers /类似工具为画布实现相机

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

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

不幸的是,只有一个滚动视图显示内容,另一滚动视图为空。 奇怪的是,将滚动视图添加到根元素(在本例中也是画布)的顺序决定了哪个获取内容,而不是将内容添加到滚动视图的顺序。

那么可以出于某种目的使用scrollviewer吗? 如果现在,您是否对如何实现能够在同一Canvas上具有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);

    }
}

注意:我同意评论者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对象作为一个 ScrollViewerContent提供,然后在其他ScrollViewer使用RenderTargetBitmap对象。 在这种情况下,你不会需要调用Measure()Arrange()自己,但需要确保你不尝试呈现位图,直到框架已经这样做了。 例如,而不是调用bitmap.Render(canvas); 在上面的构造函数中,在Loaded事件的处理程序中调用它:

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

无论哪种情况,都取决于您何时需要重新渲染位图。 这可能涉及大量工作,具体取决于渲染的复杂程度。 如果您所做的只是添加/删除子级,则对渲染的Canvas对象上的LayoutUpdated事件做出响应就足够了。 如果您需要对较小的更改(例如子元素的颜色更改)做出响应,则可能实际上需要对Canvas进行子类化,并加入适当的事件; 例如,重写OnRender()方法并在base.OnRender()返回时调用位图的Render()方法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM