19

私が 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 行 (1 行目は和音、2 行目は歌詞) は、折り返しに関しては論理的に 1 行です。単語が次の行に折り返されるとき、その上にあるすべてのコードも折り返されます。また、chord が単語をラップすると、それもラップします。例:

.

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ます。のように見え右ケリングない歌詞にしたいここに画像の説明を入力V2 番目の画像は と の間でカーニングされませんA。オレンジ色の線は、効果を視覚化するためだけにあります (ただし、コードが配置される x オフセットを示します)。最初のサンプルを生成するために使用されるコードは<TextBlock FontFamily="Times New Roman" FontSize="60">Ta VA</TextBlock>、2 番目のサンプルの と<TextBlock FontFamily="Times New Roman" FontSize="60"><Span>Ta V<Floater />A</Span></TextBlock>です。

これを行う方法についてのアイデアはRichTextBoxありますか? または、WPFでそれを行うより良い方法はありますか? サブクラス化InlineまたはRunヘルプしますか? アイデア、ハック、TextPointerマジック、コード、または関連トピックへのリンクは大歓迎です。


編集:

この問題を解決するために2つの主要な方向性を探っていますが、どちらも別の問題につながるため、新しい質問をします:

  1. RichTextBoxコードエディタに変えようとしています - How can I create subclass of class Inline? を見てください。.
  2. HB answerで提案されているように、 Panels es などの個別のコンポーネントから新しいエディターを構築します。これには多くのコーディングが必要であり、次の (未解決の) 問題も発生します。TextBox


編集#2

Markus Hütter の質の高い回答RichTextBoxは、自分のニーズに合わせて微調整しようとしていたときに期待していたよりも多くのことができることを示しています。答えを詳細に調べる時間は今しかありません。Markus はRichTextBoxマジシャンかもしれませんが、私はこれを手伝う必要がありますが、彼の解決策には未解決の問題もいくつかあります。

  1. このアプリケーションは、「美しく」印刷された歌詞に関するものです。主な目標は、タイポグラフィの観点からテキストが完璧に見えるようにすることです。和音同士が近すぎたり重なったりする場合、Markus は、距離が十分になるまで、その位置の前に繰り返しスペースを追加することを提案しています。実際には、ユーザーが 2 つのコード間の最小距離を設定できるという要件があります。その最小距離は尊重されるべきであり、必要になるまで超えてはなりません。スペースの粒度が十分ではありません.必要な最後のスペースを追加すると、おそらく必要以上にギャップを広げるでしょう. カスタム width のスペースを挿入する必要があります
  2. コードのない行 (テキストのみ) や、テキストのない行 (コードのみ) が存在する可能性があります。LineHeightドキュメント全体に対して が または他の固定値に設定されている場合25、和音のない行の上に「空の行」が表示されます。和音だけでテキストがない場合、それらのためのスペースはありません。

他にも小さな問題はありますが、解決できると思うか、重要ではないと考えています。とにかく、マーカスの答えは本当に価値があると思います-可能な方法を示すだけでなく、装飾者を使用RichTextBoxする一般的なパターンのデモンストレーションとしても。

4

2 に答える 2

16

具体的なアドバイスはできませんが、アーキテクチャに関しては、これからレイアウトを変更する必要があります

ラインは吸う

これに

グリフ ルール

他のすべてはハックです。ユニット/グリフは、単語とコードのペアになる必要があります。


編集:私はテンプレート化された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;
        }
    }
}

最初にいくつかのグリフをコレクションに追加する必要があります。そうしないと、入力フィールドがなくなります (コレクションが空の場合にフィールドを表示するデータトリガーを使用するなど、さらにテンプレートを作成することでこれを回避できます)。

これを完成させるには、TextBox のスタイル設定、改行の追加 (現在は、ラップ パネルが作成されたときにのみ改行されます)、複数のテキスト ボックスでの選択のサポートなど、多くの追加作業が必要になります。

于 2011-04-26T23:54:25.767 に答える
12

すっごく、ここで少し楽しかったです。これはどのように見えるかです:

キャプチャー

歌詞は完全に編集可能ですが、コードは現在編集できません(ただし、これは簡単な拡張です)。

これは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がまだ表示されていなかったため、これが必要でした)。

明らかに、これにはもっと多くのコーディングが必要ですが、これはあなたに出発点を与えるでしょう。ポジショニングなどをいじってみたくなるでしょう。

2人の装飾者が近すぎて重なっている場合に正しい位置を取得するには、どの装飾者が前に来るかを追跡し、現在の装飾者が重複するかどうかを確認することをお勧めします。_position次に、たとえば、 -TextPointerの前にスペースを繰り返し挿入できます。

後で決定した場合は、コードも編集可能にします。OnRenderでテキストを描画する代わりに、装飾の下にVisualTree全体を配置することができます。(これは、下にContentControlがある装飾者の例です)。ただし、CharacterRectによってAdornerを正しく配置するには、ArrangeOverideを処理する必要があることに注意してください_position

于 2011-05-01T02:07:46.707 に答える