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 :

Taskbar window structure

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 :

function TaskmanWindow: HWND;
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

var
 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

var
 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 :

procedure SetTBButtonTitle(ButtonIndex:integer; NewTitle:string);
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 :

procedure MoveTBButton(FromIndex,ToIndex:integer);
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.

procedure SetTBButtonBitmap(ButtonCommand:integer;IconFile:string);
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 :

anIcon:=SendMessage(WindowHandle, WM_GETICON, ICON_BIG, 0);
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 :

Taskbar group structure

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 :

Procedure UpdateToolbarParent;
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 :

40 Responses to “Manipulating Taskbar Buttons”

  1. Noitidart says:

    I’m a Firefox Add-on developer. And when users run Firefox profiles simultaneously they are all grouped into one group on taskbar. This document is so helpful. I’m trying to make it so profiles have separate icons and also make each profiles windows group into a separate group.

    Firefox has a ton of WinXP users that’s why I’m doing this. So thanks for this man.

  2. Noitidart says:

    Hey man, I was wondering if you (the author) of this article are still active. I needed some help please.

    In Firefox, if you run multiple profiles at same time, all windows are grouped into one. In image below I have profile named “Clean” (has the blue badge on bottom right of icon) and one named “Main” (has the red youtube badge on bottom right of icon).

    Image here:
    http://img.photobucket.com/albums/v135/noitidart/winxp-badging-progress_zps1f5d6661.png

    So see in the task bar, I want to make two groups, I want to split them by their window class.

    I got the window handle from the dwData of each button and then I got the window class with GetClassName. But I was having some difficulty moving windows out of the default group to a second group. Can you please elaborate on moving windows out of a group and into another.

    Thanks
    Noit

  3. Jānis Elsts says:

    I’m still active, but it’s been years since I’ve done anything with the XP taskbar. These days all my systems run Win7 or some variety of Linux.

    Lets see if I can find my notes…

  4. Noitidart says:

    Thank you Janis!!! I was literally just on the contact page about to email because I saw the last blog post was March 2k13 thank you very much for your update and efforts! 🙂

  5. Jānis Elsts says:

    Based on some of my old source code, there’s a lot of special cases, but if the target group already exists, it looks like you could just:

    1. Send TB_HIDEBUTTON, ButtonCommand, true to show the button.
    2. Send TB_MOVEBUTTON, ButtonIndex, TargetGroupIndex to actually move it. You can get the index with TB_COMMANDTOINDEX.

    If you need to make a new group for the moved window, that appears to be considerably more complicated. Here’s some old code I found:

    function TurnButtonIntoGroup(ButtonCommand:longint; const GroupTitle:string):boolean;
    var
     rez, went, SourceGroup, SourceGroupCommand, ButtonIndex:longint;
     aButton:TTBBUTTON;
     HaveBehind:boolean;
    begin
     result:=false;
     ButtonIndex:=CallWindowProc(OldWndProc,hToolbar,TB_COMMANDTOINDEX,
                       ButtonCommand,0);
     if ButtonIndex<0 then exit;
    
     //Check if there's another button after this one.
     fillchar(aButton,sizeof(aButton),0);
     rez:=CallWindowProc(OldWndProc,hToolbar,TB_GETBUTTON,ButtonIndex+1,integer(@aButton));
     HaveBehind:=(rez<>0) and (not HasFlag(aButton.fsStyle,BTNS_DROPDOWN));
    
     went:=0;
     SourceGroup:=ButtonIndex;
     SourceGroupCommand:=0;
     repeat
       rez:=CallWindowProc(OldWndProc,hToolbar,TB_GETBUTTON,ButtonIndex-went,integer(@aButton));
       inc(went);
     until HasFlag(aButton.fsStyle,BTNS_DROPDOWN) or (rez=0);
     if rez<>0 then begin
       SourceGroup:=ButtonIndex-went+1;
       SourceGroupCommand:=aButton.idCommand;
     end;
    
     if HaveBehind then begin
       //Make  a hidden group for the next button.
       CopyTBGroup(SourceGroup,ButtonIndex+1);
     end;
    
     if SourceGroup<ButtonIndex-1 then begin
       //Make a new hidden group before this.
       SourceGroupCommand:=CopyTBGroup(SourceGroup,ButtonIndex);
       SourceGroup:=ButtonIndex;
     end;
    
     SetTBButtonTitle(SourceGroup,GroupTitle);
     inc(MyLastCommand);
     SetTBGroupFile(SourceGroup,'Custom Group '+inttostr(MyLastCommand));
     SetTBButtonBitmap(SourceGroupCommand,AppFolder+'someicon.ico');
     CallWindowProc(OldWndProc,hToolbar,TB_HIDEBUTTON,SourceButtonCommand,integer(true));
     CallWindowProc(OldWndProc,hToolbar,TB_HIDEBUTTON,SourceGroupCommand,integer(false));
     UpdateToolbarParent;
    end;
    
  6. Jānis Elsts says:

    And here’s the function that copies groups:

    function CopyTBGroup(FromIndex,ToIndex:integer):integer;
    var
     aButton:TTBBUTTON;
     StrBuf:PWideChar;
     StrBufA:PAnsiChar;
     aString:string;
     OldSize:cardinal;
    begin
     inc(MyLastCommand);
     if MyLastCommand>=MAXINT-10 then
      MyLastCommand:=CommandOffset; //That's probably fine. Probably.
    
     OldSize:=CallWindowProc(OldWndProc,hToolbar,TB_GETBUTTONSIZE,0,0); 
     CallWindowProc(OldWndProc,hToolbar,TB_GETBUTTON,FromIndex,integer(@aButton));
     aButton.idCommand:=MyLastCommand;
     aButton.fsState := TBSTATE_ENABLED or TBSTATE_HIDDEN;
    
     //Copy the internal data.
     StrBuf:= VirtualAlloc(nil,sizeof(TDataArray),MEM_COMMIT,PAGE_READWRITE);
     Move(pointer(aButton.dwData)^,StrBuf^,sizeof(TDataArray));
     aButton.dwData:=integer(StrBuf);
    
     //Copy filename.
     StrBuf:= VirtualAlloc(nil,Max_Path*2+1,MEM_COMMIT,PAGE_READWRITE);
     Move(pointer(PDataArray(aButton.dwData)^[5])^,
          StrBuf^,
          Max_Path*2+1);
     PDataArray(aButton.dwData)^[5]:=cardinal(StrBuf);
    
     //Copy button name.
     StrBuf:=PWideChar(aButton.iString);
     aString:=StrBuf;
     StrBufA:=VirtualAlloc(nil,length(aString)+1,MEM_COMMIT,PAGE_READWRITE);
     StrPCopy(StrBufA,aString);
     aButton.iString:=integer(StrBufA);
    
     //Insert a button.
     CallWindowProc(OldWndProc,hToolbar,TB_SETBUTTONSIZE,0,OldSize);
     If ToIndex<SendMessage(hToolbar,TB_BUTTONCOUNT,0,0) then begin
      //Insert before the specified index.
      CallWindowProc(OldWndProc,hToolbar,TB_INSERTBUTTON,ToIndex,integer(@aButton));
     end else begin
      //Just add it as the last button.
      CallWindowProc(OldWndProc,hToolbar,TB_ADDBUTTONS,1,integer(@aButton));
     end;
    end;
    
  7. Noitidart says:

    Thank you Janis! I will try that out and let you know how it goes.

    Just one thing: Everytime a new window opens I’ll have to do this check and move things around programtically right? It’s not like SetClassLongPtr to set the icon of a window class and whenever new windows open it auotmatically takes the icon.

    The other thing is, I was doing support for Win7, Mac OS, and Linux. So I was doing Win7 at the same time and seperating processes was easy. But I’m stuck on setting the icon of a taskbar group when it is unpinned and keeping it same when it is pinned.

    Here is an image of me using SetClassLongPtr to set icons of all windows in Win7:
    http://i.stack.imgur.com/2RNsD.png

    So the taskbar group in that picture is not pinned. If I right click on it, then pin it, then right click again and unpin it, THEN it takes the icon of all windows. But if I right click which opens jump list it resets the icon, or if I pin it again it resets the icon. Do you have any ideas on that?

  8. Jānis Elsts says:

    Everytime a new window opens I’ll have to do this check and move things around programtically right?

    Probably. Windows can’t possibly know how your custom grouping logic works, so it’s pretty unlikely that it would be able to apply it automatically.

    But if I right click which opens jump list it resets the icon, or if I pin it again it resets the icon. Do you have any ideas on that?

    I don’t know anything about the Win7 taskbar. Perhaps this SO thread will help?
    http://stackoverflow.com/questions/969033/change-pinned-taskbar-icon-windows-7

  9. Noitidart says:

    Thanks for the SO topic, I had no luck with that. 🙁
    But no problem thanks again very much for all your hard work.
    Noit

  10. Noitidart says:

    Hi man what is the value of OldWndProc in TurnButtonIntoGroup:

    buttonIndex:=CallWindowProc(OldWndProc,hToolbar,TB_COMMANDTOINDEX, ButtonCommand,0);

    It seems to me its some global var?

  11. Jānis Elsts says:
    hToolbar := FindWindowEx( TaskmanWindow, 0, 'ToolbarWindow32' , nil );
    //...
    OldWndProc := pointer(
        SetWindowLong(hToolbar, GWL_WNDPROC, integer(@MyWindowProc)));
    

    Basically, those code examples I posted are from a DLL that’s injected into the Explorer process. OldWndProc is the original window procedure of the taskbar window, which the DLL replaces with its own.

  12. Noitidart says:

    Ah thank you very much for your continued support of my questions. I seriously appreciate it! 🙂

  13. Noitidart says:

    I’m actually doing the ReadProcessMemory/WriteProcessMemory because I’m doing it via js-ctypes. Is there anywhere I can find all the stuff that your DLL does just in case I get stuck again and wondering like what the heck to do haha.

    Here’s my js-ctypes work: https://gist.github.com/Noitidart/1fc56b28be82d45a245d

    Can copy and paste that into Firefox scratchpad with environemnt set to browser 🙂

  14. Jānis Elsts says:

    The DLL was part of an old commercial product of mine. It is not available anywhere. Frankly, I doubt you’d find it very useful – the code quality is pretty bad, and all of the comments are in Latvian, not English. I cleaned up the parts I posted here a bit, but doing that for the entire project would take a while.

  15. Noitidart says:

    Oh shoot! I definitely don’t do Latvian haha. Ok no problem thanks though 🙂

  16. Noitidart says:

    Hi again, this should be last set of questions. Forgive the continous bother.

    They are about some constnats/vars.
    * With regards to `TurnButtonIntoGroup`
    1) What is MyLastCommand initially? It seems like a global was set somewhere.
    2) Can you please share the SetTBGroupFile function.

    * With regards to `CopyTBGroup`
    1) What is value of `MAXINT`?
    2) What is `CommandOffset`? Seems like global, it’s used here: `MyLastCommand:=CommandOffset; //That’s probably fine. Probably.`
    3) What is `TDataArray`?
    4) What is `Max_Path`? Used here –> `StrBuf:= VirtualAlloc(nil,Max_Path*2+1,MEM_COMMIT,PAGE_READWRITE);`

  17. Jānis Elsts says:

    What is MyLastCommand initially? It seems like a global was set somewhere.

    The DLL makes new buttons (groups), and each button needs a unique command. It’s just an incrementing integer that gets initialized to 1000.

    Can you please share the SetTBGroupFile function.

    procedure SetTBGroupFile(GroupIndex:integer;const NewFile:String);
    var
     aButton:TTBBUTTON;
     rez:integer;
     StrBuf:PWideChar;
    begin
     rez:=CallWindowProc(OldWndProc,hToolbar,TB_GETBUTTON,GroupIndex,integer(@aButton));
     if rez=0 then exit;
     StrBuf:=VirtualALloc(nil,length(NewFile)*2+2,MEM_COMMIT,PAGE_READWRITE);
     StringToWideChar(NewFile,StrBuf,Length(NewFile));
     PDataArray(aButton.dwData)^[5]:=cardinal(StrBuf);
    end;
    

    What is value of `MAXINT`

    As the name implies, it’s the maximum value of an int(-eger).

    What is `CommandOffset`?

    The initial, semi-arbitrary command ID for new buttons. It needs to be big enough not to collide with any built-in commands. As I mentioned, I used 1000.

    What is `TDataArray`?

    TDataArray=array[0..7] of cardinal;

    Cardinal = unsigned 32-bit integer.

    What is `Max_Path`?

    A platform-specific filename length limit. See: Maximum Path Length Limitation

    If you keep this up, I’ll need to start charging consulting fees 😛

  18. Noitidart says:

    Oh my gosh hhahahahahaha!! I didn’t post because I got worried of paying you fees! I wish I could show you how much I appreciate your support. I actually got real busy and couldn’t work on this till now. Hahaha I really apreciate all your answers man and so fast! I’m actually not a C++ guy, but javascript, I’m converting this to into js-ctypes for a firefox addon 🙂

  19. NettixCode says:

    is this work for new delphi 10 and windows 10 ? did you have update this for new delphi and windows ?

  20. Jānis Elsts says:

    It’s been a very long time since I’ve touched this code. I don’t have a copy of Delphi 10 to test it with, but I doubt it would still work.

Leave a Reply