Hover Over/Select Cells, Display Full Text - Dynamic Comments

dslhs

New Member
Joined
Apr 4, 2022
Messages
44
Office Version
  1. 2019
Platform
  1. Windows
Hi,

I have a dynamic table that contains a range of information. I want to keep the cell sizes consistent, but have a way of clearly displayed long text (either by hovering over or selecting the cell).

I'd also like - if possible - to find a solution that works for Excel 365 and Excel 2019.

I have tried a VBA that turns long text into a cell comment...

VBA Code:
Sub Createcomments()
 
'Dimension variables and declare data types
Dim rng As Range
Dim Cell As Variant
 
'Enable error handling
On Error Resume Next
 
'Show inputbox and save input value to object variable rng
Set rng = Application.InputBox(Prompt:="Select a range:", _
Title:="Create comments in cells where the value is larger than column width", _
Default:=Selection.Address, Type:=8)
 
'Disable error handling
On Error GoTo 0
 
'Check if variable rang is empty
If rng Is Nothing Then
 
'Continue if variable rng is not empty
Else
 
    'Iterate through cells in object rng
    For Each Cell In rng
 
        'Check if characters in cell value is wider than column width
        If Len(Cell) * 0.9 > Cell.ColumnWidth Then
 
            'Check that there is no comment to prevent overwriting older comments
            If Cell.Comment Is Nothing Then
                 
                'Add value to cell comment
                Cell.AddComment Cell.Value
            End If
        End If
    Next Cell
End If
End Sub


This works brilliantly for a static table, but not dynamically. I have to run the macro and set the range to update the comments.

I want it to automatically apply the VBA macro to a consistent range (C7:AF101), but run the macro and update the comments every time the dynamic table is changed (indicated by a change to B2).

Alternatively, I'm happy to have an Active X text box displayed, but I can't seem to get that linked.

Many thanks for your help!
 

Excel Facts

VLOOKUP to Left?
Use =VLOOKUP(A2,CHOOSE({1,2},$Z$1:$Z$99,$Y$1:$Y$99),2,False) to lookup Y values to left of Z values.
I would use a modeless Userform for that so comments aren't needed anymore.
 
Upvote 0
I have researched modeless userform but can't see how to make it work. I am very much not a pro, so any particular guidance you can provide would be great
 
Upvote 0
Hi,

Initially, my approach was to use the Worksheet_SelectionChange event handler to place a UserForm on the screen to display the contents of the currently selected cell. This worked fine, but I figured it was much neater to mimic a comment or tooltip. I used the somewhat modified code originally posted by @Jaafar Tribak and @Dataluver in these posts:
Can Excel unhide/hide rows just by hovering a mouse over it
Help With Form Positioner
Workbook BeforeClose Cancel button

The code uses a timer to determine the position of the mouse cursor. A UserForm is used for mimicing a tooltip, so its title bar is hidden. A separate object (class module) acts as a tooltip provider, it has been made responsible for showing and hiding our custom tooltip. Since a timer is used, the timer needs to be terminated before the workbook closes. It's also unwanted to have a timer running if the user activates a sheet on which no tooltips are needed, or if another workbook is being activated, so the code also takes care of that.

The SheetOnFocus method can be customized in a way that tooltips are only shown on the range you need them for, for example (see red colored code below):
Rich (BB code):
Public Sub SheetOnFocus(ByVal argSht As Object)
    Select Case argSht.CodeName
    Case "Sheet1"
        With This
            Set .ToolTipProvider = New CRangeToolTip
            .ToolTipProvider.Initialize argWndTitle:=Excel.Application.ActiveWindow.Caption, argSht:=argSht, argRng:=argSht.Range("C2,C6:E10")
            '.ToolTipProvider.AddRange argSht.Range("C6:E10")
        End With
    
    Case "Sheet2"
        With This
            Set .ToolTipProvider = New CRangeToolTip
            .ToolTipProvider.Initialize argWndTitle:=Excel.Application.ActiveWindow.Caption, argSht:=argSht, argRng:=argSht.Range("Table1")
        End With
    End Select
End Sub

To make this all work, the workbook in which the code is running needs to have a UserForm, which should be renamed to UFToolTip (using F4 key; properties window). This UserForm needs to contain two labels, named Label1 and Label2.
Hopefully this is of some help.

Example Workbook on DropBox

CellToolTips_3.gif



This goes in the userform's module, to be renamed UFToolTip:
VBA Code:
Option Explicit

#If VBA7 Then
    #If Win64 Then
        Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr
        Private Declare PtrSafe Function SetWindowLong Lib "user32" Alias "SetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr
    #Else
        Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr
        Private Declare PtrSafe Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr
    #End If
    Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
    Private Declare PtrSafe Function DrawMenuBar Lib "user32" (ByVal hwnd As LongPtr) As Long
