Приветствую, уважаемый читатель. Ранее, я уже писал подобную статью (ссылка на нее есть ниже). Рекомендую прочесть ее прежде, чем переходить к этой, так как из первой части фундаментальные вещи остались не тронутыми.

ПЕРВАЯ ЧАСТЬ

Проанализировав статистику по предыдущей статье я понял, что этот проект может быть действительно актуален пользователям Windows. Решил последовать советам из комментариев и это того стоило.

Напомню главную задачу: фоновая программа, которая выводит в трей информацию о загрузки CPU и ОЗУ.

Как изменения были введены:

  • Вывод в трей иконок сразу с числовыми значением (ранее были сделаны статические иконки. А для того, чтобы было видно информацию, приходилось наводиться на значок курсором)

  • Градиентный переход от зеленого к красного с помощью линейной интерполяции для иконки загрузки CPU

  • Окно настройки (отключение вывода информации о процессоре или памяти, + возможность запускать окно настройки при запуске программы).

Каждый пункт постараюсь описать максимально качественно.

Для тех, кто использовал предыдущее решение.

В прошлый раз мы использовали готовые иконки. Так вот их можно смело удалять. Также нужно удалить их из .rc файла

IDR_CPUICON1 ICON "cpu4.ico"
 IDR_CPUICON2 ICON "cpu6.ico"
 IDR_CPUICON3 ICON "cpu5.ico"
 IDR_RAMICON0 ICON "ram0.ico"

И из Recourse.h

#define IDR_CPUICON1					131
 #define IDR_CPUICON2					132
 #define IDR_CPUICON3					133
 #define IDR_RAMICON0					134

Так как готовые иконки больше не используются, то наш список иконок больше не нужен. В методе инициализации убираем следующие строки

cpu_icons[0] = (HICON)LoadImage(hInstance, MAKEINTRESOURCE(IDR_CPUICON1), IMAGE_ICON, 64, 64, 0);
cpu_icons[1] = (HICON)LoadImage(hInstance, MAKEINTRESOURCE(IDR_CPUICON2), IMAGE_ICON, 64, 64, 0);
cpu_icons[2] = (HICON)LoadImage(hInstance, MAKEINTRESOURCE(IDR_CPUICON3), IMAGE_ICON, 64, 64, 0);

Общая подготовка

Для генерации иконок с числовыми значениями я использовал библиотеку GDI. Сначала выбор пал на нее, так как позволяет быстро рисовать изображения даже на старом железе в связи с чем нормально воспринимается частое обновление. Но возникла очень неприятная проблема - она не поддерживает альфа канал, только RGB. Из-за чего была иконка на фоне черного квадрата, что выглядело не очень красиво. Поэтому было принято решение использоваться более новую версию GDI+. Дабы не генерировать иконку постоянно после каждого тика, то создадим 2 хэш-таблицы. Если значения будут повторяться, то иконку будем тянуть из нее, таким образом избавимся от лишней перерисовки.

Для начала подключим библиотеку в наш проект:

#include <objidl.h>
#include <gdiplus.h>

#pragma comment(lib, "gdiplus.lib")

Заголовочный файл objidl.h не содержит инструментов для работы с графикой, но может быть необходимым для работы с GDI+. Все дело в том, что для инициализации GDI+ нужны функции, которые не входят в саму библиотеку.

Теперь добавим несколько "констант" в препроцессор

#define ICON_SIZE 64
#define ICON_CLICK (WM_APP + 1)
#define CPU_CLICK (WM_APP + 2)
#define RAM_CLICK (WM_APP + 3)
#define OPEN_AT_START (WM_APP + 4)
#define SETTINGS_MONITOR_NAME L"SETTINGS_MONITOR"
#define RAM_COLOR Gdiplus::Color(255, 200, 90, 240)

ICON_SIZE - размер иконки
ICON_CLICK - будем вызывать окно настроек по нажатию на одну из иконок
CPU_CLICK - для обработки выбора в checkbox CPU
RAM_CLICK - аналогично с ОЗУ
OPEN_AT_START - для обработки клика на checkbox, который отвечает за настройку "Открыть при запуске" окно настроек. Ниже объясню, зачем вообще эта настройка нужна
SETTINGS_MONITOR_NAME - имя переменной среды, которая будет хранить все настройки нашей программы.
RAM_COLOR - цвет иконки для ОЗУ. Я выбрал фиолетовый цвет. Можете подобрать понравившийся вам цвет и вписать его ARGB значение в это поле

Теперь напишем отдельный класс для checkbox и вынесем его в заголовочный файл checkbox.h, дабы не загромождать код. Делается это для более удобной работы с самим объектом, поскольку создается он через CreateWindowEx. Класс максимально простой:

checkbox.h
#include <windows.h>
#include <iostream>
#include <string>

class Checkbox
{
private:
	HWND parentWindow;
	HMENU hMenu;
	bool checked;
	HWND hwnd;
public:

	Checkbox(HWND hwnd_, HINSTANCE hInstance, UINT id_, LPCWCHAR text_, UINT x, UINT y, UINT width, UINT height) 
		: parentWindow(hwnd_), hMenu((HMENU)id_)
	{
		hwnd = CreateWindowEx(NULL, 
			L"BUTTON", 
			text_, 
			WS_VISIBLE | WS_CHILD | BS_CHECKBOX, 
			x, 
			y, 
			width, 
			height, 
			parentWindow, 
			hMenu, 
			hInstance, 
			nullptr);
		UnCheck();
	}

