Subscript and Superscript characters on userform labels

Jaafar Tribak

Well-known Member
Joined
Dec 5, 2002
Messages
9,596
Office Version
  1. 2016
Platform
  1. Windows
Hi,

I never noticed that sub/superscript characters couldn't be displayed on userform captions until this was raised on this forum the other day.

here is a little hack that i've managed to put together to simulate sub/superscrips via subclassing the userform.I am hoping this could be expanded to other than just label controls.


here is a workbook demo:
http://www.savefile.com/files/2109591


You will need to add a label control ( with no caption) to your userform and set its caption through the SubSuperScript routinelike: Call SubSuperScript("(a+b)^2^ = a^2^+b^2^+2ab", Me.Label1)
where superscript text is placed between (^ ^ )and subscript chars between (_ _).

The above example will output (a+b)2= a2+b2+2ab with 2 as a superscript char.


code in the userform module:
Code:
Option Explicit
 
Private Sub UserForm_Activate()
 
    'example 3
    Call SubSuperScript("(a+b)^2^ = a^2^+b^2^+2ab", Me.Label1)
 
End Sub
 
Private Sub UserForm_Terminate()
 
    Call RemoveSubclass
 
End Sub

code in a standard module:

Code:
Option Explicit
Option Base 1
 
Private Type POINTAPI
    x As Long
    y As Long
End Type
 
Private Type LOGFONT
    lfHeight As Long
    lfWidth As Long
    lfEscapement As Long
    lfOrientation As Long
    lfWeight As Long
    lfItalic As String * 1
    lfUnderline As String * 1
    lfStrikeOut As String * 1
    lfCharSet As String * 1
    lfOutPrecision As String * 1
    lfClipPrecision As String * 1
    lfQuality As String * 1
    lfPitchAndFamily As String * 1
    lfFaceName As String * 32
End Type
 
Private oLabel As Object
Private sText As String
 
Private Declare Function FindWindow Lib "user32" _
Alias "FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
 
Private Declare Function GetDC Lib "user32" _
(ByVal hwnd As Long) As Long
 
Private Declare Function ReleaseDC Lib "user32" _
(ByVal hwnd As Long, _
ByVal hdc As Long) As Long
 
Private Declare Function TextOut Lib "gdi32" _
Alias "TextOutA" _
(ByVal hdc As Long, _
ByVal x As Long, _
ByVal y As Long, _
ByVal lpString As String, _
ByVal nCount As Long) As Long
 
Private Declare Function SetBkMode Lib "gdi32" _
(ByVal hdc As Long, _
ByVal nBkMode As Long) As Long
 
Private Declare Function SetWindowLong Lib "user32" _
Alias "SetWindowLongA" _
(ByVal hwnd As Long, _
ByVal nIndex As Long, _
ByVal dwNewLong As Long) As Long
 
