Как на Ассемблере сделать скриншот отдельного окна
Одна из интересных задач при работе с окнами - захват и сохранение скриншота выбранного окна или всего экрана целиком. Во многих руководствах по программированию для этого рекомендуют использовать функцию BitBlt.Code (Assembler) : Убрать нумерацию
- ; Захват отдельного окна через BitBlt
- invoke GetWindowDC,[hWnd]
- mov [windowDC],eax
- invoke CreateCompatibleDC,[windowDC]
- mov [newDC],eax
- ; Создать пустой битмап для скриншота
- invoke CreateCompatibleBitmap,[windowDC],[window_width],[window_height]
- mov [hBitmap],eax
- invoke SelectObject,[newDC],[hBitmap]
- ; Флаг для захвата полупрозрачных окон
- CAPTUREBLT = 0x40000000
- invoke BitBlt,[newDC],0,0,[window_width],[window_height],[windowDC],\
- 0,0,SRCCOPY+CAPTUREBLT
- ; Теперь в [hBitmap] находится изображение (Bitmap) окна
Чтобы было понятно, о чем идет речь, вот пример такого скриншота. Окно Блокнота сдвинуто за границы экрана и частично перекрыто другим окном.
Захват окна с помощью BitBlt
Но, к счастью, в закромах Родины нашлась замечательная книга китайского программиста Фень Юаня "Программирование графики для Windows" (2002). Это издание без преувеличения можно назвать настольной книгой для тех, кто решил серьезно заняться графикой в Windows. А также нашелся его метод захвата, не зависящий от перекрытия или местоположения окна на экране. В своей статье Фень Юань использует отправку сообщений WM_PRINT и WM_PRINTCLIENT захватываемому окну. В современных системах появилась более удобная штатная функция WinAPI PrintWindow, которая выполняет аналогичные действия. С помощью этой функции можно легко сделать правильный скриншот практически любого окна, даже если оно полностью скрыто под другими окнами или находится за пределами экрана.
Code (Assembler) : Убрать нумерацию
- ; Захват отдельного окна через PrintWindow
- invoke GetWindowDC,[hWnd]
- mov [windowDC],eax
- invoke CreateCompatibleDC,[windowDC]
- mov [newDC],eax
- ; Создать пустой битмап для скриншота
- invoke CreateCompatibleBitmap,[windowDC],[window_width],[window_height]
- mov [hBitmap],eax
- invoke SelectObject,[newDC],[hBitmap]
- ; Захват содержимого окна
- invoke PrintWindow,[hWnd],[newDC],0
- ; Теперь в [hBitmap] находится изображение (Bitmap) окна
Захват окна с помощью PrintWindow
Осталось объединить оба способа в одну функцию и добавить в нее сохранение полученного битмапа в файл. Пример сохранения изображения в файл при помощи GDI+ я уже описывал в одной из предыдущих статей. Имя файла, в который будет сохранен готовый скриншот окна, обязательно должно быть записано в юникоде. Например:
Code (Assembler) : Убрать нумерацию
- ; Имя файла, в который будет сохранен скриншот
- file_name du 'screenshot.png',0
Code (Assembler) : Убрать нумерацию
- ;----------------------------------------------------------------------
- ; Функция создания скриншота отдельного окна или всего экрана
- ; by ManHunter / PCL
- ; http://www.manhunter.ru
- ;----------------------------------------------------------------------
- ; Параметры:
- ; hWnd - хэндл окна
- ; szFileName - имя файла, в который будет сохранен скриншот (PNG)
- ; dType - способ захвата (0 = PrintWindow, 1 = BitBlt)
- ;----------------------------------------------------------------------
- proc capture_window hWnd:DWORD, szFileName:DWORD, dType:DWORD
- ; Структура для работы с GDI+
- struct _GdiplusStartupInput
- GdiplusVersion dd ?
- DebugEventCallback dd ?
- SuppressBackgroundThread dd ?
- SuppressExternalCodecs dd ?
- ends
- ; Структура для работы с установленным кодеками изображений
- struct _ImageCodecInfo
- Clsid db 16 dup ?
- FormatID db 16 dup ?
- CodecName dd ?
- DllName dd ?
- FormatDescription dd ?
- FilenameExtension dd ?
- MimeType dd ?
- Flags dd ?
- Version dd ?
- SigCount dd ?
- SizeSize dd ?
- SigPattern dd ?
- SigMask dd ?
- ends
- ; Локальные переменные
- locals
- result dd ?
- windowDC dd ?
- newDC dd ?
- hBitmap dd ?
- encoders_count dd ?
- encoders_size dd ?
- window_width dd ?
- window_height dd ?
- memdc dd ?
- hHeap dd ?
- input dd ?
- token dd ?
- gdip_bitmap dd ?
- encoder_clsid db 16 dup ?
- rc RECT
- endl
- pusha
- mov [result],0
- ; Такое окно вообще существует?
- cmp [hWnd],HWND_DESKTOP
- je @f
- invoke IsWindow,[hWnd]
- or eax,eax
- jz .exit
- @@:
- ; Структура для работы с GDI+
- invoke GetProcessHeap
- mov [hHeap],eax
- invoke HeapAlloc,[hHeap],HEAP_ZERO_MEMORY,sizeof._GdiplusStartupInput
- mov edi,eax
- mov [input],eax
- mov [eax+_GdiplusStartupInput.GdiplusVersion],1
- lea eax,[token]
- invoke GdiplusStartup,eax,[input],NULL
- test eax,eax
- jnz .memory_free
- ; Найти подходящий кодек, в нашем случае это PNG
- lea eax,[encoders_size]
- push eax
- lea eax,[encoders_count]
- push eax
- invoke GdipGetImageEncodersSize
- test eax,eax
- jnz .gdiplus_shutdown
- invoke VirtualAlloc,0,[encoders_size],MEM_COMMIT,PAGE_READWRITE
- test eax,eax
- jz .gdiplus_shutdown
- mov ebx,eax
- invoke GdipGetImageEncoders,[encoders_count],[encoders_size],ebx
- test eax,eax
- jnz .gdiplus_shutdown
- .scan_encoders:
- mov esi,[ebx+_ImageCodecInfo.MimeType]
- mov edi,encoder_mimetype
- mov ecx,e_len shr 1
- repe cmpsw
- je .encoder_found
- add ebx,sizeof._ImageCodecInfo
- dec [encoders_count]
- jnz .scan_encoders
- jmp .gdiplus_shutdown
- .encoder_found:
- lea esi,[ebx+_ImageCodecInfo.Clsid]
- lea edi,[encoder_clsid]
- mov ecx,4
- rep movsd
- invoke VirtualFree,ebx,0,MEM_RELEASE
- ; Захват всего экрана?
- cmp [hWnd],HWND_DESKTOP
- jne @f
- invoke GetDC,[hWnd]
- mov [windowDC],eax
- invoke GetSystemMetrics,SM_CYSCREEN
- mov [window_height],eax
- invoke GetSystemMetrics,SM_CXSCREEN
- mov [window_width],eax
- jmp .create_bitmap
- @@:
- ; Захват отдельного окна
- invoke GetWindowDC,[hWnd]
- mov [windowDC],eax
- ; Получить размеры окна
- lea eax,[rc]
- invoke GetWindowRect,[hWnd],eax
- ; Window Height
- mov eax,[rc.bottom]
- sub eax,[rc.top]
- mov [window_height],eax
- ; Window Width
- mov eax,[rc.right]
- sub eax,[rc.left]
- mov [window_width],eax
- .create_bitmap:
- invoke CreateCompatibleDC,[windowDC]
- mov [newDC],eax
- ; Создать пустой битмап для скриншота
- invoke CreateCompatibleBitmap,[windowDC],[window_width],[window_height]
- mov [hBitmap],eax
- invoke SelectObject,[newDC],[hBitmap]
- ; Для HWND_DESKTOP всегда использовать BitBlt
- cmp [hWnd],HWND_DESKTOP
- je .capture_bitblt
- ; Для Desktop Window всегда использовать BitBlt
- invoke GetDesktopWindow
- cmp [hWnd],eax
- je .capture_bitblt
- ; Тип захвата окна
- ; 0 - PrintWindow
- ; 1 - BitBlt
- cmp [dType],1
- je .capture_bitblt
- ;-------------------------------------------------------------------
- ; PrintWindow
- ;-------------------------------------------------------------------
- .capture_printwindow:
- ; Захват содержимого окна
- invoke PrintWindow,[hWnd],[newDC],0
- jmp .save_image
- ;-------------------------------------------------------------------
- ; BitBlt
- ;-------------------------------------------------------------------
- .capture_bitblt:
- ; Флаг для захвата полупрозрачных окон
- CAPTUREBLT = 0x40000000
- invoke BitBlt,[newDC],0,0,[window_width],[window_height],[windowDC],\
- 0,0,SRCCOPY+CAPTUREBLT
- test eax,eax
- jz .delete_bitmap
- .save_image:
- ; Сохранить изображение в файл
- lea eax,[gdip_bitmap]
- invoke GdipCreateBitmapFromHBITMAP,[hBitmap],NULL,eax
- test eax,eax
- jnz .delete_bitmap
- lea eax,[encoder_clsid]
- invoke GdipSaveImageToFile,[gdip_bitmap],[szFileName],eax,NULL
- invoke GdipDisposeImage,[gdip_bitmap]
- mov [result],1
- .delete_bitmap:
- invoke DeleteObject,[hBitmap]
- .delete_dc:
- invoke DeleteDC,[newDC]
- .release_window_dc:
- invoke ReleaseDC,[hWnd],[windowDC]
- .gdiplus_shutdown:
- invoke GdiplusShutdown,[token]
- .memory_free:
- invoke HeapFree,[hHeap],0,[input]
- .exit:
- popa
- mov eax,[result]
- ret
- ; Используемый кодек для сохранения скриншота
- encoder_mimetype du 'image/png',0
- e_len=$-encoder_mimetype
- endp
Почему не использовать захват окна только по методу Фень Юаня с использованием функции PrintWindow? Дело в том, что при захвате всего экрана (хэндл окна равен HWND_DESKTOP), фактически делается не скриншот экрана, а скриншот голых обоев рабочего стола. Поэтому в функцию введена дополнительная проверка, и, в случае захвата всего экрана, принудительно используется метод с BitBlt. К тому же бывают ситуации, когда надо действительно сделать снимок окна с учетом перекрывающих его окон других приложений, с дочерними окнами, с курсором в окне или еще как-то так. В этом случае тоже надо использовать захват окна через BitBlt. В остальных случаях рекомендуется использовать метод захвата окна через PrintWindow.
В приложении пример программы, делающей скриншоты окна тем или другим способом по его хэндлу. Хэндл окна можно посмотреть, например, программой WinDowzer.
Просмотров: 4577 | Комментариев: 8
Внимание! Статья опубликована больше года назад, информация могла устареть!
Комментарии
Отзывы посетителей сайта о статье
ManHunter
(06.01.2016 в 22:52):
Поправил удаление хэндлов, дописал проверку на GetDesktopWindow. Аттач и текст обновил. Спасибо!
Лори
(06.01.2016 в 22:41):
Сначала следует удалять битмап, а после него контекст, и контекст не через DeleteObject, а через DeleteDC удаляется.
ManHunter
(05.01.2016 в 21:56):
Почему? Освободили контекст, освободили битмап.
Надо пробовать, меня пока устраивает HWND_DESKTOP
Лори
(05.01.2016 в 21:31):
Очень круто! Спасибо!
Вот только DeleteObject немного не в том порядке идёт.
И где-то читал мол WM_PRINT работает только для окон вызывающего потока, потому чужое окно через это так просто не взять. Надо инжектить.
А эта PrintWindow сама работает через WM_PRINT, но уже для любого окна (сама ОС делает всё что надо).
Но вроде в Win10 что-то поменялось и появились проблемы с этим. Надо будет изучить.
Ой, забыл сказать.
HWND_DESKTOP и GetDesktopWindow в некоторых ситуациях/случаях различаются, и возможно корректнее будет получать значение из функции, не?
Или и то и то отправлять на .capture_bitblt
Вот только DeleteObject немного не в том порядке идёт.
И где-то читал мол WM_PRINT работает только для окон вызывающего потока, потому чужое окно через это так просто не взять. Надо инжектить.
А эта PrintWindow сама работает через WM_PRINT, но уже для любого окна (сама ОС делает всё что надо).
Но вроде в Win10 что-то поменялось и появились проблемы с этим. Надо будет изучить.
Ой, забыл сказать.
HWND_DESKTOP и GetDesktopWindow в некоторых ситуациях/случаях различаются, и возможно корректнее будет получать значение из функции, не?
Или и то и то отправлять на .capture_bitblt
ManHunter
(09.11.2015 в 18:16):
WM_PRINT в чистом виде тоже не панацея, приложение должно уметь обрабатывать это сообщение, или придется инжектить что-то в чужой процесс. Так что остановлюсь на PrintWindow.
kero
(09.11.2015 в 17:16):
Да не за что.
Когда-то сам копался в теме, и могу добавить, что действие API PrintWindow и WM_PRINT - не так уж и аналогичны: снимок скрытого (SW_HIDE) окна через PrintWindow - это черный рект, а через WM_PRINT - (почти) точный скриншот.
Есть и еще кое-какие возможности (см. набросок http://files.rsdn.ru/42164/printlayered.zip ), а начиная с Висты есть и др. полезные API. Однако универсального решения на все случаи окон вроде бы нема.
Когда-то сам копался в теме, и могу добавить, что действие API PrintWindow и WM_PRINT - не так уж и аналогичны: снимок скрытого (SW_HIDE) окна через PrintWindow - это черный рект, а через WM_PRINT - (почти) точный скриншот.
Есть и еще кое-какие возможности (см. набросок http://files.rsdn.ru/42164/printlayered.zip ), а начиная с Висты есть и др. полезные API. Однако универсального решения на все случаи окон вроде бы нема.
ManHunter
(09.11.2015 в 11:46):
Да, все правильно. Подкорректировал текст, спасибо!
kero
(09.11.2015 в 11:34):
Привет.
PrintWindow у Фень Юаня в его статье 2000 года - не API PrintWindow (которой в Win2k и не было), а собственная процедура с тем же названием...
PrintWindow у Фень Юаня в его статье 2000 года - не API PrintWindow (которой в Win2k и не было), а собственная процедура с тем же названием...
Добавить комментарий
Заполните форму для добавления комментария