博客 / 詳情

返回

WPF 如何支撐一個靈活的流程圖編輯器?

前言

軟件開發領域,流程設計與可視化是提升系統可維護性、增強用户體驗的重要手段。無論是工作流管理、業務邏輯編排還是算法流程展示,一個靈活、易用的流程節點編輯框架都能極大地提高開發效率與系統靈活性。

本文將推薦一款基於 WPF 的開源流程節點編輯框架,通過對其核心設計與實現邏輯的解析,帶領大家從零開始手寫一個具備基礎功能的 WPF 流程圖編輯器,為實際項目中的可視化流程開發提供有價值的參考。

項目介紹

一款基於WPF的圖形化流程節點編輯工具,為大家提供一個直觀、高效的流程設計與編輯環境。通過拖拽節點、連接線等操作,可以輕鬆開發複雜的流程圖,實現業務邏輯的可視化表達。

項目功能

1、節點創建與編輯

支持多種類型的節點創建,包括但不限於開始節點、結束節點、處理節點、決策節點等。

通過簡單的點擊或拖拽操作,在畫布上添加、刪除或修改節點屬性,如節點名稱、顏色、形狀等。

2、連線管理

節點間通過連線表示流程走向,提供靈活的連線創建與編輯功能。

輕鬆地在節點間繪製直線、曲線或折線,調整連線的起點與終點,甚至設置連線的條件表達式,實現條件分支。

3、佈局調整與邊界擴展

滿足不同場景下的展示需求,支持畫布的縮放、平移以及節點的自動佈局功能。

根據需要調整畫布大小,通過手勢或按鈕控制畫布的顯示範圍。同時,框架還提供了邊界擴展功能,當節點靠近畫布邊緣時,自動擴展畫布大小,確保所有節點都能完整顯示。

4、框選與拖動

支持框選多個節點進行批量操作,如移動、刪除等。可以通過鼠標拖拽選擇框,選中多個節點後進行統一操作。同時,框架還提供節點的拖動功能,用户可以拖動單個或多個節點到指定位置。

5、數據綁定與交互

支持與後端數據的綁定,將流程圖中的節點屬性與數據庫字段或API接口關聯,實現數據的動態展示與交互。

另外,框架還提供了事件處理機制,用户可以自定義節點點擊、連線雙擊等事件的處理邏輯,增強流程圖的交互性。

項目特點

1、界面友好

WPF的豐富UI控件與動畫效果,提供直觀、美觀的操作界面。無論是節點的拖拽、連線的繪製還是屬性的編輯,都能帶來流暢的操作體驗。

2、擴展性強

設計遵循模塊化原則,各個功能模塊相對獨立,便於開發者根據需求進行二次開發或功能擴展。同時,框架提供豐富的API接口,方便與其他系統進行集成。

3、交互豐富

支持多種交互方式,如框選、拖動、右鍵菜單等,可以根據需要選擇合適的交互方式,提高操作效率。

項目技術

1、MVVM設計模式

為了實現界面與邏輯的分離,提高代碼的可維護性與可測試性,框架採用MVVM(Model-View-ViewModel)設計模式。通過數據綁定與命令機制,實現了視圖與模型之間的鬆耦合。

2、自定義控件開發

針對流程節點編輯的特殊需求,開發一系列自定義控件,如節點控件、連線控件等。控件通過繼承WPF的基礎控件類,實現了特定的功能與行為。

4、事件處理與委託

通過事件處理與委託機制,實現用户交互的響應與處理。無論是節點的點擊、連線的雙擊還是畫布的縮放,都能通過事件處理函數實現相應的邏輯。

項目代碼

框選拖動

/// <summary>
/// 添加連線更新方法
/// </summary>
/// <param name="selectedControls">選中的控件</param>
/// <param name="Xoffset">連線位置X軸的偏移量</param>
/// <param name="Yoffset">連線位置Y軸的偏移量</param>
private void UpdateConnectionsForNode(List<Control> selectedControls, double Xoffset = 0, double Yoffset = 0)
{
    // 當前不存在連線的話直接結束
    if (connections.Count == 0) return;

    foreach (XNode node in selectedControls)
    {
        List<Connection> refConns = connections.FindAll(conn => IsChildOf(conn.FromPort, node) || IsChildOf(conn.ToPort, node));

        refConns.ForEach(conn =>
        {
            Point p1 = PointAdd(GetPortCenter(conn.FromPort), new Point(Xoffset, Yoffset));
            Point p2 = PointAdd(GetPortCenter(conn.ToPort), new Point(Xoffset, Yoffset));

            var geometry = new PathGeometry();
            var figure = new PathFigure { StartPoint = p1 };
            var segment = CreateSegment("polyline", p1, p2);
            figure.Segments.Add(segment);
            geometry.Figures.Add(figure);
            conn.Path.Data = geometry;
        });
    }
}

