WPF 星空效果:创建逼真的宇宙背景

前言

视觉效果不仅是用户体验的重要组成部分,也是吸引用户注意力的关键因素之一。特别是在WPF平台上,开发拥有丰富的工具和库来创建引人入胜的界面。星空效果作为一种既美观又富有创意的视觉元素,能够为应用程序增添独特的魅力和深度,使用户仿佛置身于浩瀚宇宙之中。

本文将详细介绍如何使用WPF实现令人惊叹的星空效果。通过结合WPF的强大图形处理能力和动画特性,我们将一步步引导您构建一个逼真的星空背景,让您的应用程序界面焕发出迷人的光彩。

效果

前阵子看到ay的蜘蛛网效果和知乎的登录页背景,觉得效果很酷.自己也想写一个.于是写着写着就变成这样了.少女梦幻的赶脚有木有.我这有着一颗少女心的抠脚大汉。

实现思路

分为两个部分:

1、星星无休止的漫游.

2、星星之间的连线.

星星和连线非别放到两个容器里,以便分开操作.

星星

把星星的运动分解为X轴和Y轴两个不相干的运动,分别操作.操作就是随机生成一个速度,随机生成一个时间.运动完之后再随机生成一个速度,随机生成一个时间......无限循环.

星星的旋转也是同样的道理.

连线

首先解释下连线的规则.两个星星之间连线,每个星星都有一个连线的势力范围,就是宽度乘以连线倍率,这个连线倍率可以在窗体设置.当两个势力范围有交集的时候,就连线.

例:星1宽度5,星2宽度10,连线倍率是3,那么这两个星星的距离小于53+10 3=45时就连线,大于45时断开.如果连线倍率设置为4,则两个星星减的距离小于54+104=60时连线,大于60时断开.

实现与资源占有率

星星运动的实现有两种:

1、基于Grid和TranslateTransform.用DoubleAnimation动画控制星星的位移.

2、基于Canvas.通过帧动画控制Canvas的X,Y.

连线的实现也有两种:

1、简单粗暴.在每一帧都清空连线容器.然后双层循环星星,重新连接所有星星(符合连线规则的).

2、在每一帧循环连线,判断连线规则.符合就改变此连线的X1,Y1,X2,Y2.而不去重新new连线.不符合规则的就移除.然后依然是双层循环星星,看符合规则的两个星星间有没有连线,没有的就new一个.

众所周知,WPF做这种动画资源占有率还是比较高的,写了这么多实现,也是因为这个.

大体上还是基于Canvas的实现占用资源稍低.但也有个问题,如果给星星再加一个模糊效果的话,基于Canvas实现的资源占有率不会飙升,而是帧数明显降低.(也可能是我电脑环境的原因)

并不能说那种实现好与坏,可能具体运行环境不一样,参数设置不一样,每种实现都有不同的表现.

然后关于资源占有率问题,以我目前的水平,就只能到这了.博友们自己取舍吧.

部分代码

StarrySky.cs代码如下;

