Creating a Custom Windows Taskbar with .NET

2025-09-05

One of my first side projects that proved useful was creating a taskbar for my secondary monitor. Back in 2013, Window's support for multiple taskbar was lacking. You could have a taskbar on a secondary monitor, but it didn’t have much functionality. It also felt a bit redundant since it contained the same information as the primary taskbar. I figured I could do better.

I decided to create my own taskbar using .NET as a WPF application. The first thing I needed to figure out was what actually makes a taskbar a taskbar. A few things came to mind::

The first two points were easily achieved with XAML modifications to the main window:

WindowStyle="None"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"

The limited screen area was trickier. How could we prevent other windows from occupying the space used by the taskbar? In Windows, this is called the work area. Basically, it's the screen space available for user activity. You can control it with the native SystemParametersInfo function.

I ended up creating a helper service to manage the work area:

internal sealed class WorkAreaService : IWorkAreaService
{
	private const int SPI_SETWORKAREA = 0x002F;

	private enum WinIniFlags
	{
		SPIF_NONE = 0x000,
		SPIF_UPDATEINIFILE = 0x0001,
		SPIF_SENDWININICHANGE = 0x0002,
		SPIF_SENDCHANGE = SPIF_SENDWININICHANGE
	}

	public void SetWorkArea(Rect rect)
	{
		var successful = NativeMethods.SystemParametersInfo(SPI_SETWORKAREA, 0, ref rect, (int)WinIniFlags.SPIF_UPDATEINIFILE);

		if (!successful)
		{
			throw new Exception($"Win32 error code: {NativeMethods.LastWin32Error()}");
		}
	}

	public Rect GetSecondaryMonitorScreenBounds()
	{
		var displays = NativeMethods.QueryDisplays();
		if (displays.Count < 2)
		{
			throw new Exception("Could not find a secondary monitor");
		}
		var secondaryDisplayArea = displays[1].MonitorArea;

		return new Rect
		{
			Top = (int)secondaryDisplayArea.Top,
			Bottom = (int)secondaryDisplayArea.Bottom,
			Left = (int)secondaryDisplayArea.Left,
			Right = (int)secondaryDisplayArea.Right
		};
	}
}

With the native functions:

internal static class NativeMethods
{
	[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
	public static extern bool SystemParametersInfo(int uiAction, int uiParam, ref Rect pvParam, int fWinIni);
	
	[DllImport("user32.dll", CharSet = CharSet.Auto)]
	private static extern bool GetMonitorInfo(IntPtr hmon, ref MonitorInfo monitorinfo);
	
	[DllImport("user32.dll")]
	public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData);
	
	public static List<DisplayInfo> QueryDisplays()
	{
		var displays = new List<DisplayInfo>();
	
		EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero,
			delegate (IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData)
			{
				var monitor = new MonitorInfo();
				monitor.Size = (uint)Marshal.SizeOf(monitor);
				monitor.DeviceName = null;
				var success = GetMonitorInfo(hMonitor, ref monitor);
				if (success)
				{
					DisplayInfo displayinfo = new DisplayInfo
					{
						ScreenWidth = monitor.Monitor.Right - monitor.Monitor.Left,
						ScreenHeight = monitor.Monitor.Bottom - monitor.Monitor.Top
					};
					displayinfo.MonitorArea = new Windows.Foundation.Rect(
						monitor.Monitor.Left,
						monitor.Monitor.Top,
						displayinfo.ScreenWidth,
						displayinfo.ScreenHeight);
					displays.Add(displayinfo);
				}
				return true;
			}, IntPtr.Zero);
		return displays;
	}
	
	[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
	private struct MonitorInfo
	{
		public uint Size;
		public Rect Monitor;
		public Rect WorkArea;
		public uint Flags;
		[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
		public string DeviceName;
	}
}

SetWorkArea sets the work area, and GetSecondaryMonitorScreenBounds returns the secondary monitor area as a Windows.Foundation.Rect.

Using these functions, it was simple to get the secondary monitor work area, subtract the taskbar height, and set the new work area:

var newWorkArea = workAreaService.GetSecondaryMonitorScreenBounds();
newWorkArea.Bottom -= WINDOW_HEIGHT;
workAreaService.SetWorkArea(newWorkArea);

Preview of maximizing a window
A preview of a maximizing window preview - Windows respects the work area!

With the taskbar behaving like a real taskbar, I started adding addition features:

Admittedly some of these features are tailored to me and might not be useful to everyone. A future improvement could be a more generic plugin interface, making it easier to add or remove functionality..

Here’s what it looks like today on my secondary monitor: Example of the final taskbar
Icons, settings, and the Philips Hue dimmer are on the left, while the song viewer and media controls are on the right.

The full source code of the project is available on GitHub if you want to check it out.


Back to posts.