WPF 3D Tunnel Effect, Joining the Fun with WPF

WPF 3D Tunnel Effect, Joining the Fun with WPF

Compared to CSS and UWP, the veteran WPF is relatively cumbersome for 3D effects. First, you need to create a 3D model, then rotate the XY axes, adjust the Z-axis scaling, and finally adjust the FOV. The tunnel effect is determined by the Z-axis scaling value.

Last updated 2/21/2022 1:58 PM
ARM830
13 min read
Category
WPF
Tags
.NET WPF

CSS effect blog address:

3D shuttle effect? Easily done with CSS - ChokCoco - Blog Garden (cnblogs.com)

UWP effect blog address:

3D shuttle effect? Also achievable with UWP - dino.c - Blog Garden (cnblogs.com)

The experts truly reach the pinnacle.

I really learn a lot from this.

I’ll just have some fun and try it with WPF. The result is not great, but it’s passable.

Compared to CSS and UWP, the classic WPF is relatively complicated for 3D effects. First, you need to create a 3D model, then rotate the X and Y axes, adjust the Z‑axis stretch, and finally set the FOV. The shuttle effect depends on the value of the Z‑axis stretch; in the image it’s 10‑20.

Of course, the hardest part is the seamless loop of the animation.

Let’s do it step by step.

Creating the 3D model

This part can directly use 2D controls – there’s nothing special to study – so it’s a pure 3D model with textures applied as images.

As for creating the 3D model, the key is to start from left to right.

  1. Positions

A 3D model is made of triangles, so when describing the shape of an object, you should draw triangles as much as possible. Each point is a vertex. If identical vertices exist, choose only one.

Vertices are described in XYZ. When you look at the object from the front, the Z‑axis points toward you (positive). Y‑axis is up (positive), X‑axis is right (positive).

Note: For convenience, it is recommended to define points in the order: top‑left, bottom‑left, bottom‑right, top‑right.

The vertices of this model are defined as P1(x,y,z), P2(x,y,z), P3(x,y,z), P4(x,y,z).

  1. TriangleIndices

This describes how the vertices form triangles. When you consider a face as the front, remember to define the vertex indices counterclockwise. The vertex set is Positions, containing the coordinate points you entered, starting from index 0.

The back face is clockwise.

  1. TextureCoordinates

This specifies the positioning order of the texture. It follows a 2D coordinate system, different from the others. That is, the top‑left corner is (0,0), bottom‑right is (1,1), i.e., the usual screen coordinates.

After creating the model, you need to make four of them.

Rotating the 3D model

Now it gets interesting. After creating the four models above, we need to compose them into four faces, i.e., rotation around the Y‑axis and X‑axis, but the directions are different. We can use Transform for rotation.

Looks okay.

Let’s make four images and test.

For easier debugging, we include a progress bar.

Resources:

<MeshGeometry3D x:Key="Rect3D_O">
  <MeshGeometry3D.Positions>
    -50, 50, 0, -50, -50, 0, 50, -50, 0, 50, 50, 0
  </MeshGeometry3D.Positions>
  <MeshGeometry3D.TriangleIndices> 0 1 2 2 3 0 </MeshGeometry3D.TriangleIndices>
  <MeshGeometry3D.TextureCoordinates>
    0,0 0,1 1,1 1,0
  </MeshGeometry3D.TextureCoordinates>
  <MeshGeometry3D.Normals> 0,0,1, 0,0,1, 0,0,1, 0,0,1 </MeshGeometry3D.Normals>
</MeshGeometry3D>
<DiffuseMaterial x:Key="Img">
  <DiffuseMaterial.Brush>
    <ImageBrush ImageSource="start.jpg" />
  </DiffuseMaterial.Brush>
