In this example we’ll show how to use various features of MindFusion.Diagramming API to create a musical score editor:
Custom node types
We’ll create a StaffNode class to represent the staff, and NoteNode class to represent a musical note.
Grouping
NoteNodes 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.
Custom drawing logic
We’ll show how to draw custom graphics by overriding DrawLocal method of base DiagramNode class.
Using SVG images
We’ll show how to load an SVG image (for the G clef) and draw it as part of staff graphics.
NodeListView control
NodeListView contains prototypical node instances whose clones are added to the diagram using drag-and-drop operations. We’ll add a staff and several notes to the list to let users drag them to the score diagram.
The completed sample project can be downloaded from this link:
ScoreWriter.zip
Let’s start by defining StaffNode class to draw staves in the score diagram, and implement its Draw methods to draw five lines:
public class StaffNode : DiagramNode { public StaffNode() { var rect = Bounds; rect.Width = 200; SetBounds(rect, false, false); // disable vertical resize EnabledHandles = AdjustmentHandles.ResizeMiddleLeft | AdjustmentHandles.Move | AdjustmentHandles.ResizeMiddleRight; } public StaffNode(StaffNode prototype) : base(prototype) { } public override void DrawLocal(IGraphics graphics, RenderOptions options) { base.DrawLocal(graphics, options); for (int i = 0; i < 5; i++) { float y = i * Bounds.Height / 4; using (var pen = EffectivePen.CreateGdiPen()) graphics.DrawLine(pen, 0, y, Bounds.Width, y); } } public override void DrawShadowLocal(IGraphics graphics, RenderOptions options) { } }
Next, load an SVG drawing representing G clef and draw it at appropriate position. We’ll also override GetRepaintRect method to accommodate for parts of the clef that are drawn outside the staff’s boundaries:
static SvgContent gClef; static StaffNode() { gClef = new SvgContent(); gClef.Parse("GClef.svg"); } public override void DrawLocal(IGraphics graphics, RenderOptions options) { // ... var rect = GetLocalBounds(); rect.Inflate(0, 8); rect.X = 2; rect.Width = 14; gClef.Draw(graphics, rect); } public override RectangleF GetRepaintRect(bool includeConnected) { var rect = base.GetRepaintRect(includeConnected); rect.Inflate(0, 8); return rect; }
Create an initial StaffNode instance from Form.Load event:
var initialStaff = new StaffNode(); initialStaff.Move(10, 10); diagram.Nodes.Add(initialStaff);
If you run the project now, you should see the following diagram:
Next, define the Duration enumeration and NoteNode class to represent musical notes of various durations:
enum Duration { Whole, Half, Quarter, Eighth, Sixteenth } class NoteNode : DiagramNode { public NoteNode() { Bounds = new RectangleF(0, 0, 6, 6); Duration = Duration.Whole; } public NoteNode(Duration duration) { Bounds = new RectangleF(0, 0, 6, 6); Duration = duration; } public Duration Duration { get; set; } int position = 0; }
Implement NoteNode.Draw methods as follows:
public override void DrawLocal(IGraphics graphics, RenderOptions options) { base.DrawLocal(graphics, options); var cx = Bounds.Width / 2; var cy = Bounds.Height / 2; var gs = graphics.Save(); graphics.TranslateTransform(cx, cy); graphics.RotateTransform(-10); graphics.TranslateTransform(-cx, -cy); var bounds = GetLocalBounds(); bounds.Inflate(0, -bounds.Width / 10); var path = new GraphicsPath(); path.AddEllipse(bounds); if (Duration == Duration.Whole || Duration == Duration.Half) { bounds.Inflate(-bounds.Width / 8, -bounds.Width / 6); path.AddEllipse(bounds); } graphics.FillPath(Brushes.Black, path); graphics.Restore(gs); if (position < -1 || position > 8) { // draw ledger lines if above or below staff var pen = EffectivePen.CreateGdiPen(); var staff = (StaffNode)MasterGroup.MainItem; var yoff = staff.Bounds.Y - Bounds.Y; int i1 = position < -1 ? position : 9; int i2 = position < -1 ? -2 : position; for (int i = i1; i <= i2; i++) { if (i % 2 != 0) continue; var y = yoff + i * staff.Bounds.Height / 8; graphics.DrawLine(pen, -2, y, Bounds.Width + 2, y); } pen.Dispose(); } if (Duration != Duration.Whole) { // draw stem float x = Bounds.Width; float y = Bounds.Height / 2; var pen = new System.Drawing.Pen(Color.Black, 0.5f); graphics.DrawLine(pen, x - pen.Width / 2, y, x - pen.Width / 2, y - Bounds.Height * 2); pen.Dispose(); } if (Duration == Duration.Eighth || Duration == Duration.Sixteenth) { DrawFlag(graphics, bounds.Width, bounds.Height / 2 - bounds.Height * 2, bounds.Width + 1, bounds.Height); } if (Duration == Duration.Sixteenth) { DrawFlag(graphics, bounds.Width, bounds.Height - bounds.Height * 2, bounds.Width + 1, bounds.Height); } } void DrawFlag(IGraphics graphics, float x, float y, float w, float h) { float sh = h / 2; float sw = w / 3; var pen = new System.Drawing.Pen(Color.Black, 0.5f); x -= pen.Width / 2; graphics.DrawBezier(pen, x, y, x, y + sh, x + sw * 1.2f, y + 2 * sh, x + sw, y + 3 * sh); pen.Dispose(); } public override void DrawShadowLocal(IGraphics graphics, RenderOptions options) { } public override RectangleF GetRepaintRect(bool includeConnected) { var r = Bounds; r.Y -= r.Height * 2; r.Height *= 3; r.Width *= 2; return r; }
Now, drag a NodeListView to the form and populate it from Load handler:
nodeListView.AddNode(new StaffNode()); nodeListView.DefaultNodeSize = new SizeF(6, 6); nodeListView.AddNode(new NoteNode(Duration.Whole)); nodeListView.AddNode(new NoteNode(Duration.Half)); nodeListView.AddNode(new NoteNode(Duration.Quarter)); nodeListView.AddNode(new NoteNode(Duration.Eighth)); nodeListView.AddNode(new NoteNode(Duration.Sixteenth));
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:
public NoteNode(NoteNode prototype) : base(prototype) { Duration = prototype.Duration; } protected override void SaveTo(System.IO.BinaryWriter writer, PersistContext context) { base.SaveTo(writer, context); context.Writer.Write((int)Duration); } protected override void LoadFrom(System.IO.BinaryReader reader, PersistContext context) { base.LoadFrom(reader, context); Duration = (Duration)context.Reader.ReadInt32(); }
As a final touch for this example, let’s implement aligning notes to staves’ lines and spaces. First lets declare a helper method that returns the nearest StaffNode at specified location in diagram:
static class DiagramExtensions { static public StaffNode NearestStaff(this Diagram diagram, PointF point) { var staves = diagram.Nodes.OfType(); StaffNode nearest = null; float minDist = float.MaxValue; foreach (var staff in staves) { if (staff.ContainsPoint(point)) return staff; var borderPoint = staff.GetNearestBorderPoint(point); var dist = Utilities.Distance(borderPoint, point); if (dist < minDist) { minDist = dist; nearest = staff; } } return minDist < 20 ? nearest : null; } }
Next, implement StaffNode.Align method that aligns its argument to a line or space in the staff:
public PointF Align(PointF point, out int position) { // align to pitch line/space float h = Bounds.Height / 8; float offset = point.Y - Bounds.Y; position = (int)Math.Round(offset / h); offset = (float)Math.Round(offset / h) * h; point.Y = Bounds.Y + offset; return point; }
Add NoteNode.AlignToStaff method that will find nearest StaffNode and align the note’s position to the staff.
public StaffNode AlignToStaff() { position = 0; var staff = Parent.NearestStaff(GetCenter()); if (staff == null) return null; var alignedPoint = staff.Align(GetCenter(), out position); alignedPoint.X -= Bounds.Width / 2; alignedPoint.Y -= Bounds.Height / 2; Move(alignedPoint.X, alignedPoint.Y); return staff; }
We can align notes after drag-and-drop from NodeListView by handling diagram’s NodeCreated event. We’ll use the same handler to attach notes to that staff, so that if users move a StaffNode, its attached NoteNodes will follow.
private void OnNodeCreated(object sender, NodeEventArgs e) { var note = e.Node as NoteNode; if (note != null) { var staff = note.AlignToStaff(); if (staff != null) note.AttachTo(staff, AttachToNode.TopLeft); note.HandlesStyle = HandlesStyle.MoveOnly; } }
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:
protected override void CompleteModify(PointF end, InteractionState ist) { base.CompleteModify(end, ist); var staff = AlignToStaff(); if (staff != null) AttachTo(staff, AttachToNode.TopLeft); else { Detach(); } }
Let’s run the project and compose some music 🙂
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 😉
The code above uses MindFusion’s .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.
You can download the trial version of any MindFusion.Diagramming component from this page.
Enjoy!