1

I can use the following code to set a custom icon for the Excel application. This will change the icon of the window, and the icon displayed in the Windows taskbar:

Public Const strIcon As String = "%SystemRoot%\system32\SHELL32.dll" ' Icon file
Public Const IconIndex As Long = 137

Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal ClassName As String, ByVal WindowName As String) As Long
Public Declare Function SendMessageA Lib "user32" (ByVal HWnd As Long, ByVal wMsg As Long, ByVal wParam As Integer, ByVal lParam As Long) As Long
Public Declare Function ExtractIconA Lib "shell32.dll" (ByVal hInst As Long, ByVal lpszExeFileName As String, ByVal nIconIndex As Long) As Long
Public Const ICON_SMALL As Long = 0&
Public Const ICON_BIG As Long = 1&
Public Const WM_SETICON As Long = &H80



Sub SetupIcon()
    SetIcon strIcon, IconIndex
End Sub

Sub SetIcon(FileName As String, Optional index As Long = 0)
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    ' SetIcon
    ' This procedure sets the icon in the upper left corner of
    ' the main Excel window. FileName is the name of the file
    ' containing the icon. It may be an .ico file, an .exe file,
    ' or a .dll file. If it is an .ico file, Index must be 0
    ' or omitted. If it is an .exe or .dll file, Index is the
    ' 0-based index to the icon resource.
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    #If VBA7 And Win64 Then
        ' 64 bit Excel
        Dim HWnd As LongPtr
        Dim HIcon As LongPtr
    #Else
        ' 32 bit Excel
        Dim HWnd As Long
        Dim HIcon As Long
    #End If
    Dim n As Long
    Dim s As String
    If Dir(FileName, vbNormal) = vbNullString Then
        ' file not found, get out
        Exit Sub
    End If
    ' get the extension of the file.
    n = InStrRev(FileName, ".")
    s = LCase(Mid(FileName, n + 1))
    ' ensure we have a valid file type
    Select Case s
        Case "exe", "ico", "dll"
            ' OK
        Case Else
            ' invalid file type
            Err.Raise 5
    End Select
    HWnd = Application.HWnd
    If HWnd = 0 Then
        Exit Sub
    End If
    HIcon = ExtractIconA(0, FileName, index)
    If HIcon <> 0 Then
        SendMessageA HWnd, WM_SETICON, ICON_SMALL, HIcon
    End If
End Sub

What I notice, however, is that if a new workbook is added to the application then the custom icon is lost (at least, in the taskbar) - and it reverts to the default Excel icon.

Searching online for a solution, I found a similar question asked on SO: Changing Excel Icon doesn't work when another workbook is opened

Naturally I would not normally post a new question which is an exact duplicate of an existing question. However no (ready) solution has been offered to that linked question. I also note that the question was posted in 2012, and therefore it is highly likely that since that time our community has grown in expertise and experience. They may well be someone out here now, who knows how to solve it but has simply not seen the question. I hope the community will forgive the duplicate question (think of it as bumping the old one).

Would anyone be able to offer a solution to this? My API knowledge is almost zero. Thanks.

Pᴇʜ
  • 56,719
  • 10
  • 49
  • 73
