Use WPF to create a mind map

Use WPF to create a mind map

Mind map, catalog organization chart, fish bone chart, logic structure chart, organization chart

最后更新 4/5/2023 9:23 AM
竹天笑
预计阅读 7 分钟
分类
WPF
专题
WPF Open Source Project
标签
.NET WPF WPF Open Source Project open source

This article was submitted by netizens.

Author: Zhu Tianxiao

Original title: Using Wpf to make a mind map (Continued 3-Diagram Drawing Sheet)

Original link: https://www.example.com

Let's first submit a simple renderings. This update mainly follows Baidu's brain map.

同样老规矩,先上源码地址:https://gitee.com/akwkevin/aistudio.-wpf.-diagram

The main contents of this expansion:

1. Mind map, catalog organization chart, fish bone chart, logic structure chart, organization chart, the entry is under File New Creation.

2. Mind Map Toolbar (visible only in Mind Map mode)

2.2. insert picture

2.3. insert notes

2.4. Insert priority

2.5. insertion progress

2.6. handover type

2.7. Switch themes

2.8. Also expand nodes, select all, center, adapt to the size of the form and other functions, which will not be introduced again.

3. Add search functionality (not just mind maps can be used)

4. Next, let's introduce the core source code (layout settings)

The layout of mind maps, catalog organization charts, fish bone charts, logic structure charts, and organization charts all inherit the following interfaces:

public interface IMindLayout
{
    /// <summary>
    ///  默认节点样式设置
    /// </summary>
    /// <param name="mindNode"></param>
    void Appearance(MindNode mindNode);

    /// <summary>
    ///  节点样式设置
    /// </summary>
    /// <param name="mindNode"></param>
    /// <param name="mindTheme"></param>
    /// <param name="initAppearance"></param>
    void Appearance(MindNode mindNode, MindTheme mindTheme, bool initAppearance);

    /// <summary>
    /// 连线类型设置
    /// </summary>
    /// <param name="source"></param>
    /// <param name="sink"></param>
    /// <param name="connector"></param>
    /// <returns></returns>
    ConnectionViewModel GetOrSetConnectionViewModel(MindNode source, MindNode sink, ConnectionViewModel connector = null);

    /// <summary>
    /// 更新布局
    /// </summary>
    /// <param name="mindNode"></param>
    void UpdatedLayout(MindNode mindNode);
}

Among them: UpdatedLayout includes layout measurement MeasureOverride and placement element ArrangeOverride. Does it feel similar to re-Panel? First calculate the size of each node, and then start layout. The following is the source code of the Mind Map. If you are interested in other maps, please download the source code to view it.

public SizeBase MeasureOverride(MindNode mindNode, bool isExpanded = true)
{
    var sizewithSpacing = mindNode.SizeWithSpacing;
    if (mindNode.Children?.Count > 0)
    {
        if (mindNode.NodeLevel == 0)
        {
            var rights = mindNode.Children.Where((p, index) => index % 2 == 0).ToList();
            rights.ForEach(p => p.ConnectorOrientation = ConnectorOrientation.Left);
            var rightsizes = rights.Select(p => MeasureOverride(p, mindNode.IsExpanded && isExpanded)).ToArray();
            var lefts = mindNode.Children.Where((p, index) => index % 2 == 1).ToList();
            lefts.ForEach(p => p.ConnectorOrientation = ConnectorOrientation.Right);
            var leftsizes = lefts.Select(p => MeasureOverride(p, mindNode.IsExpanded && isExpanded)).ToArray();
            sizewithSpacing = new SizeBase(sizewithSpacing.Width + rightsizes.MaxOrDefault(p => p.Width) + +leftsizes.MaxOrDefault(p => p.Width), Math.Max(sizewithSpacing.Height, Math.Max(rightsizes.SumOrDefault(p => p.Height), leftsizes.SumOrDefault(p => p.Height))));
        }
        else
        {
            var childrensizes = mindNode.Children.Select(p => MeasureOverride(p, mindNode.IsExpanded && isExpanded)).ToArray();
            sizewithSpacing = new SizeBase(sizewithSpacing.Width + childrensizes.MaxOrDefault(p => p.Width), Math.Max(sizewithSpacing.Height, childrensizes.SumOrDefault(p => p.Height)));
        }
    }
    mindNode.DesiredSize = isExpanded ? sizewithSpacing : new SizeBase(0, 0);
    mindNode.Visible = isExpanded;

    return mindNode.DesiredSize;
}

