Тааак, я немного повеселился здесь. Вот как это выглядит:
![capture](https://i.stack.imgur.com/4CNwG.png)
Текст песни полностью редактируемый, аккордов в настоящее время нет (но это было бы легким расширением).
это 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>
и это код:
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;
}
}
}
}
с помощью этого 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);
}
}
это использует рекламного агента, как предложил Дэвид, но я знаю, что трудно найти, как там. Это, вероятно, потому что нет ни одного. Я провел несколько часов в отражателе, пытаясь найти то точное событие, которое сигнализирует о том, что расположение документа было определено.
Я не уверен, нужен ли этот диспетчерский вызов в конструкторе, но я оставил его для пуленепробиваемости. (Мне это нужно, потому что в моей настройке RichTextBox еще не был показан).
Очевидно, что для этого нужно гораздо больше кодирования, но это даст вам начало. Вам захочется поиграть с позиционированием и тому подобным.
Для правильного позиционирования, если два украшения расположены слишком близко и накладываются друг на друга, я бы посоветовал вам каким-то образом отследить, какой из них вышел раньше, и посмотреть, не перекрывается ли текущий. затем вы можете, например, итеративно вставить пробел перед _position
-TextPointer.
Если позже вы решите, что вы также хотите редактировать аккорды, вы можете вместо того, чтобы просто рисовать текст в OnRender, иметь целый VisualTree под окном. ( здесь - пример рекламодателя с ContentControl внизу). Однако остерегайтесь того, что вам нужно обработать ArrangeOveride, чтобы правильно расположить Adorner с помощью _position
CharacterRect.