用.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; }
}
注意它存儲了雪花的坐標X
、Y
、放大比例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 API
的mciSendString
函數,將其播放:
[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騷操作】
最后,祝大家聖誕節快樂!😊