c# 复制代码
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace WPFDevelopers.Controls
{

    [TemplatePart(Name = GridTemplateName, Type = typeof(Grid))]
    [TemplatePart(Name = CanvasTemplateName, Type = typeof(Canvas))]
    public class StarrySky : Control
    {
        private const string GridTemplateName = "PART_GridLineContainer";

        private const string CanvasTemplateName = "PART_CanvasStarContainer";

        private Grid _grid;
        private Canvas _canvas;

        public static readonly DependencyProperty StarCountProperty =
            DependencyProperty.Register("StarCount", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(10));

        public static readonly DependencyProperty StarSizeMinProperty =
            DependencyProperty.Register("StarSizeMin", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(5));

        public static readonly DependencyProperty StarSizeMaxProperty =
            DependencyProperty.Register("StarSizeMax", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(20));

        public static readonly DependencyProperty StarVMinProperty =
            DependencyProperty.Register("StarVMin", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(10));

        public static readonly DependencyProperty StarVMaxProperty =
            DependencyProperty.Register("StarVMax", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(20));

        public static readonly DependencyProperty StarRVMinProperty =
            DependencyProperty.Register("StarRVMin", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(90));

        public static readonly DependencyProperty StarRVMaxProperty =
            DependencyProperty.Register("StarRVMax", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(360));

        public static readonly DependencyProperty LineRateProperty =
            DependencyProperty.Register("LineRate", typeof(int), typeof(StarrySky), 
                                        new UIPropertyMetadata(3));

        private readonly Random _random = new Random();
        private StarInfo[] _stars;


        static StarrySky()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(StarrySky),
                new FrameworkPropertyMetadata(typeof(StarrySky)));
        }

        public StarrySky()
        {
            Loaded += delegate
            {
                CompositionTarget.Rendering += delegate
                {
                    StarRoamAnimation();
                    AddOrRemoveStarLine();
                    MoveStarLine();
                };
            };
        }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _grid = GetTemplateChild(GridTemplateName) as Grid;
            _canvas = GetTemplateChild(CanvasTemplateName) as Canvas;
        }
c# 复制代码
 /// <summary>
    ///     星星
    /// </summary>
    internal class StarInfo
    {
        /// <summary>
        ///     X坐标
        /// </summary>
        public double X { get; set; }

        /// <summary>
        ///     X轴速度(单位距离/帧)
        /// </summary>
        public double XV { get; set; }

        /// <summary>
        ///     X坐标以X轴速度运行的时间(帧)
        /// </summary>
        public int XT { get; set; }

        /// <summary>
        ///     Y坐标
        /// </summary>
        public double Y { get; set; }

        /// <summary>
        ///     Y轴速度(单位距离/帧)
        /// </summary>
        public double YV { get; set; }

        /// <summary>
        ///     Y坐标以Y轴速度运行的时间(帧)
        /// </summary>
        public int YT { get; set; }

        /// <summary>
        ///     对星星的引用
        /// </summary>
        public Path StarRef { get; set; }

        public Dictionary<StarInfo, Line> StarLines { get; set; }
    }
c# 复制代码
 public int StarCount
        {
            get => (int)GetValue(StarCountProperty);
            set => SetValue(StarCountProperty, value);
        }

        public int StarSizeMin
        {
            get => (int)GetValue(StarSizeMinProperty);
            set => SetValue(StarSizeMinProperty, value);
        }

        public int StarSizeMax
        {
            get => (int)GetValue(StarSizeMaxProperty);
            set => SetValue(StarSizeMaxProperty, value);
        }

        public int StarVMin
        {
            get => (int)GetValue(StarVMinProperty);
            set => SetValue(StarVMinProperty, value);
        }

        public int StarVMax
        {
            get => (int)GetValue(StarVMaxProperty);
            set => SetValue(StarVMaxProperty, value);
        }


        public int StarRVMin
        {
            get => (int)GetValue(StarRVMinProperty);
            set => SetValue(StarRVMinProperty, value);
        }

        public int StarRVMax
        {
            get => (int)GetValue(StarRVMaxProperty);
            set => SetValue(StarRVMaxProperty, value);
        }

        public int LineRate
        {
            get => (int)GetValue(LineRateProperty);
            set => SetValue(LineRateProperty, value);
        }