	void Check()
	{
		checked = true;
		SendMessage(hwnd, BM_SETCHECK, checked, 0);
	}

	void UnCheck()
	{
		checked = false;
		SendMessage(hwnd, BM_SETCHECK, checked, 0);
	}

	void Block()
	{
		EnableWindow(hwnd, FALSE);
	}

	void UnBlock()
	{
		EnableWindow(hwnd, TRUE);
	}

	bool Status()
	{
		return checked;
	}

	void Revers()
	{
		checked = !checked;
		SendMessage(hwnd, BM_SETCHECK, checked, 0);
	}
};

Не забудьте подключить хедер:

#include "checkbox.h"

Теперь добавим несколько новых переменных в проект:

Глобальные переменные
HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING];
WCHAR szWindowClass[MAX_LOADSTRING];
HANDLE htimer_ram = NULL;
HANDLE htimer_cpu = NULL;
NOTIFYICONDATA cpu_status_icon;
NOTIFYICONDATA ram_status_icon;
MEMORYSTATUSEX mem_info;
//новые
ULONG_PTR g_gdiplusToken = 0; //токен для использования GDI+
std::map<uint16_t, HICON> hash_cpu_icons; //хэш-таблица для хранения иконок CPU
std::map<uint16_t, HICON> hash_ram_icons; //аналогично для ОЗУ

byte show_window_flag; //флаг состояния окна 0x0 - скрыто; 0x5 - открыто
byte settings_param = 0b0111; //переменная настроек всей программы
//Последние 3 бита отвечают за все показ иконок и запуск окна при запуске
byte cpu_show = 0x2; // состояние иконки cpu. 0x2 - показано, 0x0 -  скрыто
byte ram_show = 0x2; // аналогично для озу

//указатели на 3 чекбокса настроек
Checkbox *cpu_checkbox;
Checkbox *ram_checkbox;
Checkbox *open_start_checkbox;

Генерация иконок

Реализуем пару функция для инициализации и очистки ресурсов, необходимых для библиотеки GDI+.

Работа с GDI+
void InitGDIPlus()
{
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, nullptr);
}

void ShutdownGDIPlus()
{
    if (g_gdiplusToken != 0)
        Gdiplus::GdiplusShutdown(g_gdiplusToken);
}

А теперь реализуем функцию для генерации иконки:

CreateIconWithNumber
ICON CreateIconWithNumber(int number, int size, int fontSize, Gdiplus::Color textColor)
{
    std::wstring text = std::to_wstring(number);

    //создаем битмапу с константой PixelFormat32bppARGB - 32 бита на пиксель:
    //по 8 бит на каждые 4 канала ARGB
    Gdiplus::Bitmap bitmap(size, size, PixelFormat32bppARGB);

    //объявляем класс, которые предоставляет инструменты для рисования на устройстве вывода
    Gdiplus::Graphics graphics(&bitmap);
    graphics.Clear(Gdiplus::Color(0, 0, 0, 0)); // Прозрачный фон

    //инициализируем шрифт
    Gdiplus::FontFamily fontFamily(L"Segoe UI");
    Gdiplus::Font font(&fontFamily, static_cast<Gdiplus::REAL>(fontSize), Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
    Gdiplus::SolidBrush brush(textColor);

    // Для вертикального центрирования измеряем высоту одной цифры
    Gdiplus::RectF charRect;
    graphics.MeasureString(L"0", 1, &font, Gdiplus::PointF(0, 0), &charRect);
    float y = (size - charRect.Height) / 2.0f;

    float x = 0.0f;
    float spacingFactor = 0.65f; // Коэффициент уменьшения межбуквенного интервала (больше => плотнее)

    for (wchar_t ch : text)
    {
        wchar_t str[2] = { ch, 0 };

        // Измеряем ширину символа
        graphics.MeasureString(str, 1, &font, Gdiplus::PointF(0, 0), &charRect);

        // Рисуем символ по координатам (x, y), вертикально выровнено по центру
        graphics.DrawString(str, 1, &font, Gdiplus::PointF(x, y), &brush);

        // Смещаем x с учётом коэффициента уменьшения межсимвольного интервала
        x += charRect.Width * spacingFactor;
    }

    HICON hIcon = nullptr;
    bitmap.GetHICON(&hIcon);
    return hIcon;
}

Решил ввести коэффициент уменьшения межбуквенного интервала, так как из-за стандартного интервала часть второй цифр обрезалась. А так можно регулировать и размер шрифта и межсимвольный интервал. По-моему, довольно гибкая настройка

В аргументах она принимает:

  • Число, которое будет выведено на иконке

  • размер самой иконки (в нашем случае 64)

  • размер шрифта

  • Цвет числа

Теперь немного изменим наши функции обратного вызова для таймером.

Таймер для ОЗУ
void CALLBACK TimerRAM(PVOID pVoid, BOOLEAN TimerOrWaitFired)
{
    static wchar_t ram_info[64];
    ZeroMemory(ram_info, 64);
    if (GlobalMemoryStatusEx(&mem_info)) {
        static uint16_t totalPhys = (uint16_t)ceil((float)mem_info.ullTotalPhys / 1024 / 1024 / 1024);
        float physUsed = (float)(mem_info.ullTotalPhys - mem_info.ullAvailPhys) / 1024 / 1024 / 1024;
        uint16_t usagePercent = mem_info.dwMemoryLoad;
        HICON icon;
        if (hash_ram_icons.find((uint16_t)usagePercent) == hash_ram_icons.end())
        {
            icon = CreateIconWithNumber(usagePercent, ICON_SIZE, 50, RAM_COLOR);
            hash_ram_icons.insert({ (uint16_t)usagePercent, icon});
        }
        ram_status_icon.hIcon = hash_ram_icons[(uint16_t)usagePercent];
        swprintf(ram_info, 64, L"%.2f\\%u Gb   %u%%", physUsed, totalPhys, usagePercent);
        lstrcpy(ram_status_icon.szTip, ram_info);
        Shell_NotifyIcon(NIM_MODIFY, &ram_status_icon);
    }
}
Таймер для CPU
void CALLBACK TimerCPU(PVOID pVoid, BOOLEAN TimerOrWaitFired)
{
    static wchar_t cpu_inf[8];
    ZeroMemory(cpu_inf, 8);
    double cpu_total = GetCPULoad();
    HICON icon;
    if (hash_cpu_icons.find((uint16_t)cpu_total) == hash_cpu_icons.end())
    {
        icon = CreateIconWithNumber((uint16_t)cpu_total, ICON_SIZE, 50, GetGradColor(cpu_total));
        hash_cpu_icons.insert({ (uint16_t)cpu_total,  icon });
    }
    cpu_status_icon.hIcon = hash_cpu_icons[(uint16_t)cpu_total];
    swprintf(cpu_inf, 8, L"%.2f %%", cpu_total);
    lstrcpy(cpu_status_icon.szTip, cpu_inf);
    Shell_NotifyIcon(NIM_MODIFY, &cpu_status_icon);
}

Ранее оно выглядело следующим образом:

Было
void CALLBACK TimerRAM(PVOID pVoid, BOOLEAN TimerOrWaitFired)
{
    static wchar_t ram_info[128];
    if (GlobalMemoryStatusEx(&mem_info)) {
        static uint16_t totalPhys = (uint16_t)ceil((float)mem_info.ullTotalPhys / 1024 / 1024 / 1024);
        float physUsed = (float)(mem_info.ullTotalPhys - mem_info.ullAvailPhys) / 1024 / 1024 / 1024;
        uint16_t usagePercent = mem_info.dwMemoryLoad;

        swprintf(ram_info, 128, L"%.2f\\%u Gb   %u%%", physUsed, totalPhys, usagePercent);
        lstrcpy(ram_status_icon.szTip, ram_info);
        Shell_NotifyIcon(NIM_MODIFY, &ram_status_icon);
    }

}

void CALLBACK TimerCPU(PVOID pVoid, BOOLEAN TimerOrWaitFired)
{
    static wchar_t cpu_inf[8];
    double cpu_total = GetCPULoad();
    if (cpu_total < 25)
        cpu_status_icon.hIcon = cpu_icons[0];
    else if (cpu_total < 50)
        cpu_status_icon.hIcon = cpu_icons[1];
    else if (cpu_total >= 50)
        cpu_status_icon.hIcon = cpu_icons[2];
    swprintf(cpu_inf, 8, L"%.2f %%", cpu_total);
    lstrcpy(cpu_status_icon.szTip, cpu_inf);
    Shell_NotifyIcon(NIM_MODIFY, &cpu_status_icon);
}

Логика такая: "Есть ли иконка в нашей хэш таблице с числом n?"
true - Берем эту иконку, false - рисуем ее и добавляем в хэш-таблицу. Таким образом избавимся от постоянной перерисовки.

Можно заметить, что теперь в таймере для процессора цвет меняется не ступенями. Применяется неизвестная функция GetGradColor(); Давайте в этом разбираться

Линейная интерполяция для градиента

Чтобы плавно менять значения в зависимости от загрузки я использовал линейную интерполяцию. Суть проста:

LERP(A, B, t) = A + t * (B - A) A < B, t ∈ [0; 1]

Это простое выражение позволяет получить промежуточное значение между числом A и B, соответствующее параметру t. Рассмотрим пример

Какое число будет соответствовать параметру t = 0.5 в промежутке [0, 50]?

LERP(0, 50, 0.5) = 0 + 0.5 * (50 - 0) = 25

Этот принцип и будем использоваться. Но сначала определимся с нашими промежутками. Как будет меняться наш цвет? Выделим 3 точки

  • Зеленый - RGB(0, 255, 0) - соответствует 0% загрузки CPU

  • Оранжевый - RGB(255, 165, 0) - соответствует 50% загрузки CPU

  • Красный RGB(255, 0, 0) - соответствует 100% загрузки CPU

Теперь введем параметр ] u = x / 50, где x - загрузка CPU в процентах

Этот параметр даст нам 2 ветки для интерполяции:
u < 1 и u > 1

