Хе хе ну вы поняли "ВОйти ВАйти".

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

В этой статье мы получим все базовые навыки, которые нужны программисту, по окончании статьи у тебя будут все необходимые навыки для C++ Junior Desktop Developer, а также ты сможешь создавать 90% программ с софт портала, меньше слов ближе к делу.

(РЕМАРКА! Эта статья не будет интересна тем, кто уже знает программирование, поэтому попрошу избавить меня от вашего «экспертного мнения» программиста.

Чуть-чуть теории и много практики

Программирование построено на 4 основных концептах:

  1. Переменные

  2. Конструкции условий

  3. Конструкции циклов

  4. И конструкции функций

В двух словах мы хитроумно играем с базовым функционалом, предоставляемым на уровне процессора\биоса\системы, комбинируем те базовые функции с какими‑то данными из переменных, добавляем условия и циклы и оборачиваем это все в новые функции.

К примеру:

У нас есть функция на уровне системы (для изменения цвета пикселя на экране, которая выходит от драйвера видеокарты, а та из комбинированной работы процессора и видеокарты).

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

Надеюсь, логика понятна, немного по сути:

Переменная — условный «маркер данных», подстановочное значение, ключевое слово, с которым что‑то ассоциировано, хранилище данных.

Конструкция условий — какое либо правило для выполнения одной части кода, либо альтернативной (состоит из заголовка «условия» и блока кода, выделенного {} собаками «тела»).

Конструкция цикла — тоже условие, но вместо альтернативного, кода повторяет код внутри тела (фигурных {} скобочек), до тех пор, пока условие выполняется.

Функция — ключевое слово, которое выполняет какой‑либо блок кода, и возвращает на место своего использования какие‑либо данные (функция может не только возвращать какие‑либо данные, но и принимать в себя данные (аргументы функции), которые вместе с функцией указаны в месте вызова, в коде, функция также состоит из заголовка и тела).

Нечто важное, что нужно знать это «типы данных» для переменных, существуют стандартные числовые и строковые, но большая часть «типов данных» это пользовательские типы данных, которые могут состоять как из 2 числовых переменных в одной, так и из нескольких окон программы в одной переменной (иначе говоря типы данных для переменных позволяют создавать любые переменные, а эти «любые переменные могут иметь внутри себя, как просто число, так и функции, циклы, условия, переменные, и другие, кнопки, окна, и тд).

Ничего страшного если непонятно разберемся на ходу.

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

К примеру: получаем от системы сообщение «координаты мыши изменены», в коде создаем условие, проверяющие пересекают ли новые координаты местоположение кнопки, и, если да, меняем цвет кнопки. Отправляем сообщение примерно также (из программы в систему), и такой цикл происходит все время, пока запущенна программа (система: нажата кнопка, программа: изменен размер окна, система: создан новый файл, программа: окно перерисовано, и тд).

Много практики

Запустим Visual Studio и создадим новый проект, выбираем классическое приложение Windows (для него даже ничего не нужно скачивать, все уже в системе), тыкаем далее\создать. Готово, можем нажать кнопку запустить, и у нас запуститься готовое пустое приложение.

Закроем его (не пугаемся!).

В редакторе понаписано много чего не понятного (ничего страшного что непонятно), и сгенерированные комментарии (вот это //зеленое на русском) не особо помогают.

Вообще для реализации Win32 приложений сгенерированный код избыточен, и не совсем понятен даже для «программистов», я думаю это такая «шуточка» от разработчиков, мол справишься с этим, значит справишься со всем (как не трудно догадаться по малому количеству программ, и в общем информации по данному подходу в сети, примерно 90% программистов — не программисты).

На самом деле из всего этого кода нам хватило бы одной функции и пары команд, ну да ладно, разберем исходный код!

Этап №1 очень сложный код

Ну шо народ погнали на!?
// WindowsProject1.cpp : Определяет точку входа для приложения.
//

// этот кусок можно пропустить (это добавил я)
// директива pragma once подразумевает
// исключение логического бага в программе,
// который может произойти из-за команды для
// подключения библиотек (файлов с заготовками кода)
// include, которая просто вставляет на свое место
// все содержимое файла, который был указан
// (включая и команды include внутри подключаемого
// файла) при вызове, из за чего могу возникать логические
// ошибки и множественные самоподключения (когда
// одна библиотека ссылается на вторую, а вторая на
// первую), в двух словах все исходники собираются
// в одни длинный исходный код (командами include),
// а команда pragma once удаляет все дубликаты,
// оставляя только самую первую include,
// лично я начинаю любой файл с pragma once,
// что бы не беспокоится о зависимостях, но это
// не обязательно (кароче пока не заморачивайтесь)
#pragma once 

// и начать отсюда
// подключаем вспомогательные библиотеки
// наборы готового кода как в этом файле, 
// где уже описанные какие-то функции (заготовки) для
// реализации и запуска нашей программы
#include "framework.h"
#include "WindowsProject2.h"

// определим вспомогательный термин 
// (читай создадим разновидность переменной)
// под названием MAX_LOADSTRING и ассоциируем
// с ней число 999, теперь везде в коде, где 
// будет встречается термин MAX_LOADSTRING
// он будет заменятся на 999
#define MAX_LOADSTRING 999

// Глобальные переменные:
// 
// hInst это глобальная переменная экземпляра нашей программы
// переменная создается следующим образом - сначала пишется ключевое слово,
// которое обозначает тип данных переменной, потом какое-нибудь имя (ключевое
// слово), с которым ассоциированных данные, то есть везде в коде, где будет
// встречается ключевое слово, оно будет автоматически заменятся на данные,
// которые были ассоциированы с этим ключевым словом
HINSTANCE hInst;                                // текущий экземпляр

// сразу создадим свою переменную, тип данных
// будет HWND, название Form1, будем хранить тут уникальный
// идентификатор на наше окно программы (формы)
HWND Form1;

// в общем то создаем пару переменных типа "буквенная многопеременная в одной",
// и сразу задаем его длину в параметре [MAX_LOADSTRING] (иначе говоря набор 
// символов, или текстовые переменные)
WCHAR szTitle[MAX_LOADSTRING];                  // Текст строки заголовка
WCHAR szWindowClass[MAX_LOADSTRING];            // имя класса главного окна

// Отправить объявления функций, включенных в этот модуль кода:
// 
// эти вспомогательные функции сейчас не важны, по сути, они просто "нормализуют"
// нашу программу для системы, создают новый экземпляр окна (из заготовок в системе) 
// и добавляют мониторинг действий (нет смысла вдаваться в подробности, и забивать голову)
// функции начинаются с заголовков, сначала идет тип данных, которые должна вернуть функция,
// потом ключевое слово, любое (название этой функции), и наконец в круглых () скобках
// "заготовки" переменных всех переменных, которые должы быть переданы функцие, в самой же
// (внутри), эти данные доступны под названиями "заготовк переменных"
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

// создаем функцию wWinMain
// эта функция принимает в себя несколько параметров,
// это "точка входа", когда программа запускается, она 
// автоматически начинает выполнять эту функцию
//
// эта функция будет обеспечивать связь системы и нашей программы
// при помощи бесконечного цикла, который повторяется до тех пор, 
// пока программа запущена, и перехватывает сообщения от системы
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_WINDOWSPROJECT2, szWindowClass, MAX_LOADSTRING);

    // адаптируем нашу программу для системы при помощи функции
    // MyRegisterClass, в качестве параметра передаем идентификатор
    // нашего окна программы, который получаем из текущей функции 
    // (wWinMain) в параметре hInstance
    MyRegisterClass(hInstance);
    
    // создаем условие, если функция (InitInstance) вернула на место своего вызова
    // значение false, вызываем встроенное в идею функций ключевое слово
    // return, при встрече которого любая функция прекращает построчное выполнение
    // кода, и возвращаем ложь в функции wWinMain (в место вызова функции)
    // Выполнить инициализацию приложения:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    // это вспомогательный код для отлова событий нажатия клавиш,
    // ассоциируем с переменной hAccelTable (типа HACCEL) результат
    // выполнения функции LoadAccelerators, сама функция извлекает из
    // ресурсов нашего файла готовые схемы сочетания клавиш и названия функций
    // для этих сочетаний (которые были сгенерированны автоматически)
    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT2));

    // создадим переменную типа MSG (сложносоставной тип данных) под названием MSG
    // в эту переменную будут попадать команды, которые в цикле будут перехватываться 
    // универсальной функцией GetMessage
    MSG msg;

    // Цикл основного сообщения:

    // создаем цикл, до тех пор пока GetMessage, с пустыми параметрами 
    // (2,3,4, не нужно вдавятся сейчас в подробности почему)
    // возвращает какой-либо результат (в переменную msg) 
    // выполняем код в теле цикла
    //
    // важный момент, почему &msg (ассоциация значений), а не
    // просто msg (присвоение значений)? потому что если создать 
    // "условный" объект кнопка, задать ему какие-либо параметры, 
    // после чего создать еще один, и попробовать скопировать параметры 
    // простым присвоением, присвоены будут не параметры кнопки, 
    // а сама кнопка (объект), следовательно просто кнопка один 
    // будет также доступна под названием кнопка два
    while (GetMessage(&msg, nullptr, 0, 0))

    // это начало тела цикла
    {
        // нестандартная конструкция условия, она подразумевает
        // простую логику, если не "!" результат выполнения функции
        // TranslateAccelerator (функция для горячих клавиш)
        // то выполняем код в условии, иначе говоря, если 
        // нажатая клавиша не выполнила функцию, которая ей
        // назначена, то выполняем код условия, а именно обрабатываем 
        // горячую клавишу (не обязательно вдавятся в подробности 
        // нам сейчас это не нужно)
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            // код обработки нажатий горячих клавиш
            // которые были указаны в схеме, которая
            // была загружена из ресурсов ранее
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    // это конец тела цикла

    // возвращаем результат выполнения функции
    // в качестве подстановочного значение 
    // (которое встает в никуда) возвращаем параметры 
    // последней команды GetMessage
    return (int) msg.wParam;
}



//
//  ФУНКЦИЯ: MyRegisterClass()
//
//  ЦЕЛЬ: Регистрирует класс окна.
//

// выше уже была упомянута эта функция, она адаптирует
// в рамках системы нашу программу, вот, собственно, само 
// содержимое это функции, оно нам не интересно, двигаемся дальше
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(IDI_WINDOWSPROJECT2));
    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT2);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

//
//   ФУНКЦИЯ: InitInstance(HINSTANCE, int)
//
//   ЦЕЛЬ: Сохраняет маркер экземпляра и создает главное окно
//
//   КОММЕНТАРИИ:
//
//        В этой функции маркер экземпляра сохраняется в глобальной переменной, а также
//        создается и выводится главное окно программы.
//

// я думаю из сгенерированных комментариев понятно, для чего эта функция
// так же выше было сказано об этой функции, ее тоже можно пропустить
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; // Сохранить маркер экземпляра в глобальной переменной

   // разве что вот ключевая функция для создания окна, которая называется
   // CreateWindow (с модификатором W, то есть одна из вариаций этой функции)
   // и возвращает идентификатор на место своего вызова, который мы сразу
   // ассоциируем с переменной hWnd
   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

   // добавим условие, если "не hWnd" (то есть
   // нет ассоциированного объекта) возвращаем FALSE
   // и завершаем выполнение функции (ключевое слово
   // return помимо возврата данных в место вызова функции,
   // подразумивает завершение этой функции
   if (!hWnd)
   {
      return FALSE;
   }

   // после создания окна программы вызываем функции для отображения
   // окна программы и его перерисовки
   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   // если условие не сработа и дошли до этого
   // места возвращяем TRUE так как по логике
   // функция выполенилась без сбоев
   return TRUE;
}

//
//  ФУНКЦИЯ: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  ЦЕЛЬ: Обрабатывает сообщения в главном окне.
//
//  WM_COMMAND  - обработать меню приложения
//  WM_PAINT    - Отрисовка главного окна
//  WM_DESTROY  - отправить сообщение о выходе и вернуться
//
//

// еще одна сгенерированная функция, эта функция подвязывается
// к нашей форме (окну программы), и обрабатывает события нашей формы (к примеру
// изменение размера окна, тык мышкой, активность формы и тд)
// это основная функция нашей программы, которая отвечает за какие нибудь события
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    // добавим ассоциацию идентификатора окна программы
    // с нашей глобальной переменной, в которой храним
    // указатель на идентификатор
    Form1 = hWnd;

    // switch разновидность условия, не совсем понятно
    // целесообразность использования альтернативного
    // условного конструктора, поскольку то же самое 
    // можно было описать и конструкцией if (но предположу
    // что несмотря на то что switch не является "канонистической 
    // конструкцией, она обеспечивает одно единственное выполнение
    // условия, затем завершает, это может обеспечивает стабильность,
    // избегая множественных срабатываний на одну команду message)

    // в данном случае условие обрабатывает сообщения,
    // которые получает из GetMessage, в соответствии
    // с определенным содержимым message (который передается
    // в параметрах функции), и если значение message соответствует
    // какой либо конструкции case, выполняет этот фрагмент кода
    switch (message)
    {
        // не стоит забывать про зажатую Ctrl и левый тык мышкой
        // это перекинет в файл, где описано это ключевое слово
        

        // WM_COMMAND это универсальное событие обработки команд
        // (к примеру отправленных через SendMessage), сама конструкция
        // подразумевает условие, если содержимое message, соответствует
        // содержимому ключевого слова WM_COMMAND, выполняет блок кода внутри
        // скобочек (содержимое, читай какие-то данные, ассоциированные с 
        // этим ключевым словом)
        case WM_COMMAND:
        {
            // тут обработка элементов меню
            // сами элементы в ресурсах файла, 
            //сгенерированы автоматически
            int wmId = LOWORD(wParam);
            // Разобрать выбор в меню:
            switch (wmId)
            {
            case IDM_ABOUT:
                DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
                break;
            case IDM_EXIT:
                DestroyWindow(hWnd);
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
        }
        break;

        // условие, если данные в message соответствуют WM_PAINT,
        // или событию отрисовки выполняем код внутри
    case WM_PAINT:
        {
            // создаем переменную типа параметры
            // области рисования
            PAINTSTRUCT ps;

            // создаем переменную типа область рисования
            // и ассоциируем с ней результат выполнения функции
            // BeginPaint (в параметрах которой передаем
            // идентификатор нашего окна, и ассоциируем данные
            // из переменной с параметрами
            HDC hdc = BeginPaint(hWnd, &ps);
            // TODO: Добавьте сюда любой код прорисовки, использующий HDC...

            // завершаем процесс рисования,
            // вызывая специальную функцию,
            // данным примером хитроумно затопарен
            // процесс рисования объектов на форме,
            // функцией BeginPaint, который продолжится,
            // после выполнения функции EndPaint
            EndPaint(hWnd, &ps);
        }
        break;

        // если переменная message 
        // содержит WM_DESTROY
        // выполняем код для завершения
        // работы приложения
    case WM_DESTROY:
        PostQuitMessage(0);
        break;

        // если message не содержит никаких сообщений
        // (или сообщения не соответствуют ни одному из условий)
        // выполняем другой код, функцию, которая гарантирует обработку
        // "неопределенного" message, она нас тоже пока не интересует
        // (но может понадобится, так как мы можем ее "переописать",
        // для каких-либо наших задач)
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }

    // вернем результат выполнения функции 0
    // можно изменить на 1 чтобы мониторить 
    // работоспособность программы, исходя
    // из возвращаемого значения этой функции
    // (то есть если функция WndProc выполнилась
    // и не вернула на место вызова 1, значит
    // какое то решение не работает, для этого
    // просто можно создать переменную (в начале
    // блока кода, с которой ассоциировать что
    // нибудь (к примеру 0), и если функция смогла
    // выполнить свой код до этой позиции, возвращяем 1
    return 0;
}

