【问题标题】:How to connect ellipses using lines in WPF?如何使用 WPF 中的线条连接椭圆?
【发布时间】:2026-02-11 02:15:01
【问题描述】:

我正在开发一个应用程序来可视化我网络中的路由器。我在每个路由器中有四个或更少的端口。我在画布上有一个带有 X 和 Y 坐标的路由器列表,它们看起来不错。

我需要根据端口和下一个路由器使用一条线将它们链接在一起。

我创建了一个具有两个路由器和两个端口的电缆类。我不知道如何将它们添加到我的窗口的 Itemescontrol 列表中。

Cable.cs
public class Cable
    {

        public Cable()
        {
        }

        public Cable(int rtr1ID, int rtr1Port, int rtr2ID, int rtr2Port)
        {
            this.rtr1ID = rtr1ID;
            this.rtr2ID = rtr2ID;
            this.rtr1Port = rtr1Port;
            this.rtr2Port = rtr2Port;
        }

        private int rtr1ID;
        public int Router1ID
        {
            get { return rtr1ID; }
            set { rtr1ID = value; }
        }

        private int rtr2ID;
        public int Router2ID
        {
            get { return rtr2ID; }
            set { rtr2ID = value; }
        }

        private int rtr1Port;
        public int Router1Port
        {
            get { return rtr1Port; }
            set { rtr1Port = value; }
        }

        private int rtr2Port;
        public int Router2Port
        {
            get { return rtr2Port; }
            set { rtr2Port = value; }
        }
    }
CableService.cs

    public class CableService
    {
        private static List<Cable> cables;

        public CableService()
        {
            cables = new List<Cable>()
            {
                new Cable{ Router1ID=1, Router1Port=1, Router2ID=2, Router2Port=4},
                new Cable{ Router1ID=2, Router1Port=3, Router2ID=5, Router2Port=2},
                new Cable{ Router1ID=5, Router1Port=3, Router2ID=3, Router2Port=2},
                new Cable{ Router1ID=4, Router1Port=4, Router2ID=1, Router2Port=1},
            };
        }

        public List<Cable> GetCables()
        {
            return cables;
        }
    }
Router.cs
public class Router : INotifyPropertyChanged
    {
        public Router()
        {
        }

        public Router(int x, int y, int id)
        {
            this.x = x;
            this.y = y;
            this.id = id;
        }

        private int x;
        public int X
        {
            get { return x; }
            set { x = value; OnPropertyChanged("X"); }
        }

        private int id;
        public int ID
        {
            get { return id; }
            set { id = value; OnPropertyChanged("ID"); }
        }


        private int y;
        public int Y
        {
            get { return y; }
            set { y = value; OnPropertyChanged("Y"); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string popName)
        {
            if(PropertyChanged!=null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(popName));
            }
        }

    }
RouterService.cs
public class RoutersServcie
    {
        private static List<Router> rtrList;
        public RoutersServcie()
        {
            rtrList = new List<Router>()
            {
                new Router{X=5, ID=1, Y=2},
                new Router{X=50, ID=2, Y=20},
                new Router{X=90, ID=3, Y=4},
                new Router{X=10, ID=4, Y=90},
                new Router{X=250, ID=5, Y=15},
            };
        }

        public List<Router> GetRouters()
        {
            return rtrList;
        }
MainWindow.xaml
<Window x:Class="RoutersMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RoutersMVVM"
        mc:Ignorable="d"
        Title="MainWindow" Height="Auto" Width="Auto">
    <DockPanel>
        <Canvas DockPanel.Dock="Top">
            <ItemsControl Name="icCircles">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas Background="Transparent" Width="Auto" Height="Auto"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemContainerStyle>
                    <Style TargetType="ContentPresenter">
                        <Setter Property="Canvas.Left" Value="{Binding Path=X,Mode=TwoWay}"  />
                        <Setter Property="Canvas.Top" Value="{Binding Path=Y,Mode=TwoWay}" />
                    </Style>
                </ItemsControl.ItemContainerStyle>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Ellipse Width="30" Height="30"  Fill="Black" Stroke="Black" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Canvas>
        <StackPanel DockPanel.Dock="Bottom" Height="20">
            <Button Click="Button_Click">Click me</Button>
        </StackPanel>
    </DockPanel>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
    {
        List<Router> line;
        public MainWindow()
        {
            InitializeComponent();
            line = new RoutersServcie().GetRouters();
            icCircles.ItemsSource = line;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            icCircles.ItemsSource = null;
            int x = new Random().Next(0,500);
            int y = new Random(x).Next(0,500);
            int nextId = line.Count()+1;
            line.Add(new Router(y, x, nextId));
            icCircles.ItemsSource = line;
        }
    }

【问题讨论】:

    标签: wpf data-binding


    【解决方案1】:

    您可以在第二个 ItemsControl 中画线,位于带有圆圈的下方。

    像这样的视图模型

    public class ViewModel
    {
        public ObservableCollection<PointItem> Points { get; }
            = new ObservableCollection<PointItem>();
    }
    
    public class PointItem
    {
        public double X { get; set; }
        public double Y { get; set; }
    }
    

    以下 XAML 在 Canvas 父级中声明了两个相互叠加的 ItemsControl。

    线条的 ItemsControl 使用带有 Line 元素的 ItemContainerStyle,该元素的 X1 和 Y1 属性绑定到前一个数据项的 X 和 Y 值,这很容易使用 RelativeSource={RelativeSource Mode=PreviousData} 绑定源模式。该控件通过 ItemsControl.AlternationIndex 附加属性上的触发器隐藏“第一”行。

    请注意,另一个控件使用带有 EllipseGeometry 的 Path 元素而不是 Ellipse 元素,以确保圆在它们各自的位置居中。

    <Canvas>
    
        <ItemsControl ItemsSource="{Binding Points}" AlternationCount="1000000">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Style.Triggers>
                        <Trigger Property="ItemsControl.AlternationIndex" Value="0">
                            <Setter Property="Visibility" Value="Hidden"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Canvas>
                        <Line Stroke="Green" StrokeThickness="3"
                              X1="{Binding X,
                                   RelativeSource={RelativeSource PreviousData}}"
                              Y1="{Binding Y,
                                   RelativeSource={RelativeSource PreviousData}}"
                              X2="{Binding X}"
                              Y2="{Binding Y}"/>
                    </Canvas>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    
        <ItemsControl ItemsSource="{Binding Points}">
            <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>
                    <Canvas>
                        <Path Fill="Red">
                            <Path.Data>
                                <EllipseGeometry RadiusX="15" RadiusY="15"/>
                            </Path.Data>
                        </Path>
                    </Canvas>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    
    </Canvas>
    

    【讨论】:

    • 你的意思是我使用建议的视图模型来替换我的两个视图模型?但是,我怎样才能用电缆连接路由器?两个数据集之间一定有联系!
    • 肯定有这样的链接。您已经拥有电缆中路由器的 ID。构造一个带有“点集合”的视图模型,圆和线可以同时使用。此处的示例仅显示如何在一组圆之间绘制线段,从一个到下一个。它仍然应该让您了解如何从更复杂的拓扑开始。