Lo scopo principale dell’applicazione su cui sto lavorando in WPF è consentire l’editing e di conseguenza la stampa di testi di canzoni con accordi di chitarra su di esso.
Probabilmente hai visto gli accordi anche se non suoni nessuno strumento. Per darti un’idea, assomiglia a questo:
E E6 I know I stand in line until you E E6 F#m BF#m B think you have the time to spend an evening with me
Ma invece di questo brutto carattere mono spaziatura voglio il font Times New Roman
con crenatura per entrambi i testi e gli accordi (accordi in carattere grassetto). E voglio che l’utente sia in grado di modificarlo.
Questo scenario non sembra supportato per RichTextBox
. Questi sono alcuni dei problemi che non so come risolvere:
TextPointer
della linea dei testi). Quando l’utente modifica i testi, voglio che l’accordo rimanga sul carattere giusto. Esempio: .
E E6 I know !!!SOME TEXT REPLACED HERE!!! in line until you
.
E E6 think you have the time to spend an F#m BF#m B evening with me
.
F#m E6 ...you have the ti me to spend...
Ta VA
e accordi su A
Voglio che il testo sia simile V
e A
Le linee arancioni servono solo a visualizzare l’effetto (ma contrassegnano x offset in cui verrà posizionato l’accordo). Il codice utilizzato per produrre il primo campione è Ta VA
e per il secondo campione Ta VA
. Qualche idea su come ottenere RichTextBox
per farlo? O c’è un modo migliore per farlo in WPF? Posso sottoclass l’aiuto Inline
o Run
? Qualsiasi idea, hack, TextPointer
magic, codice o link ad argomenti correlati sono i benvenuti.
Sto esplorando 2 direzioni principali per risolvere questo problema, ma entrambi portano a un altro problema, quindi faccio una nuova domanda:
RichTextBox
in editor di accordi – Date un’occhiata a Come posso creare una sottoclass di class Inline? . Costruisci un nuovo editor da componenti separati come il TextBox
ecc. Di Panel
come suggerito nella risposta HB . Ciò richiederebbe molto codice e comporterebbe anche problemi (irrisolti) seguenti:
La risposta di alta qualità di Markus Hütter mi ha dimostrato che molto di più può essere fatto con RichTextBox
quindi mi aspettavo che stavo cercando di modificarlo per le mie esigenze. Ho avuto il tempo di esplorare la risposta nei dettagli solo ora. Markus potrebbe essere il mago RichTextBox
Ho bisogno di aiutarmi con questo, ma ci sono alcuni problemi irrisolti con la sua soluzione pure:
LineHeight
è impostato su 25
o un altro valore fisso per l’intero documento, le linee senza accordi avranno “linee vuote” sopra di esse. Quando ci sono solo accordi e nessun testo, non ci sarà spazio per loro. Ci sono altri problemi minori ma penso di poterli risolvere o li considero non importanti. Comunque penso che la risposta di Markus sia davvero preziosa – non solo per mostrarmi la strada da percorrere ma anche come dimostrazione di un modello generale di utilizzo di RichTextBox
con l’adorner.
Non posso darti alcun aiuto concreto ma in termini di architettura devi cambiare il tuo layout da questo
A questa
Tutto il resto è un hack. La tua unità / glifo deve diventare una coppia di accordi di parole.
Edit: Sono stato ingannato con un ItemsControl basato su modelli e funziona fino ad un certo punto, quindi potrebbe essere interessante.
private readonly ObservableCollection _sheetData = new ObservableCollection (); public ObservableCollection 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; } } }
Inizialmente alcuni glifi dovrebbero essere aggiunti alla raccolta, altrimenti non ci sarà alcun campo di input (questo può essere evitato con ulteriori templating, ad esempio utilizzando un datatrigger che mostra un campo se la collezione è vuota).
Perfezionarlo richiede un sacco di lavoro aggiuntivo come lo styling dei TextBox, l'aggiunta di interruzioni di riga scritte (al momento si interrompe solo quando il pannello di wrap lo rende), supportando la selezione su più caselle di testo, ecc.
Soooo, mi sono divertito un po ‘qui. Ecco come appare:
Il testo è completamente modificabile, al momento gli accordi non lo sono (ma questa sarebbe un’estensione semplice).
questo è lo xaml:
e questo è il codice:
public partial class MainWindow { public MainWindow() { InitializeComponent(); const string input = "E E6\nI know I stand in line until you\nE E6 F#m BF#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; } } } }
usando questo 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
questo sta usando un decoratore come suggerito da David, ma so che è difficile trovare un modo per andare là fuori. Questo probabilmente perché non ce n'è. Avevo passato ore prima in Reflector a cercare di trovare quell'esatto evento che segnala che il layout del documento di stream è stato scoperto.
Non sono sicuro che la chiamata del dispatcher nel costruttore sia effettivamente necessaria, ma l'ho lasciata per essere a prova di proiettile. (Ne avevo bisogno perché nella mia configurazione il RichTextBox non era ancora stato mostrato).
Ovviamente questo richiede molto più codice, ma questo ti darà un inizio. Avrai voglia di giocare con il posizionamento e così via.
Per ottenere il posizionamento giusto se due mascherine sono troppo vicine e si stanno sovrapponendo ti suggerirei in qualche modo di tenere traccia di quale dispositivo di decorazione viene prima e vedere se quello corrente si sovrappone. quindi, ad esempio, puoi inserire iterativamente uno spazio prima di _position
-TextPointer.
Se in seguito decidi, vuoi che anche gli accordi siano modificabili, puoi semplicemente disegnare il testo in OnRender avere un intero VisualTree sotto l'adorner. ( ecco un esempio di un decoratore con un ContentControl in basso). Attenzione però che devi gestire ArrangeOveride per posizionare correttamente l'Adorner con _position
CharacterRect.