#Else
    Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex 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 FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    Private Declare Function DrawMenuBar Lib "user32" (ByVal hwnd As Long) As Long
#End If

Private Const GWL_STYLE     As Long = &HFFF0
Private Const WS_CAPTION    As Long = &HC00000

Public Function ShowDialog(ByVal argAddress As String, ByVal argText As String, ByVal argTop As Single, ByVal argLeft As Single) As Boolean
    HideCaption
    With Me
        .StartupPosition = 0    ' << Manual
        .Left = argLeft
        .Top = argTop
        .BackColor = &HC0FFFF
        .Label1.Font.Bold = True
        .Label1.Caption = argAddress
        .Label2.Font.Size = 10
        .Label2.Caption = argText
        .Show vbModeless        ' << do NOT change this to vbModal
    End With
    ShowDialog = True
End Function

Public Sub MoveDialog(ByVal argAddress As String, ByVal argText As String, ByVal argTop As Single, ByVal argLeft As Single)
    Me.Label1.Caption = argAddress
    Me.Label2.Caption = argText
    Me.Top = argTop
    Me.Left = argLeft
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CloseMode = VBA.vbFormControlMenu Then
        Cancel = True
    End If
End Sub

Private Sub HideCaption()
    #If VBA7 Then
        Dim Style As LongPtr, hFrm As LongPtr
    #Else
        Dim Style As Long, hFrm As Long
    #End If
    hFrm = FindWindow("ThunderDFrame", Me.Caption)
    Style = GetWindowLong(hFrm, GWL_STYLE)
    Style = Style And Not WS_CAPTION
    SetWindowLong hFrm, GWL_STYLE, Style
    DrawMenuBar hFrm
End Sub


This goes in a Class module, to be renamed CRangeToolTip:
VBA Code:
Option Explicit

Private Type TLocals_CRangeToolTip
    IsInitialized   As Boolean
    HostSheet       As Excel.Worksheet
    ToolTip         As UFToolTip
    TriggerRange    As Excel.Range
    CurrentRange    As Excel.Range
    IsOnScreen      As Boolean
    xlWndTitle      As String
End Type
Private This As TLocals_CRangeToolTip

Public Sub Initialize(ByVal argWndTitle As String, ByVal argSht As Excel.Worksheet, ByVal argRng As Excel.Range)
    With This
        .xlWndTitle = argWndTitle
        Set .HostSheet = argSht
        Set .TriggerRange = argRng
        Set .CurrentRange = argRng
        .IsInitialized = True
        StartTicking Me, argWndTitle
    End With
End Sub

Public Sub AddRange(ByVal argRng As Excel.Range)
    With This
        If .IsInitialized Then
            If Not argRng Is Nothing Then
                If IsSameObject(This.HostSheet, argRng.Parent) Then
                    Set .TriggerRange = Excel.Application.Union(.TriggerRange, argRng)
                End If
            End If
        Else
            VBA.Err.Raise VBA.vbObjectError + 445, "Class CRangeToolTip", "This object has not yet been initialized."
        End If
    End With
End Sub

Public Sub Terminate()
    StopTicking
    HideToolTip
    With This
        .IsInitialized = False
        Set .CurrentRange = Nothing
        Set .TriggerRange = Nothing
        Set .HostSheet = Nothing
    End With
    ' Debug.Print "CRangeToolTip is terminating through public Terminate method"
End Sub

Private Sub Class_Terminate()
    ' Debug.Print "CRangeToolTip is terminating through private Class_Terminate event"
End Sub

Public Sub OnMouseMove(ByVal argRng As Excel.Range, Optional ByVal argTop As Long, Optional ByVal argLeft As Long)
    If Not argRng Is Nothing Then
        If This.CurrentRange.Address = argRng.Address Then
            'do nothing
        Else
            Set This.CurrentRange = argRng
            If Not Excel.Application.Intersect(This.TriggerRange, argRng) Is Nothing Then
                ShowToolTip argTop, argLeft
            Else
                HideToolTip
            End If
        End If
    Else
        HideToolTip
    End If
End Sub

Public Sub HideToolTip()
    With This
        If .IsInitialized Then
            If .IsOnScreen Then
                If Not .ToolTip Is Nothing Then
                    .ToolTip.Hide
                    VBA.Unload .ToolTip
                    Set .ToolTip = Nothing
                    .IsOnScreen = False
                    With Excel.Application.ActiveSheet
                        Set This.CurrentRange = .Cells(.Rows.Count, .Columns.Count)
                    End With
                End If
            End If
        Else
            VBA.Err.Raise VBA.vbObjectError + 445, "Class CRangeToolTip", "This object has not yet been initialized."
        End If
    End With
End Sub

