大数跨境
0
0

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

WPF 实现工业组态管道,动态布局 + 速度可调流体效果 DotNet技术匠
2025-11-15
0
导读:工业组态界面新方案:WPF实现动态管道与仿真流体。

前言

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

本文将推荐一个基于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的架构设计既保证了开发效率,又为后续功能扩展预留了充足空间。项目可直接应用于能源管理、化工仿真等领域,对于提升工业软件国产化水平具有积极意义。

关键词

#WPF#管道设计器#动态生成算法#组态软件#贝塞尔曲线#MVVM#开源项目#工业仿真#矢量绘图#工业组态软件#管道控件#PBR渲染#流体模拟#Shader编程#智能拼接

最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

作者:小码编匠

出处:gitee.com/smallcore/DotNetCore
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!



END



方便大家交流、资源共享和共同成长
纯技术交流群,需要加入的小伙伴请扫码,并备注加群



推荐阅读






WinForm 的轻量级 SCADA 组态编辑器研究与实现
C# + WinForm 上位机通用框架(WinForm 组态)
开源 HMI/SCADA 系统,低代码拖拽设计的强大 Web 组态工具
推荐一款强大的开源物联网 Web 组态软件

C# 打造轻量级上位机,高效打通 MES 与视觉检测系统

不用 GPU 也能跑的 WPF 视觉检测软件(Emgu CV + SQLite)

C# 工业级扫码难题破解,用微信实现精准扫码并自动填入任意应用

工业软件缺好 UI?这套 .NET 控件库从 IO 灯到圆角按钮全搞定

WPF 如何支撑一个灵活的流程图编辑器?

基于 WinForm GDI+ 的高性能矢量画布组件

WPF + MVVM 重塑康耐视 VisionPro 调试工具
WinForm + MVP 架构联合:打造超实用图书管理系统
WPF 高颜值工业上位机快速开发框架 
WinForm +SQLite 开发的高效PLC数据采集系统
C# 开发的串口固件传输工具 支持OTA升级
C# 实现海康相机 + PLC + 数据库的工业通信集成
WinForm 无线环境监控上位机系统设计与实现
工业自动化实战:C# 实现 Basler 相机图像采集系统
C# 基于机器视觉的液体颜色识别系统

C# 实现 GB28181标准与流媒体推流的完整指南

基于 .NET + Vue 3 的线路图绘制系统实战(含源码)

WinForm 下基于策略与工厂模式的 PLC 数据采集与监控系统

C# 开发工业级温湿度上位机:实时采集与存储

面向工业自动化的 WPF PLC 风格上位机开发框架

C# 写的一个开源免费的OPC UA网关,支持西门子PLC

WinForm + FFmpeg 开发的轻量级视频压缩工具

.NET 8 + Avalonia 跨平台简易校园信息管理系统的开发实战

Windows 服务可视化管理器:安装、启停、定时全搞定

C# + WPF + SuperSocket 开发面向工业自动化的 MES 系统

告别服务宕机,C# 看门狗守护你的 WinForm 与 Windows 服务

.NET 一款高效跨平台的自动更新工具(差异更新+热修复+自动升级)

面向工厂自动化的智能语音播报方案(基于.NET Windows服务)

基于 WPF + Prism 的工业自动化监控系统开源实践

工业自动化UI太难做?WPF 这套工业级控件方案真香(附源码)

工业自动化 WPF + Halcon 的模块化机器视觉解决方案

C# 开源视觉与运动控制集成平台,模块化设计赋能工业自动化

开源福利!八款 WPF + HandyControl 工业管理系统源码全公开

WinForm + Win32 API 自定义无边框窗口实战(工业软件必备)

WPF + MVVM架构的轻量级视频播放器实现

基于 HslCommunication 的多端同步PLC远程监控系统

C# + Vue 面向工业场景的实时数据采集与监控平台

WinForm 数据采集实战:从串口通信到MES对接的轻量化解决方案

一个拒绝过度设计的 .NET 快速开发框架:开箱即用,专注"干活"

C# 工业视觉全流程实战:模板匹配、胶钉定位与下位机通信
WPF 通信控制台:功能丰富、界面美观的上位机开发实战
拿来就用!一个基于 .NET 6 + WPF 的开源数据大屏模板

WinForm + SunnyUI  与 MQTTnet 实现智能可视化的火警联动大屏系统

.NET 9 + WPF + Halcon 构建工业视觉流程框架:从架构设计到落地实践

WinForm 高分屏适配难题?一款强大的控件自适应缩放工具

基于 .NET 6 + OpenCVSharp 的跨平台工业视觉图像分析工具
WinForm 框架下的工控领域视觉检测
基于 .NET 8 + React 的轻量高效库存管理系统(前后端分离)
WPF 实时工业监控大屏:ModBus协议集成与无边框动态可视化方案
图形化操作 Windows 服务?这个开源小工具做到了
.NET 9.0 一个可复用 WPF 界面框架
手把手教会设计 WinForm 高DPI兼容程序,告别字体模糊与控件乱飞

WPF + MVVM 自助式检验报告打印机的多框架实现

C# 部署 Yolov8 全攻略:OpenVINO 与 TensorRT  双引擎加速
WPF 一款通用的嵌入式测控上位机(灵活配置免重复)
全栈 .NET 低代码引擎:权限、工作流、API动态生成,开源即用
一款基于 .NET 的轻量级 ERP 进销存系统:扫码入库、订单变标签,直达发货
.NET 8 + Vue 3 的智能工厂 MES 快速开发框架:设备监控、数据大屏全覆盖
.NET 9 + React 基于 DDD架构的动态路由 + RBAC权限实战
基于 SunnyUI 的企业级 WinForm 快速开发框架,开箱即用!
免硬件方案!基于.NET 的摄像头扫码工具(支持回车/连续扫描)
工业级 MES 系统开发 WPF + MVVM 从入门到实战(全源码/收藏版)


觉得有收获?不妨分享让更多人受益

关注「DotNet技术匠」,共同提升技术实力


收藏
点赞
分享
在看

【声明】内容源于网络
0
0
DotNet技术匠
「DotNet技术匠」聚焦.NET核心,分享深度干货、实战技巧、最新资讯、优质资源,助你领跑技术赛道,赋能开发者成长。
内容 1715
粉丝 0
DotNet技术匠 「DotNet技术匠」聚焦.NET核心,分享深度干货、实战技巧、最新资讯、优质资源,助你领跑技术赛道,赋能开发者成长。
总阅读56
粉丝0
内容1.7k