我需要一台相机"它能够同时显示来自不同视口的画布 。 我的第一个想法是简单地使用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);
}
}
答案 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
对象。在这种情况下,您不需要自己调用RenderTargetBitmap
和Measure()
,但将需要确保在框架之前不尝试渲染位图做到了。例如,不要像上面那样在构造函数中调用Arrange()
,而是在bitmap.Render(canvas);
事件的处理程序中调用它:
Loaded
在任何一种情况下,您都需要检测何时需要重新渲染位图。这可能涉及相当多的工作,具体取决于渲染的复杂程度。如果您只是添加/删除子项,则对呈现的 Loaded += (sender, e) =>
{
bitmap.Render(canvas);
};
对象上的LayoutUpdated
事件进行响应可能就足够了。如果您需要响应较小的更改,例如子元素的颜色更改,您可能需要实际上子类Canvas
并挂钩到适当的事件;例如覆盖Canvas
方法,并在OnRender()
返回后调用位图的Render()
方法。