Делаем простое удержание курсора в окне Warcraft 3

Приветствую тебя, читатель. У меня есть хобби — это старый добрый Warcraft 3. На хабре уже был цикл статей, посвященный этой замечательной игре. Хочу поделиться с комьюнити одной утилитой, пригодившейся мне при проведении стримов. Всех заинтересовавшихся прошу пройти под кат.

Предисловие

Все началось с того, что в один из выходных на фоне непрекращающегося ремонта я решил посмотреть стрим по Warcraft III. Площадок на данный момент достаточно, но мои предпочтения относятся к сайту www.goodgame.ru (не реклама). Был разочарован, что ничего интересного на тот момент не транслировалось. И тогда возникла мысль — почему бы не сделать свой стрим с блэкджэком и т.д.

Сопутствующее ПО

Для проведения трансляции, кроме всего прочего, потребуется приложение для захвата контента. На данный момент можно выделить два из них: xsplit и openbroadcaster. Честно скажу, первым не пользовался. В бесплатной версии доступен базовый функционал. Но для скачивания базовой версии придется пройти обязательную регистрацию (не то что бы это было проблемой, но...). Ко второму варианту склонила лицензия GPL и соответственно доступность исходного кода. На openbroadcaster я и остановился.

Трудности

С установкой и настройкой OBS проблем не возникло. Но запущенная игра никак не хотела захватываться в рекомендованном режиме Game capture (вероятно это связано с использованием старой версии directx при разработке игры). Поигравшись с другими режимами захвата, удалось найти два, которые обеспечивали необходимое поведение — Monitor capture и Window capture.
Первый достаточно сильно аффектит перформанс. Ощущается во время игры. Но это был рабочий вариант, что называется «из коробки».
Второй вариант приводил к дискомфорту в процессе игры — курсор постоянно выходил за границы окна. В общем, было абсолютно неиграбельно.

Решение

Был выбран второй вариант и принято решение написать утилиту для устранения описанного выше дискомфорта.
Изначально Warcraft III запускается в полноэкраном режиме.
Для запуска в оконном режиме необходимо использовать ключ "-window" в команде запуска приложения, это как раз позволит выполнить захват в режиме Windows capture.

Для удержания курсора в рамках клиентской области окна была написана первая версия утилиты. Основной цикл ее работы приведен ниже:

/* polling version */
void Controller::RunPollingLoop()
{		
	while (true)
	{
		HWND activeWindow		= GetForegroundWindow();
		HWND requiredWindow		= FindRequiredWindow(m_className, m_winTitle, 5);

		if (requiredWindow == NULL)
			throw std::runtime_error("Required window not found");
		
		m_fullScreen.Init(requiredWindow);
		m_clipHelper.Init(requiredWindow);

		if (activeWindow == requiredWindow)
		{
			if (m_clipHelper.IsClipped() || !CursorInClientArea(requiredWindow))
			{
				Sleep(g_SleepTimeOut);
				continue;
			}

			if (m_fullScreen.Enter()) 
			{	
				DEBUG_TRACE("EnterFullscreen success"); 
				m_clipHelper.Clip();
				DEBUG_TRACE("Clip");
			}
			else
			{	DEBUG_TRACE("EnterFullscreen failed"); }
		}
		else
		{
			if (m_clipHelper.IsClipped())
			{
				if (m_fullScreen.Leave())
				{ DEBUG_TRACE("LeaveFullscreen success"); }
				else
				{ DEBUG_TRACE("LeaveFullscreen failed"); }

				m_clipHelper.UnClip();
				DEBUG_TRACE("UnClip");
			}

			Sleep(g_SleepTimeOut);
		}
	}
}


Здесь используется вспомогательный класс ClipHelper для управления процессом удержания курсора и класс FullScreen для управления процессом перехода в полноэкранный режим и восстановления из него. Сам цикл реализует алгоритм поллинга активного окна с таймаутом в 500 мс. Этот момент мне не понравился сразу, но для движения дальше требовалось проверить всю концепцию, а потом заняться оптимизацией.

В процессе использования утилиты сразу возникли следующие хотелки:
— Clip проводить только в случае клика (удержания для поллинг версии) по клиентской области, чтобы иметь возможность перетаскивать окно;
— раздражал вид taskbar во время игры (актуально, если она зафиксирована). Первой мыслью было скрыть ее программно. Но в таком случае необходимо было бы отслеживать моменты выхода пользователя из игры и показывать taskbar обратно. Повышался риск оставить пользователя без панели задач. Поэтому реализацию fullscreen я решил сделать изменением размеров игрового окна до размеров разрешения монитора, за которым это окно закреплено:

bool FullScreen::Enter()
{
	if (m_fullScreen)
		return true;

	assert(m_hwnd);
	if (m_hwnd == NULL)
		return false;
	
	HMONITOR hmon = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST);
	MONITORINFO mi = { sizeof(mi) };

	if (!GetMonitorInfo(hmon, &mi)) 
		return false;

	if (!GetWindowRect(m_hwnd, &m_origWindowRect))
	{
		SecureZeroMemory(&m_origWindowRect, sizeof(m_origWindowRect));
		return false;
	}

	if (!SetWindowPos(m_hwnd, HWND_TOPMOST, 
					   mi.rcMonitor.left,
					   mi.rcMonitor.top,
					   mi.rcMonitor.right - mi.rcMonitor.left,
					   mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_SHOWWINDOW))
		return false;

	m_fullScreen = true;
	
	return true;
}


