Приветствую, уважаемый читатель. Ранее, я уже писал подобную статью (ссылка на нее есть ниже). Рекомендую прочесть ее прежде, чем переходить к этой, так как из первой части фундаментальные вещи остались не тронутыми.
Проанализировав статистику по предыдущей статье я понял, что этот проект может быть действительно актуален пользователям 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(); Давайте в этом разбираться
Линейная интерполяция для градиента
Чтобы плавно менять значения в зависимости от загрузки я использовал линейную интерполяцию. Суть проста:
Это простое выражение позволяет получить промежуточное значение между числом A и B, соответствующее параметру t. Рассмотрим пример
Какое число будет соответствовать параметру t = 0.5 в промежутке [0, 50]?
Этот принцип и будем использоваться. Но сначала определимся с нашими промежутками. Как будет меняться наш цвет? Выделим 3 точки
Зеленый - RGB(0, 255, 0) - соответствует 0% загрузки CPU
Оранжевый - RGB(255, 165, 0) - соответствует 50% загрузки CPU
Красный RGB(255, 0, 0) - соответствует 100% загрузки CPU
Теперь введем параметр, где x - загрузка CPU в процентах
Этот параметр даст нам 2 ветки для интерполяции:
u < 1 и u > 1
Теперь интерполируем:
Канал
Канал
- здесь A = 0, B = 255. Если наше u > 1, соответственно загрузка CPU > 50%, а это значит, канал R больше не меняется. С помощью функций max и min будем это регулировать
Канал
С математикой разобрались, теперь реализуем функцию:
Функция 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;
}
Попробуйте проверить это на бумаге. Поэтапно.
Часть будет уменьшать G канал до 165 при u < 1,
а затем до 0 при 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"
Вот и финальный результат:


Вот и все. После того как она была готова, появлялись мысли о том, чтобы заняться ее продажей, но решил поделиться этим проектом с вами. Я учел предыдущие ошибки и постарался сделать эту статью более подробной и информативной. Ниже приведен исходный код программы. Можно скачать готовое решение на моем гитхабе.
Остается только закинуть .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!!!
udachi_23
У меня вопрос по производительности: насколько сильно использование GDI+ вместо чистого GDI повлияло на потребление ресурсов, учитывая более частый апдейт иконок? Есть ли заметная разница на старом железе? Мне кажется, разница в потреблении ресурсов между GDI и GDI+ в режиме выполнения незначительна, но может я ошибаюсь
Penguin_pelmen Автор
Разница совсем незначительна. Если посмотреть в диспетчер задач, то программа вообще не нагружает процессор и использует меньше 1 Мб ОЗУ. На старом железе я не проверял