Developing a Visio-like Flowchart Software in WPF

Developing a Visio-like Flowchart Software in WPF

When you want to draw a flowchart, you'll find that many software either require a license key or a membership. So I thought, why not create my own flowchart software?

Last updated 2/27/2022 3:04 PM
Alan.hsiang
16 min read
Category
WPF
Tags
.NET WPF Visio

When you want to draw a flowchart, you'll find that many software tools either require a license key or a membership. At that point, I thought, can I create my own flowchart software? This article uses a simple example to briefly describe how to use WPF to create your own flowchart software. It is only for learning and sharing purposes. If there are any shortcomings, please point them out.

Involved Knowledge

This example is mainly developed using WPF technology, involving the following knowledge points:

  1. WPF drawing, such as rectangles, lines, and other related graphics techniques.
  2. Thumb control. This control allows users to drag freely. The movable graphics controls used in the example all inherit from the Thumb control.

Introduction to Thumb Control

The Thumb control is a WPF control used for user dragging. By default, the Thumb control is not in the toolbox and needs to be added manually, as follows:

Toolbox --> General --> Right-click --> Choose Items, then open the [Choose Toolbox Items] dialog, as shown below:

On the [Choose Toolbox Items] page, open WPF Components --> Select Thumb --> Click the OK button, as shown below:

After successful addition, you can drag it from the toolbox to the page, as shown below:

A brief introduction to the Thumb control is as follows:

namespace System.Windows.Controls.Primitives
{
    //
    // Summary:
    //     Represents a control that can be dragged by the user.
    [DefaultEvent("DragDelta")]
    [Localizability(LocalizationCategory.NeverLocalize)]
    public class Thumb : Control
    {
        //
        // Summary:
        //     Identifies the System.Windows.Controls.Primitives.Thumb.DragStarted routed event.
        //
        // Returns:
        //     The identifier for the System.Windows.Controls.Primitives.Thumb.DragStarted routed event.
        public static readonly RoutedEvent DragStartedEvent;
        //
        // Summary:
        //     Identifies the System.Windows.Controls.Primitives.Thumb.DragDelta routed event.
        //
        // Returns:
        //     The identifier for the System.Windows.Controls.Primitives.Thumb.DragDelta routed event.
        public static readonly RoutedEvent DragDeltaEvent;
        //
        // Summary:
        //     Identifies the System.Windows.Controls.Primitives.Thumb.DragCompleted routed event.
        //
        // Returns:
        //     The identifier for the System.Windows.Controls.Primitives.Thumb.DragCompleted routed event.
        public static readonly RoutedEvent DragCompletedEvent;
        //
        // Summary:
        //     Identifies the System.Windows.Controls.Primitives.Thumb.IsDragging dependency property.
        //
        // Returns:
        //     The identifier for the System.Windows.Controls.Primitives.Thumb.IsDragging dependency property.
        public static readonly DependencyProperty IsDraggingProperty;

        //
        // Summary:
        //     Initializes a new instance of the System.Windows.Controls.Primitives.Thumb class.
        public Thumb();

        //
        // Summary:
        //     Gets whether the System.Windows.Controls.Primitives.Thumb has logical focus and mouse capture and the left mouse button is pressed.
        //
        // Returns:
        //     true if the System.Windows.Controls.Primitives.Thumb has focus and mouse capture; otherwise false. The default is false.
        [Bindable(true)]
        [Browsable(false)]
        [Category("Appearance")]
        public bool IsDragging { get; protected set; }

        //
        // Summary:
        //     Occurs when the System.Windows.Controls.Primitives.Thumb control receives logical focus and mouse capture.
        [Category("Behavior")]
        public event DragStartedEventHandler DragStarted;
        //
        // Summary:
        //     Occurs one or more times as the mouse position changes when the System.Windows.Controls.Primitives.Thumb control has logical focus and mouse capture.
        [Category("Behavior")]
        public event DragDeltaEventHandler DragDelta;
        //
        // Summary:
        //     Occurs when the System.Windows.Controls.Primitives.Thumb control loses mouse capture.
        [Category("Behavior")]
        public event DragCompletedEventHandler DragCompleted;

