Blazor 中的音乐符号 - 第 I 部分
Blazor 中的客户端音乐符号渲染。
更新:这是我的 Blazor 系列的第一部分。本文适用于 Blazor 0.4。第二部分,关于 Blazor 0.5.1,可以在这里找到:https://codeproject.org.cn/Articles/1254712/Music-Notation-in-Blazor-Part-2
引言
我最近发表了一篇关于 Manufaktura.Controls
库的文章,该库支持在各种 Web、桌面和移动环境中渲染乐谱。不幸的是,该库提供的所有 Web 实现都是基于服务器的。如果您打算显示静态乐谱(如本示例所示),这不成问题,但如果您想动态修改乐谱,可能会导致显著的延迟,如此示例所示(当您使用左侧的键盘控件添加音符时,音符会出现延迟,因为渲染是在服务器端完成的)。
有一些 JavaScript 乐谱库,如 Vexflow,但 Manufaktura.Controls
的最大优势是所有实现都使用单一代码库。在本文中,我将展示如何使用现有的 Manufaktura.Controls
代码库在客户端渲染乐谱。为此,我将使用 Blazor,这是一个在浏览器中运行 WebAssembly
的 .NET Web 框架。
创建组件
我不会详细介绍 Blazor 的工作原理以及如何创建 Blazor 项目。这个主题已经由各种文章涵盖,例如这篇。让我简要说明一下,您需要以下组件来启动一个 Blazor 项目:
然后在 VS 中,您只需创建一个新的 ASP.NET Core 项目并选择 Blazor 作为模板。
假设我们已经创建了一个空的 Blazor 项目。
首先,我们将创建一个 NoteViewer
组件——这个想法与 Manufaktura.Controls
的桌面实现中的 NoteViewer
控件或 ASP.NET MVC 和 ASP.NET Core 的 NoteViewerFor
Razor 扩展类似。您可以在这些文章中看到这些概念是如何工作的。现在,让我们将 NoteViewer.cshtml 添加到 Shared 文件夹。
@using Manufaktura.Controls.Model
@using Manufaktura.Controls.Rendering.Implementations
<RawHtml Content="@RenderScore()"></RawHtml>
@functions {
[Parameter]
Score Score { get; set; }
[Parameter]
HtmlScoreRendererSettings Settings { get; set; }
private int canvasIdCount = 0;
public string RenderScore()
{
IScore2HtmlBuilder builder;
if (Settings.RenderSurface == HtmlScoreRendererSettings.HtmlRenderSurface.Canvas)
builder = new Score2HtmlCanvasBuilder
(Score, string.Format("scoreCanvas{0}", canvasIdCount), Settings);
else if (Settings.RenderSurface == HtmlScoreRendererSettings.HtmlRenderSurface.Svg)
builder = new Score2HtmlSvgBuilder
(Score, string.Format("scoreCanvas{0}", canvasIdCount), Settings);
else throw new NotImplementedException("Unsupported rendering engine.");
string html = builder.Build();
canvasIdCount++;
return html;
}
}
上面的组件接受两个参数:一个需要渲染的 Score
和一个 Settings
对象。Settings
直接来自 Manufaktura.Controls
的 Web 实现,并在此处详细描述。RenderScore()
方法使用 IScore2HtmlBuilder
将 Score
转换为 HTML 代码。
Blazor 目前不提供渲染原始 HTML 的可能性(如 Razor 中的 Html.Raw()
方法),因此我们将为此创建一个组件。将 RawHtml.cshtml 添加到 Shared 文件夹。
@using HtmlAgilityPack;
@using Microsoft.AspNetCore.Blazor;
@using Microsoft.AspNetCore.Blazor.RenderTree;
@if (Content == null)
{
<span>Loading...</span>
}
else
{
@DynamicHtml
}
@functions {
[Parameter] string Content { get; set; }
RenderFragment DynamicHtml { get; set; }
protected override void OnInit()
{
RenderHtml();
}
private void RenderHtml()
{
DynamicHtml = null;
DynamicHtml = builder =>
{
var HtmlContent = Content;
if (HtmlContent == null) return;
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(HtmlContent);
var htmlBody = htmlDoc.DocumentNode;
Decend(htmlBody, builder);
};
}
private void Decend(HtmlNode ds, RenderTreeBuilder b)
{
foreach (var nNode in ds.ChildNodes)
{
if (nNode.NodeType == HtmlNodeType.Element)
{
b.OpenElement(0, nNode.Name);
if (nNode.HasAttributes) Attributes(nNode, b);
if (nNode.HasChildNodes) Decend(nNode, b);
b.CloseElement();
}
else
{
if (nNode.NodeType == HtmlNodeType.Text)
{
b.AddContent(0, nNode.InnerText);
}
}
}
}
private void Attributes(HtmlNode n, RenderTreeBuilder b)
{
foreach (var a in n.Attributes)
{
b.AddAttribute(0, a.Name, a.Value);
}
}
}
此组件的代码(经过一些修改)取自此项目:https://github.com/EdCharbeneau/BlazeDown
正如您所见,RawHtml
组件使用 HtmlAgilityPack
。您可以从 Nuget 获取它(使用 .NET Core 版本)。
这里有一点解释:首先,我们有由 IScore2HtmlBuilder
实现创建的 HTML 代码。HTML 代码是 string
形式的,所以我们必须明确告诉 Blazor 如何渲染它。首先,我们使用 HtmlAgilityPack
解析 HTML 代码。然后,我们遍历所有子节点和属性,并告诉 RenderTreeBuilder
一一渲染它们。这是在一个接受 RenderTreeBuilder
作为参数的委托方法中完成的。Blazor 在数据绑定期间会调用此方法。
在页面上使用组件
现在我们可以将 NoteViewer
组件添加到页面。将其插入 Index.cshtml 文件。
@using Manufaktura.Controls.Model
@using Manufaktura.Controls.Linq
@using Manufaktura.Controls.Extensions
@using Manufaktura.Controls.Rendering
@using Manufaktura.Controls.Rendering.Implementations
@using Manufaktura.Music.Model
@using Manufaktura.Music.Model.MajorAndMinor
@using Manufaktura.Controls.Model.Fonts
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
<NoteViewer Score=@score Settings=@settings />
<button class="btn btn-primary" onclick="@AddNote">Add note</button>
@functions {
Score score = Score.CreateOneStaffScore(Clef.Treble, MajorScale.C);
HtmlScoreRendererSettings settings = new HtmlScoreRendererSettings
{
RenderSurface = HtmlScoreRendererSettings.HtmlRenderSurface.Svg
};
void AddNote()
{
score.FirstStaff.Elements.Add
(new Note(Pitch.G4, RhythmicDuration.Quarter)); //https://github.com/aspnet/Blazor/issues/934
}
protected override void OnInit()
{
base.OnInit();
score.FirstStaff.AddRange(StaffBuilder
.FromPitches(Pitch.C4, Pitch.D4, Pitch.E4, Pitch.F4, Pitch.G4, Pitch.E4)
.AddRhythm("8 8 8 8 4 4"));
var musicFontUris = new[]
{ "/fonts/Polihymnia.svg", "/fonts/Polihymnia.ttf", "/fonts/Polihymnia.woff" };
settings.RenderingMode = ScoreRenderingModes.AllPages;
settings.Fonts.Add(MusicFontStyles.MusicFont,
new HtmlFontInfo("Polihymnia", 22, musicFontUris));
settings.Fonts.Add(MusicFontStyles.StaffFont,
new HtmlFontInfo("Polihymnia", 24, musicFontUris));
settings.Fonts.Add(MusicFontStyles.GraceNoteFont,
new HtmlFontInfo("Polihymnia", 14, musicFontUris));
settings.Fonts.Add(MusicFontStyles.LyricsFont,
new HtmlFontInfo("Open Sans", 9, "/fonts/OpenSans-Regular.ttf"));
settings.Fonts.Add(MusicFontStyles.TimeSignatureFont,
new HtmlFontInfo("Open Sans", 12, "/fonts/OpenSans-Regular.ttf"));
settings.Fonts.Add(MusicFontStyles.DirectionFont,
new HtmlFontInfo("Open Sans", 10, "/fonts/OpenSans-Regular.ttf"));
settings.Scale = 1;
settings.CustomElementPositionRatio = 0.8;
settings.IgnorePageMargins = true;
}
}
为了使其工作,您必须引用 Manufaktura.Controls
和 Manufaktura.Music
库。您可以在开头提到的文章中找到它们,或者从这个页面获取发布版本。您还需要将音乐字体添加到解决方案中。您可以在本文附带的文件中找到 Polihymnia.ttf。
在 OnInit
方法中,使用 StaffBuilder
API 创建一个示例 Score
。有关创建乐谱的更多信息,请参阅此页上的文章。
运行应用程序
现在您可以运行该应用程序,它看起来应该像这样:
看起来我们成功地使用现有的 Manufaktura.Controls
代码库在客户端渲染了一个简单的乐谱。但是当点击“添加音符”按钮时会发生什么?根据 AddNote
方法的实现,一个新的音符应该出现在乐谱上。
void AddNote()
{
score.FirstStaff.Elements.Add(new Note(Pitch.G4, RhythmicDuration.Quarter));
}
不幸的是,它却抛出了一个异常。
MonoPlatform.ts:70 Uncaught Error: Microsoft.AspNetCore.Blazor.Browser.Interop.JavaScriptException: Cannot set attribute on non-element child
这个问题与这里描述的问题类似:
我猜这是 Blazor 的一个 bug(此示例中使用了版本 0.4),我希望 Blazor 的开发者将来能解决它。如果 Blazor 的新版本解决了这个问题,或者我找到了一个变通方法,我将更新本文。
关注点
让我们看看在创建 Score
时使用 Rebeam()
方法会发生什么。
score.FirstStaff.AddRange(StaffBuilder
.FromPitches(Pitch.C4, Pitch.D4, Pitch.E4, Pitch.F4, Pitch.G4, Pitch.E4)
.AddRhythm("8 8 8 8 4 4")
.Rebeam());
异常被抛出。
Uncaught (in promise) Error: System.MemberAccessException:
Cannot create an abstract class: System.Reflection.Emit.DynamicMethod
at System.Linq.Expressions.Compiler.LambdaCompiler.Compile
(:59341/System.Linq.Expressions.LambdaExpression lambda) <0x1fcd558 +
0x00016> in <656221f224e346f8864575303b78815b>:0
at System.Linq.Expressions.LambdaExpression.Compile
(:59341/System.Boolean preferInterpretation) <0x1fcd308 + 0x0002a>
in <656221f224e346f8864575303b78815b>:0
at :59341/System.Linq.Expressions.LambdaExpression.Compile () <0x1fcd030 +
0x0000a> in <656221f224e346f8864575303b78815b>:0
at Manufaktura.Controls.Extensions.StaffBuilder+<>c.<Rebeam>b__13_1
(:59341/System.Reflection.TypeInfo t) <0x1e1bac0 + 0x00028> in <97e4516ea72e4e27bcedc5e90becc4b7>:0
at :59341/System.Linq.Enumerable+WhereSelectEnumerableIterator`2[TSource,TResult].MoveNext ()
<0x1e1ae68 + 0x0008c> in <ae6c925511ec4c7fa3cc179890e4f18f>:0
at :59341/System.Linq.Enumerable+<CastIterator>d__29`1[TResult].MoveNext ()
<0x1e1a830 + 0x000ac> in <ae6c925511ec4c7fa3cc179890e4f18f>:0
Rebeam()
方法搜索程序集以查找适当的 RebeamStrategy
,但有一个条件是排除 abstract
类。我不明白为什么它会尝试实例化一个 abstract
类。也许这是 Blazor 的另一个 bug。我将把这个问题报告给 Blazor 的开发者,如果找到解决方案,我会更新本文。