// 點位相加
private Point PointAdd(Point p1, Point p2)
{
    return new Point(p1.X + p2.X, p1.Y + p2.Y);
}

private bool IsChildOf(FrameworkElement child, FrameworkElement parent)
{
    var current = child;
    while (current != null)
    {
        if (current == parent)
            return true;
        current = VisualTreeHelper.GetParent(current) as FrameworkElement;
    }
    return false;
}

線條拖動 | 線條類型切換

private void OutputPort_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (sender is FrameworkElement outputPort)
    {
        fromPort = outputPort;
        startPoint = GetPortCenter(outputPort);

        currentPath = new System.Windows.Shapes.Path
        {
            Stroke = Brushes.MediumPurple,
            StrokeThickness = lineThickness,
            Data = new PathGeometry()
        };
        currentPath.MouseLeftButtonDown += Path_MouseLeftButtonDown;
        currentPath.MouseEnter += Path_MouseEnter;
        currentPath.MouseLeave += Path_MouseLeave;

        MainCanvas.Children.Add(currentPath);
        isConnecting = true;

        e.Handled = true;
    }
}

private Point GetPortCenter(FrameworkElement port)
{
    var point = new Point(port.Width / 2, port.Height / 2);
    // 將當前點相對於port的座標轉換為當前點相對於Canvas的座標位置,Canvas會先獲取point左上角的位置,然後再偏移point.X,point.Y
    var position = port.TranslatePoint(point, MainCanvas);
    return position;
}

private PathSegment CreateSegment(string type, Point startPoint, Point endPoint)
{
    if (string.IsNullOrEmpty(type))
        throw new Exception("type 類型不能為空");
    PathSegment segment;
    if (type == "polyline")
    {
        if (startPoint.X <= endPoint.X - 40) // 兩邊距離大於40
        {
            double centerX = (startPoint.X + endPoint.X) / 2;
            var polyline = new PolyLineSegment
            {
                Points = new PointCollection()
                {
                    new Point(centerX,startPoint.Y),
                    new Point(centerX,endPoint.Y),
                    new Point(endPoint.X,endPoint.Y)    // 終點
                }
            };
            segment = polyline;
        }
        else
        {
            double centerY = (startPoint.Y + endPoint.Y) / 2;
            var polyline = new PolyLineSegment
            {
                Points = new PointCollection()
                {
                    new Point(startPoint.X + 20,startPoint.Y),
                    new Point(startPoint.X + 20,centerY),
                    new Point(endPoint.X - 20,centerY),
                    new Point(endPoint.X - 20,endPoint.Y),
                    new Point(endPoint.X,endPoint.Y)    // 終點
                }
            };
            segment = polyline;
        }
    }
    else
    {
        var bezier = new BezierSegment
        {
            Point1 = new Point(startPoint.X + 50, startPoint.Y),
            Point2 = new Point(endPoint.X - 50, endPoint.Y),
            Point3 = endPoint
        };
        segment = bezier;
    }

    return segment;
}

項目效果

框架極大地簡化複雜流程的設計與實現過程,為項目的快速迭代與交付提供有力支持。

項目源碼

源碼結構清晰,註釋詳細,為大家提供良好的學習與參考價值。

代碼中包含節點的創建、連線的繪製、佈局的調整、框選與拖動的實現等核心功能,是學習WPF開發與流程節點編輯的好示例。

Gitee:https://gitee.com/Zero_0002/process-node-editing-framework

總結

流程節點編輯框架是一款功能強大、易於使用的圖形化流程設計工具。通過其豐富的功能特性、友好的操作界面以及強大的技術,框架在軟件開發領域展現出了廣泛的用途。

關鍵詞

WPF、流程節點編輯、連線管理、框選拖動、邊界擴展、數據綁定、MVVM模式、自定義控件、項目源碼、交互豐富流程圖、節點編輯、可視化、C#、開源項目、JSON、MVVM、拖拽、連線

最後

如果你覺得這篇文章對你有幫助,不妨點個贊支持一下!你的支持是我繼續分享知識的動力。如果有任何疑問或需要進一步的幫助,歡迎隨時留言。

也可以加入微信公眾號[DotNet技術匠] 社區,與其他熱愛技術的同行一起交流心得,共同成長!

user avatar prepared 頭像 13917911249 頭像 ximinghui 頭像 phytium_developers 頭像 duokeli 頭像 kubesphere 頭像 jueqiangqingtongsan 頭像 insus 頭像 Langx 頭像 wuhuacong 頭像 wuliaodechaye 頭像 manshenjiroudehuajuan 頭像
12 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.