Last Update: 2017 - 11 - 29
How to hide the Taskbar Button of a window. - An excursion to the raw COM API with VBA.
by Philipp Stiefel, originally published October 9th, 2017
Photo by Philipp Stiefel, 2017. All rights reserved.
A couple of weeks ago someone asked on UtterAccess how to hide the Taskbar button of the Access application window. While I still not fully understand the requirement behind this question, I was intrigued how to do that.
Initially, there were several hints to the Shell_NotifyIcon-API posted, but this is meant to manage icons in the Notification Area (“SysTray) of the Windows Taskbar. I used it for that purpose in my Access-SystraySample download. Unfortunately, this API is not suitable for the task at hand.
Here is an excerpt from my initial answer to this question on UtterAccess.
I second ADezii's assessment that it is going to be very hard to remove the icon from the taskbar.
The Shell_NotifyIcon-API does not apply to this situation, as it only deals with the notification area ("SysTray") of the taskbar, but not with "normal" taskbar buttons. [...]
I think this can only be achieved by calling the DeleteTab-Method on the ITaskbarList-Interface. And here we are, right at the very hard part.
The obvious question here should be: Why is this so very hard? - I’m somewhat surprised that nobody on the forum asked that.
Let’s start with a high-level view on the ITaskbarList interface I mentioned.
The ITaskbarList - COM Interface
ITaskbarList is one of many Windows Shell Interfaces. These are part of the Windows API, but other than the more commonly used Windows API functions, they are part of an object-oriented API. More precisely, they are Component Object Model (COM) Interfaces implemented by components of the Windows operation system.
If you want to use (call) any of the methods on those interfaces, you need an instance of an object that implements the interface you want to use.
In general, this is nothing special. We use objects and their methods all the time in Visual Basic. We either reference an object library and declare a variable as a type from that library or we invoke the CreateObject-Function and use the created instance of that object with late binding.
Both approaches require a registered ActiveX class library to work. - Bad news: With those Shell Interfaces, there is none.
These Shell Interfaces are primarily meant to be used with the C++ language. Using them from a VBA enabled application like Microsoft Access is not intended. Trying to do so regardless will be a tremendous challenge.
So, I dug out the bible of COM programming, Inside COM+ by Guy and Henry Eddon (affiliate link), and started to learn the basics of COM.
Create an object without CreateObject
Usually, we create ActiveX-objects with the CreateObject-Function. That is pretty easy. You only need to know the class name and the name of the library/application containing that class. Pass this as string in the format Libraryname.Classname to CreateObject and at once there is your object returned.
As mentioned, the first hurdle is the lack of an ActiveX library with an (VBA usable) automation server. The ITaskbarList interface is just a COM interface. COM is the foundation on which ActiveX (and BTW the .Net Framework too) is build.
ActiveX adds the sugar on top of COM that is required for it to be palatable to VBA. Without the ActiveX enhancements to COM, a raw COM object is not creatable from VBA. (It’s not at all usable directly, but more on that later.)
If there is just a COM component without an ActiveX object, you can still create an instance of that component using the CoCreateInstance Windows API function. To use this function you need the class id (CLSID) of the component instead of the class-/library-name.
COM components are referenced by their interface rather than by the identity of their Classes/Objects. Hence you need an interface id (IID) in addition to the class id to specify which interface the created object you want to use.
BTW: There is no secret magic necessary to get the required IIDs and CLSIDs. They are all publicly available in the C header files of the Windows SDK, downloadable from Microsoft. In this particular case, it’s the file Shobjidl.h.
With some naivety, I initially started creating an instance of the TaskbarList component with the interface of ITaskbarList. I was hoping to create the component and be able to use it right away with a couple of lines of code likes this.
Dim myTaskbarList As Object Dim CLSID As GUID Dim IIDITaskbarListGuid As GUID IIDFromString StrPtr(IID_ITASKBAR_LIST), IIDITaskbarListGuid CLSIDFromString StrPtr(CLSID_TASKBAR_LIST), CLSID CoCreateInstance CLSID, 0, CLSCTX_INPROC_SERVER, IIDITaskbarListGuid, myTaskbarList ' now myTaskbarList is an instance of the COM component/interface
That sort of worked. I got an instance of … something. Unfortunately, whenever I tried to invoke any method of this object, like typing myTaskbar.DeleteTab and running that code, Access/VBA instantly crashed.
I then changed my code to create the object and retrieve the interface IUnknown instead of ITaskbarList. IUnknown is mother of all COM interfaces. Every COM Component has to implement (or inherit) this interface by it very definition. - Still no luck. Calling the most basic and fundamental methods of the COM API, AddRef and ReleaseRef, still crashed Access/VBA.
The whole matter seemed to be even more complicated than initially thought.
Calling methods of an object that has no methods
After some (a lot actually) pondering and research I finally spotted a detail I missed before and began to understand the issue.
Invoking any method of that weird object returned from CoCreateInstance crashes VBA because, as far as VBA was concerned, that “object” has no methods. Even worse, for VBA it is not even a valid Object at all!
The Object type in VB(A) depends on the COM interface IDispatch. This interface provides the infrastructure to find the address of a method by its name. That needs to happen whenever we call any method of any Object type variable in VBA code. So, this interface is absolutely essential to use an Object instance in VBA programming.
The TaskbarList COM Component does not implement IDispatch. It only inherits IUnknown and implements its own interface ITaskbarList and nothing else.
There are two possible options to deal with this kind of situation. The common approach, at least outside the Access world, is to create an Interface Definition Language (IDL) file. This is a simple text file describing the interface. Here is a screenshot of the IDL for ITaskbarList.
The IDL file can then be compiled to a Type Library (TLB) that contains the necessary information about the methods of an Object for VBA. But it would require adding a reference to that TLB file and early binding the object variables to the TaskbarList type.
The above code would now look like this:
Dim myTaskbarList As TaskbarListTLB.TaskbarList Dim CLSID As GUID Dim IIDITaskbarListGuid As GUID IIDFromString StrPtr(IID_ITASKBAR_LIST), IIDITaskbarListGuid CLSIDFromString StrPtr(CLSID_TASKBAR_LIST), CLSID CoCreateInstance CLSID, 0, CLSCTX_INPROC_SERVER, IIDITaskbarListGuid, myTaskbarList ' now myTaskbarList is an instance of TaskbarListTLB.TaskbarList ' and it is actually a usable instance in VBA
This actually works. However, you would now need to distribute the type library file together with your Access application and make sure it is always properly referenced.
I very much dislike to reference external libraries in Access because it quite often creates issues with broken references that more or less cripple your whole application.
Calling raw COM API methods
There is another way to call the methods of a COM Component. The low-level DispCallFunc API function. This function directly invokes a method by its address in memory. The base address of the object instance is easily retrievable from the object variable with the ObjPtr-function in VBA. Now we only need to know the relative addresses of the methods of that object.
The methods of an object are listed in the Virtual Method Tables (vtable). The vtable itself is located at a fixed position for all COM objects. The offset of a function address can be derived from the order of the functions/methods in the vtable.
The problem here is that, in order to call a particular method of an object, we need to know its position in the vtable. This can be derived from the IDL or from the header files but it needs to be added to your VBA code manually in form of a constant or enum item.
If you are familiar with the Windows API, these should be the important bits to know before calling a method of a COM component with DispCallFunc. - However, there is one final part I myself do not fully understand yet.
All the arguments to the method you want to call need to be wrapped in Variant data type variables and there addresses to the variants passed to the DispCallFunc function in an array.
However, for some data types you wrap the address of the original variable in the Variant and for other types, you just use the variable directly. - It was only able to figure this out on a case by case basis for the methods I used in this sample by trial-and-error. I do not really understand the mechanisms involved here (yet) and therefore cannot explain them.
Putting it all together
Now, we got all the knowledge in place to put all the piece into place. I incorporate all the code in a class module to hide all the internal complexity. The Class_Initialize event procedure will set up all the COM infrastructure by …
Then the instance of our class is ready. We can now call any of the ITaskbarList methods, passing in the window handle of our application window or any form to add it to the taskbar or remove it from the taskbar.
It is the beauty of class modules that all that complexity is neatly hidden away. The code to hide a taskbar button are merely three simple lines now.
Public Sub TestTaskBarListDelete() Dim tb As TaskbarList Set tb = New TaskbarList tb.DeleteTab Application.hWndAccessApp End Sub
The usage of the other functions is similar and should be self-explaining.
You can download the class module here:
Update 2017-11-03 - Warning: With Access 2010 Access will crash when explicitly inspecting an Object that actually is only an IUnknown in the VBA-Debugger. In Access 2016 the Access will crash already if there is any variable of that type in scope where you put a break point. This code still fully works in Access 2016, but do not put a break point inside the TaskbarList class module!
Conclusion / Wrap up
While I am fully aware that I left out quite a lot of details, I still hope this article was an interesting and educating read. This type of raw COM programming is certainly much too tedious and error-prone to be used for everyday problems in your VBA projects. Nevertheless, I think these techniques can be a useful tool for some very special problems that cannot be solved otherwise.
Update 2017-11-03: I recently presented this topic on the AEK (Access Developer Conference). That presentation in German is available as video recording: COM API Programmierung mit VBA, AEK20-Abend, Hannover.
© 1999 - 2017 by Philipp Stiefel