Click here to Skip to main content
Click here to Skip to main content

A tool to order the window buttons in your taskbar

By , 9 Aug 2005
 

Note: This tool only works on Windows XP and later!

Introduction

This tool allows you to alter the order of the window buttons in your taskbar.

Background

I normally have about 20 windows open, and switching between them is a lot quicker if they are in my "normal" order. For instance, I always like Outlook to be the first button in my taskbar, so I know where to click when I get mail. The problem is that when Outlook hangs, I have to kill its process. Then when I restart it, it appears at the end of the list, and is soon hidden among my other windows. With this tool, I can easily move Outlook back to the start where it belongs, without closing and re-opening all my other windows.

This problem is a pet peeve of mine - if the order of your windows doesn't bother you, then you don't need this tool :)

How it works

This tool uses a lot of PInvoke calls - if you want to see the details, look in the "Common/Kernel32.cs" and "Common/User32.cs" files in the source. It also uses an unsafe method (my first one), which made me feel a bit dirty :). I started to write it in C++, but after four years of C#, it was just too painful.

The basic idea is to get a list of the windows in the taskbar, show them and allow them to be reordered, and then apply the new ordering. Sounds easy :)

Getting a list of the windows

This proved to be the hardest part. Fortunately, I found a way to do it, but it only works on Windows XP and later. Windows 2000 uses a different window hierarchy, and I don't know (or care :) about earlier OSs.

Getting the buttons window handle

Using Spy++, I found that the buttons window in the taskbar is actually a ToolbarWindow32 - one of the common Windows controls. This was luck, as Microsoft could easily have used a custom window class. As it is, we know quite a lot about ToolbarWindow32. The first thing to do is get its window handle. We can do this using GetDesktopWindow and FindWindowEx:

IntPtr hDesktop = User32.GetDesktopWindow();
IntPtr hTray    = User32.FindWindowEx( hDesktop , 0, "Shell_TrayWnd"   , null );
IntPtr hReBar   = User32.FindWindowEx( hTray    , 0, "ReBarWindow32"   , null );
IntPtr hTask    = User32.FindWindowEx( hReBar   , 0, "MSTaskSwWClass"  , null );
IntPtr hToolbar = User32.FindWindowEx( hTask    , 0, "ToolbarWindow32" , null );

This just walks down the window hierarchy from the desktop window. We now have a handle on the ToolbarWindow32, and can start the fun stuff :)

Getting the number of buttons

This is easy, as we know about the TB_BUTTONCOUNT message. All we have to do is send this message using our window handle, and it will return the count:

UInt32 count = User32.SendMessage( _ToolbarWindowHandle, TB.BUTTONCOUNT, 0, 0 );

Note that the window count is more than we would expect. See Groups for an explanation.

Getting each button's info

This also should be easy, because we know about the TB_GETBUTTON message. It's a bit more complicated because we are accessing a ToolbarWindow32 from a different process, so we have to do a bit of memory management. See Cross-process memory access for details. It also uses an unsafe block, as we need to pass the addresses of structures as pointers. I will concentrate on the algorithm for now:

TBBUTTON tbButton;
TBBUTTON* ipRemoteBuffer = & tbButton; // unsafe
for ( int i = 0 ; i < count ; i++ )
{
    User32.SendMessage(
        hToolbar,
        TB.GETBUTTON,
        ( IntPtr ) i,
        ipRemoteBuffer );
}

Note that this is a pseudo-code - it doesn't compile, it just gives you an idea of the algorithm.

We now have a TBBUTTON structure filled with information for each button. If we look at the fsState field, we find that some of the buttons are hidden, and that there are the expected number of visible buttons. Lucky again :) Note that the TBBUTTON structure has a 32-bit dwData field that is for user data. This will come in handy later on...

Getting the button text

The TBBUTTON structure has a String field, but it's not that easy to use. It's easier to use a TB_GETBUTTONTEXT message to let the control do the work:

int chars = ( int ) User32.SendMessage(
    hToolbar,
    TB.GETBUTTONTEXTW,
    ( IntPtr ) tbButton.idCommand,
    ipRemoteBuffer );

Note that this message takes a CommandId, which we got from the TBBUTTON structure.

Getting the window handle

This was the really lucky part. I thought to myself: "Where would I keep the window handle?". They must keep it somewhere, to enable the activation of the correct window when a button is selected. The obvious place to keep it would be in a structure for each button, and the obvious place to keep the pointer to this structure would be in the dwData field of each TBBUTTON.