Теперь интерполируем:

  • Канал B = const = 0

  • Канал R = 255 * min(u, 1) - здесь A = 0, B = 255. Если наше u > 1, соответственно загрузка CPU > 50%, а это значит, канал R больше не меняется. С помощью функций max и min будем это регулировать

  • Канал G = 255 - 90 * min(u, 1) - 165  * max(u-1, 0)

С математикой разобрались, теперь реализуем функцию:

Функция GetGradColor
Gdiplus::Color GetGradColor(double x) {
    double u = x / 50.0;
    Gdiplus::Color c(
        255, //alfa
        int(255 * min(u, 1.0)), //r
        int(255 - 90 * min(u, 1.0) - 165 * max(u - 1.0, 0)), //g
        0);  //b
    return c;
}

Попробуйте проверить это на бумаге. Поэтапно.
Часть 255 - 90 * u будет уменьшать G канал до 165 при u < 1,

а затем до 0 при u > 1, 165 - 165 * (u - 1)

Инициализация параметров и работа с настройками

Для начала расскажу идею для настройки программы. У нас есть переменная settings_param. последние 3 бита - есть наши настройки.

  • Включен последний бит - отображается статус CPU

  • Включен предпоследний бит - отображается статус RAM

  • Включен 3-й бит с конца - окно настроек открывается при запуске программы

Чтобы эти настройки сохранялись при перезапуске ОС, будем создавать переменную среды, об этом в следующей главе.

Представлю код стандартной функции инициализации с комментариями.

InitInstance
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance;

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU,
      300, 300, 300, 300, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }
//этот блок разберем в следующей главе
   try
   {
        settings_param = ReadSettings();
   }
   catch (std::runtime_error &msg)
   {
       settings_param = 0b0111;
       WriteSettings();
   }

  //инициализируем чекбоксы, задаем им координаты на нашем окне и текст
   cpu_checkbox = new Checkbox(hWnd, hInstance, CPU_CLICK, L"CPU", 100, 50, 100, 30);
   ram_checkbox = new Checkbox(hWnd, hInstance, RAM_CLICK, L"RAM", 100, 100, 100, 30);
   open_start_checkbox = new Checkbox(hWnd, hInstance, OPEN_AT_START, L"Open at start", 100, 150, 110, 30);
//инициализируем GDI+
   InitGDIPlus();
  
   mem_info.dwLength = sizeof(mem_info);

  //Все как в первой части. Но теперь мы заранее не выставляем готовые иконки,
  //ведь нам нужно было отказаться от них

  //в поле uCallbackMessage указываем наше значение, которое будем обрабатывать
  //при нажатии на иконку
   cpu_status_icon.cbSize = sizeof(NOTIFYICONDATA);
   cpu_status_icon.hWnd = hWnd;
   cpu_status_icon.uID = 1;
   cpu_status_icon.uCallbackMessage = ICON_CLICK;
   cpu_status_icon.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;

   ram_status_icon.cbSize = sizeof(NOTIFYICONDATA);
   ram_status_icon.hWnd = hWnd;
   ram_status_icon.uID = 2;
   ram_status_icon.uCallbackMessage = ICON_CLICK;
   ram_status_icon.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;

//проверяем настройки каждого бита и выставляем необходимые настройки для
//чекбокса и флагов
   if (settings_param & 0b0100)
   {
       show_window_flag = 5;
       open_start_checkbox->Check();
   }
   if (settings_param & 0b0010)
   {
       ram_show = 0;
       ram_checkbox->Check();
   }
   if (settings_param & 0b0001)
   {
       cpu_show = 0;
       cpu_checkbox->Check();
   }
//ну а теперь в соответсвии с флагами у нас будут отображаеться необходимые элементы
   ShowWindow(hWnd, show_window_flag);
   Shell_NotifyIcon(cpu_show, &cpu_status_icon);
   Shell_NotifyIcon(ram_show, &ram_status_icon);
//Без изменений. Запускаем параллельные таймеры
   if (!CreateTimerQueueTimer(&htimer_cpu, NULL, TimerCPU, NULL, 1000, 1500, WT_EXECUTEDEFAULT))
   {
       std::cerr << "Error cpu timer\n";
       return -1;
   }

   if (!CreateTimerQueueTimer(&htimer_ram, NULL, TimerRAM, NULL, 1000, 1500, WT_EXECUTEDEFAULT))
   {
       std::cerr << "Error ram timer\n";
       return -2;
   }
   return TRUE;
}

Теперь разберем функцию обратного вызова для обработки сообщений:

CALLBACK WndProc
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case ICON_CLICK:
        if (lParam == WM_LBUTTONDOWN)
        {
            show_window_flag ^= 5;
            ShowWindow(hWnd, show_window_flag);
        }
        break;
    case WM_CLOSE:
        show_window_flag = 0;
        ShowWindow(hWnd, show_window_flag);
        break;
    case WM_COMMAND:
        if (HIWORD(wParam) == BN_CLICKED)
        {
            switch (LOWORD(wParam))
            {
                case CPU_CLICK:
                    cpu_checkbox->Revers();
                    cpu_show ^= 0x2;
                    settings_param ^= 0b001;
                    Shell_NotifyIcon(cpu_show, &cpu_status_icon);
                    open_start_checkbox->UnBlock();
                    WriteSettings();
                    break;
                case RAM_CLICK:
                    ram_checkbox->Revers();
                    ram_show ^= 0x2;
                    settings_param ^= 0b010;
                    Shell_NotifyIcon(ram_show, &ram_status_icon);
                    open_start_checkbox->UnBlock();
                    WriteSettings();
                    break;
                case OPEN_AT_START:
                    open_start_checkbox->Revers();
                    settings_param ^= 0b100;
                    WriteSettings();
                    break;
                }
                if (!(settings_param & 1) && !(settings_param & 2))
                {
                    settings_param = 0b100;
                    open_start_checkbox->Check();
                    open_start_checkbox->Block();
                    WriteSettings();
                }
        }
        break;
    case WM_DESTROY:
        ShutdownGDIPlus();
        Shell_NotifyIcon(NIM_DELETE, &cpu_status_icon);
        Shell_NotifyIcon(NIM_DELETE, &ram_status_icon);
        DeleteTimerQueueTimer(NULL, htimer_cpu, NULL);
        DeleteTimerQueueTimer(NULL, htimer_ram, NULL);
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

Пройдемся подробнее по блокам снизу-вверх.

Блок WM_DESTROY:

Все просто. При завершении работы программы очищаем ресурсы GDI+, удаляем иконки и удаляем таймеры.

Блок WM_COMMAND:

wParam содержит id чекбокса, по которому был сделан клик. Что происходит для CPU_CLICK и RAM_CLICK (по сути это и id наших таймеров):

  • Меняем значение checkbox на противоположное

  • Устанавливаем флаг отображения значка операцией XOR

  • Инвертируем бит в переменной нашей глобальной настройки settings_param

  • Обновляем структуру иконки

  • Разблокируем чекбокс open_start_checkbox

  • Записываем информацию о настройках в переменную среду

Я столкнулся со следующей важной проблемой: если юзер отключил показ обоих иконок и отключил возможность открытия окна настроек при запуске программы, то у просто повиснет фоновый процесс, а взаимодействие с ним будет невозможным. Придется через диспетчер задач отключать процесс, лезть в реестр, менять значение переменной и заново запускать программу. Поэтому решение следующее: если отключен показ обоих иконок, то окно настроек отключить будет невозможно. Жестко включим чекбокс, выставим нужные настройки и зафиксируем эти настройки в settings_param и отключим чекбокс. Этим и занимается блок с if-ом.

Блок WM_CLOSE:

Вход в него осуществляется при попытке закрыть окно нажатием на крестик. Не обрабатывая это сообщение, наше окно просто закроется. Действие довольно машинальное и завершать работу таким образом будет не совсем удобно (это банально может быть случайный клик). Поэтому при нажатии на крестик мы просто скроем окно настроек.

Блок ICON_CLICK:

Как раз обработка нажатия на иконку. С помощью XOR меняем флаг и скрываем/показываем окно настроек.

Таким образом мы имеем довольно удобную работу с окном настроек и их применением, так как предусмотрены интуитивные действия пользователя.

Сохранение настроек в переменную среды

Нужно реализовать 2 функции - чтение и запись переменной среды. Будем использоваться переменную в разделе пользователя.

Чтобы использовать переменную для машины, то могут потребоваться права администратора. Да и оно не особо нужно нам. Можете посмотреть переменные, которые используются в вашим пользователем прямо сейчас. Нажмите WIN+R, введите в окно regedit (реестр Windows) и перейдите во вкладку \HKEY_CURRENT_USER\Environment. Сюда мы и будем записывать нашу переменную. При перезапуске системы она сохранит свое значение, мы ее прочитаем и выставим все настройки.

WriteSettings
bool WriteSettings(int n)
{
    std::wstring valueStr = std::to_wstring(n);
    HKEY hKey;
//открываем раздел в реестре 
    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Environment", 0, KEY_SET_VALUE, &hKey) != ERROR_SUCCESS)
        return false;
//RegSetValueEx Устанавливает данные и тип указанного значения в ключе реестра. Если значение с таким именем ещё не присутствует в ключе, функция добавляет его. 
    LONG result = RegSetValueEx(
        hKey,
        SETTINGS_MONITOR_NAME,
        0,
        REG_SZ,
        reinterpret_cast<const BYTE *>(valueStr.c_str()),
        static_cast<DWORD>((valueStr.size() + 1) * sizeof(wchar_t)));
  //закрываем ключ
    RegCloseKey(hKey);

    if (result != ERROR_SUCCESS)
        return false;
//отправляем сообщение в броадкаст, чтобы все приложения увидели изменение в реестре
    SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
        (LPARAM)L"Environment",
        SMTO_ABORTIFHUNG, 5000, nullptr);

    return true;
}

Она принимает число, которое нужно записать в реестр.

ReadSettings
byte ReadSettings()
{
    HKEY hKey;
    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Environment", 0, KEY_QUERY_VALUE, &hKey) != ERROR_SUCCESS)
        throw std::runtime_error("Couldnt open the key registry Environment");

    wchar_t buffer[4];
    DWORD bufferSize = sizeof(buffer);
    DWORD type = 0;

    LONG result = RegQueryValueExW(
        hKey,
        SETTINGS_MONITOR_NAME,
        nullptr,
        &type,
        reinterpret_cast<LPBYTE>(buffer),
        &bufferSize
    );

    RegCloseKey(hKey);

    if (result != ERROR_SUCCESS)
        throw std::runtime_error(std::string("variable was not found"));

    if (type != REG_SZ)
        throw std::runtime_error(std::string("variable has in incorrect format"));

    try {
        return (byte)std::stoi(buffer);
    }
    catch (...) {
        throw std::runtime_error(std::string("Couldnt convert the value to a number"));
    }
}