// Обработчик сообщений для окна "О программе".

// аналогично только не для основного окна программы,
// а для диалога "Об программе", его разбирать смысла нет
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, LOWORD(wParam));
            return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}

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

Вот код этой функции:

MessageBox(NULL, TEXT("Текст"), TEXT("Заголовок"), 0);

Она принимает в себя разные данные, и показывает текст в маленьком окошке. Важный момент! Для редактора кода (компилятора) весь текст внутри потенциальные «ключевые слова», с которыми ассоциировано что ни будь, соответственно, чтобы текст использовать ни как ключевое к как «текст», его нужно помещать в «« двойные кавычки.

Вернемся к коду, найдем функцию с названием «WndProc» и переделаем ее, внутри условия добавим новый блочок условия для события нажатия левой кнопки мыши:

        case WM_LBUTTONDOWN:
        {

        }
        break;

Вот примерно такая конструкция, это аналог условия — если содержимое message соответствует данным, ассоциированным с клочимым словом WM_LBUTTONDOWN, выполняем код внутри, после чего ключевым словом break прерываем дальнейшее выполнение конструкции switch.

Разместим функцию показа сообщения внутри этого блока (между {} у тела конструкции switch), а этот блок в функции WndProc. Далее запустим ткнем мышкой в любом месте окна нашей программы, и увидим следующее:

 

Отлично мы теперь ты «попытка в программиста»!

Этап №2 самое сложное закончилось, начинается АД

Правым тыком по папке с исходными файлами (в обозревателе решений) создаем новый файл, в моем случае это «Source.cpp», в вашем думаю тоже (не суть как важно). Мы создали новый файл, он абсолютно пустой, но мы сейчас наполним его (ручками, не копирую мой код, это очень важно!).

Создадим «свой тип данных», а также познакомимся поближе:

Разворачивай меня полностью
// подключим вспомогательные библиотеки
#pragma once 
#include "stddef.h"
#include <string> 
#include <Windows.h>
#include <Strsafe.h>

// не будем заморачиватся, с++ очень старый язык
// это еще одна разновидность подключения библиотек
// (просто подключает не весь файл, а кусок из файла)
using namespace std;

// создадим наш собственный тип данных,
// что создавать переменные нашего типа данных
// (для удобства)
class MyVariableUniversalType {

	// опишем несколько переменных
	// разных типов данных, внутри нашего
	//
	// мы опишем их в "приватном" блоке кода
	// описывающего наш тип данных, чтобы они
	// были видны только нашему типу данных
	// и недоступны другим
private:

	bool BooleanType;
	int IntegerType;
	string TextType;

	//
	// единственное, это опишем еще одну функцию
	// что такое protected пока не нужно думать,
	// просто сделайте так же
	//
protected:
	// так как тип данных LPCWSTR Long Pointer to Constant Wide String
	// не совсем классический тип данных строка (не совсем "канонистический"), 
	// я использовал сводный тип данных строка из другой библиотеки, 
	// которую подключил сверху, поэтому опишем конвертор из str::string в LPCWSTR
	// вдаваться в подробности на этом этапе не нужно, дальше можно 
	// пропустить и просто скопировать
	LPWSTR TransformStringToWideString(string agrument1)
	{

		// создадим вспомогательную переменную
		// типа мультипеременная с кодами символов
		// (с которой мы ассоциируем результат создания 
		// нового массива типа WCHAR), длинной в 
		// количество символов из параметра, получаемого 
		// в функцию, при помощи встроенной функции 
		// length (встроенной в тип string)
		LPWSTR tmp_return = new WCHAR[agrument1.length()];

		// вызовем функцию, которая конвертирует набор символов строки
		// str::string в нужный нам набор символов, и сразу помещает его
		// в мультипеременную, функция принимает в себя указатель кодировки,
		// указатель способа конвертирования, текст, размер в байтах,
		// указатель переменной (в которую поместить результат), длину
		// в символах
		MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, agrument1.c_str(), agrument1.size(), tmp_return, agrument1.length());

		// из за особенностей типа LPWSTR
		// код последнего символа должен 
		// быть 0, это указатель того что
		// это конец текста
		tmp_return[agrument1.length()] = 0;

		// вернем результатом содержимое 
		return tmp_return;
	}

	// далее опишем общедоступные части нашего
	// типа данных (то есть доступные не только 
	// внутри нашего типа данных)
public:
	LPWSTR TransformStringToWideString2(string agrument1)
	{

		// создадим вспомогательную переменную
		// типа мультипеременная с кодами символов
		// (с которой мы ассоциируем результат создания 
		// нового массива типа WCHAR), длинной в 
		// количество символов из параметра, получаемого 
		// в функцию, при помощи встроенной функции 
		// length (встроенной в тип string)
		LPWSTR tmp_return = new WCHAR[agrument1.length()];

		// вызовем функцию, которая конвертирует набор символов строки
		// str::string в нужный нам набор символов, и сразу помещает его
		// в мультипеременную, функция принимает в себя указатель кодировки,
		// указатель способа конвертирования, текст, размер в байтах,
		// указатель переменной (в которую поместить результат), длину
		// в символах
		MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, agrument1.c_str(), agrument1.size(), tmp_return, agrument1.length());

		// из-за особенностей типа LPWSTR
		// код последнего символа должен 
		// быть 0, это указатель того, что
		// это конец текста
		tmp_return[agrument1.length()] = 0;

		// вернем результатом содержимое 
		return tmp_return;
	}

	// создадим общедоступную переменную
	// типа LPCWSTR, с которой ассоциируем
	// ничего (L вначале нужно, потому что
	// у компилятора и программы разные кодировки)
	// вникать в детали пока не нужно
	LPCWSTR LPCWSTRTextType = L"";

	// создадим функцию, одноименную с нашим типом данных,
	// такая функция будет вызывается при попытке создать новый
	// экземпляр переменной нашего типа данных (конструктор, или
	// инициирующий блок), в момент создания будем наполнять нашу переменную
	// базовыми данными (ложью для типа булевых данных, -1 для числовых,
	// ничем для строковых)
	MyVariableUniversalType() {
		BooleanType = false;
		IntegerType = -1;
		TextType = "";
	}

	// теперь для закрепления знаний опишем несколько вспомогательных
	// функций, которые будут сразу в нашем типе данных (то есть будут
	// доступны в каждой новой копии переменной), без необходимости
	// описания этих функций по новый

	// создадим функцию для конвертирования
	// булевой переменной в числовую
	int BooleanTypeToInteger(bool argument1)
	{

		// создадим условие, если значение
		// передаваемое в качестве параметра (аргумента)
		// соответствует истине, завершаем функцию
		// и возвращаем 1, иначе 0 (числовые аналоги)
		if (argument1 = true)
		{
			return 1;
		}
		else
		{
			return 0;
		}
	}

	// создадим обратную функцию
	bool IntegerTypeToBoolean(int argument1)
	{

		// создадим условие, если значение
		// передаваемое в качестве параметра (аргумента)
		// больше 0, завершаем функцию с
		// возвратом результата true, иначе false
		if (argument1 > 0)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	// создадим функцию для конвертирования 
	// булевой переменной в строковую
	string BooleanTypeToString(bool argument1)
	{

		// создадим условие, если значение
		// передаваемое в качестве параметра (аргумента)
		// соответствует истине, завершаем функцию
		// и возвращаем текст true, иначе false
		if (argument1 = true)
		{
			return "true";
		}
		else
		{
			return "false";
		}
	}

	// и обратную
	// тут немного сложнее, поскольку
	// вариантов написания может быть много,
	// и надо обработать их все
	bool StringTypeToBoolean(string argument1)
	{

		// создадим вспомогательную
		// переменную, сразу ассоциируем
		// с ней значение false, чтобы
		// в случае если ни одно из условий
		// не сработает, функция вернула результат
		// в место вызова
		bool tmp_return = "false";

		// создадим переменную (мультипеременную, много значений в одной)
		// в которую специальным образом сразу поместим
		// все возможные варианты текстового значения true
		string true_variants[]
		{
			"true",
			"True",
			"TRUE",
			"truE",
			"TRue"
			/* ... и тд */
		};

		// создадим переменную счетчик для цикла
		int a = 0;

		// создадим переменную с общим количеством записей в
		// мультипеременной при помощи встроенной функции size
		int b = true_variants->size();

		// и в цикле проверим каждое из значений массива true_variants
		// на соответствие 
		while (a < b) {

			// создадим условие, если значение
			// передаваемое в качестве параметра (аргумента)
			// соответствует нашему теоретическому значению
			// из мультипеременной (к конкретному значению мы
			// обращаемся по индексу записи [], который берем
			// из переменной счетчика), во вспомогательную переменную
			// tmp_return помещаем true, и завершаем цикл
			if (argument1 == true_variants[a])
			{
				tmp_return = true;
				a = b + 1;
			}

			// после каждого повтора увеличиваем счетчик на 1
			// чтобы таким образом перебрать все варианты
			a = a + 1;
		}

		// вернем значение из вспомогательной переменной
		return tmp_return;
	}

	//
	// 
	// нечто подобное можно сделать и для числовых данных
	// 
	//

	// чтобы не растягивать код еще больше опишем
	// обращения к данным в нашем типе данных
	// (в канонистических языках программирования есть
	// такое понятие как свойства, в этом подходе с++
	// нет (явно), поэтому мы опишем свойства "получить" и 
	// "изменить" в виде функций (сейчас непонятно что
	// это и как это полезно, и удобно, но со временем 
	// понимание придет)


	// опишем функцию для получения из нашего типа
	// содержимое в формате LPCWSTR, в качестве
	// параметра укажем тип данных, которые нужно
	// превратить в LPCWSTR и вернуть (параметром
	// будем передавать четко описанный текстовый
	// указатель
	LPCWSTR GetVariableValue(string call_back_type)
	{

		// создадим условие, если параметр,
		// передаваемый в функцию соответствует
		// слову boolean, выполняем возврат результата,
		// который получается в результате использования
		// функции BooleanTypeToString, в которой в качестве параметра
		// был передан параметр из скрытой переменной BooleanType, а результат
		// выполнения функции BooleanTypeToString был сразу передан
		// в качестве параметра в функцию TransformStringToWideString,
		// которая уже на свое место вернула результат (уже совей обработки 
		// результата) на место, где должны быть данные для выполнения возврата 
		// из этой функции, и в переменную LPCWSTRTextType
		if (call_back_type == "Boolean")
		{
			LPCWSTRTextType =
				TransformStringToWideString(
					BooleanTypeToString(BooleanType)
				);

			return
				TransformStringToWideString(
					BooleanTypeToString(BooleanType)
				);
		}

		// сделаем нечто подобное для строковой переменной
		if (call_back_type == "String")
		{
			LPCWSTRTextType =
				TransformStringToWideString(
					TextType
				);

			return
				TransformStringToWideString(
					TextType
				);
		}

		// вернем ничего, если
		// ни одно из условий не сработало
		LPCWSTRTextType = L"";
		return L"";
	}

	//
	//
	// далее должна быть аналогичная функция 
	// для определения из типа данных из LPCWSTRTextType,
	// конвертирования в соответствующий простой тип,
	// и перемещение в переменную, но так как тут уже 400
	// строчек кода (хотя я планировал уложиться в 100) 
	// рекомендую погуглить как ее реализовать
	//
	//

};

// классы (так называются наши типы данных), подразумевают
// такое явление как наследование решений, это позволяет 
// создать новый тип данных, но при этом использовать в качестве
// заготовки все содержимое типа данных, который является
// родителем этого, в данном примере я создаю MyChildrenUniversalType
// и через : (двоеточие) указываю в какую секцию этого класса
// поместить все решения из класса родителя (я выбрал private,
// чтобы все лишние функции не мешали при обращении к этому классу),
// далее идет название класса, который должен является родителем,
// таким подходом к программированию (это называется ООП) можно
// значительно упростить процесс для крупных программ, перенося
// уже готовые функции в другие классы, и использовать их когда нужно
class MyChildrenUniversalType : private MyVariableUniversalType {
public:

	// конструктор класса можно оставить пустым,
	// хотя можно было даже не описывать ни конструктор,
	// ни деструктор
	void MyVariableUniversalType() {

	}

	// еще одно полезное свойство класса
	// это создание множества функций под одним
	// ключевым словом (это называется перегрузка),
	// и позволяет в зависимости от ситуации (от набора 
	// параметров, которые были переданы в функцию) 
	// использовать разные решения, в данном случае я 
	// создаю несколько функций для конвертирования
	// текста (AnyTo), и задаю им разные аргументы,
	// чтобы во время вызова этой функции в коде не
	// возится с разными типами данных, пытаясь их
	// подвести под соответствие одного к другому
	LPCWSTR AnyTo(string Any_std_string) {

		// получить доступ к функции TransformStringToWideString
		// из другого типа данных мы можем благодаря секции protected,
		// она создана специально для того, чтобы все что находится внутри
		// секции не было видимо для всех, но было доступно для 
		// использования в дочерних классах
		return (LPCWSTR)TransformStringToWideString(Any_std_string);
	}