</DiffuseMaterial>
<Viewport3D x:Name="View3D_2">
  <Viewport3D.Camera>
    <PerspectiveCamera
      FieldOfView="{Binding ElementName=FOV, Path=Value}"
      Position="0,0,100"
      LookDirection="0,0,-1"
    />
  </Viewport3D.Camera>
  <ModelVisual3D>
    <ModelVisual3D.Content>
      <Model3DGroup>
        <AmbientLight Color="White" />
        <GeometryModel3D
          Geometry="{StaticResource Rect3D_O}"
          Material="{StaticResource Img}"
          BackMaterial="{StaticResource Img}"
        >
          <GeometryModel3D.Transform>
            <Transform3DGroup>
              <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                <RotateTransform3D.Rotation>
                  <AxisAngleRotation3D
                    Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
                    Axis="0 1 0"
                  />
                </RotateTransform3D.Rotation>
              </RotateTransform3D>
              <ScaleTransform3D
                ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
              />
            </Transform3DGroup>
          </GeometryModel3D.Transform>
        </GeometryModel3D>
        <GeometryModel3D
          Geometry="{StaticResource Rect3D_O}"
          Material="{StaticResource Img}"
          BackMaterial="{StaticResource Img}"
        >
          <GeometryModel3D.Transform>
            <Transform3DGroup>
              <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                <RotateTransform3D.Rotation>
                  <AxisAngleRotation3D
                    Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
                    Axis="1 0 0"
                  />
                </RotateTransform3D.Rotation>
              </RotateTransform3D>
              <ScaleTransform3D
                ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
              />
            </Transform3DGroup>
          </GeometryModel3D.Transform>
        </GeometryModel3D>
        <GeometryModel3D
          Geometry="{StaticResource Rect3D_O}"
          Material="{StaticResource Img}"
          BackMaterial="{StaticResource Img}"
        >
          <GeometryModel3D.Transform>
            <Transform3DGroup>
              <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                <RotateTransform3D.Rotation>
                  <AxisAngleRotation3D
                    Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
                    Axis="0 1 0"
                  />
                </RotateTransform3D.Rotation>
              </RotateTransform3D>
              <ScaleTransform3D
                ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
              />
            </Transform3DGroup>
          </GeometryModel3D.Transform>
        </GeometryModel3D>
        <GeometryModel3D
          Geometry="{StaticResource Rect3D_O}"
          Material="{StaticResource Img}"
          BackMaterial="{StaticResource Img}"
        >
          <GeometryModel3D.Transform>
            <Transform3DGroup>
              <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                <RotateTransform3D.Rotation>
                  <AxisAngleRotation3D
                    Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
                    Axis="1 0 0"
                  />
                </RotateTransform3D.Rotation>
              </RotateTransform3D>
              <ScaleTransform3D
                ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
              />
            </Transform3DGroup>
          </GeometryModel3D.Transform>
        </GeometryModel3D>
      </Model3DGroup>
    </ModelVisual3D.Content>
  </ModelVisual3D>
</Viewport3D>

<Slider
  Grid.Row="1"
  x:Name="Left_Top_Z"
  Minimum="-90"
  Value="12"
  Maximum="90"
  ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<Slider
  Grid.Row="2"
  x:Name="Right_Bottom_Z"
  Minimum="-90"
  Value="-12"
  Maximum="0"
  ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<Slider
  Grid.Row="3"
  x:Name="ScaleZ_2"
  Background="Red"
  Minimum="10"
  Maximum="100"
  ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
>
  <Slider.Style>
    <Style TargetType="Slider">
      <Style.Triggers>
          <Trigger Property="Tag" Value="1">
              <Trigger.EnterActions>
                  <BeginStoryboard >
                      <Storyboard>
                          <DoubleAnimation  RepeatBehavior="Forever"  Storyboard.TargetProperty="Value"   From="11" To="20" BeginTime="0:0:0"   Duration="0:0:10" />
                      </Storyboard>
                  </BeginStoryboard>
              </Trigger.EnterActions>
          </Trigger>
      </Style.Triggers>
    </Style>
  </Slider.Style>
</Slider>
<Slider
  Grid.Row="4"
  x:Name="FOV"
  Minimum="0"
  Maximum="180"
  Value="176"
  ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>

Run it and see:

Looks like it’s starting to feel right.

All that’s left is to fine‑tune the animation.

We want a seamless loop – essentially, we need two animation phases that are indistinguishable at their boundaries. Referencing the experts’ basic ideas, we rely on opacity and delayed playback to make the start and end appear seamless.

That means:

Here we will implement this approach purely in XAML.

First, we need to solve the timing problem. When the page loads, we modify some property of an object via animation. That object has a trigger that monitors it and starts a timer, then plays the animation.

To modify the value, we use the ObjectAnimationUsingKeyFrames animation.

For the trigger, we use a data trigger because it supports binding.