Private Sub ShowToolTip(Optional ByVal argTop As Long, Optional ByVal argLeft As Long)
    With This
        If .IsInitialized Then
            If .ToolTip Is Nothing Then
                Set .ToolTip = New UFToolTip
            End If
            If .IsOnScreen Then
                .ToolTip.MoveDialog .CurrentRange.Address(False, False), .CurrentRange.Text, argTop, argLeft
            Else
                .xlWndTitle = Excel.Application.ActiveWindow.Caption
                .IsOnScreen = .ToolTip.ShowDialog(.CurrentRange.Address(False, False), .CurrentRange.Text, argTop, argLeft)
                VBA.AppActivate .xlWndTitle
            End If
        Else
            VBA.Err.Raise VBA.vbObjectError + 445, "Class CRangeToolTip", "This object has not yet been initialized."
        End If
    End With
End Sub

Private Function IsSameObject(ByVal argObjOne As Object, ByVal argObjTwo As Object) As Boolean
    IsSameObject = VBA.ObjPtr(argObjOne) = VBA.ObjPtr(argObjTwo)
End Function


This goes in the ThisWorkbook module:
VBA Code:
Option Explicit

Private Sub Workbook_Open()
    SheetOnFocus Excel.Application.ActiveSheet
End Sub

Private Sub Workbook_Activate()
    SheetOnFocus Excel.Application.ActiveSheet
End Sub

Private Sub Workbook_Deactivate()
    SheetOnExit
End Sub

Private Sub Workbook_SheetActivate(ByVal Sh As Object)
    SheetOnFocus Sh
End Sub

Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
    SheetOnExit
    SheetOnFocus Sh
End Sub

Private Sub Workbook_SheetDeactivate(ByVal Sh As Object)
    SheetOnExit
End Sub

Private Sub Workbook_BeforeClose(Cancel As Boolean)
    SheetOnExit
    
    '=========================================================================
    'IMPORTANT !! This line must be the last line in the BeforeClose event.
    '=========================================================================
    MonitorClosePrompt(BeforeCloseCancelArgument:=Cancel) = True
End Sub


This goes in a standard module:
VBA Code:
Option Explicit

' SOURCE:
' https://www.mrexcel.com/board/threads/can-excel-unhide-hide-rows-just-by-hovering-a-mouse-over-it.1124546/
' https://www.mrexcel.com/board/threads/help-with-form-positioner.156485/page-2#post-767451
' https://www.mrexcel.com/board/threads/workbook-beforeclose-cancel-button.1054190/#post-5062639

#If VBA7 Then
    Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As Long
    Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
    Private Declare PtrSafe Function GetCursorPos Lib "user32.dll" (ByRef lpPoint As POINTAPI) As Long
    Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
    Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hDC As LongPtr) As Long
    Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hDC As LongPtr, ByVal nIndex As Long) As Long
    Private Declare PtrSafe Function GetActiveWindow Lib "user32" () As LongPtr
    Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hwnd As LongPtr, ByVal dwId As Long, ByRef riid As GUID, ByRef ppvObject As Any) As Long
    Private Declare PtrSafe Function AccessibleChildren Lib "oleacc" (ByVal paccContainer As Office.IAccessible, ByVal iChildStart As Long, ByVal cChildren As Long, ByRef rgvarChildren As Any, ByRef pcObtained As Long) As Long
    Private Declare PtrSafe Function IIDFromString Lib "ole32.dll" (ByVal lpsz As LongPtr, lpiid As GUID) As Long
    Private Declare PtrSafe Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" (ByVal idHook As Long, ByVal lpfn As LongPtr, ByVal hmod As LongPtr, ByVal dwThreadId As Long) As LongPtr
    Private Declare PtrSafe Function UnhookWindowsHookEx Lib "user32" (ByVal hhk As LongPtr) As Long
    Private Declare PtrSafe Function CallNextHookEx Lib "user32" (ByVal hHook As LongPtr, ByVal ncode As Long, ByVal wParam As LongPtr, lParam As Any) As LongPtr
    Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long
    Private Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As LongPtr, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
    
    Private hHook As LongPtr
#Else
    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 Declare Function GetCursorPos Lib "user32.dll" (ByRef lpPoint As POINTAPI) 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 GetDeviceCaps Lib "gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
    Private Declare Function GetActiveWindow Lib "user32" () As Long
    Private Declare Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hwnd As Long, ByVal dwId As Long, ByRef riid As GUID, ByRef ppvObject As Any) As Long
    Private Declare Function AccessibleChildren Lib "oleacc" (ByVal paccContainer As Office.IAccessible, ByVal iChildStart As Long, ByVal cChildren As Long, ByRef rgvarChildren As Any, ByRef pcObtained As Long) As Long
    Private Declare Function IIDFromString Lib "ole32.dll" (ByVal lpsz As Long, lpiid As GUID) As Long
    Private Declare Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" (ByVal idHook As Long, ByVal lpfn As Long, ByVal hmod As Long, ByVal dwThreadId As Long) As Long
    Private Declare Function UnhookWindowsHookEx Lib "user32" (ByVal hhk As Long) As Long
    Private Declare Function CallNextHookEx Lib "user32" (ByVal hHook As Long, ByVal ncode As Long, ByVal wParam As Long, lParam As Any) As Long
    Private Declare Function GetCurrentThreadId Lib "kernel32" () As Long
    Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
    
    Private hHook As Long
