Create guitar chords editor in WPF (from RichTextBox?)

I cannot give you any concrete help but in terms of architecture you need to change your layout from this

lines suck

To this

glyphs rule

Everything else is a hack. Your unit/glyph must become a word-chord-pair.


Edit: I have been fooling around with a templated ItemsControl and it even works out to some degree, so it might be of interest.

<ItemsControl Grid.IsSharedSizeScope="True" ItemsSource="{Binding SheetData}"
              Name="_chordEditor">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition SharedSizeGroup="A" Height="Auto"/>
                    <RowDefinition SharedSizeGroup="B" Height="Auto"/>
                </Grid.RowDefinitions>
                <Grid.Children>
                    <TextBox Name="chordTB" Grid.Row="0" Text="{Binding Chord}"/>
                    <TextBox Name="wordTB"  Grid.Row="1" Text="{Binding Word}"
                             PreviewKeyDown="Glyph_Word_KeyDown" TextChanged="Glyph_Word_TextChanged"/>
                </Grid.Children>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
private readonly ObservableCollection<ChordWordPair> _sheetData = new ObservableCollection<ChordWordPair>();
public ObservableCollection<ChordWordPair> SheetData
{
    get { return _sheetData; }
}
public class ChordWordPair: INotifyPropertyChanged
{
    private string _chord = String.Empty;
    public string Chord
    {
        get { return _chord; }
        set
        {
            if (_chord != value)
            {
                _chord = value;
                // This uses some reflection extension method,
                // a normal event raising method would do just fine.
                PropertyChanged.Notify(() => this.Chord);
            }
        }
    }

    private string _word = String.Empty;
    public string Word
    {
        get { return _word; }
        set
        {
            if (_word != value)
            {
                _word = value;
                PropertyChanged.Notify(() => this.Word);
            }
        }
    }

    public ChordWordPair() { }
    public ChordWordPair(string word, string chord)
    {
        Word = word;
        Chord = chord;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}
private void AddNewGlyph(string text, int index)
{
    var glyph = new ChordWordPair(text, String.Empty);
    SheetData.Insert(index, glyph);
    FocusGlyphTextBox(glyph, false);
}

private void FocusGlyphTextBox(ChordWordPair glyph, bool moveCaretToEnd)
{
    var cp = _chordEditor.ItemContainerGenerator.ContainerFromItem(glyph) as ContentPresenter;
    Action focusAction = () =>
    {
        var grid = VisualTreeHelper.GetChild(cp, 0) as Grid;
        var wordTB = grid.Children[1] as TextBox;
        Keyboard.Focus(wordTB);
        if (moveCaretToEnd)
        {
            wordTB.CaretIndex = int.MaxValue;
        }
    };
    if (!cp.IsLoaded)
    {
        cp.Loaded += (s, e) => focusAction.Invoke();
    }
    else
    {
        focusAction.Invoke();
    }
}

private void Glyph_Word_TextChanged(object sender, TextChangedEventArgs e)
{
    var glyph = (sender as FrameworkElement).DataContext as ChordWordPair;
    var tb = sender as TextBox;

    string[] glyphs = tb.Text.Split(' ');
    if (glyphs.Length > 1)
    {
        glyph.Word = glyphs[0];
        for (int i = 1; i < glyphs.Length; i++)
        {
            AddNewGlyph(glyphs[i], SheetData.IndexOf(glyph) + i);
        }
    }
}

private void Glyph_Word_KeyDown(object sender, KeyEventArgs e)
{
    var tb = sender as TextBox;
    var glyph = (sender as FrameworkElement).DataContext as ChordWordPair;

    if (e.Key == Key.Left && tb.CaretIndex == 0 || e.Key == Key.Back && tb.Text == String.Empty)
    {
        int i = SheetData.IndexOf(glyph);
        if (i > 0)
        {
            var leftGlyph = SheetData[i - 1];
            FocusGlyphTextBox(leftGlyph, true);
            e.Handled = true;
            if (e.Key == Key.Back) SheetData.Remove(glyph);
        }
    }
    if (e.Key == Key.Right && tb.CaretIndex == tb.Text.Length)
    {
        int i = SheetData.IndexOf(glyph);
        if (i < SheetData.Count - 1)
        {
            var rightGlyph = SheetData[i + 1];
            FocusGlyphTextBox(rightGlyph, false);
            e.Handled = true;
        }
    }
}

Initially some glyph should be added to the collection, otherwise there will be no input field (this can be avoided with further templating, e.g. by using a datatrigger that shows a field if the collection is empty).

Perfecting this would require a lot of additional work like styling the TextBoxes, adding written line breaks (right now it only breaks when the wrap panel makes it), supporting selection accross multiple textboxes, etc.

Leave a Comment