65.9K
CodeProject 正在变化。 阅读更多。
Home

Blazor 中的音乐符号 - 第 I 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2018年7月18日

MIT

4分钟阅读

viewsIcon

22695

downloadIcon

240

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 项目:

  1. .NET Core SDK 2.1 - 您可以在此处获取
  2. Visual Studio 2017 版本 15.7 - 已作为 VS 2017 的更新提供
  3. 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() 方法使用 IScore2HtmlBuilderScore 转换为 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.ControlsManufaktura.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 的开发者,如果找到解决方案,我会更新本文。

© . All rights reserved.