So I had a look at the dwData fields, and they appeared to be pointers. OK so far. Then I had a look at the memory they pointed to, and there they were: the first field stores the window handle :))) Microsoft developers aren't so different, after all :)

Using the window handle

The rest is easy. I used Thomas Caudal [^] 's TreeListView [^] which made the UI easy. A bit of glue to handle moving the items around, and then the "Apply" button.

All we have to do to apply the new order is to hide all the windows using ShowWindow, and then show them one at a time in the desired order. Easy peasy :)

Points of interest

A few things could do with a bit more explanation.

Groups

In XP and later, you have the option of grouping similar taskbar buttons together. This tool works with this switched on or off. In either case, Explorer adds a dummy button, which is hidden, at the start of each group. This is why TB_BUTTONCOUNT reports more buttons than are visible. These extra buttons are easy to identify, as they don't have a window handle.

Out of interest, the behaviour is controlled by a couple of registry entries:

  • "HKCU\Software\Microsoft\Windows\CurrentVersion \Explorer\Advanced\TaskbarGlomming" is 0 if grouping is switched off, and 1 if it is on.
  • "HKCU\Software\Microsoft\Windows\CurrentVersion \Explorer\Advanced\TaskbarGroupSize" is the minimum number of windows before they are collapsed into one button. I have this set to 99 to effectively turn it off. You can set this value with TweakUI from the XP PowerToys (it's probably safer).

Cross-process memory access

This was a tricky one. Some of the required Win32 functions use structures to move information around. This works well if you and your target are in the same process space, but falls apart in our case. The naive implementation doesn't work. If you declare a structure, and get a pointer to it to pass into your function, the pointer is to some address in your virtual memory space. The function can't know this, and thinks that it is pointing to a block of its virtual memory. This can only lead to General Protection Faults at best.

The solution is to allocate space for your structure in Explorer's virtual memory, call the function, and then copy the data back into your virtual memory so you can use it. This works without elevated privileges because Explorer runs under the local user account. As we use a few functions one after the other, I just allocate a page of memory, and use this for all the structures.

The basic algorithm is this:

  • use GetWindowThreadProcessId to get the process ID from a window handle.
  • use OpenProcess to turn the process ID into a process handle.
  • use VirtualAllocEx to allocate some memory in Explorer's virtual memory space.
  • call the required API function to fill the buffer.
  • use ReadProcessMemory to copy the buffer into our virtual memory space.
  • don't forget to use VirtualFreeEx to free the buffer, and CloseHandle to finish.

I will just show one call here, the others are similar:

private unsafe bool GetTBButton(
    IntPtr hToolbar,
    int i, // button id
    ref TBBUTTON tbButton,
    ... )
{
    // One page
    const int BUFFER_SIZE = 0x1000;

    byte[] localBuffer = new byte[ BUFFER_SIZE ];

    UInt32 processId = 0;
    UInt32 threadId =
        User32.GetWindowThreadProcessId(
            hToolbar,
            out processId );

    IntPtr hProcess =
        Kernel32.OpenProcess(
            ProcessRights.ALL_ACCESS,
            false,
            processId );

    if ( hProcess == IntPtr.Zero ) return false;

    IntPtr ipRemoteBuffer = Kernel32.VirtualAllocEx(
        hProcess,
        IntPtr.Zero,
        new UIntPtr( BUFFER_SIZE ),
        MemAllocationType.COMMIT,
        MemoryProtection.PAGE_READWRITE );

    if ( ipRemoteBuffer == IntPtr.Zero ) return false;

    // TBButton
    fixed ( TBBUTTON* pTBButton = & tbButton )
    {
        IntPtr ipTBButton = new IntPtr( pTBButton );

        int b = ( int ) User32.SendMessage(
            hToolbar,
            TB.GETBUTTON,
            ( IntPtr ) i,
            ipRemoteBuffer );

        if ( b == 0 ) { Debug.Assert( false ); return false; }

        Int32 dwBytesRead = 0;
        IntPtr ipBytesRead = new IntPtr( & dwBytesRead );

        bool b2 = Kernel32.ReadProcessMemory(
            hProcess,
            ipRemoteBuffer,
            ipTBButton,
            new UIntPtr( ( uint ) sizeof( TBBUTTON ) ),
            ipBytesRead );

        if ( ! b2 ) { Debug.Assert( false ); return false; }
    }
        
    ...

    Kernel32.VirtualFreeEx(
        hProcess,
        ipRemoteBuffer,
        UIntPtr.Zero,
        MemAllocationType.RELEASE );

    Kernel32.CloseHandle( hProcess );

    return true;
}

The unsafe keyword allows us to use pointers, and the fixed keyword locks the object in memory, so that the GC doesn't move it while we copy the data back to our virtual memory space. Apart from that, it's just a bit of P/Invoke. Check out the "Common\Kernel32.cs" and "Common\User32.cs" files in the source if you're interested.

Icons

This is just a bit of fluff. The TreeListView doesn't work properly without an ImageList, so I added one. Then I thought it would be good to show the default window icons to distinguish the items. My first attempt wasn't too successful. I used GetClassLong with the GCL_HICONSM parameter to get the small icon associated with the window class. This works for most applications, but not for my .NET apps. It seems that the framework generates a new window class each time an application runs, but it doesn't set the icon handle - nice :) These apps do, however, correctly return an icon handle when sent a WM_GETICON message, so that's what I did in the end. I also set the window class icon handle for this tool in the main form's OnHandleCreated override, but this isn't necessary.

Shameless plugs

I used the code from a couple of my previous articles in this project. I used my OSVersion [^] class to check that we're running on XP or later. And I used my TreeCollection [^] as a backing store. I also used the GlobalMemoryStatusEx API to display memory usage in the "OS Version" dialog - just for fun :)

Conclusion

Well, that's about it. If you just want to use this tool, that's fine - I hope you find it useful. If you want to dig a little deeper, then you can look at the code as an example of P/Invoke and cross-process memory access.

I just want to mention the pinvoke.net [^] wiki site - very handy and mostly accurate :)

