Click here to Skip to main content
15,881,381 members
Articles / Desktop Programming / Win32

WIN Taskbar is Waaay too High, Here's a Tiny One

Rate me:
Please Sign up or sign in to vote.
4.18/5 (7 votes)
14 Jan 2020CPOL5 min read 9K   222   13   1
With some code snippets to give beginners (+ others) ideas

Image 1

A Tiny Taskbar in C#

This solution provides a tiny, sortable Windows taskbar of WinXP style.

I really don't like the fat Win one.
And I really don't like all this grouping + auto-sort.
Your brain knows the best where the taskbar-button you want was last time!

You can drag the taskbar buttons in the order you like. The last order is saved on exit.
To use it, I dragged the Win taskbar to the left side + made it auto-hide.
The notification area is just shown, it has no functionality.

Chapters

To better understand the code, you may look at it in Visual Studio.
It explains a lot to you when you move over the code.
Example: Move over 'ManagementEventWatcher', you get: 'Initializes a new instance of the ManagementEventWatcher class when given a WMI event query'.

You can change some things easily in the project's settings (height, color of taskbar, etc.)

Creating the Application Desktop Toolbar

MainForm is inherited from ShellLib.ApplicationDesktopToolbar which is included in the project as ApplicationDesktopToolbar.dll.

This class allows to create an application desktop toolbar.
This is a window that is similar to the Windows taskbar. It is anchored to an edge of the screen, and it typically contains buttons that give the user quick access to other applications and windows. The system prevents other applications from using the desktop area used by an appbar.

C#
namespace TinyTaskbar {
      public partial class MainForm : ShellLib.ApplicationDesktopToolbar {

            // ...

            public MainForm() {
                  // do not throw exception if wrong thread accesses Control-Handle
                  CheckForIllegalCrossThreadCalls = false;
                  InitializeComponent();

                  // anchor the main window to bottom of screen
                  Edge = AppBarEdges.Bottom;
                  // ...
            }

I found the source code by for this at:
www.codeproject.com/Articles/3728/C-does-Shell-Part-3#xx1796941xx
and compiled it to ApplicationDesktopToolbar.dll.

Creating a List of TaskbarButtons With the Exact Information Needed

C#
// to store a taskbarButton with its index (in collection of MainForm's controls)
// and associated processId, windowHandle
private class TaskbarButton {
      // make this member accessible all alone
      public int Index { get; set; }
      public Button Button;
      public int ProcessId;
      public IntPtr WindowHandle;
}

// a list of all taskbarButtons
private List<TaskbarButton> taskbarButtons = new List<TaskbarButton>();

Making the Access of Project Settings Shorter

C#
// ...
using TinyTaskbar.Properties;

namespace TinyTaskbar {
      public partial class MainForm : ShellLib.ApplicationDesktopToolbar {
            // ...

