Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# does Shell, Part 3

0.00/5 (No votes)
2 Mar 2003 4  
This article is about Application Desktop Toolbars, which are applications that can align to the screen much like the taskbar. The article will develop a base class for developing such apps.

Sample Image - csdoesshell3.jpg

Introduction

I know that in my last article, I've promised to begin explaining how to extend the shell but I found some very interesting information that concerns the shell and I must write about it before I start extending the shell.

So, what is this all about? This article is about Application Desktop Toolbars, shortly called Appbars. What are those appbars? Appbars are applications that can be aligned to one of the screen edges. For example, ICQ is an appbar, because it can be aligned to the right or left edge of the screen, and when it is docked, if you maximize some other window, it is still visible because the system tells the rest of the windows that the working area is smaller. Another famous example of an appbar is the TaskBar, yes, the bar with the list of opened applications, that we all have. The taskbar can be aligned to all the edges but cannot be in a floating state.

During this article, we will develop a class named ApplicationDesktopToolbar. This class inherits System.Windows.Forms.Form. So when we want to make our application behave like an appbar, all we need to do is inherit from ApplicationDesktopToolbar instead of System.Windows.Forms.Form.

Note: No extra reading material is needed for understanding this article. But here are some links you might want to read after you read this article:

MSDN: Using Application Desktop Toolbars

Another Note: The code added to this article contains a new version of the ShellLib library we started to develop in the previous parts.

Section 1: Basics

So now that we know what an Application Desktop Toolbar is (I told you, like taskbar but can also float!), let's get busy. There is good news and bad news when we come to develop such an application. The bad news is that there is no magical API that does the work for us, we need to do the work ourselves. The good news is that the operating system helps us know when to use the functions we have made in the bad news section..

Let's begin with the good news. What does the operating system supply us? The OS supplies us information about windows movements that might affect our window, so we could act accordingly. Imagine that you have an appbar that is aligned to the bottom, where the taskbar is, and the user resizes the taskbar, in that case, we should resize our appbar.

Another info the OS supplies us is the free working space. When we want to dock our appbar, we need to know what size we want to set it. Again, considering other appbars in the system and the taskbar.

All we need to do in order to accept the OS information is to register in a special list of the OS. The OS keeps a list of all the appbars in the system and when something happens that might affect an appbar, the OS sends messages to the appbars in this list, so that the appbars can respond (move, size, disappear, etc.).

You might wonder how this registration is done, or how the OS notifies us about the changes. This is explained in the following section.

Section 2: Communicating with the OS

How does the OS and our program communicate? The answer is divided in two. First, how does the OS communicates with our program? Simply, by sending window messages into our window procedure. But what about the second part? How do we communicate with the OS? We communicate with the OS with the help of an API function called SHAppBarMessage. This function has two parameters, the first is the message we want to send to the OS, and the second parameter is a struct that contains more information about the message being sent.

Here is the original declaration of the SHAppBarMessage API and the APPBARDATA struct in C++:

UINT_PTR SHAppBarMessage( 
        DWORD dwMessage,        // Appbar message value to send.
        PAPPBARDATA pData);     // Address of an APPBARDATA structure.     
typedef struct _AppBarData {
    DWORD cbSize;
    HWND  hWnd;
    UINT  uCallbackMessage;
    UINT  uEdge;
    RECT  rc;
    LPARAM  lParam;
} APPBARDATA, *PAPPBARDATA;

and the C# equivalent:

// Sends an appbar message to the system. 
[DllImport("shell32.dll")]
public static extern UInt32 SHAppBarMessage(
    UInt32 dwMessage,         // Appbar message value to send.
    ref APPBARDATA pData);    // Address of an APPBARDATA structure. The 
                              // content of the structure depends on the 
                              // value set in the dwMessage parameter. 
 
[StructLayout(LayoutKind.Sequential)]
public struct APPBARDATA
{
    public UInt32 cbSize;
    public IntPtr hWnd;
    public UInt32 uCallbackMessage;
    public UInt32 uEdge;
    public RECT rc;
    public Int32 lParam;
}

Let's explain some of this. The first parameter of the SHAppBarMessage API is dwMessage, this parameter is the message being sent. It can be one of the following enum values:

