[英]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
对象作为一个 ScrollViewer
的Content
提供,然后在其他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.