c# 复制代码
 public void InitStar()
        {
            //清空星星容器
            _stars = new StarInfo[StarCount];
            _canvas.Children.Clear();
            _grid.Children.Clear();
            //生成星星
            for (var i = 0; i < StarCount; i++)
            {
                double size = _random.Next(StarSizeMin, StarSizeMax + 1); //星星尺寸
                var starInfo = new StarInfo
                {
                    X = _random.Next(0, (int)_canvas.ActualWidth),
                    XV = (double)_random.Next(-StarVMax, StarVMax) / 60,
                    XT = _random.Next(6, 301), //帧
                    Y = _random.Next(0, (int)_canvas.ActualHeight),
                    YV = (double)_random.Next(-StarVMax, StarVMax) / 60,
                    YT = _random.Next(6, 301), //帧
                    StarLines = new Dictionary<StarInfo, Line>()
                };
                var star = new Path
                {
                    Data = Application.Current.Resources["PathStarrySky"] as Geometry,
                    Width = size,
                    Height = size,
                    Stretch = Stretch.Fill,
                    Fill = GetRandomColorBursh(),
                    RenderTransformOrigin = new Point(0.5, 0.5),
                    RenderTransform = new RotateTransform { Angle = 0 }
                };
                Canvas.SetLeft(star, starInfo.X);
                Canvas.SetTop(star, starInfo.Y);
                starInfo.StarRef = star;
                //设置星星旋转动画
                SetStarRotateAnimation(star);
                //添加到容器
                _stars[i] = starInfo;
                _canvas.Children.Add(star);
            }
        }
       
        private void SetStarRotateAnimation(Path star)
        {
            double v = _random.Next(StarRVMin, StarRVMax + 1); //速度
            double a = _random.Next(0, 360 * 5); //角度
            var t = a / v; //时间
            var dur = new Duration(new TimeSpan(0, 0, 0, 0, (int)(t * 1000)));

            var sb = new Storyboard
            {
                Duration = dur
            };
            //动画完成事件 再次设置此动画
            sb.Completed += (S, E) => { SetStarRotateAnimation(star); };

            var da = new DoubleAnimation
            {
                To = a,
                Duration = dur
            };
            Storyboard.SetTarget(da, star);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
            sb.Children.Add(da);
            sb.Begin(this);
        }
        private SolidColorBrush GetRandomColorBursh()
        {
            var r = (byte)_random.Next(128, 256);
            var g = (byte)_random.Next(128, 256);
            var b = (byte)_random.Next(128, 256);
            return new SolidColorBrush(Color.FromRgb(r, g, b));
        }
        /// <summary>
        ///     星星漫游动画
        /// </summary>
        private void StarRoamAnimation()
        {
            if (_stars == null)
                return;

            foreach (var starInfo in _stars)
            {
                //X轴运动
                if (starInfo.XT > 0)
                {
                    //运动时间大于0,继续运动
                    if (starInfo.X >= _canvas.ActualWidth || starInfo.X <= 0)
                        //碰到边缘,速度取反向
                        starInfo.XV = -starInfo.XV;
                    //位移加,时间减
                    starInfo.X += starInfo.XV;
                    starInfo.XT--;
                    Canvas.SetLeft(starInfo.StarRef, starInfo.X);
                }
                else
                {
                    //运动时间小于0,重新设置速度和时间
                    starInfo.XV = (double)_random.Next(-StarVMax, StarVMax) / 60;
                    starInfo.XT = _random.Next(100, 1001);
                }

                //Y轴运动
                if (starInfo.YT > 0)
                {
                    //运动时间大于0,继续运动
                    if (starInfo.Y >= _canvas.ActualHeight || starInfo.Y <= 0)
                        //碰到边缘,速度取反向
                        starInfo.YV = -starInfo.YV;
                    //位移加,时间减
                    starInfo.Y += starInfo.YV;
                    starInfo.YT--;
                    Canvas.SetTop(starInfo.StarRef, starInfo.Y);
                }
                else
                {
                    //运动时间小于0,重新设置速度和时间
                    starInfo.YV = (double)_random.Next(-StarVMax, StarVMax) / 60;
                    starInfo.YT = _random.Next(100, 1001);
                }
            }
        }
        /// <summary>
        ///     添加或者移除星星之间的连线
        /// </summary>
        private void AddOrRemoveStarLine()
        {
            //没有星星 直接返回
            if (_stars == null || StarCount != _stars.Length)
                return;

            //生成星星间的连线
            for (var i = 0; i < StarCount - 1; i++)
                for (var j = i + 1; j < StarCount; j++)
                {
                    var star1 = _stars[i];
                    var x1 = star1.X + star1.StarRef.Width / 2;
                    var y1 = star1.Y + star1.StarRef.Height / 2;
                    var star2 = _stars[j];
                    var x2 = star2.X + star2.StarRef.Width / 2;
                    var y2 = star2.Y + star2.StarRef.Height / 2;
                    var s = Math.Sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1)); //两个星星间的距离
                    var threshold = star1.StarRef.Width * LineRate + star2.StarRef.Width * LineRate;
                    if (s <= threshold)
                    {
                        if (!star1.StarLines.ContainsKey(star2))
                        {
                            var line = new Line
                            {
                                X1 = x1,
                                Y1 = y1,
                                X2 = x2,
                                Y2 = y2,
                                Stroke = GetStarLineBrush(star1.StarRef, star2.StarRef)
                            };
                            star1.StarLines.Add(star2, line);
                            _grid.Children.Add(line);
                        }
                    }
                    else
                    {
                        if (star1.StarLines.ContainsKey(star2))
                        {
                            _grid.Children.Remove(star1.StarLines[star2]);
                            star1.StarLines.Remove(star2);
                        }
                    }
                }
        }

        /// <summary>
        ///     移动星星之间的连线
        /// </summary>
        private void MoveStarLine()
        {
            //没有星星 直接返回
            if (_stars == null)
                return;

            foreach (var star in _stars)
                foreach (var starLine in star.StarLines)
                {
                    var line = starLine.Value;
                    line.X1 = star.X + star.StarRef.Width / 2;
                    line.Y1 = star.Y + star.StarRef.Height / 2;
                    line.X2 = starLine.Key.X + starLine.Key.StarRef.Width / 2;
                    line.Y2 = starLine.Key.Y + starLine.Key.StarRef.Height / 2;
                }
        }

        /// <summary>
        ///     获取星星连线颜色画刷
        /// </summary>
        /// <param name="star0">起始星星</param>
        /// <param name="star1">终点星星</param>
        /// <returns>LinearGradientBrush</returns>
        private LinearGradientBrush GetStarLineBrush(Path star0, Path star1)
        {
            return new LinearGradientBrush
            {
                GradientStops = new GradientStopCollection
                {
                    new GradientStop { Offset = 0, Color = (star0.Fill as SolidColorBrush).Color },
                    new GradientStop { Offset = 1, Color = (star1.Fill as SolidColorBrush).Color }
                }
            };
        }
    }

