我如何使用 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()
方法。
我需要一个 "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()
方法。