History

  • 8th August 2005 - Version 2.
    • hIcon bug fixed.
    • changed icon.
  • 27th May 2005 - Version 1.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Nicholas Butler
United Kingdom United Kingdom
Member

I built my first computer, a Sinclair ZX80, on my 11th birthday in 1980.
In 1992, I completed my Computer Science degree and built my first PC.
I discovered C# and .NET 1.0 Beta 1 in late 2000 and loved them immediately.
I have been writing concurrent software professionally, using multi-processor machines, since 1995.
 
In real life, I have spent 3 years travelling abroad,
I have held a UK Private Pilots Licence for 20 years,
and I am a PADI Divemaster.
 
I now live near idyllic Bournemouth in England.
 
If you would like help with multithreading, please contact me via my website:
 
 
I can work 'virtually' anywhere!

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
SuggestionVista x64 not supportedmemberzpro14 Mar '13 - 2:32 
please add this to the supported OSs section, this will save time for those who try first and read later...
 
thanks
Questionadding textbox on top of the task barmemberprinctreme110 Sep '12 - 1:46 
Hi
how can i add textbox on top of the task bar using vb dot net in windows 7 os.
Please reply ..
GeneralExcellentmemberKnasenmc314 Jan '11 - 3:57 
Great article.
 
Hint: The GetTBButton method can be used to get the buttons in the System Tray as well. Since it is a ToolbarWindow32 as well, it was just copy and paste. To get the System Tray instead of the Toolbar, you use the following code:
 
IntPtr hShell = User32.FindWindowEx(IntPtr.Zero, IntPtr.Zero, "Shell_TrayWnd", null);
IntPtr hTray = User32.FindWindowEx(hShell, IntPtr.Zero, "TrayNotifyWnd", null);
IntPtr hPager = User32.FindWindowEx(hTray, IntPtr.Zero, "SysPager", null);
IntPtr hToolbar = User32.FindWindowEx(hPager, IntPtr.Zero, "ToolbarWindow32", null);
return hToolbar;
GeneralMy vote of 5memberchatravin3 Oct '10 - 8:24 
A real professional article,
thanks to Nicholas.
I've seen his other articles, one better than the other
GeneralWindows 7 ProblemsmemberDrWatsonCoding25 Aug '10 - 8:31 
In Windows 7, the TaskList was completely revamped. To my knowledge it is no longer a ToolbarWindow32.
The class is now "MSTaskListWClass". I believe that it no longer responds to any of the TB_ window messages.
Do you (or anyone reading) have any ideas about how to interact with this new taskbar? I am particularly interested in retrieving the RECT of the taskbar items...something which the ITaskList does not provide (to my knowledge).
 
Thanks!
GeneralNice but...membergaby_la_star25 Aug '09 - 23:01 
... your code is vastly undocumented. I know this doesn't bother some people but some others as me really can't read it.
 