public enum AppBarMessages
{
    // Registers a new appbar and specifies the message identifier
    // that the system should use to send notification messages to 
    // the appbar. 
    New                       = 0x00000000,    
    // Unregisters an appbar, removing the bar from the system's 
    // internal list.
    Remove                    = 0x00000001,
    // Requests a size and screen position for an appbar.
    QueryPos                  = 0x00000002,    
    // Sets the size and screen position of an appbar. 
    SetPos                    = 0x00000003,    
    // Retrieves the autohide and always-on-top states of the 
    // Microsoft® Windows® taskbar. 
    GetState                  = 0x00000004,    
    // Retrieves the bounding rectangle of the Windows taskbar. 
    GetTaskBarPos             = 0x00000005,    
    // Notifies the system that an appbar has been activated. 
    Activate                  = 0x00000006,    
    // Retrieves the handle to the autohide appbar associated with
    // a particular edge of the screen. 
    GetAutoHideBar            = 0x00000007,    
    // Registers or unregisters an autohide appbar for an edge of 
    // the screen. 
    SetAutoHideBar            = 0x00000008,    
    // Notifies the system when an appbar's position has changed. 
    WindowPosChanged          = 0x00000009,    
    // Sets the state of the appbar's autohide and always-on-top 
    // attributes.
    SetState                  = 0x0000000a    
}

The messages will be explained later. The second parameter pData, contains extra information we need to supply when we send the message. Note that not all the fields of the struct should be filled. It depends on the message.

Section 3: Registering our AppBar

The first step in making our program an appbar is register it in the OS appbars list. The registration is done by sending the message AppBarMessages.New. When this message is sent, the following members of the APPBARDATA struct should be set. The hWnd should be set to our window handle, the uCallbackMessage should be set to a unique message id that the OS will use to communicate with our program.

Let's review this one more time because I know it's a little fuzzy. We want to register our window as an appbar, so we use the SHAppBarMessage to send the OS the message AppBarMessages.New with our window handle and a unique message id. When the OS wants to notify my window of something that I should know about, it sends a message to my window procedure, the message that the OS will send is the unique message id I gave during the registration. The OS probably does something like:

SendMessage([window handle],[unique message id],[notify code],
            [notify extra info]);

So in my window procedure, I need to respond to the unique message id, and according to the wParam, I'll know which notification message the OS sends me.

Why should the message id be unique? Because I need to respond to it in my window procedure, I can't just give a random value because it might be a valid message id. So how do I get a number that doesn't fit any window message? To do that, we use the API function RegisterWindowMessage, this function receives a string and returns the message id attached to that string, if the string does not exist, it creates a new unique message id and returns it. This API is usually used to communicated between two applications on the same computer (each application calls this API with the same string and they both get the same message id and now they can communicate by sending this message id). So we need to declare this function in order to use it, here is the C++ declaration:

UINT RegisterWindowMessage(
LPCTSTR lpString);   // message string

and the C# equivalent:

// The RegisterWindowMessage function defines a new window message that is 
// guaranteed to be unique throughout the system. The message value can be 
// used when sending or posting messages. 
[DllImport("user32.dll")]
public static extern UInt32 RegisterWindowMessage(
    [MarshalAs(UnmanagedType.LPTStr)]
    String lpString);    // Pointer to a null-terminated string that 
                         // specifies the message to be registered. 

Now that we know how to do the registration of an appbar, let's see some code that does it. First, here is how we get a new unique message id for later use, this function is called from the constructor:

private UInt32 RegisterCallbackMessage()
{
    String uniqueMessageString = Guid.NewGuid().ToString();
    return ShellApi.RegisterWindowMessage(uniqueMessageString);
}

There is not much to explain, first I get a unique string by generating a GUID and then I use this string to get a message id.

Here is a function that does the registration:

private Boolean AppbarNew()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uCallbackMessage = RegisterCallbackMessage();

    // install new appbar
    UInt32 retVal = ShellApi.SHAppBarMessage((UInt32)AppBarMessages.New, 
                                              ref msgData);
        
    return (retVal!=0);
}

Here, I create a new APPBARDATA struct, set its size, my window handle, and a unique message id and then call the SHAppBarMessage with the AppBarMessages.New message. The code is not complete because I need to store the unique message id for later use in the window procedure.

