{"id":1298,"date":"2015-09-23T11:21:29","date_gmt":"2015-09-23T11:21:29","guid":{"rendered":"http:\/\/mindfusion.eu\/blog\/?p=1298"},"modified":"2021-01-20T16:53:39","modified_gmt":"2021-01-20T16:53:39","slug":"create-a-musical-score-writer-using-mindfusion-diagram-component","status":"publish","type":"post","link":"https:\/\/mindfusion.dev\/blog\/create-a-musical-score-writer-using-mindfusion-diagram-component\/","title":{"rendered":"Create a musical score writer using MindFusion diagram component."},"content":{"rendered":"<p>In this example we&#8217;ll show how to use various features of MindFusion.Diagramming API to create a musical score editor:<\/p>\n<p><strong>Custom node types<\/strong><br \/>\nWe&#8217;ll create a StaffNode class to represent the staff, and NoteNode class to represent a musical note.<\/p>\n<p><strong>Grouping<\/strong><br \/>\nNoteNodes will be attached to the StaffNode they were dropped onto (or nearby). If users move the staff around, the notes from the group will follow it.<\/p>\n<p><strong>Custom drawing logic<\/strong><br \/>\nWe&#8217;ll show how to draw custom graphics by overriding DrawLocal method of base DiagramNode class.<\/p>\n<p><strong>Using SVG images<\/strong><br \/>\nWe&#8217;ll show how to load an SVG image (for the G clef) and draw it as part of staff graphics.<\/p>\n<p><strong>NodeListView control<\/strong><br \/>\nNodeListView contains prototypical node instances whose clones are added to the diagram using drag-and-drop operations. We&#8217;ll add a staff and several notes to the list to let users drag them to the score diagram.<\/p>\n<p>The completed sample project can be downloaded from this link:<br \/>\n<a href=\"https:\/\/mindfusion.dev\/_samples\/ScoreWriter.zip\">ScoreWriter.zip<\/a><\/p>\n<p>Let&#8217;s start by defining StaffNode class to draw staves in the score diagram, and implement its Draw methods to draw five lines:<\/p>\n<pre>public class StaffNode : DiagramNode\n{\n\tpublic StaffNode()\n\t{\n\t\tvar rect = Bounds;\n\t\trect.Width = 200;\n\t\tSetBounds(rect, false, false);\n\n\t\t\/\/ disable vertical resize\n\t\tEnabledHandles =\n\t\t\tAdjustmentHandles.ResizeMiddleLeft |\n\t\t\tAdjustmentHandles.Move |\n\t\t\tAdjustmentHandles.ResizeMiddleRight;\n\t}\n\n\tpublic StaffNode(StaffNode prototype) : base(prototype)\n\t{\n\t}\n\n\tpublic override void DrawLocal(IGraphics graphics, RenderOptions options)\n\t{\n\t\tbase.DrawLocal(graphics, options);\n\n\t\tfor (int i = 0; i &lt; 5; i++)\n\t\t{\n\t\t\tfloat y = i * Bounds.Height \/ 4;\n\t\t\tusing (var pen = EffectivePen.CreateGdiPen())\n\t\t\t\tgraphics.DrawLine(pen, 0, y, Bounds.Width, y);\n\t\t}\n\t}\n\n\tpublic override void DrawShadowLocal(IGraphics graphics, RenderOptions options)\n\t{\n\t}\n}\n<\/pre>\n<p>Next, load an SVG drawing representing G clef and draw it at appropriate position. We&#8217;ll also override GetRepaintRect method to accommodate for parts of the clef that are drawn outside the staff&#8217;s boundaries:<\/p>\n<pre>static SvgContent gClef;\n\nstatic StaffNode()\n{\n\tgClef = new SvgContent();\n\tgClef.Parse(\"GClef.svg\");\n}\n\npublic override void DrawLocal(IGraphics graphics, RenderOptions options)\n{\n\t\/\/ ...\n\n\tvar rect = GetLocalBounds();\n\trect.Inflate(0, 8);\n\trect.X = 2;\n\trect.Width = 14;\n\tgClef.Draw(graphics, rect);\n}\n\npublic override RectangleF GetRepaintRect(bool includeConnected)\n{\n\tvar rect = base.GetRepaintRect(includeConnected);\n\trect.Inflate(0, 8);\n\treturn rect;\n}\n<\/pre>\n<p>Create an initial StaffNode instance from Form.Load event:<\/p>\n<pre>var initialStaff = new StaffNode();\ninitialStaff.Move(10, 10);\ndiagram.Nodes.Add(initialStaff);\n<\/pre>\n<p>If you run the project now, you should see the following diagram:<br \/>\n<img decoding=\"async\" src=\"http:\/\/mindfusion.dev\/_samples\/scorewriter1.png\" alt=\"score writer diagram in c#\" \/><\/p>\n<p>Next, define the Duration enumeration and NoteNode class to represent musical notes of various durations:<\/p>\n<pre>enum Duration\n{\n\tWhole,\n\tHalf,\n\tQuarter,\n\tEighth,\n\tSixteenth\n}\n\nclass NoteNode : DiagramNode\n{\n\tpublic NoteNode()\n\t{\n\t\tBounds = new RectangleF(0, 0, 6, 6);\n\t\tDuration = Duration.Whole;\n\t}\n\n\tpublic NoteNode(Duration duration)\n\t{\n\t\tBounds = new RectangleF(0, 0, 6, 6);\n\t\tDuration = duration;\n\t}\n\n\tpublic Duration Duration { get; set; }\n\n\tint position = 0;\n}\n<\/pre>\n<p>Implement NoteNode.Draw methods as follows:<\/p>\n<pre>public override void DrawLocal(IGraphics graphics, RenderOptions options)\n{\n\tbase.DrawLocal(graphics, options);\n\n\tvar cx = Bounds.Width \/ 2;\n\tvar cy = Bounds.Height \/ 2;\n\n\tvar gs = graphics.Save();\n\tgraphics.TranslateTransform(cx, cy);\n\tgraphics.RotateTransform(-10);\n\tgraphics.TranslateTransform(-cx, -cy);\n\n\tvar bounds = GetLocalBounds();\n\tbounds.Inflate(0, -bounds.Width \/ 10);\n\tvar path = new GraphicsPath();\n\tpath.AddEllipse(bounds);\n\n\tif (Duration == Duration.Whole || Duration == Duration.Half)\n\t{\n\t\tbounds.Inflate(-bounds.Width \/ 8, -bounds.Width \/ 6);\n\t\tpath.AddEllipse(bounds);\n\t}\n\tgraphics.FillPath(Brushes.Black, path);\n\n\tgraphics.Restore(gs);\n\n\tif (position &lt; -1 || position &gt; 8)\n\t{\n\t\t\/\/ draw ledger lines if above or below staff\n\t\tvar pen = EffectivePen.CreateGdiPen();\n\t\tvar staff = (StaffNode)MasterGroup.MainItem;\n\t\tvar yoff = staff.Bounds.Y - Bounds.Y;\n\t\tint i1 = position &lt; -1 ? position : 9;\n\t\tint i2 = position &lt; -1 ? -2 : position;\n\t\tfor (int i = i1; i &lt;= i2; i++)\n\t\t{\n\t\t\tif (i % 2 != 0)\n\t\t\t\tcontinue;\n\t\t\tvar y = yoff + i * staff.Bounds.Height \/ 8;\n\t\t\tgraphics.DrawLine(pen, -2, y, Bounds.Width + 2, y);\n\t\t}\n\t\tpen.Dispose();\n\t}\n\n\tif (Duration != Duration.Whole)\n\t{\n\t\t\/\/ draw stem\n\t\tfloat x = Bounds.Width;\n\t\tfloat y = Bounds.Height \/ 2;\n\t\tvar pen = new System.Drawing.Pen(Color.Black, 0.5f);\n\t\tgraphics.DrawLine(pen,\n\t\t\t\t            x - pen.Width \/ 2, y,\n\t\t\t\t            x - pen.Width \/ 2, y - Bounds.Height * 2);\n\t\tpen.Dispose();\n\t}\n\n\tif (Duration == Duration.Eighth || Duration == Duration.Sixteenth)\n\t{\n\t\tDrawFlag(graphics,\n\t\t\t\t    bounds.Width,\n\t\t\t\t    bounds.Height \/ 2 - bounds.Height * 2,\n\t\t\t\t    bounds.Width + 1,\n\t\t\t\t    bounds.Height);\n\t}\n\n\tif (Duration == Duration.Sixteenth)\n\t{\n\t\tDrawFlag(graphics,\n\t\t\t\t    bounds.Width,\n\t\t\t\t    bounds.Height - bounds.Height * 2,\n\t\t\t\t    bounds.Width + 1,\n\t\t\t\t    bounds.Height);\n\t}\n}\n\nvoid DrawFlag(IGraphics graphics, float x, float y, float w, float h)\n{\n\tfloat sh = h \/ 2;\n\tfloat sw = w \/ 3;\n\n\tvar pen = new System.Drawing.Pen(Color.Black, 0.5f);\n\tx -= pen.Width \/ 2;\n\tgraphics.DrawBezier(pen,\n\t\t\t            x, y,\n\t\t\t            x, y + sh,\n\t\t\t            x + sw * 1.2f, y + 2 * sh,\n\t\t\t            x + sw, y + 3 * sh);\n\tpen.Dispose();\n}\n\npublic override void DrawShadowLocal(IGraphics graphics, RenderOptions options)\n{\n}\n\npublic override RectangleF GetRepaintRect(bool includeConnected)\n{\n\tvar r = Bounds;\n\tr.Y -= r.Height * 2;\n\tr.Height *= 3;\n\tr.Width *= 2;\n\treturn r;\n}\n<\/pre>\n<p>Now, drag a NodeListView to the form and populate it from Load handler:<\/p>\n<pre>nodeListView.AddNode(new StaffNode());\n\nnodeListView.DefaultNodeSize = new SizeF(6, 6);\nnodeListView.AddNode(new NoteNode(Duration.Whole));\nnodeListView.AddNode(new NoteNode(Duration.Half));\nnodeListView.AddNode(new NoteNode(Duration.Quarter));\nnodeListView.AddNode(new NoteNode(Duration.Eighth));\nnodeListView.AddNode(new NoteNode(Duration.Sixteenth));\n<\/pre>\n<p>Drag and drop will not work just yet. First, we must enable the DiagramView.AllowDrop property to accept drag-and-drop events. Next, the custom classes must implement a copy constructor and serialization methods to be able to instantiate them through OLE drag events:<\/p>\n<pre>public NoteNode(NoteNode prototype) : base(prototype)\n{\n\tDuration = prototype.Duration;\n}\n\nprotected override void SaveTo(System.IO.BinaryWriter writer, PersistContext context)\n{\n\tbase.SaveTo(writer, context);\n\tcontext.Writer.Write((int)Duration);\n}\n\nprotected override void LoadFrom(System.IO.BinaryReader reader, PersistContext context)\n{\n\tbase.LoadFrom(reader, context);\n\tDuration = (Duration)context.Reader.ReadInt32();\n}\n<\/pre>\n<p>As a final touch for this example, let&#8217;s implement aligning notes to staves&#8217; lines and spaces. First lets declare a helper method that returns the nearest StaffNode at specified location in diagram:<\/p>\n<pre>static class DiagramExtensions\n{\n\tstatic public StaffNode NearestStaff(this Diagram diagram, PointF point)\n\t{\n\t\tvar staves = diagram.Nodes.OfType();\n\n\t\tStaffNode nearest = null;\n\t\tfloat minDist = float.MaxValue;\n\n\t\tforeach (var staff in staves)\n\t\t{\n\t\t\tif (staff.ContainsPoint(point))\n\t\t\t\treturn staff;\n\n\t\t\tvar borderPoint = staff.GetNearestBorderPoint(point);\n\t\t\tvar dist = Utilities.Distance(borderPoint, point);\n\t\t\tif (dist &lt; minDist)\n\t\t\t{\n\t\t\t\tminDist = dist;\n\t\t\t\tnearest = staff;\n\t\t\t}\n\t\t}\n\n\t\treturn minDist &lt; 20 ? nearest : null;\n\t}\n}\n<\/pre>\n<p>Next, implement StaffNode.Align method that aligns its argument to a line or space in the staff:<\/p>\n<pre>public PointF Align(PointF point, out int position)\n{\n\t\/\/ align to pitch line\/space\n\n\tfloat h = Bounds.Height \/ 8;\n\tfloat offset = point.Y - Bounds.Y;\n\tposition = (int)Math.Round(offset \/ h);\n\toffset = (float)Math.Round(offset \/ h) * h;\n\tpoint.Y = Bounds.Y + offset;\n\treturn point;\n}\n<\/pre>\n<p>Add NoteNode.AlignToStaff method that will find nearest StaffNode and align the note&#8217;s position to the staff.<\/p>\n<pre>public StaffNode AlignToStaff()\n{\n\tposition = 0;\n\n\tvar staff = Parent.NearestStaff(GetCenter());\n\tif (staff == null)\n\t\treturn null;\n\n\tvar alignedPoint = staff.Align(GetCenter(), out position);\n\talignedPoint.X -= Bounds.Width \/ 2;\n\talignedPoint.Y -= Bounds.Height \/ 2;\n\tMove(alignedPoint.X, alignedPoint.Y);\n\n\treturn staff;\n}\n<\/pre>\n<p>We can align notes after drag-and-drop from NodeListView by handling diagram&#8217;s NodeCreated event. We&#8217;ll use the same handler to attach notes to that staff, so that if users move a StaffNode, its attached NoteNodes will follow.<\/p>\n<pre>private void OnNodeCreated(object sender, NodeEventArgs e)\n{\n\tvar note = e.Node as NoteNode;\n\tif (note != null)\n\t{\n\t\tvar staff = note.AlignToStaff();\n\t\tif (staff != null)\n\t\t\tnote.AttachTo(staff, AttachToNode.TopLeft);\n\n\t\tnote.HandlesStyle = HandlesStyle.MoveOnly;\n\t}\n}\n<\/pre>\n<p>Finally, override NoteNode.CompleteModify to align notes after user moves them to a different position on the staff or to another staff in the score:<\/p>\n<pre>protected override void CompleteModify(PointF end, InteractionState ist)\n{\n\tbase.CompleteModify(end, ist);\n\n\tvar staff = AlignToStaff();\n\tif (staff != null)\n\t\tAttachTo(staff, AttachToNode.TopLeft);\n\telse\n\t{\n\t\tDetach();\n\t}\n}\n<\/pre>\n<p>Let&#8217;s run the project and compose some music \ud83d\ude42<br \/>\n<img decoding=\"async\" src=\"http:\/\/mindfusion.dev\/_samples\/scorewriter2.png\" alt=\".net diagram control\" \/><\/p>\n<p>A fully-featured scorewriter software would also allow for drawing rest, sharp and flat symbols, C and F clefs, and some other musical notation features, but these are left as exercise to the reader \ud83d\ude09<\/p>\n<p>The code above uses MindFusion\u2019s .NET API and can be used with Windows Forms, WPF, Silverlight and ASP.NET diagramming components. The Java API for Android and desktop Swing application will look similar, with setter method calls instead of property assignments.<\/p>\n<p>You can download the trial version of any MindFusion.Diagramming component from <a href=\"http:\/\/mindfusion.dev\/download-diagramming-pack.html\">this page.<\/a><\/p>\n<p>Enjoy!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this example we&#8217;ll show how to use various features of MindFusion.Diagramming API to create a musical score editor: Custom node types We&#8217;ll create a StaffNode class to represent the staff, and NoteNode class to represent a musical note. Grouping &hellip; <a href=\"https:\/\/mindfusion.dev\/blog\/create-a-musical-score-writer-using-mindfusion-diagram-component\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false},"version":2}},"categories":[95,74],"tags":[110,264,3,347],"class_list":["post-1298","post","type-post","status-publish","format-standard","hentry","category-diagramming-2","category-sample-code","tag-net","tag-c","tag-diagram","tag-scorewriter"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p3RlKs-kW","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts\/1298","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/comments?post=1298"}],"version-history":[{"count":3,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts\/1298\/revisions"}],"predecessor-version":[{"id":2558,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts\/1298\/revisions\/2558"}],"wp:attachment":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/media?parent=1298"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/categories?post=1298"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/tags?post=1298"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}