        //
        // Summary:
        //     Cancels the drag operation for the System.Windows.Controls.Primitives.Thumb.
        public void CancelDrag();
        //
        // Summary:
        //     Creates a System.Windows.Automation.Peers.AutomationPeer for the System.Windows.Controls.Primitives.Thumb control.
        //
        // Returns:
        //     A System.Windows.Automation.Peers.ThumbAutomationPeer for the System.Windows.Controls.Primitives.Thumb control.
        protected override AutomationPeer OnCreateAutomationPeer();
        //
        // Summary:
        //     Responds to a change in the value of the System.Windows.Controls.Primitives.Thumb.IsDragging property.
        //
        // Parameters:
        //   e:
        //     The event data.
        protected virtual void OnDraggingChanged(DependencyPropertyChangedEventArgs e);
        //
        // Summary:
        //     Provides class handling for the System.Windows.ContentElement.MouseLeftButtonDown event.
        //
        // Parameters:
        //   e:
        //     The event data.
        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e);
        //
        // Summary:
        //     Provides class handling for the System.Windows.ContentElement.MouseLeftButtonUp event.
        //
        // Parameters:
        //   e:
        //     The event data.
        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e);
        //
        // Summary:
        //     Provides class handling for the System.Windows.UIElement.MouseMove event.
        //
        // Parameters:
        //   e:
        //     The event data.
        protected override void OnMouseMove(MouseEventArgs e);
    }
}

From the summary above, we can see that the Thumb control provides three events:

  • Drag start event: public event DragStartedEventHandler DragStarted;
  • Drag progress event: public event DragDeltaEventHandler DragDelta;
  • Drag completion event: public event DragCompletedEventHandler DragCompleted;

Thumb Control Example

First, add a Thumb control to the window page, then add three events as follows:

<Window
  x:Class="DemoVisio.MainWindow1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:DemoVisio"
  mc:Ignorable="d"
  Title="Thumb Example"
  Height="450"
  Width="800"
>
  <canvas>
    <Thumb
      x:Name="thumb"
      Canvas.Left="0"
      Canvas.Top="0"
      Height="100"
      Width="100"
      DragStarted="thumb_DragStarted"
      DragDelta="thumb_DragDelta"
      DragCompleted="thumb_DragCompleted"
    />
  </canvas>
</Window>

Then add code to the three events, with the focus on the DragDelta event, as shown below:

namespace DemoVisio
{
    /// <summary>
    /// Interaction logic for MainWindow1.xaml
    /// </summary>
    public partial class MainWindow1 : Window
    {
        public MainWindow1()
        {
            InitializeComponent();
        }

        private void thumb_DragStarted(object sender, DragStartedEventArgs e)
        {
            // Drag started
        }

        /// <summary>
        /// Drag method
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
        {
            Thumb myThumb = (Thumb)sender;
            double nTop = Canvas.GetTop(myThumb) + e.VerticalChange;
            double nLeft = Canvas.GetLeft(myThumb) + e.HorizontalChange;
            Canvas.SetTop(myThumb, nTop);
            Canvas.SetLeft(myThumb, nLeft);
        }

        private void thumb_DragCompleted(object sender, DragCompletedEventArgs e)
        {
            // Drag completed
        }
    }
}

Note that in XAML, the Thumb must be inside a Canvas layout container, and both Canvas.Left="0" and Canvas.Top="0" must be set. If not set, the values will be NaN and dragging won't work.

By default, the Thumb control is just an ugly square, as shown below:

Drag Control Base Class

In this example, the flowchart requires multiple controls (such as circles, rectangles, lines, etc.). It would be inefficient to implement the three events [DragStarted, DragDelta, DragCompleted] for each control. Therefore, a base class ThumbControl is defined to unify the implementation. The details are as follows:

namespace DemoVisio
{
    public class ThumbControl : Thumb
    {
        /// <summary>
        /// Whether text input is enabled
        /// </summary>
        public bool IsEnableInput { get { return (bool)GetValue(IsEnableInputProperty); } set {SetValue( IsEnableInputProperty, value); } }

        /// <summary>
        /// Dependency property
        /// </summary>
        public static readonly DependencyProperty IsEnableInputProperty = DependencyProperty.Register("IsEnableInput",typeof(bool),typeof(ThumbControl));

        public ThumbControl():base() {
            this.DragStarted += ThumbControl_DragStarted;
            this.DragDelta += ThumbControl_DragDelta;
            this.DragCompleted += ThumbControl_DragCompleted;
            this.MouseDoubleClick += ThumbControl_MouseDoubleClick;
            this.IsKeyboardFocusedChanged += ThumbControl_IsKeyboardFocusedChanged;
        }

        public void SetIsEnableInput(bool flag)
        {
            this.IsEnableInput = flag;
        }

