问题描述
我在 WPF 中开发的应用程序的主要目的是允许编辑并因此打印带有吉他和弦的歌词.
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
但不是这种丑陋的等宽字体,我想要Times New Roman
字体,歌词和和弦都带有字距调整(粗体和弦).我希望用户能够编辑它.
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.
RichTextBox
似乎不支持这种情况.这些是我不知道如何解决的一些问题:
This does not appear to be supported scenario for RichTextBox
. These are some of the problems that I don't know how to solve:
- 和弦的位置固定在歌词文本中的某个字符上(或者更普遍的是歌词行的
TextPointer
).当用户编辑歌词时,我希望和弦保持在正确的字符上.示例:
- 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
- 换行:2 行(第 1 行和弦,第 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
和弦.我希望歌词看起来像 而不是 .第二张图片没有在V
和A
之间进行字距调整.橙色线仅用于可视化效果(但它们标记了放置和弦的 x 偏移).用于生成第一个样本的代码是<TextBlock FontFamily="Times New Roman" FontSize="60">Ta VA</TextBlock>
和用于生成第二个样本的代码.
- Say I have lyrics line
Ta VA
and chord overA
. I want the lyrics to look like not like . Second picture is not kerned betweenV
andA
. 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>
. - 尝试将
RichTextBox
变成和弦编辑器 - 看看 如何创建 Inline 类的子类?. 从
Panel
sTextBox
es 等单独的组件构建新的编辑器,如 HB回答.这将需要大量编码并导致以下(未解决的)问题:- Trying to turn
RichTextBox
into chords editor - Have a look at How can I create subclass of class Inline?. Build new editor from separate components like
Panel
sTextBox
es etc. as suggested in H.B. answer. This would need a lot of coding and also led to following (unsolved) problems:- 组件将根据它们的布局位置改变它们的宽度/高度(行首处的空白去除等)
- 字距必须是在组件边界处手动插入.
- 如何让 RichTextBox 看起来像 TextBlock?(不优雅的黑客/解决方法是已知的)
- Components will change their Width/Height according to they layout position (white space removal at line beginning etc.)
- Kerning will have to be inserted manually at components boundaries.
- How to make RichTextBox look like TextBlock? (not elegant hack/workaround is known)
- 此应用程序将全部与精美"的印刷歌词有关.主要目标是从排版的角度来看文本看起来很完美.当和弦彼此靠得太近或什至重叠时,Markus 建议我在其位置之前迭代地添加附加空格,直到它们的距离足够.实际上要求用户可以设置2个和弦之间的最小距离.应遵守该最小距离,除非必要,否则不得超过.空间不够细粒度 - 一旦我添加了最后一个所需的空间,我可能会将间隙扩大到必要的程度 - 这将使文档看起来糟糕",我认为它不能被接受.我需要插入自定义宽度的空间.
- 可能有没有和弦的行(只有文本),甚至可能有没有文本的行(只有和弦).当
LineHeight
设置为25
或整个文档的其他固定值时,它会导致没有和弦的行在其上方有空行".当只有和弦而没有文本时,它们就没有空间了. - 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.
- There could be lines with no chords (only text) or even lines with no text (only chords). When
LineHeight
is set to25
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.
关于如何让 RichTextBox
做到这一点的任何想法?或者在 WPF 中有更好的方法吗?我将子类化 Inline
或 Run
有帮助吗?欢迎提供任何想法、技巧、TextPointer
魔法、代码或相关主题的链接.
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.
我正在探索解决这个问题的两个主要方向,但都导致了另一个问题,所以我提出了新问题:
I'm exploring 2 major directions to solve this problem but both lead to another problems so I ask new question:
编辑#2
Markus Hütter 的高质量回答向我展示了使用 RichTextBox
可以做的事情比我自己尝试调整它以满足我的需要时所期望的要多得多.我现在才有时间详细探讨答案.Markus 可能是 RichTextBox
魔术师,我需要帮助我解决这个问题,但他的解决方案也有一些未解决的问题:
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:
还有其他小问题,但我要么认为我可以解决它们,要么认为它们不重要.无论如何,我认为 Markus 的回答非常有价值 - 不仅向我展示了可能的方法,而且还展示了使用 RichTextBox
和装饰器的一般模式.
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.
推荐答案
我不能给你任何具体的帮助,但在架构方面你需要从这里改变你的布局
I cannot give you any concrete help but in terms of architecture you need to change your layout from this
为此
其他一切都是黑客.您的单位/字形必须成为词-和弦对.
Everything else is a hack. Your unit/glyph must become a word-chord-pair.
我一直在玩弄模板化的 ItemsControl,它甚至在某种程度上起作用,所以它可能很有趣.
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.
这篇关于在 WPF 中创建吉他和弦编辑器(来自 RichTextBox?)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!