Section 4: Setting the AppBar Size

The next step to pay attention to is what happens when we want to set the appbar size and location. The first thing to remember is that when we set the appbar position, we need to take care that it doesn't disturb any other appbar including the taskbar. So in order to change the appbar position, we need to follow these steps:

  1. Our application should propose the OS where we want to position our appbar. This proposition should include the edge we want to be docked to, and the size of our window (in a RECT struct). We do that by sending the OS the message AppBarMessages.QueryPos. After sending this message, the OS will check if it has problems with the proposed position and if it does, it will fix our rectangle so that it doesn't disturb any other appbar. Note that by sending the message, we don't change anything yet, we only check if the position we want is valid, and get in return a valid position, no moving has accord yet.
  2. After doing repairs to the returning rectangle, we need to send the OS the message AppBarMessages.SetPos. This message changes the position of our appbar in the OS internal appbar list. Two things are important to understand in this step.
    1. The OS will first recheck the position that we asked to see if we do not disturb any other appbar, after checking it will again fix the rectangle so that it won't disturb any appbar and only then, it will set the rectangle info in the internal list of appbars.
    2. The second thing to notice is that this message does not actually move the window! It only changes the rectangle in the internal appbar list.
  3. After the AppBarMessages.SetPos message has returned, we need to actually move our window to the position registered in the internal list. This is done by calling the MoveWindow API or even more simple, just setting the Size and Location properties of the form. One thing to notice is we need to move the window to the rectangle that returned from the AppBarMessages.SetPos message, because the OS might have changed it.

In order to do these steps in our program, we will define two helper functions that take care of sending the messages:

private void AppbarQueryPos(ref ShellApi.RECT appRect)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uEdge = (UInt32)m_Edge;
    msgData.rc = appRect;

    // query position for the appbar
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.QueryPos, ref msgData);
    appRect    = msgData.rc;
}

private void AppbarSetPos(ref ShellApi.RECT appRect)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uEdge = (UInt32)m_Edge;
    msgData.rc = appRect;

    // set postion for the appbar
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.SetPos, ref msgData);
    appRect    = msgData.rc;
}

These two functions are very simple. Both accept the proposed rectangle, create an APPBARDATA struct and fill it with the needed information (the edge comes from a private variable of the class). And then, send the OS the message using the API SHAppBarMessage. One thing to note about this code is that after we send the message, we set the appRect variable to the rectangle returned by the message, because it might have changed.

Now I'll present here the code that sizes our class. The code follows the 3 steps. One thing to note about the code is that m_PrevSize is a private variable that stores the size of window when it was in float mode. The idea behind the function is that if the width of the window was 90 and the height was 40 and you dock the window to the top, then the height will remain 40 and the width will be the width of the entire screen. Here is the code:

private void SizeAppBar() 
{
    // prepare the proposed rectangle
    ShellApi.RECT rt = new ShellApi.RECT();

    if ((m_Edge == AppBarEdges.Left) || 
        (m_Edge == AppBarEdges.Right)) 
    {
        rt.top = 0;
        rt.bottom = SystemInformation.PrimaryMonitorSize.Height;
        if (m_Edge == AppBarEdges.Left) 
        {
            rt.right = m_PrevSize.Width;
        }
        else 
        {
            rt.right = SystemInformation.PrimaryMonitorSize.Width;
            rt.left = rt.right - m_PrevSize.Width;
        }
    }
    else 
    {
        rt.left = 0;
        rt.right = SystemInformation.PrimaryMonitorSize.Width;
        if (m_Edge == AppBarEdges.Top) 
        {
            rt.bottom = m_PrevSize.Height;
        }
        else 
        {
            rt.bottom = SystemInformation.PrimaryMonitorSize.Height;
            rt.top = rt.bottom - m_PrevSize.Height;
        }
    }

    // Step 1: check the proposed rectangle using QueryPos
    AppbarQueryPos(ref rt);
    
    switch (m_Edge) 
    { 
        case AppBarEdges.Left: 
            rt.right = rt.left + m_PrevSize.Width;
            break; 
        case AppBarEdges.Right: 
            rt.left= rt.right - m_PrevSize.Width;
            break; 
        case AppBarEdges.Top: 
            rt.bottom = rt.top + m_PrevSize.Height;
            break; 
        case AppBarEdges.Bottom: 
            rt.top = rt.bottom - m_PrevSize.Height;
            break; 
    }

    // Step 2: after fixing the rectangle, set the proposed rectangle using 
    //         SetPos

    AppbarSetPos(ref rt);

    // Step 3: Do the actual moving of the window
    Location = new Point(rt.left,rt.top);
    Size = new Size(rt.right - rt.left,rt.bottom - rt.top);
}