        /// <summary>
        /// Whether the control has keyboard focus
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ThumbControl_IsKeyboardFocusedChanged(object sender, DependencyPropertyChangedEventArgs e)
        {

            this.IsEnableInput = this.IsKeyboardFocused;

        }

        /// <summary>
        /// Double-click event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ThumbControl_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            this.IsEnableInput = true;
        }

        private void ThumbControl_DragStarted(object sender, DragStartedEventArgs e)
        {
            // Start moving
        }

        private void ThumbControl_DragDelta(object sender, DragDeltaEventArgs e)
        {
            Thumb myThumb = (Thumb)sender;
            double nTop = Canvas.GetTop(myThumb) + e.VerticalChange;
            double nLeft = Canvas.GetLeft(myThumb) + e.HorizontalChange;
            Canvas.SetTop(myThumb, nTop);
            Canvas.SetLeft(myThumb, nLeft);
        }

        private void ThumbControl_DragCompleted(object sender, DragCompletedEventArgs e)
        {
            // Move completed
        }
    }
}

Specific Graphic Controls

After encapsulating the base class, other controls can be used on top of it, displaying different appearances through ControlTemplate, as shown below:

  1. Rectangle

In a flowchart, rectangles usually represent processes. The implementation code is as follows:

<UserControl
  x:Class="DemoVisio.SquareControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:DemoVisio"
  mc:Ignorable="d"
  d:DesignHeight="450"
  d:DesignWidth="800"
>
  <canvas>
    <local:ThumbControl
      x:Name="s"
      BorderThickness="1"
      Canvas.Top="0"
      Canvas.Left="0"
      Width="100"
      Height="60"
      Background="AliceBlue"
    >
      <Thumb.Template>
        <ControlTemplate>
          <Border
            Width="{Binding ElementName=s, Path=Width}"
            Height="{Binding ElementName=s, Path=Height}"
            BorderBrush="Black"
            Background="{Binding ElementName=s, Path=Background}"
            BorderThickness="{Binding ElementName=s, Path=BorderThickness}"
            Padding="2"
          >
            <TextBox
              Background="{Binding ElementName=s, Path=Background}"
              BorderThickness="0"
              VerticalAlignment="Center"
              IsEnabled="{Binding ElementName=s, Path=IsEnableInput}"
              MinLines="3"
              MaxLines="3"
              HorizontalContentAlignment="Center"
              VerticalContentAlignment="Center"
            ></TextBox>
          </Border>
        </ControlTemplate>
      </Thumb.Template>
    </local:ThumbControl>
  </canvas>
</UserControl>
  1. Circle

In a flowchart, circles usually represent start and end points, as shown below:

<UserControl
  x:Class="DemoVisio.CircleControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:DemoVisio"
  mc:Ignorable="d"
  d:DesignHeight="450"
  d:DesignWidth="800"
>
  <canvas>
    <local:ThumbControl
      x:Name="s"
      Canvas.Top="0"
      Canvas.Left="0"
      Width="60"
      Height="60"
      Background="AliceBlue"
      BorderThickness="1"
    >
      <local:ThumbControl.Template>
        <ControlTemplate>
          <Grid>
            <Border
              Width="{Binding Width}"
              Height="{Binding Height}"
              CornerRadius="30"
              BorderThickness="{Binding ElementName=s, Path=BorderThickness}"
              BorderBrush="Black"
              Background="{Binding ElementName=s, Path=Background}"
            >
              <TextBox
                VerticalAlignment="Center"
                HorizontalContentAlignment="Center"
                VerticalContentAlignment="Center"
                MaxLines="2"
                MinLines="1"
                Width="{Binding Width}"
                Height="{Binding Height}"
                Background="{Binding ElementName=s, Path=Background}"
                BorderThickness="0"
                IsEnabled="{Binding ElementName=s, Path=IsEnableInput}"
              ></TextBox>
            </Border>
          </Grid>
        </ControlTemplate>
      </local:ThumbControl.Template>
    </local:ThumbControl>
  </canvas>
</UserControl>
  1. Rhombus

In a flowchart, rhombuses usually represent decisions, expressing boolean values in programs. As shown below:

<UserControl
  x:Class="DemoVisio.RhombusControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:DemoVisio"
  mc:Ignorable="d"
  d:DesignHeight="450"
  d:DesignWidth="800"