	LPCWSTR AnyTo(LPCSTR Any_LPCSTR) {
		return (LPCWSTR)Any_LPCSTR;
	}

	LPCWSTR AnyTo(char* Any_CHAR) {
		return (LPCWSTR)Any_CHAR;
	}
};

Теперь, когда мы получили базовое представление о том, что такое программирование, и как это работает, можем приступить к созданию чего‑либо полезного, а именно кнопки (но, если дополнить наш тестовый Source.cpp недостающими функциями, он станет вполне себе полезным, а иногда даже незаменимым).

Этап №3 ада больше нет, сейчас только не осмысляемо сложное

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

Для начала правым тыком по исходным файлам создадим еще один файл cpp, у меня это Source1.cpp, у вас, решайте сами.

Бом бом бом (наш класс кнопки)
// новый уровень сложности, комментариев будет меньше

// эта строчка описана в первом этапе в самом начале
#pragma once 

// подключим библиотеки
#include "framework.h"
#include "windows.h"
#include "winuser.h"

// создадим наш тип данных, и опишем его сущность
// (на этот раз создадим кнопку)
class ObjectButton {
private:
	// поскольку целесообразность
	// "свойств класса" крайне условна
	// в с++ (и носит собой в основном
	// задачу избавить код от потенциальных
	// конфликтов с занятыми словами, под
	// какие-либо кунштюки, так как язык очень емкий, и
	// старый), опишем свойства в публичной секции
	// и сразу оттуда будем управлять объектом
public:

	// создадим простые числовые переменные
	// с которыми ассоциируем размеры и расположение
	// нашей кнопки на форме (ширину, высоту, отступ
	// из левого края окна программы, отступ
	// сверху окна программы)
	int Width = 100;
	int Height = 40;
	int Left = 10;
	int Top = 10;

	// код цвета для бордюра кнопки
	// и текста кнопки (аналогично для
	// ситуации, когда мышь над кнопкой)
	COLORREF BorderColor = RGB(0, 0, 0);
	COLORREF TextColor = RGB(0, 0, 0);
	COLORREF BorderActiveColor = RGB(120, 150, 120);
	COLORREF TextActiveColor = RGB(120, 150, 120);

	// переменную, где будет хранится текст
	// который мы будем отрисовывать на кнопке
	// (L перед текстом я использую, потому что
	// у компилятора и создаваемого приложения
	// разные кодировки), не заморачивайтесь,
	// просто делайте также
	LPCWSTR Caption = L"Button";

	// внутренние вспомогательные переменные
	// (вот их имеет смысл описать в приватной секции,
	// ну да ладно)
	int cfg_use_animation = 1;
	int cfg_enable_click_on_button = 1;
	int cfg_enable_border = 1;
	LPCSTR cfg_text_style = "Tahoma";
	int cfg_text_size = 18;
	int help_button_active_status = 0;
	int help_left_window_magrin_size = 8;
	int help_top_window_magrin_size = 51;
	int help_button_first_draw_status = 0;

	//
	//
	// далее опишем тело нашего типа данных (тело класса)
	//
	//


	// создадим "конструктор" для объекта
	// (эта функция будет вызывается, когда
	// мы будет создавать в переменную новый экземпляр
	// этого объекта)
	ObjectButton(int width, int height, int left, int top) {
		Width = width;
		Height = height;
		Left = left;
		Top = top;
	}

	// это "заголовок" функции, которая 
	// не определена, но должна быть у нашего
	// объекта кнопка (то есть эта заготовка,
	// на место которой мы поместим функцию,
	// которая будет выполнятся в момент нажатия
	// кнопки, а поскольку мы описываем тип данных
	// (шаблон), а не конкретную кнопку, мы не знаем
	// (пока) какой функционал будет у этой конкретной кнопки
	// (не обязательно вдавятся в подробности), но о назначении
	// звездочки я говорил в конце этапа 2)
	void(*ButtonClick)();

	// сразу же опишем функцию для проверки
	// координат, в которых происходит нажатие,
	// если координаты пересекаются с ожидаемым
	// расположением нашей кнопки, вызываем функцию
	// ButtonClick, о которой я говорил выше
	// (в качестве аргумента будет передавать уникальный
	// идентификатор окна программы, на которой будет находится
	// создаваемый нами объект кнопка)
	bool CheckMouseCoordinatesBeforeStartClickFunction(HWND form_link) {

		// создадим переменную, в которую
		// получим координаты мыши (текущие),
		// тип данных POINT, по сути, две числовых
		// переменных в одной (такой тип принимает
		// готовая функция для получения координат)
		POINT mouse_coordinates;

		// вызовем функцию (из библиотек),
		// которая позволяет получить в какую-либо переменную
		// текущие координаты мыши (в качестве параметра указываем
		// ранее созданную для этого переменную
		GetCursorPos(&mouse_coordinates);

		// создадим еще одну переменную (типа RECT,
		// он же квадрат, переменная состоит из 4 числовых
		// переменных в одной), именно такую переменную
		// принимает в себя готовая функция из библиотек
		// для получения размеров окна программы, и его
		// расположения на рабочем столе
		RECT program_size_and_position_on_screen;

		// вызовем функцию для получения размеров и позиции
		// окна программы (в качестве параметров указываем
		// идентификатор окна программы, и переменную, с 
		// которой можно ассоциировать результат выполнения функции)
		GetWindowRect(form_link, &program_size_and_position_on_screen);

		// создадим вспомогательную переменную,
		// в которой будем хранить состояние о статусе
		// кнопки (была ли вызвана функция по нажатию,
		// или нет)
		bool help_execute_status = false;

		// создадим "сложносоставное" условие (&& двойной амперсант это
		// логическое и, || а это логическое или),
		// если координаты мыши по оси Х (отступ слева)
		// меньше, чем отступ окна программы (отступ слева у окна), который был
		// приплюсован к отступу слева у кнопки (на форме, в переменной Left), 
		// и (&&) координаты мыши по оси Х меньше, чем отступ окна программы +
		// отступ конкретной кнопки на форме + ширина кнопки + 8 (пикселей бордюра
		// окна программы), тогда выполняем фрагмент кода внутри этого сложного условия
		if (
			mouse_coordinates.x > program_size_and_position_on_screen.left + Left
			&& mouse_coordinates.x < program_size_and_position_on_screen.left + Left + Width
			)
		{
			// выполняем еще одно условие (идентичное первому, но теперь для высоты,
			// то есть вместо Left -> Top, вместо Width -> Height), и таким образом
			// проверяем находится ли мышь в диапазоне от начала кнопки, до ее конца,
			// то есть в рамках размеров кнопки
			if (
				mouse_coordinates.y > program_size_and_position_on_screen.top + Top
				&& mouse_coordinates.y < program_size_and_position_on_screen.top + Top + Height)
			{
				// во вспомогательную переменную отправляем
				// (меняем) значение, которое будет сигнализировать что
				// функция CheckMouseCoordinatesBeforeStartClickFunction
				// вызвала функцию нажатия кнопки
				help_execute_status = true;

				// вызываем саму функцию (которую мы пока не знаем,
				// поэтому просто указываем заголовок этой теоретической
				// функции), но при условии, что в свойстве кнопки (переменной вверху)
				// с названием cfg_enable_click_on_button ассоциированно число 1
				if (cfg_enable_click_on_button == 1) {
					ButtonClick();
				}
			}
			else {
				help_execute_status = false;
			}
		}

		// возвращаем содержимое вспомогательной переменной со статусом нажатия
		return help_execute_status;
	}

	// опишем функцию, эта функция будет проверять находится ли мышь
	// над областью, где находится кнопка (по сути, дублирующийся функционал
	// с кнопкой нажатия, поэтому имеет смысл переписать ту функцию выше,
	// а в функции нажатия уже вызвать эту функцию для проверки координат)
	// отличие этой функции в том, что она может проверять по какой-либо одной 
	// оси, а не по обеим сразу
	bool CheckIsMouseOnThisButtonOrNo(HWND form_link, int axis_direction) {

		// дублируем код аналогично
		// представленному в функции нажатия
		POINT mouse_coordinates;
		RECT program_size_and_position_on_screen;
		//
		GetCursorPos(&mouse_coordinates);
		GetWindowRect(form_link, &program_size_and_position_on_screen);

		// но теперь создаем другую конструкцию условия,
		// если в параметре при вызове функции передано число 0
		// проверяем координаты только по оси Х, иначе по оси У
		if (axis_direction == 0) {

			// условие проверки координат по оси Х
			if (
				mouse_coordinates.x > program_size_and_position_on_screen.left + Left
				&& mouse_coordinates.x < program_size_and_position_on_screen.left + Left + Width
				) {

				// возвращаем истину, если проверка
				// пройдена
				return true;
			}
		}

		// конструкция else встречается впервые, это тот самый блочок кода, который выполняется
		// как альтернатива (если условие не выполнилось), для дополнения условия этим фрагментом,
		// надо просто удалить ; в конце основного условия, добавить слово else и развернуть тело
		// {} блока кода, но конструкция if может работать и без него
		else {

			// все то же самое только теперь по оси У
			if (
				mouse_coordinates.y > program_size_and_position_on_screen.top + Top
				&& mouse_coordinates.y < program_size_and_position_on_screen.top + Top + Height
				) {

				// возвращаем истину, если проверка
				// пройдена
				return true;
			}
		}

		// если проверки не завершили функцию,
		// соответственно координаты неправильные,
		// и завершаем функцию с false
		return false;
	}

	// опишем функцию, которая будет
	// отрисовывать текст (надпись на кнопке),
	// в качестве параметров передаем в функцию
	// указатель на область рисования, текст,
	// цвет отрисовки текста
	void DrawButtonText(HDC hdc, LPCWSTR text, COLORREF color)
	{

		// создаем вспомогательную переменную, с которой
		// ассоциируем результат создания стиля текста,
		// при помощи функции (из библиотек) CreateFontA,
		// функция принимает в себя очень много параметров
		HFONT hFont;

		// ассоциируем с нашей переменной результат
		// выполнения функции для создания нашего 
		// стиля шрифта, при этом в качестве параметров
		// имени шрифта и размера шрифта указываем наши
		// переменные со свойствами стиля текста, остальное
		// оставляем по умолчанию
		hFont = CreateFontA(
			cfg_text_size, // задаем высоту в em
			cfg_text_size / 3, // ширину (в em), делим на 3 от размера текста
			0, // градус наклона (вращение текста)
			0, // градус угла плоскости (наклон горизонта, от которого высчитывается градус наклона)
			FW_NORMAL, // толщина символов (тонкий, средний, толстый, очень толстый и тд)
			false, // рисовать курсивом (да или нет)
			false, // подчеркивать
			false, // зачеркивать
			OUT_DEFAULT_PRECIS, // указатель степени подгона шрифта под заданные параметры
			CLIP_DEFAULT_PRECIS, // указатель степени обрезки букв, которые вылезают за пределы своей области
			DEFAULT_CHARSET, // указатель кодировки для символов
			DEFAULT_QUALITY, // общее качество отрисовки (применения сглаживания, обводки, и тд)
			DEFAULT_PITCH | FF_DONTCARE, // отступ между символами (передаю сразу два 
			// параметра, через условную "запятую", второй
			// параметр указывает может ли расстояние между 
			// символами быть постоянным, или любым,
			// в параметре указано любое расстояние)
			cfg_text_style // имя шрифта
		);

		// вызываем функцию, которая привяжет
		// созданный нами шрифт с областью рисования,
		// в качестве параметров передаем идентификатор области
		// рисования, и наш стиль шрифта
		SelectObject(hdc, hFont);

		// создаем вспомогательную переменную типа SIZE
		// (две числовых переменных в одной), в которую будем
		// получать размер текста в пикселях, для конкретной
		// области рисования (формы программы), из библиотечной
		// (готовой функции)
		SIZE help_text_size;

		// получаю размеры текста в пикселях (в качестве параметров
		// область рисования, текст, размер строки (в байтах)),
		// в переменную, с которой нужно ассоциировать результат выполнения
		// этой функции (help_text_size)
		GetTextExtentPoint32(hdc, text, lstrlen(text), &help_text_size);

		// вызываем другую готовую функцию, эта функция
		// меняет цвет, которым будет напечатан текст,
		// в качестве параметра передаем указатель на область рисования
		// (нашу форму), и цвет текста (из аргумента color)
		SetTextColor(hdc, color);

		// вычисляем середину области кнопки, чтобы
		// отрисовать текст "по центру", для этого из ширины 
		// и высоты кнопки (разделенные на 2) вычитаем
		// ширину, и высоту текста (также разделенную на 2)
		// результат ассоциируем с переменными х и у
		int x = (Width / 2) - (help_text_size.cx / 2);
		int y = (Height / 2) - (help_text_size.cy / 2);

		// вызываем функцию для отрисовки текста на области рисования
		// в качестве параметров функция принимает область рисования,
		// отступ слева (от края формы), отступ сверху (от края формы),
		// текст для рисования, размер текста в байтах (текст берем из
		// второго параметра, получаемого при вызове функции)
		TextOut(hdc, Left + x, Top + y, text, lstrlen(text));
	}

	// теперь опишем функцию для отрисовки рамки у кнопки
	// для этого используем функцию SetPixel (позволяет
	// изменить цвет конкретного пикселя на области рисования),
	// используем циклы и размеры кнопки (из свойств объекта),
	// эта функция не возвращает никаких значений на место своего
	// вызова, поэтому тип данных у функции void (ничего), команда
	// return также не требуется, функция принимает параметры 
	// с указанием области рисования и цветом отрисовки
	void DrawButtonBorder(HDC hdc, COLORREF color) {

		// создадим условие, если значение свойства
		// cfg_enable_border равно 0, завершаем
		// выполнение функции еще до начала
		if (cfg_enable_border == 0)
		{
			return;
		}

		// создаем числовые переменные а и б, ассоциируя с ними
		// свойства объекта, но так чтобы можно было нарисовать
		// прямую линию в цикле, к примеру из верхней левой точки 
		// вправо, изменяя цвет каждого пикселя на пути, до тех пор
		// пока значение переменной старта (а, или левого верхнего края)
		// не станет равным (б, или левого верхнего края + ширина кнопки)
		int a = Left;
		int b = Left + Width;

		// вот пример
		// создаем цикл, до тех пор, пока, а меньше б повторяем код цикла
		while (a <= b) {

			// вызываем функцию изменения цвета пикселя, в качестве параметров
			// передаем указатель на область рисования, координаты оси х и у,
			// новый цвет пикселя (берем из color)
			SetPixel(hdc, a, Top, color);

			// эта упрощенная форма записи a = a + 1;
			// созданная специально для циклов
			a++;
		}

		// аналогичным образом протягиваем линию из правого
		// верхнего края вниз по размеру высоты кнопки
		a = Top;
		b = Top + Height;

		while (a <= b) {
			SetPixel(hdc, Left + Width, a, color);
			a++;
		}

		// еще одну из левого нижнего края, по ширине
		a = Left;
		b = Left + Width;

		while (a <= b) {
			SetPixel(hdc, a, Top + Height, color);
			a++;
		}

		// и последнюю из левого верхнего края по высоте
		a = Top;
		b = Top + Height;

		while (a <= b) {
			SetPixel(hdc, Left, a, color);
			a++;
		}
	}