I was looking at your code because I am searching a way to resize the taskbar buttons in the ToolbarWindow32, height and width. I know how to change the width using registry but not the height. If you have any idea, I'll be happy to know.
GeneralChange button TextmemberC470214 Dec '08 - 5:53 
Is it possible to change the text of the button?
GeneralThanks!!!memberMember 60385522 Aug '08 - 12:25 
Your article saved me time on my current project. This is wonderful article and I implemented it in VC++.
QuestionHide - Unhide aswell?memberStumproot25 Feb '08 - 0:03 
Would it be possible to add the possibility to temporarily hide a taskbar button?
AnswerRe: Hide - Unhide aswell?memberMember 60385522 Aug '08 - 12:24 
It should be possible by using TB_HIDEBUTTON in sendmessage API. I'm currently using that to hide buttons in systray for our project. The message can be tagged along with TRUE/FALSE to hide/unhide.
GeneralInserting button to windows explorer from a shell extension.memberMember 14341726 Dec '07 - 3:21 
Hi to insert toolbar button to the windows explorer i am using the following method.
The button is inserted properly, but the text is coming jung characters.
Any help is greatly appreciated.
 

TBBUTTON Button = new TBBUTTON();
Button.idCommand = 1000;
Button.fsState = (byte)TBSTATE.TBSTATE_ENABLED;
Button.fsStyle = (byte)(BTNS.BTNS_BUTTON | BTNS.BTNS_AUTOSIZE | BTNS.BTNS_SHOWTEXT);
Button.dwData = 0;
Button.iString = Marshal.StringToHGlobalUni("Mymenu");
Button.iBitmap = iBitmap;
bool Result = SetToolBarButton(toolbarhwnd, buttoncount, ref Button);
 
[StructLayout(LayoutKind.Sequential)]
internal struct TBBUTTON
{
public Int32 iBitmap;
public Int32 idCommand;
public byte fsState;
public byte fsStyle;
public byte bReserved1;
public byte bReserved2;
public UInt32 dwData;
public IntPtr iString;
}
 
private unsafe bool SetToolBarButton(IntPtr hToolbar, int index, ref TBBUTTON tbButton)
{
// One page
const int BUFFER_SIZE = 0x1000;
byte[] localBuffer = new byte[BUFFER_SIZE];
UInt32 processId = 0;
UInt32 threadId = Win32.GetWindowThreadProcessId(hToolbar, out processId);
IntPtr hProcess = Win32.OpenProcess(ProcessRights.ALL_ACCESS, false, processId);
if (hProcess == IntPtr.Zero)
{
Debug.Assert(false);
return false;
}
 
IntPtr ipRemoteBuffer = Win32.VirtualAllocEx(hProcess, IntPtr.Zero, new UIntPtr(BUFFER_SIZE), MemAllocationType.COMMIT, MemoryProtection.PAGE_READWRITE);
if (ipRemoteBuffer == IntPtr.Zero)
{
Debug.Assert(false);
return false;
}
 
fixed (TBBUTTON* pTBButton = &tbButton)
{
IntPtr ipTBButton = new IntPtr(pTBButton);
 
// this is fixed
Int32 dwBytesWritten = 0;
IntPtr ipBytesWritten = new IntPtr(&dwBytesWritten);
 
//uint lpWrite;
//IntPtr lpWritePointer = new IntPtr(&(lpWrite));
 
bool b4 = Win32.WriteProcessMemory(hProcess,
ipRemoteBuffer,
ipTBButton,
new UIntPtr(BUFFER_SIZE),
out ipBytesWritten);
 
if (!b4)
{
Debug.Assert(false);
return false;
}
int chars = (int)Win32.SendMessage(hToolbar, (uint)TB.TB_INSERTBUTTON, (IntPtr)index, ipRemoteBuffer);
if (chars == -1)
{
Debug.Assert(false);
return false;
}
}
 
Win32.VirtualFreeEx(hProcess, ipRemoteBuffer, UIntPtr.Zero, MemAllocationType.RELEASE);
Win32.CloseHandle(hProcess);
 
return true;
}
GeneralThe answer to my dreams!membersusan hopkin4 Dec '07 - 17:04 
This is just great. It makes me so happy. I always need my email to be in the first location. If it isn't and I needed to restart it I HATE it when I need to go and look for where it is.
 
THANK YOU THANK YOU!
 
Now I can become even more weird with what comes second, third etc...
 
Susan
GeneralDoes not work under Windows Vista x64memberSpaceman288 Nov '07 - 5:51 
Somehow, even though the TM_ButtonCount seems to work fine, it does not work when trying to access the Text and handle.
Running under Windows Vista X64.
I suspect that the new security layer is preventing my C# program to access the explorer process correctly through SendMessage.