StarrySky.xaml代码如下;

xml 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:controls="clr-namespace:WPFDevelopers.Controls">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Basic/ControlBasic.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    <RadialGradientBrush x:Key="StarrySkyRadialGradientBrush" GradientOrigin="0.5,0" Center="0.5,0.3" RadiusX="0.7">
        <GradientStop Color="#FF04040E" Offset="0"/>
        <GradientStop Color="#FF24315D" Offset="1"/>
    </RadialGradientBrush>
    <Style TargetType="{x:Type controls:StarrySky}" 
           BasedOn="{StaticResource ControlBasicStyle}">
        <Setter Property="Background" Value="{StaticResource StarrySkyRadialGradientBrush}"></Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:StarrySky}">
                    <Grid Background="{TemplateBinding Background}">
                        <Grid x:Name="PART_GridLineContainer"/>
                        <Canvas x:Name="PART_CanvasStarContainer"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

StarrySkyExample.xaml代码如下;

xml 复制代码
<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.StarrySkyExample"
             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:wpfdev="https://github.com/WPFDevelopersOrg/WPFDevelopers"
             xmlns:ws="https://github.com/WPFDevelopersOrg.WPFDevelopers.Minimal"
             xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Foreground" Value="White"></Setter>
            <Setter Property="FontSize" Value="14"></Setter>
            <Setter Property="VerticalAlignment" Value="Center"></Setter>
            <Setter Property="Margin" Value="2"></Setter>
        </Style>
        <Style TargetType="{x:Type StackPanel}">
            <Setter Property="Margin" Value="2"></Setter>
        </Style>
        <Style TargetType="{x:Type TextBox}" 
               BasedOn="{StaticResource {x:Type TextBox}}">
            <Setter Property="ws:ElementHelper.Watermark" Value="输入内容"></Setter>
            <Setter Property="Margin" Value="-30,0"></Setter>
            <Setter Property="Width" Value="100"></Setter>
        </Style>
    </UserControl.Resources>
    <Grid>
        <wpfdev:StarrySky Name="myStarrySky">

        </wpfdev:StarrySky>
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="星星个数:"></TextBlock>
                <TextBox x:Name="tbx_starCount" 
                         Text="{Binding ElementName=myStarrySky,Path=StarCount}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最小尺寸:"></TextBlock>
                <TextBox Name="tbx_starSizeMin"  
                         Text="{Binding ElementName=myStarrySky,Path=StarSizeMin}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最大尺寸:"></TextBlock>
                <TextBox Name="tbx_starSizeMax" 
                         Text="{Binding ElementName=myStarrySky,Path=StarSizeMax}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最小速度:"></TextBlock>
                <TextBox Name="tbx_starVMin" 
                         Text="{Binding ElementName=myStarrySky,Path=StarVMin}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最大速度:"></TextBlock>
                <TextBox Name="tbx_starVMax" 
                         Text="{Binding ElementName=myStarrySky,Path=StarVMax}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最小转速:"></TextBlock>
                <TextBox Name="tbx_starRVMin" 
                         Text="{Binding ElementName=myStarrySky,Path=StarRVMin}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最大转速:"></TextBlock>
                <TextBox Name="tbx_starRVMax" 
                         Text="{Binding ElementName=myStarrySky,Path=StarRVMax}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="连线倍率:"></TextBlock>
                <TextBox Name="tbx_lineRate" 
                         Text="{Binding ElementName=myStarrySky,Path=LineRate}"></TextBox>
            </StackPanel>
            <Button Name="btn_render" Content="生成" Click="btn_render_Click"/>
        </StackPanel>
    </Grid>