#End If

Private Const POINTSPERINCH As Long = 72
Private Const LOGPIXELSX    As Long = 88
Private Const LOGPIXELSY    As Long = 90

Private Const CUSTOMIZEDINTERVAL As Long = 200

Private Const WH_CBT            As Long = 5
Private Const HCBT_DESTROYWND   As Long = 4
Private Const CHILDID_SELF      As Long = &H0&
Private Const NAVDIR_FIRSTCHILD As Long = &H7&
Private Const NAVDIR_NEXT       As Long = &H5&
Private Const S_OK              As Long = &H0&
Private Const OBJID_CLIENT      As Long = &HFFFFFFFC
Private Const IID_IAccessible   As String = "{618736E0-3C3D-11CF-810C-00AA00389B71}"

Private Type GUID
    Data1 As Long
    Data2 As Integer
    Data3 As Integer
    Data4(0 To 7) As Byte
End Type

Private Type POINTAPI
    x As Long
    y As Long
End Type

Private Type TLOCALS
    CursorPos               As POINTAPI
    ToolTipProvider         As CRangeToolTip
    xlWndTitle              As String
    PixelsPerPointX         As Single
    PixelsPerPointY         As Single
    PointsPerPixelX         As Single
    PointsPerPixelY         As Single
    hDC                     As Long
End Type

Private This As TLOCALS

Public Enum RelativePosition
    TopLeft
    TopRight
    BottomLeft
    BottomRight
End Enum


#If VBA7 Then
    Private Sub TickTock(ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal idEvent As LongPtr, ByVal dwTime As Long)
#Else
    Private Sub TickTock(ByVal hwnd As Long, ByVal wMsg As Long, ByVal idEvent As Long, ByVal dwTime As Long)
#End If
    Dim DisplayOnPos As POINTAPI, CurWnd As Excel.Window, Rng As Excel.Range, Shp As Excel.Shape
    
    On Error Resume Next
    If GetActiveWindow <> 0 Then
        Set CurWnd = Excel.Application.ActiveWindow
        If CurWnd.Caption = This.xlWndTitle Then
            If CurWnd.WindowState <> xlMinimized Then
                GetCursorPos This.CursorPos
                With This.CursorPos
                    Set Rng = CurWnd.RangeFromPoint(.x, .y)
                    DisplayOnPos = PointOnScreen(Rng, TopRight)
                    This.ToolTipProvider.OnMouseMove Rng, DisplayOnPos.y, DisplayOnPos.x
                    Set Shp = Nothing
                    Set Shp = ActiveSheet.Shapes(CurWnd.RangeFromPoint(.x, .y).Name)
                    If Not Shp Is Nothing Then
                        This.ToolTipProvider.HideToolTip
                    End If
                End With
            End If
        End If
    End If
End Sub

Public Sub StartTicking(ByVal argToolTipProvider As CRangeToolTip, ByVal argWndTitle As String)
    Set This.ToolTipProvider = argToolTipProvider
    This.xlWndTitle = argWndTitle
    SetTimer Application.hwnd, 1, CUSTOMIZEDINTERVAL, AddressOf TickTock
End Sub

Public Sub StopTicking()
    KillTimer Application.hwnd, 1
    Set This.ToolTipProvider = Nothing
End Sub