The most important part of the whole process is the timeline – when each of the two sets of animations appears and hides.

Final code

<Window.Resources>
  <Storyboard
    RepeatBehavior="Forever"
    Storyboard.TargetName="ScaleZ_2"
    Duration="0:0:10"
    x:Key="sb"
  >
    <DoubleAnimation
      Storyboard.TargetName="ScaleZ_2"
      Storyboard.TargetProperty="Value"
      From="11"
      To="20"
      BeginTime="0:0:0"
      Duration="0:0:10"
    />
    <DoubleAnimationUsingKeyFrames
      Storyboard.TargetProperty="Opacity"
      Storyboard.TargetName="View3D_2"
      BeginTime="0:0:0"
    >
      <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
      <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" />
      <LinearDoubleKeyFrame KeyTime="0:0:6" Value="1" />
      <LinearDoubleKeyFrame KeyTime="0:0:10" Value="0" />
    </DoubleAnimationUsingKeyFrames>
  </Storyboard>
  <MeshGeometry3D x:Key="Rect3D_O">
    <MeshGeometry3D.Positions>
      -50, 50, 0, -50, -50, 0, 50, -50, 0, 50, 50, 0
    </MeshGeometry3D.Positions>
    <MeshGeometry3D.TriangleIndices>
      0 1 2 2 3 0
    </MeshGeometry3D.TriangleIndices>
    <MeshGeometry3D.TextureCoordinates>
      0,0 0,1 1,1 1,0
    </MeshGeometry3D.TextureCoordinates>
    <MeshGeometry3D.Normals>
      0,0,1, 0,0,1, 0,0,1, 0,0,1
    </MeshGeometry3D.Normals>
  </MeshGeometry3D>
  <DiffuseMaterial x:Key="Img">
    <DiffuseMaterial.Brush>
      <ImageBrush ImageSource="start.jpg" />
    </DiffuseMaterial.Brush>
  </DiffuseMaterial>
</Window.Resources>
<Window.Triggers>
  <EventTrigger RoutedEvent="Loaded" SourceName="ScaleZ">
    <BeginStoryboard>
      <Storyboard
        Storyboard.TargetName="ScaleZ"
        Storyboard.TargetProperty="Tag"
        Duration="0:0:5.1"
      >
        <ObjectAnimationUsingKeyFrames>
          <DiscreteObjectKeyFrame KeyTime="0:0:5">
            <DiscreteObjectKeyFrame.Value> 1 </DiscreteObjectKeyFrame.Value>
          </DiscreteObjectKeyFrame>
        </ObjectAnimationUsingKeyFrames>
      </Storyboard>
    </BeginStoryboard>
    <BeginStoryboard Storyboard="{StaticResource sb}" />
  </EventTrigger>
