deadlyvices
deadlyvices

Reputation: 883

How to display text with subscripts or superscripts in the Title Bar?

I would like to be able to show subscripted text in the Title Bar of a Windows Form or WPF Window. The reason for this is simple. Our project team has written a molecule editor:

enter image description here

Instead of just displaying its name, 'ACME', we would like to show something like:

ACME - Editing C6H12Cl

where the text is subscripted (and possibly superscripted) and whether the control is shown in a Windows Forms or WPF host.

Upvotes: 4

Views: 2603

Answers (2)

Jimi
Jimi

Reputation: 32278

The original question was asking how to insert a RichTextBox in a Form's Caption, to show a chemical formula.
It could be done, of course: you could take a look at DwmExtendFrameIntoClientArea and the Docs here: Custom Window Frame Using DWM and a number of SO questions.

But it's kind of overshooting, here. There's a simpler alternative: make use of the existing Unicode SubScript symbols (it's an Unicode category) to reproduce the formulas.

These are the base SubScript and SuperScript Unicode CodePoints of the Hindu-Arabic numerals:

char[] subScriptNumbers = {
    '\u2080', '\u2081', '\u2082', '\u2083', '\u2084',
    '\u2085', '\u2086', '\u2087', '\u2088', '\u2089'
};
char[] superScriptNumbers = {
    '\u2070', '\u00B9', '\u00B2', '\u00B3', '\u2074',
    '\u2075', '\u2076', '\u2077', '\u2078', '\u2079'
};

As hinted in comments, the simple formula: C6H12Cl can be transformed to C₆H₁₂Cl, mapping the numbers to the corresponding Unicode values in the SubScript range. For example:

this.Text = string.Concat("C6H12Cl".Select(c => char.IsDigit(c) ? subScriptNumbers[c-48] : c));

Or, since the SubScript CodePoints are sequential (the SuperScript ones are not):

const int subScriptBase = 0x2080;
string chem = "C6H12Cl";
// Or this.Title, in WPF
this.Text = chem.Aggregate(new StringBuilder(), (sb, c) => 
    sb.Append(char.IsDigit(c) ? (char)(subScriptBase + c - 48) : c)).ToString();

Since it appears that there's someone interested, I propose a slightly more complex parser (which uses the same logic), to produce different kinds of formulas:

The class shown here can convert sequences of SubScript/SuperScript numerals or letters, using a simple notation (similar to the notation of the Markup used by Wikipedia):

SuperScript: [+:symbols]          A[+:12]    => A¹²  
SubScript:   [-:symbols]          A[-:12]    => A₁₂
Fraction:    [f:symbols/symbols]  A·[f:x/12] => A·ˣ⁄₁₂

For example:

string formula = "N[+:(x+2)] · H[+:3] · γLog[-:e] + δ· [f:n11/x]";
// Or this.Text, in WinForms
this.Title = UniSubSup.Parse(formula);

will print:

N⁽ˣ⁺²⁾·H³·γLogₑ + δ·ⁿ¹¹⁄ₓ

Note1:
When a markup contains spaces, it's skipped: hence [+:(x+2)] is parsed, while [+:(x + 2)] is not (in case these brackets should be ignored).

Note2:
I did not include all the letters because not all fonts support all the CodePoints in the SubScript and SuperScript categories. The Subscript n (\u2099), which is relatively common (e.g., Logn), is not available in most of font types (since sub/super Scripting is generated by different means).
Some (very few) Fonts do have these glyphs. WebSites like fileformat.info can provide this information.

public class UniSubSup
{
    const char joiner =       '\u200D';
    const char nonJoiner =    '\u200C';
    const char fraction =     '\u2044';
    const char solidusShort = '\u0337';
    const char solidusLong =  '\u0338';

    protected internal static Dictionary<string, Func<string, string>> actions =
        new Dictionary<string, Func<string, string>>()
        {
            ["-"] = (s) => sub(s),
            ["+"] = (s) => sup(s),
            ["f"] = (s) => fract(s),
        };

    internal static string sub(string s) => 
        s.Aggregate(new StringBuilder(), (sb, c) => sb.Append(subScripts[c])).ToString();

    internal static string sup(string s) => 
        s.Aggregate(new StringBuilder(), (sb, c) => sb.Append(superScripts[c])).ToString();

    internal static string fract(string str)
    {
        var sb = new StringBuilder();
        var parts = str.Split('/');
        parts[0].Aggregate(sb, (s, c) => sb.Append(superScripts[c]));
        sb.Append(fraction);
        parts[1].Aggregate(sb, (s, c) => sb.Append(subScripts[c]));
        return sb.ToString();
    }

    static RegexOptions options = RegexOptions.Singleline | RegexOptions.Compiled;

    public static string Parse(string input)
    {
        string pattern = @"\[(\D{1}):(\S\/?\S*?)\]";
        var matches = Regex.Matches(input, pattern, options);
        var result = new StringBuilder(input);
        foreach (Match m in matches) 
            result = result.Replace(m.Value, actions[m.Groups[1].Value](m.Groups[2].Value));
        }
        return result.ToString();
    }

    internal static Dictionary<char, char> superScripts = new Dictionary<char, char>()
    {
        ['0'] = '\u2070', ['1'] = '\u00B9', ['2'] = '\u00B2', ['3'] = '\u00B3',
        ['4'] = '\u2074', ['5'] = '\u2075', ['6'] = '\u2076', ['7'] = '\u2077',
        ['8'] = '\u2078', ['9'] = '\u2079',
        ['+'] = '\u207A', ['-'] = '\u207B', ['='] = '\u207C',
        ['('] = '\u207D', [')'] = '\u207E',
        ['e'] = '\u1D49', ['n'] = '\u207F', ['x'] = '\u02E3' 
    };

    internal static Dictionary<char, char> subScripts = new Dictionary<char, char>()
    {
        ['0'] = '\u2080', ['1'] = '\u2081', ['2'] = '\u2082', ['3'] = '\u2083',
        ['4'] = '\u2084', ['5'] = '\u2085', ['6'] = '\u2086', ['7'] = '\u2087',
        ['8'] = '\u2088', ['9'] = '\u2089',
        ['+'] = '\u208A', ['-'] = '\u208B', ['='] = '\u208C',
        ['('] = '\u208D', [')'] = '\u208E', ['/'] = '\u2044',
        ['e'] = '\u2091', ['n'] = '\u2099', ['x'] = '\u2093'
    };
}

Upvotes: 6

Mike Williams
Mike Williams

Reputation: 182

Update on question raised by OP

I am the "poor other sod" who works on this project who has used the above to implement it.

The result is shown below, we now have subscripted characters in the title bar happy days.

This is the code that did the trick in conjunction with the definition of the number to unicode translation seen above

this.Text = "ACME - Editing" + string.Concat("C4H9NO2".Select(c => char.IsDigit(c) ? subScriptNumbers[c-48] : c));

enter image description here

Upvotes: 2

Related Questions