</UserControl>
c# 复制代码
using System.Windows.Controls;

namespace WPFDevelopers.Samples.ExampleViews
{
    /// <summary>
    /// StarrySkyExample.xaml 的交互逻辑
    /// </summary>
    public partial class StarrySkyExample : UserControl
    {
        public StarrySkyExample()
        {
            InitializeComponent();
        }

        private void btn_render_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            myStarrySky.InitStar();
        }
    }
}

StarrySkyExample.xaml.cs代码如下;

源码下载

星空效果的两种实现

基于Canvas的不是粗暴刷新的那个版本优化.当然没有质的改变,只是优化了一点点.主要是在星星对象上添加对连线的引用列表.循环列表,而不是去循环线的容器,逻辑简单了,少层循环.大概100个星减少5%的cup消耗.

优化后的源码下载

源码:files.cnblogs.com/files/tsliw...

Gtihub:github.com/WPFDevelope...

Gitee:gitee.com/WPFDevelope...

总结

通过本文的详细介绍,我们已经成功地探索了如何在WPF中实现令人惊叹的星空效果。从说明到代码实现,再到优化与扩展,每一步都在帮助大家充分利用WPF的强大功能,创造出既美观又高效的星空背景。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 DotNet技术匠 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:普通的地球人

出处:cnblogs.com/tsliwei/p/6282183.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!

相关推荐
system_error_x16 分钟前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn2 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈3 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780513 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123453 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒3 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记4 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598784 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端
雨中飘荡的记忆4 小时前
Claude Code 入门必会:@、!、Slash Commands,一篇讲清楚
人工智能·后端
用户8356290780514 小时前
如何使用 Python 在 Word 文档中创建和格式化列表
后端·python