</Window.Triggers>
<Grid>
  <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition Height="auto" />
    <RowDefinition Height="auto" />
    <RowDefinition Height="auto" />
    <RowDefinition Height="auto" />
    <RowDefinition Height="auto" />
    <RowDefinition Height="auto" />
  </Grid.RowDefinitions>
  <Viewport3D x:Name="View3D_1">
    <Viewport3D.Style>
      <Style TargetType="Viewport3D">
        <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=ScaleZ,Path=Value}" Value="1">
                <DataTrigger.EnterActions>
                    <BeginStoryboard >
                        <Storyboard>
                            <DoubleAnimationUsingKeyFrames  RepeatBehavior="Forever" Storyboard.TargetProperty="Opacity"  BeginTime="0:0:0"  Duration="0:0:10">
                                <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
                                <LinearDoubleKeyFrame KeyTime="0:0:5" Value="1"/>
                                <LinearDoubleKeyFrame KeyTime="0:0:8" Value="1"/>
                                <LinearDoubleKeyFrame KeyTime="0:0:10" Value="0"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
        </Style.Triggers>
      </Style>
    </Viewport3D.Style>
    <Viewport3D.Camera>
      <PerspectiveCamera
        FieldOfView="{Binding ElementName=FOV, Path=Value}"
        Position="0,0,100"
        LookDirection="0,0,-1"
      />
    </Viewport3D.Camera>
    <ModelVisual3D>
      <ModelVisual3D.Content>
        <Model3DGroup>
          <AmbientLight Color="White" />
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
                      Axis="0 1 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
                      Axis="1 0 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
                      Axis="0 1 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
                      Axis="1 0 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
        </Model3DGroup>
      </ModelVisual3D.Content>
    </ModelVisual3D>
  </Viewport3D>
  <Viewport3D x:Name="View3D_2">
    <Viewport3D.Camera>
      <PerspectiveCamera
        FieldOfView="{Binding ElementName=FOV, Path=Value}"
        Position="0,0,100"
        LookDirection="0,0,-1"
      />
    </Viewport3D.Camera>
    <ModelVisual3D>
      <ModelVisual3D.Content>
        <Model3DGroup>
          <AmbientLight Color="White" />
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
                      Axis="0 1 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
                      Axis="1 0 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
                      Axis="0 1 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
          <GeometryModel3D
            Geometry="{StaticResource Rect3D_O}"
            Material="{StaticResource Img}"
            BackMaterial="{StaticResource Img}"
          >
            <GeometryModel3D.Transform>
              <Transform3DGroup>
                <RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
                  <RotateTransform3D.Rotation>
                    <AxisAngleRotation3D
                      Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
                      Axis="1 0 0"
                    />
                  </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <ScaleTransform3D
                  ScaleZ="{Binding  ElementName=ScaleZ_2, Path=Value}"
                />
              </Transform3DGroup>
            </GeometryModel3D.Transform>
          </GeometryModel3D>
        </Model3DGroup>
      </ModelVisual3D.Content>
    </ModelVisual3D>
  </Viewport3D>

  <Slider
    Grid.Row="1"
    x:Name="Left_Top_Z"
    Minimum="-90"
    Value="12"
    Maximum="90"
    ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
  />
  <Slider
    Grid.Row="2"
    x:Name="Right_Bottom_Z"
    Minimum="-90"
    Value="-12"
    Maximum="0"
    ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
  />
  <Slider
    Grid.Row="3"
    x:Name="ScaleZ"
    Background="Red"
    Minimum="10"
    Maximum="100"
    ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
  >
    <Slider.Style>
      <Style TargetType="Slider">
        <Style.Triggers>
            <Trigger Property="Tag" Value="1">
                <Trigger.EnterActions>
                    <BeginStoryboard >
                        <Storyboard>
                            <DoubleAnimation  RepeatBehavior="Forever"  Storyboard.TargetProperty="Value"   From="11" To="20" BeginTime="0:0:0"   Duration="0:0:10" />
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.EnterActions>
            </Trigger>
        </Style.Triggers>
      </Style>
    </Slider.Style>
  </Slider>
  <Slider
    Grid.Row="4"
    x:Name="FOV"
    Minimum="0"
    Maximum="180"
    Value="176"
    ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
  />
  <Slider
    Grid.Row="5"
    x:Name="ScaleZ_2"
    Background="Black"
    Minimum="10"
    Maximum="100"
    ToolTip="{Binding  RelativeSource={RelativeSource Mode=Self}, Path=Value}"
  />
  <StackPanel Grid.Row="6" Orientation="Vertical">
    <TextBlock>
      <Run Text=" View3D_1 opacity" />
      <Run Text="{Binding ElementName=View3D_1, Path=Opacity}" />
      <Run Text=" View3D_1 value" />
      <Run Text="{Binding ElementName=ScaleZ, Path=Value}" />
      <Run Text=" Tag value" />
      <Run Text="{Binding ElementName=ScaleZ, Path=Tag}" />
      <Run Text=" ZIndex value" />
      <Run Text="{Binding ElementName=View3D_1, Path=(Panel.ZIndex)}" />
    </TextBlock>
    <TextBlock>
      <Run Text=" View3D_2 opacity" />
      <Run Text="{Binding ElementName=View3D_2, Path=Opacity}" />
      <Run Text=" View3D_2 value" />
      <Run Text="{Binding ElementName=ScaleZ_2, Path=Value}" />
      <Run Text=" ZIndex value" />
      <Run Text="{Binding ElementName=View3D_2, Path=(Panel.ZIndex)}" />
    </TextBlock>
  </StackPanel>
</Grid>

Thus, we achieve this effect without any C# code.

I must admit that the transition at time 0 is not perfect – there’s a slight double‑image – so there’s still much to learn.

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