在 WPF 中创建吉他和弦编辑器(来自 RichTextBox?)

发布于 2024-11-03 18:56:44 字数 3723 浏览 1 评论 0 原文

我在 WPF 中开发的应用程序的主要目的是允许编辑并随后打印带有吉他和弦的歌词。

即使您不演奏任何乐器,您也可能见过和弦。为了给你一个想法,它看起来像这样:

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

但是我想要使用 Times New Roman 字体,而不是这种丑陋的等距字体,并且歌词和和弦(和弦以粗体显示)都带有字距调整。我希望用户能够编辑它。

RichTextBox 似乎不支持这种情况。这些是我不知道如何解决的一些问题:

  • 和弦的位置固定在歌词文本中的某些字符上(或者更一般地说歌词行的 TextPointer)。当用户编辑歌词时,我希望和弦保留在正确的字符上。例子:

E                                       E6
I know !!!SOME TEXT REPLACED HERE!!! in line until you
  • 换行:在换行时,2 行(第一行是和弦,第二行是歌词)在逻辑上是一行。当一个单词换行到下一行时,它上面的所有和弦也应该换行。另外,当和弦环绕单词时,它也会环绕该单词。例子:

E                  E6
think you have the time to spend an
F#m            B F#m B
evening with me
  • 即使和弦彼此太接近,和弦也应该保持在正确的字符上。在这种情况下,歌词行中会自动插入一些额外的空格。例子:

                  F#m E6
  ...you have the ti  me to spend... 
  • 假设我有歌词行 Ta VA 和和弦 A。我希望歌词看起来像 kering right 而不是 在此处输入图像描述。第二张图片未在 VA 之间调整字距。橙色线只是为了可视化效果(但它们标记了和弦放置的 x 偏移量)。用于生成第一个示例的代码为 Ta VA ,用于生成第二个示例的代码为 Ta V

关于如何让 RichTextBox 执行此操作有什么想法吗?或者在WPF中有更好的方法吗?我可以对 InlineRun 进行子类化吗?欢迎任何想法、技巧、TextPointer 魔法、代码或相关主题的链接。


编辑:

我正在探索解决这个问题的两个主要方向,但这两个方向都会导致另一个问题,所以我提出新问题:

  1. 尝试将 RichTextBox 变成和弦编辑器 - 看看 如何创建 Inline 类的子类?
  2. 按照 HB 答案。这需要大量编码,并且还会导致以下(未解决的)问题:


Edit#2

Markus Hütter 的高质量答案 向我展示了当我尝试根据自己的需要调整它时,使用 RichTextBox 可以完成更多的事情。我现在才有时间详细探讨答案。 Markus 可能是 RichTextBox 魔术师,我需要帮助我解决这个问题,但他的解决方案也存在一些未解决的问题:

  1. 这个应用程序将全部是关于“漂亮”打印的歌词。主要目标是从印刷的角度来看文本看起来很完美。当和弦彼此距离太近甚至重叠时,Markus 建议我在其位置之前迭代地添加附加空格,直到它们的距离足够。实际上要求用户可以设置两个和弦之间的最小距离。应遵守该最小距离,除非必要,否则不得超过。空格不够细粒度 - 一旦我添加了所需的最后一个空格,我可能会使间隙比必要的更宽 - 这将使文档看起来“糟糕”,我认为它不会被接受。 我需要插入自定义宽度的空间
  2. 可能存在没有和弦(只有文本)的行,甚至可能没有文本(只有和弦)的行。当 LineHeight 设置为 25 或整个文档的其他固定值时,将导致没有和弦的行上方有“空行”。当只有和弦而没有文本时,就没有空间容纳它们。

还有其他一些小问题,但我要么认为我可以解决它们,要么认为它们不重要。无论如何,我认为 Markus 的答案非常有价值 - 不仅向我展示了可能的方法,而且还演示了将 RichTextBox 与装饰器一起使用的一般模式。

Main purpose of application I'm working on in WPF is to allow editing and consequently printing of songs lyrics with guitar chords over it.

You have probably seen chords even if you don't play any instrument. To give you an idea it looks like this:

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

But instead of this ugly mono-spaced font I want to have Times New Roman font with kerning for both lyrics and chords (chords in bold font). And I want user to be able to edit this.

This does not appear to be supported scenario for RichTextBox. These are some of the problems that I don't know how to solve:

  • Chords have their positions fixed over some character in lyrics text (or more generally TextPointer of lyrics line). When user edits lyrics I want chord to stay over right character. Example:

.

E                                       E6
I know !!!SOME TEXT REPLACED HERE!!! in line until you
  • Line wrapping: 2 lines (1th with chords and 2th with lyrics) are logically one line when it comes to wrapping. When a word wraps to next line all chords that are over it should also wrap. Also when chord wraps the word that it is over it also wrap. Example:

.

E                  E6
think you have the time to spend an
F#m            B F#m B
evening with me
  • Chords should stay over right character even when chords are too near to each other. In this case some extra space is automatically inserted in lyrics line. Example:

