Blog. Just Blog

Как на Ассемблере сделать скриншот отдельного окна

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Одна из интересных задач при работе с окнами - захват и сохранение скриншота выбранного окна или всего экрана целиком. Во многих руководствах по программированию для этого рекомендуют использовать функцию BitBlt.
  1.         ; Захват отдельного окна через BitBlt
  2.         invoke  GetWindowDC,[hWnd]
  3.         mov     [windowDC],eax
  4.  
  5.         invoke  CreateCompatibleDC,[windowDC]
  6.         mov     [newDC],eax
  7.  
  8.         ; Создать пустой битмап для скриншота
  9.         invoke  CreateCompatibleBitmap,[windowDC],[window_width],[window_height]
  10.         mov     [hBitmap],eax
  11.  
  12.         invoke  SelectObject,[newDC],[hBitmap]
  13.  
  14.         ; Флаг для захвата полупрозрачных окон
  15.         CAPTUREBLT = 0x40000000
  16.         invoke  BitBlt,[newDC],0,0,[window_width],[window_height],[windowDC],\
  17.                 0,0,SRCCOPY+CAPTUREBLT
  18.         ; Теперь в [hBitmap] находится изображение (Bitmap) окна
Флаг CAPTUREBLT обеспечивает захват полупрозрачных окон с альфа-каналом. Способ реально рабочий, очень простой в реализации, но у него есть один огромный недостаток. Если окно, с которого требуется сделать снимок, перекрыто другими окнами или частично находится за пределами экрана, то оно так и будет сохранено с перекрывающими кусками чужих окон, а отсутствующая на экране область и вовсе будет заменена черным пятном.

Чтобы было понятно, о чем идет речь, вот пример такого скриншота. Окно Блокнота сдвинуто за границы экрана и частично перекрыто другим окном.

Захват окна с помощью BitBlt
Захват окна с помощью BitBlt

Но, к счастью, в закромах Родины нашлась замечательная книга китайского программиста Фень Юаня "Программирование графики для Windows" (2002). Это издание без преувеличения можно назвать настольной книгой для тех, кто решил серьезно заняться графикой в Windows. А также нашелся его метод захвата, не зависящий от перекрытия или местоположения окна на экране. В своей статье Фень Юань использует отправку сообщений WM_PRINT и WM_PRINTCLIENT захватываемому окну. В современных системах появилась более удобная штатная функция WinAPI PrintWindow, которая выполняет аналогичные действия. С помощью этой функции можно легко сделать правильный скриншот практически любого окна, даже если оно полностью скрыто под другими окнами или находится за пределами экрана.
  1.         ; Захват отдельного окна через PrintWindow
  2.         invoke  GetWindowDC,[hWnd]
  3.         mov     [windowDC],eax
  4.  
  5.         invoke  CreateCompatibleDC,[windowDC]
  6.         mov     [newDC],eax
  7.  
  8.         ; Создать пустой битмап для скриншота
  9.         invoke  CreateCompatibleBitmap,[windowDC],[window_width],[window_height]
  10.         mov     [hBitmap],eax
  11.  
  12.         invoke  SelectObject,[newDC],[hBitmap]
  13.  
  14.         ; Захват содержимого окна
  15.         invoke  PrintWindow,[hWnd],[newDC],0
  16.         ; Теперь в [hBitmap] находится изображение (Bitmap) окна
Вот то же самое окно из первого примера. Как видите, оно сохранено целиком, несмотря на сдвиг и перекрытие.

Захват окна с помощью PrintWindow
Захват окна с помощью PrintWindow

Осталось объединить оба способа в одну функцию и добавить в нее сохранение полученного битмапа в файл. Пример сохранения изображения в файл при помощи GDI+ я уже описывал в одной из предыдущих статей. Имя файла, в который будет сохранен готовый скриншот окна, обязательно должно быть записано в юникоде. Например:
  1. ; Имя файла, в который будет сохранен скриншот
  2. file_name du 'screenshot.png',0
