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 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>
.
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:
- 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:- 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)
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:
- 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.
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
To this
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.
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.
Soooo, I had a little fun here. This is how it looks like:
The lyrics is fully editable, the chords are currently not (but this would be an easy extension).
this is the xaml:
and this is the code:
using this Adorner:
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.