First, we build our first proposition for the rectangle. Then, we send the AppBarMessages.QueryPos message, then we fix the proposed rectangle and send the AppBarMessages.SetPos message and finally we do the actual moving of the window. Simple as that.

Section 5: Other Messages

So, until now, we saw several uses of the SHAppBarMessage API. We used it in the functions: AppbarNew, AppbarQueryPos and AppbarSetPos. In this section, we will quickly specify all the rest of the messages that can be sent and their wrapping functions.

We will start with the AppBarMessages.Remove message. This message is sent when we want to remove our window from the OS internal appbar list. After sending this message, we will stop receiving notifications from the OS and our window will be treated as a normal window. All we need to specify is the window handle to be removed. Here is a nice wrapper function that do it:

private Boolean AppbarRemove()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
            
    // remove appbar
    UInt32 retVal = ShellApi.SHAppBarMessage((UInt32)AppBarMessages.Remove,
                                              ref msgData);
    
    // set previous size and location    
    Size = m_PrevSize;
    Location = m_PrevLocation;

    return (retVal!=0);
}

The function creates an APPBARDATA struct, set the hWnd member and send the AppBarMessages.Remove message, after sending the message, the function restores the position of the window.

On to the next message. The rules of the Appbars say that when an appbar application receives the message WM_ACTIVATE, it should send the message AppBarMessages.Activate to the OS. So here is the AppbarActivate function code:

private void AppbarActivate()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    
    // send activate to the system
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.Activate, ref msgData);
}

Nothing to explain. Again, according to the rules, when our application receives the message WM_WINDOWPOSCHANGED, it should send the message AppBarMessages.WindowPosChanged to the OS. Here is the code:

private void AppbarWindowPosChanged()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    
    // send windowposchanged to the system 
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.WindowPosChanged, 
                              ref msgData);
}

The OS allows us to set a property of the appbar called autohide, which usually means that the appbar disappears when not in focus, and reappears when needed. One limitation is that you can set only one autohide appbar per edge. So if the taskbar has autohide set, you cannot set your appbar on the same edge of the taskbar and to set it to autohide. Two messages that help with the autohide stuff are AppBarMessages.GetAutoHideBar and AppBarMessages.SetAutoHideBar. The first one gets an edge and returns the handle of the window that has autohide set on that edge, if no window has it, returns null. The second message gets an edge, a state and a window handle and sets it as autohide on the specified edge. Note that the second message can return failure if there is already an autohide appbar on that edge. One important thing to notice is that these messages do not take care of the hiding and showing of the window. This is entirely up to you to write, they only manage the states internally so you won't set two autohide appbars on the same edge. The code for using these messages is as follows:

private Boolean AppbarSetAutoHideBar(Boolean hideValue)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.hWnd = Handle;
    msgData.uEdge = (UInt32)m_Edge;
    msgData.lParam = (hideValue) ? 1 : 0;
            
    // set auto hide
    UInt32 retVal = ShellApi.SHAppBarMessage(
        (UInt32)AppBarMessages.SetAutoHideBar,
        ref msgData);
    return (retVal!=0);
}

private IntPtr AppbarGetAutoHideBar(AppBarEdges edge)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.uEdge = (UInt32)edge;
            
    // get auto hide
    IntPtr retVal = (IntPtr)ShellApi.SHAppBarMessage(
        (UInt32)AppBarMessages.GetAutoHideBar,
        ref msgData);
    return retVal;
}

