Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / Windows Forms
Article

A tool to order the window buttons in your taskbar

Rate me:
Please Sign up or sign in to vote.
4.90/5 (53 votes)
9 Aug 2005CPOL8 min read 251.4K   7.1K   108   58
Move your window buttons into your preferred order. WinXP or later only.

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

Image 1

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:

C#
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:

C#
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:

C#
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:

C#
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)


Written By
United Kingdom United Kingdom
I discovered C# and .NET 1.0 Beta 1 in late 2000 and loved them immediately.
I have been writing software professionally in C# ever since

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.

I can work 'virtually' anywhere!

Comments and Discussions

 
QuestionThe idea is good Pin
Gluups11-Nov-23 12:08
Gluups11-Nov-23 12:08 
QuestionIt does not work in Windows 10 Pin
Win32nipuh8-May-16 21:22
professionalWin32nipuh8-May-16 21:22 
AnswerRe: It does not work in Windows 10 Pin
Nettix Code7-Jun-23 22:27
Nettix Code7-Jun-23 22:27 
QuestionUnknown OS Pin
gultron19-May-15 5:56
gultron19-May-15 5:56 
GeneralWARNING!!! Pin
Matthew Miller16-Oct-14 18:41
Matthew Miller16-Oct-14 18:41 
GeneralRe: WARNING!!! Pin
Ray K25-Feb-15 21:51
Ray K25-Feb-15 21:51 
SuggestionVista x64 not supported Pin
zpro14-Mar-13 2:32
zpro14-Mar-13 2:32 
Questionadding textbox on top of the task bar Pin
princtreme110-Sep-12 1:46
princtreme110-Sep-12 1:46 
GeneralExcellent Pin
Knasenmc314-Jan-11 3:57
Knasenmc314-Jan-11 3:57 
GeneralMy vote of 5 Pin
chatravin3-Oct-10 8:24
chatravin3-Oct-10 8:24 
GeneralWindows 7 Problems PinPopular
DrWatsonCoding25-Aug-10 8:31
DrWatsonCoding25-Aug-10 8:31 
GeneralNice but... Pin
gbrlhtclcq25-Aug-09 23:01
gbrlhtclcq25-Aug-09 23:01 
GeneralChange button Text Pin
C470214-Dec-08 5:53
C470214-Dec-08 5:53 
GeneralThanks!!! Pin
Member 60385522-Aug-08 12:25
Member 60385522-Aug-08 12:25 
QuestionHide - Unhide aswell? Pin
Stumproot25-Feb-08 0:03
Stumproot25-Feb-08 0:03 
AnswerRe: Hide - Unhide aswell? Pin
Member 60385522-Aug-08 12:24
Member 60385522-Aug-08 12:24 
GeneralInserting button to windows explorer from a shell extension. Pin
Shafeeque O.K.26-Dec-07 3:21
Shafeeque O.K.26-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! Pin
susan hopkin4-Dec-07 17:04
susan hopkin4-Dec-07 17:04 
GeneralDoes not work under Windows Vista x64 Pin
Spaceman288-Nov-07 5:51
Spaceman288-Nov-07 5:51 
GeneralRe: Does not work under Windows Vista x64 Pin
Member 60385522-Aug-08 12:27
Member 60385522-Aug-08 12:27 
GeneralRe: Does not work under Windows Vista x64 Pin
Decad10-Oct-08 14:20
Decad10-Oct-08 14:20 
GeneralRe: Does not work under Windows Vista x64 Pin
Elvandar6-Apr-09 23:20
Elvandar6-Apr-09 23:20 
Generalno need for GetDesktopWindow() Pin
goher9-May-07 0:01
goher9-May-07 0:01 
QuestionNot supported for C++ Pin
coka2cora9-Jan-07 22:26
coka2cora9-Jan-07 22:26 
GeneralAlternate method that is cleaner Pin
Bob_Friday7-Dec-06 14:05
Bob_Friday7-Dec-06 14:05 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.