	// теперь опишем функцию для отчистки области рисования
	void ClearButtonDraw(HWND hwnd) {

		// создадим вспомогательную переменную, с которой
		// ассоциируем координаты и размеры области нашей кнопки,
		// эта переменная нужна для готовой функции, которая очистит
		// область (переменная RECT это 4 числовых переменных в одной)
		RECT help_button_coordinates_and_size;

		// наполняем переменную из свойств нашей кнопки
		// (из значения Left мы вычитаем отступы меню
		// (которое было сгенерировано + бордюр окна, 19 и 32 пикселей),
		// слева вычитаем бордюр окна, 8 пикселей), для того чтобы
		// удалить всю рамку кнопки, вылезаем за 1 пиксель
		// для right и botton (конструкция help_left_window_magrin_size - 1)
		help_button_coordinates_and_size.left = Left - (help_left_window_magrin_size);
		help_button_coordinates_and_size.top = Top - (help_top_window_magrin_size);
		help_button_coordinates_and_size.right = (Left + Width) - (help_left_window_magrin_size - 1);
		help_button_coordinates_and_size.bottom = (Top + Height) - (help_top_window_magrin_size - 1);

		// вызываем функцию очистки
		// (ВАЖНО! даже если указать строго
		// область для стирания, функция
		// все равно сделает невалидной
		// всю область окна программы,
		// соответственно необходимо перерисовать
		// все объекты, но не всегда, типичная
		// наследованная функция с неполной
		// обратно совместимостью), в качестве 
		// параметров область для рисования,
		// координаты (прямоугольник) области
		// стирания, третий параметр ответ на вопрос о
		// стирании
		InvalidateRect(hwnd, &help_button_coordinates_and_size, TRUE);
	}

	// опишем функцию анимации при наведении курсора
	// в качестве параметров функция будет принимать идентификатор
	// окна программы, и указатель на область рисования
	void ActiveButtonAnimate(HWND hwnd, HDC hdc) {

		// создаем условие, которое вызывает нашу функцию для
		// проверки координат мыши, и ее пересечения области кнопки
		if (
			CheckIsMouseOnThisButtonOrNo(hwnd, 0) == true
			&& CheckIsMouseOnThisButtonOrNo(hwnd, 1) == true
			) {
			// добавим условие, завязанное на вспомогательном свойстве объекта
			// оно позволит отрисовывать кнопку один раз при наведении курсора,
			// и один раз при покидании курсором области кнопки (суть в 4 условиях,
			// то есть может быть 4 разных состояния (курсор над кнопкой, но 
			// help_button_active_status равно 0, курсор над кнопкой, но 
			// help_button_active_status равно 1 и тд)
			if (help_button_active_status < 1) {

				// рисуем рамку с цветом из свойств объекта
				DrawButtonBorder(hdc, BorderActiveColor);

				// рисуем текст с цветом из свойств нашего объекта
				DrawButtonText(hdc, Caption, TextActiveColor);

				// изменяем значение вспомогательной переменной на активный
				help_button_active_status = 1;
			}
		}
		// если курсор не над кнопкой проверяем установлено ли
		// help_button_active_status в 1, если да, перерисовываем
		// кнопку не активным, а простым (основным) цветом
		else {
			if (help_button_active_status > 0) {

				// рисуем рамку с цветом из свойств объекта
				DrawButtonBorder(hdc, BorderColor);
				// рисуем текст с цветом из свойств нашего объекта
				DrawButtonText(hdc, Caption, TextColor);
				// изменяем значение вспомогательной переменной на активный
				help_button_active_status = 0;
			}
		}
	}

	// напишем последнюю функцию, это будет функция конструктора
	// на самом деле у нас уже есть функция конструктора (одноименная
	// с названием типа данных, в самом начале), но так как у нас
	// последовательный для чтения код, опишем еще раз
	void Create(HWND hwnd, HDC hdc) {

		// создаем условие (с помощью вспомогательного свойства
		// нашего класса), благодаря которому выполняем функцию
		// Create только один раз после создания объекта, в любой
		// удобный момент
		if (help_button_first_draw_status == 0) {
			help_button_first_draw_status = 1;
		}
		else {
			// так как функция ничего не возвращает на свое место, команду return
			// используем для того, чтобы прервать выполнение функции, в случае
			// если значение вспомогательного свойства help_button_first_draw_status
			// не равно 0 (то есть функция уже была выполнена ранее)
			return;
		}
		// рисуем рамку с цветом из свойств объекта
		DrawButtonBorder(hdc, BorderColor);

		// рисуем текст с цветом из свойств нашего объекта
		DrawButtonText(hdc, Caption, TextColor);
	}

	// это упрощенная функция для отрисовки линий по "перемещению пера",
	// то есть мы сначала перемещаем в какие-то координаты условную точку
	// начала отрисовки, и из нее протягиваем линию в другу точку, потом 
	// снова перемещаем и тд, я просто оставлю это здесь
	void DrawLine(HDC hdc, COLORREF color, int x_from, int y_from, int x_to, int y_to) {

		MoveToEx(hdc, x_from, y_from, NULL);
		LineTo(hdc, x_to, y_to);

	}
};

Итак, теперь у нас есть базовый класс для объекта кнопка, поэтому теперь мы можем перейти непосредственно к основному коду программы.