Finally, we have three more simple messages that might help us, those messages are used when working with the taskbar. The first two: AppBarMessages.GetState and AppBarMessages.SetState let us get or set the state of the taskbar. When I say state, I mean whether it's AlwaysOnTop and whether it's AutoHide. It can be useful for example if you want to set the AlwaysOnTop like the taskbar state. The last message is AppBarMessages.GetTaskBarPos which returns the rectangle of the taskbar. Here are the wrapping functions:

private AppBarStates AppbarGetTaskbarState()
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
            
    // get taskbar state
    UInt32 retVal = ShellApi.SHAppBarMessage(
        (UInt32)AppBarMessages.GetState, 
        ref msgData);
    return (AppBarStates)retVal;
}

private void AppbarSetTaskbarState(AppBarStates state)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
    msgData.lParam = (Int32)state;
            
    // set taskbar state
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.SetState, ref msgData);
}

private void AppbarGetTaskbarPos(out ShellApi.RECT taskRect)
{
    // prepare data structure of message
    ShellApi.APPBARDATA msgData = new ShellApi.APPBARDATA();
    msgData.cbSize = (UInt32)Marshal.SizeOf(msgData);
            
    // get taskbar position
    ShellApi.SHAppBarMessage((UInt32)AppBarMessages.GetTaskBarPos, 
                              ref msgData);
    taskRect = msgData.rc;
}

Quite simple. So these are all the functions we can send to the operating system. In the next section, we will see what notifications the OS might send to us.

Section 6: OS Notifications

Remember we have sent the AppBarMessages.New message? We had to supply to the OS a unique message id. So as I've mentioned before, when the OS wants to send us notifications, it uses the unique message id we supplied and sends it to our window procedure with the wParam as the notification code. Fortunately, there are only four notifications we might receive, which are declared nicely in the enum AppBarNotifications:

public enum AppBarNotifications
{
    // Notifies an appbar that the taskbar's autohide or 
    // always-on-top state has changed—that is, the user has selected 
    // or cleared the "Always on top" or "Auto hide" check box on the
    // taskbar's property sheet. 
    StateChange            = 0x00000000,    
    // Notifies an appbar when an event has occurred that may affect 
    // the appbar's size and position. Events include changes in the
    // taskbar's size, position, and visibility state, as well as the
    // addition, removal, or resizing of another appbar on the same 
    // side of the screen.
    PosChanged             = 0x00000001,    
    // Notifies an appbar when a full-screen application is opening or
    // closing. This notification is sent in the form of an 
    // application-defined message that is set by the ABM_NEW message. 
    FullScreenApp          = 0x00000002,    
    // Notifies an appbar that the user has selected the Cascade, 
    // Tile Horizontally, or Tile Vertically command from the 
    // taskbar's shortcut menu.
    WindowArrange          = 0x00000003    
}

We will start with AppBarNoifications.PosChanged. This notification tells us that something happened that might affect our window position or size, so our response we be probably resizing our window by calling the AppBarMessages.QueryPos and AppBarMessages.SetPos messages. This notification can be sent for example when another appbar is attached to our appbar edge or when an appbar that was on our edge has changed its size or position.

Next, we might get the notification AppBarNotifications.StateChange. This notification is sent when the state of the taskbar is changing. Again, the state means the AlwaysOnTop or AutoHide settings. A possible response to this notification is to get the state of the taskbar using the message AppBarMessages.GetState and changing our window accordingly.

Another notification we might get is AppBarNotifications.FullScreenApp. This notification is sent when a new window enters the full window mode or when the last full window closes. According to the lParam, we can tell which case it is. The normal response to this notification is to change our z-order when a new full window opens and restore our z-order when the last full window closes. If you ask yourself what this full window mode is, try pressing F11 on your Internet Explorer.

Finally, the last notification, AppBarNotifications.WindowArrange. This notification is sent when the user commands the shell to rearrange the opened windows (Tile, Cascade). This notification is sent twice. Once before the arrangement and once after the arrangement. According to the lParam, you can tell whether it is before or after the arrangement. A normal response to this message is to hide our window before the arrangement and to show our window after the arrangement, so our window will not participate in the arrangement and hold its position.

These are all the notifications that are available for us. I've also explained some possible uses of these notifications, and later, you will see code that fits their explanations.

Section 7: Mixing Our Knowledge

So, now that we know what the operating system supplies us and it is all wrapped up nicely, all that is left to do is mix it up and create our ApplicationDesktopToolbar class.