Оптимизация

Во второй версии утилиты поллинг активного окна был заменен хуком сообщений WM_ACTIVATE и WM_LBUTTONDOWN. Для этого я использовал два типа хуков: WH_CALLWNDPROC и WH_MOUSE. Суть в том, что мы отслеживаем требуемые события игрового окна и уведомляем нашу утилиту через окно-сервер. Хук вешался только для процесса игры. Таким образом, игра должна быть запущена до утилиты:

BOOL SetWinHook(HWND hWnd, DWORD threadId)
{
	if (g_hWndSrv != NULL)
		return FALSE; //already hooked
	
	g_hCallWndHook = SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)CallWndHookProc, g_hInst, threadId);
	if (g_hCallWndHook != NULL)
	{ 
		g_hMouseHook = SetWindowsHookEx(WH_MOUSE, (HOOKPROC)MouseHookProc, g_hInst, threadId);
		if (g_hMouseHook != NULL)
		{
			g_hWndSrv = hWnd;
			return TRUE;
		}
		ClearWinHook();
	}

	return FALSE;
}

А основной цикл работы свелся к следующей процедуре:

LRESULT CALLBACK Controller::MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	if (uMsg == WM_ACTIVATE) 
    { 
		switch (wParam)
		{
		case WA_ACTIVE:
			DEBUG_TRACE("WA_ACTIVE");
			gs_ActivateClip = true;
			break;
		case WA_CLICKACTIVE:
			DEBUG_TRACE("WA_CLICKACTIVE");
			gs_ActivateClip = true;
			break;
		case WA_INACTIVE:
			DEBUG_TRACE("WA_INACTIVE");
			gs_ActivateClip = false;
			if (g_ControllerPtr->ClipCursorHelper().IsClipped())
			{
				if (g_ControllerPtr->FullScreenHelper().Leave())
				{ DEBUG_TRACE("LeaveFullscreen success"); }
				else
				{ DEBUG_TRACE("LeaveFullscreen failed"); }

				g_ControllerPtr->ClipCursorHelper().UnClip();
				DEBUG_TRACE("UnClip");
			}
			break;
		}
		return 0;
	}
	else if (uMsg == WM_LBUTTONDOWN)
	{
		DEBUG_TRACE("WM_LBUTTONDOWN");
		
		if (!gs_ActivateClip)
			return 0;

		if (g_ControllerPtr->ClipCursorHelper().IsClipped())
			return 0;

		if (g_ControllerPtr->FullScreenHelper().Enter()) 
		{	
			DEBUG_TRACE("EnterFullscreen success"); 
			g_ControllerPtr->ClipCursorHelper().Clip();
			DEBUG_TRACE("Clip");
		}
		else
		{	DEBUG_TRACE("EnterFullscreen failed"); }
		
		return 0;
	}
	
    return DefWindowProc(hwnd, uMsg, wParam, lParam); 
}

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

Послесловие

Была разработана утилита, призванная сделать процесс стрима любимой игры более комфортным, чем предлагаемый рабочий вариант «из коробки». Буду рад, если кто-то почерпнет для себя что-то интересное. Весь исходный код залит на github WinClipCursor.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 9

    0
    А не лучше ли было сделать псевдополноэкранный режим? То есть просто растягуть окно поверх всего включая панель задач.
      +1
      А он собственно именно так и сделан.
        –1
        А на кой чёрт тогда вообще вся возня с мышью?
          +2
          а если у пользователя несколько мониторов?
            0
            какая возня имеется в виду?
        +3
        Можно упростить. Вам не нужна DLL с хуком.

        — для отслеживания активного приложения можно пользоваться RegisterShellHookWindow и ловить событие HSHELL_WINDOWACTIVATED.
        — чтобы поймать мышь используйте SetWindowsHookEx(WH_MOUSE_LL, HookProc, nullptr, 0); Lowl-lewel mouse hook, установленный таким образом, будет вызывается в контексте вашего процесса, не важно в каком процессе событие произошло. MSDN says: «This hook is called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.»
        После этих изменений длл можно убирать.

        И еще по мелочи:

        — вам не нужно хранить хэндл хука, в глобальных переменных, CallNextHookEx можно вызывать c nullptr в качестве первого параметра
        — лучше использовать nullptr вместо NULL, С++0x же
        — так луче не делать: PostMessage(g_hWndSrv, WM_ACTIVATE, swpStruct->wParam, swpStruct->lParam); У окна есть свои сообщения с такими кодами, и лучше не мешать их с чужими. Задефайнте что-нибудь кастомное WM_USER+ и посылайте его.
        — hWndSrv сделайте WS_CHILD of HWND_MESSAGE
        — пользуйтесь W-функциями, А-шки остались в ОС только для backward compatibility.
          0
          Спасибо.
          Добавлю чуть позже в солюшен отдельный проект для реализации low-level hook версии.
          Что касается мелочей, то вы правы, следует причесать код.
            0
            Не за что. Когда будете ll хук писать, из него тоже посылайте сообщение, хотя он и в вашем процессе. Дело в том, что код ll хука time-critical. Если он будет выполняться более 200ms, хук будет удален из chain'а хуков системой и никогда больше не вызовется.
            0
            На goodgame есть куча стримов по Warcraft 3 в записи.

            Only users with full accounts can post comments. Log in, please.