Для начала подключим наш файл «нашу библиотеку» к основному файлу программы (лучше это сделать вверху, но можно где угодно, главное, чтобы не было логических ошибок (функции вызываются раньше, чем они описаны).

Для этого воспользуемся командой #include.

#include "Source1.cpp"

Переместимся чуть глубже и где ни будь в коде, объявим новые переменные, и сразу ассоциируем с ними результат инициации (создания) этих переменных, у меня это будет секция глобальных переменных.

ObjectButton* MOYA_KNOPKA_AHAHAHA = new ObjectButton(100, 50, 100, 200);

Назвать можно как угодно, у меня это «МОЯ КНОПКА АХАХАХА», которая будет создаваться в координатах 100 пикселей от левого края, 200 сверху, размером 100 на 50 пикселей.

Синтаксис не простой, так как тип данных идентичен с функцией. Звездочка говорит нам о том, что ключевое слово (MOYA_KNOPKA_AHAHAHA) является указателем на данные типа «наш тип данных» (ObjectButton), с которым мы ассоциируем результат выполнения встроенной функции new, которая создает новый экземпляр объекта, попутно вызывая инициирующую функцию (ObjectButton).

Надеюсь, с этим понятно, но даже если нет, ничего страшного, все придет со временем. Наша кнопка создана, но, если запустить программу мы ничего не увидим (потому что кнопка не отрисовывается), поэтому давайте перейдем к секции с «событиями» (это функция WinProc).

И сразу добавим несколько «событий»:

    case WM_CREATE:
    {

    }
    break;

    case WM_LBUTTONDOWN:
    {
        MessageBox(NULL, TEXT("Текст"), TEXT("Заголовок"), 0);

    }
    break;

    case WM_KEYDOWN:
    {

    }
    break;


    case WM_MOUSEMOVE:
    {

    }
    break;

Думаю, из названий понятно, за что отвечают эти события. Для начала дополним событие WM_PAINT (событие отрисовки):

            // добавим условие, если значение свойства
            // help_button_first_draw_status равно 0,
            // значит функция Create еще никогда не выполнялась,
            // после создания нового экземпляра объекта, выполним
            // ее, при этом вычитая размеры бордюра и меню, так как
            // событие отрисовки формы вызывается много раз,
            // в том числе до того, как буду объявлены объекты меню и
            // бардюра окна программы
            if (MOYA_KNOPKA_AHAHAHA->help_button_first_draw_status == 0) {
                MOYA_KNOPKA_AHAHAHA->Left = MOYA_KNOPKA_AHAHAHA->Left - 8;
                MOYA_KNOPKA_AHAHAHA->Top = MOYA_KNOPKA_AHAHAHA->Top - 51;
                MOYA_KNOPKA_AHAHAHA->Create(Form1, hdc);
                MOYA_KNOPKA_AHAHAHA->Left = MOYA_KNOPKA_AHAHAHA->Left + 8;
                MOYA_KNOPKA_AHAHAHA->Top = MOYA_KNOPKA_AHAHAHA->Top + 51;
            }

Запустим программу и увидим нечто подобное (не забывайте указать этот код между BeginPaint и EndPaint).

Отлично! Все проще чем кажется, теперь добавим анимацию:

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

void AHAHAHA_BTN_ShowMessage()
{
    MessageBox(NULL, TEXT("Текст"), TEXT("Заголовок"), 0);
}

В событии создания формы (WM_CREATE) настроим нашу кнопку:

    case WM_CREATE:
    {
        MOYA_KNOPKA_AHAHAHA->ButtonClick = &AHAHAHA_BTN_ShowMessage;
        MOYA_KNOPKA_AHAHAHA->Caption = TEXT("Ткни и увидишь");

    }

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

И последняя — это обработка «тыка» мышкой по кнопке:

    case WM_LBUTTONDOWN:
    {
        MOYA_KNOPKA_AHAHAHA->CheckMouseCoordinatesBeforeStartClickFunction(Form1);
    }

Сохраняем, запускаем, видим следующее:

У меня текст вылез за пределы кнопки, но не суть. Не будем останавливается, создаем последний новый файл (у меня по старинке Source2.cpp).

Этап №4 Настолько неописуемо сложно что легко!

На этот раз опишем тип данных для создания объекта, который сможешь загружать, и предоставлять удобную работу с текстовыми файлами (ну и сохранять разумеется), назовем его собирательным термином StringList, подобный класс есть во многих языках программирования.

Ближе к делу (ВПЕРЕД СОБАКИ! К ПОБЕДЕ!)
// подключаем библиотеки
#pragma once 
#include <iostream>
#include <fstream>
#include "windows.h"
#include "winuser.h"
#include "winbase.h"
#include <string> 
#include "stddef.h"
#include <Strsafe.h>
#include <vector>
using namespace std;

// теперь заключительный этап создание кода,
// с минимумом комментариев, и максимумом 
// терминологии (для придания "каноничности"
// нашей программе)
class ObjectTextFileLoader {

    // для простоты понимания объясню один момент,
    // мало кто помнит, но точно также как function, 
    // if, while, switch, и тд, являются 
    // "заголовками конструкций", private секция
    // в классе является заголовком класса, а public его
    // телом (по аналогии с {телом конструкций}), и реализацией
private:

    // путь к файлу
    LPCWSTR HFilePath;

    // оригинальное содержимое файла
    string HText;

    // массив строк файла, так как в с++ нет такого понятия 
    // как "безразмерный массив", который можно было бы
    // дополнить, определим переменную типа vector (из 
    // библиотеки std), она подразумевает под собой аналог массива
    std::vector<std::string> HStringsList;

public:


    // конструктор класса (функция, которая выполняется
    // в момент создания нового экземпляра объекта)
    ObjectTextFileLoader(LPWSTR file_path)
    {
        // перенесем путь к файлу (из аргумента функции)
        // в свойство FilePath (переменную в классе, или
        // конкретном экземпляре объекта)
        HFilePath = file_path;
    }

    // загрузка и чтение файла, для этого мы используем
    // функцию CreateFileW, которая подготовит файл для чтения
    // 
    LPSTR LoadFromFile(LPCWSTR file_path)
    {
        HFilePath = file_path;

        // открываем файл универсальной командой для создания,
        // перезаписи файла
        HANDLE original_file = CreateFileW(HFilePath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

        // создаем переменную file_size типа DWORD (разновидность
        // числовой переменной, для работы с двоичными числами),
        // с переменной ассоциируем результата выполнения функции
        // GetFileSize, чтобы получить размер загружаемого файла
        // в битах
        DWORD file_size = GetFileSize(original_file, NULL);

        // создаем вспомогательные переменные
        // для чтения данных из файла, и переноса
        // в переменную внутри программы,
        // для этого создаем числовую переменную
        // current_read_position (нужна для функции
        // ReadFile, для реализации частичного, или
        // последовательного (полного) чтения файла),
        // в данном случае переменная не используется,
        // но обязательна для функции
        DWORD current_read_position = 0;

        // создаем вспомогательный обработчик для создания
        // переменной tmp_result, с которым ассоциируем результат
        // выделения памяти в программе, размером в file_size,
        // при этом сразу заполняем выделенную память нулями
        HGLOBAL handler_tmp_result = GlobalAlloc(GHND, file_size);

        // фиксируем выделенный фрагмент памяти (для защиты от
        // использования в других решениях, внутри нашей программы),
        // в качестве временного хранилища (то есть для сохранения
        // целостности загружаемых из файлов данных), который сразу 
        // описываем как тип данных LPSTR (функция ReadFile
        // может принимать в себя любой тип данных переменной,
        // в которую нужно направлять прочитанное)
        LPSTR tmp_result = (LPSTR)GlobalLock(handler_tmp_result);

        // вызываем функцию ReadFile (в качестве аргументов передаем
        // переменную с "файловым потоком", хранилище для перемещения считанных
        // данных, общие количество байтов, которые нужно прочитать, позицию, с
        // которой нужно начинать чтение (мы загружаем содержимое
        // файла целиком, и сразу помещаем в переменную tmp_result, но лучше
        // выполнять фрагментированное чтение (с фиксированным количеством
        // байтов, читаемых за 1 вызов функции, к примеру 1024) через промежуточную
        // переменную, в цикле (чтобы сразу интерпретировать, или дешифровывать
        // прочитанные данные), последней аргумент (NULL), предназначен для случаев,
        // когда файл открыт не в режиме чтения, а с мультидоступом (чтение, запись,
        // изменение другими программами, и тд), для смещения в соответствующую
        // позицию байт
        ReadFile(original_file, tmp_result, file_size, &current_read_position, NULL);

        // разблокируем, ранее зафиксированный кусок памяти, так как
        // переменная уже наполнена (использовать ее просто невозможно)
        GlobalUnlock(handler_tmp_result);

        // закрываем файловый поток, так как файл больше не нужен
        CloseHandle(original_file);

        // вернем результат
        HText = tmp_result;

        // выводим сообщение
        MessageBox(NULL, TEXT("Текст был загружен из файла"), TEXT("Показать содержимое из текста"), 0);
        return tmp_result;

    }

    // добавим функцию для конвертирования строки string в строку LPCWSTR,
    // на самом деле у нас уже есть такая функция (в другом классе), но
    // так как функция не большая, ничего страшного не случиться если ее 
    // продублировать (кроме того, архитектура нашей программы не предполагает
    // использование компонентов программы между собой, так как программа
    // спроектированна по схеме зависимостей "все к одному")
    LPCWSTR ConvertToLPCWSTR(string a) {
        LPWSTR tmp_return = new WCHAR[a.length()];
        MultiByteToWideChar(
            CP_ACP,
            MB_ERR_INVALID_CHARS,
            a.c_str(),
            a.size(),
            tmp_return,
            a.length()
        );
        tmp_return[a.length()] = 0;
        return tmp_return;
    }

    // добавим функцию для добавления
    // текста в конец оригинального
    // содержимого файла
    void Append(string text)
    {
        // используем стандартную функцию
        // из класса string
        HText.append(text);
    }

    // добавим функцию для получения
    // конкретной буквы, исходя из индекса 
    // этой буквы в тексте
    string GetOneLetterByIndex(int letter_index)
    {
        string tmp_return = "";
        // for это разновидность цикла, внутри позволяет
        // сразу определить счетчик, условие, и операция для повтора
        for (int i = 0; i < HText.length(); i++)
        {
            // если индекс счетчика равен
            // индексу аргумента возвращаем результат
            // и завершаем цикл
            if (i == letter_index)
            {
                tmp_return = HText[letter_index];
                i = HText.length();
            }
        }
        return tmp_return;
    }

    // добавим функцию для добавления новых строк
    // в HStringsList
    void AddEnd(int count)
    {
        // получаем в переменную текущее количество
        // строк в HStringsList, к которому добавляем
        // count
        int a = Count() + count;

        // имзеняем размер на новый
        HStringsList.resize(a);
    }

    // добавим функцию для получения количества строк в HStringList
    int Count()
    {
        return HStringsList.size();
    }

    // добавим функцию для показа на экране
    // содержимого конкретной строки
    void ShowStringText(int string_index)
    {
        // вызываем диалоговое окно, в котором отображаем
        // содержимое строки из текста
        MessageBox(NULL, ConvertToLPCWSTR(HStringsList[string_index]), TEXT("Показать содержимое из текста"), 0);
    }

    // создадим функцию для разбора
    // текста файла по отдельным строкам
    void DetouchInToStringList()
    {
        //
        //
        // Сначала получаем количество строк из текста
        // (было бы проще сделать для этого отдельную функцию)
        //
        //

        // создаем переменную счетчик строк из текста
        int strings_count = 0;

        // если хранилище текста пустое
        // завершаем функцию
        if (HText.empty() == true)
        {
            // exit аналог прерывания функции
            // (ну точнее return выполняется
            // аналогично exit, но не суть)
            exit;
        }

        // создадим переменные для цикла
        int a = 0;
        int b = HText.length();

        // запускаем цикл (это инвертированный цикл),
        // он создан чтобы действия внутри цикла 
        // были выполнены хотя бы один раз, даже
        // если условие цикла не выполняется
        do {
            
            // если код символа (текущей буквы) соответствует коду
            // ключевого слова, которое означает символ (символ n 
            // через косую черту, я говорил об этом раньше "\n")
            // переноса на новую строку, выполняем код условия
            // (на самом деле это два кода #10+#13, 10 завершение
            // строки, 13 новая строка)
            if (HText[a] == 13)
            {
                strings_count = strings_count + 1;
            }
            a++;
        } while (a < b);

        // по завершении цикла добавляем последнюю строку
        strings_count = strings_count + 1;

        //
        //
        // Теперь когда мы знаем количество строк в оригинальном тексте
        // изменяем размер HStringsList, вырезаем из текста переносы строк
        // и перемещаем этот текст в соответствующие строки массива HStringsList
        //
        //

        // переопределим количество строк в HStringsList
        HStringsList.resize(strings_count);

        // создаем (переопределим, они уже созданы) переменные
        a = 0;
        b = HText.length();

        // первый символ текста теряется (не понятно куда,
        // предположу кривизна компилятора, или программы в 
        // работе с памятью), поэтому добавим его
        string c = GetOneLetterByIndex(0);
        int d = 0;

        // запускаем цикл 
        do {

            // если код символа соответствует коду
            // ключевого слова, которое означает символ (\n)
            // переноса на новую строку, выполняем код условия
            if (HText[a] == 13)
            {
                // помещаем значение в строку в массиве строк
                HStringsList[d] = c;
                c = "";
                d++;
            }
            a++;

            // помещаем символ в переменную, которая
            // собирает строку, но при условии, что код символа
            // не соответствует символу переноса на новую строку
            if (HText[a] != '\n')
            {
                c = c + HText[a];
            }
        } while (a < b);

        // заполняем последнюю строку, так как
        // наша переменная strings_count содержи количество
        // строк, а счет массива HStringsList начинается с 0,
        // чтобы получить индекс последней строки strings_count - 1
        HStringsList[strings_count - 1] = c;
    }

    // создадим функцию для изменения
    // значений в HStringsList
    void ChangeInString(int index, string new_value)
    {
        HStringsList[index] = new_value;
    }

    // создадим функцию для получения содержимого строки
    string Strings(int index)
    {
        return HStringsList[index];
    }

    // и ее альтернативу для LPCWSTR
    LPCWSTR StringsA(int index)
    {
        return ConvertToLPCWSTR(HStringsList[index]);
    }

    // создадим функцию для сохранения файла
    void SaveToFile(LPCWSTR file_path)
    {
        // создаем вспомогательные переменные
        string tmp_buffer = "";
        int a = 0;
        int b = Count() - 1;

        // запускаем цикл для сборки
        // нового содержимого файла
        // из HStringsList
        while (a < b)
        {
            tmp_buffer = tmp_buffer + HStringsList[a] + "\n";
            a++;
        }

        // так как в последней строке указатель новой строки не нужен
        // добавляем последнюю строку без него
        tmp_buffer = tmp_buffer + HStringsList[a];

        // конвертируем новое содержимое в LPCWSTR (других функций под рукой нет)
        LPCWSTR tmp_result = ConvertToLPCWSTR(tmp_buffer);

        // получаем количество байтов для записи
        DWORD file_size = tmp_buffer.length() * sizeof(wchar_t);

        // создаем переменную (счетчик записанных байтов)
        DWORD current_read_position = 0;

        // создаем файловый поток (теперь GENERIC_WRITE в режиме записи)
        HANDLE original_file = CreateFileW(file_path, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

        // записываем в файл все за один раз (так лучше не делать)
        WriteFile(original_file, tmp_result, file_size, &current_read_position, NULL);

        // закрываем файловый поток
        CloseHandle(original_file);

        // вызываем сообщение
        MessageBox(NULL, TEXT("Текст был записан в файл"), TEXT("Показать содержимое из текста"), 0);
    }
};

Ну и немного переделаем код программы (приведем его в порядок), добавим несколько кнопок, добавим работу с клавишами, надписи (можно поиграться со свойствами cfg в нашей кнопке, и уже из одной кнопки у нас получается 3 компонента, кнопка, текст, панель), работу с файлом, ну и так по мелочи.

Пара па па
// WindowsProject1.cpp : Определяет точку входа для приложения.
//

// этот кусок можно пропустить
// директива pragma once подразумевает
// исключение логического бага в программе,
// который может произойти из-за команды для
// подключения библиотек (файлов с заготовками кода)
// include, которая просто вставляет на свое место
// все содержимое файла, который был указан
// (включая и команды include внутри подключаемого
// файла) при вызове, из за чего могу возникать логические
// ошибки и множественные самоподключения (когда
// одна библиотека ссылается на вторую, а вторая на
// первую), в двух словах все исходники собираются
// в одни длинный исходный код (командами include),
// а команда pragma once удаляет все дубликаты,
// оставляя только самую первую include,
// лично я начинаю любой файл с pragma once,
// что бы не беспокоится о зависимостях, но это
// не обязательно (кароче пока не заморачивайтесь)
#pragma once 

// и начать отсюда
// подключаем вспомогательные библиотеки
// наборы готового кода как в этом файле, 
// где уже описанные какие-то функции (заготовки) для
// реализации и запуска нашей программы
#include "framework.h"
#include "WindowsProject1.h"
#include "windows.h"
#include "winuser.h" 
#include <string> 
#include <stdlib.h>  
using namespace std;

#include "Source.cpp"
#include "Source1.cpp"
#include "Source2.cpp"

// определим вспомогательный термин 
// (читай создадим разновидность переменной)
// под названием MAX_LOADSTRING и ассоциируем
// с ней число 999, теперь везде в коде, где 
// будет встречается термин MAX_LOADSTRING
// он будет заменятся на 999
#define MAX_LOADSTRING 999

// Глобальные переменные:
// 
// hInst это глобальная переменная экземпляра нашей программы
HINSTANCE hInst;                                // текущий экземпляр

// я сделаю глобальную переменную для простоты понимания
// с которой ассоциирую уникальный идентификатор нашего окна
// программы
HWND Form1;

// создадим новую переменную типа наша кнопка,
// с которой ассоциируем результат создания 
// нового экземпляра объекта (звездочка (*)
// после ObjectButton обозначает (для компилятора)
// что мы не вызываем одноименную функцию из файла Source1.cpp,
// а обращаемся к типу данных ObjectButton, после чего уже
// вызываем конструктор с функцией ObjectButton (не нужно сейчас над этим
// заворачивается все придет со временем)

// Создадим еще несколько объектов кнопка
ObjectButton* lbl_text_hello = new ObjectButton(400, 50, 150, 200);
ObjectButton* btn_change_windows_size = new ObjectButton(150, 50, 50, 320);
ObjectButton* lbl_text_hide_button = new ObjectButton(550, 50, 210, 380);
ObjectButton* btn_show_message = new ObjectButton(150, 50, 50, 440);
ObjectButton* btn_exit = new ObjectButton(150, 50, 50, 560);

ObjectButton* btn_load_file = new ObjectButton(150, 50, 50, 380);
ObjectButton* btn_save_file = new ObjectButton(150, 50, 50, 440);
ObjectButton* btn_show_file = new ObjectButton(150, 50, 50, 500);
ObjectButton* btn_add_one = new ObjectButton(150, 50, 210, 500);

// теперь создадим новый экземпляр нашего объекта ObjectTextFileLoader
ObjectTextFileLoader* TextLoader1 = new ObjectTextFileLoader((LPWSTR)L"test.txt");

// создадим функцию, которая будет меня положение окна программы,
// и размер окна программы, в качестве параметров в функцию будем
// передавать размер окна программы (двумя параметрами - ширина и высота)
void execute_application_postion_and_size(HWND Form, int width, int height) {

    // системная функция ожидания
    // в качестве параметра принимает
    // период ожидания в мс (1000 
    // соответствует 1 секунде)
    Sleep(1000);

    // создадим переменные, с которыми ассоциируем
    // результат выполнения функции для получения
    // информации о компьютере, с параметров SM_CXSCREEN,
    // требующим ширину экрана в пикселях
    int screen_width = GetSystemMetrics(SM_CXSCREEN);

    // аналогично для высоты экрана
    int screen_height = GetSystemMetrics(SM_CYSCREEN);

    // создадим две переменные, с новыми размерами окна
    int program_width = width;
    int program_height = height;

    int program_left = (screen_width / 2) - (program_width / 2);
    int program_top = (screen_height / 2) - (program_height / 2);

    // изменим положение окна (при помощи готовой функции),
    // разместив окно по центру экрана
    SetWindowPos(
        Form,
        HWND_TOP,
        program_left,
        program_top,
        program_width,
        program_height,
        SWP_ASYNCWINDOWPOS
    );
}
// опишем ее альтернативный вариант без аргументов
void execute_application_postion_and_sizeA() {
    execute_application_postion_and_size(Form1, 1024, 700);
}

// опишем вспомогательную функцию для начальной отрисовки
void ExecuteMyBotton(ObjectButton* btn, HDC Canvas) {
    if (btn->help_button_first_draw_status == 0) {
        btn->Left = btn->Left - 8;
        btn->Top = btn->Top - 51;
        btn->Create(Form1, Canvas);
        btn->Left = btn->Left + 8;
        btn->Top = btn->Top + 51;
    }
}

// опишем функцию выхода из программы              
void ExitFromProgram() {

    // вызовем функцию для завершения программы,
    // все программы во всех системах (на всех языках)
    // завершаются кодом 1, в случае завершения без
    // сбоев, и 0 (со сбоями)
    PostQuitMessage(1);
}

// опишем функцию для загрузки файла test.txt из
// папки с программой (если файл находится в одной
// директории с программой не надо указывает полный путь)
void LoadFileFromComputer()
{ 

    // создадим переменную пути
    // я использую путь к моему файлу на диске
    // (прямой), потому что для указания релитивного
    // пути нужyа библиотека, которой нет в стандартном
    // проекте программы (который создан в стандарте
    // с++ 14), в то же время функции из библиотеки 
    // shellapi не представляется возможным использовать 
    // так как они не имеют полной обратной совместимости
    // с windows 10 и ограничения на размер пути, 
    // такой вот порочный круг, а еще мне лень, поэтому так,
    // насколько я помню вызов стандартного диалога выбора
    // файла решает эту проблему (но эт не точно, так как я
    // использую другой язык (в повседневности), а с++ (нативным)
    // давно не работаю, двойной \\ слеш в пути потому 
    // что символ \ зарезервирован редактором для ключевых обозначений
    string file_path = "D:\\a\\test.txt";
     
    // загрузим файл   
    TextLoader1->LoadFromFile((LPWSTR)TextLoader1->ConvertToLPCWSTR(file_path));

    // разберем на строки
    TextLoader1->DetouchInToStringList();
}

// опшием функцию для сохранения файла test.txt
void SaveToFileInComputer()
{
    // создадим переменную с путем к файлу
    LPWSTR fp = (LPWSTR)L"D:\\a\\test.txt";

    // сохраним файл
    TextLoader1->SaveToFile(fp);
}

// создадим функцию для отображения содержимого 
void ShowContent()
{
    // соберем текст в переменную, 
    // выполним текстовую конкатенацию 
    // (от слова контакт),
    // функция std::to_string позволяет 
    // трансформировать число в строку
    string a = "Содержимое строки #" + std::to_string(TextLoader1->Count()) + " - " + TextLoader1->Strings(TextLoader1->Count() - 1);

    // вызовем диалог с сообщением
    MessageBox(NULL, TextLoader1->ConvertToLPCWSTR(a), TEXT("Показать содержимое из текста"), 0);
}

// создадим функцию для добавления строки в конец файла
void AddOneString()
{
    // добавим 1 строку в конец HStringList
    TextLoader1->AddEnd(1);

    // создадим переменную для текста строки
    string a = "New String #" + std::to_string(TextLoader1->Count());

    // изменим содержимое последний строки на содержимое переменной а
    TextLoader1->ChangeInString(TextLoader1->Count() - 1, a);

    // вызовем диалог с сообщением
    MessageBox(NULL, TEXT("Добавлена 1 строка в конец файла!"), TEXT("Показать содержимое из текста"), 0);
}

// в общем то создаем пару переменных типа "буквенный массив",
// и сразу задаем его длину в параметре [MAX_LOADSTRING]
WCHAR szTitle[MAX_LOADSTRING];                  // Текст строки заголовка
WCHAR szWindowClass[MAX_LOADSTRING];            // имя класса главного окна

// Отправить объявления функций, включенных в этот модуль кода:
// 
// эти вспомогательные функции сейчас не важны, по сути, они просто "нормализуют"
// нашу программу для системы, создают новый экземпляр окна (из заготовок в системе) 
// и добавляют мониторинг действий (нет смысла вдаваться в подробности, и забивать голову)
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

// создаем функцию
// эта функция принимает в себя несколько параметров,
// это "точка входа", когда программа запускается, она 
// автоматически начинает выполнять эту функцию
//
// эта функция будет обеспечивать связь системы и нашей программы
// при помощи бесконечного цикла, который повторяется до тех пор, 
// пока программа запущена, и перехватывает сообщения от системы
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_WINDOWSPROJECT1, szWindowClass, MAX_LOADSTRING);

    // адаптируем нашу программу для системы при помощи функции
    // MyRegisterClass, в качестве параметра передаем идентификатор
    // нашего окна программы, который получаем из текущей функции 
    // (wWinMain) в параметре hInstance
    MyRegisterClass(hInstance);

    // Выполнить инициализацию приложения:

    // создаем условие, если функция вернула на место своего вызова
    // значение false, вызываем встроенное в идею функций ключевое слово
    // return, при встрече которого любая функция прекращает построчное выполнение
    // кода, и возвращаем ложь в функции wWinMain (в место вызова функции)
    if (!InitInstance(hInstance, nCmdShow))
    {
        return FALSE;
    }


    // это вспомогательный код для отлова событий нажатия клавиш,
    // ассоциируем с переменной hAccelTable (типа HACCEL) результат
    // выполнения функции LoadAccelerators, сама функция извлекает из
    // ресурсов нашего файла готовые схемы сочетания клавиш и названия функций
    // для этих сочетаний
    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT1));

    // создадим переменную типа MSG (сложносоставной тип данных) под названием MSG
    // в эту переменную будут попадать команды, которые в цикле будут перехватываться 
    // универсальной функцией GetMessage
    MSG msg;

    // Цикл основного сообщения:

    // создаем цикл, до тех пор пока GetMessage, с пустыми параметрами 
    // (2,3,4, не нужно вдавятся сейчас в подробности почему)
    // возвращает какой-либо результат (в переменную msg) 
    // выполняем код в теле цикла
    //
    // важный момент, почему &msg (ассоциация значений), а не
    // просто msg (присвоение значений)? потому что если создать 
    // "условный" объект кнопка, задать ему какие-либо параметры, 
    // после чего создать еще один, и попробовать скопировать параметры 
    // простым присвоением, присвоены будут не параметры кнопки, 
    // а сама кнопка (объект), следовательно просто кнопка один 
    // будет также доступна под названием кнопка два
    while (GetMessage(&msg, nullptr, 0, 0))
        // это начало тела цикла
    {
        // нестандартная конструкция условия, она подразумевает
        // простую логику, если не "!" результат выполнения функции
        // TranslateAccelerator (функция для горячих клавиш)
        // то выполняем код в условии, иначе говоря, если 
        // нажатая клавиша не выполнила функцию, которая ей
        // назначена, то выполняем код условия, а именно обрабатываем 
        // горячую клавишу (не обязательно вдавятся в подробности 
        // нам сейчас это не нужно)
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            // код обработки нажатий горячих клавиш
            // которые были указаны в схеме, которая
            // была загружена из ресурсов ранее
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    // это конец тела цикла

    // возвращаем результат выполнения функции
    // в качестве подстановочного значение 
    // (которое встает в никуда) возвращаем параметры 
    // последней команды GetMessage
    return (int)msg.wParam;
}



