Приветствую, уважаемый читатель. Ранее, я уже писал подобную статью (ссылка на нее есть ниже). Рекомендую прочесть ее прежде, чем переходить к этой, так как из первой части фундаментальные вещи остались не тронутыми.
Проанализировав статистику по предыдущей статье я понял, что этот проект может быть действительно актуален пользователям 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_CPUICON1131 #define IDR_CPUICON2132 #define IDR_CPUICON3133 #define IDR_RAMICON0134
Так как готовые иконки больше не используются, то наш список иконок больше не нужен. В методе инициализации убираем следующие строки
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!!!
ссылка на оригинал статьи https://habr.com/ru/articles/938430/
Добавить комментарий