This project has moved. For the latest updates, please go here.

PianoRoll

Dec 4, 2016 at 6:54 AM
I have customized Mark's Pianoroll2 so that you can display, edit and zoom notes based on MidiEvents.
I would like to post it, is this the proper place?![Image]

When implement it looks like this:
(https://i.imgsafe.org/3ca723d9d1.jpg)

Incidentally, I associate the PianoRoll not with a MidiEventCollection, as the original piano roll does, but rather with a single list of MIdiEvents (one track). This is because for most purposes you want to view your tracks separately (which is presumably the reason for having midi type 1 files).
(You wouldn't want all your track note data mixed together on one piano roll)
So, you create an instance of the piano roll for each track that you get when you open Naudio.Midi.MidiFile("")
Dec 14, 2016 at 7:14 AM
There doesn't seem to be much activity on this discussion board, but I'll go ahead and post my code for the Piano Roll here.
I tried to remove elements specific to other parts of my project so that the Piano Roll can be used as is, so I hope I adjusted it correctly... (Also, the C# is translated from VB. One of these days I'll try to get used to curly brackets but that time as not yet arrived for me :)

First, the XAML for the GUI:
<UserControl x:Class="PianoRoll"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="133" d:DesignWidth="413" x:Name="PRoll" >
        
    <Grid>
            
                <Border BorderThickness="4" BorderBrush="#FF5885AA" CornerRadius="4" Background="#FFF7F0F0">
            <DockPanel>
                <DockPanel x:Name="TopDB" DockPanel.Dock="Top" Height="20">
                    <Line HorizontalAlignment="Stretch" DockPanel.Dock="Bottom" Stroke="Black" StrokeThickness="2" X2="{Binding ElementName=TopDB, Path=ActualWidth}"></Line>
                    <StackPanel Width ="70">
                        <ComboBox x:Name="SnapToCB" Margin="0, 0, 0, 2" SelectedValuePath="Content">
                            <ComboBoxItem Content="Snap" IsSelected="True" Tag="No snap"/>
                            <ComboBoxItem Content="1/2 note" Tag="Snap to 1/2"/>
                            <ComboBoxItem Content="1/4 note" Tag="Snap to 1/4"/>
                            <ComboBoxItem Content="1/8 note" Tag="Snap to 1/8"/>
                            <ComboBoxItem Content="1/16 note" Tag="Snap to 1/16"/>
                        </ComboBox>
                    </StackPanel>
                    <StackPanel DockPanel.Dock="Right" Background="{DynamicResource {x:Static SystemColors.ControlColor}}" Width="{x:Static SystemParameters.VerticalScrollBarWidth}"></StackPanel>
                    <ScrollViewer x:Name="ScaleScrollViewer" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
                        <DockPanel>
                            <Canvas x:Name="ScaleCanvas" Width="{Binding ElementName=NoteCanvas, Path=ActualWidth}" Background="#FFFFE6C9"></Canvas>
                        <Canvas x:Name="RightFillScaleCanvas"></Canvas>
                        </DockPanel>
                    </ScrollViewer>
                  </DockPanel>
                
                <DockPanel x:Name="ScrollBarDB" DockPanel.Dock="Bottom" Height="15">
                    <Label x:Name="RepLabel" Margin="0, -3, 0, 0" FontSize="7" DockPanel.Dock="Left" Width="{Binding ElementName=PianoStackPanel, Path=ActualWidth}" Background="#FFC7D6CD"></Label>
                    <ScrollBar x:Name="RLScrollBar" Orientation="Horizontal" Background="#FFD4CCE6"/>
                </DockPanel>
                <ScrollViewer x:Name="UDScrollViewer" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
                    <DockPanel x:Name="MainDockPanel" HorizontalAlignment="Stretch">
                        <DockPanel x:Name="PianoDockPanel"  DockPanel.Dock="Left" Width="70" LastChildFill="False">
                            <DockPanel.Background>
                                <ImageBrush ImageSource="pack://siteoforigin:,,,/CustomControls/Resources/PianoLong.bmp"/>
                            </DockPanel.Background>
                            <Line DockPanel.Dock="Right"  Y1="0" Y2="{Binding ElementName=PianoStackPanel, Path=ActualHeight}" X1="0" X2="0" Stroke="Black" StrokeThickness="1">
                                <Line.Fill>
                                    <ImageBrush/>
                                </Line.Fill>
                            </Line>
                            <Canvas x:Name="NoteNameCanvas"></Canvas>
                        </DockPanel>
                        <ScrollViewer x:Name="RLScrollViewer" HorizontalScrollBarVisibility="Hidden"  Height="{Binding ElementName=GridCanvas, Path=ActualHeight}" VerticalScrollBarVisibility="Auto" Background="Cornsilk">
                            
                                <Canvas x:Name="HolderCanvas" VerticalAlignment="Top" HorizontalAlignment="Left"  >

                                <AdornerDecorator Height="{Binding ElementName=NoteCanvas, Path=ActualHeight}" Width="{Binding ElementName=NoteCanvas, Path=ActualWidth}" HorizontalAlignment="Left" VerticalAlignment="Top"  x:Name="MeasBufAdornerLayer" Visibility="Visible" >
                                    <Grid>
                                        <Grid.Background>
                                            <LinearGradientBrush  EndPoint="1,0.5" StartPoint="0,0.5" >
                                                <GradientStop Color="#00000000" Offset="0"/>
                                                <GradientStop x:Name="MeasBuf1" Color="#00000000" Offset="1"/>
                                                <GradientStop x:Name="MeasBuf2" Color="#917CEE6A" Offset="1"/>
                                                <GradientStop Color="#917CEE6A" Offset="1"/>
                                            </LinearGradientBrush>
                                        </Grid.Background>
                                    </Grid>
                                </AdornerDecorator>

                                <Canvas x:Name="GridCanvas" Background="Transparent" Top="0" Left="0" Height="{Binding ActualHeight, ElementName=NoteCanvas}" >
                                    
                                </Canvas>
                                <Canvas x:Name="NoteCanvas" Left="0" Top="0" Focusable="True" Height="{Binding ElementName=GridCanvas, Path=ActualHeight}" >
                                    <Canvas.CacheMode>
                                        <BitmapCache EnableClearType="False"  RenderAtScale="1" SnapsToDevicePixels="True" />
                                    </Canvas.CacheMode>
                                </Canvas>
                                <Canvas x:Name="RightFillNoteCanvas"></Canvas>

                            </Canvas>
                        </ScrollViewer>
                          
                </DockPanel>
            </ScrollViewer>
            </DockPanel>
        </Border>
    </Grid>
</UserControl>
This Piano Roll is associated with one track (list of MidiEvents) from a MidiEventCollection. I did this because it seems most logical (having all note from all tracks on the same Piano Roll is huge visual mess). So, you would create a new instance of PianoRoll for each track when you open a MidiFile. (However, you have to set the DetaTicks per quarter note for each instance of PianoRoll.)
Also, some extra things I've implemented for this Piano Roll is the ability to move notes with the mouse,
a snap-to feature (1/2, 1/4, 1/8, 1/16 note or measure), from a selected from a ComboBox. Or no snap to), zooming and a numbered grid.
There are three layers of Canvases, the "Holder Canvas", the size of which is used to determine whether or not the scrollbars should be visible.
Also, a "GridCanvas" and "NoteCanvas" on top of this. This setup was necessary for zooming, which is easy to do in WPF but not without caveats.
The problem with just applying a ScaleTransform to the control is that things appear ugly as you increase the scale. I think WPF just takes a visual snapshot of the control and renders a larger image of it, which means lines get thicker, etc.. It just doesn't look nice. So you have to reset the StrokeThickness of lines within your layers to the inverse (1/ScaleTransform). This is all done in the code behind which is posted below.
Dec 14, 2016 at 7:23 AM
Below I'll also post the keyboard image I used as the background image for PianoDockPanel.
You could make your own image, or use the one I use. It's one long bitmap with all 128 keys shown.

Also in the above code, it might be wondered why I had to implement an extra horizontal scrollbar and disabled the one in the scrollviewer. It's because of the asymmetrical way the scrolling has to be done.
That is, horizontal scrolling of the note grid has to also scroll the top grid (which shows the beat numbers), but not the PianoDockPanel. Conversely, vertical scrolling of the note grid has to vertical scroll the PianoDockPanel, but not the top grid. So, I chose to put one of the scroll bars outside of the scrollviewer to make it simpler.
Dec 14, 2016 at 7:25 AM
Edited Dec 14, 2016 at 7:32 AM
Here is the code behind (Part I):
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using NAudio.Midi;

public class PianoRoll : UserControl

{
    private double xScale = 1.0 / 10;
    private double yScale = 10;
    private LinearGradientBrush NoteBrush = new LinearGradientBrush();

    private LinearGradientBrush SelectedNoteBrush = new LinearGradientBrush();

    public PianoRoll()
    {
        // This call is required by the designer.
        InitializeComponent();

        if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) {
            return;
        }

        DrawHorizontalLines();

        Canvas.SetZIndex(GridCanvas, 2);
        //Bring grid canvas above holder canvas
        Canvas.SetZIndex(NoteCanvas, 3);
        //Bring note canvas to top

        //Define brushes (could be moved to Xaml designer)
        NoteBrush.StartPoint = new Point(0, 0);
        NoteBrush.EndPoint = new Point(0, 1);
        NoteBrush.GradientStops.Add(new GradientStop(Colors.White, 0));
        NoteBrush.GradientStops.Add(new GradientStop(Colors.CadetBlue, 1));
        SelectedNoteBrush.StartPoint = new Point(0, 0);
        SelectedNoteBrush.EndPoint = new Point(0, 1);
        SelectedNoteBrush.GradientStops.Add(new GradientStop(Colors.White, 0));
        SelectedNoteBrush.GradientStops.Add(new GradientStop(Colors.Red, 1));


    }

    private void DrawHorizontalLines()
    {
        int GridHeight = yScale * 128;

        for (int noteno = 0; noteno <= 127; noteno++) {
            switch (true) {

                case noteno % 12 == 1:
                case noteno % 12 == 3:
                case noteno % 12 == 6:
                case noteno % 12 == 8:
                case noteno % 12 == 10:
                    //C#, Eb, F#, Ab, Bb
                    //Black key lines
                    Rectangle blackkeyrect = new Rectangle();
                    blackkeyrect.Fill = new SolidColorBrush(Color.FromArgb(100, 180, 180, 180));
                    blackkeyrect.SetValue(Canvas.TopProperty, (GridHeight - yScale) - (noteno * yScale));
                    blackkeyrect.Stroke = Brushes.Black;
                    blackkeyrect.Margin = new Thickness(-1, 0, 0, 0);
                    BindingOperations.SetBinding(blackkeyrect, Rectangle.WidthProperty, new Binding {
                        Source = HolderCanvas,
                        Path = new PropertyPath("ActualWidth")
                    });
                    blackkeyrect.Height = yScale;
                    HolderCanvas.Children.Add(blackkeyrect);

                    break;
                default:
                    //White key lines
                    Line newline = new Line();
                    newline.Stroke = Brushes.Black;
                    newline.StrokeThickness = 1;
                    newline.X1 = 0;
                    BindingOperations.SetBinding(newline, Line.X2Property, new Binding {
                        Source = HolderCanvas,
                        Path = new PropertyPath("ActualWidth")
                    });
                    newline.Y1 = 0;
                    newline.Y2 = 0;
                    newline.SetValue(Canvas.TopProperty, (GridHeight - yScale) - (noteno * yScale));
                    HolderCanvas.Children.Add(newline);

                    break;
            }
            GridCanvas.Height = GridHeight;

        }


    }
    private SolidColorBrush measureSeparatorBrush = new SolidColorBrush(Colors.Black);
    private SolidColorBrush beatSeparatorBrush = new SolidColorBrush(Colors.LightGray);
    private int lastPosition;

    private int GridDisplayWidth;

    private void DrawVerticalGrid()
    {
        GridCanvas.Children.Clear();
        int beat = 0;
        long n = 0;
        while (n <= GridDisplayWidth) {
            Line line = new Line();
            Line line2 = new Line();
            TextBlock nTB = new TextBlock();
            line.X1 = n * xScale;
            line.X2 = line.X1;
            line.Y1 = 0;
            line.Y2 = 128 * yScale;
            line2.X1 = n * xScale;
            line2.X2 = line2.X1;
            line2.Y1 = 0;
            line2.Y2 = ScaleCanvas.ActualHeight;
            if (beat % 4 == 0) {
                line.Stroke = measureSeparatorBrush;
                line2.Stroke = measureSeparatorBrush;
                nTB.Margin = new Thickness(line2.X1, 0, 0, 0);
                nTB.Text = (Conversion.Int(Conversion.Int(n / DeltaTicks) / 4) + 1).ToString;
            } else {
                line.Stroke = beatSeparatorBrush;
                line2.Stroke = beatSeparatorBrush;
            }
            GridCanvas.Children.Add(line);
            ScaleCanvas.Children.Add(line2);
            ScaleCanvas.Children.Add(nTB);
            beat += 1;
            n += DeltaTicks;
        }


    }


    private void DrawNotes()
    {
        NoteCanvas.Children.Clear();

        foreach (MidiEvent midiEvent in MidiEvents) {
            if (midiEvent.CommandCode == MidiCommandCode.NoteOn) {
                NoteOnEvent NoteOn = (NoteOnEvent)midiEvent;
                if (NoteOn.OffEvent != null) {
                    Path rectpath = GetNoteRectPath(NoteOn.NoteNumber, NoteOn.AbsoluteTime, NoteOn.NoteLength, ref NoteOn);
                    NoteHighPt = Math.Max(NoteHighPt, NoteOn.NoteNumber);
                    NoteLowPt = Math.Min(NoteLowPt, NoteOn.NoteNumber);
                    rectpath.MouseDown += MouseDownInNote;
                    rectpath.MouseUp += MouseUpInNote;
                    rectpath.MouseEnter += MouseEnterNote;
                    rectpath.MouseLeave += MouseLeaveNote;
                    rectpath.MouseMove += MouseMoveNote;
                    NoteCanvas.Children.Add(rectpath);
                }
            }
        }


    }

    private Path GetNoteRectPath(int noteNumber, long startTime, int duration, ref NoteOnEvent CustNoteOnEv)
    {

        Path rpath = new Path();
        RectangleGeometry rectgeom = new RectangleGeometry();

        rectgeom.Rect = new Rect(Convert.ToDouble(CustNoteOnEv.AbsoluteTime) * xScale, Convert.ToDouble(127 - CustNoteOnEv.NoteNumber) * yScale + 1, Convert.ToDouble(CustNoteOnEv.NoteLength) * xScale, (yScale - 1));

        rectgeom.RadiusX = 1;
        rectgeom.RadiusY = 1;
        rpath.Stroke = RectBorderBrush;
        rpath.StrokeThickness = 1;
        rpath.Margin = new Thickness(0, 0, 0, 0);
        rpath.Fill = NoteBrush;
        rpath.Data = rectgeom;

        return rpath;

    }

    SolidColorBrush RectBorderBrush = new SolidColorBrush(Colors.DarkBlue);
    public int NoteHighPt = 0;
    public int NoteLowPt = 127;
    int InitialWidth;
    public DependencyProperty MidiEventsDP { get; set; }
    public DependencyProperty DeltaTicksDP { get; set; }

    public void UpdatePianoRoll()
    {
        //Get Last position (for grid width)
        lastPosition = 0;
        foreach (MidiEvent midiEvent in MidiEvents) {
            if (midiEvent.CommandCode == MidiCommandCode.NoteOn) {
                NoteOnEvent noteOn = (NoteOnEvent)midiEvent;
                if (noteOn.OffEvent != null) {
                    lastPosition = Math.Max(lastPosition, noteOn.AbsoluteTime + noteOn.NoteLength);
                }
            }
        }

        // a quarter note is 20 units wide
        xScale = (20.0 / DeltaTicks);

        //Add an extra measure at right as visual buffer
        GridDisplayWidth = (lastPosition + (DeltaTicks * 4) - (lastPosition % (DeltaTicks * 4))) + (4 * DeltaTicks);
        InitialWidth = GridDisplayWidth * xScale;

        NoteCanvas.Width = InitialWidth;

        DrawVerticalGrid();

        DrawNotes();

        HolderCanvas.Width = Math.Max(InitialWidth, RLScrollViewer.ActualWidth);
        GridCanvas.Width = HolderCanvas.ActualWidth;
        ScaleCanvas.Width = HolderCanvas.ActualWidth;
        MeasBufAdornerLayer.Width = HolderCanvas.ActualWidth;

        //Linear gradient brush settings for non-editable area
        MeasBuf1.Offset = ((NoteCanvas.ActualWidth - (4 * 20)) / HolderCanvas.ActualWidth);
        MeasBuf2.Offset = MeasBuf1.Offset;

        ScaleFactorX = RLScrollViewer.ActualWidth / InitialWidth;
        ScalePianoRoll();

        UDScrollViewer.ScrollToVerticalOffset(((NoteLowPt + NoteHighPt) / 2) * yScale - UDScrollViewer.ActualHeight / 2);
        //Scroll to midrange area
        RLScrollViewer.ScrollToHorizontalOffset(0);

    }
    public int DeltaTicks {
        get { return GetValue(DeltaTicksDP); }
        set { SetValue(DeltaTicksDP, value); }
    }
    public List<MidiEvent> MidiEvents {
        get { return GetValue(MidiEventsDP); }
        set {
            SetValue(MidiEventsDP, value);
            UpdatePianoRoll();
        }
    }


    private void RLScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        int scrollval = e.NewValue * ((InitialWidth * ScaleFactorX) - RLScrollViewer.ActualWidth);
        RLScrollViewer.ScrollToHorizontalOffset(scrollval);
        ScaleScrollViewer.ScrollToHorizontalOffset(scrollval);

    }
    private void NoteCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdateHorizontalScrollBar();
    }
    private void UpdateHorizontalScrollBar()
    {
        RLScrollViewer.UpdateLayout();
        if (HolderCanvas.ActualWidth == 0)
            return;

        decimal p = (RLScrollViewer.ActualWidth) / (InitialWidth * ScaleFactorX);
        if (p >= 1) {
            ScrollBarDB.Visibility = Windows.Visibility.Collapsed;
        } else {
            ScrollBarDB.Visibility = Windows.Visibility.Visible;
            RLScrollBar.ViewportSize = (RLScrollBar.Maximum - RLScrollBar.Minimum) * p / (1 - p);
        }
        RLScrollBar.Value += 0.01;
        RLScrollBar.Value -= 0.01;
        //trigger

    }
    private double ScaleFactorX = 1;

    private void ScalePianoRoll()
    {
        HolderCanvas.LayoutTransform = new ScaleTransform(ScaleFactorX, 1);
        //includes GridCanvas & NoteCanvas
        ScaleCanvas.LayoutTransform = new ScaleTransform(ScaleFactorX, 1);
        NoteCanvas.LayoutTransform = new ScaleTransform(1 / ScaleFactorX, 1);
        //Don't scale up the note canvas - only geometries in it

        //Adjust grid line thicknesses
        foreach (UIElement uielm in GridCanvas.Children) {
            if (uielm.GetType() == typeof(Line)) {
                ((Line)uielm).StrokeThickness = 1 / ScaleFactorX;
            } else {
                uielm.RenderTransform = new ScaleTransform(1 / ScaleFactorX, 1);
            }
        }
        //Adjust note sizes
        foreach (UIElement uielm in NoteCanvas.Children) {
            if (uielm.GetType() == typeof(Path)) {
                Path rpath = (Path)uielm;
                rpath.Data.Transform = new ScaleTransform(ScaleFactorX, 1);
            }
        }
        //Adjust scale line thicknesses
        foreach (UIElement uielm in ScaleCanvas.Children) {
            if (uielm.GetType() == typeof(Line)) {
                ((Line)uielm).StrokeThickness = 1 / ScaleFactorX;
            } else {
                uielm.RenderTransform = new ScaleTransform(1 / ScaleFactorX, 1);
            }
        }

        MeasBuf1.Offset = ((NoteCanvas.ActualWidth - (4 * 20)) / HolderCanvas.ActualWidth);
        MeasBuf2.Offset = MeasBuf1.Offset;

        //Add extra measures to fill space at right if smaller than the control
        if (GridDisplayWidth * xScale < RLScrollViewer.ActualWidth / ScaleFactorX) {
            GridCanvas.Width = (RLScrollViewer.ActualWidth / ScaleFactorX);
            GridDisplayWidth = (RLScrollViewer.ActualWidth / ScaleFactorX) / xScale;
            DrawVerticalGrid();
        }

        UpdateHorizontalScrollBar();

    }


