用.NET做聖誕節音樂盒


用.NET做聖誕節音樂盒

我曾經用這個程序送給我女朋友(現老婆)😊……首先來看一下最終運行效果:

(當然還有一首《We wish you Merry Christmas》的八音盒BGM,但由於gif的關系,你們可能聽不到。)

雪花是怎么做的?

首先本文將着重講解代碼,因此所有的資源都將在運行時下載,雪的的資源是從“碼雲”下載的,地址如下:https://gitee.com/sdcb/lovegl/raw/master/lovegl2014/icon1.ico

其顯示效果如下:

那么是怎么讓其旋轉起來的呢?我寫了一個Snow類,用來表示雪花:

class Snow
{
    readonly static string _snow = LoadUrlAsTempFile("https://gitee.com/sdcb/lovegl/raw/master/lovegl2014/icon1.ico");
    
    public Guid Id { get; set; } = Guid.NewGuid();
    
    public float Direction { get; set; }

    public float RotationAngle { get; set;}

    public float Scale { get; set; }

    public float Speed { get; set; }

    public float AngularSpeed { get; set;}

    public float X { get; set;}
    
    public float Y { get; set;}
    
    public bool IsOffScreen 
        => X < -50 || X > RenderWindow.Width + 50 || Y > RenderWindow.Height + 50;
    
    public RenderWindow RenderWindow { get; set; }
}

注意它存儲了雪花的坐標XY、放大比例Scale、注意我並沒有使用矢量類型Vector2,因此速度描述拆分成了角速度AngularSpeed、線速度Speed、當前的旋轉角RotationAngle和運行方向Direction幾個變量。另外還有一個幫助屬性IsOffScreen用於判斷雪花是否仍然在屏幕上。

雪花運行時,我通過一個Update函數來控制這些屬性的變化:

public void Update(RenderTimer timer)
{
    var dt = (float)timer.DurationSinceLastFrame.TotalSeconds;
    X += (float)(Speed * Math.Sin(Direction) * dt);
    Y += (float)(Speed * Math.Cos(Direction) * dt);
    RotationAngle += AngularSpeed * dt;
}

然后通過一個矩陣變換,即可完成雪花的平衡、縮放和旋轉:

public void Draw(BitmapManager bmp, RenderTarget ctx)
{
    ctx.Transform = 
        Matrix3x2.Translation(-24, -24) * 
        Matrix3x2.Rotation(RotationAngle) * 
        Matrix3x2.Scaling(Scale) * 
        Matrix3x2.Translation(X, Y);
    ctx.DrawBitmap(bmp[_snow], 1.0f, BitmapInterpolationMode.Linear);
}

注意第一個-24,指的是先平衡到雪花的中心間(-24, -24),然后再依次旋轉、縮放和平移。

最后,還需要一個函數,用於隨機在屏幕上生成不同大小、不同方向、不同線速度、不同角速度的雪花:

static Random r = new Random();
public static Snow CreateRandom(RenderWindow renderWindow)
{
    return new Snow
    {
        Direction = r.NextFloat(-30, 30) * (float)Math.PI / 180, 
        Scale = r.NextFloat(0.5f, 1.2f), 
        Speed = r.NextFloat(80, 100), 
        AngularSpeed = r.NextFloat(-120, 120) * (float)Math.PI / 180, 
        X = r.NextFloat(50, renderWindow.Width - 50), 
        Y = -50, 
        RenderWindow = renderWindow, 
    };
}

音樂是怎么播放的?

音樂也是從“碼雲”上下載的,當初搞C++版的時間為了節省資源文件,音樂部分我做成了一個代碼文件,該文件地址如下:https://gitee.com/sdcb/lovegl/raw/master/lovegl2014/music.cpp

它的本質是一個.mid文件。解析一個C++文件可能很復雜,但細一看,會發現這個C++文件很有規律:

除了第一行那個聲明外,里面的資源都是一個個的字節字面量,因此可以使用C#LINQ定則表達式不費吹灰之力即可將其解析出來:

static string GetMusicPath()
{
	var url = "https://gitee.com/sdcb/lovegl/raw/master/lovegl2014/music.cpp";
	$"Loading {url}...".Dump();

	var musicBytes = Util.Cache(() =>
	{
		var resp = httpClient.GetAsync(url).Result;
		var content = resp.Content.ReadAsStringAsync().Result;
		return Regex.Matches(content, @"0x([\dA-F]{2})")
			.Cast<Match>()
			.Select(x => byte.Parse(x.Groups[1].Value, NumberStyles.HexNumber))
			.ToArray();
	}, url);
	
	var path = Path.GetTempPath() + "merry-christmas.mid";
	File.WriteAllBytes(path, musicBytes);
	return path;
}

