通过基本的WPF教新手? (我不要嘲笑它。)

时间:2011-01-22 13:22:04

标签: wpf

问题听起来如此。无论我多么努力,多久我都会尝试理解WPF,我觉得我正撞在墙上。我喜欢Winforms,一切都很有意义。

作为一个例子,我正在尝试编写一个简单的应用程序,它允许我布置一堆二维路径(由折线表示)并拖动它们的顶点,并使顶点信息与演示者同步(也就是说,我想,一个ViewModel)

因此问题是:

  • 使窗口识别IExtendedObjectPresenter的实例作为数据源;
  • IExtendedObject的集合中,为每个IExtendedObject绘制一条折线;
  • 对于扩展对象中的每个顶点,由集合IExtendedObject.Points表示,将折线顶点放置在指定的坐标处。

IDE在这里完全没有给我任何帮助。 XAML中可用的很多属性对我来说都没有意义。因为似乎隐含地做了很多,所以没有明显的地方可以告诉窗户该做什么。

在我定期并告知RTFM之前,我想再次强调我已经多次研究过WPF的基本概念。我对它的了解不比我第一次发布时所做的更多。它似乎完全无法穿透。给出一种行为类型的示例并不以任何方式适用于稍微不同的行为,因此您将回到原点。我希望重复和有针对性的例子可能会在某些时候打开我的脑袋。

2 个答案:

答案 0 :(得分:26)

我同情你。真正理解WPF需要很长时间,完成最简单的事情会非常令人沮丧。但是,潜入一个对专家来说不容易的问题只是要求麻烦。您需要处理更简单的任务并阅读大量代码,直到事情开始变得有意义。唐纳德克努特说,在你做这些练习之前,你并不真正了解这些材料。

我解决了你的问题,我承认有很多先进的概念干净利落地做到这一点并且在MVVM上加强使得它变得更加困难。对于它的价值,这里是一个零问题的代码隐藏解决方案,符合MVVM的精神。

这是XAML:

<Grid>
    <Grid.Resources>
        <local:PolylineCollection x:Key="sampleData">
            <local:Polyline>
                <local:Coordinate X="50" Y="50"/>
                <local:Coordinate X="100" Y="100"/>
                <local:Coordinate X="50" Y="150"/>
            </local:Polyline>
        </local:PolylineCollection>
    </Grid.Resources>
    <Grid DataContext="{StaticResource sampleData}">
        <ItemsControl ItemsSource="{Binding Segments}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}" X2="{Binding End.X}" Y2="{Binding End.Y}" Stroke="Black" StrokeThickness="2"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <ItemsControl ItemsSource="{Binding ControlPoints}">
            <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.ItemTemplate>
                <DataTemplate>
                    <Ellipse Margin="-10,-10,0,0" Width="20" Height="20" Stroke="DarkBlue" Fill="Transparent">
                        <i:Interaction.Behaviors>
                            <local:ControlPointBehavior/>
                        </i:Interaction.Behaviors>
                    </Ellipse>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Grid>

以下是支持类:

public class Coordinate : INotifyPropertyChanged
{
    private double x;
    private double y;