В процессе написания функции создания скриншота выяснилась важная особенность. Структура GdiplusStartupInput, которая используется при инициализации GDI+ функцией GdiplusStartup, не должна находиться на стеке, под нее надо динамически выделять память или она должна быть описана в сегменте данных. Для большей самодостаточности функции создания скриншота я остановился на первом способе.
  1. ;----------------------------------------------------------------------
  2. ; Функция создания скриншота отдельного окна или всего экрана
  3. ; by ManHunter / PCL
  4. ; http://www.manhunter.ru
  5. ;----------------------------------------------------------------------
  6. ; Параметры:
  7. ;     hWnd - хэндл окна
  8. ;     szFileName - имя файла, в который будет сохранен скриншот (PNG)
  9. ;     dType - способ захвата (0 = PrintWindow, 1 = BitBlt)
  10. ;----------------------------------------------------------------------
  11. proc capture_window hWnd:DWORD, szFileName:DWORD, dType:DWORD
  12.  
  13. ; Структура для работы с GDI+
  14. struct _GdiplusStartupInput
  15.         GdiplusVersion           dd ?
  16.         DebugEventCallback       dd ?
  17.         SuppressBackgroundThread dd ?
  18.         SuppressExternalCodecs   dd ?
  19. ends
  20.  
  21. ; Структура для работы с установленным кодеками изображений
  22. struct _ImageCodecInfo
  23.         Clsid             db 16 dup ?
  24.         FormatID          db 16 dup ?
  25.         CodecName         dd ?
  26.         DllName           dd ?
  27.         FormatDescription dd ?
  28.         FilenameExtension dd ?
  29.         MimeType          dd ?
  30.         Flags             dd ?
  31.         Version           dd ?
  32.         SigCount          dd ?
  33.         SizeSize          dd ?
  34.         SigPattern        dd ?
  35.         SigMask           dd ?
  36. ends
  37.  
  38. ; Локальные переменные
  39. locals
  40.         result          dd ?
  41.         windowDC        dd ?
  42.         newDC           dd ?
  43.         hBitmap         dd ?
  44.         encoders_count  dd ?
  45.         encoders_size   dd ?
  46.         window_width    dd ?
  47.         window_height   dd ?
  48.         memdc           dd ?
  49.         hHeap           dd ?
  50.         input           dd ?
  51.         token           dd ?
  52.         gdip_bitmap     dd ?
  53.         encoder_clsid   db 16 dup ?
  54.         rc      RECT
  55. endl
  56.         pusha
  57.  
  58.         mov     [result],0
  59.  
  60.         ; Такое окно вообще существует?
  61.         cmp     [hWnd],HWND_DESKTOP
  62.         je      @f
  63.         invoke  IsWindow,[hWnd]
  64.         or      eax,eax
  65.         jz      .exit
  66. @@:
  67.         ; Структура для работы с GDI+
  68.         invoke  GetProcessHeap
  69.         mov     [hHeap],eax
  70.         invoke  HeapAlloc,[hHeap],HEAP_ZERO_MEMORY,sizeof._GdiplusStartupInput
  71.         mov     edi,eax
  72.         mov     [input],eax
  73.  
  74.         mov     [eax+_GdiplusStartupInput.GdiplusVersion],1
  75.         lea     eax,[token]
  76.         invoke  GdiplusStartup,eax,[input],NULL
  77.         test    eax,eax
  78.         jnz     .memory_free
  79.  
  80.         ; Найти подходящий кодек, в нашем случае это PNG
  81.         lea     eax,[encoders_size]
  82.         push    eax
  83.         lea     eax,[encoders_count]
  84.         push    eax
  85.         invoke  GdipGetImageEncodersSize
  86.         test    eax,eax
  87.         jnz     .gdiplus_shutdown
  88.         invoke  VirtualAlloc,0,[encoders_size],MEM_COMMIT,PAGE_READWRITE
  89.         test    eax,eax
  90.         jz      .gdiplus_shutdown
  91.         mov     ebx,eax
  92.         invoke  GdipGetImageEncoders,[encoders_count],[encoders_size],ebx
  93.         test    eax,eax
  94.         jnz     .gdiplus_shutdown
  95. .scan_encoders:
  96.         mov     esi,[ebx+_ImageCodecInfo.MimeType]
  97.         mov     edi,encoder_mimetype
  98.         mov     ecx,e_len shr 1
  99.         repe    cmpsw
  100.         je      .encoder_found
  101.         add     ebx,sizeof._ImageCodecInfo
  102.         dec     [encoders_count]
  103.         jnz     .scan_encoders
  104.         jmp     .gdiplus_shutdown
  105. .encoder_found:
  106.         lea     esi,[ebx+_ImageCodecInfo.Clsid]
  107.         lea     edi,[encoder_clsid]
  108.         mov     ecx,4
  109.         rep     movsd
  110.         invoke  VirtualFree,ebx,0,MEM_RELEASE
  111.  
  112.         ; Захват всего экрана?
  113.         cmp     [hWnd],HWND_DESKTOP
  114.         jne     @f
  115.  
  116.         invoke  GetDC,[hWnd]
  117.         mov     [windowDC],eax
  118.  
  119.         invoke  GetSystemMetrics,SM_CYSCREEN
  120.         mov     [window_height],eax
  121.         invoke  GetSystemMetrics,SM_CXSCREEN
  122.         mov     [window_width],eax
  123.         jmp     .create_bitmap
  124. @@:
  125.         ; Захват отдельного окна
  126.         invoke  GetWindowDC,[hWnd]
  127.         mov     [windowDC],eax
  128.  
  129.         ; Получить размеры окна
  130.         lea     eax,[rc]
  131.         invoke  GetWindowRect,[hWnd],eax
  132.  
  133.         ; Window Height
  134.         mov     eax,[rc.bottom]
  135.         sub     eax,[rc.top]
  136.         mov     [window_height],eax
  137.         ; Window Width
  138.         mov     eax,[rc.right]
  139.         sub     eax,[rc.left]
  140.         mov     [window_width],eax
  141.  
  142. .create_bitmap:
  143.         invoke  CreateCompatibleDC,[windowDC]
  144.         mov     [newDC],eax
  145.  
  146.         ; Создать пустой битмап для скриншота
  147.         invoke  CreateCompatibleBitmap,[windowDC],[window_width],[window_height]
  148.         mov     [hBitmap],eax
  149.  
  150.         invoke  SelectObject,[newDC],[hBitmap]
  151.  
  152.         ; Для HWND_DESKTOP всегда использовать BitBlt
  153.         cmp     [hWnd],HWND_DESKTOP
  154.         je      .capture_bitblt
  155.  
  156.         ; Для Desktop Window всегда использовать BitBlt
  157.         invoke  GetDesktopWindow
  158.         cmp     [hWnd],eax
  159.         je      .capture_bitblt
  160.  
  161.         ; Тип захвата окна
  162.         ; 0 - PrintWindow
  163.         ; 1 - BitBlt
  164.         cmp     [dType],1
  165.         je      .capture_bitblt
  166.  
  167.         ;-------------------------------------------------------------------
  168.         ; PrintWindow
  169.         ;-------------------------------------------------------------------
  170. .capture_printwindow:
  171.         ; Захват содержимого окна
  172.         invoke  PrintWindow,[hWnd],[newDC],0
  173.         jmp     .save_image
  174.  
  175.         ;-------------------------------------------------------------------
  176.         ; BitBlt
  177.         ;-------------------------------------------------------------------
  178. .capture_bitblt:
  179.         ; Флаг для захвата полупрозрачных окон
  180.         CAPTUREBLT = 0x40000000
  181.         invoke  BitBlt,[newDC],0,0,[window_width],[window_height],[windowDC],\
  182.                 0,0,SRCCOPY+CAPTUREBLT
  183.         test    eax,eax
  184.         jz      .delete_bitmap
  185.  
  186. .save_image:
  187.         ; Сохранить изображение в файл
  188.         lea     eax,[gdip_bitmap]
  189.         invoke  GdipCreateBitmapFromHBITMAP,[hBitmap],NULL,eax
  190.         test    eax,eax
  191.         jnz     .delete_bitmap
  192.  
  193.         lea     eax,[encoder_clsid]
  194.         invoke  GdipSaveImageToFile,[gdip_bitmap],[szFileName],eax,NULL
  195.  
  196.         invoke  GdipDisposeImage,[gdip_bitmap]
  197.         mov     [result],1
  198. .delete_bitmap:
  199.         invoke  DeleteObject,[hBitmap]
  200. .delete_dc:
  201.         invoke  DeleteDC,[newDC]
  202. .release_window_dc:
  203.         invoke  ReleaseDC,[hWnd],[windowDC]
  204. .gdiplus_shutdown:
  205.         invoke  GdiplusShutdown,[token]
  206. .memory_free:
  207.         invoke  HeapFree,[hHeap],0,[input]
  208. .exit:
  209.         popa
  210.         mov     eax,[result]
  211.         ret
  212.  
  213. ; Используемый кодек для сохранения скриншота
  214. encoder_mimetype du 'image/png',0
  215. e_len=$-encoder_mimetype
  216.  
  217. endp