public void ArrangeOverride(MindNode mindNode)
{
    if (mindNode.NodeLevel == 0)
    {
        mindNode.DesiredPosition = mindNode.Position;

        if (mindNode.Children?.Count > 0)
        {
            var rights = mindNode.Children.Where(p => p.ConnectorOrientation == ConnectorOrientation.Left).ToList();
            double left = mindNode.DesiredPosition.X + mindNode.ItemWidth  + mindNode.Spacing.Width;
            double lefttop = mindNode.DesiredPosition.Y + mindNode.ItemHeight / 2 - Math.Min(mindNode.DesiredSize.Height, rights.SumOrDefault(p => p.DesiredSize.Height)) / 2;
            foreach (var child in rights)
            {
                child.Offset = new PointBase(child.Offset.X - child.RootNode.Offset.X, child.Offset.Y - child.RootNode.Offset.Y);

                child.DesiredPosition = new PointBase(left + child.Spacing.Width, lefttop + child.DesiredSize.Height / 2 - child.ItemHeight / 2);
                child.Left = child.DesiredPosition.X + child.Offset.X;
                child.Top = child.DesiredPosition.Y + child.Offset.Y;
                lefttop += child.DesiredSize.Height;

                ArrangeOverride(child);

                var connector = mindNode.Root?.Items.OfType<ConnectionViewModel>().FirstOrDefault(p => p.SourceConnectorInfo?.DataItem == mindNode && p.SinkConnectorInfoFully?.DataItem == child);
                connector?.SetSourcePort(mindNode.FirstConnector);
                connector?.SetSinkPort(child.LeftConnector);
                connector?.SetVisible(child.Visible);
            }

            var lefts = mindNode.Children.Where(p => p.ConnectorOrientation == ConnectorOrientation.Right).ToList();
            double right = mindNode.DesiredPosition.X - mindNode.Spacing.Width;
            double righttop = mindNode.DesiredPosition.Y + mindNode.ItemHeight / 2 - Math.Min(mindNode.DesiredSize.Height, lefts.SumOrDefault(p => p.DesiredSize.Height)) / 2;
            foreach (var child in lefts)
            {
                child.Offset = new PointBase(child.Offset.X - child.RootNode.Offset.X, child.Offset.Y - child.RootNode.Offset.Y);
                child.DesiredPosition = new PointBase(right - child.Spacing.Width - child.ItemWidth, righttop + child.DesiredSize.Height / 2 - child.ItemHeight / 2);
                child.Left = child.DesiredPosition.X + child.Offset.X;
                child.Top = child.DesiredPosition.Y + child.Offset.Y;
                righttop += child.DesiredSize.Height;

                ArrangeOverride(child);

                var connector = mindNode.Root?.Items.OfType<ConnectionViewModel>().FirstOrDefault(p => p.SourceConnectorInfo?.DataItem == mindNode && p.SinkConnectorInfoFully?.DataItem == child);
                connector?.SetSourcePort(mindNode.FirstConnector);
                connector?.SetSinkPort(child.RightConnector);
                connector?.SetVisible(child.Visible);
            }
        }

        mindNode.Offset = new PointBase();//修正后归0
    }
    else
    {
        if (mindNode.GetLevel1Node().ConnectorOrientation == ConnectorOrientation.Left)
        {
            double left = mindNode.DesiredPosition.X + mindNode.ItemWidth + mindNode.Spacing.Width;
            double top = mindNode.DesiredPosition.Y + mindNode.ItemHeight / 2 - Math.Min(mindNode.DesiredSize.Height, mindNode.Children.SumOrDefault(p => p.DesiredSize.Height)) / 2;
            if (mindNode.Children?.Count > 0)
            {
                foreach (var child in mindNode.Children)
                {
                    child.Offset = new PointBase(child.Offset.X - child.RootNode.Offset.X, child.Offset.Y - child.RootNode.Offset.Y);
                    child.DesiredPosition = new PointBase(left + child.Spacing.Width, top + child.DesiredSize.Height / 2 - child.ItemHeight / 2);
                    child.Left = child.DesiredPosition.X + child.Offset.X;
                    child.Top = child.DesiredPosition.Y + child.Offset.Y;
                    top += child.DesiredSize.Height;

                    ArrangeOverride(child);

                    var connector = mindNode.Root?.Items.OfType<ConnectionViewModel>().FirstOrDefault(p => p.SourceConnectorInfo?.DataItem == mindNode && p.SinkConnectorInfoFully?.DataItem == child);
                    connector?.SetSourcePort(mindNode.RightConnector);
                    connector?.SetSinkPort(child.LeftConnector);
                    connector?.SetVisible(child.Visible);
                }
            }
        }
        else
        {
            double right = mindNode.DesiredPosition.X  - mindNode.Spacing.Width;
            double top = mindNode.DesiredPosition.Y + mindNode.ItemHeight / 2 - Math.Min(mindNode.DesiredSize.Height, mindNode.Children.SumOrDefault(p => p.DesiredSize.Height)) / 2;
            if (mindNode.Children?.Count > 0)
            {
                foreach (var child in mindNode.Children)
                {
                    child.Offset = new PointBase(child.Offset.X - child.RootNode.Offset.X, child.Offset.Y - child.RootNode.Offset.Y);
                    child.DesiredPosition = new PointBase(right - child.Spacing.Width - child.ItemWidth, top + child.DesiredSize.Height / 2 - child.ItemHeight / 2);
                    child.Left = child.DesiredPosition.X + child.Offset.X;
                    child.Top = child.DesiredPosition.Y + child.Offset.Y;
                    top += child.DesiredSize.Height;

                    ArrangeOverride(child);

                    var connector = mindNode.Root?.Items.OfType<ConnectionViewModel>().FirstOrDefault(p => p.SourceConnectorInfo?.DataItem == mindNode && p.SinkConnectorInfoFully?.DataItem == child);
                    connector?.SetSourcePort(mindNode.LeftConnector);
                    connector?.SetSinkPort(child.RightConnector);
                    connector?.SetVisible(child.Visible);
                }
            }
        }
    }


}

5. 最后为了方便大家使用,我封装了一个思维脑图的控件 MindEditor,可以直接绑定 json 格式的数据,数据改变,可以直接加载应用。(见AIStudio.Wpf.DiagramDesigner.Demo

近期会持续更新,欢迎大家光临艾竹 (akwkevin) - Gitee.com,支持的朋友们,点个小星星,你们的支持能燃烧我开源的力量。

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 9/13/2025

Migration from WPF to Avalonia series: Why I have to migrate WPF programs to Avalonia

In the past few years, our host computer software has been mainly developed using WPF and WinForm. These technologies are really easy to use on the Windows platform, and they have also accompanied us through the stage of small-scale trial production to today's large-scale delivery. However, with the development of business and changes in customer needs, the single Windows technology stack has gradually become a hurdle that we must overcome.

继续阅读
同分类 / 同标签 1/26/2025

WPF internationalizes with custom XML files

This article describes in detail the methods of using custom XML files to achieve internationalization in WPF programs, including installing the necessary NuGet package, dynamically obtaining language lists, dynamically switching languages, using translation strings in code and xaml interfaces, etc. It also provides source code links to help developers easily internationalize WPF applications.

继续阅读