WPF可视化一个简单的2D世界

问 题

    

I'm fairly new to WPF and looking for a simple solution to the problem described below. I've tried to make this as short as possible.

I'm trying to visualize a "world" that is modeled by:

  • A map image, with a known origin in meters (e.g. top-left corner is 14,27) and resolution in cm/pixel. The map keeps growing every few seconds. Maps are small so no paging/tiling is required.
  • Real-world elements and points of interest. Each element has a 2D position in meters within the map area. Also, each element might move.

Regarding the model side, I have a WorldState class that keeps the map and elements:

interface IWorldState
{
    IEnumerable<IWorldElement> Elements { get; }
    IMapData CurrentMap { get; }
}

interface IWorldElement
{
    WorldLocation { get; }
    event EventHandler LocationChanged;
}

interface IMapData
{
    string FilePath { get; }
    WorldLocation TopLeft { get; }
    Size MapSize { get; }
}

Now regarding the visualization, I've chosen the Canvas class to draw the map and elements. Each type of element (inherits from IWorldElement) should be drawn differently. There might be more than one map canvas, with a subset of the elements.

<Canvas x:Name="mapCanvas">
    <Image x:Name="mapImage" />
</Canvas>

In code, I need to set the map image file when it changes:

void MapChanged(IWorldState worldState)
{
    mapImage.Source = worldState.CurrentMap.FilePath;
}

To draw the elements, I have a method to convert WorldLocation to (Canvas.Left, Canvas.Top) :

Point WorldToScreen(WorldLocation worldLocation, IWorldState worldState)
{
    var topLeft = worldState.CurrentMap.TopLeft;
    var size = worldState.CurrentMap.Size;
    var left = ((worldLocation.X - topLeft.X) / size.X) * mapImage.ActualWidth;
    var top = ((worldLocation.Y - topLeft.Y) / size.Y) * mapImage.ActualHeight;
    return new Point(left, top);
}

Now for the question, how should I glue the world model and the canvas together? This can be summarized by:

  1. Where to put the MapChanged and WorldToScreen functions.
  2. When an element moves the world location needs to be converted to screen coordinates.
  3. Each type of element should be drawn differently, for example ellipse with text or filled rectangle.

What is the recommended way to implement the "glue" layer when using WPF?

解决方案

DataBinding is the way to go. Read here, http://msdn.microsoft.com/en-us/magazine/dd419663.aspx.

Once you've set up a viewmodel and set the datacontext of the view.

I suggest putting your elements in an observablecollection and bind it to an itemscontrol in the canvas.

For the elements to be positioned, you must create a custom ItemTemplate for the ItemsControl which uses a canvas, rather than the default stackpanel as container.

Then you can go ahead and create datatemplate for the various types of elements you have to get a specific look and feel pr element type.

This is a rough outline of a solution, hope this helps.

Example:

<Window x:Class="WorldCanvas.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WorldCanvas"
Title="WorldCanvas" Height="500" Width="500"
>
<Window.Resources>
    <DataTemplate DataType="{x:Type local:HouseVM}" >
        <Canvas>
            <Rectangle Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}" Width="13" Height="23" Fill="Brown" />
        </Canvas>
    </DataTemplate>

    <DataTemplate DataType="{x:Type local:BallVM}">
        <Canvas>
            <Ellipse Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}" Width="13" Height="13" Fill="Blue" />
        </Canvas>
    </DataTemplate>
</Window.Resources>
<Grid>
    <Canvas x:Name="TheWorld" Background="DarkGreen">
    <Button Content="MoveFirst" Click="Button_Click" />
    <ItemsControl ItemsSource="{Binding Entities}">
        <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Left" Value="{Binding X}" />
                <Setter Property="Canvas.Top" Value="{Binding Y}" />
            </Style>
        </ItemsControl.ItemContainerStyle>
    </ItemsControl>
</Canvas>

</Grid>

 

    public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
        var worldViewModel = new WorldViewModel();
        DataContext = worldViewModel;
    }

    void Button_Click(object sender, RoutedEventArgs e)
    {
        var viewModel = DataContext as WorldViewModel;
        if(viewModel != null)
        {
            var entity = viewModel.Entities.First();
            entity.X +=10;
        }
    }
}

Viewmodels

  public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string propertyName)
    {
        if(PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

public class WorldViewModel : ViewModelBase
{
    ObservableCollection<EntityVM> entities;

    public ObservableCollection<EntityVM> Entities {
        get { return entities; }
        set 
        { 
            entities = value;
            NotifyPropertyChanged("Entities"); 
        }
    }

    public WorldViewModel()
    {
        Entities = new ObservableCollection<EntityVM>();
        int y=0;
        for(int i=0; i<30; i++)
        {
            if(i %2 == 0)
            {
                Entities.Add(new BallVM(i*10, y+=20));
            }
            else
            {
                Entities.Add(new HouseVM(i*20, y+=20));
            }
        }
    }       
}   

public class EntityVM : ViewModelBase
{
    public EntityVM(double x, double y)
    {
        X = x;
        Y = y;
    }

    private double _x;
    public double X
    {
        get
        {
            return _x;
        }
        set
        {
            _x = value;
            NotifyPropertyChanged("X");
        }
    }

    private double _y;
    public double Y
    {
        get
        {
            return _y;
        }
        set
        {
            _y = value;
            NotifyPropertyChanged("Y");
        }
    }
}

public class BallVM : EntityVM
{
    public BallVM(double x, double y) : base(x, y)
    {
    }
}

public class HouseVM : EntityVM
{
    public HouseVM(double x, double y)  : base(x, y)
    {
    }
}
posted @ 2017-07-26 09:42  不惑不悔  阅读(1026)  评论(0编辑  收藏  举报