Search code examples
c#wpfwinformssubscriptsuperscript

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.


Solution

  • 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'
        };
    }