大数跨境
0
0

WPF 实现工业组态管道,动态布局 + 速度可调流体效果

WPF 实现工业组态管道,动态布局 + 速度可调流体效果 dotNET跨平台
2025-11-17
5
导读:前言工业自动化领域,组态软件作为人机交互的核心工具,承担着数据采集、过程监控和设备控制等重要功能。其中管道设

前言

工业自动化领域,组态软件作为人机交互的核心工具,承担着数据采集、过程监控和设备控制等重要功能。其中管道设计模块作为流程工业的视觉化基础,其动态建模能力直接影响系统的实用性和用户体验。

本文将推荐一个基于WPF开发的管道设计器,通过创新的动态生成算法实现了管道的实时绘制与交互设计,为组态软件开发提供可复用的技术方案。

项目介绍

传统方案多采用静态图像拼接或预设路径模板,存在灵活性差、开发效率低等问题。项目采用WPF的矢量绘图引擎,通过自定义的路径生成算法,实现了管道的动态建模与实时交互。

项目功能

核心功能模块包含三大交互场景:

路径创建:用户首次点击画布确定起点,移动鼠标时实时生成贝塞尔曲线管道,通过插值算法保证曲线平滑度

节点固定:二次点击确认转折点,系统自动优化路径角度并固定当前段管道

设计终止:右键菜单提供结束设计选项,支持导出SVG格式矢量图或生成设计数据文件

交互流程设计遵循"所见即所得"原则,通过事件驱动机制实现:

  • 鼠标左键:记录坐标点并触发路径计算

  • 鼠标移动:实时更新管道末端位置

  • 鼠标右键:弹出操作菜单

  • 键盘快捷键:支持撤销/重做操作

项目特点

动态建模能力:采用增量式路径生成算法,支持任意转折点的实时添加

视觉优化处理:内置抗锯齿渲染和自适应线宽调整,确保不同分辨率下的显示效果

数据驱动设计:管道属性(直径、颜色、流向)与业务数据解耦,便于后期扩展

跨平台潜力:WPF的跨平台方案(如Avalonia)移植可行性已验证

项目代码

PipeRender的类,核心作用

根据一组管道节点(PipeLine)的坐标信息,动态生成一条具有真实工业外观的、可平滑拐弯的管道矢量图形(PathGeometry),并将其渲染到 WPF 界面中。