Private Function PointOnScreen(ByVal argObject As Object, Optional ByVal argPos As RelativePosition = TopLeft) As POINTAPI
    ' Converts Points to Screen Pixels
    Dim ZoomPerc As Double, CurWin As Excel.Window
    Set CurWin = Excel.Application.ActiveWindow
    ZoomPerc = CurWin.Zoom / 100

    With This
        .hDC = GetDC(0)
        .PixelsPerPointX = GetDeviceCaps(.hDC, LOGPIXELSX) / POINTSPERINCH
        .PointsPerPixelX = POINTSPERINCH / GetDeviceCaps(.hDC, LOGPIXELSX)
        .PixelsPerPointY = GetDeviceCaps(.hDC, LOGPIXELSY) / POINTSPERINCH
        .PointsPerPixelY = POINTSPERINCH / GetDeviceCaps(.hDC, LOGPIXELSY)
            Select Case argPos
                Case TopLeft
                    PointOnScreen.x = CurWin.PointsToScreenPixelsX(argObject.Left * (.PixelsPerPointX * ZoomPerc)) * .PointsPerPixelX
                    PointOnScreen.y = CurWin.PointsToScreenPixelsY(argObject.Top * (.PixelsPerPointY * ZoomPerc)) * .PointsPerPixelY
                Case TopRight
                    PointOnScreen.x = CurWin.PointsToScreenPixelsX((argObject.Left + argObject.Width) * (.PixelsPerPointX * ZoomPerc)) * .PointsPerPixelX
                    PointOnScreen.y = CurWin.PointsToScreenPixelsY(argObject.Top * (.PixelsPerPointY * ZoomPerc)) * .PointsPerPixelY
                Case BottomLeft
                    PointOnScreen.x = CurWin.PointsToScreenPixelsX(argObject.Left * (.PixelsPerPointX * ZoomPerc)) * .PointsPerPixelX
                    PointOnScreen.y = CurWin.PointsToScreenPixelsY((argObject.Top + argObject.Height) * (.PixelsPerPointY * ZoomPerc)) * .PointsPerPixelY
                Case BottomRight
                    PointOnScreen.x = CurWin.PointsToScreenPixelsX((argObject.Left + argObject.Width) * (.PixelsPerPointX * ZoomPerc)) * .PointsPerPixelX
                    PointOnScreen.y = CurWin.PointsToScreenPixelsY((argObject.Top + argObject.Height) * (.PixelsPerPointY * ZoomPerc)) * .PointsPerPixelY
            End Select
        ReleaseDC 0, .hDC
    End With
End Function


Public Property Let MonitorClosePrompt(ByVal BeforeCloseCancelArgument As Boolean, ByVal Monitor As Boolean)
    If ThisWorkbook.Saved Then Exit Property
    If BeforeCloseCancelArgument And Monitor Then Exit Property
    If Monitor Then
        If hHook <> 0 Then Exit Property
        hHook = SetWindowsHookEx(WH_CBT, AddressOf CBT_Func, 0, GetCurrentThreadId())
    Else
        If hHook = 0 Then Exit Property
        Call UnhookWindowsHookEx(hHook)
        hHook = 0
    End If
End Property