//
//  ФУНКЦИЯ: MyRegisterClass()
//
//  ЦЕЛЬ: Регистрирует класс окна.
//

// выше уже была упомянута эта функция, она адаптирует
// в рамках системы нашу программу, вот, собственно, само 
// содержимое это функции, оно нам не интересно, двигаемся дальше
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(IDI_WINDOWSPROJECT1));
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1);
    wcex.lpszClassName = szWindowClass;
    wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

//
//   ФУНКЦИЯ: InitInstance(HINSTANCE, int)
//
//   ЦЕЛЬ: Сохраняет маркер экземпляра и создает главное окно
//
//   КОММЕНТАРИИ:
//
//        В этой функции маркер экземпляра сохраняется в глобальной переменной, а также
//        создается и выводится главное окно программы.
//

// я думаю из сгенерированных комментариев понятно, для чего эта функция
// так же выше было сказано об этой функции, ее тоже можно пропустить
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    hInst = hInstance; // Сохранить маркер экземпляра в глобальной переменной

    // разве что вот ключевая функция для создания окна, которая называется
    // CreateWindow (с модификатором W, то есть одна из вариаций этой функции)
    // и возвращает идентификатор на место своего вызова, который мы ассоциируем
    // с глобальной переменной, в которой держим уникальный идентификатор нашего окна
    // программы
    HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

    // добавим условие, если "не hWnd" (то есть
    // нет ассоциированного объекта) возвращаем FALSE
    // и завершаем выполнение функции
    if (!hWnd)
    {
        return FALSE;
    }

    // после создания окна программы вызываем функции для отображения
    // окна программы и его перерисовки
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

//
//  ФУНКЦИЯ: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  ЦЕЛЬ: Обрабатывает сообщения в главном окне.
//
//  WM_COMMAND  - обработать меню приложения
//  WM_PAINT    - Отрисовка главного окна
//  WM_DESTROY  - отправить сообщение о выходе и вернуться
//
//
// еще одна сгенерированная функция, эта функция подвязывается
// к нашей форме, и обрабатывает события нашей формы (к примеру
// изменение размера окна, тык мышкой, активность формы и тд)
// это основная функция нашей программы
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    // добавим ассоциацию идентификатора окна программы
    // с нашей глобальной переменной, в которой храним
    // указатель на идентификатор
    Form1 = hWnd;

    // switch разновидность условия, не совсем понятно
    // целесообразность использования альтернативного
    // условного конструктора, поскольку то же самое 
    // можно было описать и конструкцией if (но предположу
    // что несмотря на то что switch не является "канонистической 
    // конструкцией, она обеспечивает одно единственное выполнение
    // условия, затем завершает, это может обеспечивает стабильность,
    // избегая множественных срабатываний на одну команду message)

    // в данном случае условие обрабатывает сообщения,
    // которые получает из GetMessage, в соответствии
    // с определенным содержимым message (который передается
    // в параметрах функции), и если значение message соответствует
    // какой либо конструкции case, выполняет этот фрагмент кода
    switch (message)
    {
        // не стоит забывать про зажатую Ctrl и левый тык мышкой
        // это перекинет в файл, где описано это ключевое слово
        //
        // WM_COMMAND это универсальное событие обработки команд
        // (к примеру отправленных через SendMessage)	
        case WM_COMMAND:
        {
            // тут обработка элементов меню
            // сами элементы в ресурсах файла, 
            //сгенерированы автоматически
            int wmId = LOWORD(wParam);
            // Разобрать выбор в меню:
            switch (wmId)
            {
            case IDM_ABOUT:
                DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
                break;
            case IDM_EXIT:
                DestroyWindow(hWnd);
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
        }
        break;
        //
        // 
        // дополним наш код новым условием обработки
        // если message соответствует значению WM_LBUTTONDOWN
        // выполняем код в условии (WM_LBUTTONDOWN левый тык мышки)
        //
        //
        // это событие создания окна программы
        // в нем ассоциируем со свойством нашего объекта кнопка (под
        // названием ButtonClick, ссылку на функцию, которую мы недавно 
        // описали, а также настраиваем другие нужные параметры)
        case WM_CREATE: {
            execute_application_postion_and_size(Form1, 900, 700);

            btn_change_windows_size->ButtonClick = &execute_application_postion_and_sizeA;
            btn_change_windows_size->Caption = TEXT("Изменить размер окна");

            lbl_text_hello->Caption = TEXT("Я у мамки программииииист!");
            lbl_text_hello->cfg_enable_border = 0;
            lbl_text_hello->cfg_text_size = 48;
            lbl_text_hello->TextColor = RGB(126, 134, 231);


            lbl_text_hide_button->Caption = TEXT("<- Нажми Esc чтобы скрыть эту кнопку и Q чтобы снова показать!");
            lbl_text_hide_button->cfg_enable_border = 0;
            lbl_text_hide_button->cfg_text_size = 24;
            lbl_text_hide_button->TextColor = RGB(126, 190, 231);

            btn_exit->Caption = TEXT("Выход");
            btn_exit->TextColor = RGB(255, 52, 9);
            btn_exit->BorderColor = RGB(255, 52, 9);
            btn_exit->ButtonClick = &ExitFromProgram;

            btn_load_file->Caption = TEXT("Открыть файл");
            btn_load_file->ButtonClick = &LoadFileFromComputer;

            btn_show_file->Caption = TEXT("Последняя строка");
            btn_show_file->ButtonClick = &ShowContent;

            btn_save_file->Caption = TEXT("Сохранить");
            btn_save_file->ButtonClick = &SaveToFileInComputer;

            btn_add_one->Caption = TEXT("Добавить строку");
            btn_add_one->ButtonClick = &AddOneString;
        }
        break;

        // добавим событие нажатия кнопки клавиатуры
        // (есть несколько состояний, нажимается, зажата, отпускается),
        // внутри события добавим код, который будет проверять полученное
        // (в wParam) значение, во время вызова события WM_KEYDOWN
        case WM_KEYDOWN: {

            // создаем условие, если нажата клавиша Esc,
            // удаляем с экрана кнопку
            if (wParam == VK_ESCAPE)
            {
                btn_load_file->ClearButtonDraw(Form1);
            }

            // добавим еще одно
            // 
            // если функция (из библиотек), дял получения
            // текущей нажатой клавиши с заданным кодом клавиши Q (0x51)
            // вернула код, соответствующий состоянию нажатия и удерживания,
            // снова отрисовываем кнопку (при помощи изменения вспомогательного
            // свойства help_button_active_status, которое намеренно устанавливаем
            // в 1, чтобы функция (ActiveButtonAnimate) подумала, что сейчас кнопка 
            // отрисаована в активном состоянии), получив область рисования при помощи
            // другой функции из библиотек (GetWindowDC), конструкция условия
            // использует & потому что мы ассоциируем битовые данные (коды состояния
            // клавиш), а компилятор не способен это трансформировать в логическое условие
            // соответствия (==), функция GetAsyncKeyState возвращает на место своего вызова
            // код, дополняющий 0x8000 таким образом чтобы вместе они сделали 0x0001, или
            // битовое выполнение конструкции условия, через параметр истины (отправку кода
            // "истины" в условие), соответствующий истине в архитектуре конструкции if (не надо над 
            // этим заморачиваться и забивать себе голову)
            if (0x8000 & GetAsyncKeyState(0x51)) {
                HDC hdc = GetWindowDC(Form1);
                btn_load_file->help_button_active_status = 1;
                btn_load_file->ActiveButtonAnimate(Form1, hdc);
            }

        }
        break;

        // добавим событие срабатывающие при перемещении
        // мыши, в него добавим функцию, которая вызывает
        // проверку и анимацию кнопки, так как наша функция требует
        // в себя область рисования, получаем идентификатор
        // области рисования в переменную соответствующего типа
        // по результату вызова функции GetWindowDC (в которую
        // передаем идентификатор окна программы)
        case WM_MOUSEMOVE: {
            HDC hdc = GetWindowDC(Form1);
            btn_change_windows_size->ActiveButtonAnimate(Form1, hdc);
            btn_exit->ActiveButtonAnimate(Form1, hdc);

            btn_load_file->ActiveButtonAnimate(Form1, hdc);
            btn_show_file->ActiveButtonAnimate(Form1, hdc);
            btn_save_file->ActiveButtonAnimate(Form1, hdc);
            btn_add_one->ActiveButtonAnimate(Form1, hdc);
        }
        break;

        // добавим событие, выполняющееся при нажатии левой кнопки мыши
        // внутри события добавим срабатывание функции 
        // CheckMouseCoordinatesBeforeStartClickFunction
        case WM_LBUTTONDOWN: 
        {
            btn_change_windows_size->CheckMouseCoordinatesBeforeStartClickFunction(Form1);
            btn_exit->CheckMouseCoordinatesBeforeStartClickFunction(Form1);
             
            btn_load_file->CheckMouseCoordinatesBeforeStartClickFunction(Form1);
            btn_show_file->CheckMouseCoordinatesBeforeStartClickFunction(Form1);
            btn_save_file->CheckMouseCoordinatesBeforeStartClickFunction(Form1);
            btn_add_one->CheckMouseCoordinatesBeforeStartClickFunction(Form1);
        }
        break;

        // если условие соответствует событию отрисовки выполняем код внутри
        case WM_PAINT:
        {
            // создаем переменную типа параметры
            // области рисования
            PAINTSTRUCT ps;

            // создаем переменную типа область рисования
            // и ассоциируем с ней результат выполнения функции
            // BeginPaint (в параметрах которой передаем
            // идентификатор нашего окна, и ассоциируем данные
            // из переменной с параметрами
            HDC hdc = BeginPaint(hWnd, &ps);

            // добавим условие, если значение свойства
            // help_button_first_draw_status равно 0,
            // значит функция Create еще никогда не выполнялась,
            // после создания нового экземпляра объекта, выполним
            // ее, при этом вычитая размеры бордюра и меню, так как
            // событие отрисовки формы вызывается много раз,
            // в том числе до того, как буду объявлены объекты меню и
            // бардюра окна программы
            if (lbl_text_hello->help_button_first_draw_status == 0) {
                lbl_text_hello->Left = lbl_text_hello->Left - 8;
                lbl_text_hello->Top = lbl_text_hello->Top - 51;
                lbl_text_hello->Create(Form1, hdc);
                lbl_text_hello->Left = lbl_text_hello->Left + 8;
                lbl_text_hello->Top = lbl_text_hello->Top + 51;
            }

            // чтобы не дублировать код исключения (описан сверху),
            // и не вмешивается в целостность нашего класса кнопки
            // имеет смысл описать функцию, которая будет в себя
            // принимать переменную с объектом кнопка, и в функции
            // делать тоже самое, а в месте, где нужен этот код
            // вставим эту функцию
            ExecuteMyBotton(btn_change_windows_size, hdc);
            ExecuteMyBotton(lbl_text_hello, hdc);
            ExecuteMyBotton(lbl_text_hide_button, hdc);
            ExecuteMyBotton(btn_exit, hdc);

            ExecuteMyBotton(btn_load_file, hdc);
            ExecuteMyBotton(btn_show_file, hdc);
            ExecuteMyBotton(btn_save_file, hdc);
            ExecuteMyBotton(btn_add_one, hdc);


            // TODO: Добавьте сюда любой код прорисовки, использующий HDC...
            // завершаем процесс рисования,
            // вызывая специальную функцию
            EndPaint(hWnd, &ps);
        }
        break;

        // если переменная message 
        // содержит WM_DESTROY
        // выполняем код для завершения
        // работы приложения
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
            // если message не содержит никаких сообщений
            // (или сообщения не соответствуют ни одному из условий)
            // выполняем другой код, функцию, которая гарантирует обработку
            // "неопределенного" message, она нас тоже пока не интересует
            // (но может понадобится, так как мы можем ее "переописать",
            // для каких-либо наших задач)
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }


        // вернем результат выполнения функции 0
        // можно изменить на 1 чтобы мониторить 
        // работоспособность программы, исходя
        // из возвращаемого значения этой функции
        // (то есть если функция WndProc выполнилась
        // и не вернула на место вызова 1, значит
        // какое то решение не работает, для этого
        // просто можно создать переменную (в начале
        // блока кода, с которой ассоциировать что
        // нибудь (к примеру 0), и если функция смогла
        // выполнить свой код до этой позиции, возвращяем 1
    return 0;
}