public classPipeRender
{
    PipeLine Pipe { getset; }
    public PipeRender(PipeLine pipeLine)
    {
        this.Pipe = pipeLine;
    }
    staticint SpAngleMin = 5;
    staticint SpAngleMax = 175;
    public PathGeometry Render(System.Windows.Controls.Grid control, System.Windows.Controls.Grid tar=null)
    {
        control.Children.Clear();
        using (System.Drawing.Graphics graphics = System.Drawing.Graphics.FromImage(new System.Drawing.Bitmap(11)))
        {
            for (int i = 1; i < Pipe.Count; i++)
            {
                if (Pipe[i].EndPoint.X == Pipe[i - 1].EndPoint.X && Pipe[i].EndPoint.Y == Pipe[i - 1].EndPoint.Y)
                { returnnull; }
                double pipeLen = CalcTools.Distance(Pipe[i].EndPoint.X, Pipe[i].EndPoint.Y, Pipe[i - 1].EndPoint.X, Pipe[i - 1].EndPoint.Y);
                //先沿水平方向生成一个管道
                //左上
                Pipe[i].CalcPoints[0] = new System.Drawing.PointF((float)Pipe[i - 1].EndPoint.X, (float)(Pipe[i - 1].EndPoint.Y - Pipe.PipeDiameter / 2));
                //右上
                Pipe[i].CalcPoints[1] = new System.Drawing.PointF((float)(Pipe[i - 1].EndPoint.X + pipeLen), (float)(Pipe[i - 1].EndPoint.Y - Pipe.PipeDiameter / 2));
                //左下
                Pipe[i].CalcPoints[2] = new System.Drawing.PointF((float)Pipe[i - 1].EndPoint.X, (float)(Pipe[i - 1].EndPoint.Y + Pipe.PipeDiameter / 2));
                //右下
                Pipe[i].CalcPoints[3] = new System.Drawing.PointF((float)(Pipe[i - 1].EndPoint.X + pipeLen), (float)(Pipe[i - 1].EndPoint.Y + Pipe.PipeDiameter / 2));
                
                double pipeRotageAngle = CalcTools.Angle(new System.Windows.Point(Pipe[i - 1].EndPoint.X, Pipe[i - 1].EndPoint.Y)
                    , new System.Windows.Point(Pipe[i - 1].EndPoint.X + 100, Pipe[i - 1].EndPoint.Y)
                    , new System.Windows.Point(Pipe[i].EndPoint.X, Pipe[i].EndPoint.Y));
                CalcTools.VectorClockDirection pipeDir = CalcTools.VectorClockDirectionCalc(new System.Windows.Point(Pipe[i - 1].EndPoint.X + 100, Pipe[i - 1].EndPoint.Y)
                    , new System.Windows.Point(Pipe[i - 1].EndPoint.X, Pipe[i - 1].EndPoint.Y)
                    , new System.Windows.Point(Pipe[i].EndPoint.X, Pipe[i].EndPoint.Y));
                pipeRotageAngle = (pipeDir == CalcTools.VectorClockDirection.Clockwise ? pipeRotageAngle : -pipeRotageAngle);
                //将水平管道旋转到实际的角度
                graphics.TranslateTransform((float)Pipe[i - 1].EndPoint.X, (float)Pipe[i - 1].EndPoint.Y);
                graphics.RotateTransform((float)pipeRotageAngle);//旋转角度
                graphics.TranslateTransform(-(float)Pipe[i - 1].EndPoint.X, -(float)Pipe[i - 1].EndPoint.Y);
                graphics.Transform.TransformPoints(Pipe[i].CalcPoints);
                graphics.ResetTransform();
            }
        }

        for (int i = 1; i < Pipe.Count; i++)
        {
            if (i < Pipe.Count - 1)
            {
                //同心圆的圆心
                Pipe[i].Angle = CalcTools.Angle(Pipe[i].EndPoint, Pipe[i - 1].EndPoint, Pipe[i + 1].EndPoint);
                Pipe[i].ClockDirection = CalcTools.VectorClockDirectionCalc(Pipe[i - 1].EndPoint, Pipe[i].EndPoint, Pipe[i + 1].EndPoint);
                System.Windows.Point CircleCenterPoint = new System.Windows.Point();

                Pipe[i].SmoothConnect = true;
                Pipe[i].Aviable = true;
                if (SpAngleMin < Pipe[i].Angle && Pipe[i].Angle <= SpAngleMax)
                {
                    //管道的交点
                    System.Windows.Point Intersection1 = CalcTools.GetIntersection5_175(Pipe[i].CalcPoints[0], Pipe[i].CalcPoints[1], Pipe[i + 1].CalcPoints[0], Pipe[i + 1].CalcPoints[1]);
                    System.Windows.Point Intersection2 = CalcTools.GetIntersection5_175(Pipe[i].CalcPoints[2], Pipe[i].CalcPoints[3], Pipe[i + 1].CalcPoints[2], Pipe[i + 1].CalcPoints[3]);
                    Pipe[i].Intersection1 = Intersection1;
                    Pipe[i].Intersection2 = Intersection2;
                    //计算同心圆的圆心
                    float per = 1.5f;
                    per = per * (float)(Pipe[i].Angle / 180);

                    if (Pipe[i].ClockDirection == CalcTools.VectorClockDirection.Clockwise)
                    {
                        CircleCenterPoint.X = Intersection1.X + (Intersection1.X - Intersection2.X) * per;
                        CircleCenterPoint.Y = Intersection1.Y + (Intersection1.Y - Intersection2.Y) * per;
                    }
                    else
                    {
                        CircleCenterPoint.X = Intersection2.X + (Intersection2.X - Intersection1.X) * per;
                        CircleCenterPoint.Y = Intersection2.Y + (Intersection2.Y - Intersection1.Y) * per;
                    }
                    Pipe[i].P11 = CalcTools.LinePointProjection(Pipe[i].CalcPoints[0], Pipe[i].CalcPoints[1], CircleCenterPoint);//前一根管道
                    Pipe[i].P12 = CalcTools.LinePointProjection(Pipe[i].CalcPoints[2], Pipe[i].CalcPoints[3], CircleCenterPoint);//前一根管道
                    Pipe[i].P21 = CalcTools.LinePointProjection(Pipe[i + 1].CalcPoints[0], Pipe[i + 1].CalcPoints[1], CircleCenterPoint);//后一根管道
                    Pipe[i].P22 = CalcTools.LinePointProjection(Pipe[i + 1].CalcPoints[2], Pipe[i + 1].CalcPoints[3], CircleCenterPoint);//后一根管道
                    //管道长度不够需要延长
                    if (Pipe[i + 1].CalcPoints[0].X < Pipe[i + 1].CalcPoints[1].X && Pipe[i + 1].CalcPoints[1].X < Pipe[i].P21.X)
                    {
                        Pipe[i + 1].CalcPoints[1] = CalcTools.WinPoint2DrawPointF(Pipe[i].P21);
                        Pipe[i + 1].CalcPoints[3] = CalcTools.WinPoint2DrawPointF(Pipe[i].P22);
                        Pipe[i + 1].EndPoint = new Point(Pipe[i].P21.X + (Pipe[i].P22.X - Pipe[i].P21.X) / 2
                            , Pipe[i].P21.Y + (Pipe[i].P22.Y - Pipe[i].P21.Y) / 2);
                    }
                    elseif (Pipe[i + 1].CalcPoints[0].X > Pipe[i + 1].CalcPoints[1].X && Pipe[i + 1].CalcPoints[1].X > Pipe[i].P21.X)
                    {
                        Pipe[i + 1].CalcPoints[1] = CalcTools.WinPoint2DrawPointF(Pipe[i].P21);
                        Pipe[i + 1].CalcPoints[3] = CalcTools.WinPoint2DrawPointF(Pipe[i].P22);
                        Pipe[i + 1].EndPoint = new Point(Pipe[i].P21.X + (Pipe[i].P22.X - Pipe[i].P21.X) / 2
                            , Pipe[i].P21.Y + (Pipe[i].P22.Y - Pipe[i].P21.Y) / 2);
                    }
                    //计算同心圆的半径
                    Pipe[i].Diameter1 = CalcTools.Distance(CircleCenterPoint.X, CircleCenterPoint.Y, Pipe[i].P11.X, Pipe[i].P11.Y);
                    Pipe[i].Diameter2 = CalcTools.Distance(CircleCenterPoint.X, CircleCenterPoint.Y, Pipe[i].P12.X, Pipe[i].P12.Y);
                    Pipe[i].CircleCenterPoint = new Point(CircleCenterPoint.X, CircleCenterPoint.Y);
                }
                elseif (Pipe[i].Angle > SpAngleMax)
                {
                    Pipe[i].Intersection1 = CalcTools.GetIntersection5_175(Pipe[i].CalcPoints[0], Pipe[i].CalcPoints[1], Pipe[i + 1].CalcPoints[0], Pipe[i + 1].CalcPoints[1]);
                    Pipe[i].Intersection2 = CalcTools.GetIntersection5_175(Pipe[i].CalcPoints[2], Pipe[i].CalcPoints[3], Pipe[i + 1].CalcPoints[2], Pipe[i + 1].CalcPoints[3]);
                    Pipe[i].SmoothConnect = false;
                }
                elseif (Pipe[i].Angle <= SpAngleMin)
                {
                    Pipe[i].Aviable = false;
                    returnnull;
                }
            }
        }

        if (this.Pipe.Count <= 1)
        { returnnull; }

        PathGeometry pathGeometryF = new PathGeometry();
        PathFigure pathFigureF = new PathFigure();
        pathFigureF.StartPoint = new Point(Pipe[1].CalcPoints[0].X, Pipe[1].CalcPoints[0].Y);

        for (int i = 1; i < Pipe.Count - 1; i++)
        {
            PipeEndPoint pipeEnd = Pipe[i];
            if (pipeEnd.Aviable)
            {
                if (pipeEnd.SmoothConnect)
                {
                    SweepDirection dir = pipeEnd.ClockDirection == CalcTools.VectorClockDirection.CounterClockwise
                        ? SweepDirection.Clockwise
                        : SweepDirection.Counterclockwise;
                    pathFigureF.Segments.Add(new LineSegment(pipeEnd.P11, false));
                    ArcSegment arc1 = new ArcSegment(pipeEnd.P21, new Size(pipeEnd.Diameter1, pipeEnd.Diameter1), pipeEnd.Angle
                        , false, dir, false);
                    pathFigureF.Segments.Add(arc1);
                }
                else
                {
                    pathFigureF.Segments.Add(new LineSegment(pipeEnd.Intersection1, false));
                }
            }
        }
        pathFigureF.Segments.Add(new LineSegment(CalcTools.WinPoint2DrawPointF(Pipe[Pipe.Count - 1].CalcPoints[1]), false));
       
        pathFigureF.Segments.Add(new LineSegment(CalcTools.WinPoint2DrawPointF(Pipe[Pipe.Count - 1].CalcPoints[3]), false));
        for (int i = Pipe.Count - 2; i > 0; i--)
        {
            if (Pipe[i].Aviable)
            {
                if (Pipe[i].SmoothConnect)
                {
                    SweepDirection dir = Pipe[i].ClockDirection == CalcTools.VectorClockDirection.CounterClockwise
                        ? SweepDirection.Counterclockwise
                        : SweepDirection.Clockwise;
                    pathFigureF.Segments.Add(new LineSegment(Pipe[i].P22, false));
                    ArcSegment arc = new ArcSegment(Pipe[i].P12, new Size(Pipe[i].Diameter2, Pipe[i].Diameter2), Pipe[i].Angle
                        , false, dir, false);
                    pathFigureF.Segments.Add(arc);
                }
                else
                {
pathFigureF.Segments.Add(new LineSegment(Pipe[i].Intersection2, false));
                }
            }
        }
  pathFigureF.Segments.Add(new LineSegment(CalcTools.WinPoint2DrawPointF(Pipe[1].CalcPoints[2]), false));

        pathFigureF.IsClosed = true;
        pathGeometryF.Figures.Add(pathFigureF);
        return pathGeometryF;
    }
}

项目效果

通过与流体模拟模块结合,可实现管道内介质流动的视觉化呈现,包括流速变化、压力分布等高级效果。




项目源码

完整代码托管于Gitee平台,建议开发环境:Visual Studio 2022 + .NET 6.0,项目已配置好NuGet依赖包,可直接编译运行。

Gitee:https://gitee.com/hurstyang/pipedesigner

总结

通过创新性的动态路径生成算法,解决了传统组态软件管道设计中的灵活性不足问题。基于WPF的架构设计既保证了开发效率,又为后续功能扩展预留了充足空间。项目可直接应用于能源管理、化工仿真等领域,对于提升工业软件国产化水平具有积极意义。 

【声明】内容源于网络
0
0
dotNET跨平台
专注于.NET Core的技术传播。在这里你可以谈微软.NET,Mono的跨平台开发技术。在这里可以让你的.NET项目有新的思路,不局限于微软的技术栈,横跨Windows,
内容 883
粉丝 0
dotNET跨平台 专注于.NET Core的技术传播。在这里你可以谈微软.NET,Mono的跨平台开发技术。在这里可以让你的.NET项目有新的思路,不局限于微软的技术栈,横跨Windows,
总阅读14.4k
粉丝0
内容883