Private Declare Function CallWindowProc Lib "user32" _
Alias "CallWindowProcA" _
(ByVal lpPrevWndFunc As Long, _
ByVal hwnd As Long _
, ByVal Msg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
 
Private Const GWL_WNDPROC = (-4)
Private Const WM_MOVE = &H3
Private Const WM_ACTIVATEAPP = &H1C
 
Private Declare Function MoveWindow Lib "user32" _
(ByVal hwnd As Long, _
ByVal x As Long, _
ByVal y As Long, _
ByVal nWidth As Long, _
ByVal nHeight As Long, _
ByVal bRepaint As Long) As Long
 
Private Declare Function CreateFontIndirect Lib "gdi32" _
Alias "CreateFontIndirectA" _
(lpLogFont As LOGFONT) As Long
 
Private Declare Function SelectObject Lib "gdi32" _
(ByVal hdc As Long, _
ByVal hObject As Long) As Long
 
Private Declare Function DeleteObject Lib "gdi32" _
(ByVal hObject As Long) As Long
 
Private Declare Function GetTextExtentPoint32 Lib "gdi32" _
Alias "GetTextExtentPoint32A" _
(ByVal hdc As Long, _
ByVal lpsz As String, _
ByVal cbString As Long, _
lpSize As POINTAPI) As Long
 
Private lOldFont As Long
Private lHwnd As Long
Private lPrevWnd As Long
 
Private Function CallBack _
(ByVal hwnd As Long, ByVal Msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
 
    Dim textSize As POINTAPI
    Dim tPt1 As POINTAPI
    Dim sCurString As String
    Dim sString As String
    Dim sCurChar As String * 1
    Dim lDC As Long
    Dim lLeft As Long
    Dim lTop  As Long
    Dim lRight  As Long
    Dim lBottom  As Long
    Dim i As Long
    Dim sglCurTextWidth As Single
 
 
    sString = sText
    Select Case Msg
        Case WM_MOVE, WM_ACTIVATEAPP
            lDC = GetDC(hwnd)
            SetBkMode lDC, 1
            For i = 1 To Len(sString)
                sCurChar = Mid(sString, i, 1)
                If sCurChar <> "_" And sCurChar <> "^" Then
                    With oLabel
                        lTop = .Top * 1.3333
                        lLeft = .Left * 1.3333
                        lRight = (.Left + .Width) * 1.333
                        lBottom = (.Top + .Height) * 1.333
                    End With
                    tPt1.x = lLeft
                    GetTextExtentPoint32 lDC, sCurString, _
                    Len(sCurString), textSize
                    sglCurTextWidth = textSize.x
                    Call SetFont(lDC, SmallFont:=True)
                    On Error Resume Next
                    WorksheetFunction.Match i, _
                    AssignSuperSubScriptCharPosToArray(sString), 0
                    If Err = 0 Then
                        On Error GoTo 0
                        TextOut lDC, lLeft + sglCurTextWidth, _
                        lBottom, sCurChar, Len(sCurChar)
                        GoTo nxt
                    End If
                    On Error Resume Next
                    WorksheetFunction.Match i, _
                    AssignSuperSubScriptCharPosToArray(sString, True), 0
                    If Err = 0 Then
                        On Error GoTo 0
                        TextOut lDC, lLeft + sglCurTextWidth, _
                        lTop, sCurChar, Len(sCurChar)
                        GoTo nxt
                    End If
                    Call SetFont(lDC)
                    TextOut lDC, lLeft + sglCurTextWidth, _
                    lTop, sCurChar, Len(sCurChar)
nxt:
                    SelectObject lDC, lOldFont
                    sCurString = (sCurString & sCurChar)
                End If
            Next
            ReleaseDC hwnd, lDC
            Exit Function
    End Select
    CallBack = CallWindowProc _
    (lPrevWnd, hwnd, Msg, wParam, ByVal lParam)
 
End Function
 
Private Sub SetFont(DC As Long, Optional SmallFont As Boolean)
    Dim uFont As LOGFONT
    Dim lFHwnd As Long
 
    With uFont
        .lfFaceName = "Arial" & Chr$(0)
        If SmallFont Then
            .lfHeight = 8 ' change these font params as required
            .lfWidth = 7 '
            lFHwnd = CreateFontIndirect(uFont)
            lOldFont = SelectObject(DC, lFHwnd)
        Else
            .lfHeight = 14 ' change these font params as required
            .lfWidth = 8 '
        End If
    End With
    lFHwnd = CreateFontIndirect(uFont)
    lOldFont = SelectObject(DC, lFHwnd)
    DeleteObject lFHwnd
 
End Sub
 
Sub SubSuperScript(text As String, Label As Object)
 
    Call SuClassForm(text, Label)
 
End Sub
 
Private Sub SuClassForm(text As String, Label As Object)
 
    Dim i As Long
    Dim dOldtimer As Double
 
    dOldtimer = Timer
    Set oLabel = Label
    oLabel.AutoSize = True
    sText = text
    lHwnd = FindWindow(vbNullString, Label.Parent.Caption)
    lPrevWnd = SetWindowLong(lHwnd, GWL_WNDPROC, AddressOf CallBack)
    Do
        i = i + 1
        DoEvents
    Loop Until Timer - dOldtimer > 0.0001
    With Label.Parent
        .Move .Left + 1, .Top, .Width, .Height
        .Move .Left - 1, .Top, .Width, .Height
    End With
 
End Sub
 
Private Function AssignSuperSubScriptCharPosToArray _
(text As String, Optional Superscript As Boolean) _
As Long()
 
    Dim ar1() As Long
    Dim ar2() As Long
    Dim loops
    Dim n As Long
    Dim i As Long
    Dim j As Long
    Dim k As Long
 
    On Error Resume Next
 
    If Superscript Then
        n = 1
        For i = 1 To Len(text)
            If Mid(text, i, 1) = "^" Then
                ReDim Preserve ar1(n)
                ar1(n) = i
                n = n + 1
            End If
        Next
    Else
        n = 1
        For i = 1 To Len(text)
            If Mid(text, i, 1) = "_" Then
                ReDim Preserve ar1(n)
                ar1(n) = i
                n = n + 1
            End If
        Next
    End If
    For i = 1 To UBound(ar1) Step 2
        loops = (ar1(i + 1) - ar1(i)) - 1
        For j = 1 To loops
            k = k + 1
            ReDim Preserve ar2(k)
            ar2(k) = ar1(i) + j
        Next j
    Next i
 
    AssignSuperSubScriptCharPosToArray = (ar2)
 
End Function
 
Sub RemoveSubclass()
 
    SetWindowLong lHwnd, GWL_WNDPROC, lPrevWnd
 
End Sub

Tested on WinXP excel2003. I would be interested to know if there are any bugs when tested on a other systems.

Regards.
 

Excel Facts

Wildcard in VLOOKUP
Use =VLOOKUP("Apple*" to find apple, Apple, or applesauce
You've been busy Jaafar - amazing! (y)

It works for me in Excel 2000 on Windows XP, although the font isn't the default Tahoma 8 point.
 
Upvote 0
You've been busy Jaafar - amazing! (y)

It works for me in Excel 2000 on Windows XP, although the font isn't the default Tahoma 8 point.

The font can be changed in the SetFont routine as required although it may take some trial and error before looking right.

Special care needs to be taken if/when editing the code as this could potentially crash excel. - debuger not working when subclassing - so any work should be saved before !

Thanks for testing the code Andrew.

Regards.
 
Upvote 0
Here is an update: http://www.savefile.com/files/2110558

As opposed to the previous code which could only be applied to one Label at a time, the update can now be applied to any number of labels. Having said that, the more labels the slower the redraw of the userform.

One other thing is that ,occasionally, when the excel window is desactivated and then activated again, the labels do not refresh until the userform is physically moved by the user.
It looks as if excel is not fast enough to catch the WM_ACTIVATEAPP and WM_ENABLE messages.- There may be more bugs if tested on other machines.

Anyway, here is an example of the updated code that works for more than one label.

Example requirements (4 Labels without caption )


In the UserForm module:

Code:
Option Explicit
 
Private Sub UserForm_Activate()
 
    Call SubSuperScript _
    ("Ba(BrO_3_)_2_·2H_2_O = barium bromate dihydrate ", Me.Label1)
 
    Call SubSuperScript("H_2_O", Me.Label2)
 
    Call SubSuperScript("(a+b)^2^ = a^2^+b^2^+2ab", Me.Label3)
 
    Call SubSuperScript("C^ hapter^  O^ ne^", Me.Label4)
 
End Sub

In a Standard module :

Code:
Option Explicit
Option Base 1
 
Private Type POINTAPI
    x As Long
    y As Long
End Type
 
Private Type LOGFONT
    lfHeight As Long
    lfWidth As Long
    lfEscapement As Long
    lfOrientation As Long
    lfWeight As Long
    lfItalic As String * 1
    lfUnderline As String * 1
    lfStrikeOut As String * 1
    lfCharSet As String * 1
    lfOutPrecision As String * 1
    lfClipPrecision As String * 1
    lfQuality As String * 1
    lfPitchAndFamily As String * 1
    lfFaceName As String * 32
End Type
 
Private Declare Function FindWindow Lib "user32" _
Alias "FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
 
Private Declare Function GetDC Lib "user32" _
(ByVal hwnd As Long) As Long
 
Private Declare Function ReleaseDC Lib "user32" _
(ByVal hwnd As Long, _
ByVal hdc As Long) As Long
 
Private Declare Function TextOut Lib "gdi32" _
Alias "TextOutA" _
(ByVal hdc As Long, _
ByVal x As Long, _
ByVal y As Long, _
ByVal lpString As String, _
ByVal nCount As Long) As Long
 
Private Declare Function SetBkMode Lib "gdi32" _
(ByVal hdc As Long, _
ByVal nBkMode As Long) As Long
 
Private Declare Function SetWindowLong Lib "user32" _
Alias "SetWindowLongA" _
(ByVal hwnd As Long, _
ByVal nIndex As Long, _
ByVal dwNewLong As Long) As Long
 
Private Declare Function CallWindowProc Lib "user32" _
Alias "CallWindowProcA" _
(ByVal lpPrevWndFunc As Long, _
ByVal hwnd As Long _
, ByVal Msg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
 
Private Const GWL_WNDPROC = (-4)
Private Const WM_MOVE = &H3
Private Const WM_ACTIVATEAPP = &H1C
Const WM_ENABLE = &HA
Private Const WM_DESTROY = &H2
 
Private Declare Function CreateFontIndirect Lib "gdi32" _
Alias "CreateFontIndirectA" _
(lpLogFont As LOGFONT) As Long
 
Private Declare Function SelectObject Lib "gdi32" _
(ByVal hdc As Long, _
ByVal hObject As Long) As Long
 
Private Declare Function DeleteObject Lib "gdi32" _
(ByVal hObject As Long) As Long
 
Private Declare Function GetTextExtentPoint32 Lib "gdi32" _
Alias "GetTextExtentPoint32A" _
(ByVal hdc As Long, _
ByVal lpsz As String, _
ByVal cbString As Long, _
lpSize As POINTAPI) As Long
 
Private Declare Function PostMessage Lib "user32" _
Alias "PostMessageA" _
(ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
 
Private Declare Function SetTimer _
Lib "user32" _
    (ByVal hwnd As Long, _
    ByVal nIDEvent As Long, _
    ByVal uElapse As Long, _
    ByVal lpTimerFunc As Long) As Long
 
Private Declare Function KillTimer _
Lib "user32" _
    (ByVal hwnd As Long, _
    ByVal nIDEvent As Long) As Long
 
Private LabelControlArray() As Object
Private LabelTextArray() As String
Private lOldFont As Long
Private lHwnd As Long
Private lPrevWnd As Long
Private lCounter As Long
Private lTimerID As Long
Private oLabel As Object
 
Private Function CallBack _
(ByVal hwnd As Long, ByVal Msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
 
    Dim textSize As POINTAPI
    Dim sCurString As String
    Dim sString As String
    Dim sCurChar As String * 1
    Dim lDC As Long
    Dim lLeft As Long
    Dim lTop  As Long
    Dim lRight  As Long
    Dim lBottom  As Long
    Dim i As Long
    Dim lLblCount As Long
    Dim sglCurTextWidth As Single
 
    Select Case Msg
 
        Case WM_MOVE
 
            lDC = GetDC(hwnd)
            SetBkMode lDC, 1
            For lLblCount = LBound(LabelControlArray) _
            To UBound(LabelControlArray)
                sString = LabelTextArray(lLblCount)
                For i = 1 To Len(sString)
                    sCurChar = Mid(sString, i, 1)
                    If sCurChar <> "_" And sCurChar <> "^" Then
                        With LabelControlArray(lLblCount)
                            lTop = .Top * 1.3333
                            lLeft = .Left * 1.3333
                            lRight = (.Left + .Width) * 1.333
                            lBottom = (.Top + .Height) * 1.333
                        End With
                        GetTextExtentPoint32 lDC, sCurString, _
                        Len(sCurString), textSize
                        sglCurTextWidth = textSize.x
                        Call SetFont(lDC, SmallFont:=True)
                        On Error Resume Next
                        WorksheetFunction.Match i, _
                        AssignSuperSubScriptCharPosToArray(sString), 0
                        If Err = 0 Then
                            On Error GoTo 0
                            TextOut lDC, lLeft + sglCurTextWidth, _
                            lBottom, sCurChar, Len(sCurChar)
                            GoTo nxt
                        End If
                        On Error Resume Next
                        WorksheetFunction.Match i, _
                        AssignSuperSubScriptCharPosToArray(sString, True), 0
                        If Err = 0 Then
                            On Error GoTo 0
                            TextOut lDC, lLeft + sglCurTextWidth, _
                            lTop, sCurChar, Len(sCurChar)
                            GoTo nxt
                        End If
                        Call SetFont(lDC)
                        TextOut lDC, lLeft + sglCurTextWidth, _
                        lTop, sCurChar, Len(sCurChar)
nxt:
                        SelectObject lDC, lOldFont
                        sCurString = (sCurString & sCurChar)
                    End If
                Next i
                sCurString = ""
            Next lLblCount
            ReleaseDC hwnd, lDC
            Exit Function
 
        Case WM_ACTIVATEAPP, WM_ENABLE
            PostMessage hwnd, WM_MOVE, 0, 0
 
        Case WM_DESTROY
            Call RemoveSubclass
 
    End Select
 
    CallBack = CallWindowProc _
    (lPrevWnd, hwnd, Msg, wParam, ByVal lParam)
 
End Function
 
Private Sub SetFont(DC As Long, Optional SmallFont As Boolean)
 
    Dim uFont As LOGFONT
    Dim lFHwnd As Long
 
    With uFont
        .lfFaceName = "Arial" & Chr$(0)
        If SmallFont Then
            .lfHeight = 8 ' change these font params as required
            .lfWidth = 7 '
            lFHwnd = CreateFontIndirect(uFont)
            lOldFont = SelectObject(DC, lFHwnd)
        Else
            .lfHeight = 14 ' change these font params as required
            .lfWidth = 8 '
        End If
    End With
    lFHwnd = CreateFontIndirect(uFont)
    lOldFont = SelectObject(DC, lFHwnd)
    DeleteObject lFHwnd
 
End Sub
 
Sub SubSuperScript(text As String, label As Object)
 
    Set oLabel = label
    If lCounter = 0 Then lCounter = 1
    ReDim Preserve LabelControlArray(lCounter)
    ReDim Preserve LabelTextArray(lCounter)
    LabelTextArray(lCounter) = text
    Set LabelControlArray(lCounter) = label
    lCounter = lCounter + 1
   If lTimerID = 0 Then
    lTimerID = SetTimer(0, 0, 1, AddressOf TimerProc)
   End If
 
End Sub
 
Private Sub TimerProc()
 
    KillTimer 0, lTimerID
    Call SuClassForm(oLabel)
 
End Sub
 
Private Sub SuClassForm(label As Object)
 
    If lPrevWnd = 0 Then
        lHwnd = FindWindow(vbNullString, label.Parent.Caption)
        lPrevWnd = SetWindowLong(lHwnd, GWL_WNDPROC, AddressOf CallBack)
        With label.Parent
            .Move .Left + 1, .Top, .Width, .Height
            .Move .Left - 1, .Top, .Width, .Height
        End With
    End If
 
End Sub
 
Private Function AssignSuperSubScriptCharPosToArray _
(text As String, Optional Superscript As Boolean) _
As Long()
 
    Dim ar1() As Long
    Dim ar2() As Long
    Dim loops
    Dim n As Long
    Dim i As Long
    Dim j As Long
    Dim k As Long
 
    On Error Resume Next
 
    If Superscript Then
        n = 1
        For i = 1 To Len(text)
            If Mid(text, i, 1) = "^" Then
                ReDim Preserve ar1(n)
                ar1(n) = i
                n = n + 1
            End If
        Next
    Else
        n = 1
        For i = 1 To Len(text)
            If Mid(text, i, 1) = "_" Then
                ReDim Preserve ar1(n)
                ar1(n) = i
                n = n + 1
            End If
        Next
    End If
    For i = 1 To UBound(ar1) Step 2
        loops = (ar1(i + 1) - ar1(i)) - 1
        For j = 1 To loops
            k = k + 1
            ReDim Preserve ar2(k)
            ar2(k) = ar1(i) + j
        Next j
    Next i
 
    AssignSuperSubScriptCharPosToArray = (ar2)
 
End Function
 
Private Sub RemoveSubclass()
 
    SetWindowLong lHwnd, GWL_WNDPROC, lPrevWnd
    lPrevWnd = 0
    lTimerID = 0
    Erase LabelControlArray()
    Erase LabelTextArray()
 
End Sub

As opposed to VB controls, VBA controls are lightweight and have no hwnd which makes them impossible to be subclassed and difficult to manipulate. This limits the possibility of more robust and stable coding.

Regards.
 
Upvote 0
i can 't get hold of Windows Pre- XP in order to test the code. I am curious to know if this works in other than XP (In particular for the GetTextExtentPoint32 API which , according to the msdn documentation,may behave differently depending on the OS).

Would anybody working on a OS different from XP kindly try the workbook and let me know if it works.

I see no reason to try and further expand this to other controls if it's not stable and consistent enough.

Regards.
 
Upvote 0

Forum statistics

Threads
1,214,644
Messages
6,120,709
Members
448,983
Latest member
Joaquim_Baptista

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top