>
  <canvas>
    <local:ThumbControl
      x:Name="s"
      Canvas.Left="0"
      Canvas.Top="0"
      Width="80"
      Height="80"
      Background="AliceBlue"
      BorderThickness="1"
    >
      <Thumb.Template>
        <ControlTemplate>
          <Border
            Width="{Binding ElementName=s, Path=Width}"
            Height="{Binding ElementName=s, Path=Height}"
            BorderBrush="Black"
            Background="{Binding ElementName=s, Path=Background}"
            BorderThickness="{Binding ElementName=s, Path=BorderThickness}"
            Padding="1"
          >
            <TextBox
              Background="{Binding ElementName=s, Path=Background}"
              BorderThickness="0"
              VerticalAlignment="Center"
              IsEnabled="{Binding ElementName=s, Path=IsEnableInput}"
              Width="50"
              Height="50"
              HorizontalContentAlignment="Center"
              VerticalContentAlignment="Center"
            >
              <TextBox.RenderTransform>
                <RotateTransform
                  CenterX="25"
                  CenterY="25"
                  Angle="-45"
                ></RotateTransform>
              </TextBox.RenderTransform>
            </TextBox>
          </Border>
        </ControlTemplate>
      </Thumb.Template>
      <Thumb.RenderTransform>
        <TransformGroup>
          <RotateTransform
            CenterX="40"
            CenterY="40"
            Angle="45"
          ></RotateTransform>
          <ScaleTransform
            CenterX="40"
            CenterY="40"
            ScaleX="0.8"
          ></ScaleTransform>
          <TranslateTransform X="10" Y="15"></TranslateTransform>
        </TransformGroup>
      </Thumb.RenderTransform>
    </local:ThumbControl>
  </canvas>
</UserControl>
  1. Line

In a flowchart, lines usually represent connections between two processes, as shown below:

<UserControl
  x:Class="DemoVisio.LineArrowControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:DemoVisio"
  mc:Ignorable="d"
  d:DesignHeight="450"
  d:DesignWidth="800"
>
  <canvas>
    <local:ThumbControl
      x:Name="s"
      Canvas.Left="0"
      Canvas.Top="0"
      Width="100"
      Height="100"
      Background="AliceBlue"
      IsEnableInput="False"
    >
      <local:ThumbControl.Template>
        <ControlTemplate>
          <Grid>
            <Line
              X1="0"
              Y1="0"
              X2="0"
              Y2="100"
              Stroke="Black"
              StrokeThickness="2"
              HorizontalAlignment="Center"
            >
            </Line>
            <Line
              X1="-5"
              Y1="90"
              X2="1"
              Y2="100"
              StrokeThickness="2"
              Stroke="Black"
              HorizontalAlignment="Center"
            ></Line>
            <Line
              X1="8"
              Y1="90"
              X2="3"
              Y2="100"
              StrokeThickness="2"
              Stroke="Black"
              HorizontalAlignment="Center"
            ></Line>
            <TextBox
              VerticalAlignment="Center"
              Height="30"
              HorizontalContentAlignment="Center"
              BorderThickness="0"
              VerticalContentAlignment="Center"
              MinLines="1"
              MaxLines="2"
              IsEnabled="{Binding ElementName=s, Path=IsEnableInput}"
              Opacity="0"
              Visibility="{Binding ElementName=s, Path=IsEnableInput}"
            ></TextBox>
          </Grid>
        </ControlTemplate>
      </local:ThumbControl.Template>
    </local:ThumbControl>
  </canvas>
</UserControl>

Main Window

The main window is used to draw the flowchart. It is divided into two parts: the left side is the control list, and the right side is the layout container, as shown below:

<Window
  x:Class="DemoVisio.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:DemoVisio"
  mc:Ignorable="d"
  Title="Flowchart"
  Height="800"
  Width="800"
  MouseDown="Window_MouseDown"
  Loaded="Window_Loaded"