Like every class, we will begin with its constructor. In the constructor, we want the class to register our unique message id. Another thing we want is that the FormBorderStyle will be set to SizableToolWindow. You might wonder why we do that. Well, it turns out that when the operating system is managing the windows in the system, it takes care that all the windows that appears in the taskbar cannot enter the area that is occupied by the appbars, which in our case will cause that even though we declared that we are an appbar, the system will not let us position our window in the requested position. To solve this issue, we need to set our window to be a toolbar, so it will get the WS_EX_TOOLBAR style, and then it should work. Now pay close attention: This answer is what Microsoft published in MSDN in response to this problem. Unfortunately, this is not accurate. If it was accurate, then all I would have to do is to set the property ShowInTask and that was enough. Also, setting the FormBorderStyle as SizableToolWindow still show our window in the taskbar! I don't have a good answer for it. I assume that it was once correct, but today the reality has changed a bit. Finally, I know that it works only if the FormBorderStyle is SizableToolWindow or FixedToolWindow, regardless of the ShowInTask property.

Here is how the constructor looks like:

public ApplicationDesktopToolbar()
{
    FormBorderStyle = FormBorderStyle.SizableToolWindow;
    
    // Register a unique message as our callback message
    CallbackMessageID = RegisterCallbackMessage();
    if (CallbackMessageID == 0)
        throw new Exception("RegisterCallbackMessage failed");
}

Also, we need some private variables to store some information:

// saves the current edge
private AppBarEdges m_Edge = AppBarEdges.Float;

// saves the callback message id
private UInt32 CallbackMessageID = 0;

// are we in dock mode?
private Boolean IsAppbarMode = false;

// save the floating size and location
private Size m_PrevSize;
private Point m_PrevLocation;

Before we will talk about the window procedure, here are some small functions I should explain:

On load of the form, I need to save the previous size and location of the form:

protected override void OnLoad(EventArgs e)
{
    m_PrevSize = Size;
    m_PrevLocation = Location;
    base.OnLoad(e);
}

On closing the form, we should call AppbarRemove to remove the window from the internal list:

protected override void OnClosing(CancelEventArgs e)
{
    AppbarRemove();
    base.OnClosing(e);
}

If the size changes while we are docked to an edge, I want to save only some of size information, because if we are aligned to the bottom or top, I don't need to save the width info because it is set to the screen width:

protected override void OnSizeChanged(EventArgs e)
{
    if (IsAppbarMode)
    {
        if (m_Edge == AppBarEdges.Top || m_Edge == AppBarEdges.Bottom)
            m_PrevSize.Height = Size.Height;
        else
            m_PrevSize.Width = Size.Width;

        SizeAppBar();
    }
    
    base.OnSizeChanged(e);
}

One last code before talking about message handling, the public property Edge. When it is set, it should call AppBarNew or AppBarRemove, depending on whether it changes to float or a screen edge:

public AppBarEdges Edge 
{
    get 
    {
        return m_Edge;
    }
    set 
    {
        m_Edge = value;
        if (value == AppBarEdges.Float)
            AppbarRemove();
        else
            AppbarNew();

        if (IsAppbarMode)
            SizeAppBar();
    }
}

Section 8: Window Procedure

At last, the window procedure, the place where all fits in. In our window procedure, we will take care of four messages. The first message is the message that we receive when a notification arrives. The second message is WM_ACTIVATE, in this message, we need to call AppBarActivate. The third message is WM_WINDOWPOSCHANGED, Here, we need to call AppBarWindowPosChanged. and the last message is WM_NCHITTEST. Of course, I'll explain why I respond to this last message. First, you need to remember that the window can be sized, but I want that if the window is docked, then sizing will be allowed in only one direction instead of the normal 4 directions. What I mean is that if our window is docked to the top edge, then I want to be able to size only the bottom border! So what we do is according to the current docking edge we allow only one border to be sized. Soon you will see the code to do it.

So this is the window procedure code:

