To avoid any confusion, let me say that this article will talk about the taskbar buttons that represent the visible windows of currently running applications. I will not discuss the system tray or Quick Launch. Most information given here is applicable only to Windows XP and possibly newer versions of Windows (though I am not able to verify that). Windows 2000 has a different taskbar structure, and probably so do other – older – versions.
Most of the code examples are taken from my wsTaskborg utility.
This is a rather long article 😉
The relevant part of window structure looks like this :
The “Running Applications” window with ToolbarWindow32 class is a standard Toolbar control that is used to display taskbar buttons. You can read more about toolbar controls on MSDN.
To work with this toolbar you will need to retrieve its handle. You can do this by numerous FindWindowEx() calls, or by using an undocumented API function GetTaskmanWindow(). Like this :
TGetTaskmanWindow = function(): HWND; stdcall;
Result := 0;
hUser32 := GetModuleHandle(‘user32.dll’);
if (hUser32 > 0) then
@GetTaskmanWindow := GetProcAddress(hUser32, ‘GetTaskmanWindow’);
if Assigned(GetTaskmanWindow) then
Result := GetTaskmanWindow;
//Get the toolbar window
hToolbar := FindWindowEx(TaskmanWindow, 0, ‘ToolbarWindow32’, nil );
Note : most functions that access the toolbar will expect you to provide the data/buffers from the adress space of explorer.exe process. So you will need to either use WriteProcessMemory() & ReadProcessMemory() or inject you DLL into the target process. I will assume the latter, as it will notably simplify the task. The former approach is described in this article on CodeProject, which served as basis and inspiration for this post. I included a simple DLL-injection example in my Hiding from NT Task Manager article.
The toolbar buttons can be either visible or hidden. This mostly has to do with groups, which I’ll discuss later. For now, lets look at some basic functions and structures that you’ll need to work with these buttons.
Getting the button info
aButton:TTBBUTTON; //TBBUTTON if not Delphi
rez:=SendMessage(hToolbar, TB_GETBUTTON, ButtonIndex, integer(@aButton));
This will fill the aButton structure with some information about the button at position ButtonIndex. Indexes are zero-based. SendMessage will return zero if there is no button with that index.
The TBButton structure
This structure is described quite well in MSDN and SDK helpfiles, so I’ll focus on parts that are relevant to this particular case.
TTBBUTTON is a record-type structure that contains the following elements :
A zero-based index of a bitmap in an image list associted with the toolbar. I will discuss this in more detail in Icons, below.
A zero-based command identifier for the button. Explorer reuses old identifiers, so if you wish to assign your own identifier to a button, start with a large number to avoid conflicts (1000 should be fine). All buttons should have unique identifiers.
A collection of flags that describes the button’s state. All buttons have the TBSTATE_ENABLED flag. Hidden buttons also have the TBSTATE_HIDDEN flag. Visible buttons commonly have a TBSTATE_WRAP flag, and sometimes TBSTATE_ELLIPSES (indicates that button text is too long and the last part is replaced by “…”).
A collection of flags describing the style of button. All buttons have BTNS_NOPREFIX and BTNS_CHECK flags, meaning that they can be in “checked” and “unchecked” states. A button representing the currently active window has the TBSTATE_CHECKED state flag. Buttons that function as groups have BTNS_DROPDOWN and BTNS_WHOLEDROPDOWN flags, meaning that they display a dropdown menu when clicked.
bReserved: array[1..2] of Byte;
As far as I know, this isn’t used.
A pointer to an Unicode string that represents the button title. See below for a more convienent ways of retrieving and setting the title.
MSDN defines this as user-defined data. In this particular case it is a pointer to a structure described in the next paragraph…
The undocumented data structure
I used Delphi built-in debugger to look at where dwData was pointing and this is what I could decipher : dwData points to an undocumented array of seven dword/cardinal values. The values are such :
Data is the handle of a window associated with that button. This is always zero for groups.
Data is $40 for the button and the (in)visible group button of an active window. It is zero for all other buttons.
Data looks like a pointer to a structure of approximately 32 bytes. I didn’t find out what it was. Curiously the third dword of that structure is equal to Data…
Data – unknown. Not a pointer. May change during the existance of the button.
Data – unknown. Often equal to Data.
Data For groups it is a pointer to an Unicode string that contains the executable path of application that the grouped windows belong to. For example, if you have a group of Notepad windows, this will point to “C:\WINDOWS\NOTEPAD.EXE”. This element is always zero for all buttons that are not window groups.
Data appears to be a flag of some kind. It is zero for invisible groups, usually 3 for visible groups, 0, 5 or 8 for other buttons. It appears to be related to the type of application.
Usually you will only need Data and Data.
The returned value is the total number of buttons, including the invisible buttons.
Get the button title
TextLen:=255; //arbitrary value
TextLen:=SendMessage(hToolbar, TB_GETBUTTONTEXT, ButtonIndex, integer(aText));
Note that if you’re doing this from another process, you’ll ned to allocate the aText buffer in the remote process (explorer.exe)!
Set the button title
You can use something like this :
rez:=SendMessage(hToolbar, TB_GETBUTTON, ButtonIndex, integer(@aButton));
if rez=0 then exit;
StrBuf:=VirtualAlloc(nil, length(NewTitle)+1, MEM_COMMIT, PAGE_READWRITE);
//Don’t use AllocMem here!
aInfo.dwMask:=TBIF_TEXT or TBIF_BYINDEX;
SendMessage(hToolbar, TB_SETBUTTONINFO, ButtonIndex, integer(@aInfo));
//note that the old text isn’t freed here. I tried doing it, and it crashed Explorer.
This actually seems quite easy, as you can use TB_MOVEBUTTON message to move a button from one position to another :
SendMessage(hToolbar, TB_MOVEBUTTON, FromIndex, ToIndex);
However, if it is a single button, you must also move the button representing its group. If the button is a group, you must also move the invisible buttons representing the contents of the window group. You have to be very careful not to mess up the ordering of buttons, or some of them might disappear, become incorrectly grouped etc. See Groups below.
Setting a new icon
To set a new icon for a button, you will need to add this icon to the Image List that is associated with the toolbar (which will give you the index of the newly added image) and update the button information by a TB_SETBUTTONINFO call. MSDN tells us to use TB_ADDBITMAP to add a new bitmap, but that didn’t seem to work correctly for me (the icon gets added, but the index is wrong). So instead I used TB_GETIMAGELIST to retrieve the handle of the image list and added the icon with ImageList_Add. This worked okay.
aIcon:=LoadImage(0, PAnsiChar(IconFile), IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
if aIcon=0 then aIcon := LoadIcon(0, IDI_WINLOGO); //a “default” icon 😛
ImList:=SendMessage(hToolbar, TB_GETIMAGELIST, 0, 0);
ImageIndex := ImageList_AddIcon(imList, aIcon);
fillchar(aInfo, sizeof(aInfo), 0);
SendMessage(hToolbar, TB_SETBUTTONINFO, ButtonCommand, integer(@aInfo));
Retrieving an icon
I expect you could do it by retrieving the ImageList handle as show above and using
ImageList_GetIcon(). Another way is to use the window handle (see Data above) to get the icon associated with the window, but that doesn’t always work. For example :
if anIcon=0 then
anIcon := GetClassLong(WindowHandle, GCL_HICONSM);
Alternatively, you could use ExtractIcon() to get an icon for a group. The path to the executable is stored in Data.
Buttons with style BTNS_DROPDOWN represent groups. They also don’t have an associated window handle. They can be visible or invisible. These group buttons are created regardless of whether window grouping is on of off. The overall taskbar structure is like this :
Gray shapes represent invisible elements, green – visible buttons.
If window grouping is turned off, there is a separate invisible “group button” + a visible button for every top-level window. If grouping is on, there may be more than one button in a group (a “group” in this context is a button with BTNS_DROPDOWN style, followed by one or more buttons without that flag). Up until a given treshold these simple buttons are visible and the group button – invisible (this treshold is stored in registry at HKEY_CURRENT_USER \Software\ Microsoft\ Windows\ CurrentVersion\ Explorer\ Advanced\ TaskbarGroupSize). When there are TaskbarGroupSize windows of the same kind (belonging to the same application), they are grouped – the group button is made visible and the simple buttons following it are hidden. Apparently Explorer also somehow retrieves an icon suitable for the group at that moment, as invisible groups don’t initially have icons assigned.
To programmatically create a group you need to send a TB_ADDBUTTONS message (see MSDN for details), creating a button with the apropriate style, state and other values. You must also initialize the dwData member and the structure it points to. Then you can make this new button visible (use the TB_HIDEBUTTON message), move some simple buttons behind it and hide them to create a group. When doing this you must consider a lot of special cases, so that no buttons end up in wrong groups or states.
To ungroup a group of windows you can unhide all the simple buttons it contains and create a new, invisible group button (you could copy the original group button) right before each of them, so that every button has its own group.
Sometimes, if you do something that changes the number of visible buttons (hide some, create a new button, etc) the size of new buttons will be wrong. Explorer uses a nifty (and – obviously – undocumented) algorithm to resize the buttons, but this algorithm doesn’t get triggered when your application/DLL manipulates the taskbar. You can trigger by making Explorer think that the taskbar has been moved :
//get the parent of taskman window
if punkts1.X<0 then punkts1.X:=0;
if punkts1.y<0 then punkts1.y:=0;
//set it to exactly the same position and size
abs(aRect.Right – aRect.Left),
abs(arect.Bottom – aRect.Top),
SWP_NOOWNERZORDER or SWP_FRAMECHANGED); //these flags are important!
So… that’s about it. Thanks for reading and good luck 🙂Related posts :