Подтвердить что ты не робот

Тренер новичков через базовый WPF? (Я не делаю этого.)

Вопрос такой же, как кажется. Независимо от того, насколько сложно и как часто я пытаюсь понять WPF, мне кажется, что я ударился головой о стену. Мне нравится Winforms, где все имеет смысл.

В качестве примера я пытаюсь написать простое приложение, которое позволит мне выложить кучу 2-D путей (представленных полилиниями) и перетащить их вершины вокруг, а информация о вершинах синхронизируется с ведущим (то есть, я полагаю, ViewModel)

Таким образом, проблема такова:

  • Сделать окно распознать экземпляр IExtendedObjectPresenter в качестве источника данных;
  • Из коллекции IExtendedObject нарисуйте одну полилинию для каждого IExtendedObject;
  • Для каждой вершины расширенного объекта, представленного коллекцией IExtendedObject.Points, поместите вершину полилинии в указанные координаты.

IDE вообще не дает мне никакой помощи. Мне кажется, что ни один из свойств много, доступных для XAML. Поскольку так кажется, что это делается неявно, нет очевидного места, чтобы просто указать окну, что делать.

Прежде чем я узнаю RTTM, я хотел бы еще раз подчеркнуть, что я много раз изучал основные понятия WPF. Я не знаю об этом больше, чем когда это было в первый раз. Это кажется совершенно непонятным. Примеры, приведенные для одного типа поведения, никоим образом не применимы к даже немного отличающемуся поведению, поэтому вы вернетесь к квадрату. Я надеюсь, что повторение и целевые экзамены могут включить свет в моей голове в какой-то момент.

4b9b3361

Ответ 1

Я сочувствую вам. На самом деле понимание 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 к вашему проекту.

Ответ 2

Я покажу, как создать WPF-приложение с шаблоном MVVM для 2D-Poliline с перетаскиваемыми вершинами.

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, а на Canvas. ItemsControl точек применяет специальный шаблон контейнера с целью размещения элементов на необходимых координатах. Но строки ItemsControl не требуют таких шаблонов, и в какой-то момент это странно. Два последних DataTemplates очевидны.

PointView.xaml

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

Поля слева и сверху равны половине Width и Height. У нас есть прозрачный Fill, потому что это свойство не имеет значения по умолчанию и события мыши не работают.

Это почти все. Остается только функция drag-n-drop.

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-х моделей.