Private Function CBT_Func(ByVal ncode As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
    Dim IID As GUID
    Dim oAccObj As IAccessible
    Dim vCancelBtn As Variant
    Dim sBuffer As String * 255
    Dim i As Long
    
    On Error Resume Next
    If ncode = HCBT_DESTROYWND Then
        If GetClassName(wParam, sBuffer, Len(sBuffer)) <> 0 Then
            If Left$(sBuffer, InStr(sBuffer, vbNullChar) - 1) = "NUIDialog" Then
                MonitorClosePrompt(BeforeCloseCancelArgument:=False) = False
                Call IIDFromString(StrPtr(IID_IAccessible), IID)
                If AccessibleObjectFromWindow(wParam, OBJID_CLIENT, IID, oAccObj) = S_OK Then
                    If AccessibleChildren(oAccObj, 0, 1, vCancelBtn, 1) = S_OK Then
                        Set vCancelBtn = vCancelBtn.accNavigate(NAVDIR_FIRSTCHILD, CHILDID_SELF)
                        Set vCancelBtn = vCancelBtn.accNavigate(NAVDIR_FIRSTCHILD, CHILDID_SELF)
                        For i = 1 To 10
                            Set vCancelBtn = vCancelBtn.accNavigate(NAVDIR_NEXT, CHILDID_SELF)
                            If i = 10 And Not IsEmpty(vCancelBtn.accFocus) Then
                                Debug.Print "You Clicked CANCEL"
                                SetTimer Application.hwnd, 0, 0, AddressOf BeforeCloseOnCancel
                                GoTo NxtHook
                            End If
                        Next i
                    End If
                End If
            End If
        End If
    End If
NxtHook:
    CBT_Func = CallNextHookEx(hHook, ncode, wParam, lParam)
End Function

Private Sub BeforeCloseOnCancel()
    On Error Resume Next
    KillTimer Application.hwnd, 0
    On Error GoTo 0
    'MsgBox "CANCEL button clicked !" & vbLf & vbLf & _
    '"Running vba code after cancelling the closing of the workbook is now possible."

    SheetOnFocus Excel.Application.ActiveSheet
End Sub


Public Sub SheetOnFocus(ByVal argSht As Object)
    Select Case argSht.CodeName
    Case "Sheet1"
        With This
            Set .ToolTipProvider = New CRangeToolTip
            .ToolTipProvider.Initialize argWndTitle:=Excel.Application.ActiveWindow.Caption, argSht:=argSht, argRng:=argSht.Range("C2,C6:E10")
            '.ToolTipProvider.AddRange argSht.Range("C6:E10")
        End With
    
    Case "Sheet2"
        With This
            Set .ToolTipProvider = New CRangeToolTip
            .ToolTipProvider.Initialize argWndTitle:=Excel.Application.ActiveWindow.Caption, argSht:=argSht, argRng:=argSht.Range("Table1")
        End With
    End Select
End Sub

Public Sub SheetOnExit()
    With This
        If Not .ToolTipProvider Is Nothing Then
            .ToolTipProvider.Terminate
            Set .ToolTipProvider = Nothing
        End If
    End With
End Sub

'Callback for Backstage.onShow
Public Sub BackstageOnShow(contextObject As Object)
    If Excel.Application.ActiveWorkbook.FullName = ThisWorkbook.FullName Then
        SheetOnExit
    End If
End Sub

'Callback for Backstage.onHide
Public Sub BackstageOnHide(contextObject As Object)
    If Excel.Application.ActiveWorkbook.FullName = ThisWorkbook.FullName Then
        SheetOnFocus Excel.Application.ActiveSheet
    End If
End Sub
 
Upvote 0
In addition to my previous post, it seems I forgot to mention that the used approach has a minor drawback. When the active worksheet uses a tooltip provider and the range involved is within user's view, switching to backstage view (Ribbon, File tab) doesn't prevent Excel from showing our custom tooltips as if the worksheet involved was displayed.
Hence, it would be preferable the workbook this code is running in is modified in a way this behaviour will be eliminated (the provided example workbook is already modified this way).

First a plain text file named CustomUI14.xml is needed, containing the code below. Notepad or any other text editor can be used for that. Store this file on disk and please note that the red colored text needs to be replaced with the filename of your workbook.
Rich (BB code):
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui">
  <ribbon startFromScratch="false"/>
  <backstage onShow="filename.xlsm!BackstageOnShow" onHide="filename.xlsm!BackstageOnHide">
  </backstage>
</customUI>

The contents of this XML file needs to be added to your workbook. This can be done in two ways. Beforehand be sure your workbook isn't open within Excel.

The easiest way is to use Fernando Andreu's OfficeRibbonXEditor which can be downloaded from GitHub. Install and run OfficeRibbonXEditor, click on Open Office Document to load your workbook, on the editor's menu click Insert, click on Office 2010+ Custom UI Part. Now right click on CustomUI14.xml and click Open. Finally paste the xml-code as per above in the right hand pane, click Save and close the ribbon editor. Done.

A slightly more cumbersome way is to use a zip manager, like WinZip or 7-zip. Excel uses the OOXML format and workbooks are in fact zip files containing multiple xml files. Open your workbook within the zip manager, create a new folder and rename that folder in CustomUI. Within zip manager open the newly created CustomUI folder and put the CustomUI14.xml file in that folder (drag&drop). Now navigate to and open the _rels folder, open the .rels file and replace the closing tag:
Rich (BB code):
</Relationships>
with:
Rich (BB code):
<Relationship Id="SomeRandomIDNotYetUsed" Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility" Target="customUI/customUI14.xml"/></Relationships>

Be sure these changes are saved. From now on, with your modified workbook open, Excel will invoke the appropriate macros on entering and leaving Excels backstage view, and unwanted and distracting tooltips aren't displayed within the backstage anymore.
 
Upvote 0
Hi GWteB . Thanks for posting this code.

You will forgive me if I make a few observations.

A) Excel crashed immediately after opening the file.

The culprit was the hDC UDT member which expects a LongLong handle in excel x64 which is the version I use.

The following fixes the problem:
VBA Code:
Private Type TLOCALS
    CursorPos               As POINTAPI
    ToolTipProvider         As CRangeToolTip
    xlWndTitle              As String
    PixelsPerPointX         As Single
    PixelsPerPointY         As Single
    PointsPerPixelX         As Single
    PointsPerPixelY         As Single
    #If Win64 Then
        hDC                 As LongLong
    #Else
        hDC                 As Long
    #End If
End Type

B) Regarding the way the userform-based tooltip looks, I think, the edges are bumped and not flat as they should look in real tooltips (removing the WS_EX_DLGMODALFRAME bit fixes that)... Adding a tiny black frame around the tooltip would also be good (FrameRect api).

Also, I think it would be better to give the the tooltip the user's current default system backcolor for tooltips ( GetSysColor+ TranslateColor)

Here is the entire useform module that I would use for a more natural looking tooltip :
VBA Code:
Option Explicit

Private Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