            // by putting: using 'Namespace'.Properties; (see above),
            // all application settings are accessible by Settings.Default.'Settingsname'
            private int buttonWidth = Settings.Default.ButtonMaxWidth;

Checking the Type of a Variable

Explains itself:

C#
private bool AddTaskbarButton(object processIdOrWindowHandle) {
    // ...
    if (processIdOrWindowHandle.GetType() == typeof(int)) {

Subscribing to Events to Get Informed if Processes are Started or Stopped

WMI contains an event infrastructure that produces notifications about changes in WMI data and services. WMI event classes provide notification when specific events occur.
A WqlEventQuery represents a WMI event query in its query language (WQL) which is a subset of SQL.

The code makes a query for everything (*) in the Win32_ProcessStartTrace-Class.
This WMI-Class receives information about starting processes.

C#
private void MainForm_Load(object sender, EventArgs e) {
    // execute this codeblock in a parallel thread
    Task.Run(() => {
        // ...
        // create ManagementEventWatcher that watches for events specified in a WMI event query
        // Make a query for everything (*) in the Win32_ProcessStartTrace-class
        watchProcessStarted =
            new ManagementEventWatcher(
                new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace")
            );
        // subscribe to event of the watcher that occurs for started processes
        // to react by adding a taskbarButton
        watchProcessStarted.EventArrived +=
            new EventArrivedEventHandler(NewProcessEventHandler);
         // start watching
         watchProcessStarted.Start();

         // same for processes that are stopped, with Win32_ProcessStopTrace-Class
         watchProcessStopped =
             new ManagementEventWatcher(
                 new WqlEventQuery("SELECT * FROM Win32_ProcessStopTrace")
             );
         // subscribe to event to react by removing a taskbarButton
         watchProcessStopped.EventArrived +=
             new EventArrivedEventHandler(StopProcessEventHandler);
         watchProcessStopped.Start();
         // ...
   });
}

Using User-Settings

By deriving from ApplicationSettingsBase, you can implement the application settings feature in Window Forms applications (Save information in the same XML-format a project does).
The settings file is named 'user.config', stored in '%USERPROFILE%\Local Settings\'ApplicationName'\...'

ValueTuple(int ProcessId, int WindowHandle) buttonInfo declares buttonInfo as a ValueTuple with 2 members of int, named 'ProcessId' + 'WindowHandle'.
You can also declare: (int, int), then the members are accessed by 'Item1' + 'Item2'.

C#
public partial class MainForm : ShellLib.ApplicationDesktopToolbar {
    // to store processIds, windowHandles of all taskbarButtons at app quit
    // this being a field of the class, the settings are loaded at start
    private UserSettings userSettings = new UserSettings();
    // ...
    private void MainForm_Load(object sender, EventArgs e) {
        // ...
        if (userSettings.SavedButtonInfos != null) {
            // add taskbarButtons in last order of taskbarButtons saved in userSettings 
            foreach ((int ProcessId, int WindowHandle) 
                      buttonInfo in userSettings.SavedButtonInfos) {
                // from processId or windowHandle
                if (buttonInfo.ProcessId != 0) {
                    AddTaskbarButtonFromProcessId(buttonInfo.ProcessId);
                }
                else {
                    AddTaskbarButtonFromWindowHandle((IntPtr) buttonInfo.WindowHandle);
                }
            }
        }
        // ...
    }
    // ...
    private void ExitApplication_Click(object sender, EventArgs e) {
        // ...
        userSettings.SavedButtonInfos.Clear();
        foreach (TaskbarButton taskbarButton in taskbarButtons) {
            userSettings.SavedButtonInfos.Add(
                (taskbarButton.ProcessId, (int) taskbarButton.WindowHandle)
            );
        }
        userSettings.Save();
        // ...
    }
    //...
}

// userSettings class, derive from ApplicationSettingsBase
internal class UserSettings : ApplicationSettingsBase {
    // only one setting here
    // to save processId + windowHandle of the taskbarButtons at exit
    // doesn't serialize (int, intPtr), so: (int, int)
    [UserScopedSetting]
    public List<(int, int)> SavedButtonInfos {
        get { return (List<(int, int)>) this["SavedButtonInfos"]; }
        set { this["SavedButtonInfos"] = value; }
   }
}

Drawing with the Graphics-Class and with WIN32

C#
Bitmap sourceBitmap = new Bitmap(Width, Height);
// is equal to
Bitmap sourceBitmap = new Bitmap(this.Width, this.Height);

The this-Keyword is no more needed to access members of the class.
Maybe you like to use it though, for clarity. I personally prefer shorter code.

C#
Graphics graphicsSourceBitmap = Graphics.FromImage(sourceBitmap);

This code gets a Graphics to the Bitmap to draw to.
The Graphics-Class provides methods for drawing objects. It encapsulates the GDI+ drawing surface.
A Graphics is associated with a specific device context.

C#
IntPtr hdcSourceBitmap = graphicsSourceBitmap.GetHdc();

This gets the device context handle of the Graphics you need to draw with WIN32.

C#
graphicsSourceBitmap.ReleaseHdc(hdcSourceBitmap);

After drawing, you have to release the device context handle, because it's an unmanaged resource.

C#
NativeMethods.PrintWindow(Handle, hdcSourceBitmap, 0);

PrintWindow() only works with forms, not with controls, therefore I have to get the whole tinyTaskbar window.
WIN32-Function Imports & Constants shall always be located in a class named 'NativeMethods'.

C#
Invoke(new MethodInvoker(delegate () {
        dragForm = new Form { // ...

By using Invoke, the dragFrom is created in the thread of MainForm > it uses its message loop.

The entire code snippet is as follows:

C#
private void StartDragging() {
    // ...
    // create a bitmap of the size of the whole tinyTaskbar window
    Bitmap sourceBitmap = new Bitmap(Width, Height);
    Graphics graphicsSourceBitmap = Graphics.FromImage(sourceBitmap);
    // and get the handle to the graphics' device context
    IntPtr hdcSourceBitmap = graphicsSourceBitmap.GetHdc();
    // put the whole content of the tinyTaskbar-Window to that bitmap
    NativeMethods.PrintWindow(Handle, hdcSourceBitmap, 0);
    graphicsSourceBitmap.ReleaseHdc(hdcSourceBitmap);
    // create the bitmap for the dragFrom
    dragFormBitmap = new Bitmap(dragButton.Button.Width + 2, dragButton.Button.Height + 2);
    Graphics graphicsDragFormBitmap = Graphics.FromImage(dragFormBitmap);
    // draw the portion of the tinyTaskbar-Form that represents the taskbarButton to drag
    graphicsDragFormBitmap.DrawImage(sourceBitmap, 1, 1,
        new Rectangle(4 + buttonNr * buttonWidth, 0, 
                      dragButton.Button.Width, drag.Button.Height),
           GraphicsUnit.Pixel);
    // ...
    Invoke(new MethodInvoker(delegate () {
        // ...
        dragForm = new Form {
            Location = new Point(dragWinLocationX,
                Screen.PrimaryScreen.WorkingArea.Height - draggedTaskbarButton.Button.Height),
            FormBorderStyle = FormBorderStyle.None,
            StartPosition = FormStartPosition.Manual,
            BackgroundImage = dragFormBitmap
        };
        // ...
        dragForm.Show();
        // has to be set after Show(), otherwise Windows sets a minimum size
        dragForm.Size = new Size(dragButton.Button.Width + 2, dragButton.Button.Height + 2);
    }));
}

Parallel Programming: Locking Critical Code-Blocks

You have here two methods that handle events and have to manipulate the same resouces.
These two methods can be called multiple times in a very short time span, so we have to assure that only one thread at a time manipulates these resources.

The lock statement acquires the mutual-exclusion lock for a given object, executes a statement block, and then releases the lock. While a lock is held, the thread that holds the lock can again acquire and release the lock. Any other thread is blocked from acquiring the lock and waits until the lock is released.

C#
public partial class MainForm : ShellLib.ApplicationDesktopToolbar {
    private object processChangeLock = new object();
    //...
    private void NewProcessEventHandler(object sender, EventArrivedEventArgs eventArgs) {
        // ...
        // first thread puts a lock here, following threads wait here until
        // the first thread has released this lock (this lock exists in other places)
        lock (processChangeLock) {
            // attempt to add the taskbarButton, if added highlight the button
            if (AddTaskbarButtonFromProcessId(processId)) {
                SetButtonHighlighted(taskbarButtons.Count - 1); 
            }
        }
    }

    private void StopProcessEventHandler(object sender, EventArrivedEventArgs eventArgs) {
        // first thread puts a lock here, following threads wait here until
        // the first thread has released this lock (this lock exists in other places) 
        lock (processChangeLock) {
            for (int buttonNr = 0; buttonNr < taskbarButtons.Count; buttonNr++) {
                // find the concerned taskbarButton by checking the processId
                if (GetProcessId(eventArgs) == taskbarButtons[buttonNr].ProcessId) {
                    // remove taskbarButton from the collection of MainForm's controls 
                    // and taskbarButtons-list 
                    RemoveTaskbarButton(buttonNr);
                    break;
                }
            }
        } 
    }
}

You should never use public objects for locks because they may be locked by external code.

Finding a Window of Any Running Process and Accessing It

WIN32 Function

C#
HWND FindWindow(LPCSTR lpClassName, LPCSTR lpWindowName );

Retrieves a handle to the top-level window whose class name and window name match the specified strings. This function does not search child windows. The search is case-insensitive.

WIN32 Function

C#
HWND FindWindowExA(HWND hWndParent, HWND hWndChildAfter, LPCSTR lpszClass, LPCSTR lpszWindow);

Retrieves a handle to a window whose class name and window name match the specified strings. The function searches child windows of the specified parent window, beginning with the one following the specified child window. The search is case-insensitive.

C#
private void GetSystrayArea() {
    // gets main handle of notification area window by its class name                  
    IntPtr hWndTray = NativeMethods.FindWindow("Shell_TrayWnd", null);
    if (hWndTray != IntPtr.Zero) {
        // finds the window with the whole notification area
        //                        
        hTrayNotifyWnd = 
            NativeMethods.FindWindowEx(hWndTray, IntPtr.Zero, "TrayNotifyWnd", null);
    }
    if (hTrayNotifyWnd == IntPtr.Zero) return;
    // ...
}

private void RefreshSystrayProc(object notUsed) {
    // ...
    // here we get the content of the window found
    NativeMethods.PrintWindow(hTrayNotifyWnd, hdcSystraySourceBitmap, 0);
    // ...
}

Refreshing a Control Uses a Lot of Processor Time, Do Fast Refresh Only if Mouse Is Over the Control

By subscribing to the matching events, the refresh-time (the interval of refreshSystray Timer) is set to
Settings.Default.SystrayFastInterval (1500 ms) on MouseEnter
Settings.Default.SystraySlowInterval (5000 ms) on MouseLeave.

C#
private void GetSystrayArea() {
    // ...
    Invoke(new MethodInvoker(delegate () {
        // create the pictureBox that gets the notification area
        systrayBox = new PictureBox();
        // subscribe to these events to change refresh interval of the systrayBox
        systrayBox.MouseEnter += delegate (object sender, EventArgs e) {
                refreshSystray.Change(0, Settings.Default.SystrayFastInterval);
            };
        systrayBox.MouseLeave += delegate (object sender, EventArgs e) {
                refreshSystray.Change(0, Settings.Default.SystraySlowInterval);
            };
        // add systrayBox to MainForm's controls
        Controls.Add(systrayBox);
    }));

    refreshSystray = new System.Threading.Timer(
        RefreshSystrayProc, null, 500, Settings.Default.SystraySlowInterval
    );
}

Retrieving the Small Window Icon (Which Is Shown Next to the Window Text in the Titlebar)

Got this snippet form an answer at stackoverflow.com. It's very useful, so I wanted to spread it.
For the WM_GETICON message, the ICON_SMALL2 parameter means:
Retrieve the small icon provided by the application. If the application does not provide one, the system uses the system-generated icon for that window.
Didn't check the GetClassLong + LoadIcon stuff, don't know what it's for (probably to provide an icon if SendMessage returns none).

C#
public Image GetSmallWindowIcon(IntPtr hWnd) {
    try {
        IntPtr hIcon = default(IntPtr);
        // send WM_GETICON message to the window concerned
        hIcon = NativeMethods.SendMessage(
            hWnd, NativeMethods.WM_GETICON, NativeMethods.ICON_SMALL2, IntPtr.Zero);

        if (hIcon == IntPtr.Zero) { hIcon = GetClassLongPtr(hWnd, NativeMethods.GCL_HICON); }
        if (hIcon == IntPtr.Zero) {
            hIcon = NativeMethods.LoadIcon(IntPtr.Zero, (IntPtr) 0x7F00/*IDI_APPLICATION*/);
        }
        if (hIcon != IntPtr.Zero) {
            return new Bitmap(Icon.FromHandle(hIcon).ToBitmap(), 16, 16);
        }
        else return null;
    }
    catch (Exception) { return null; }
}

private IntPtr GetClassLongPtr(IntPtr hWnd, int nIndex) {
    if (IntPtr.Size == 4) {
        return new IntPtr((long) NativeMethods.GetClassLongPtr32(hWnd, nIndex));
    }
    else return { NativeMethods.GetClassLongPtr64(hWnd, nIndex); }
}

Have fun!

History

  • 9th January, 2020: Initial version

License

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


Written By
President of The bird feeding company
France France
Coding 4 fun.

Comments and Discussions

 
GeneralMy vote of 5 Pin
LightTempler9-Jan-20 7:29
LightTempler9-Jan-20 7:29 

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.