Читает переменную из указанного раздела реестра

Теперь вернемся к блоку try-catche. Здесь мы пытаемся прочитать значение из переменной. Но если программа запущена впервые, то никакой переменной среды там не будет. В случае выброса ошибки при чтении запишем значение этой переменной, предварительно поднимем все флаги.

   try
   {
        settings_param = ReadSettings();
   }
   catch (std::runtime_error &msg)
   {
       settings_param = 0b0111;
       WriteSettings(settings_param);
   }

Заключение

Осталось добавить иконки нашего оконного приложения. Я просто взял иконку из первой части, перекрасил в фиолетовый цвет, а затем подтягивал ее в ресурсы .rc:

IDI_CPUSTATUSICON       ICON                    "small_icon.ico"
IDI_SMALL               ICON                    "small_icon.ico"

Вот и финальный результат:

Без окна настроек
Без окна настроек
Окно настроек с отключенным CPU
Окно настроек с отключенным CPU

Вот и все. После того как она была готова, появлялись мысли о том, чтобы заняться ее продажей, но решил поделиться этим проектом с вами. Я учел предыдущие ошибки и постарался сделать эту статью более подробной и информативной. Ниже приведен исходный код программы. Можно скачать готовое решение на моем гитхабе.

Остается только закинуть .exe в папку автозагрузки

ПОЛНЫЙ КОД
#include "framework.h"
#include "CPU_status_icon.h"
#include "checkbox.h"
#include <objidl.h>
#include <gdiplus.h>
#include <windows.h>
#include <shellapi.h>
#include <iostream>
#include <string>
#include <map>

#pragma comment(lib, "gdiplus.lib")

#pragma comment(lib, "Ole32.lib")

#define MAX_LOADSTRING 100
#define ICON_SIZE 64
#define ICON_CLICK (WM_APP + 1)
#define CPU_CLICK (WM_APP + 2)
#define RAM_CLICK (WM_APP + 3)
#define OPEN_AT_START (WM_APP + 4)
#define SETTINGS_MONITOR_NAME L"SETTINGS_MONITOR"
#define RAM_COLOR Gdiplus::Color(255, 200, 90, 240)

// Глобальные переменные:
HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING];
WCHAR szWindowClass[MAX_LOADSTRING];
HANDLE htimer_ram = NULL;
HANDLE htimer_cpu = NULL;
NOTIFYICONDATA cpu_status_icon;
NOTIFYICONDATA ram_status_icon;
MEMORYSTATUSEX mem_info;
ULONG_PTR g_gdiplusToken = 0;
std::map<uint16_t, HICON> hash_cpu_icons;
std::map<uint16_t, HICON> hash_ram_icons;

byte show_window_flag;
byte settings_param = 0b0111;
byte cpu_show = 0x2;
byte ram_show = 0x2;

Checkbox *cpu_checkbox;
Checkbox *ram_checkbox;
Checkbox *open_start_checkbox;


ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);

Gdiplus::Color GetGradColor(double x) {
    double u = x / 50.0;
    Gdiplus::Color c(
        255, //alfa
        int(255 * min(u, 1.0)), //r
        int(255 - 90 * min(u, 1.0) - 165 * max(u - 1.0, 0.0)), //g
        0);  //b
    return c;
}

void InitGDIPlus()
{
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, nullptr);
}

void ShutdownGDIPlus()
{
    if (g_gdiplusToken != 0)
        Gdiplus::GdiplusShutdown(g_gdiplusToken);
}

