Creating a Mind Map with WPF

Creating a Mind Map with WPF

Mind maps, directory organization charts, fishbone diagrams, logic structure charts, organization charts

Last updated 4/5/2023 9:23 AM
竹天笑
7 min read
Category
WPF
Topic
WPF Open Source Project
Tags
.NET WPF WPF Open Source Project Open Source

This article is contributed by a netizen.

Author: Bamboo Sky Laugh (竹天笑)

Original Title: Using Wpf to Create a Mind Map (Continued 3-Diagram Drawing Board)

Original Link: https://www.cnblogs.com/akwkevin/p/17288814.html

First, a simple effect diagram. This update mainly imitates Baidu Mind Map.

As usual, let's first provide the source code link: https://gitee.com/akwkevin/aistudio.-wpf.-diagram

Main contents of this extension:

1. Mind Map, Organization Chart, Fishbone Diagram, Logic Structure Chart, Organizational Structure Chart — accessed under File > New.

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

2.2. Insert Image

2.3. Insert Note

2.4. Insert Priority

2.5. Insert Progress

2.6. Switch Type

2.7. Switch Theme

2.8. Also includes expand nodes, select all, center, fit to window, etc., which are not further introduced.

3. Added search functionality (not limited to mind maps)

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

The layouts of Mind Map, Organization Chart, Fishbone Diagram, Logic Structure Chart, and Organizational Structure Chart all inherit the following interface:

public interface IMindLayout
{
    /// <summary>
    ///  Default node style settings
    /// </summary>
    /// <param name="mindNode"></param>
    void Appearance(MindNode mindNode);

    /// <summary>
    ///  Node style settings
    /// </summary>
    /// <param name="mindNode"></param>
    /// <param name="mindTheme"></param>
    /// <param name="initAppearance"></param>
    void Appearance(MindNode mindNode, MindTheme mindTheme, bool initAppearance);

    /// <summary>
    ///  Connection type settings
    /// </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>
    ///  Update layout
    /// </summary>
    /// <param name="mindNode"></param>
    void UpdatedLayout(MindNode mindNode);
}

Among them, UpdatedLayout includes layout measurement MeasureOverride and arrangement of elements ArrangeOverride. It feels similar to re‑implementing a Panel: first calculate the size occupied by each node, then start layout. Below is the source code for the mind map. For other diagram types, please download the source code if interested.

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();//reset to zero after correction
    }
    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. Finally, for ease of use, I have encapsulated a mind map control MindEditor that can directly bind JSON‑format data. When the data changes, it can be loaded and applied directly. (See AIStudio.Wpf.DiagramDesigner.Demo)

Updates will continue in the near future. Welcome to visit Bamboo (akwkevin) - Gitee.com. For friends who support me, please click the star — your support fuels my open‑source power.

Keep Exploring

Related Reading

More Articles
Same category / Same tag 9/13/2025

Migration Series from WPF to Avalonia: Why I Must Migrate My WPF Application to Avalonia

In the past few years, our host computer software has mainly been developed using WPF and WinForm . These technologies work well on the Windows platform and have accompanied us from small-scale trial production to the current stage of large-scale delivery. However, with business development and changes in customer requirements, the single Windows technology stack has gradually become a hurdle we must overcome.

Continue Reading
Same category / Same tag 1/26/2025

Implementing Internationalization in WPF Using Custom XML Files

This article details the method of implementing internationalization in WPF applications using custom XML files, including installing the necessary NuGet packages, dynamically retrieving the language list, dynamically switching languages, using translated strings in code and XAML interfaces, and provides a source code link to help developers easily achieve internationalization in WPF applications.

Continue Reading