Перейти к содержанию

Antonshka

Пользователи+
  • Постов

    410
  • Зарегистрирован

  • Посещение

  • Победитель дней

    16

Сообщения, опубликованные Antonshka

  1. Не знаю, хорошо ли это, писать сейчас на форуме. Ведь форум сейчас на компьютере у @Xipho, плюс никто не пишет ничего чего-то, плюс форум как я понял будет скоро закрыт.

    Однако, напишу все же, раз тему завел. Да простят меня все.

     

    Я лишь хотел сказать, что я закончил с этой таблицей акселератора. И для тех кому интересно, реализовал я ее так.

    Это пример того как создается главное окно, создается меню, назначаются горячие клавиши.

    Спойлер
    constexpr UINT ACCELERATOR_OpenProject = 0;
    constexpr UINT ACCELERATOR_OpenFolder = 1;
    constexpr UINT ACCELERATOR_Delete = 2;
    
    int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) {
    	WGW::GUI::initializeWGWLibrary(hInstance);
      
    	WGW::Window* mainWindow = new WGW::Window(TRUE, 0, 0, L"MainWindowName", 0, 0, 900, 900);
    	mainWindow->menu.enabled = TRUE;
    	WGW::PopupMenu* popupMenuFile = new WGW::PopupMenu();
    	WGW::ItemOfMainMenu* fileItem = mainWindow->menu.addItem(WGW::EnabledState::Enabled, L"File", nullptr, popupMenuFile);
    
    	WGW::ItemOfMainMenu* editItem = mainWindow->menu.addItem(WGW::EnabledState::Enabled, L"Edit", nullptr, nullptr);
    	WGW::PopupMenu* popupMenuEdit = new WGW::PopupMenu();
    	editItem->setPopupMenu(popupMenuEdit);
    
    	WGW::ItemOfPopupMenu* openProjectItem = popupMenuFile->addItem(WGW::EnabledState::Enabled,
    		WGW::TypeOfItemOfMenu::Default, WGW::CheckableState::Uncheckable, WGW::CheckedState::Unchecked,
    		WGW::CheckMarkType::Flag, L"OpenProject", L"Ctrl + G", nullptr, nullptr, WGW::SettingsButtonState::Disabled, nullptr);
    	
    	WGW::ItemOfPopupMenu* openFolderItem = popupMenuFile->addItem(WGW::EnabledState::Enabled,
    		WGW::TypeOfItemOfMenu::Default, WGW::CheckableState::Uncheckable, WGW::CheckedState::Unchecked,
    		WGW::CheckMarkType::Flag, L"OpenFolder", L"Ctrl + Shift + K", nullptr, nullptr, WGW::SettingsButtonState::Disabled, nullptr);
    
    	WGW::ItemOfPopupMenu* deleteItem = popupMenuEdit->addItem(WGW::EnabledState::Enabled,
    		WGW::TypeOfItemOfMenu::Default, WGW::CheckableState::Uncheckable, WGW::CheckedState::Unchecked,
    		WGW::CheckMarkType::Flag, L"Delete", L"Ctrl + D", nullptr, nullptr, WGW::SettingsButtonState::Disabled, nullptr);
    
    
    	mainWindow->keystroke->addKeystrokeEntry(ACCELERATOR_OpenProject, FALSE, VK_CONTROL, 0x47);
    	mainWindow->keystroke->addKeystrokeEntry(ACCELERATOR_OpenFolder, FALSE, VK_CONTROL, VK_SHIFT, 0x4B);
    	mainWindow->keystroke->addKeystrokeEntry(ACCELERATOR_Delete, FALSE, VK_CONTROL, 0x44); //FALSE значит NoRepeat
      
      	mainWindow->runMessageCycle();
     	return 0;
    }

     

     

    Это главный messageLoop

    Спойлер
    while ((resultOfGetMessage = GetMessageW(&msg, NULL, 0, 0)) != 0)	{
      if (resultOfGetMessage == -1) {
        break;
      }
      else {
        if (!translateAccelerator(&msg)) {
          TranslateMessage(&msg);
          DispatchMessageW(&msg);
        }
      }
    }

     

     

    Это translateAccelerator

    Спойлер
    BOOL Window::translateAccelerator(MSG* msg) {
      BOOL isProcessed{ FALSE };
      if (msg->message == WM_KEYDOWN) {
        Keystroke::determinePressedKeys();
        for (auto& window : allCreatedLibWindowObjects) { //allCreatedLibWindowObjects = inline static std::unordered_map<size_t, Window*> allCreatedLibWindowObjects;
          for (auto& entry : window.second->keystroke->entries) {
            if (entry->isRepeated || !entry->isNeedReset) {
              if (entry->isKeysMatches(Keystroke::pressedKeys)) {
                entry->isNeedReset = TRUE;
                PostMessageW(window.second->hWnd, WM_USERAcceleratorEvent, entry->commandID, 0);
                isProcessed = TRUE;
              }
            }
          }
        }
      }
      else if (msg->message == WM_KEYUP) {
        Keystroke::determinePressedKeys();
        if (!Keystroke::pressedKeys.size()) {
          for (auto& window : allCreatedLibWindowObjects) {
            for (auto& entry : window.second->keystroke->entries) {
              entry->isNeedReset = FALSE;
            }
          }
        }
        else {
          for (auto& window : allCreatedLibWindowObjects) {
            for (auto& entry : window.second->keystroke->entries) {
              if (entry->isNeedReset) {
                if (!entry->isKeysMatches(Keystroke::pressedKeys)) {
                  entry->isNeedReset = FALSE;
                }
              }
            }
          }
        }
      }
      return isProcessed;
    }

     

     

    А это классы для работы самодельного аккселератора. Кое-какой функционал будет еще добавлен для них. Keystroke.h

    Спойлер
    namespace WGW
    {
    	class Keystroke;
    
    	class KeystrokeEntry {
    		friend class Keystroke;
    		friend class Window;
    	protected:
    		KeystrokeEntry() = default;
    		KeystrokeEntry(CONST KeystrokeEntry&) = delete;
    		KeystrokeEntry& operator=(CONST KeystrokeEntry&) = delete;
    		~KeystrokeEntry() = default;
    	protected:
    		BOOL isKeysMatches(std::vector<UINT>& pressedKeys);
    	protected:
    		UINT commandID{ 0 };
    		BOOL isNeedReset{ FALSE };
    		BOOL isRepeated{ FALSE };
    		std::vector<UINT> keys;
    	};
    
    	//-------------------------------------------------------------------------------------------
    
    	class Keystroke {
    		friend class Window;
    	protected:
    		Keystroke(Window* windowOwner);
    		Keystroke(CONST Keystroke&) = delete;
    		Keystroke& operator=(CONST Keystroke&) = delete;
    		~Keystroke();
    	public:
    		BOOL addKeystrokeEntry(UINT commandID, BOOL isRepeated, UINT key1, UINT key2 = 0, UINT key3 = 0, UINT key4 = 0, UINT key5 = 0);
    		BOOL deleteKeystrokeEntry(UINT commandID);
    	protected:
    		static VOID determinePressedKeys();
    	protected:
    		inline static std::vector<Keystroke*> allCreatedLibKeystrokeObjects;
    	protected:
    		std::vector<KeystrokeEntry*> entries;
    		inline static BYTE keyboardState[256];
    		inline static std::vector<UINT> pressedKeys;
    		Window* windowOwner{ nullptr };
    	};
    }

     

     

    И Keystroke.cpp

    Спойлер
    BOOL KeystrokeEntry::isKeysMatches(std::vector<UINT>& pressedKeys) {
    		BOOL isKeysMathes{ FALSE };
    		for (auto& key : keys) {
    			isKeysMathes = FALSE;
    			for (auto& pressedKey : pressedKeys) {
    				if (key == pressedKey) {
    					isKeysMathes = TRUE;
    					break;
    				}
    			}
    			if (!isKeysMathes) {
    				break;
    			}
    		}
    		return isKeysMathes;
    	} 
      
      Keystroke::Keystroke(Window* windowOwner) : windowOwner(windowOwner) {
    		allCreatedLibKeystrokeObjects.push_back(this);
    	}
    
    	Keystroke::~Keystroke() {
    		if (!GUI::isMessageCycleTerminated) {
    			std::erase(allCreatedLibKeystrokeObjects, this);
    		}
    		for (auto& entry : entries) {
    			delete entry;
    		}
    	}
    
    	BOOL Keystroke::addKeystrokeEntry(UINT commandID, BOOL isRepeated, UINT key1, UINT key2, UINT key3, UINT key4, UINT key5) {
    		BOOL isCommandIDExist{ FALSE };
    		BOOL isKeystrokeEntryAdded{ FALSE };
    		for (auto& entry : entries) {
    			if (entry->commandID == commandID) {
    				isCommandIDExist = TRUE;
    				break;
    			}
    		}
    		if (!isCommandIDExist) {
    			KeystrokeEntry* entry = new KeystrokeEntry();
    			entry->commandID = commandID; 
    			entry->isRepeated = isRepeated;
    			entry->keys.push_back(key1);
    			if (key2) { entry->keys.push_back(key2); }
    			if (key3) { entry->keys.push_back(key3); }
    			if (key4) { entry->keys.push_back(key4); }
    			if (key5) { entry->keys.push_back(key5); }
    			entries.push_back(entry);
    			isKeystrokeEntryAdded = TRUE;
    		}
    		return isKeystrokeEntryAdded;
    	}
      
      BOOL Keystroke::deleteKeystrokeEntry(UINT commandID) {
    		BOOL isKeystrokeEntryDeleted{ FALSE };
    		KeystrokeEntry* entryToDelete{ nullptr };
    		for (auto& entry : entries) {
    			if (entry->commandID == commandID) {
    				entryToDelete = entry;
    				break;
    			}
    		}
    		if (entryToDelete) {
    			std::erase(entries, entryToDelete);
    			delete entryToDelete;
    			isKeystrokeEntryDeleted = TRUE;
    		}
    		return isKeystrokeEntryDeleted;
    	}
    
    	VOID Keystroke::determinePressedKeys() {
    		pressedKeys.clear();
    		BOOL result = GetKeyboardState(keyboardState);
    		if (result) {
    			for (UINT i = 0; i < 256; i++) {
    				if (i != VK_LSHIFT && i != VK_RSHIFT &&
    					i != VK_LCONTROL && i != VK_RCONTROL &&
    					i != VK_LMENU && i != VK_RMENU)
    				{
    					if (keyboardState[i] & 0x80) {
    						pressedKeys.push_back(i);
    					}
    				}
    			}
    		}
    	}
    }

     

     

    • Плюс 2
  2. Привет, с наступившим Новый Годом🌲! Хорошо его провести.

    Кто-нибудь игрался с самодельной таблицей акселераторов?

    Как известно, стандартная WinAPI таблица акселераторов поддерживает только одну нажатую клавишу, совместно с SHIFT, CONTROL, или ALT. Например, позволено делать только так, - "SHIFT + ALT + M", или "ALT + G".

    Но что если нужно так, "SHIFT + ALT + M + A + T"?

     

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

    В основном цикле сообщений будет своя функция translateAccelerator

    Спойлер
    while ((resultOfGetMessage = GetMessageW(&msg, NULL, 0, 0)) != 0)	{
      if (resultOfGetMessage == -1) {
        break;
      }
      else {
        translateAccelerator(&msg);
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
      }
    }

     

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

    Список горячих клавиш думаю будет таким -

    Спойлер
    struct Keystroke {
      std::array<BYTE> hotkeys;
      CustomAcceleratorsIDs id{ 0 }
    };
        
    std::vector<Keystroke*> Keystrokes; //Это будет полем класса, у каждого виждета свое.

     

     

    Если проверка на совпадение покажет, что такие клавиши для этого виджета установленны, то будет послано сообщение, этому виджету -

    Спойлер
    constexpr DWORD WM_USERAcceleratorMessage = WM_APP + 1; //Допустим это будет тип сообщения
    //a это, существующие различные ID
    enum class CustomAcceleratorsIDs : INT { CreateFile, OpenFile };
    
    
    BOOL isSequenceFound{ FALSE };
    
    //Допустим проверка на совпадение показала TRUE
    isSequenceFound = TRUE;
    Keystroke* foundedKeytroke = Keystrokes.at(индекс, при котором найдено совпадение клавиш)
      
      
    if (isSequenceFound){
      PostMessageW(hWnd, WM_USERAcceleratorMessage, (WPARAM)foundedKeytroke, 0); //hWnd - это хендл окна у которого нашлось совпадение клавиш
    }
    
    //Далее, в своей оконой процедуре, это окно проверит сообщение
    switch (msg){
      case WM_USERAcceleratorMessage: return wmAcceleratorMessage(wParam);
    }
    
    LRESULT wmAcceleratorMessage(WPARAM wParam){
      Keystroke* foundedKeytroke = reinterpret_cast<Keystroke*>(wParam)
      switch (foundedKeytroke.id){
        case CustomAcceleratorsIDs::CreateFile: return createFile();
        case CustomAcceleratorsIDs::OpenFile: return openFile(); 
      }
    }

     

     

    Осталось придумать как эффективнее проверять клавиши. Как вообще отслеживать последовательность нажатых клавиш.

  3. 29 минут назад, SerVick сказал:

    Вообще я бы поблагодарил бы ещё Джаста за его мнение.. т.к. дополнительный стимул к учёбе ещё мне дал бы

    Стимул обычно увеличивается словами поддержки. Например, - "потерпи, со временем ты это освоишь, и я проходил через подобные трудности".

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

    Не видишь ли недоброжелательность, вместо увеличения стимула, уменьшать его?

    Но, как я уже сказал, на меня эти слова не подействовали. Я уже не новичок, - путь определенный я уже проделал, и способности свои я уже узнал.

     

    12 минут назад, SerVick сказал:

    А вот положительную сторону форума почему то не упомянул Алекс, если уж объективно смотреть.

    Вопрос к нему был не про положительную сторону.

    • Спасибо 1
  4. 1 час назад, SerVick сказал:

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

    Как я вижу, @Alex2411 не чернит ресурс, но говорит по факту. Ему @Xipho задал конкретный вопрос, он дал ему конкретный ответ. Указал на недоброжелательность.

    Такую недоброжелательность я лично ощутил и на себе.

    Спойлер
    В 12.08.2021 в 18:20, Antonshka сказал:

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

     

    @JustHack - Если для тебе это самое сложное в изучении программирования, возможно, программирование это не для тебя. Как говорится:

    Цитата

    @JustHack - Кесарю - кесарево, пекарю - пекарево, а слесарю - слесарево.

    Правда лично меня эти слова никак не задели. К моему удивлению.

     

     

    Недоброжелательность на форуме была, этот факт. Недоброжелательность не способствует развитию форума, это тоже факт. @Alex2411 просто указал на эти факты.

     

    А что касается доброжелательности, и пользы от форума, - ее конечно было несказанно больше.

    Я не думаю что активность форума мала из-за недоброжелательности. Она была крайне редка. В сравнении с доброжелательностью.

     

  5. 20 часов назад, Xipho сказал:

    Вот это и есть основная проблема.

    21 час назад, Xipho сказал:

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

    Именно. Но этим времени заниматься нет.

     

    Может быть тебе это не доставляет удовольствие. Может быть дело вовсе не в отсутствии времени.

    Есть уникальная по своей силе фраза "Кто хочет, ищет возможности, кто не хочет, ищет причины". Лучше этой "лакмусовой бумажки" я ничего еще не встречал. Она раскрывает истинные намерения человека на 200 процентов. Раскрывает всегда.

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

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

     

    Активности на форуме практически нет, естественно это должно доставлять автору некоторые неудовольствия. Умножим это на то, что такое положение вещей наблюдается уже в течении 10 лет.

    Итак, человек, который поставил мне недоумевающий смайлик, в сообщении о поддержке закрытии форума? Чего тебе не понравилось? Не хочет @Xipho получать неудовольствие, хочет получать удовольствие. Я его в этом поддержал. За что такой смайлик? За то ли, что для тебя закрытие форума не выгодно?

    Впрочем, не важно, за что такой смайлик. @Xipho человек взрослый, решение свое он обдумал. Я поступил бы также как он, я пошел бы тратить свое время в пользу своего удовольствия.

    • Плюс 1
  6. Я поддерживаю. Форум нужно закрывать. Общаются в основном всего несколько человек. Их можно сосчитать по пальцам.

    Я рад, что @Xipho решился на это!

    Не подумайте, что я против форума. Я за появление свободного времени у @Xipho.

    • Зачем оно здесь? 1
  7. 1 час назад, Xipho сказал:

    Не понял твою идею, ну да ладно. Я бы на твоём месте не стал изобретать велосипеды, а использовал бы protobuf в бинарном варианте.

    Идея с std::unordered_map простая. Все объекты PopupMenu хранятся в unordered_map. У каждого объекта PopupMenu есть специальное поле, которое есть его ключ в unordered_map. Этот ключ уникальный, его тип INT. Указатель же на сам объект хранится как значение в unordered_map. То есть, unordered_map(Key, Value) = unordered_map(INT, PopupMenu*)

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

    Посмотрю про protobuf. Интересно, что это. Спасибо.

     

    1 час назад, Xipho сказал:

    Удачи в этом нелегком деле. А ведь можно было избежать большого количества рефакторинга, изначально продумав архитектуру...

    Архитектура библиотеки со дня ее создания почти не изменилась. Она меня устраивает.

    Большой и постоянный рефакторинг основан на постоянном увеличении опыта и знаний. А также и идей.

    Конечно, было бы интересно посмотреть как пишут программы специалисты, поучиться у них.

     

    1 час назад, Xipho сказал:

    Потому что распылял своё внимание, не шёл последовательно, не намечал план (хотя бы в том же Trello).

    Кое-какой план все же есть.

    У меня есть блокнот, в котором я пишу задания. Например, - сделать нужно то-то и то-то, это нужно удалить, это добавить, над этим подумать.

  8. Что-то @Xipho не ответил. Возможно времени нет у него.

    В любом случае, с сериализацией/десериализацией я закончил. Основа этого дела, это использование std::unordered_map. Ключ используется для выбора правильного объекта.

    Строки сохраняются в файл на диск так, - сперва сохраняется длина строки, затем ее содержимое.

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

     

    Теперь можно заняться улучшайзенгом кода. Превращением его из каши во что-то более понятное. А то @Xipho, увидев его, потеряет сознание.

    Прошел год, (365 дней), а я написал только PopupMenu, MainMenu, ToolTip, и TitleBar (в форме кода-каши). Ай да молодец я.

    • Плюс 1
  9. В 18.12.2022 в 17:54, Xipho сказал:

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

     

    Что если поле класса std::wstring? Плюс этот странный sizeof.

    Спойлер
    class Sample {
    public:
    	std::wstring text;
    	INT value1{ 0 };
    };
    
    Sample sample;
    sample.text = L"1234fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
    WGW::GUILogger::write(WGW::LoggerNotifyType::Info, L"Sample size = %d\n", sizeof(sample));
    
    //12/21/22  17:30:13 - [INFO] Sample size = 48

     

    Спойлер
    class Sample {
    public:
    	std::wstring text;
    	INT value1{ 0 };
    	INT value2{ 0 };
    };
    
    Sample sample;
    sample.text = L"1234fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
    WGW::GUILogger::write(WGW::LoggerNotifyType::Info, L"Sample size = %d\n", sizeof(sample));
    
    //12/21/22  17:32:41 - [INFO] Sample size = 48
    //Все равно 48

     

    Спойлер
    class Sample {
    public:
    	std::wstring text;
    	INT value1{ 0 };
    	INT value2{ 0 };
    	INT value3{ 0 };
    };
    
    Sample sample;
    sample.text = L"1234fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
    WGW::GUILogger::write(WGW::LoggerNotifyType::Info, L"Sample size = %d\n", sizeof(sample));
    
    //12/21/22  17:33:35 - [INFO] Sample size = 56
    //Теперь 56

     

     

    Такое странное поведение sizeof основано на Data structure alignment.

     

  10. 3 минуты назад, Garik66 сказал:

    ИМХО - для рядового пользователя это сложно будет, сомневаюсь, что он оценит это.

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

  11. Появилась хорошая идея, как сохранить/восстановить объекты PopupMenu.

    Раньше PopupMenu назначалось контроллу по указателю.

    Спойлер
    //Псевдокод.
    class PopupMenu {};
    class Button {
      private:
      PopupMenu* popupMenu{ nullptr };
    };
    
    int main() {
      PopupMenu pop1 = new PopupMenu();
      PopupMenu pop2 = new PopupMenu();
      PopupMenu pop3 = new PopupMenu();
      Button btn1 = new Button();
      Button btn2 = new Button();
      Button btn3 = new Button();
      
      btn1.popupMenu = pop1;
      btn2.popupMenu = pop1; //Можно один и тот же PopupMenu назначать разным контроллам.
      btn3.popupMenu = pop2;
      
      return 0;
    }

     

     

    Теперь будет так

    Спойлер
    //Псевдокод.
    class Widget { //Базовый класс для всех контроллов, будь-то PopupMenu, Button, EditBox
      privat:
      	inline static std::vector<Widget*> allWidgetsInLib;
      private:
      	INT popupMenuKey{ -1 };
    }
    
    class PopupMenu : public Widget {
      public:
        PopupMenu() {
          	//Допустим в векторе allPopupMenusInLib есть 5 штук PopupMenu.
    		//Первые три PopupMenu имеют ключи (keyInAllPopupMenusInLib), 1, 2, 3, а четвертый имеет ключ 7, а пятый 12 (это например потому что пользователь 		  //удалил PopupMenu с 4 по 6, и с 8 по 11).
          	//Тогда новый ключ, для новосозданного PopupMenu, будет равен 4
    		allPopupMenusInLib.push_back()
            keyInAllPopupMenusInLib = calculateMyKey() //метод calculateMyKey, пройдя по вектору, и встретив первую непоследовательнось ключей, вернет 4
        }
      	~PopupMenu() {
          //Пройди по вектору allWidgetsInLib, и у всех контроллов, у которых ключ PopupMenu равен моему, установи значение этого ключа в -1. Это значит 	 	   //PopupMenu не назначен.
        }
      privat:
      	inline static INT getKey(PopupMenu* popupMenuArg) {
          //Если popupMenuArg существует в allPopupMenusInLib, верни его ключ, то есть keyInAllPopupMenusInLib. Если не существует, верни -1
        }
      privat:
    	inline static std::vector<PopupMenu*> allPopupMenusInLib;
      privat:
      	INT keyInAllPopupMenusInLib{ -1 };
    };
    
    class Button : public Widget {
      public:
      	INT addPopupMenu(PopupMenu* popupMenuArg) {
          //Если popupMenuArg существует в allPopupMenusInLib {
          	  popupMenuKey = PopupMenu::getKey(popupMenuArg);
              this->popupMenuKey = popupMenuKey;
          }
          return this->popupMenuKey;
        }
    };
    
    int main() {
      PopupMenu pop1 = new PopupMenu();
      PopupMenu pop2 = new PopupMenu();
      PopupMenu pop3 = new PopupMenu();
      Button btn1 = new Button();
      Button btn2 = new Button();
      Button btn3 = new Button();
      
      btn1.addPopupMenu(pop1);
      btn2.addPopupMenu(pop1); //Можно один и тот же PopupMenu назначать разным контроллам.
      btn3.addPopupMenu(pop2);
      
      deserializeSettings();
      //По закрытии приложения, где-то в дугом месте вызовется serializeSettings();
      return 0;
    }
    
    VOID deserializeSettings() {
      //Если файл сохранения существует, то загрузи и примени его.
      //Сохранение/Восстановление объектов и их ссылок теперь заключается в:
      // - при сериализации, каждый виджет сохраняет на диск свое поле popupMenuKey. При десериализации, 
      //   каждый виджет загружает с диска свое поле popupMenuKey.
      // - при сериализации, каждый PopupMenu сохраняет на диск свои атрибуты (цвет, фонт, и т.д), а также keyInAllPopupMenusInLib. При десериализации,
      //   каждый PopupMenu, сперва проверяет, есть ли уже в allPopupMenusInLib PopupMenu с его ключем (keyInAllPopupMenusInLib), и если уже есть,
      //   тогда просто замени его атрибуты своими, а если еще нет, тогда создай новый PopupMenu, со своим ключем и со своими атрибутами.
    }

     

     

  12. 1 час назад, Xipho сказал:

    А причем тут указатели?

    Представь себе главное меню какого-либо приложения. Первый элемент такого меню обычно называется Файл. Нажимая на него, появляется PopupMenu. В этом PopupMenu обычно следующие названия пунктов, - Открыть, Сохранить, Сохранить как, Выход, и т.п.

    Пункт Открыть вызывает к примеру функцию VOID Open(), - так предусмотрел разработчик. Однако, пользователь, по желанию разработчика, может активировать режим модификации. Он, пользователь, включив такой режим, открывает этот PopupMenu, где Открыть, Сохранить, Сохранить как, Выход, затем, кликает по нему правой кнопкой мыши, перед ним появляется другое PopupMenu, над этим PopupMenu. Это второе PopupMenu, имеет пункты со следующими названиями, - Изменить название пункта, Изменить опцию пункта (то есть функцию), и другие.

    Выбрав Изменить опцию пункта, появляется окно со списком (ListView например, или TreeView). В нем перечислены все (дозволенные) существующие опции/функции, одну из которых можно назначить на этот пункт.

    Например, есть функция VOID SaveAs() для пункта Сохранить как. Пользователь может выбрать ее из списка и назначить на пункт Выход. Это просто пример.

     

    Так вот, текущая задача, - это сериализация/десериализация этого указателя на функцию VOID SaveAs(). Так чтобы она теперь была для пункта Выход. Даже после перезапуска приложения.

     

    Частично это задача теперь уже решена, если даже не полностью -

    Это поле класса PopupMenu.

    inline static std::map<std::wstring, OnClickForItemOfPopupMenu> userClickFunctionsForItems;

    Разработчик заполняет этот std::map, определяя тем самым для пользователя допустимые варианты выбора опций.

    WGW::PopupMenu::userClickFunctionsForItems.insert(std::make_pair(L"Открыть", open)); //open, это указатель на VOID open()
    WGW::PopupMenu::userClickFunctionsForItems.insert(std::make_pair(L"Параметры", showParameters)); //showParameters, это указатель на VOID showParameters()
    WGW::PopupMenu::userClickFunctionsForItems.insert(std::make_pair(L"Копировать", copy)); //copy, это указатель на VOID copy()

    Пользователь, в ListView контролле, выбирает либо Открыть, либо Параметры, либо Копировать. Выбрав например Параметры, это слово сохраняется в поле userItemClickFunction класса Item. Все, переназначение опции/функции для конкретно этого пункта совершено.

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

    userClickFunctionsForItems.at(item->originalItem->userItemClickFunction)(&onClickEventForItemOfPopupMenu);
    item->originalItem->userItemClickFunction //userItemClickFunction это std::wstring равное Открыть, Параметры, или Копировать, смотря что выбрал пользователь.

     

    Это дело с указателем сейчас работает нормально. Для сериализации/десериализации нужно по идее просто сохранить/восстановить это поле со строкой - userItemClickFunction класса Item.

     

    Сейчас нужно придумать сохранение/восстановление объектов PopupMenu. Плюс их правильное назначение для контроллов, после восстановления.

  13. Я вернулся к написанию библиотеки GUI.

    Казалось бы, самодельная система PopupMenu уже закончена, но нет, мне пришла странная идея сделать её лучше чем в QT, - больше функционала, больше гибкости. PopupMenu на любой вкус и цвет. Причем для этого абсолютно не придется менять что-то в классе, - меняй только открытые свойства.

     

    И это идея удалась, - благодаря всем нам конечно. Система PopupPenu, а с ней и система MainMenu, получились ну реально крутые.

    Дело дошло до того, что была организована интерактивная замена PopupMenu'ек и их итемов в режиме реального времени. Как например в FireFox, когда можно мышкой ухватить итем какой-либо закладки в PopupMenu, и перетащить в другой PopupMenu. Причем создается такое окно-отрывок (snippet),  с итемом на нем.

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

    Говоря короче, конечный пользователь (здесь это тот кто запустит уже готовый exe) может сам настроить под себя всю систему MainMenu и PopupMenu, - если разработчик позволит ему так делать, например добавив чек-бокс, для активации режима модификации.

     

    Для сохранения всех переделок, сделанных конечным пользователем, нужна сериализация/десериализация.

    Я поторопился, - интерактивная система была создана раньше системы сериализации. Хотя, когда только все начиналось, какое-то представления, как эту сериализацию потом организовать, у меня все-таки было. Но сейчас, я не могу вспомнить, что я там напланировал.

     

    В любом случае, рано или поздно, эта система сериализации будет создана.

    • Плюс 1
  14. Привет, привет, любители логических задач!  У меня снова есть для вас кое-что странное.

     

    В коде приложения, определен класс Sample. Также, в коде приложения определены три его объекта-экземпляра, А, B, и C.

    Класс Sample имеет одно единственное поле PTR, - указатель на функцию. Это поле по умолчанию равно nullptr.

    В коде приложения есть множество разных функций. Сигнатуры этих функций совпадают с типом PTR.

     

    Запускаем приложение!

    Наша единственная задача в приложении, - назначить для поля PTR объектов A, B, и C, свой указатель на какую-либо функцию. После назначения, мы закрываем приложение.

    Приложение должно сохранить назначенные указатели на диск. При следующем запуске приложения, оно, приложение, найдя файл сохранения, прочитает его, и восстановит значения полей объектов А, B, и C.

     

    Но это еще не все. Мы можем захотеть создать еще несколько объектов динамически, повторив с ними тоже, что было сделано с объектами А, B, и C. И теперь, приложение должно при закрытии сохранить еще и динамически созданные объекты, плюс и их поля.

     

    Как вам такой сюжет?

     

     

  15. 3 часа назад, Xipho сказал:

    Почитай про принципы KISS и YAGNI

     

    Спасибо за советы. Почитал, для тех кому тоже интересно, вот выписка -

    Спойлер

    KISS – keep it short simple – делайте вещи проще. Порой наиболее правильное решение – это наиболее простая реализация задачи, в которой нет ничего лишнего.

    Эта аббревиатура (англ. kiss — поцелуй, целовать) мне самому всегда нравилась — как по форме, так и по значению: Keep it simple, stupid («Сделай это проще, тупица») или, если кому-то не нравится называться тупицей, есть вариант Keep it stupid simple («Пусть всё будет простым до безобразия»), который ещё лучше передаёт смысл аббревиатуры.

    Решая какую-нибудь проблему, можно так увлечься, что сам не заметишь, как уже занялся оверинжинирингом или, как я люблю говорить, вовсю палишь из пушки по воробьям. Задача в итоге, конечно, будет решена — но её можно было бы выполнить куда проще и изящнее.

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

    Проверяйте, достаточно ли понятны ваши логические цепочки. Хватит ли знаний вашим коллегам, чтобы в них разобраться? Простой код и простой дизайн уменьшают риск ошибок, да и читать такой код проще. В общем, не забывайте про KISS!

     

     

    YAGNI — You ain’t gonna need it – вам это не понадобится. Все что не предусмотрено техническим заданием проекта, не должно быть в нем.

    Принцип, иначе известный как You ain’t gonna need it («Вам это не понадобится»), пришёл из экстремального программирования. Согласно ему создавать какую-то функциональность следует только тогда, когда она действительно нужна.

    Дело в том, что в рамках Agile-методологий нужно фокусироваться только на текущей итерации проекта. Работать на опережение, добавляя в проект больше функциональности, чем требуется в данный момент, — не очень хорошая идея, учитывая, как быстро могут меняться планы.

    Что я имею в виду, говоря про смену планов? Итерации в Agile довольно короткие — то есть вы будете получать какой-то фидбэк уже на ранних этапах разработки, и потенциально он может изменить направление всей работы над проектом. Так зачем вам тратить время на функцию, которая в итоге окажется совершенно ненужной?

    Однако замечу напоследок, что если вы используете методы каскадной разработки, где вся работа планируется заранее и все стараются чётко придерживаться плана, принцип YAGNI неприменим

  16. Если суммировать, то получается, самая лучшая формула создания потока, его завершения, а также выгрузки DLL, на С++, это

     

    - поток всегда создается через _beginthreadex

    - поток всегда завершается сам, выходя через свой return

    - FreeLibraryAndExitThread никогда не вызывается

    - выгрузкой библиотеки, через FreeLibrary, занимается стороннее приложение/процесс. Как вариант, тот же, который и загрузил её.

     

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

     

    Когда же процесс сам лично загружает DLL, то там немного другая история, и ее, по крайней мере я, здесь не рассматривал.

  17. 18 минут назад, Xipho сказал:

    В твоем потоке есть вызовы CRT?

    Я ждал этот вопрос.

    Пока нет, но когда они могут появиться, я не хочу возвращаться и переписывать с CreateThread на _beginthreadex. Плюс, я не могу знать, вызывают ли те WinAPI функции или не WinAPI функции, которые я могу использовать, внутренне CRT функции. Или вообще, чья-то сторонняя библиотеке, которую я могу подгрузить с Гитхаба, или еще откуда. Зачем нужно беспокойство об этом.

     

    20 минут назад, imaginary сказал:

    А ты в курсе что в итоге всё равно вызывается CreateThread?

    Да, об этом я читал. _beginthreadex, создает структуру tiddata для CRT, затем вызывает CreateThread. Структура нужна для работы с CRT.

    Но что нам дает это знание, что все равно вызывается CreateThread?

    Я почему против CreateThread то, - потому что по словам Рихтера, поток, созданный с ее помощью, не работает с CRT. Вот и вся причина. А так, это единственная функция в Windows, которая способна запустить поток.

     

    28 минут назад, imaginary сказал:

    Если уж так трясёшься не использовать CreateThread что бы ты его не видел у себя в коде, значит не надо выгружать библиотеку, один раз загрузить и будет находиться в процессе, пока не закроют его.

    Нет, так не пойдет. Моя задача докопаться до истины, как бы это не звучало странно.

    Этот вопрос должен быть закрыт раз и навсегда. Раз я уж взялся за него. Я должен уметь выгружать библиотеку правильно.

     

    Для того я и поднял этот вопрос. Обсудить его, прояснить его для себя и нас.

  18. Не понял некоторых твоих слов.

    По поводу CreateThread, вот выписка с MSDN Remark CreateThread

    Спойлер

    A thread in an executable that calls the C run-time library (CRT) should use the _beginthreadex and _endthreadex functions for thread management rather than CreateThread and ExitThread; this requires the use of the multithreaded version of the CRT. If a thread created using CreateThread calls the CRT, the CRT may terminate the process in low-memory conditions.

    Так что никаких CreateThread. Только _beginthreadex. Более того, даже std::thread использует _beginthreadex.

     

    Дальше, по логике, так как поток мы создаем через функцию _beginthreadex,то и выход должен быть через соответствующую ей функцию _endthreadex, которая вызывается неявно когда поток возвращает управление через свой return. Но при вызове FreeLibraryAndExitThread вызывается ExitThread, а не _endthreadex, соответственно блок памяти потока tiddata не освобождается, это первая утечка. Вторая утечка, произойдет если поток не выйдет через свой return, а выйдет раньше, примеры кода я тебе написал выше.

     

    Также, DLL не выгрузится, если поток который впоследствии вызовет FreeLibraryAndExitThread, был создан через _beginthreadex. Да это и неправильно, как я уже написал.

     

    Я загружаю DLL через свою EXE, через CreateRemoteThread

    Спойлер
    VOID injectDll(DWORD processID, CONST std::wstring& filePath) {
    	BOOL isDllInjected{ FALSE };
    	HMODULE kernell32 = GetModuleHandleW(L"kernel32.dll");
    	if (kernell32) {
    		LPTHREAD_START_ROUTINE loadLibWAddress = reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(kernell32, "LoadLibraryW"));
    		HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
    		if (loadLibWAddress && process) {
    			DWORD pathSize = filePath.size() * sizeof(WCHAR);
    			LPVOID allocForDLLPath = VirtualAllocEx(process, NULL, pathSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    			if (allocForDLLPath) {
    				WriteProcessMemory(process, allocForDLLPath, filePath.c_str(), pathSize, NULL);
    				HANDLE remoteThread = CreateRemoteThread(process, NULL, 0, loadLibWAddress, allocForDLLPath, 0, NULL);
    				if (remoteThread) {
    					WaitForSingleObject(remoteThread, INFINITE);
    					CloseHandle(remoteThread);
    				}
    				isDllInjected = TRUE;
    				VirtualFreeEx(process, allocForDLLPath, 0, MEM_RELEASE);
    			}
    		}
    		if (process) {
    			CloseHandle(process);
    		}
    	}
    	if (!isDllInjected) {
    		std::wstring errorNotify = L"Inject DLL failed";
    		mainWindow->setNotifyMessage(errorNotify);
    		mainWindow->invalidateFullRectWithErase();
    	}
    	else {
    		std::wstring errorNotify = L"DLL injected \n You can close this window";
    		mainWindow->setNotifyMessage(errorNotify);
    		mainWindow->invalidateFullRectWithErase();
    	}
    }

     

    В DLL

    Спойлер
    BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
        switch (fdwReason) {
        case DLL_PROCESS_ATTACH:
            createThreadGUI(hinstDLL);
            break;
        }
        return TRUE;
    }

     

    createThreadGUI()

    Спойлер
    HINSTANCE instanceDll{ NULL };
    HANDLE dllGUIThreadID{ NULL };
    
    
    VOID createThreadGUI(HINSTANCE hinstDLL) {
    	instanceDll = hinstDLL;
    	dllGUIThreadID = reinterpret_cast<HANDLE>(_beginthreadex(NULL, 0, threadProcedure, NULL, 0, NULL));
    	if (dllGUIThreadID) {
    		CloseHandle(dllGUIThreadID);
    	}
    }

     

    threadProcedure

    Спойлер
    unsigned WINAPI threadProcedure(LPVOID param) {
    	//Разный рабочий код.
    	//Выход должен быть через return 0;
    	return 0;
    }

     

     

     

  19. 24 минуты назад, imaginary сказал:

    Да какая разница, он в любом случае выходит в ExitThread в итоге, возвращается в одно место, что через return, что так, что разводить полемику на пустом месте

    Разница есть, в утечке памяти.

     

    Сравни

    Спойлер
    #include <windows.h>
    #include <stdio.h>
    
    class CSomeObj {
    public:
    	CSomeObj() { printf("Constructor\r\n"); }
    	~CSomeObj() { printf("Destructor\r\n"); }
    };
    
    CSomeObj g_GlobalObj;
    
    int main() {
    	CSomeObj LocalObj;
    	ExitThread(0);
    	return 0;
    }

     

    с этим.

    Спойлер
    #include <windows.h>
    #include <stdio.h>
    
    class CSomeObj {
    public:
    	CSomeObj() { printf("Constructor\r\n"); }
    	~CSomeObj() { printf("Destructor\r\n"); }
    };
    
    CSomeObj g_GlobalObj;
    
    int main() {
    	CSomeObj LocalObj;
    	//ExitThread(0);
    	return 0;
    }

     

     

    Первый код выведет,

    Спойлер

    Constructor
    Constructor

    второй код выведет.

    Спойлер

    Constructor
    Constructor
    Destructor
    Destructor

     

    Но это мы рассмотрели только объекты нами созданных классов. А помимо них есть еще и другие объекты классов, в библиотеках С++. Да и кто еще знает, что там еще есть, что еще очищается при выходе через естественный return.

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

  20. 23 минуты назад, Xipho сказал:

    Как мне кажется, ты сильно подвержен явлению под названием "давление авторитета". Почитай, что это такое, и почему это плохо.

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

     

    15 минут назад, Xipho сказал:

    А может, Рихтер как раз имел в виду не прямой вызов ExitThread, а вызов через FreeeLibraryAndExitThread?

    Не понял этого предложения.

    Прямой или не прямой вызов ExitThread, - в любом случае, при ExitThread, поток уже не выйдет через return своей процедуры.

  21. 19 минут назад, Xipho сказал:

    Ну ок. Что мешает в потоке сделать вечный цикл типа while(isRunning), а в DLL_PROCESS_DETACH выставить isRunning в false?

    Мешает race condition. Ведь -

     

    - я вызываю FreeLibrary из потока, reference count для DLL уменьшается, становится 0. Система готова выгрузить DLL, но, вначале идет вызов DllMain

    - вызвается DllMain с DLL_PROCESS_DETACH. Всё, система решается выгрузить DLL. И выгружает, допустим. Но у нас же там еще поток не завершен, и плюс мы туда еще должны вернуться, из FreeLibrary(). А куда возвращаться, кода DLL уже нет в памяти.

     

    Это из MSDN Remak для FreeLibraryAndExitThread

    Спойлер

    The FreeLibraryAndExitThread function allows threads that are executing within a DLL to safely free the DLL in which they are executing and terminate themselves. If they were to call FreeLibrary and ExitThread separately, a race condition would exist. The library could be unloaded before ExitThread is called.

     

    А это из книги Рихтера

    Спойлер

    Если поток станет сам вызывать FreeLibrary и ExitThread, возникнет очень серьез-
    ная проблема: FreeLibrary тут же отключит DLL от адресного пространства процесса.
    После возврата из FreeLibrary код, содержащий вызов ExitThread, окажется недосту-
    пен, и поток попытается выполнить не известно что. Это приведет к нарушению до-
    ступа и завершению всего процесса!
    С другой стороны, если поток обратится к FreeLibraryAndExitThread, она вызовет
    FreeLibrary, и та сразу же отключит DLL. Но следующая исполняемая инструкция на-
    ходится в Kernel32.dll, а не в только что отключенной DLL. Значит, поток сможет про-
    должить выполнение и вызвать ExitThread, которая корректно завершит его, не воз-
    вращая управления.

     

    Не зря придумана FreeLibraryAndExitThread. Но, она нам не подходит, как я написал уже.

    Она вызывает ExitThread, это плохо, по Рихтеру. Плюс она вызывает ExitThread а не _endthreadex. Ведь мы должны запустить поток с _beginthreadex, и завершить его мы должны соответственно с _endthreadex, чтобы очистить в потоке блок памяти tiddata.

     

  22. Потому что так пишет Рихтер.

    Как я написал в начале поста, Рихтер настоятельно не рекомендует самостоятельное завершение потока.

    Спойлер

    Явные вызовы ExitProcess и ExitThread — распространенная ошибка, которая
    мешает правильной очистке ресурсов. В случае ExitThread процесс продолжа-
    ет работать, но при этом весьма вероятна утечка памяти или других ресурсов.

     

    Поток можно завершить четырьмя способами:
    1 функция потока возвращает управление (рекомендуемый способ);
    2 поток самоуничтожается вызовом функции ExitThread (нежелательный способ);
    3 один из потоков данного или стороннего процесса вызывает функцию Termi-
    nateThread (нежелательный способ);
    4 завершается процесс, содержащий данный поток (тоже нежелательно).

     

    Далее, по логике Рихтера, так как API функция FreeLibraryAndExitThread, вызывает ExitThread, - значит этот вариант выгрузки DLL отпадает. Тем более этот вариант отпадает, что согласно моему тесту, FreeLibraryAndExitThread, вызванная из потока запущенного через _beginthreadex, не выгружает библиотеку! Даже не вызывается DLL_PROCESS_DETACH.

    А вот если FreeLibraryAndExitThread вызвана из потока запущенного через CreateThread, тогда библиотека выгружается.

    Но по совету того же Рихтера, нужно использовать всегда _beginthreadex.

    Спойлер

    CreateThread — это Windows-функция, создающая поток. Но никогда не вы-
    зывайте ее, если Вы пишете код на C/C++. Вместо нее Вы должны использо-
    вать функцию _beginthreadex из библиотеки Visual C++. (Если Вы работаете с
    другим компилятором, он должен поддерживать свой эквивалент функции
    CreateThread.) Что именно делает _beginthreadex и почему это так важно, я
    объясню потом.

     

    Значит, нужно использовать только _beginthreadex и FreeLibrary. Но на MSDN пишут

    Спойлер

    A thread that must unload the DLL in which it is executing and then terminate itself should call FreeLibraryAndExitThread instead of calling FreeLibrary and ExitThread separately. Otherwise, a race condition can occur. For details, see the Remarks section of FreeLibraryAndExitThread.

    Получается расхождение.

     

    1 час назад, Xipho сказал:

    Почему библиотека не может сама убить свой поток в секции DLL_PROCESS_DETACH?

    Потому что Рихтер так пишет. Поток должен выйти через свой return 0. Если завершить поток в DLL_PROCESS_DETACH, он уже не вернется в свою процедуру, и не вызовет деструкторы и прочие подобные вещи, который компилятор встраивает в конце процедуры.

     

    Итог, нужно соблюсти следующие условия.

    1 - поток запускается только через _beginthreadex

    2 - DLL выгружается только через FreeLibrary

    3 - поток завершается только сам, возвращая управление через return 0.

  23. Придумал вариант получше. Никаких дополнительных DLL.

    Ехе файл инжектора вначале инжектит основную DLL, и записывает в специальную её переменную свой путь на диске. Далее Exe файл завершает свою работу.

    Затем, когда нужно выгрузить DLL, эта DLL находит по переданному ей пути Exe файл инжектора, и запускает его с параметром Unload. Далее Exe делает все то же что и ранее делала дополнительная DLL. То есть Exe выгружает DLL, а затем и сам завершается.

     

    Если DLL не найдет Exe по указанному пути, или если процесс в который была заинжектена DLL будет намерен завершиться, то Exe не будет запускаться.

     

×
×
  • Создать...

Важная информация

Находясь на нашем сайте, Вы автоматически соглашаетесь соблюдать наши Условия использования.