Chris Melville
  • 1,476
  • 1
  • 14
  • 30
  • Wouldn't it be the case of calling the UDF again once the new workbook is added? I haven't tried the UDF but looking at it, it looks like it's just resetting the icon – Zac Aug 10 '18 at 12:18
  • @Zac - You'd think so, wouldn't you? However that's the first thing I tried. Strangely, it doesn't work to just call the sub again after creating a new workbook. It seems the very presence of a second workbook blocks the function. – Chris Melville Aug 10 '18 at 12:26
  • Not sure what you mean by `new workbook is added to the application`? Are you importing data from another workbook? or you are just opening another workbook? or your `app` (again not sure if you are just calling your workbook an `app` or you've actually built an `app`) somehow imports the `other` workbook? I suspect the answer is somewhere in the middle – Zac Aug 10 '18 at 12:30
  • @Zac - Application.Workbooks.Add – Chris Melville Aug 10 '18 at 12:34
  • Not sure if this will help but found some links that might shed some more light on the subject: [1](https://www.myengineeringworld.net/2014/10/change-excel-icon-workbook-shortcut-vba.html), [2](https://stackoverflow.com/questions/33568766/how-to-change-excel-taskbar-icon) and [3](https://stackoverflow.com/questions/5748968/cannot-change-excel-icon-in-taskbar-with-vba) – Zac Aug 10 '18 at 13:03
  • If you use this code after `Application.Workbooks.Add` then you should try `ActiveWindow.hWnd` instead of `Application.hWnd`. – BrakNicku Aug 10 '18 at 13:03
  • @BrakNicku - hWnd is not a valid property of ActiveWindow. That returns an error. – Chris Melville Aug 10 '18 at 13:44
  • Window objects do have [Hwnd property](https://msdn.microsoft.com/en-us/vba/excel-vba/articles/window-hwnd-property-excel) - tested in 2013. – BrakNicku Aug 10 '18 at 13:53
  • @BrakNicku - Good to know about 2013. I'm on 2007, so it's good to know there's a compatibility issue if one uses that property. – Chris Melville Aug 10 '18 at 13:59

1 Answers1

1

When you start Excel it uses an application icon enter image description here.

It uses it until you create any workbook beside the one initially created by Excel. Then it explodes workbooks on a taskbar and you get two buttons with a workbook icons enter image description here.

Even if you close second workbook, the first still uses a workbook icon. When you close all workbooks, it will revert to the application icon (you can check it by calling your SetupIcon and closing all workbooks), but after creating any workbook it switches back to the workbook icon.

You should try to enumerate all workbook windows and change icons also for them.

I'm not sure if this can be done directly in VBA, but you could use winapi functions FindWindowEx, EnumChildWindows, GetWindow.

Main Excel window has class name XLMAIN. It contains XLDESK which contains workbooks (EXCEL7) and other children. Use Spy++ to inspect hierarchy.

This behavior probably depends on taskbar settings and available space. If taskbar doesn't explode buttons it will show application icon.


Checked it and unfortunatelly it doesn't work. It changes icons of workbook windows (when not maximized), but icons on taskbar stay te same.


This works, but it is a little hackish. I'm using hardcoded class name MS-SDIb. This is implementation detail of Excel 2007 and may not work in other versions.

'Doesn't work for me
'Public Const strIcon As String = "%SystemRoot%\system32\SHELL32.dll" ' Icon file
Public Const strIcon As String = "C:\Windows\system32\SHELL32.dll" ' Icon file

Public Const IconIndex As Long = 137

Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal ClassName As String, ByVal WindowName As String) As Long
Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWndParent As Long, ByVal hWndChildAfter As Long, ByVal lpszClassName As String, ByVal lpszCaption As String) As Long
' For 64 bit may need replacing with SetClassLongPtr
Declare Function SetClassLong Lib "user32" Alias "SetClassLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Const GCL_HICON As Long = -14
Const GCL_HICONSM As Long = -34
Declare Function GetWindowThreadProcessId Lib "user32" (ByVal hwnd As Long, lpdwProcessId As Long) As Long

Public Declare Function SendMessageA Lib "user32" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Integer, ByVal lParam As Long) As Long
Public Declare Function ExtractIconA Lib "shell32.dll" (ByVal hInst As Long, ByVal lpszExeFileName As String, ByVal nIconIndex As Long) As Long
Public Const ICON_SMALL As Long = 0&
Public Const ICON_BIG As Long = 1&
Public Const WM_SETICON As Long = &H80


Sub SetupIcon()
    SetIcon strIcon, IconIndex
End Sub

