Taming the Windows taskbar for LabVIEW

This article was posted on June 26, 2012 on lavag.org and has since disappeared from the net, after lavag.org upgraded and lost the blog functionality.

Note: The current VI library can be found here.

Prompted by a post here on Lavag.org several days ago, I started to dig into the possibilities to control the Windows 7 Taskbar interface from a LabVIEW application. This Taskbar is in fact a feature that has evolved over time from various concepts such as the Windows start menu, the Quick Launch bar, the Shell Notification area to the current Windows 7 taskbar, that combines some of the mentioned features with newer features added in the Vista and Windows 7 release.

A quick search showed on the NI site a little LabVIEW library that used the progress bar functionality in the taskbar buttons. This utility was based on a .Net component to access the Windows taskbar API. This is not really ideal, since the Windows taskbar API is in fact unmanaged, and incorporating a .Net intermediate library makes the whole solution somewhat heavy weight. More importantly, the taskbar API is based on Windows COM, a technology that builds on top of OLE and is the basis of ActiveX. But COM is only a building block of ActiveX and not equal to ActiveX, so there is no way to use the ActiveX functionality in LabVIEW for accessing this API. The involvement of COM however adds extra obstacles, since COM is by default using an apartment threading model, which is not exactly single threaded, but for the not so technically versed reader, it can be mostly seen as single threaded component. It does support access from multiple threads, but at a horrible cost, that is mostly invisible to a casual user.

This complication gets especially important, if one wants to use some of the other features of the taskbar that require more complex data interfaces than just sending scalar integers to the taskbar manager.

One example would be thumb-buttons, which can be used to allow control of some application operations through small little buttons underneath the thumbnail preview. The probably best known example for this would be some media players, such as the Windows Media Player shown below.

But in order for this, the component managing the taskbar has to be able to send messages to the application, whose taskbar button is shown. So we need to be able to let the taskbar manager message into LabVIEW, but in a way that lets us react to those messages in our own LabVIEW application. A .Net interface could use .Net events that get mapped to callback VIs. This would be a relatively elegant solution, but above mentioned COM limitations make this a little difficult to handle.

Since I have a lot more experience with interfacing to Windows APIs through DLLs, than with .Net, I decided to take a look what would be needed for this. The messaging from the taskbar manager to a LabVIEW program can be solved relatively easily by using user events and calling the documented PostLVUserEvent() C function from the external code. The taskbar manager sends Windows messages to the application in question (here LabVIEW) so we need to install also a message filter hook that intercepts those messages and translates them into the desired user event.

Everything seemed fairly straightforward at first after reading the documentation for ITaskbarList3 on MSDN. And my first attempts at controlling the progress bar functionality seemed very promising. Below code is all that is needed to access the method to set the progress state of a taskbar button.

MgErr SetProgressState(HWND hwnd, uInt32 state)
  ITaskbarList3 *ptbl;
  HRESULT hr = CoCreateInstance(&CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, &IID_ITaskbarList3, &ptbl);
  if (SUCCEEDED(hr))
    hr = ITaskbarList3_HrInit(ptbl);
    if (SUCCEEDED(hr))
      hr = ITaskbarList3_SetProgressState(ptbl, hwnd, state);
  return hr;

For anyone wondering here, yes above code is standard C code and while COM objects are in fact using OOP techniques, their ABI is defined in such a way that they stay independent of the used C++ compiler, in order to allow calling of COM interfaces from code created with different compilers than what was used to create the COM object. And the Windows headers also define a standard C interface mechanism for most COM interfaces. Compiling this code into a DLL and calling it correctly with the Call Library Node works like a charm.