#If VBA7 Then
    #If Win64 Then
        Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr
        Private Declare PtrSafe Function SetWindowLong Lib "user32" Alias "SetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr
    #Else
        Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr
        Private Declare PtrSafe Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr
    #End If
    Private Declare PtrSafe Function DrawMenuBar Lib "user32" (ByVal hwnd As LongPtr) As Long
    Private Declare PtrSafe Function FrameRect Lib "user32" (ByVal hDc As LongPtr, lpRect As RECT, ByVal hBrush As LongPtr) As Long
    Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
    Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hDc As LongPtr) As Long
    Private Declare PtrSafe Function CreateSolidBrush Lib "gdi32" (ByVal crColor As Long) As LongPtr
    Private Declare PtrSafe Function DeleteObject Lib "gdi32" (ByVal hObject As LongPtr) As Long
    Private Declare PtrSafe Function GetClientRect Lib "user32" (ByVal hwnd As LongPtr, lpRect As RECT) As Long
    Private Declare PtrSafe Function IUnknown_GetWindow Lib "shlwapi" Alias "#172" (ByVal pIUnk As IUnknown, ByVal hUf As LongPtr) As Long
    Private Declare PtrSafe Function TranslateColor Lib "oleAut32.dll" Alias "OleTranslateColor" (ByVal clr As OLE_COLOR, ByVal palet As LongPtr, col As Long) As Long
    Private Declare PtrSafe Function GetSysColor Lib "user32" (ByVal nIndex As Long) As Long

#Else
    Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex 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 DrawMenuBar Lib "user32" (ByVal hwnd As Long) As Long
    Private Declare Function FrameRect Lib "user32" (ByVal hDc As Long, lpRect As RECT, ByVal hBrush As Long) 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 CreateSolidBrush Lib "gdi32" (ByVal crColor As Long) As Long
    Private Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As Long
    Private Declare Function GetClientRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long
    Private Declare Function IUnknown_GetWindow Lib "shlwapi" Alias "#172" (ByVal pIUnk As IUnknown, ByVal hUf As Long) As Long
    Private Declare Function TranslateColor Lib "oleAut32.dll" Alias "OleTranslateColor" (ByVal clr As OLE_COLOR, ByVal palet As Long, col As Long) As Long
    Private Declare Function GetSysColor Lib "user32" (ByVal nIndex As Long) As Long

#End If


Public Function ShowDialog(ByVal argAddress As String, ByVal argText As String, ByVal argTop As Single, ByVal argLeft As Single) As Boolean

    Const COLOR_INFOBK = 24
    Dim lBackColor As Long

    Call HideCaption
    With Me
        .StartupPosition = 0    ' << Manual
        .Left = argLeft
        .Top = argTop
         Call TranslateColor(GetSysColor(COLOR_INFOBK), 0, lBackColor)
        .BackColor = lBackColor '&HC0FFFF
        .Label1.Font.Bold = True
        .Label1.Caption = argAddress
        .Label2.Font.Size = 10
        .Label2.Caption = argText
        .Show vbModeless        ' << do NOT change this to vbModal
    End With
    Call DrawTipRect
    ShowDialog = True
End Function

Public Sub MoveDialog(ByVal argAddress As String, ByVal argText As String, ByVal argTop As Single, ByVal argLeft As Single)
    Me.Label1.Caption = argAddress
    Me.Label2.Caption = argText
    Me.Top = argTop
    Me.Left = argLeft
    Call DrawTipRect
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CloseMode = VBA.vbFormControlMenu Then
        Cancel = True
    End If
End Sub

Private Sub HideCaption()

    Const GWL_STYLE = &HFFF0
    Const GWL_EXSTYLE = (-20)
    Const WS_CAPTION = &HC00000
    Const WS_DISABLED = &H8000000
    Const WS_EX_DLGMODALFRAME = &H1&
    Const WS_EX_NOACTIVATE = &H8000000
    Const WS_EX_TOOLWINDOW = &H80

    #If Win64 Then
        Dim hFrm As LongLong, Style As LongLong
    #Else
        Dim hFrm As Long, Style As Long
    #End If

    Call IUnknown_GetWindow(Me, VarPtr(hFrm))
    Style = GetWindowLong(hFrm, GWL_STYLE)
    Style = Style And Not WS_CAPTION And Not WS_DISABLED
    Call SetWindowLong(hFrm, GWL_STYLE, Style)
    Style = GetWindowLong(hFrm, GWL_EXSTYLE)
    Style = Style And Not (WS_EX_DLGMODALFRAME)
    Style = Style Or WS_EX_TOOLWINDOW Or WS_EX_NOACTIVATE
    Call SetWindowLong(hFrm, GWL_EXSTYLE, Style)
    Call DrawMenuBar(hFrm)
  
End Sub

