Manipulating Taskbar Buttons
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 😉
Contents
The basics
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 :
type
TGetTaskmanWindow = function(): HWND; stdcall;
var
hUser32: THandle;
GetTaskmanWindow: TGetTaskmanWindow;
begin
Result := 0;
hUser32 := GetModuleHandle(‘user32.dll’);
if (hUser32 > 0) then
begin
@GetTaskmanWindow := GetProcAddress(hUser32, ‘GetTaskmanWindow’);
if Assigned(GetTaskmanWindow) then
begin
Result := GetTaskmanWindow;
end;
end;
end;
….
//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 buttons
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 :
iBitmap: Integer;
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.
idCommand: Integer;
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.
fsState: Byte;
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 “…”).
fsStyle: Byte;
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.
iString: Integer;
A pointer to an Unicode string that represents the button title. See below for a more convienent ways of retrieving and setting the title.
dwData: Longint;
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[0] is the handle of a window associated with that button. This is always zero for groups.
Data[1] is $40 for the button and the (in)visible group button of an active window. It is zero for all other buttons.
Data[2] 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[4]…
Data[3] – unknown. Not a pointer. May change during the existance of the button.
Data[4] – unknown. Often equal to Data[3].
Data[5] 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[6] 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[0] and Data[5].
Working with buttons
Counting buttons
Count:=SendMessage(hToolbar,TB_BUTTONCOUNT,0,0);
The returned value is the total number of buttons, including the invisible buttons.
Get the button title
aText:PAnsiChar;
TextLen:integer;
….
TextLen:=255; //arbitrary value
aText:=AllocMem(TextLen);
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 :
var
aButton:TTBBUTTON;
rez:integer;
StrBuf:PAnsiChar;
aInfo:TTBBUTTONINFO;
begin
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!
StrPCopy(StrBuf,NewTitle);
fillchar(aInfo, sizeof(aInfo),0);
aInfo.cbSize:=sizeof(aInfo);
aInfo.dwMask:=TBIF_TEXT or TBIF_BYINDEX;
aInfo.pszText:=StrBuf;
SendMessage(hToolbar, TB_SETBUTTONINFO, ButtonIndex, integer(@aInfo));
//note that the old text isn’t freed here. I tried doing it, and it crashed Explorer.
end;
Moving buttons
This actually seems quite easy, as you can use TB_MOVEBUTTON message to move a button from one position to another :
begin
SendMessage(hToolbar, TB_MOVEBUTTON, FromIndex, ToIndex);
end;
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.
var
aInfo:TTBBUTTONINFO;
imageIndex:integer;
ImList,aIcon:cardinal;
begin
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);
aInfo.cbSize:=sizeof(aInfo);
aInfo.dwMask:=TBIF_IMAGE;
aInfo.iImage:=imageIndex;
SendMessage(hToolbar, TB_SETBUTTONINFO, ButtonCommand, integer(@aInfo));
end;
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[0] 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[5].
Groups
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.
Other thoughts
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 :
var
aRect:TRect;
punkts1:TPoint;
aParent,aBar:cardinal;
begin
//get the parent of taskman window
aBar:=TaskmanWindow;
GetWindowRect(aBar, aRect);
aParent:=GetParent(aBar);
punkts1.X:=aRect.Left;
Punkts1.Y:=aRect.Top;
ScreenToClient(aParent, punkts1);
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
SetWindowPos(aBar, HWND_TOP,
punkts1.X, punkts1.Y,
abs(aRect.Right – aRect.Left),
abs(arect.Bottom – aRect.Top),
SWP_NOOWNERZORDER or SWP_FRAMECHANGED); //these flags are important!
end;
So… that’s about it. Thanks for reading and good luck 🙂
Related posts :
MadCHook.dll is identified as Win32:Trojan-gen {Other} by Avast 4.7
after running this program, madCHook.dll is locked and cannot be deleted until I kill it and restart EXPLORER.EXE.
Ugh, I hoped something like that wouldn’t happen. This is most likely a false positive. If you really want to delete the file (though I’m sure it’s completely harmless) you should uncheck the “Enable…” checkbox under “Options” and close the program (I think that should work).
madCHook.dll sometimes gets falsely identified as a virus/trojan, because it is used for API hooking/DLL injection – techniques that some viruses use, too.
See also :
http://www.madshi.net/madCodeHookDescription.htm
http://forum.madshi.net/viewtopic.php?t=147
http://forum.madshi.net/viewtopic.php?t=12
Thank you very much for such a helpful and nice written article.
excellent information
good writing too
Hi. I am having a hard time getting the icon. It seems wm_geticon does not ever work, and GetClassLong(wHnd, GCL.HICONSM); or GetClassLong(wHnd, GCL.HICON); does. But I noticed in a rare case (remote desktops input capture window) does not get the icon and it crashes. I tried every method you suggested but I cant seem to get the information I need. I tried using ImageList_GetIcon but it always returns 0. I was giving it a valid handle from TB_GETIMAGELIST. I then tried to use ExtractIcon but I was unable to get the path to the executable from data[5]. I tried to read all the bytes from dwdata but the only readable text I found was part of the buttons name. I’m probably doing something wrong so I was wondering if you could help. Btw I am using c# and the readprocessmemory method of code injection.
data[5] only contains the executable name if the button is a group.
It has been a while since I wrote this post so some specifics have faded from memory.
You could get *an icon* by finding the exe file for the process that created the window using the window handle and extracting the icon from the file. Read about how to find the exe here – http://www.cs.bgsu.edu/drhutch/otherinfo/appnamefromhwnd.html
That might not be the exact window’s icon but it’s better than nothing.
Thanks a ton. Your article has been invaluable to me.
+à+++à+Õ+à+Æ+à+Õ+àòÄÖ]+àòÀÝ+à+
Hi White Shadow,
Nice article! Do you have any sample code in VC++ to retrieve Button Title & Setting New Button Title. I am currently customizing a Microsoft Management Console and want to change a few Menu items.
Thanks in Advance!
Ramkey
Hey Ramkey,
Nope, sorry. I don’t usually program in VC++.
There might be something in MSDN that you could use.
typedef struct tagNMTOOLBAR {
NMHDR hdr;
//Command identifier of the button
//associated with the notification
int iItem;
TBBUTTON tbButton;
int cchText;
LPTSTR pszText;
RECT rcButton; //Version 5.80
} NMTOOLBAR, FAR* LPNMTOOLBAR;
My coworker found this while trying to prove me wrong about MS’ documentation on this subject. He succeeded in proving me wrong about the point I was trying to make, but at the same time proved the larger issue of MS documentation sucking right =P
This is the dwData structure in the TTBButtonInfo when talking about the taskbar, it says user defined because it really is…user defined. It can be any type of structure but by default it is an NMTOOLBAR for explorer.exe’s taskbar.
As a sidenote, please update this guide. It has been invalueable for me in writing an app I’m working on right now, and I’m sure it will be for anyone else.
Eh, I think I’d need to re-read and re-research it all to be able to update this article. I haven’t done anything of this kind for a long time, mostly focused on web programming now.
Hi
Can anybody supply me with a fulle working demo code project on grouping buttons of different applications on the Windows taskbar?
I’m using Delphi 6.
Thanks in advance
Elton
Probably not 😛
Hi,
I was searching for a long time to manipulate the system tray icons same like the above. Could you drop some references if you have?
If you want something you can use, Taskbar Shuffle looks like it could do the job. If you want to program your own application to move the tray icons, I don’t have any useful suggestions for you.
Hi, I was wondering how can u succeed 2 set a new ICON as u said, I ‘ve tried 2 do that use the same CODE, however, I failed! OR if u did this by “Inject dll”? or what else? Can u pls kindly give mi some more suggestions? 3Q!
I think I did it from the injected DLL, yes. It’s been quite a while since I wrote that code, so I don’t recall the specifics.
Very nice article! Thank you!
Im tryng to detect mouse clicks on applications taskbar btn. Some idea? Thank you
[…] […]