如何在分离视图和模型时将世界坐标映射到屏幕坐标?

How to map world coordinates to screen coordinates while separating view & model?

我正在使用 WPF 在 C# 中编写 n 体模拟作为练习,我 运行 解决了显示程序结果的基本设计问题。

我有一个实体 class,它存储位置、速度、质量等基本信息。然后是 PhysicsEngine class,它具有实体的 ObservableCollection 并执行所有数学运算。当我必须将实体的位置绑定到某些图形元素以在屏幕上显示它们的移动时,问题就出现了。 space 中的距离很大,所以我显然需要以某种方式处理位置信息并将其 运行 转换为屏幕坐标。作为快速破解,我将其添加到实体 class:

public Vector3 ScreenPosition
{
     get
     {
          return new Vector3(
               (Pos.X / Scale) - (Diameter / 2),
               (Pos.Y / Scale) - (Diameter / 2),
               (Pos.Z / Scale) - (Diameter / 2)
               // where Scale is an arbitrary number depending on how big the simulation is
          );
     }
}

这只是 returns 经过一些数学运算以适合屏幕上的所有内容的位置。当“相机”静止时效果很好,但现在我想让它移动 - 也许将相机放在某个星球上,或者放大和缩小。

继续使用这个 hack 看起来很难看 - 我不得不将关于相机位置和缩放级别的各种细节暴露给实体的低级别 class,这真的不应该关心View 或知道它是如何显示的。

我尝试制作 DisplayEntities 的第二个 ObservableCollection,它包含显示每个实体所需的所有数据,并且在每次模拟时,它会循环遍历实体列表,然后在 DisplayEntities 中更新它们各自的兄弟,并在视图的代码隐藏我以编程方式向每个 DisplayEntity 添加和绑定几何形状,但事实证明这非常缓慢且不切实际 - 我需要遍历所有实体,检查它们是否已经有 DisplayEntity,如果有则更新,如果没有,请添加一个新的 DisplayEntity,更不用说删除实体时会发生什么了。

我还尝试将实体 class 包装在另一个 class 中,其中包含显示它所需的所有信息,这消除了两个集合的问题,但似乎是相同的抽象问题和以前一样出现了——EntityVM 必须知道相机位置、角度、缩放级别,我必须在每个刻度上遍历每一个并更新它们的值——又慢又不灵活。

来自 WinForms 中的直接图形,这种情况看起来真的很令人沮丧 - 在 WinForms 中,我可以在代码隐藏中创建一个函数,当被调用时在某某坐标处绘制圆圈,做我想做的任何数学运算,因为我不需要考虑绑定。每当我想绘制任何东西时,我只需要向它传递一个坐标列表,它根本不关心我的实际对象,具有讽刺意味的是,这似乎比我用 WPF 烹制的意大利面条更好地将视图与模型分开。

我该如何设计它才能产生一个优雅的、非 clusterfuck 的解决方案?

(在此先感谢您,如果我的 post 在某些方面有所欠缺,请告诉我,这是我第一次 posting :) )


编辑:为清楚起见,这是我认为代码隐藏的重要部分:

public void AddEntitiesToCanvas()
{
    PhysicsEngine engine = (PhysicsEngine)this.DataContext;
    for (int i = 0; i < engine.Entities.Count; i++)
    {
        Binding xBinding = new Binding("Entities[" + i + "].VPos.X");
        Binding yBinding = new Binding("Entities[" + i + "].VPos.Y");
        Binding DiameterBinding = new Binding("Entities[" + i + "].Diameter");

        Ellipse EntityShape = new Ellipse();
        EntityShape.Fill = new SolidColorBrush(Colors.Black);

        EntityShape.SetBinding(WidthProperty, DiameterBinding);
        EntityShape.SetBinding(HeightProperty, DiameterBinding);
        EntityShape.SetBinding(Canvas.LeftProperty, xBinding);
        EntityShape.SetBinding(Canvas.TopProperty, yBinding);
        EntityShape.SetValue(Canvas.ZIndexProperty, 100);

        canvas.Children.Add(EntityShape);
    }
}

XML 文件只包含一个空的 Canvas。


编辑 2:这是我更新视图的重要部分

<DataTemplate>
    <Path Fill="Black">
        <Path.RenderTransform>
            <TranslateTransform X="{Binding VPos.X}" Y="{Binding VPos.Y}"/>
        </Path.RenderTransform>
        <Path.Data>
            <EllipseGeometry Center="0, 0" RadiusX="{Binding Diameter}" RadiusY="{Binding Diameter}"/>
        </Path.Data>
    </Path>
</DataTemplate>

编辑 3:我尝试使用绑定转换器;但是,转换器还需要从 PhysicsEngine class 访问相机信息才能执行计算。我想过让转换器成为 PhysicsEngine class 的 属性 以便它可以访问所有私人信息然后这样做,这显然行不通:

<Path.RenderTransform>
    <TranslateTransform X="{Binding Pos.X, Converter={Binding ScreenPosConverter}}" Y="{Binding Pos.Y, Converter={Binding ScreenPosConverter}}"/>
</Path.RenderTransform>

Binding Converter 是适合这项工作的工具吗?如果是,我如何将相机信息传递给它?

我最终用 MultiBinding 和 IMultiValueConverter 做到了:


<ItemsControl ItemsSource="{Binding Entities}">

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas x:Name="canvas"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Path Fill="{Binding Color}">

                <Path.RenderTransform>
                    <TranslateTransform>

                        <TranslateTransform.X>
                            <MultiBinding  Converter="{StaticResource converter}">
                                <Binding Path="Pos.X"/>
                                <Binding Path="DataContext.Camera.X" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Grid}"/>
                            </MultiBinding>
                        </TranslateTransform.X>

                        <TranslateTransform.Y>
                            <MultiBinding Converter="{StaticResource converter}">
                                <Binding Path="Pos.Y"/>
                                <Binding Path="DataContext.Camera.Y" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Grid}"/>
                            </MultiBinding>
                        </TranslateTransform.Y>

                    </TranslateTransform>
                </Path.RenderTransform>

                <Path.Data>
                    <EllipseGeometry Center="0, 0" RadiusX="{Binding Diameter}" RadiusY="{Binding Diameter}"/>
                </Path.Data>

            </Path>
        </DataTemplate>
    </ItemsControl.ItemTemplate>

</ItemsControl>