Private Sub DrawTipRect()

    #If Win64 Then
        Dim hFrm As LongLong, hDc As LongLong, hBrush As LongLong
    #Else
        Dim hFrm As Long, hDc As Long, hBrush As Long
    #End If

    Dim tTTipRect As RECT

    DoEvents
    Call IUnknown_GetWindow(Me, VarPtr(hFrm))
    Call GetClientRect(hFrm, tTTipRect)
    hDc = GetDC(hFrm)
    hBrush = CreateSolidBrush(0)
    Call FrameRect(hDc, tTTipRect, hBrush)
    Call DeleteObject(hBrush)
    Call ReleaseDC(hFrm, hDc)

End Sub

C) Regarding the backstage view issue, I think, it is easier to just add a small boolean function as follows: ( This will save you the need to alter the XML file.)
VBA Code:
Function IsBackstageView() As Boolean
    IsBackstageView = CBool(FindWindowEx(Application.hwnd, 0, "FullpageUIHost", vbNullString))
End Function

Note that the same problem happens if the VBE window is displayed and active but again, you can use a small boolean function for this as well as follows:
VBA Code:
Function IsVBEView() As Boolean
    IsVBEView = GetActiveWindow = FindWindow("wndclass_desked_gsk", vbNullString)
End Function

Now that you have these two boolean functions ready, you can use them at the start of the TickTock timer callback as follows :
VBA Code:
#If VBA7 Then
    Private Sub TickTock(ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal idEvent As LongPtr, ByVal dwTime As Long)
#Else
    Private Sub TickTock(ByVal hwnd As Long, ByVal wMsg As Long, ByVal idEvent As Long, ByVal dwTime As Long)
#End If
    Dim DisplayOnPos As POINTAPI, CurWnd As Excel.Window, Rng As Excel.Range, Shp As Excel.Shape
  
    On Error Resume Next
  
    If IsBackstageView Or IsVBEView Then Exit Sub

D) Regarding the cancelling of the workbook close, there seems to be a bug in excel that stops the Workbook_BeforeClose event handler from firing if closing is cancelled more than once! I need to investigate this further (This seems to happen at least in excel 2016 -not sure about other xl versions) . Because of this , I would replace the Workbook_BeforeClose event with the old-fashioned Auto_Close Macro in the Standard Module :
VBA Code:
Sub Auto_Close()
    SheetOnExit  
    '=========================================================================
    'IMPORTANT !! This line must be the last line in the BeforeClose event.
    '=========================================================================
    MonitorClosePrompt(BeforeCloseCancelArgument:=Cancel) = True
End Sub
This should also make the code more compact.

E) One final thing:
You are using VBA.Err.Raise in your class module without the windows timer being released first . I would first make sure to release the timer to avoid potential crashes.
VBA Code:
KillTimer Application.hwnd, 0
VBA.Err.Raise VBA.vbObjectError + 445, .....

Also, speaking of potential erros, I think you will need to add some error handling. One nasty error that also crashed excel was simply adding a new worksheet while the timer was running. The error happend in the SheetOnFocus routine. You may want to look into that.

Regards.

PS: An alternative to using a windows timer for this kind of stuff is the use of the CommandBars OnUpdate event. I have used this hack before. It is a bit slower alternative but safer.
 
Upvote 0
You could try to edit your original code to work dynamically:
VBA Code:
Private Sub Worksheet_Change(ByVal Target As Range)
    Dim rng As Range
    Dim Cell As Variant
    
    If Not Intersect(Target, Me.ListObjects(1).DataBodyRange) Is Nothing Then
     
        Set rng = Me.ListObjects(1).DataBodyRange
         
        If Not rng Is Nothing Then
            For Each Cell In rng
                If Len(Cell) * 0.9 > Cell.ColumnWidth Then
                    If Cell.Comment Is Nothing Then
                        Cell.AddComment Cell.Value
                    End If
                End If
            Next Cell
        End If
    End If
End Sub
 
Upvote 0
You could try to edit your original code to work dynamically:
VBA Code:
Private Sub Worksheet_Change(ByVal Target As Range)
    Dim rng As Range
    Dim Cell As Variant
   
    If Not Intersect(Target, Me.ListObjects(1).DataBodyRange) Is Nothing Then
    
        Set rng = Me.ListObjects(1).DataBodyRange
        
        If Not rng Is Nothing Then
            For Each Cell In rng
                If Len(Cell) * 0.9 > Cell.ColumnWidth Then
                    If Cell.Comment Is Nothing Then
                        Cell.AddComment Cell.Value
                    End If
                End If
            Next Cell
        End If
    End If
End Sub
Hi,

For the life of me, I can't get this to work? Would this replace the entirety of the VBA I listed above or just a section? When I enter it as a module, I can't seem to run it and it doesn't add any comments by itself

Many thanks
 
Upvote 0

Forum statistics

Threads
1,223,239
Messages
6,170,947
Members
452,368
Latest member
jayp2104

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