Dec 14, 2016 at 7:26 AM
Code behind part 2:
private void HolderCanvas_MouseWheel(object sender, MouseWheelEventArgs e)
    {
        int NoMeasures = (GridDisplayWidth / DeltaTicks) / 4;
        ScaleFactorX = Math.Min(Math.Max(ScaleFactorX + (e.Delta / 2000), 1), NoMeasures / 2);

        ScalePianoRoll();

    }
    Path SelectedNotePath;
    NoteOnEvent SelectedNote;
    double CursorY;
    double CursorX;
    bool MouseDownOnNote;
    RectangleGeometry SelRectGeom;
    double SelRectTop;
    double SelRectLeft;
    public event NotesChangedEventHandler NotesChanged;
    public delegate void NotesChangedEventHandler();
    private void MouseUpInNote(object sender, MouseButtonEventArgs e)
    {
        MouseDownOnNote = false;
        SelectedNotePath.ReleaseMouseCapture();
        if (NotesChanged != null) {
            NotesChanged();
        }
    }
    private bool GetNoteFromGeom(MidiEvent midev)
    {

        bool IsNote = false;
        if (midev.GetType == typeof(NoteOnEvent)) {
            NoteOnEvent midnoteonev = (NoteOnEvent)midev;
            IsNote = (SelRectGeom.Rect.Left == (midnoteonev.AbsoluteTime * xScale)) & (SelRectGeom.Rect.Top == ((127 - midnoteonev.NoteNumber) * yScale + 1));
            //IsNote = IsNote And SelRectGeom.Rect.Width = midnoteonev.NoteLength * xScale     'not necessary - enough to just get the x and y locations
        }

        return IsNote;

    }

    private void MouseDownInNote(object sender, MouseButtonEventArgs e)
    {
        MouseDownOnNote = true;
        CursorY = e.GetPosition(sender).Y;
        CursorX = e.GetPosition(sender).X;

        SelectedNotePath = (Path)sender;
        SelRectGeom = SelectedNotePath.Data;
        SelRectTop = SelRectGeom.Rect.Y;
        SelRectLeft = SelRectGeom.Rect.X;
        SelectedNote = MidiEvents.Find(GetNoteFromGeom);

        SelectedNotePath.CaptureMouse();

        Canvas.SetZIndex(SelectedNotePath, NoteCanvas.Children.Count);
        //show this note above all other notes

    }

    private void MouseMoveNote(object sender, MouseEventArgs e)
    {
        if (MouseDownOnNote) {
            //Snap mouse y position to nearest note and limit to between 0-127
            SelectedNote.NoteNumber = Math.Min(Math.Max(127 - Conversion.Int((SelRectTop + e.GetPosition(sender).Y - CursorY) / yScale), 0), 127);

            int NewPos = SelRectLeft / xScale + (e.GetPosition(sender).X - CursorX) / (xScale * ScaleFactorX);
            //Get the nearest snap position and limit to within max/min range
            int SnapPosition = Math.Min(Math.Max(0, (NewPos - Conversion.Int(NewPos % NoteSnapTo))), lastPosition - Conversion.Int(lastPosition % NoteSnapTo));

            int prevselnotetime = SelectedNote.AbsoluteTime;
            SelectedNote.AbsoluteTime = SnapPosition;
            SelectedNote.OffEvent.AbsoluteTime += (SelectedNote.AbsoluteTime - prevselnotetime);

            SelRectGeom.Rect = new Rect(Convert.ToDouble(SelectedNote.AbsoluteTime) * xScale, Convert.ToDouble(127 - SelectedNote.NoteNumber) * yScale + 1, Convert.ToDouble(SelectedNote.NoteLength) * xScale, (yScale - 1));

        }

        //Scroll with mouse drag
        if (e.GetPosition(UDScrollViewer).Y < 0) {
            UDScrollViewer.ScrollToVerticalOffset(UDScrollViewer.VerticalOffset - 0.1);
        }
        if (e.GetPosition(UDScrollViewer).Y > UDScrollViewer.ActualHeight) {
            UDScrollViewer.ScrollToVerticalOffset(UDScrollViewer.VerticalOffset + 0.1);
        }
        if (e.GetPosition(RLScrollViewer).X < 0) {
            RLScrollBar.Value -= 0.001;
        }
        if (e.GetPosition(RLScrollViewer).X > RLScrollViewer.ActualWidth) {
            RLScrollBar.Value += 0.001;
        }

    }
    private void MouseEnterNote(object sender, MouseEventArgs e)
    {
        dynamic thisNotePath = (Path)sender;
        thisNotePath.Fill = SelectedNoteBrush;
    }

    private void MouseLeaveNote(object sender, MouseEventArgs e)
    {
        dynamic thisNotePath = (Path)sender;
        thisNotePath.Fill = NoteBrush;
    }
    int NoteSnapTo = 1;

    private void SnapToCB_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        switch (SnapToCB.SelectedValue.ToString) {
            case "Snap":
                NoteSnapTo = 1;
                break;
            case "1/2 note":
                NoteSnapTo = DeltaTicks * 2;
                break;
            case "1/4 note":
                NoteSnapTo = DeltaTicks;
                break;
            case "1/8 note":
                NoteSnapTo = DeltaTicks / 2;
                break;
            case "1/16 note":
                NoteSnapTo = DeltaTicks / 4;
                break;
        }

    }


}
Dec 14, 2016 at 7:39 AM
Edited Dec 14, 2016 at 7:41 AM
Here is the piano bitmap I use:

Image
Dec 14, 2016 at 7:50 AM
Incidentally, the MidiEventsDP and DeltaTicksDP are set as dependency properties to allow binding to other controls depending on how the PianoRoll will be implemented. If it's not to have binding with anything, the dependency properties could be changed to simple Properties.