GeneralRe: Does not work under Windows Vista x64memberMember 60385522 Aug '08 - 12:27 
I am able to use the approach on vista 32bit. I havent tested it on 64bit. But my point is the security layer is not blocking anything as the explorer process and this process run at same integrity level(Medium).
GeneralRe: Does not work under Windows Vista x64memberDecad10 Oct '08 - 14:20 
Under Vista x64 , i try to get the tbButton.dwData value for each button (icon in toolbar32) so i can later read TRAYDATA struct and always i get IntPtr.Zero value(0x00000...).
 
At windows XP x86 works fine.
GeneralRe: Does not work under Windows Vista x64memberElvandar6 Apr '09 - 23:20 
I had the same problem. This is due to the definition of the struct TBBUTTON:
 
typedef struct _TBBUTTON {
      int iBitmap;
      int idCommand;
      BYTE fsState;
      BYTE fsStyle;
#ifdef _WIN64
      BYTE bReserved[6];               // padding for alignment
#elif defined(_WIN32)
      BYTE bReserved[2];               // padding for alignment
#endif
      DWORD_PTR dwData;
      INT_PTR iString;
} TBBUTTON, NEAR* PTBBUTTON, *LPTBBUTTON;
typedef const TBBUTTON *LPCTBBUTTON;
 
The problem is related to the padding bytes in the middle of the structure.
 
I solved the problem creating my own TBBUTTON32 and TBBUTTON64 structures and choosing the one to use at runtime. I know this is not the best solution, but I need to have only one executable for both 32 and 64 bit system, and this solution is good for me Smile | :)
Generalno need for GetDesktopWindow()membergoher9 May '07 - 0:01 
from MSDN
"If hwndParent is NULL, the function uses the desktop window as the parent window"
u can pass IntPtr.Zero in the first FindWindowEx in the hwndParent instead of
hDesktop
 
any way very very coog code.
QuestionNot supported for C++membercav295909 Jan '07 - 22:26 
When I check the dwData from TBBUTTON structur after SendMessage with TB_GETBUTTON, it is a address pointed to CCCCCCCC. Cry | :((
GeneralAlternate method that is cleanermemberBob_Friday7 Dec '06 - 14:05 
Thank you for a great article. The only problem I see is that when rearranging the buttons, all of the windows must be hidden and then made visible. This is kind of icky looking.
 
I have modified your concept to instead use the ITaskbarList interface.
 
Use the ITaskbarList.DeleteTab(handle) method to remove the button from the task bar, and the ITaskbarList.AddTab(handle) method to put it back.
 
Reduces time and flicker since the windows that belong to the buttons are not themselves made invisible.
 
Again, thanks for a great article.

GeneralSupport for VistamemberMark Stafford23 Sep '06 - 9:11 
Nick: I made updates to your OSVersion class to support Windows Vista. If you want I can send it to you, just email me.
 
Mark Stafford
mark@staffordcastle.com

QuestionCan I get new added or removed button event?memberjucysoft14 Feb '06 - 17:26 
How to get new added or removed or notification task event from taskbar (button)?
 
Thank you,
ChangYoungLaugh | :laugh:

AnswerRe: Can I get new added or removed button event?memberRadu Sebastian LAZIN29 Jun '06 - 6:35 
I would like to know if that's possible too...
AnswerRe: Can I get new added or removed button event?memberJan Stavngaard7 Jun '07 - 20:46 
I once implemented a callback each time a new window occurs, from there you can check the taskbar buttons
QuestionTreeListView Missing?memberDennis McMahon16 Aug '05 - 4:25 
Not sure if this thread is still active, but first wanted to let you know this is a cool tool and I am interested in how it was put together (and use it of course). I have one problem running the sample project however,I am missing the class behind the reference to System.Windows.Forms.TreeListView - there isn't a TreeListView class in the System.Windows.Forms namespace. I am running .Net framework 1.1 and VS 2003. Is this a seprarate dll I need to download?
 
Thanks for any information you can provide and again nifty tool.
DMcMahon
AnswerRe: TreeListView Missing?memberNicholas Butler16 Aug '05 - 5:42 
Thanks Smile | :)
 
TreeListView is a component from a codeproject article by Thomas Caudal. There is a link in this article just above the 'Points of interest' section.
 
I included the two assemblies required in the source zip. They should be referenced in my project, but if not, just add them.
 
I don't know why Thomas put his component in the System.Windows.Forms namespace -- it's just the way he did it.
 
----------------------------
Be excellent to each other Smile | :)

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 10 Aug 2005
Article Copyright 2005 by Nicholas Butler
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid