example .net wpf user-interface richtextbox music-notation

.net - example - Crear editor de acordes de guitarra en WPF(de RichTextBox?)



richtextbox c# example (2)

El principal objetivo de la aplicación en la que estoy trabajando en WPF es permitir la edición y, en consecuencia, la impresión de letras de canciones con acordes de guitarra sobre ella.

Probablemente has visto acordes incluso si no tocas ningún instrumento. Para que te hagas una idea se ve así:

E E6 I know I stand in line until you E E6 F#m B F#m B think you have the time to spend an evening with me

Pero en lugar de esta fea fuente mono-espaciada quiero tener Times New Roman fuente Times New Roman con kerning para letras y acordes (acordes en negrita). Y quiero que el usuario pueda editar esto.

Esto no parece ser un escenario compatible para RichTextBox . Estos son algunos de los problemas que no sé cómo resolver:

  • Los acordes tienen sus posiciones fijas sobre algún carácter en el texto de la letra (o, más generalmente, en el TextPointer de la línea de la letra). Cuando el usuario edita las letras, quiero que el acorde permanezca sobre el personaje correcto. Ejemplo:

.

E E6 I know !!!SOME TEXT REPLACED HERE!!! in line until you

  • Línea de ajuste: 2 líneas (1 con acordes y 2 con letras) son lógicamente una línea cuando se trata de ajustar. Cuando una palabra se ajusta a la siguiente línea, todos los acordes que están sobre ella también deben envolverse. También cuando el acorde envuelve la palabra que está sobre él también se envuelve. Ejemplo:

.

E E6 think you have the time to spend an F#m B F#m B evening with me

  • Los acordes deben mantenerse sobre el carácter correcto, incluso cuando los acordes están demasiado cerca uno del otro. En este caso, algún espacio extra se inserta automáticamente en la línea de letras. Ejemplo:

.

F#m E6 ...you have the ti me to spend...

  • Digamos que tengo la línea de letras Ta VA y acorde sobre A Quiero que la letra se vea como diferente a . La segunda imagen no está marcada entre V y A Las líneas naranjas están ahí solo para visualizar el efecto (pero marcan x desplazamientos donde se colocará el acorde). El código utilizado para producir la primera muestra es <TextBlock FontFamily="Times New Roman" FontSize="60">Ta VA</TextBlock> y para la segunda muestra <TextBlock FontFamily="Times New Roman" FontSize="60"><Span>Ta V<Floater />A</Span></TextBlock> .

¿Alguna idea sobre cómo hacer que RichTextBox haga esto? ¿O hay una mejor manera de hacerlo en WPF? ¿Subclasificaré la ayuda Inline o Run ? Cualquier idea, hacks, TextPointer magic, código o enlaces a temas relacionados son bienvenidos.

Editar:

Estoy explorando 2 direcciones principales para resolver este problema, pero ambas conducen a otro problema, así que hago una nueva pregunta:

  1. Intentando convertir RichTextBox en un editor de acordes. Eche un vistazo a ¿Cómo puedo crear una subclase de la clase Inline? .
  2. Cree un nuevo editor a partir de componentes separados como los TextBox Panel etc., como se sugiere en la respuesta de HB . Esto necesitaría mucha codificación y también daría lugar a problemas siguientes (no resueltos):

Editar # 2

La respuesta de alta calidad de Markus Hütter me ha demostrado que se puede hacer mucho más con RichTextBox que esperaba cuando intentaba modificarlo para mis necesidades. He tenido tiempo para explorar la respuesta en detalles solo ahora. Markus podría ser el mago de RichTextBox . Necesito ayudarme con esto, pero también hay algunos problemas no resueltos con su solución:

  1. Esta aplicación será todo sobre letras impresas "bellamente". El objetivo principal es que el texto se vea perfecto desde el punto de vista tipográfico. Cuando los acordes están demasiado cerca uno del otro o incluso superpuestos, Markus sugiere que yo agregue espacios de adición iterativamente antes de su posición hasta que su distancia sea suficiente. En realidad, existe el requisito de que el usuario pueda establecer una distancia mínima entre 2 acordes. Esa distancia mínima debe respetarse y no superarse hasta que sea necesario. Los espacios no son lo suficientemente granulares; una vez que agregué el último espacio necesario, probablemente acortaré la brecha de lo necesario, lo que hará que el documento se vea "mal". Necesitaría insertar espacio de ancho personalizado .
  2. Podría haber líneas sin acordes (solo texto) o incluso líneas sin texto (solo acordes). Cuando LineHeight se establece en 25 u otro valor fijo para todo el documento, las líneas sin acordes tendrán "líneas vacías" sobre ellas. Cuando solo hay acordes y no hay texto, no habrá espacio para ellos.

Hay otros problemas menores, pero creo que puedo resolverlos o los considero no importantes. De todos modos, creo que la respuesta de Markus es realmente valiosa, no solo por mostrarme el camino posible, sino también como una demostración del patrón general de uso de RichTextBox con adorner.


No puedo darte ninguna ayuda concreta, pero en términos de arquitectura necesitas cambiar tu diseño desde este

A esto

Todo lo demás es un hack. Tu unidad / glifo debe convertirse en una palabra-acorde-par.

Edición: he estado jugando con un control de elementos templado e incluso funciona en cierta medida, por lo que podría ser de interés.

<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; } } }

Inicialmente, debe agregarse un glifo a la colección, de lo contrario no habrá campo de entrada (esto puede evitarse con más plantillas, por ejemplo, utilizando un datatrigger que muestre un campo si la colección está vacía).

Perfeccionar esto requeriría un montón de trabajo adicional como diseñar los TextBoxes, agregar saltos de línea escritos (en este momento solo se rompe cuando el panel de ajuste lo hace), admitir la selección a través de múltiples cuadros de texto, etc.


Soooo, me divertí un poco aquí. Así es como esto luce:

La letra es completamente editable, los acordes no lo son actualmente (pero esto sería una extensión fácil).

este es el xaml

<Window ...> <AdornerDecorator> <!-- setting the LineHeight enables us to position the Adorner on top of the text --> <RichTextBox TextBlock.LineHeight="25" Padding="0,25,0,0" Name="RTB"/> </AdornerDecorator> </Window>

y este es el código:

public partial class MainWindow { public MainWindow() { InitializeComponent(); const string input = "E E6/nI know I stand in line until you/nE E6 F#m B F#m B/nthink you have the time to spend an evening with me "; var lines = input.Split(''/n''); var paragraph = new Paragraph{Margin = new Thickness(0),Padding = new Thickness(0)}; // Paragraph sets default margins, don''t want those RTB.Document = new FlowDocument(paragraph); // this is getting the AdornerLayer, we explicitly included in the xaml. // in it''s visual tree the RTB actually has an AdornerLayer, that would rather // be the AdornerLayer we want to get // for that you will either want to subclass RichTextBox to expose the Child of // GetTemplateChild("ContentElement") (which supposedly is the ScrollViewer // that hosts the FlowDocument as of http://msdn.microsoft.com/en-us/library/ff457769(v=vs.95).aspx // , I hope this holds true for WPF as well, I rather remember this being something // called "PART_ScrollSomething", but I''m sure you will find that out) // // another option would be to not subclass from RTB and just traverse the VisualTree // with the VisualTreeHelper to find the UIElement that you can use for GetAdornerLayer var adornerLayer = AdornerLayer.GetAdornerLayer(RTB); for (var i = 1; i < lines.Length; i += 2) { var run = new Run(lines[i]); paragraph.Inlines.Add(run); paragraph.Inlines.Add(new LineBreak()); var chordpos = lines[i - 1].Split('' ''); var pos = 0; foreach (string t in chordpos) { if (!string.IsNullOrEmpty(t)) { var position = run.ContentStart.GetPositionAtOffset(pos); adornerLayer.Add(new ChordAdorner(RTB,t,position)); } pos += t.Length + 1; } } } }

utilizando este Adorner:

public class ChordAdorner : Adorner { private readonly TextPointer _position; private static readonly PropertyInfo TextViewProperty = typeof(TextSelection).GetProperty("TextView", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly EventInfo TextViewUpdateEvent = TextViewProperty.PropertyType.GetEvent("Updated"); private readonly FormattedText _formattedText; public ChordAdorner(RichTextBox adornedElement, string chord, TextPointer position) : base(adornedElement) { _position = position; // I''m in no way associated with the font used, nor recommend it, it''s just the first example I found of FormattedText _formattedText = new FormattedText(chord, CultureInfo.GetCultureInfo("en-us"),FlowDirection.LeftToRight,new Typeface(new FontFamily("Arial").ToString()),12,Brushes.Black); // this is where the magic starts // you would otherwise not know when to actually reposition the drawn Chords // you could otherwise only subscribe to TextChanged and schedule a Dispatcher // call to update this Adorner, which either fires too often or not often enough // that''s why you''re using the RichTextBox.Selection.TextView.Updated event // (you''re then basically updating the same time that the Caret-Adorner // updates it''s position) Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => { object textView = TextViewProperty.GetValue(adornedElement.Selection, null); TextViewUpdateEvent.AddEventHandler(textView, Delegate.CreateDelegate(TextViewUpdateEvent.EventHandlerType, ((Action<object, EventArgs>)TextViewUpdated).Target, ((Action<object, EventArgs>)TextViewUpdated).Method)); InvalidateVisual(); //call here an event that triggers the update, if //you later decide you want to include a whole VisualTree //you will have to change this as well as this ----------. })); // | } // | // | public void TextViewUpdated(object sender, EventArgs e) // | { // V Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(InvalidateVisual)); } protected override void OnRender(DrawingContext drawingContext) { if(!_position.HasValidLayout) return; // with the current setup this *should* always be true. check anyway var pos = _position.GetCharacterRect(LogicalDirection.Forward).TopLeft; pos += new Vector(0, -10); //reposition so it''s on top of the line drawingContext.DrawText(_formattedText,pos); } }

Esto es usar un adorno como lo sugirió David, pero sé que es difícil encontrar una manera de salir. Eso es probablemente porque no hay ninguno. Pasé horas antes en el reflector tratando de encontrar el evento exacto que señala que el diseño del documento de flujo se ha resuelto.

No estoy seguro si esa llamada del despachador en el constructor es realmente necesaria, pero la dejé adentro para ser a prueba de balas. (Necesitaba esto porque en mi configuración aún no se había mostrado el RichTextBox).

Obviamente, esto necesita mucha más codificación, pero esto te dará un comienzo. Usted querrá jugar con el posicionamiento y tal.

Para obtener la posición correcta si dos adornos están demasiado cerca y se superponen, le sugiero que, de alguna manera, haga un seguimiento de qué adorno viene antes y vea si el actual se superpondría. luego, por ejemplo, puede insertar un espacio iterativamente antes de la _position -TextPointer.

Si más tarde decide, también desea editar los acordes, en lugar de simplemente dibujar el texto en OnRender, puede tener un VisualTree completo debajo del adornador. ( here hay un ejemplo de un adorno con un ContentControl debajo). Sin embargo, tenga en cuenta que tiene que manejar el ArrangeOveride para colocar correctamente el Adornador por el _position CharacterRect.