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






4.18/5 (7 votes)
With some code snippets to give beginners (+ others) ideas
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
- Creating the application desktop toolbar
- Refreshing a control uses a lot of processor time, do fast refresh only if mouse is over the control
- Retrieving the small window icon (which is shown next to the window text in the titlebar)
- Creating a List of TaskbarButtons with the exact information needed
- Making the access of project settings shorter
- Checking the type of a variable
- Subscribing to Events to get informed if processes are started or stopped
- Using User-Settings
- Drawing with the Graphics-Class and with WIN32
- Parallel Programming: locking critical code-blocks
- Finding a window of any running process and accessing it
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
.
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 Arik Poznanski 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
// 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
// ...
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:
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.
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
'.
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
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.
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.
IntPtr hdcSourceBitmap = graphicsSourceBitmap.GetHdc();
This gets the device context handle of the Graphics
you need to draw with WIN32
.
graphicsSourceBitmap.ReleaseHdc(hdcSourceBitmap);
After drawing, you have to release the device context handle, because it's an unmanaged resource.
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
'.
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:
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.
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 lock
s because they may be locked by external code.
Finding a Window of Any Running Process and Accessing It
WIN32 Function
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
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.
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
.
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).
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