太好了,我在这里玩得很开心。这是它的样子:
歌词是完全可编辑的,和弦目前不是(但这将是一个简单的扩展)。
这是xml:
<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;
}
}
}
}
使用这个装饰器:
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,然后才能通过_position
CharacterRect 正确定位 Adorner。