protected override void WndProc(ref Message msg)
{
    if (IsAppbarMode)
    {
        if (msg.Msg == CallbackMessageID)
        {
            OnAppbarNotification(ref msg);
        }
        else if (msg.Msg == (int)WM.ACTIVATE)
        {
            AppbarActivate();
        }
        else if (msg.Msg == (int)WM.WINDOWPOSCHANGED)
        {
            AppbarWindowPosChanged();
        }
        else if (msg.Msg == (int)WM.NCHITTEST)
        {
            OnNcHitTest(ref msg);
            return;
        }
    }
        
    base.WndProc(ref msg);
}

The AppbarActivate and AppbarWindowPosChanged functions are familiar. We will now review the code for OnNcHitTest:

void OnNcHitTest(ref Message msg)
{
    DefWndProc(ref msg);
    if ((m_Edge == AppBarEdges.Top) && 
        ((int)msg.Result == (int)MousePositionCodes.HTBOTTOM))
        0.ToString();    
    else if ((m_Edge == AppBarEdges.Bottom) && 
        ((int)msg.Result == (int)MousePositionCodes.HTTOP))
        0.ToString();    
    else if ((m_Edge == AppBarEdges.Left) && 
        ((int)msg.Result == (int)MousePositionCodes.HTRIGHT))
        0.ToString();    
    else if ((m_Edge == AppBarEdges.Right) && 
        ((int)msg.Result == (int)MousePositionCodes.HTLEFT))
        0.ToString();    
    else if ((int)msg.Result == (int)MousePositionCodes.HTCLOSE)
        0.ToString();    
    else
    {
        msg.Result = (IntPtr)MousePositionCodes.HTCLIENT;
        return;
    }
    base.WndProc(ref msg);
}

Well, actually, it's not very complicated. First, I call the default window procedure to get the mouse position. Then I check if the edge is top and the mouse position is the bottom border then I do nothing. The same is for the rest of the edges. If the mouse position is the close button, I also do nothing so the user can press the close button even if it's docked. Finally, in all other cases, I return HTCLIENT which means that the mouse position is on the client area. So, for example, if the user position the mouse over the top border when the window is docked to the top edge, the window will think that the user is pointing on the client area and will do nothing instead of showing the sizeable icon and let it size it. One thing to notice about this code is that every time I need to do nothing, I do 0.ToString(), this is because we can't put an empty statement in C# and every other way to form the code will result in less readable code, So don't be confused, the 0.ToString() does nothing.

Finally, the last function, OnAppbarNotification, this is where we handle the notifications we receive from the OS:

void OnAppbarNotification(ref Message msg)
{
    AppBarStates state;
    AppBarNotifications msgType = (AppBarNotifications)(Int32)msg.WParam;
            
    switch (msgType)
    {
        case AppBarNotifications.PosChanged:
            SizeAppBar();
            break;
                
        case AppBarNotifications.StateChange:
            state = AppbarGetTaskbarState();
            if ((state & AppBarStates.AlwaysOnTop) !=0)
            {
                TopMost = true;
                BringToFront();
            }
            else
            {
                TopMost = false;
                SendToBack();
            }
            break;

        case AppBarNotifications.FullScreenApp:
            if ((int)msg.LParam !=0)
            {
                TopMost = false;
                SendToBack();
            }
            else
            {
                state = AppbarGetTaskbarState();
                if ((state & AppBarStates.AlwaysOnTop) !=0)
                {
                    TopMost = true;
                    BringToFront();
                }
                else
                {
                    TopMost = false;
                    SendToBack();
                }
            }
                    
            break;

        case AppBarNotifications.WindowArrange:
            if ((int)msg.LParam != 0)    // before
                Visible = false;
            else                         // after
                Visible = true;
                    
            break;
    }
}

I'm not going to explain this function at all! If you want explanation, read section 6 again: OS Notifications.

We have finished developing the ApplicationDesktopToolbar class.

Using the Class

This should be your favorite part, using the class is very simple. All you need to do is change this line:

public class frmMain : Form

to this line:

public class frmMain : ShellLib.ApplicationDesktopToolbar

Then when you want to dock your application to an edge, just set the Edge property like this:

private void frmMain_Load(object sender, System.EventArgs e)
{
    this.Edge = AppBarEdges.Left;
}

I recommend you try the test program for this article, so you could see the result for yourself.

That's it!

Hope you like it. Don't forget to vote.

History

  • 3rd March, 2003: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here