Creating a Custom Windows Taskbar with .NET
2025-09-05One 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::
- Always on top
- No visible tray or primary taskbar icon
- Workable screen space that excludes the taskbar area
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);

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:
- Dynamic shortcuts with icons - click to launch, right-click to delete, middle-click to drag and reposition
- Displaying the currently playing song from Spotify or Foobar2000
- Play/Pause and Next media controls for controlling music playback
- Music favorites - buttons to be able to add the current playing song to an
m3uplaylist that could be started at will - Philips Hue dimmer control
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:

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.