Більшість застосунків працюють у власному "пісочнику" — вони отримують події тільки коли мають фокус, запускаються вручну користувачем, і завершуються при закритті вікна. Проте існує клас програм, що вимагають глибшої інтеграції з операційною системою: вони мають реагувати на події навіть без фокусу, запускатися автоматично при завантаженні системи, працювати у фоні без UI.
Розглянемо чотири сценарії, що демонструють необхідність таких можливостей:
Сценарій перший: Глобальні гарячі клавіші. Ви розробляєте утиліту для створення скріншотів. Користувач має натиснути Ctrl+Shift+S у будь-якому застосунку — і ваша програма має миттєво зробити знімок екрану, навіть якщо вона згорнута у трей. Звичайні події клавіатури не працюють без фокусу — потрібен глобальний hotkey через RegisterHotKey() або keyboard hook через SetWindowsHookEx().
Сценарій другий: Моніторинг активності користувача. Компанія розробляє time-tracking застосунок для фрілансерів. Програма має відстежувати, в якому застосунку користувач працює, скільки часу проводить у кожному, коли робить паузи. Для цього потрібен low-level mouse hook та keyboard hook, що перехоплюють всі події миші та клавіатури у системі, незалежно від активного вікна.
Сценарій третій: Фонова служба без UI. Антивірус має постійно працювати у фоні, сканувати файли при їх відкритті, оновлювати бази даних, і все це без втручання користувача. Звичайний застосунок завершиться при виході користувача з системи — потрібна Windows Service, що запускається при завантаженні ОС, працює під системним акаунтом, і не залежить від сесії користувача.
Сценарій четвертий: Tray-застосунок з контекстним меню. Ви створили утиліту для управління буфером обміну. Вона має постійно працювати у фоні, відображатися як іконка у system tray (біля годинника), і при кліку правою кнопкою показувати меню з історією буфера. Це вимагає NotifyIcon та інтеграції з Windows Shell.
Ця тема — про інструменти, що дозволяють вашим програмам стати "громадянами першого класу" у Windows: реагувати на системні події, працювати у фоні, інтегруватися з Shell.
Hook (гачок) — це механізм Windows, що дозволяє застосунку "підчепитися" до ланцюжка обробки системних подій. Коли відбувається подія (натискання клавіші, рух миші, створення вікна), Windows спочатку викликає всі зареєстровані hooks, і тільки потім передає подію цільовому застосунку.
Аналогія: уявіть конвеєр на заводі. Деталь (подія) рухається по конвеєру, і на кожному етапі (hook) робітник може її оглянути, змінити, або навіть зупинити конвеєр. Windows hooks працюють аналогічно — ваш код може "оглянути" кожну подію у системі.
Windows підтримує 15 типів hooks, кожен для певного класу подій:
Win+L)GetMessage() або PeekMessage(). Дозволяє модифікувати або фільтрувати повідомлення до їх обробки.Hooks можуть бути двох типів:
Thread-Specific Hook — працює тільки для конкретного потоку (зазвичай вашого). Події з інших потоків/процесів не перехоплюються. Простіший у реалізації, не вимагає DLL.
Global Hook — працює для всіх потоків у системі. Вимагає:
WH_KEYBOARD_LL та WH_MOUSE_LL працюють у вашому процесі, але отримують події з усієї системи (не потрібна DLL)HHOOK SetWindowsHookEx(
int idHook, // Тип hook (WH_KEYBOARD_LL, WH_MOUSE_LL, тощо)
HOOKPROC lpfn, // Вказівник на hook procedure (callback)
HINSTANCE hmod, // Handle на DLL (NULL для LL hooks)
DWORD dwThreadId // ID потоку (0 = global hook)
);
Параметри:
idHook — константа типу hook (WH_KEYBOARD_LL = 13, WH_MOUSE_LL = 14)lpfn — вказівник на вашу функцію-callback, що оброблятиме подіїhmod — handle на DLL (для global hooks) або NULL (для LL hooks)dwThreadId — ID потоку для thread-specific hook, або 0 для globalПовертає: Handle на hook (HHOOK), або NULL при помилці.
Ваша hook procedure має сигнатуру:
LRESULT CALLBACK HookProc(
int nCode, // Код дії (HC_ACTION = обробити подію)
WPARAM wParam, // Додаткова інформація (залежить від типу hook)
LPARAM lParam // Вказівник на структуру з деталями події
);
Правила:
nCode < 0 — обов'язково викликати CallNextHookEx() без обробкиCallNextHookEx() для передачі події наступному hook у ланцюжкуСтворимо простий keylogger, що логує всі натискання клавіш у системі.
using System.Runtime.InteropServices;
using System.Diagnostics;
class KeyboardHook : IDisposable
{
// P/Invoke декларації
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
// Константи
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
// Делегат для hook procedure
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
// Структура для low-level keyboard input
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
public uint vkCode; // Virtual key code
public uint scanCode; // Scan code
public uint flags; // Flags
public uint time; // Timestamp
public IntPtr dwExtraInfo;
}
private IntPtr _hookId = IntPtr.Zero;
private HookProc _hookProc; // Зберігаємо делегат, щоб GC не зібрав його
public event Action<Keys, bool>? KeyPressed; // bool = isShiftPressed
public KeyboardHook()
{
_hookProc = HookCallback; // Зберігаємо посилання
}
public void Install()
{
if (_hookId != IntPtr.Zero)
return; // Вже встановлено
using var curProcess = Process.GetCurrentProcess();
using var curModule = curProcess.MainModule;
_hookId = SetWindowsHookEx(
WH_KEYBOARD_LL,
_hookProc,
GetModuleHandle(curModule?.ModuleName ?? ""),
0 // 0 = global hook
);
if (_hookId == IntPtr.Zero)
{
int error = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(error);
}
Console.WriteLine("✓ Keyboard hook встановлено");
}
public void Uninstall()
{
if (_hookId == IntPtr.Zero)
return;
UnhookWindowsHookEx(_hookId);
_hookId = IntPtr.Zero;
Console.WriteLine("✓ Keyboard hook видалено");
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
{
var hookStruct = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
var key = (Keys)hookStruct.vkCode;
// Перевіряємо, чи натиснуто Shift
bool isShiftPressed = (GetAsyncKeyState((int)Keys.ShiftKey) & 0x8000) != 0;
KeyPressed?.Invoke(key, isShiftPressed);
}
// ОБОВ'ЯЗКОВО викликати CallNextHookEx
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
public void Dispose()
{
Uninstall();
GC.SuppressFinalize(this);
}
}
// Використання
class Program
{
static void Main()
{
Console.WriteLine("═══════════════════════════════════════════════════");
Console.WriteLine(" KEYBOARD HOOK DEMO");
Console.WriteLine("═══════════════════════════════════════════════════");
Console.WriteLine("\nНатискайте клавіші (Ctrl+C для виходу)\n");
using var hook = new KeyboardHook();
hook.KeyPressed += (key, isShift) =>
{
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
var shiftIndicator = isShift ? " [SHIFT]" : "";
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write($"[{timestamp}] ");
Console.ResetColor();
Console.WriteLine($"{key}{shiftIndicator}");
};
hook.Install();
// Message loop (обов'язковий для hooks)
Console.WriteLine("Hook активний. Натисніть Ctrl+C для зупинки...\n");
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
};
// Простий message loop
while (!cts.Token.IsCancellationRequested)
{
Thread.Sleep(100);
}
Console.WriteLine("\n✓ Завершення...");
}
}
Створимо утиліту для відстеження активності миші: рух, кліки, час простою.
using System.Runtime.InteropServices;
using System.Diagnostics;
class MouseHook : IDisposable
{
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
private const int WH_MOUSE_LL = 14;
private const int WM_MOUSEMOVE = 0x0200;
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_RBUTTONDOWN = 0x0204;
private const int WM_MBUTTONDOWN = 0x0207;
private const int WM_MOUSEWHEEL = 0x020A;
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
private struct MSLLHOOKSTRUCT
{
public POINT pt;
public uint mouseData;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int X;
public int Y;
}
private IntPtr _hookId = IntPtr.Zero;
private HookProc _hookProc;
public event Action<int, int>? MouseMoved;
public event Action<int, int, string>? MouseClicked;
public event Action<int>? MouseWheelScrolled;
private DateTime _lastActivity = DateTime.Now;
private long _totalMoves = 0;
private long _totalClicks = 0;
private double _totalDistance = 0;
private POINT _lastPoint;
public MouseHook()
{
_hookProc = HookCallback;
}
public void Install()
{
if (_hookId != IntPtr.Zero)
return;
using var curProcess = Process.GetCurrentProcess();
using var curModule = curProcess.MainModule;
_hookId = SetWindowsHookEx(
WH_MOUSE_LL,
_hookProc,
GetModuleHandle(curModule?.ModuleName ?? ""),
0
);
if (_hookId == IntPtr.Zero)
{
int error = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(error);
}
Console.WriteLine("✓ Mouse hook встановлено");
}
public void Uninstall()
{
if (_hookId == IntPtr.Zero)
return;
UnhookWindowsHookEx(_hookId);
_hookId = IntPtr.Zero;
Console.WriteLine("✓ Mouse hook видалено");
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
var hookStruct = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
_lastActivity = DateTime.Now;
switch ((int)wParam)
{
case WM_MOUSEMOVE:
_totalMoves++;
if (_lastPoint.X != 0 || _lastPoint.Y != 0)
{
double dx = hookStruct.pt.X - _lastPoint.X;
double dy = hookStruct.pt.Y - _lastPoint.Y;
_totalDistance += Math.Sqrt(dx * dx + dy * dy);
}
_lastPoint = hookStruct.pt;
MouseMoved?.Invoke(hookStruct.pt.X, hookStruct.pt.Y);
break;
case WM_LBUTTONDOWN:
_totalClicks++;
MouseClicked?.Invoke(hookStruct.pt.X, hookStruct.pt.Y, "Left");
break;
case WM_RBUTTONDOWN:
_totalClicks++;
MouseClicked?.Invoke(hookStruct.pt.X, hookStruct.pt.Y, "Right");
break;
case WM_MBUTTONDOWN:
_totalClicks++;
MouseClicked?.Invoke(hookStruct.pt.X, hookStruct.pt.Y, "Middle");
break;
case WM_MOUSEWHEEL:
int delta = (short)((hookStruct.mouseData >> 16) & 0xFFFF);
MouseWheelScrolled?.Invoke(delta);
break;
}
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
public TimeSpan GetIdleTime() => DateTime.Now - _lastActivity;
public long GetTotalMoves() => _totalMoves;
public long GetTotalClicks() => _totalClicks;
public double GetTotalDistance() => _totalDistance;
public void Dispose()
{
Uninstall();
GC.SuppressFinalize(this);
}
}
// Використання
class Program
{
static async Task Main()
{
Console.WriteLine("═══════════════════════════════════════════════════");
Console.WriteLine(" MOUSE ACTIVITY TRACKER");
Console.WriteLine("═══════════════════════════════════════════════════\n");
using var hook = new MouseHook();
hook.MouseClicked += (x, y, button) =>
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {button} Click at ({x}, {y})");
Console.ResetColor();
};
hook.Install();
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
};
// Статистика кожні 5 секунд
var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
Console.WriteLine("Відстежування активності... (Ctrl+C для зупинки)\n");
try
{
while (await timer.WaitForNextTickAsync(cts.Token))
{
var idle = hook.GetIdleTime();
var moves = hook.GetTotalMoves();
var clicks = hook.GetTotalClicks();
var distance = hook.GetTotalDistance();
Console.WriteLine($"\n📊 Статистика:");
Console.WriteLine($" Рухів миші: {moves:N0}");
Console.WriteLine($" Кліків: {clicks:N0}");
Console.WriteLine($" Пройдено: {distance / 1000:F1} метрів (на екрані)");
Console.WriteLine($" Час простою: {idle.TotalSeconds:F1}s");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("\n✓ Зупинено");
}
}
}
Альтернатива hooks для глобальних гарячих клавіш — функція RegisterHotKey(). Вона простіша у використанні, але менш гнучка.
✅ Простота
GetMessage(), не потрібно обробляти кожну подію клавіатури. Просто реєструєте комбінацію — і отримуєте повідомлення WM_HOTKEY.✅ Продуктивність
✅ Системна Підтримка
❌ Обмеження
A або F1 без модифікаторів. Hooks дозволяють будь-які комбінації.using System.Runtime.InteropServices;
using System.Drawing;
using System.Drawing.Imaging;
class HotkeyManager : IDisposable
{
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
// Модифікатори
private const uint MOD_ALT = 0x0001;
private const uint MOD_CONTROL = 0x0002;
private const uint MOD_SHIFT = 0x0004;
private const uint MOD_WIN = 0x0008;
// Virtual key codes
private const uint VK_S = 0x53;
private const uint VK_F12 = 0x7B;
private const int WM_HOTKEY = 0x0312;
private readonly IntPtr _hwnd;
private readonly Dictionary<int, Action> _hotkeys = new();
private int _nextId = 1;
public HotkeyManager(IntPtr hwnd)
{
_hwnd = hwnd;
}
public int Register(uint modifiers, uint key, Action callback)
{
int id = _nextId++;
if (!RegisterHotKey(_hwnd, id, modifiers, key))
{
int error = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(error);
}
_hotkeys[id] = callback;
return id;
}
public void Unregister(int id)
{
if (_hotkeys.ContainsKey(id))
{
UnregisterHotKey(_hwnd, id);
_hotkeys.Remove(id);
}
}
public void ProcessMessage(int msg, IntPtr wParam, IntPtr lParam)
{
if (msg == WM_HOTKEY)
{
int id = wParam.ToInt32();
if (_hotkeys.TryGetValue(id, out var callback))
{
callback?.Invoke();
}
}
}
public void Dispose()
{
foreach (var id in _hotkeys.Keys.ToList())
{
UnregisterHotKey(_hwnd, id);
}
_hotkeys.Clear();
}
}
// Screenshot утиліта
class ScreenshotTool
{
public static void TakeScreenshot(string filename)
{
var bounds = Screen.PrimaryScreen.Bounds;
using var bitmap = new Bitmap(bounds.Width, bounds.Height);
using var graphics = Graphics.FromImage(bitmap);
graphics.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);
bitmap.Save(filename, ImageFormat.Png);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"✓ Screenshot збережено: {filename}");
Console.ResetColor();
}
}
// Message-only window для hotkeys
class HotkeyWindow : Form
{
private HotkeyManager? _hotkeyManager;
public HotkeyWindow()
{
// Створюємо невидиме вікно
this.ShowInTaskbar = false;
this.WindowState = FormWindowState.Minimized;
this.FormBorderStyle = FormBorderStyle.None;
this.Opacity = 0;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
_hotkeyManager = new HotkeyManager(this.Handle);
// Реєструємо Ctrl+Shift+S
_hotkeyManager.Register(
HotkeyManager.MOD_CONTROL | HotkeyManager.MOD_SHIFT,
HotkeyManager.VK_S,
() =>
{
string filename = $"screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.png";
ScreenshotTool.TakeScreenshot(filename);
}
);
// Реєструємо Ctrl+Alt+F12
_hotkeyManager.Register(
HotkeyManager.MOD_CONTROL | HotkeyManager.MOD_ALT,
HotkeyManager.VK_F12,
() =>
{
Console.WriteLine("🔔 Hotkey Ctrl+Alt+F12 натиснуто!");
Application.Exit();
}
);
Console.WriteLine("✓ Hotkeys зареєстровано:");
Console.WriteLine(" • Ctrl+Shift+S - Зробити скріншот");
Console.WriteLine(" • Ctrl+Alt+F12 - Вийти");
}
protected override void WndProc(ref Message m)
{
_hotkeyManager?.ProcessMessage(m.Msg, m.WParam, m.LParam);
base.WndProc(ref m);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_hotkeyManager?.Dispose();
}
base.Dispose(disposing);
}
}
// Точка входу
class Program
{
[STAThread]
static void Main()
{
Console.WriteLine("═══════════════════════════════════════════════════");
Console.WriteLine(" SCREENSHOT TOOL");
Console.WriteLine("═══════════════════════════════════════════════════\n");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new HotkeyWindow());
}
}
Ctrl+Shift+S — миттєво створюється скріншот! Програма працює у фоні та реагує на hotkey з будь-якого місця. Це той самий механізм, що використовують Lightshot, ShareX, Greenshot.Windows Service — це спеціальний тип застосунку, що:
Типові use cases:
Windows Service складається з трьох компонентів:
.exe файл з логікою служби.NET надає клас BackgroundService для створення служб. Це абстракція над старим ServiceBase API.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class MyBackgroundService : BackgroundService
{
private readonly ILogger<MyBackgroundService> _logger;
public MyBackgroundService(ILogger<MyBackgroundService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Service запущено о {time}", DateTimeOffset.Now);
try
{
while (!stoppingToken.IsCancellationRequested)
{
// Ваша логіка тут
_logger.LogInformation("Service працює о {time}", DateTimeOffset.Now);
// Чекаємо 10 секунд або до cancellation
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
catch (OperationCanceledException)
{
// Нормальне завершення
_logger.LogInformation("Service зупиняється...");
}
catch (Exception ex)
{
_logger.LogError(ex, "Критична помилка в service");
throw;
}
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service отримав команду Stop");
return base.StopAsync(cancellationToken);
}
}
dotnet new worker -n MyWindowsService
cd MyWindowsService
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class FileMonitorService : BackgroundService
{
private readonly ILogger<FileMonitorService> _logger;
private readonly string _watchPath;
private FileSystemWatcher? _watcher;
public FileMonitorService(ILogger<FileMonitorService> logger)
{
_logger = logger;
_watchPath = @"C:\MonitoredFolder";
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("File Monitor Service запущено");
// Створюємо папку якщо не існує
if (!Directory.Exists(_watchPath))
{
Directory.CreateDirectory(_watchPath);
_logger.LogInformation("Створено папку: {path}", _watchPath);
}
// Налаштовуємо FileSystemWatcher
_watcher = new FileSystemWatcher(_watchPath)
{
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
Filter = "*.*",
EnableRaisingEvents = true
};
_watcher.Created += OnFileCreated;
_watcher.Changed += OnFileChanged;
_watcher.Deleted += OnFileDeleted;
_logger.LogInformation("Моніторинг папки: {path}", _watchPath);
return Task.CompletedTask;
}
private void OnFileCreated(object sender, FileSystemEventArgs e)
{
_logger.LogInformation("Файл створено: {name}", e.Name);
}
private void OnFileChanged(object sender, FileSystemEventArgs e)
{
_logger.LogInformation("Файл змінено: {name}", e.Name);
}
private void OnFileDeleted(object sender, FileSystemEventArgs e)
{
_logger.LogInformation("Файл видалено: {name}", e.Name);
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("File Monitor Service зупиняється");
_watcher?.Dispose();
return base.StopAsync(cancellationToken);
}
}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
// Додаємо підтримку Windows Services
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "File Monitor Service";
});
// Реєструємо нашу службу
builder.Services.AddHostedService<FileMonitorService>();
// Налаштовуємо логування
builder.Services.AddLogging(logging =>
{
logging.AddEventLog(settings =>
{
settings.SourceName = "FileMonitorService";
});
});
var host = builder.Build();
await host.RunAsync();
dotnet publish -c Release -r win-x64 --self-contained
# Запустіть PowerShell від імені адміністратора
# Встановлення
sc.exe create "FileMonitorService" binPath="C:\Path\To\MyWindowsService.exe"
# Налаштування автозапуску
sc.exe config "FileMonitorService" start=auto
# Запуск
sc.exe start "FileMonitorService"
# Перевірка статусу
sc.exe query "FileMonitorService"
# Зупинка
sc.exe stop "FileMonitorService"
# Видалення
sc.exe delete "FileMonitorService"
C:\MonitoredFolder — подія з'явиться у Event Viewer (Windows Logs → Application). Служба працює навіть якщо ніхто не увійшов у систему.Tray Application (або System Tray App) — це застосунок, що відображається як іконка у system tray (біля годинника). Він працює у фоні, не має головного вікна, і взаємодіє з користувачем через контекстне меню іконки.
Приклади: Dropbox, OneDrive, Spotify, Discord, антивіруси.
.NET надає клас NotifyIcon (у System.Windows.Forms) для створення tray застосунків.
using System.Windows.Forms;
using System.Drawing;
class TrayApp : ApplicationContext
{
private NotifyIcon _notifyIcon;
private ContextMenuStrip _contextMenu;
public TrayApp()
{
// Створюємо контекстне меню
_contextMenu = new ContextMenuStrip();
_contextMenu.Items.Add("Показати повідомлення", null, ShowNotification);
_contextMenu.Items.Add("Статистика", null, ShowStats);
_contextMenu.Items.Add(new ToolStripSeparator());
_contextMenu.Items.Add("Вийти", null, Exit);
// Створюємо іконку у tray
_notifyIcon = new NotifyIcon
{
Icon = SystemIcons.Application, // Або завантажте власну іконку
ContextMenuStrip = _contextMenu,
Text = "My Tray App",
Visible = true
};
// Подія подвійного кліку
_notifyIcon.DoubleClick += (s, e) => ShowNotification(s, e);
// Показуємо balloon tip при запуску
_notifyIcon.ShowBalloonTip(
3000,
"Tray App",
"Застосунок запущено у фоні",
ToolTipIcon.Info
);
}
private void ShowNotification(object? sender, EventArgs e)
{
_notifyIcon.ShowBalloonTip(
5000,
"Повідомлення",
$"Поточний час: {DateTime.Now:HH:mm:ss}",
ToolTipIcon.Info
);
}
private void ShowStats(object? sender, EventArgs e)
{
var uptime = DateTime.Now - Process.GetCurrentProcess().StartTime;
MessageBox.Show(
$"Uptime: {uptime.TotalMinutes:F1} хвилин\n" +
$"Memory: {Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024} MB",
"Статистика",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
}
private void Exit(object? sender, EventArgs e)
{
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
Application.Exit();
}
}
class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TrayApp());
}
}
Створимо tray-застосунок для управління історією буфера обміну.
using System.Windows.Forms;
using System.Drawing;
using System.Collections.Generic;
class ClipboardManager : ApplicationContext
{
private NotifyIcon _notifyIcon;
private ContextMenuStrip _contextMenu;
private List<string> _clipboardHistory = new();
private const int MaxHistorySize = 10;
private System.Threading.Timer? _clipboardTimer;
private string _lastClipboard = "";
public ClipboardManager()
{
InitializeUI();
StartClipboardMonitoring();
}
private void InitializeUI()
{
_contextMenu = new ContextMenuStrip();
UpdateContextMenu();
_notifyIcon = new NotifyIcon
{
Icon = SystemIcons.Application,
ContextMenuStrip = _contextMenu,
Text = "Clipboard Manager",
Visible = true
};
_notifyIcon.ShowBalloonTip(
2000,
"Clipboard Manager",
"Моніторинг буфера обміну активний",
ToolTipIcon.Info
);
}
private void StartClipboardMonitoring()
{
// Перевіряємо буфер кожні 500ms
_clipboardTimer = new System.Threading.Timer(
CheckClipboard,
null,
TimeSpan.Zero,
TimeSpan.FromMilliseconds(500)
);
}
private void CheckClipboard(object? state)
{
try
{
if (Clipboard.ContainsText())
{
string text = Clipboard.GetText();
if (!string.IsNullOrWhiteSpace(text) && text != _lastClipboard)
{
_lastClipboard = text;
AddToHistory(text);
}
}
}
catch
{
// Ігноруємо помилки доступу до буфера
}
}
private void AddToHistory(string text)
{
// Обмежуємо довжину для відображення
string displayText = text.Length > 50
? text.Substring(0, 47) + "..."
: text;
_clipboardHistory.Insert(0, text);
if (_clipboardHistory.Count > MaxHistorySize)
{
_clipboardHistory.RemoveAt(_clipboardHistory.Count - 1);
}
// Оновлюємо меню у UI потоці
_notifyIcon.ContextMenuStrip?.Invoke((Action)UpdateContextMenu);
}
private void UpdateContextMenu()
{
_contextMenu.Items.Clear();
if (_clipboardHistory.Count == 0)
{
_contextMenu.Items.Add("(історія порожня)", null, null).Enabled = false;
}
else
{
for (int i = 0; i < _clipboardHistory.Count; i++)
{
string text = _clipboardHistory[i];
string displayText = text.Length > 50
? text.Substring(0, 47) + "..."
: text;
var item = new ToolStripMenuItem(
$"{i + 1}. {displayText}",
null,
(s, e) => CopyToClipboard(text)
);
_contextMenu.Items.Add(item);
}
}
_contextMenu.Items.Add(new ToolStripSeparator());
_contextMenu.Items.Add("Очистити історію", null, ClearHistory);
_contextMenu.Items.Add(new ToolStripSeparator());
_contextMenu.Items.Add("Вийти", null, Exit);
}
private void CopyToClipboard(string text)
{
Clipboard.SetText(text);
_notifyIcon.ShowBalloonTip(
1000,
"Скопійовано",
"Текст скопійовано у буфер обміну",
ToolTipIcon.Info
);
}
private void ClearHistory(object? sender, EventArgs e)
{
_clipboardHistory.Clear();
UpdateContextMenu();
_notifyIcon.ShowBalloonTip(
1000,
"Очищено",
"Історія буфера обміну очищена",
ToolTipIcon.Info
);
}
private void Exit(object? sender, EventArgs e)
{
_clipboardTimer?.Dispose();
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
Application.Exit();
}
}
class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ClipboardManager());
}
}
Windows Hooks
SetWindowsHookEx() — встановлення hookWH_KEYBOARD_LL, WH_MOUSE_LL — low-level hooks без DLLCallNextHookEx() — обов'язковий виклик для ланцюжкаGlobal Hotkeys
RegisterHotKey() — простіша альтернатива hooksWM_HOTKEY — повідомлення при натисканніWindows Services
BackgroundService — сучасний підхід (.NET 6+)sc.exe — управління службамиTray Applications
NotifyIcon — іконка у system trayContextMenuStrip — контекстне менюShowBalloonTip() — сповіщенняApplicationContext — застосунок без головного вікнаСтворіть утиліту для логування активності користувача:
Реалізуйте повноцінний інструмент для скріншотів:
Ctrl+Shift+S — скріншот всього екрануCtrl+Shift+A — скріншот активного вікнаCtrl+Shift+R — скріншот області (selection)Створіть Windows Service для моніторингу системи:
Реалізуйте повноцінний recorder макросів:
Hooks Performance
CallNextHookEx()Service Reliability
Tray UX
Security
Реєстр Windows — Центральна База Конфігурації Системи
Повний розбір Windows Registry — від архітектури Hives та Keys до практичних прикладів автозапуску програм, файлових асоціацій, персоналізації системи та моніторингу змін. Теорія, API та вау-ефекти з детальними прикладами.
Системне Програмування C# (Windows) — 07.system-programming-windows
Нова папка 07.system-programming-windows. Модуль 06 ігнорується (буде видалений). 20 файлів, 5 блоків. Фокус: багатопоточність та асинхронність від А до Я.