Sub SetIcon(FileName As String, Optional index As Long = 0)
    #If VBA7 And Win64 Then
        ' 64 bit Excel
        Dim hwnd As LongPtr
        Dim DeskHWnd As LongPtr
        Dim Workbook As LongPtr
        Dim HIcon As LongPtr
    #Else
        ' 32 bit Excel
        Dim hwnd As Long
        Dim DeskHWnd As Long
        Dim Workbook As Long
        Dim HIcon As Long
    #End If
    Dim ThreadId As Long
    Dim n As Long
    Dim s As String
    If Dir(FileName, vbNormal) = vbNullString Then
        ' file not found, get out
        Exit Sub
    End If
    ' get the extension of the file.
    n = InStrRev(FileName, ".")
    s = LCase(Mid(FileName, n + 1))
    ' ensure we have a valid file type
    Select Case s
        Case "exe", "ico", "dll"
            ' OK
        Case Else
            ' invalid file type
            Err.Raise 5
    End Select
    hwnd = Application.hwnd
    If hwnd = 0 Then
        Exit Sub
    End If
    ThreadId = GetWindowThreadProcessId(hwnd, ByVal 0&)
    DeskHWnd = FindWindowEx(hwnd, 0, "XLDESK", vbNullString)
    If DeskHWnd = 0 Then
        Exit Sub
    End If

    HIcon = ExtractIconA(0, FileName, index)
    If HIcon = 0 Then
        Exit Sub
    End If

    SendMessageA hwnd, WM_SETICON, ICON_SMALL, HIcon
    SendMessageA hwnd, WM_SETICON, ICON_BIG, HIcon
    ' For 64 bit may need replacing with SetClassLongPtr
    SetClassLong hwnd, GCL_HICON, HIcon
    SetClassLong hwnd, GCL_HICONSM, HIcon

    WorkbookHWnd = FindWindowEx(DeskHWnd, 0, "EXCEL7", vbNullString)
    Do While WorkbookHWnd <> 0
        SendMessageA WorkbookHWnd, WM_SETICON, ICON_SMALL, HIcon
        SendMessageA WorkbookHWnd, WM_SETICON, ICON_BIG, HIcon

        WorkbookHWnd = FindWindowEx(DeskHWnd, WorkbookHWnd, "EXCEL7", vbNullString)
    Loop
    SetClassLong WorkbookHWnd, GCL_HICON, HIcon
    SetClassLong WorkbookHWnd, GCL_HICONSM, HIcon

    WorkbookHWnd = FindWindowEx(0, 0, "MS-SDIb", vbNullString)
    Do While WorkbookHWnd <> 0
        ' Check if WorkbookHWnd was created by same thread as Application.hwnd
        If ThreadId = GetWindowThreadProcessId(WorkbookHWnd, ByVal 0&) Then
            SendMessageA WorkbookHWnd, WM_SETICON, ICON_SMALL, HIcon
            SendMessageA WorkbookHWnd, WM_SETICON, ICON_BIG, HIcon
            SetClassLong WorkbookHWnd, GCL_HICON, HIcon
            SetClassLong WorkbookHWnd, GCL_HICONSM, HIcon
        End If

        WorkbookHWnd = FindWindowEx(0, WorkbookHWnd, "MS-SDIb", vbNullString)
    Loop
End Sub

Works even for new workbooks thanks to changing class icon with SetClassLong.

BUG: Each call leaks icon returned by ExtractIconA.

Daniel Sęk
  • 2,504
  • 1
  • 8
  • 17
  • Thanks. Sorry for the delay in reply. I tested it, and it seems to be working - however could you please clarify the exact nature of the bug you alluded to? Is it a memory leak? Something that could crash the app? I need to be sure, before I incorporate this code, that it won't cause problems. Thanks. – Chris Melville Aug 16 '18 at 15:08
  • For `ExtractIcon` you need to call `DestroyIcon`, but you souldn't call `DestroyIcon` when it is used by windows. If you change icons only once you can leave it as is (system will destroy icon when process exits), but if you call it many times, you will run out of resource handles (they are limited per process). – Daniel Sęk Aug 16 '18 at 15:29
  • Question: when Excel quits, will it release all the handles? How many handles are there available at once? – Chris Melville Aug 16 '18 at 17:59
  • When process (eg Excel) quits, system will free all not freed handles. There is no problem if you change icons only once. If you plan to change them many times, after setting new icon call `DestroyIcon` for old icon (you can keep it in global variable and destroy it at the end of `SetIcon`). Limit is about 10000. [https://blogs.technet.microsoft.com/markrussinovich/2010/02/24/pushing-the-limits-of-windows-user-and-gdi-objects-part-1/](https://blogs.technet.microsoft.com/markrussinovich/2010/02/24/pushing-the-limits-of-windows-user-and-gdi-objects-part-1/). – Daniel Sęk Aug 16 '18 at 18:09
  • When you say "destroy icon" what does this mean exactly? I know virtually nothing about API. What exactly is being destroyed? Maybe you could edit your answer to include the destroy code for old settings? I only plan to use one icon - but I may have to apply the procedure several times when new workbooks are opened: i.e every time workbooks.add is executed, I will need to call this to ensure that the taskbar icon stays customised. It won't be 10,000 times though :) – Chris Melville Aug 16 '18 at 18:19
  • `DestroyIcon` is API call. In [`ExtractIcon` documentation](https://learn.microsoft.com/pl-pl/windows/desktop/api/shellapi/nf-shellapi-extracticona) remark section states "When it is no longer needed, you must destroy the icon handle returned by ExtractIcon by calling the DestroyIcon function.". [`DestroyIcon` documentation](https://learn.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-destroyicon). New workbooks get new icon automatically, you dont have to call this procedure more than once. If you plan to use only one icon separate loading an setting... – Daniel Sęk Aug 16 '18 at 18:47
  • ... load it and store it somewhere. In `SetIcon` just reuse earlier loaded icon. – Daniel Sęk Aug 16 '18 at 18:48