.net 在WPF中创建吉他和弦编辑器(来自RichTextBox?)
我在WPF中工作的应用程序的主要目的是允许编辑和打印带有吉他和弦的歌曲歌词 即使你不演奏任何乐器,你也可能见过和弦。给你一个想法,它看起来像这样:.net 在WPF中创建吉他和弦编辑器(来自RichTextBox?),.net,wpf,user-interface,richtextbox,music-notation,.net,Wpf,User Interface,Richtextbox,Music Notation,我在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 但是我不想使用这种难
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
- 换行:换行时,2行(第1行带和弦,第2行带歌词)在逻辑上是一行。当一个单词换行到下一行时,它上面的所有和弦也应该换行。同样,当和弦把它所在的单词包起来时,它也会包起来。例如:
- 和弦应该保持在正确的角色上,即使和弦彼此太近。在这种情况下,会在歌词行中自动插入一些额外的空间。例如:
- 假设我有歌词行
和和弦在Ta VA
上。我希望歌词看起来像不像。第二张图片未在A
和V
之间紧排。橙色线仅用于可视化效果(但它们标记了弦放置的x偏移)。用于生成第一个样本的代码为A
Ta VA
,用于生成第二个样本的代码为Ta VA
Inline
或运行
帮助?欢迎任何想法、黑客、TextPointer
magic、代码或相关主题的链接
编辑: 我正在探索解决这个问题的两个主要方向,但这两个方向都会导致另一个问题,所以我提出了一个新问题:
RichTextBox
转换为和弦编辑器-查看面板
s文本框
es等)构建新编辑器。这将需要大量的编码,并导致以下(未解决的)问题:
- (行开始处的空白清除等)
- 在组件边界处
- (已知的不是优雅的破解/解决方法)
编辑#2 已经向我展示了使用
RichTextBox
可以做的事情比我自己在尝试调整它以满足我的需要时所期望的要多得多。直到现在,我才有时间详细探究答案。马库斯可能是RichTextBox
魔术师我需要帮助我,但他的解决方案也存在一些未解决的问题:
LineHeight
设置为25
或其他固定值时,将导致没有和弦的行上方出现“空行”。当只有和弦而没有文本时,它们就没有空间了还有一些小问题,但我认为我可以解决它们,或者我认为它们并不重要。无论如何,我认为马库斯的回答真的很有价值——不仅是为了向我展示可能的方法,而且是为了展示使用adorner的
RichTextBox
的一般模式。我不能给你任何具体的帮助,但在架构方面,你需要从这个角度改变布局
对此
其他一切都是胡闹。单位/字形必须成为单词和弦对
编辑:我一直在玩弄一个模板化的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 void AddNewGlyph(字符串文本,整数索引)
{
var glyph=new ChordWordPair(text,String.Empty);
SheetData.Insert(索引、图示符);
FocusGlyphTextBox(glyph,false);
}
专用void FocusGlyphTextBox(ChordWordPair glyph,bool movecarettoned)
{
var cp=_chordEditor.ItemContainerGenerator.ContainerFromItem(glyph)作为ContentPresenter;
动作焦点动作=()=>
{
var grid=visualtreeheloper.GetChild(cp,0)作为网格;
var wordTB=grid.Children[1]作为文本框;
键盘焦点(wordTB);
如果(MoveCareToEnd)
{
wordTB.CaretIndex=int.MaxValue;
}
};
如果(!cp.IsLoaded)
{
cp.Loaded+=(s,e)=>focusAction.Invoke();
}
其他的
{
focusAction.Invoke();
}
}
私有无效图示符\u Word\u TextChanged(对象发送者,TextChangedEventArgs e)
{
var glyph=(发送方作为框架元素)。DataContext作为ChordWordPair;
var tb=发送方作为文本框;
字符串[]glyphs=tb.Text.Split(“”);
如果(glyphs.Length>1)
{
glyph.Word=glyphs[0];
for(int i=1;iE 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...
<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;
}
}
}
<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);
}
}