// Обработчик сообщений для окна "О программе".
// аналогично только не для основного окна программы,
// а для диалога "Об программе", его разбирать смысла нет
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, LOWORD(wParam));
            return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}

В итоге должно получится это:

Поздравляю, теперь ты полноценный Junior C++ Desktop Developer, если сверху изучить работу с памятью и работу с базами данных, будешь уже Middle C++ Engine Developer, ну а если овладевать всеми тонкостями нет смысла LOL, поэтому добавить сверху только знания библиотек и с++ последней версии, и получиться «ценный специалист».

На это пожалуй все.

Что можно сказать по итогу: Теперь ты знаешь как создавать графические приложения на С++, знаешь базовые понятия классов (и самое главное умеешь их использовать), знаком с азами работы в RunTime, и умеешь работать с файлами.

Кароч оставляй комментарий, это мотивирует (я потратил 4 вечера на эту статью, 3 из которых на комментарии и исправление орфографии) меня написать еще что ни будь интересное, а также пиши если что‑то было непонятно, по возможности постараюсь ответить (но у меня ограничение из‑за рейтинга на 1 коммент в день).

Спасибо за внимание!

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


  1. SerJook
    07.12.2023 10:55
    +4

    А кому сейчас нужны Desktop developer-ы, тем более junior? И вроде на голом Winapi сейчас никто не пишет, берут Qt или не дай бог какой-нибудь MFC.


    1. iminfinitylol Автор
      07.12.2023 10:55
      -2

      И вроде на голом Winapi 

      ну если вроде то такие я думаю никому не нужны (Т.Т)


      1. alexac
        07.12.2023 10:55
        +12

        На голом WinAPI тоже пишут, но весьма не много, и как правило когда редактируют куски какого-либо фреймворка, который на нижнем уровне все равно будет иметь доступ к WinAPI.

        Другой вопрос, что это примерно худший гайд по основам языка и созданию UI приложений, который я видел. Автор, пожалуйста, выучи сначала что-то сам, прежде чем пытаться учить этому других.

        Ты прав в том, что в сущности программирование — композиция абстракций от элементарного уровня до очень сложного. Однако это не все, что нужно понимать. Еще очень важно понимать и знать общепринятые паттерны, алгоритмы и структуры данных, которые повсеместно используются для этой композиции. Инженер при работе не думает о том, как решать простые задачи, он знает типовые решения и применяет их. И другой инженер, читая код спустя пол года, с пары взглядов узнает паттерны и понимает, что делает этот код. Не менее важно знать инструменты, которые ты используешь. C++ не тот язык, который можно освоить прочитав одну статью за пол часа. Уходят месяцы и годы, чтобы понять большинство его аспектов.

        UI-приложения — один из худших примеров для изучения. Да, они отлично награждают визуальной демонстрацией прогресса обучения. Однако это очень высокий уровень абстракции для тех, кто еще не освоил азов. Поэтому все нормальные учебники начинаются с консольных приложений. Гораздо проще и понятнее осваивать основы языка, не тратя силы и время на сложные вещи, для которых все равно еще рано.

        Некоторые из ошибок которые бросились в глаза:

        1. #pragma once - стоит выучить, что это и зачем нужно. И почему это не нужно добавлять в .cpp файлы (хорошие компиляторы с правильными настройками напишут предупреждение про это).

        2. using namespace std - плохая привычка. И нет, это совсем не другой способ импорта.

        3. #define - очень плохой с точки зрения C++ способ объявлять константы.

        4. Глобальные переменные — антипаттерн. Невозможно нормально контроллировать, какая часть программы и когда их поменяет, что приводит к нежелательным эффектам тогда, когда ты этого не ожидаешь. Также логику, использующую глобальные переменные бывает очень тяжело и неприятно тестировать. Крайне редко, без них нельзя обойтись, в норме же от них надо избавляться.

        5. Есть веские причины, почему практически всегда свойства класса помещают в private: секцию, или по крайней мере в protected:. Если этого не нужно, возможно не нужен сам класс.

        6. Любому new должен соответствовать delete, а в современном C++ ни того, ни другого присутствовать не должно. Есть умные указатели, которые помогают управлять памятью.

        7. Использовать #include на .cpp файлах — в корне не правильно и приводит к ошибкам линковки.


        1. yatanai
          07.12.2023 10:55
          +3

          Я согласен что наш автор картошка, но вот эти все аргументы ну как бы...

          1. #pragma once вообще не стоит использовать, если подразумевается использовать другой компилятор. Так как это всё ещё "фича", а многие gcc ориентированные компиляторы будут ругать на него, если не "чекнуть флаги"

          2. Использовать using namespace можно, но только внутри каких-то функций, .срр файлов или при специфичной организации кода.

          3. Согласен с #define, но никто не объясняет почему...

          4. Глобальные переменные это нормальная организация, но которая влечёт за собой тьму проблем из-за невнимательности при реализации. Обычно этот подход обрамляется специфичным поведением которое и позволяет контролировать доступ к переменной. Если бы это было антипаттерном, никто бы не пользовался фабриками и всё передавали бы по ссылке/указателю, как это любят делать в С.

          5. Нету никаких причин обязывать людей писать class с приватными полями. Тоесть ты подразумеваешь, что если объект состоит только из публичных полей то это struct иначе class? Напомню просто, что структуры, изначально, просто способ запихнуть несколько типов в один, а вот классы как раз и интегрировали в себя методы и права доступа. В целом это вкусовщина. Вон UnrealEngine весь на структурах написан, чот я холиваров не слышал по этому поводу.

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

          7. Чисто технически #include просто копирует файл в текущий, так что как бы можно... Вопрос нахера?


          1. alexac
            07.12.2023 10:55
            +1

            1. по факту подавляющее большинство современных компиляторов поддерживают #pragma once без проблем. Но например у gcc и clang есть предупреждение про использование в .cpp файле, которое, к тому же включено по умолчанию:

            alex-ac@alex-ac-mbp /tmp $ echo '#pragma once' >test.cc
            alex-ac@alex-ac-mbp /tmp $ clang++ -c test.cc
            test.cc:1:9: warning: #pragma once in main file [-Wpragma-once-outside-header]
            #pragma once
                    ^
            1 warning generated.
            
            1. да, согласен, но не бездумно везде. На самом деле эта рекомендация может уйти прошлое с модулями. Главное чтобы никто не начал писать export using namespace std;. И вообще, какой-нибудь using namespace std::placeholders; внутри функции бывает очень полезен.

            2. Элементарно же. #define - просто подставляет текст. нет информации о типах, есть вероятность, что будет парситься не так, как этого ожидаешь если рядом какие-то сложные выражения. Объявление константы добавляет сущность, которая имеет явный тип, на которую распространяется ODR, на которую зарезервировано место в бинарнике, etc. Гораздо меньше вариантов выстрелить себе в ногу. Есть два повода писать #define в современном C++ - если объявленное значение нужно использовать в #if, или если объявляется макрос с параметрами, который позволяет значительно сократить написание повторяющегося кода. Причем, последнее спорно. В целом, чем дальше развивается язык, тем меньше становится потребность в препроцессоре.

            3. Да, глобальные переменные ок для некоторых задач. Но для общего назначения это антипаттерн.

            4. Все ради инкапсуляции. Все, что находится в приватной секции не является интерфейсом класса (за исключением того, как это влияет на размер объекта и vtable). Декларируя в публичной секции поля мы декларируем эти поля частью интерфейса класса и лишаем себя возможности инкапсулировать логику связанную с изменением значений этих полей. Когда мы прячем поля в приватной секции и добавляем к публичному интерфейсу геттер/сеттер, мы оставляем возможность инкапсулировать любую логику в сеттер или делегировать хранение значение в какой-то другой объект/объекты. В Visual C++, objective-c, swift, C# и JS есть property, которые позволяют сделать property частью публичного интерфейса, при этом оставляя возможность инкапсулировать логику по изменению их значений (и по факту являются синтаксическим сахаром для гет/сеттеров. Но в стандартном C++ этого нет, поэтому оправдано убирать все в приватную секцию всегда. И да, если нет необходимости в инкапсуляции данных, возможно действительно нужны структуры, которые сейчас, по факту, — те же классы, имеющие только публичную секцию.

            5. Ну вот да, в современном C++ видеть new/delete без реальной на то необходимости — плохо. Аллокация на стеке — гораздо лучше. Если надо в куче — make_unique/make_shared, пожалуйста.

            6. Нет. Во-первых, есть компиляторы, которые на такое ругаются при достаточно параноидальных настройках. Во-вторых, многие билд-системы будут делать предположение, что .cpp файл — это отдельная единица трансляции, которая должна быть скомпилирована в объектник и добавлена на этапе линковки. Включение целиком одного .cpp файла в другой приведет к тому, что все символы в этом файле будут объявлены в двух единицах трансляции. И если для какой-то части из них это не имеет значения благодаря ODR, то в большинстве случаев это приведет к ошибкам линковки этих символов. По крайней мере к предупреждениям о том, что это случилось и линковщику пришлось выбирать между ними.

            Я намеренно не хотел расписывать подробно по каждому из пунктов, почему это так. У автора явно все слишком плохо с знанием языка, чтобы понимать причины и концепции на которых основаны эти объяснения, а те, кто знают язык и без объяснений про это знают. Моей целью было указать на ошибки, чтобы автор сам изучил эти вопросы и усвоил это лучше, чем если бы ему сказали ответ, который он все равно до конца не понимает.


    1. SpiderEkb
      07.12.2023 10:55
      +10

      И вроде на голом Winapi сейчас никто не пишет, берут Qt или не дай бог какой-нибудь MFC.

      Писать вы можете на чем угодно, но знать API платформы (если вы, конечно, не тот самый "мамкин программист") все-таки нужно.

      Иначе можно немножко оконфузится - один такой "товарисч" после пары дней боданий с фреймворком мне с пеной у рта доказывал что "вот так сделать невозможно потому что этого в фреймворке не предусмотрено" (конкретики уже не помню, что там было, какой фреймворк был - скорее все это бы VCL и Borland C++ Builder). На что я ему "это" написал на WinAPI минут за 15.

      Вообще, в WinAPI есть много такого, что вроде бы и не каждый день нужно (и в фреймворки это никто не затащит), но иногда бывает полезным. Так что лазать в MSDN очень рекомендуется.

      Что, впрочем, не отменяет поверхностность данной статьи. Начать с того, что нет внятного объяснения как устроено GUI приложение Windows (очередь сообщений, чтение сообщений, диспетчер сообщений, оконные функции обработки сообщений и вообще что все построено на сообщениях) - все это можно обрисовать и без кода. А потом уже, когда понятна общая структура, тогда уже код.


    1. SIISII
      07.12.2023 10:55

      Ну, для общего развития и для понимания того, как оно работает, неплохо уметь немного работать на голом API, без использования сторонних библиотек и т.п. Но вряд ли это оправдано для реальной разработки, конечно.


      1. SpiderEkb
        07.12.2023 10:55

        Чтобы понять как оно работает, такую учебную программу лучше всего написать на pure C и голом WinAPI. И еще с разными типами контролов побаловаться. А потом уже все остальное.


    1. reishi
      07.12.2023 10:55
      +2

      Что не так с Desktop developer-и?


  1. Oceanshiver
    07.12.2023 10:55
    +33

    Спасибо за статью! Я даже не успел дочитать ее, а мне уже позвонил HR и принял в компанию на зп 600к в месяц!


    1. shasoftX
      07.12.2023 10:55
      +16

      Вы зря не дочитали статью до конца прежде чем согласится, потеряли 400 тысяч /месяц


      1. Oceanshiver
        07.12.2023 10:55

        Я не жадный и ленивый, меня все устроило


  1. iig
    07.12.2023 10:55
    +10

    я потратил 4 вечера на эту статью

    Напрасно.


  1. johnkimoo
    07.12.2023 10:55
    +11

    Весьма пренебрежительно написано, конечно, и по отношению к аудитории, и в целом к теме статьи. Вряд ли зайдёт новичкам-непрограммистам.

    Даже типично-советский трудовик в моей школе был более серьёзен. Мог, конечно, и подзатыльник дать, и молотком замахнуться -- но по делу, за несоблюдение ТБ, например. Но никогда не объяснял тему в стиле "берём вот эту хреновину аххахаха..."


    1. SIISII
      07.12.2023 10:55
      +3

      У нас трудовик, в частности, терпеть не мог, когда отверстия называли дырками :) Ну, как-то я так назвал, он типа наехал, а я ему говорю: мол, сами гляньте: края рваные, форма не пойми какая... (отверстие я действительно жутко кривое тогда сделал). Он подошёл, посмотрел и говорит: "Да, ты, прав, это не отверстие, это дырка" :)

      Ну а насчёт оценки стиля статьи полностью согласен. Писать надо серьёзно, а не just for lulz (серьёзно -- не значит занудно).


    1. PuerteMuerte
      07.12.2023 10:55
      +1

      Весьма пренебрежительно написано, конечно, и по отношению к аудитории, и в целом к теме статьи. Вряд ли зайдёт новичкам-непрограммистам.

      Не знаю, мне бы в середине 90-х такое понравилось. И по стилю подачи, и по обсуждаемой теме.


  1. dyadyaSerezha
    07.12.2023 10:55
    +6

    Увы, как активно ищущий сейчас работу, могу сказать, что 99+% предложений работы связаны с микросервисами на облаках, большими модными базами данных и прочими Докерами. Никаких нативных/десктопных программ и на фиг никому не нужно, увы (сам большой спец по десктопу).


    1. simenoff
      07.12.2023 10:55

      Да, десктопная разработка в упадке

      На чём нынче без гемора можно сваять GUI-приложение? C# и всё?


      1. Arkasha
        07.12.2023 10:55
        +2

        Без гемора на qt


        1. SpiderEkb
          07.12.2023 10:55

          Можно еще посмотреть в сторону wxWidgets.


      1. nev3rfail
        07.12.2023 10:55

        Компоуз на десктопе ощущается очень приятно, ещё и кроссплатформа во все поля. Но жор и жирный рантайм (жаба) делают его специфическим выбором для целей коммерческой разработки.

        Если его когда-нибудь отвяжут от жвм, то это будет самая настоящая серебряная пуля, как мне кажется: он не отягощен лицензией, не прибит гвоздями к винде, не привет из девяностых. Использую как сейчас основное решение когда надо быстро накидать к чему-нибудь гуй.


    1. firehacker
      07.12.2023 10:55

      Присоединяюсь к вашей скорби и негодованию, шлю лучи добра.


  1. Arkasha
    07.12.2023 10:55
    +6

    И почему статьи такого рода всегда пишут люди, у которых в школе был кол по русскому?


  1. GBR-613
    07.12.2023 10:55
    +3

    Ну вот зачем так? Человек, который приучается использовать вещи, плохо понимая и не пытаясь вникнуть в то, что они из себя представляют (как #pragma и #define в Вашем случае), это не программист, а бомба замедленного действия: он чего-то сляпает, посмотрит - вроде работает, а потом у людей недели уйдут, чтобы понять, в чем проблема.

    У меня был такой случай. Была аппликация на Java SWT. Это на 99% процентов дейстаительно платформно-независимое, но для какой то штуки понадобилось получить handler окна и чего-то с ним сделать. И вот настало время поддерживать 64 бита наряду с 32. Так девочка, которая этим занималась, просто тупо сделала casting, что-то типа:

    HWND h = (int32)this.GetWindowHandle();

    Это, как понимаете, чтобы гарантированно работало в обоих случаях.

    К счастью, процесс просто убивался во время работы, и сразу было видно где.


    1. randomsimplenumber
      07.12.2023 10:55

      тупо сделала casting,

      Но зачем? HWND он же и на 64 битах HWND?


      1. mayorovp
        07.12.2023 10:55

        Потому что в пересказе ошибка. Нет в Java никакого HWND.

        Есть только handle, который int на одних платформах и long на других.


        1. GBR-613
          07.12.2023 10:55

          Да, так оно и есть. Это было давно, а я уже несколько лет Java не занимался.
          Исправлю.


        1. GBR-613
          07.12.2023 10:55
          +1

          @randomsimplenumber Опции исправить я не вижу, значит придется писать отдельно.
          Изначально было примерно так:
          int h = this.GetWindowHandle();
          Потом выяснилось, что с 64 бит - ошибка присвоения типа.
          И девочка ее "исправила":
          int h = (int)this.GetWindowHandle();


  1. inscriptios
    07.12.2023 10:55
    +11

    РЕМАРКА! Эта статья не будет интересна тем, кто уже знает программирование, поэтому попрошу избавить меня от вашего «экспертного мнения» программиста.

    Эта статья не будет интересна никому, поэтому попрошу избавить всех от вашей «экспертной статьи».


    1. Radish
      07.12.2023 10:55

      Очень плохая привычка говорить за всех.


  1. YourDesire
    07.12.2023 10:55
    +3

    Это даже для 1 апреля, как-то слишком.


  1. NutsUnderline
    07.12.2023 10:55
    -1

    А подскажите ньюбу! Я скачал Visual Studio, какой то там новой версии, и она сказала мне зарегистрироваться, иначе работать не будет. Ну зарегистрировался, паспорт не спросили, только почту.. работает. Даже без интернета.. а потом перестала - запросила интернет. Спустя какое то время. И, типа, проверила логин, и чё то ему не понравилось: типа страна .. не работаем тут.. короче лесом послала. Ичемнетеперь ? как ее использовать?


    1. Arenoros
      07.12.2023 10:55
      +1

      Это с каких пор VS не работает в "какой то" стране? Пользуюсь ежедневно и чёт проблем не наблюдаю.


      1. NutsUnderline
        07.12.2023 10:55

        Ну вот я не знаю, у меня залокировалось на учетке.. где то год назад еще.. типа free edition была с сайта микрософта в общем то. поэтому и спрашиваю


        1. ZirakZigil
          07.12.2023 10:55
          +1

          Самое близкое к тому, что может соответствовать вашим словам: вы скачали демо платной версии, заблокировалась она по истечению демо-периода, а "неправильная страна" написали уже в окне покупки лицензии. Пользуйтесь community edition и будет вам счастье.


          1. NutsUnderline
            07.12.2023 10:55

            спасибо, но нет. vs2015.3.com_enu.iso - community edition  2015 года и учетка гвоздями прибита была


    1. simenoff
      07.12.2023 10:55
      +2

      Вы что-то не то скачали https://visualstudio.microsoft.com/ru/free-developer-offers/


      1. NutsUnderline
        07.12.2023 10:55

        Спасибо, я решил вспомнить че у меня случилось и есть два момента

        1) качал то я с comuniti edition но мне нужен был offline инсталятор, в результате я поставил только VS2015 comuniti edition (vs2015.3.com_enu.iso) с заведением и введением учетки MS

        2) Учетку мне действительно заблокировали, но ошибся в причине, вот что пишет

        Ваша учетная запись заблокирована

        Мы обнаружили действия, которые нарушают наше соглашение об использовании служб Майкрософт, и заблокировали вашу учетную запись.

        Разблокирование учетной записи

        Нажмите Далее, и мы отправим код проверки на ваш телефон. После ввода кода вы сможете вернуться в свою учетную запись.

        Знакомая история. Телефон их мне им сдалось вводить .

        Вопрос такой: для VS2022 CE мне тоже учетка будет нужна?


    1. mayorovp
      07.12.2023 10:55

      Вы вместо бесплатной версии скачали триал платной.

      Решение тут простое - удалить студию и скачать VS Community Edition.


      1. NutsUnderline
        07.12.2023 10:55

        Вопрос такой: для VS2022 CE мне тоже учетка будет нужна?


        1. mayorovp
          07.12.2023 10:55
          +1

          Нет. Она тоже попросит учётку, но это окно можно просто закрыть.


  1. klvov
    07.12.2023 10:55

    Эх, и влепил бы статье минус, да комментов жалко )


  1. Green21
    07.12.2023 10:55

    Даже у таких статей на хабре есть плюс - в комментах можно узнать много нового о языках и фреймворках))


  1. moooV
    07.12.2023 10:55
    +2

    Снова как окунулся в журнал ][Акер 2000 года выпуска. Ждём от автора статьи как ломать аську и где разжиться халявным инетом.


  1. qrKot
    07.12.2023 10:55
    +2

    Какая-то не смешная юмореска (это же юмореска, да???)

    Поздравляю, теперь ты полноценный Junior C++ Desktop Developer, если сверху изучить работу с памятью и работу с базами данных, будешь уже Middle C++ Engine Developer,

    вот вы пошутили (вы же пошутили, да??? пошутили???), а кто-то с этим на собеседование придет! Зачем вы так?


    1. inscriptios
      07.12.2023 10:55

      Я тоже вначале подумал, что автор просто толстый тролль, но прочитав половину этого "труда", понял, что все намного проще...


  1. d00m911
    07.12.2023 10:55

    Без обид, уважаемый автор, но вы сами плохо знаете С++. У вас, как минимум, нет понимания того, что такое препроцессор и для чего нужны его команды, иначе ничем нельзя объяснить некоторые части вашего кода.


  1. simenoff
    07.12.2023 10:55
    +1

    Статьи в тему:

    Десктопные UI https://tonsky.livejournal.com/311960.html

    Пациент умер, выносите https://tonsky.livejournal.com/320566.html

    Что случилось с GUI-фреймворками? https://tonsky.livejournal.com/320961.html


  1. NikolayNikT
    07.12.2023 10:55

    Создать Button или Label много ума не нужно, а что дальше?


  1. serafims
    07.12.2023 10:55

    А на чем сейчас эффективно писать десктоп-приложения, кроссплатформенные,
    по уровню как какой-нибудь "мой офис"?

    Вот знаю Delphi, но громких проектов новых на ней не слышно.


    1. simenoff
      07.12.2023 10:55
      -1

      Да фактически ни на чём

      Про Делфи кто-то метко сказал: "Это какой-то гроб на колёсиках"

      Незря:


    1. Oceanshiver
      07.12.2023 10:55

      .net + AvaloniaUI, например



  1. Emelian
    07.12.2023 10:55

    > Как войти в айти за час с нуля, на языке с++ Win32 (графические приложения) «для самых маленьких»

    Это называется: «Дайте мне любую, высокую и светлую идею и я ее... испохаблю!»

    На самом деле С++ и WinAPI это очень классные вещи,  но такая подача вызывает естественный вопрос: «А на фейхуа это мне надо?».

    Лично я считаю, что программирование лучше демонстрировать с конца. Т.е., показываете, допустим, какую-нибудь готовую программу и спрашиваете: «Вы желаете заниматься этим?». Или, как вариант, ставите задачу, которую собираетесь решить и говорите: «Вы хотите иметь программу, которая делает подобное?». А то, все любят учить начинающих, поскольку для этого много ума не надо, но никто не хочет учить уму-разуму «кончающих».

    Но вернемся к C++ / WinAPI. Могу рассказать свой опыт. Пробовал Ассемблер (и даже дизассемблер, см. мой сайт http://erfaren.narod.ru/ - тогда наш регион был украинским, сейчас в составе РФ), Си, С++, WinAPI, Qt, wxWidgets, Win32++, WTL. Отдельно можно поговорить о других языках программирования: Visual FoxPro, Java, Python. В последнее время выбрал для себя C++ / WTL, потом успел разочароваться в WTL и перешел на C++ / WinAPI. Однако, к моему удивлению, сейчас у меня возникла вторая любовь к WTL. Это очень небольшая библиотека, в исходных кодах, но достаточно полезная для десктопного программирования с ориентацией на пользовательский интерфейс (GUI). Просто она достаточно глубокая и мощная для своего класса, что не слишком очевидно, пока не начнешь писать на ней реальные задачи.

    А эволюция перехода от одного инструмента программирования к другому определялся теми задачами (в качестве хобби), которые я решал. Из последних тем, это создание обучающей программы «Scholium» («Сколиум»), v. 2.1 (для изучения иностранных языков - http://scholium.webservis.ru/ ) и программы «MediaText» подготовки данных для нее. Обе написаны на C++ / WTL, но последняя пока не опубликована, поскольку нужна только для первой программы, которая еще «не пошла в массы». Но ее скриншот я могу привести здесь.

    http://scholium.webservis.ru/Pics/MediaText.png

    А вот пример незаконченной версии обучающей программы, написанной на чистом C++ / WinAPI.

    http://scholium.webservis.ru/Pics/CellsEdit.png

    Исходя из данной информации, уже можно ставить вопрос новичкам: "Вы хотите этим заниматься?" Если, да, то далее начинаем осваивать WinAPI с WTL'ем или без, либо посылаем всё "на хутор бабочек ловить" и занимаемся своим вебом, как это делает большинство современных программистов.

    Кстати, в "МедлиаТексте" самое трудное было внедрить в программу медиа-проигрыватель (опенсорсный FFPlay.c). Вот бы еще, по аналогии с этим внедрить с свою программу чистые листы Эксел, без лишних прибамбасов, то вообще было бы здорово, но это уже будет другая задача.


    1. Oceanshiver
      07.12.2023 10:55

      WinAPI, WTL... На дворе 2023 год, вы бы еще MFC вспомнили


  1. mikntp
    07.12.2023 10:55

    Как толсто, но никто не оценил )


  1. deadlock1979
    07.12.2023 10:55
    +1

    Ощущение, что попал обратно в нулевые.

    Или это такой толстый вброс?


  1. duke_alba
    07.12.2023 10:55

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

    В целом, хватило названия... К сожалению, встречаются люди, верящие подобному. Они даже деньги платят за всякие курсы, обещающие с нуля и до звёзд всего за пару месяцев. Нехорошо, стыдно это.