HICON CreateIconWithNumber(int number, int size, int fontSize, Gdiplus::Color textColor)
{
    std::wstring text = std::to_wstring(number);

    Gdiplus::Bitmap bitmap(size, size, PixelFormat32bppARGB);
    Gdiplus::Graphics graphics(&bitmap);
    graphics.Clear(Gdiplus::Color(0, 0, 0, 0));

    Gdiplus::FontFamily fontFamily(L"Segoe UI");
    Gdiplus::Font font(&fontFamily, static_cast<Gdiplus::REAL>(fontSize), Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
    Gdiplus::SolidBrush brush(textColor);

    Gdiplus::RectF charRect;
    graphics.MeasureString(L"0", 1, &font, Gdiplus::PointF(0, 0), &charRect);
    float y = (size - charRect.Height) / 2.0f;

    float x = 0.0f;
    float spacingFactor = 0.65f;
    for (wchar_t ch : text)
    {
        wchar_t str[2] = { ch, 0 };
        graphics.MeasureString(str, 1, &font, Gdiplus::PointF(0, 0), &charRect);
        graphics.DrawString(str, 1, &font, Gdiplus::PointF(x, y), &brush);
        x += charRect.Width * spacingFactor;
    }

    HICON hIcon = nullptr;
    bitmap.GetHICON(&hIcon);
    return hIcon;
}

float GetCPULoad() {
    static FILETIME idleTimePrev = {}, kernelTimePrev = {}, userTimePrev = {};

    FILETIME idleTime, kernelTime, userTime;
    if (!GetSystemTimes(&idleTime, &kernelTime, &userTime)) return 0.0;

    auto toUInt64 = [](FILETIME ft) {
        return ((uint64_t)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
        };

    uint64_t idleDiff = toUInt64(idleTime) - toUInt64(idleTimePrev);
    uint64_t kernelDiff = toUInt64(kernelTime) - toUInt64(kernelTimePrev);
    uint64_t userDiff = toUInt64(userTime) - toUInt64(userTimePrev);

    idleTimePrev = idleTime;
    kernelTimePrev = kernelTime;
    userTimePrev = userTime;

    uint64_t total = kernelDiff + userDiff;
    return total ? (1.0 - (float)idleDiff / total) * 100.0 : 0.0;
}

void CALLBACK TimerRAM(PVOID pVoid, BOOLEAN TimerOrWaitFired)
{
    static wchar_t ram_info[64];
    ZeroMemory(ram_info, 64);
    if (GlobalMemoryStatusEx(&mem_info)) {
        static uint16_t totalPhys = (uint16_t)ceil((float)mem_info.ullTotalPhys / 1024 / 1024 / 1024);
        float physUsed = (float)(mem_info.ullTotalPhys - mem_info.ullAvailPhys) / 1024 / 1024 / 1024;
        uint16_t usagePercent = mem_info.dwMemoryLoad;
        HICON icon;
        if (hash_ram_icons.find((uint16_t)usagePercent) == hash_ram_icons.end())
        {
            icon = CreateIconWithNumber(usagePercent, ICON_SIZE, 50, RAM_COLOR);
            hash_ram_icons.insert({ (uint16_t)usagePercent, icon});
        }
        ram_status_icon.hIcon = hash_ram_icons[(uint16_t)usagePercent];
        swprintf(ram_info, 64, L"%.2f\\%u Gb   %u%%", physUsed, totalPhys, usagePercent);
        lstrcpy(ram_status_icon.szTip, ram_info);
        Shell_NotifyIcon(NIM_MODIFY, &ram_status_icon);
    }
}

void CALLBACK TimerCPU(PVOID pVoid, BOOLEAN TimerOrWaitFired)
{
    static wchar_t cpu_inf[8];
    ZeroMemory(cpu_inf, 8);
    double cpu_total = GetCPULoad();
    HICON icon;
    if (hash_cpu_icons.find((uint16_t)cpu_total) == hash_cpu_icons.end())
    {
        icon = CreateIconWithNumber((uint16_t)cpu_total, ICON_SIZE, 50, GetGradColor(cpu_total));
        hash_cpu_icons.insert({ (uint16_t)cpu_total,  icon });
    }
    cpu_status_icon.hIcon = hash_cpu_icons[(uint16_t)cpu_total];
    swprintf(cpu_inf, 8, L"%.2f %%", cpu_total);
    lstrcpy(cpu_status_icon.szTip, cpu_inf);
    Shell_NotifyIcon(NIM_MODIFY, &cpu_status_icon);
}

bool WriteSettings(int n)
{
    std::wstring valueStr = std::to_wstring(n);
    HKEY hKey;

    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Environment", 0, KEY_SET_VALUE, &hKey) != ERROR_SUCCESS)
        return false;

    LONG result = RegSetValueEx(
        hKey,
        SETTINGS_MONITOR_NAME,
        0,
        REG_SZ,
        reinterpret_cast<const BYTE *>(valueStr.c_str()),
        static_cast<DWORD>((valueStr.size() + 1) * sizeof(wchar_t)));
    RegCloseKey(hKey);

    if (result != ERROR_SUCCESS)
        return false;

    SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
        (LPARAM)L"Environment",
        SMTO_ABORTIFHUNG, 5000, nullptr);

    return true;
}

byte ReadSettings()
{
    HKEY hKey;
    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Environment", 0, KEY_QUERY_VALUE, &hKey) != ERROR_SUCCESS)
        throw std::runtime_error("Couldnt open the key registry Environment");

    wchar_t buffer[4];
    DWORD bufferSize = sizeof(buffer);
    DWORD type = 0;

    LONG result = RegQueryValueExW(
        hKey,
        SETTINGS_MONITOR_NAME,
        nullptr,
        &type,
        reinterpret_cast<LPBYTE>(buffer),
        &bufferSize
    );

    RegCloseKey(hKey);

    if (result != ERROR_SUCCESS)
        throw std::runtime_error(std::string("variable was not found"));

    if (type != REG_SZ)
        throw std::runtime_error(std::string("variable has in incorrect format"));

    try {
        return (byte)std::stoi(buffer);
    }
    catch (...) {
        throw std::runtime_error(std::string("Couldnt convert the value to a number"));
    }
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // TODO: Разместите код здесь.

    // Инициализация глобальных строк
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_CPUSTATUSICON, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_CPUSTATUSICON));

    MSG msg;

    // Цикл осовного сообщения:
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int) msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(1));
    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_CPUSTATUSICON);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance;

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU,
      300, 300, 300, 300, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }

   try
   {
        settings_param = ReadSettings();
   }
   catch (std::runtime_error &msg)
   {
       settings_param = 0b0111;
       WriteSettings(settings_param);
   }

   cpu_checkbox = new Checkbox(hWnd, hInstance, CPU_CLICK, L"CPU", 100, 50, 100, 30);
   ram_checkbox = new Checkbox(hWnd, hInstance, RAM_CLICK, L"RAM", 100, 100, 100, 30);
   open_start_checkbox = new Checkbox(hWnd, hInstance, OPEN_AT_START, L"Open at start", 100, 150, 110, 30);

   InitGDIPlus();
   mem_info.dwLength = sizeof(mem_info);

   cpu_status_icon.cbSize = sizeof(NOTIFYICONDATA);
   cpu_status_icon.hWnd = hWnd;
   cpu_status_icon.uID = 1;
   cpu_status_icon.uCallbackMessage = ICON_CLICK;
   cpu_status_icon.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;

   ram_status_icon.cbSize = sizeof(NOTIFYICONDATA);
   ram_status_icon.hWnd = hWnd;
   ram_status_icon.uID = 2;
   ram_status_icon.uCallbackMessage = ICON_CLICK;
   ram_status_icon.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;


   if (settings_param & 0b0100)
   {
       show_window_flag = 5;
       open_start_checkbox->Check();
   }
   if (settings_param & 0b0010)
   {
       ram_show = 0;
       ram_checkbox->Check();
   }
   if (settings_param & 0b0001)
   {
       cpu_show = 0;
       cpu_checkbox->Check();
   }

   ShowWindow(hWnd, show_window_flag);
   Shell_NotifyIcon(cpu_show, &cpu_status_icon);
   Shell_NotifyIcon(ram_show, &ram_status_icon);

   if (!CreateTimerQueueTimer(&htimer_cpu, NULL, TimerCPU, NULL, 1000, 1500, WT_EXECUTEDEFAULT))
   {
       std::cerr << "Error cpu timer\n";
       return -1;
   }

   if (!CreateTimerQueueTimer(&htimer_ram, NULL, TimerRAM, NULL, 1000, 1500, WT_EXECUTEDEFAULT))
   {
       std::cerr << "Error ram timer\n";
       return -2;
   }
   return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case ICON_CLICK:
        if (lParam == WM_LBUTTONDOWN)
        {
            show_window_flag ^= 5;
            ShowWindow(hWnd, show_window_flag);
        }
        break;
    case WM_CLOSE:
        show_window_flag = 0;
        ShowWindow(hWnd, show_window_flag);
        break;
    case WM_COMMAND:
        if (HIWORD(wParam) == BN_CLICKED)
        {
            switch (LOWORD(wParam))
            {
                case CPU_CLICK:
                    cpu_checkbox->Revers();
                    cpu_show ^= 0x2;
                    settings_param ^= 0b001;
                    Shell_NotifyIcon(cpu_show, &cpu_status_icon);
                    open_start_checkbox->UnBlock();
                    WriteSettings(settings_param);
                    break;
                case RAM_CLICK:
                    ram_checkbox->Revers();
                    ram_show ^= 0x2;
                    settings_param ^= 0b010;
                    Shell_NotifyIcon(ram_show, &ram_status_icon);
                    open_start_checkbox->UnBlock();
                    WriteSettings(settings_param);
                    break;
                case OPEN_AT_START:
                    open_start_checkbox->Revers();
                    settings_param ^= 0b100;
                    WriteSettings(settings_param);
                    break;
                }
                if (!(settings_param & 1) && !(settings_param & 2))
                {
                    settings_param = 0b100;
                    open_start_checkbox->Check();
                    open_start_checkbox->Block();
                    WriteSettings(settings_param);
                }
        }
        break;
    case WM_DESTROY:
        ShutdownGDIPlus();
        Shell_NotifyIcon(NIM_DELETE, &cpu_status_icon);
        Shell_NotifyIcon(NIM_DELETE, &ram_status_icon);
        DeleteTimerQueueTimer(NULL, htimer_cpu, NULL);
        DeleteTimerQueueTimer(NULL, htimer_ram, NULL);
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

Не забудьте добавить checkbox.h!!!

Мой гитхаб!

Комментарии (6)


  1. udachi_23
    19.08.2025 11:31

    У меня вопрос по производительности: насколько сильно использование GDI+ вместо чистого GDI повлияло на потребление ресурсов, учитывая более частый апдейт иконок? Есть ли заметная разница на старом железе? Мне кажется, разница в потреблении ресурсов между GDI и GDI+ в режиме выполнения незначительна, но может я ошибаюсь


    1. Penguin_pelmen Автор
      19.08.2025 11:31

      Разница совсем незначительна. Если посмотреть в диспетчер задач, то программа вообще не нагружает процессор и использует меньше 1 Мб ОЗУ. На старом железе я не проверял


  1. diakin
    19.08.2025 11:31


  1. nikolz
    19.08.2025 11:31

    А какая загрузка CPU этим приложением?


    1. Penguin_pelmen Автор
      19.08.2025 11:31

      Весьма мала, меньше 1% по диспетчеру


  1. antonluba
    19.08.2025 11:31

    Еще 25 лет назад в Delphi отрисовывал динамические иконки для трея. Ностальгия прям