Приветствую, уважаемый читатель. Ранее, я уже писал подобную статью (ссылка на нее есть ниже). Рекомендую прочесть ее прежде, чем переходить к этой, так как из первой части фундаментальные вещи остались не тронутыми.
Проанализировав статистику по предыдущей статье я понял, что этот проект может быть действительно актуален пользователям Windows. Решил последовать советам из комментариев и это того стоило.
Напомню главную задачу: фоновая программа, которая выводит в трей информацию о загрузки CPU и ОЗУ.
Как изменения были введены:
-
Вывод в трей иконок сразу с числовыми значением (ранее были сделаны статические иконки. А для того, чтобы было видно информацию, приходилось наводиться на значок курсором)
-
Градиентный переход от зеленого к красного с помощью линейной интерполяции для иконки загрузки CPU
-
Окно настройки (отключение вывода информации о процессоре или памяти, + возможность запускать окно настройки при запуске программы).
Каждый пункт постараюсь описать максимально качественно.
В прошлый раз мы использовали готовые иконки. Так вот их можно смело удалять. Также нужно удалить их из .rc файла И из Recourse.h Так как готовые иконки больше не используются, то наш список иконок больше не нужен. В методе инициализации убираем следующие строкиДля тех, кто использовал предыдущее решение.
IDR_CPUICON1 ICON "cpu4.ico"
IDR_CPUICON2 ICON "cpu6.ico"
IDR_CPUICON3 ICON "cpu5.ico"
IDR_RAMICON0 ICON "ram0.ico"#define IDR_CPUICON1 131
#define IDR_CPUICON2 132
#define IDR_CPUICON3 133
#define IDR_RAMICON0 134cpu_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 timern";
return -1;
}
if (!CreateTimerQueueTimer(&htimer_ram, NULL, TimerRAM, NULL, 1000, 1500, WT_EXECUTEDEFAULT))
{
std::cerr << "Error ram timern";
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_USEREnvironment. Сюда мы и будем записывать нашу переменную. При перезапуске системы она сохранит свое значение, мы ее прочитаем и выставим все настройки.
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 timern";
return -1;
}
if (!CreateTimerQueueTimer(&htimer_ram, NULL, TimerRAM, NULL, 1000, 1500, WT_EXECUTEDEFAULT))
{
std::cerr << "Error ram timern";
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!!!
Автор: Penguin_pelmen