So this initial and quick success asked of course for more and I started to look into adding thumbbar buttons. This requires to first send a list of images to the taskbar manager, to be used for the visuals of the buttons. This is quite a bit more involved, since it basically requires some form of bitmap and creating the code to do this translation is quite cumbersome. I took a few shortcuts here in the beginning but immediately seemed to run into a road block. Even with the most trivial code of just loading an existing bitmap into an imagelist and passing this to the taskbar API function ITaskbarList3::ThumbBarSetImageList(), this always returned with a generic failure. However a quick C test program doing exactly the same worked flawlessly. Some googling showed that the image list is in fact implemented in the Windows Common Controls library. There exist two different versions that are very different in nature. The old 5.x version is a simpler version not supporting any theming, while the newer 6.x version implements theming support. Supposedly the Windows taskbar uses of course the newer version, but checking the LabVIEW executable showed that it also uses this version, yet it seemed my DLL was using the older version and the Windows taskbar manager was failing to recognize my imagelist because it used a different internal implementation. But no magic seemed to help to make the imagelist created in my DLL to be recognized by the taskbar manager. Several hours later and after having stepped through lots of assembly code in debug mode, I suddenly realized that the imagelist that was passed to the taskbar manager, was not at all implemented in the memory area that was used by the Common Controls library but rather in the Remote Procedure Call library. How could that be?

Suddenly the fog started to clear up and I remembered various bits about COM apartment threading. Basically any Windows application wanting to use COM functionality has to call CoInitialize() before trying to access any COM object. I knew that LabVIEW was doing that early on during initialization, as otherwise the whole ActiveX interface inside LabVIEW would be simply impossible. However each COM component can specify in the registry if it is apartment, or free threading. Apartment threading means that the component needs to be always called from the same thread, while a free threading component can deal with being called from any thread. Most COM components only support apartment threading, and this includes most Windows COM components and in fact just about any ActiceX component out there. So LabVIEW calls CoInitialize() early on during initialization but of course in the UI thread. And if our DLL then calls CoCreateInstance() from a different thread, Windows correctly determines that this would violate the apartment threading contract for the ITaskbarList object and instantiates an intermediate marshaling layer that is using the RPC library. This marshaling layer basically translates the entire object and all method parameters into a stream of binary data, that can be transmitted through memory streams or even network sockets in the case of remote invocation through DCOM (Distributed COM). The serialized stream is then sent to an RPC daemon that hooks into the application message queue, and sends the data to the server (here the taskbar manager) whenever the application is retrieving messages from the Windows message queue. The same happens in reverse order for any return parameters the server sends to the client. This message queue hooking is also the reason that you can end up with deadlocked applications when using ActiveX and not being very careful. If there is any marshaling involved in the execution of the COM/ActiveX object, this will only work if the application in question is still servicing the Windows event message queue, by regularly calling GetMessage() to retrieve new messages from the OS. But if you happen to lock out that loop in your application because you run the marshaled code execution in the same thread, a classical dead lock occurs.
This marshaling seems to use the old Common Control 5.x format for the Image-lists, and the taskbar manager expecting 6.x image-lists simply fails when it tries to verify the image-list object before accessing its content. I’m not sure there is any way to make the RPC serialization in COM to use Common Control 6.x image-lists, but this was not really necessary, once I realized what the problem was. Setting the Call Library Node, that was calling my DLL function, to run in the UI Thread was all that was really necessary. Since now the CoCreateInstance() was executed in the same thread that LabVIEW had called CoInitialize() earlier on, the whole marshaling was left out and the image-list that I had created got passed directly to the taskbar manager and the functions started to simply work.

There was a bit more work to be done in converting the LabVIEW Pixmap data structure into a Windows bitmap, which could be properly used as image-list source. Bitmaps are tricky to handle and even trickier to translate between different formats, but a bit of trial and error eventually resolved that too. I chose to use Pixmaps instead of a file path to a bitmap file, because the Windows API for image-lists only supports Windows BMP bitmaps, and initial tests had shown, that it was a bit difficult to get the necessary 32 bit bitmaps for the required transparency to show without any artifacts. Allowing to use LabVIEW pixmaps instead, one can easily load 32 bit PNG files with alpha channel, but it’s possible to use JPG or BMP image sources as well with the according LabVIEW VIs.

And here is the current result of this work:

Tree of VIs

Thumbbar functionality

Leave a Reply

Your email address will not be published. Required fields are marked *