    public double X
    {
        get { return x; }
        set { x = value; OnPropertyChanged("X", "Point"); }
    }
    public double Y
    {
        get { return y; }
        set { y = value; OnPropertyChanged("Y", "Point"); }
    }
    public Point Point
    {
        get { return new Point(x, y); }
        set { x = value.X; y = value.Y; OnPropertyChanged("X", "Y", "Point"); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(params string[] propertyNames)
    {
        foreach (var propertyName in propertyNames)
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class Polyline : List<Coordinate>
{
}

public class Segment
{
    public Coordinate Start { get; set; }
    public Coordinate End { get; set; }
}

public class PolylineCollection : List<Polyline>
{
    public IEnumerable<Segment> Segments
    {
        get
        {
            foreach (var polyline in this)
            {
                var last = polyline.FirstOrDefault();
                foreach (var coordinate in polyline.Skip(1))
                {
                    yield return new Segment { Start = last, End = coordinate };
                    last = coordinate;
                }
            }
        }
    }

    public IEnumerable<Coordinate> ControlPoints
    {
        get
        {
            foreach (var polyline in this)
            {
                foreach (var coordinate in polyline)
                    yield return coordinate;
            }
        }
    }
}

public class ControlPointBehavior : Behavior<FrameworkElement>
{
    private bool mouseDown;
    private Vector delta;

    protected override void OnAttached()
    {
        var canvas = AssociatedObject.Parent as Canvas;
        AssociatedObject.MouseLeftButtonDown += (s, e) =>
        {
            mouseDown = true;
            var mousePosition = e.GetPosition(canvas);
            var elementPosition = (AssociatedObject.DataContext as Coordinate).Point;
            delta = elementPosition - mousePosition;
            AssociatedObject.CaptureMouse();
        };
        AssociatedObject.MouseMove += (s, e) =>
        {
            if (!mouseDown) return;
            var mousePosition = e.GetPosition(canvas);
            var elementPosition = mousePosition + delta;
            (AssociatedObject.DataContext as Coordinate).Point = elementPosition;
        };
        AssociatedObject.MouseLeftButtonUp += (s, e) =>
        {
            mouseDown = false;
            AssociatedObject.ReleaseMouseCapture();
        };
    }
}

此解决方案使用行为,这是实现与MVVM交互的理想选择。

如果您不熟悉行为,请安装Expression Blend 4 SDK并添加此命名空间:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

并将System.Windows.Interactivity添加到您的项目中。

答案 1 :(得分:13)

我将展示如何使用具有可拖动顶点的2D-Poliline构建具有MVVM模式的WPF应用程序。

<强> PointViewModel.cs

public class PointViewModel: ViewModelBase
{
    public PointViewModel(double x, double y)
    {
        this.Point = new Point(x, y);
    }

    private Point point;

    public Point Point
    {
        get { return point; }
        set
        {
            point = value;
            OnPropertyChanged("Point");
        }
    }
}

ViewModelBase仅包含接口INotifyPropertyChanged的实现。这对于反映视觉表现的clr属性的变化是必要的。

<强> LineViewModel.cs

public class LineViewModel
{
    public LineViewModel(PointViewModel start, PointViewModel end)
    {
        this.StartPoint = start;
        this.EndPoint = end;
    }

    public PointViewModel StartPoint { get; set; }
    public PointViewModel EndPoint { get; set; }
}

它引用了Points,因此将自动接收更改。

<强> MainViewModel.cs

public class MainViewModel
{
    public MainViewModel()
    {
        this.Points = new List<PointViewModel>
        {
            new PointViewModel(30, 30),
            new PointViewModel(60, 100),
            new PointViewModel(50, 120)
        };
        this.Lines = this.Points.Zip(this.Points.Skip(1).Concat(this.Points.Take(1)),
            (p1, p2) => new LineViewModel(p1, p2)).ToList();
    }

    public List<PointViewModel> Points { get; set; }
    public List<LineViewModel> Lines { get; set; }
}

它包含点和线的样本数据

<强> MainVindow.xaml

<Window.Resources>
    <ItemsPanelTemplate x:Key="CanvasPanelTemplate">
        <Canvas/>
    </ItemsPanelTemplate>
    <Style x:Key="PointListBoxItem">
        <Setter Property="Canvas.Left" Value="{Binding Point.X}"/>
        <Setter Property="Canvas.Top" Value="{Binding Point.Y}"/>
    </Style>
    <DataTemplate x:Key="LineTemplate">
        <Line X1="{Binding StartPoint.Point.X}" X2="{Binding EndPoint.Point.X}" Y1="{Binding StartPoint.Point.Y}" Y2="{Binding EndPoint.Point.Y}" Stroke="Blue"/>
    </DataTemplate>
    <DataTemplate x:Key="PointTemplate">
        <view:PointView />
    </DataTemplate>
</Window.Resources>
<Grid>
    <ItemsControl ItemsSource="{Binding Lines}" ItemsPanel="{StaticResource CanvasPanelTemplate}" ItemTemplate="{StaticResource LineTemplate}"/>
    <ItemsControl ItemsSource="{Binding Points}" ItemContainerStyle="{StaticResource PointListBoxItem}" ItemsPanel="{StaticResource CanvasPanelTemplate}"
                  ItemTemplate="{StaticResource PointTemplate}"/>
</Grid>

这里有很多技巧。首先,这些ItemsControls不是基于纵向StackPanel,而是基于CanvasItemsControl点应用一个特殊的容器模板,目标是将项目放在必要的坐标上。但ItemsControl行不需要这样的模板,而且在某些时候它很奇怪。最后两个DataTemplates是显而易见的。

<强> PointView.xaml

<Ellipse Width="12" Height="12" Stroke="Red" Margin="-6,-6,0,0" Fill="Transparent"/>

左边和上边距等于WidthHeight的一半。我们有一个透明的Fill,因为此属性没有默认值,并且鼠标事件不起作用。

这几乎都是。只有拖放功能仍然存在。

<强> PointView.xaml.cs

public partial class PointView : UserControl
{
    public PointView()
    {
        InitializeComponent();

        this.MouseLeftButtonDown += DragSource_MouseLeftButtonDown;
        this.MouseMove += DragSource_MouseMove;
    }

    private bool isDraggingStarted;

    private void DragSource_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        this.isDraggingStarted = true;
    }

    private void DragSource_MouseMove(object sender, MouseEventArgs e)
    {
        if (isDraggingStarted == true)
        {
            var vm = this.DataContext as PointViewModel;
            var oldPoint = vm.Point;

            DataObject data = new DataObject("Point", this.DataContext);
            DragDropEffects effects = DragDrop.DoDragDrop(this, data, DragDropEffects.Move);

            if (effects == DragDropEffects.None) //Drag cancelled
                vm.Point = oldPoint;

            this.isDraggingStarted = false;
        }
    }

<强> MainVindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();

        this.AllowDrop = true;
        this.DragOver += DropTarget_DragOver;

    }

    private void DropTarget_DragOver(object sender, DragEventArgs e)
    {
        var vm = e.Data.GetData("Point") as PointViewModel;
        if (vm != null)
            vm.Point = e.GetPosition(this);
    }
}

所以你的样本是使用2个xaml文件和3个视图模型完成的。