>
  <Window.Resources>
    <style TargetType="TextBlock">
      <Setter Property="HorizontalAlignment" Value="Center"></Setter>
    </style>
  </Window.Resources>
  <Grid ShowGridLines="True">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" MinWidth="150"></ColumnDefinition>
      <ColumnDefinition Width="*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <StackPanel Grid.Column="0" x:Name="left" Orientation="Vertical">
      <Rectangle
        Width="80"
        Height="50"
        Fill="AliceBlue"
        Stroke="Black"
        Margin="5"
        x:Name="rectangle"
        MouseLeftButtonDown="rectangle_MouseLeftButtonDown"
      ></Rectangle>
      <TextBlock Text="Rectangle"></TextBlock>
      <Rectangle
        Width="100"
        Height="100"
        Fill="AliceBlue"
        Stroke="Black"
        Margin="5"
        x:Name="rhombus"
        MouseLeftButtonDown="rhombus_MouseLeftButtonDown"
      >
        <Rectangle.RenderTransform>
          <TransformGroup>
            <RotateTransform
              CenterX="50"
              CenterY="50"
              Angle="45"
            ></RotateTransform>
            <ScaleTransform
              CenterX="50"
              CenterY="50"
              ScaleX="0.5"
              ScaleY="0.7"
            ></ScaleTransform>
          </TransformGroup>
        </Rectangle.RenderTransform>
      </Rectangle>
      <TextBlock Text="Rhombus" Margin="0,5"></TextBlock>
      <Line
        X1="0"
        Y1="0"
        X2="0"
        Y2="80"
        Stroke="Black"
        StrokeThickness="2"
        HorizontalAlignment="Center"
        Margin="5"
        x:Name="line"
        MouseLeftButtonDown="line_MouseLeftButtonDown"
      ></Line>
      <TextBlock Text="Line"></TextBlock>
      <Ellipse
        Width="60"
        Height="60"
        Fill="AliceBlue"
        Stroke="Black"
        Margin="5"
        x:Name="circle"
        MouseLeftButtonDown="circle_MouseLeftButtonDown"
      ></Ellipse>
      <TextBlock Text="Circle"></TextBlock>
    </StackPanel>
    <canvas Grid.Column="1" x:Name="right"> </canvas>
  </Grid>
</Window>

When clicking the basic controls on the left, new controls are generated in the container on the right. As shown below:

namespace DemoVisio
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// List of controls
        /// </summary>
        private List<Control> rightControls = new List<Control>();

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_MouseDown(object sender, MouseButtonEventArgs e)
        {
            //this.Focus();
            //this.one.SetIsEnableInput(false);
            foreach (var control in this.right.Children) {
                var thumb = control as ThumbControl;
                if (thumb != null)
                {
                    thumb.SetIsEnableInput(false);
                }
            }
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            var width = this.right.ActualWidth;
            var height = this.right.ActualHeight;

            int x = 0;
            int y = 0;
            // Horizontal lines
            while (y<height) {
                Line line = new Line();
                line.X1 = x;
                line.Y1 = y;
                line.X2 = width;
                line.Y2 = y;
                line.Stroke = Brushes.LightGray;
                line.StrokeThickness = 1;
                this.right.Children.Add(line);
                y = y + 10;
            }
            // Reinitialize values
            x = 0;
            y = 0;
            // Vertical lines
            while (x < width) {
                Line line = new Line();
                line.X1 = x;
                line.Y1 = y;
                line.X2 = x;
                line.Y2 = height;
                line.Stroke = Brushes.LightGray;
                line.StrokeThickness = 1;
                this.right.Children.Add(line);
                x = x + 10;
            }
        }

        private void rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            SquareControl squareControl = new SquareControl();
            this.right.Children.Add(squareControl);
        }

        private void rhombus_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            RhombusControl rhombusControl = new RhombusControl();
            this.right.Children.Add(rhombusControl);
        }

        private void line_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            LineArrowControl lineArrowControl = new LineArrowControl();
            this.right.Children.Add(lineArrowControl);
        }

        private void circle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            CircleControl circleControl = new CircleControl();
            this.right.Children.Add(circleControl);
        }
    }
}

Screenshot

The basic screenshot of the example is as follows:

Remarks

The above example is just a basic demonstration, not fully functional. It is intended to provide a starting point for discussion and learning, promoting mutual progress. I hope it can inspire everyone.

贺新郎·九日

【Author】Liu Kezhuang 【Dynasty】Song
Deep dark endless sky. How to bear, slanting wind and fine rain, tangled sorrow like weaving.
Old eyes have seen the world all my life, relying on a hundred-foot tower.
Looking at the vast, autumn colors of a thousand cliffs.
White-haired scholar weeping for the divine land, utterly desolate, not shedding tears at Cow Mountain.
Chasing past events, gone without a trace.

Young, I was proud of my soaring pen. Now, spring flowers have fallen, my heart full of desolation.
Often I resent that people today lack new ideas, fond of talking about the wild guests of the Southern Dynasty.
Bringing out the broken hat year after year.
If I face the yellow flowers without drinking, I fear the yellow flowers will also laugh at my loneliness.
Wild geese fly north, the sun hides in the west.
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