Параметры функции: hWnd - хэндл окна, скриншот которого надо сделать, szFileName - указатель на имя файла, в которое будет сохранен скриншот (только PNG и строка в юникоде), dType - метод захвата: 0 - через PrintWindow, 1 - через BitBlt.

Почему не использовать захват окна только по методу Фень Юаня с использованием функции PrintWindow? Дело в том, что при захвате всего экрана (хэндл окна равен HWND_DESKTOP), фактически делается не скриншот экрана, а скриншот голых обоев рабочего стола. Поэтому в функцию введена дополнительная проверка, и, в случае захвата всего экрана, принудительно используется метод с BitBlt. К тому же бывают ситуации, когда надо действительно сделать снимок окна с учетом перекрывающих его окон других приложений, с дочерними окнами, с курсором в окне или еще как-то так. В этом случае тоже надо использовать захват окна через BitBlt. В остальных случаях рекомендуется использовать метод захвата окна через PrintWindow.

В приложении пример программы, делающей скриншоты окна тем или другим способом по его хэндлу. Хэндл окна можно посмотреть, например, программой WinDowzer.

Пример программы с исходным текстом (FASM)Пример программы с исходным текстом (FASM)

Screenshot.Demo.zip (4,869 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 4467 | Комментариев: 8

Метки: Assembler, окна
Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
ManHunter (06.01.2016 в 22:52):
Поправил удаление хэндлов, дописал проверку на GetDesktopWindow. Аттач и текст обновил. Спасибо!
Лори (06.01.2016 в 22:41):
Сначала следует удалять битмап, а после него контекст, и контекст не через DeleteObject, а через DeleteDC удаляется.
ManHunter (05.01.2016 в 21:56):
ЦитатаВот только DeleteObject немного не в том порядке идёт.

Почему? Освободили контекст, освободили битмап.

ЦитатаHWND_DESKTOP и GetDesktopWindow в некоторых ситуациях/случаях различаются

Надо пробовать, меня пока устраивает HWND_DESKTOP
Лори (05.01.2016 в 21:31):
Очень круто! Спасибо!
Вот только 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. Однако универсального решения на все случаи окон вроде бы нема.
ManHunter (09.11.2015 в 11:46):
Да, все правильно. Подкорректировал текст, спасибо!
kero (09.11.2015 в 11:34):
Привет.
PrintWindow у Фень Юаня в его статье 2000 года - не API PrintWindow (которой в Win2k и не было), а собственная процедура с тем же названием...

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.07 сек. / MySQL: 2 (0.0061 сек.) / Память: 4.5 Mb
Наверх