.

                  F#m E6
  ...you have the ti  me to spend... 
  • Say I have lyrics line Ta VA and chord over A. I want the lyrics to look like kering right not like enter image description here. Second picture is not kerned between V and A. Orange lines are there only to visualize the effect (but they mark x offsets where chord would be placed). Code used to produce first sample is <TextBlock FontFamily="Times New Roman" FontSize="60">Ta VA</TextBlock> and for second sample <TextBlock FontFamily="Times New Roman" FontSize="60"><Span>Ta V<Floater />A</Span></TextBlock>.

Any ideas on how to get RichTextBox to do this ? Or is there better way to do it in WPF? Will I sub-classing Inline or Run help? Any ideas, hacks, TextPointer magic, code or links to related topics are welcome.


Edit:

I'm exploring 2 major directions to solve this problem but both lead to another problems so I ask new question:

  1. Trying to turn RichTextBox into chords editor - Have a look at How can I create subclass of class Inline?.
  2. Build new editor from separate components like Panels TextBoxes etc. as suggested in H.B. answer. This would need a lot of coding and also led to following (unsolved) problems:


Edit#2

Markus Hütter's high quality answer has shown me that a lot more can be done with RichTextBox then I expected when I was trying to tweak it for my needs myself. I've had time to explore the answer in details only now. Markus might be RichTextBox magician I need to help me with this but there are some unsolved problems with his solution as well:

  1. This application will be all about "beautifully" printed lyrics. The main goal is that the text looks perfect from the typographic point of view. When chords are too near to each other or even overlapping Markus suggests that I iteratively add addition spaces before its position until their distance is sufficient. There is actually requirement that the user can set minimum distance between 2 chords. That minimum distance should be honored and not exceeded until necessary. Spaces are not granular enough - once I add last space needed I'll probably make the gap wider then necessary - that will make the document look 'bad' I don't think it could be accepted. I'd need to insert space of custom width.
  2. There could be lines with no chords (only text) or even lines with no text (only chords). When LineHeight is set to 25 or other fixed value for whole document it will cause lines with no chords to have "empty lines" above them. When there are only chords and no text there will be no space for them.

There are other minor problems but I either think I can solve them or I consider them not important. Anyway I think Markus's answer is really valuable - not only for showing me possible way to go but also as a demonstration of general pattern of using RichTextBox with adorner.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

森罗 2024-11-10 18:56:44

我无法给您任何具体的帮助,但在架构方面,您需要将布局从

linesuck

更改为

glyphsrule

其他一切都是黑客行为。您的单位/字形必须成为单词和弦对。


编辑:我一直在玩弄模板化的ItemsControl,它甚至在某种程度上发挥了作用,所以它可能很有趣。

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

最初应该将一些字形添加到集合中,否则将没有输入字段(这可以通过进一步模板化来避免,例如,通过使用在集合为空时显示字段的数据触发器)。

完善这一点需要大量额外的工作,例如设计文本框的样式、添加书面换行符(目前仅在换行面板进行换行时才会中断)、支持跨多个文本框的选择等。

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.

梦晓ヶ微光ヅ倾城 2024-11-10 18:56:44

Soooo,我在这里玩得很开心。它看起来像这样:

capture

歌词是完全可编辑的,和弦目前还不能编辑(但这将是一个简单的扩大)。

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

    }
}

使用这个装饰器:

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 之前迭代地插入一个空格。

如果您稍后决定,您也希望和弦可编辑,您可以在装饰器下拥有整个 VisualTree,而不是仅在 OnRender 中绘制文本。 (此处是下面带有 ContentControl 的装饰器示例)。但请注意,您必须处理 ArrangeOveride,然后才能通过 _position CharacterRect 正确定位装饰器。

Soooo, I had a little fun here. This is how it looks like:

capture

The lyrics is fully editable, the chords are currently not (but this would be an easy extension).

this is the 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>

and this is the code:

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

    }
}

using this 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);
    }
}

this is using an adorner like david suggested, but I know it is hard to find a how to out there. That's probably because there is none. I had spent hours before in reflector trying to find that exact event that signals that the layout of the flowdocument has been figured out.

I'm not sure if that dispatcher call in the constructor is actually needed, but I left it in for being bulletproof. (I needed this because in my setup the RichTextBox had not been shown yet).

Obviously this needs a lot more coding, but this will give you a start. You will want to play around with positioning and such.

For getting the positioning right if two adorners are too close and are overlapping I'd suggest you somehow keep track of which adorner comes before and see if the current one would overlap. then you can for example iteratively insert a space before the _position-TextPointer.

If you later decide, you want the chords editable too, you can instead of just drawing the text in OnRender have a whole VisualTree under the adorner. (here is an example of an adorner with a ContentControl underneath). Beware though that you have to handle the ArrangeOveride then to correctly position the Adorner by the _position CharacterRect.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文