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
.
namespace TinyTaskbar {
public partial class MainForm : ShellLib.ApplicationDesktopToolbar {
public MainForm() {
CheckForIllegalCrossThreadCalls = false;
InitializeComponent();
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
private class TaskbarButton {
public int Index { get; set; }
public Button Button;
public int ProcessId;
public IntPtr WindowHandle;
}
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 {
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) {
Task.Run(() => {
watchProcessStarted =
new ManagementEventWatcher(
new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace")
);
watchProcessStarted.EventArrived +=
new EventArrivedEventHandler(NewProcessEventHandler);
watchProcessStarted.Start();
watchProcessStopped =
new ManagementEventWatcher(
new WqlEventQuery("SELECT * FROM Win32_ProcessStopTrace")
);
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 {
private UserSettings userSettings = new UserSettings();
private void MainForm_Load(object sender, EventArgs e) {
if (userSettings.SavedButtonInfos != null) {
foreach ((int ProcessId, int WindowHandle)
buttonInfo in userSettings.SavedButtonInfos) {
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();
}
}
internal class UserSettings : ApplicationSettingsBase {
[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);
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() {
Bitmap sourceBitmap = new Bitmap(Width, Height);
Graphics graphicsSourceBitmap = Graphics.FromImage(sourceBitmap);
IntPtr hdcSourceBitmap = graphicsSourceBitmap.GetHdc();
NativeMethods.PrintWindow(Handle, hdcSourceBitmap, 0);
graphicsSourceBitmap.ReleaseHdc(hdcSourceBitmap);
dragFormBitmap = new Bitmap(dragButton.Button.Width + 2, dragButton.Button.Height + 2);
Graphics graphicsDragFormBitmap = Graphics.FromImage(dragFormBitmap);
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();
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) {
lock (processChangeLock) {
if (AddTaskbarButtonFromProcessId(processId)) {
SetButtonHighlighted(taskbarButtons.Count - 1);
}
}
}
private void StopProcessEventHandler(object sender, EventArrivedEventArgs eventArgs) {
lock (processChangeLock) {
for (int buttonNr = 0; buttonNr < taskbarButtons.Count; buttonNr++) {
if (GetProcessId(eventArgs) == taskbarButtons[buttonNr].ProcessId) {
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() {
IntPtr hWndTray = NativeMethods.FindWindow("Shell_TrayWnd", null);
if (hWndTray != IntPtr.Zero) {
hTrayNotifyWnd =
NativeMethods.FindWindowEx(hWndTray, IntPtr.Zero, "TrayNotifyWnd", null);
}
if (hTrayNotifyWnd == IntPtr.Zero) return;
}
private void RefreshSystrayProc(object notUsed) {
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 () {
systrayBox = new PictureBox();
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);
};
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);
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);
}
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