最后通過Windows APImciSendString函數,將其播放:

[DllImport("winmm.dll")]
private static extern long mciSendString(string Cmd, StringBuilder StrReturn, int ReturnLength, IntPtr HwndCallback);

// ...
void Initialize()
{
    _musicPath = GetMusicPath();
    $"open music: {_musicPath}...".Dump();
    mciSendString($"open {_musicPath} type sequencer alias music", null, 0, Handle);
}

注意通過mciSendString播放的音樂在播放完成后,並不像.NET API返回一個Task這樣方便,而是通過發送一個MM_MCINOTIFY的窗體事件,因此需要多花幾行代碼搞定循環播放這件事:

void PlayMusic()
{
    $"play music: {_musicPath}...".Dump();
    mciSendString($"play music from 0 notify", null, 0, Handle);
}

protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);
    
    const int MM_MCINOTIFY = 953;
    if (m.Msg == MM_MCINOTIFY)
    {
        PlayMusic();
    }
}

最后,循環播放的聖誕彩燈

以防沒人注意到,我先來個特寫(注意那三個燈是旋轉播放的):

這實際上是由三張圖片組成:

readonly string[] _mcs = new[]
{
    "https://gitee.com/sdcb/lovegl/raw/master/lovegl2014/design/mc1.png",
    "https://gitee.com/sdcb/lovegl/raw/master/lovegl2014/design/mc2.png",
    "https://gitee.com/sdcb/lovegl/raw/master/lovegl2014/design/mc3.png",
}.Select(x => LoadUrlAsTempFile(x)).ToArray();

int _mcIndex = 0;
public string CurrentMc => _mcs[_mcIndex];

要定期循環播放,我定義了一個Timer,定時500毫秒,然后修改當前顯示的圖片索引即可:

readonly System.Windows.Forms.Timer _timer;

public MerryChristmas()
{
    // ...

    _timer = new System.Windows.Forms.Timer() { Interval = 500, Enabled = true };
    _timer.Tick += (o, args) =>
    {
        _mcIndex = (_mcIndex + 1) % _mcs.Length;
    };
}

然后渲染時,顯示為“當前”的那張圖片:

protected override void OnDraw(DeviceContext ctx)
{
    ctx.Clear(Color.Transparent);
    ctx.DrawBitmap(XResource.Bitmaps[CurrentMc], 1.0f, InterpolationMode.Linear);
}

至於為何它也能拖拽,因為我取了個巧,這里我創建了兩個窗口,讓這個整個窗口顯示Merry Christmas的圖片,然后使之可以拖拽即可:

DragMoveEnabled = true;

總結

其實這並不是我的發明,這個早在我上大學時Windows XP時代就看到過,我的大學室友都覺得它很炫酷,於是我進行了很久很久的探索。

最開始的時候,我使用WPF對其進行了“仿制”,通過WPF中的AllowsTransparency = true來達到屏幕透明的效果。但其性能低下,CPU使用率往往徘徊在13%左右(單核100%)。

后來我了解到Direct2D以及UpdateLayeredWindowIndirect方法,所以我立即用C++再對這個程序進行了重制。CPU使用率降低至0%C++代碼已在5年前開源在“碼雲”(這個也是我當年送給我女朋友的版本):https://gitee.com/sdcb/lovegl/

再后來……直到最近,.NET革命再次爆發,我又拿起了最拿手的C#,基於我做的FlysEngine再次對其進行了重制。這次重制的原則是對新手友好,因此代碼盡可能簡短,一個文件搞定,不要用項目,因此所有資源文件都是運行時從“碼雲”下載。

說了這么多,我也曾對這個最早的、別人寫的這個程序進行了一定的研究,我發現他並沒有使用Direct2D,而是用的最簡單的GDI和多窗口模式,每個雪花都是一個窗口。然后它的定時器時間設得比較長,因此雪花旋轉並不像我的這樣平滑,因此他的那個程序也能在保持CPU使用率相對較低的同時確保用戶體驗。

福利:所有這些可運行、完整的代碼,都可以在我的Github博客數據上進行下載:https://github.com/sdcb/blog-data/tree/master/2019/20191225-merry-christmas-by-dotnet

應很多朋友建議,我把該程序編譯了一個二進制版本,各位可以自行下載運行(沒有病毒):鏈接: https://pan.baidu.com/s/15J6o1FLaWSUGEVrEsb0YXw 提取碼: 6mpr

喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】

DotNet騷操作

最后,祝大家聖誕節快樂!😊


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM