0

I'm dumping the screen of a client window and writing it to the disk using C++ and the Windows API as follows:

const auto window_handle = FindWindow(nullptr, ...);
const auto output_file_path = L"output.png";

// Get the window screenshot
std::cout << "Performing screenshot... ";
HBITMAP bitmap;
const auto screen_shot_successfully_performed = perform_screen_shot(window_handle, bitmap);

if (screen_shot_successfully_performed)
{
    std::cout << "OK!" << std::endl;

    // Save it
    std::cout << "Saving image... ";
    const auto saving_image_succeeded = save_image(bitmap, output_file_path);
    if (saving_image_succeeded)
    {
        std::cout << "OK!" << std::endl;
    }
}

ImageEncoding.h:

#pragma once

#include <windows.h>

#include <gdiplus.h>
#include <gdiplustypes.h>
#include <iostream>
#include "ErrorHandling.h"
#include "Conversions.h"
using namespace Gdiplus;
#pragma comment (lib, "Gdiplus.lib")

inline int get_encoder(const WCHAR* format, CLSID* p_clsid)
{
    UINT image_encoders_count = 0;
    UINT image_encoder_array_size = 0;

    GetImageEncodersSize(&image_encoders_count, &image_encoder_array_size);
    if (image_encoder_array_size == 0)
    {
        return -1; // Failure
    }

    const auto p_image_codec_info = static_cast<ImageCodecInfo*>(malloc(image_encoder_array_size));
    if (p_image_codec_info == nullptr)
    {
        return -1; // Failure
    }

    GetImageEncoders(image_encoders_count, image_encoder_array_size, p_image_codec_info);

    for (UINT image_encoder_index = 0; image_encoder_index < image_encoders_count; image_encoder_index++)
    {
        const auto image_codec_info = p_image_codec_info[image_encoder_index];
        const auto mime_type = image_codec_info.MimeType;
        const auto comparison_result = wcscmp(mime_type, format);
        if (comparison_result == 0)
        {
            *p_clsid = image_codec_info.Clsid;
            free(p_image_codec_info);
            return image_encoder_index; // Success
        }
    }

    free(p_image_codec_info);
    return -1; // Failure
}

inline WCHAR* get_image_format_from_filename(const WCHAR* filename)
{
    std::wstring wide_string_filename(filename);
    const std::string filename_string(wide_string_filename.begin(),
                                      wide_string_filename.end());
    const auto file_extension = get_file_extension(filename_string);
    std::stringstream image_format_buffer;
    image_format_buffer << "image/" << file_extension;
    const auto encoder_format = image_format_buffer.str();
    return to_wide_char(encoder_format.c_str());
}

inline bool save_image(const HBITMAP bitmap, const WCHAR* filename)
{
    GdiplusStartupInput gdiplus_startup_input;
    ULONG_PTR gdiplus_token;
    const auto startup_status = GdiplusStartup(&gdiplus_token, &gdiplus_startup_input, nullptr);
    if (startup_status != Gdiplus::Ok)
    {
        std::cout << "[ERROR] GdiplusStartup() failed: " << startup_status << std::endl;
        return false;
    }

    auto image = new Bitmap(bitmap, nullptr);

    CLSID my_cls_id;
    const auto format = get_image_format_from_filename(filename);
    const auto encoder_return_value = get_encoder(format, &my_cls_id);

    if (encoder_return_value == -1)
    {
        std::cout << "[ERROR] Encoder not available: ";
        std::wcout << format << std::endl;
        delete image;
        return false;
    }

    const auto image_saving_status = image->Save(filename, &my_cls_id, nullptr);
    if (image_saving_status != Gdiplus::Ok)
    {
        std::cout << "[ERROR] Saving image failed: " << startup_status << std::endl;
        delete image;
        return false;
    }

    delete image;
    GdiplusShutdown(gdiplus_token);

    return true;
}

inline bool perform_screen_shot(const HWND window_handle, HBITMAP& bitmap)
{
    RECT rectangle;
    const auto successful = GetClientRect(window_handle, &rectangle);
    if (!successful)
    {
        const auto last_error_message = get_last_error_as_string();
        std::cout << "[ERROR] Cannot get client rectangle: " << last_error_message << std::endl;
        exit(EXIT_FAILURE);
    }

    if (IsRectEmpty(&rectangle))
    {
        std::cout << "[ERROR] The client rectangle is empty: Maybe the window is minimized?" << std::endl;
        return false;
    }

    const auto hdc_screen = GetDC(nullptr);
    const auto hdc = CreateCompatibleDC(hdc_screen);
    bitmap = CreateCompatibleBitmap(hdc_screen,
                                    rectangle.right - rectangle.left,
                                    rectangle.bottom - rectangle.top);
    SelectObject(hdc, bitmap);
    const auto window_successfully_printed = PrintWindow(window_handle, hdc, PW_CLIENTONLY);
    if (!window_successfully_printed)
    {
        const auto last_error_message = get_last_error_as_string();
        std::cout << "[ERROR] Window not printed: " << last_error_message << std::endl;
        exit(EXIT_FAILURE);
    }

    return true;
}

On my machine (Windows 10 Pro 64-bit) this code always works perfectly. As you can see, the return values are validated so errors should be caught. However, another user on Windows 10 64-bit always gets a black screen image despite all the functions succeeding. Is there anything I'm not addressing correctly? How can this bug be fixed? I've tried using PNG and BMP encoders but same outcome (most likely because the HBITMAP was already black at that point).

BullyWiiPlaza
  • 17,329
  • 10
  • 113
  • 185
  • 4
    `PrintWindow()` doesn't work very well. Try `GetWindowDC()` and `BitBlt()` from that DC into your bitmap. Or `GetClientRect()`+`ClientToScreen()` and `BitBlt()` that rectangle from the screen DC to your bitmap. Or look at the [Screen Capture API](https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/screen-capture) added in Win10. Or the [Desktop Duplication API](https://docs.microsoft.com/en-us/windows/desktop/direct3ddxgi/desktop-dup-api) added in Win8. Or Direct3D. See [Ways to capture the screen](https://blogs.msdn.microsoft.com/dsui_team/2013/03/25/ways-to-capture-the-screen/) – Remy Lebeau Jun 21 '19 at 22:34
  • 2
    To avoid black screen with UWP apps (DWM), you must add the flag **PW_RENDERFULLCONTENT** (it works fine on my Windows 10 version (1803, 17134.820)) – Castorix Jun 22 '19 at 10:52
  • @Castorix: Using this flag does not produce client area screenshots. The screenshots include the title bar and are shifted a bit to the left on my machine. Also, this flag is not documented: https://learn.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-printwindow – BullyWiiPlaza Jun 26 '19 at 09:26

1 Answers1

0

As commented by Remy Lebeau, PrintWindow() does not seem to be reliable since it directly captures the client part of a window, even if it isn't visible on the screen (e.g. it's behind another window). One thing which seems to work much better is to perform a conventional screenshot and restricting the area to the client area of the target window. This has the disadvantage that the window must be visible to be captured. The code for this is e.g. the following:

// Get the horizontal and vertical screen sizes in pixel
inline POINT get_window_resolution(const HWND window_handle)
{
    RECT rectangle;
    GetClientRect(window_handle, &rectangle);
    const POINT coordinates{rectangle.right, rectangle.bottom};
    return coordinates;
}

#include <iostream>
#include <ole2.h>
#include <olectl.h>

inline bool save_bitmap(const LPCSTR file_path,
                        const HBITMAP bitmap, const HPALETTE palette)
{
    PICTDESC pict_description;

    pict_description.cbSizeofstruct = sizeof(PICTDESC);
    pict_description.picType = PICTYPE_BITMAP;
    pict_description.bmp.hbitmap = bitmap;
    pict_description.bmp.hpal = palette;

    LPPICTURE picture;
    auto initial_result = OleCreatePictureIndirect(&pict_description, IID_IPicture, false,
                                                   reinterpret_cast<void**>(&picture));

    if (!SUCCEEDED(initial_result))
    {
        return false;
    }

    LPSTREAM stream;
    initial_result = CreateStreamOnHGlobal(nullptr, true, &stream);

    if (!SUCCEEDED(initial_result))
    {
        picture->Release();
        return false;
    }

    LONG bytes_streamed;
    initial_result = picture->SaveAsFile(stream, true, &bytes_streamed);

    const auto file = CreateFile(file_path, GENERIC_WRITE, FILE_SHARE_READ, nullptr,
                                 CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);

    if (!SUCCEEDED(initial_result) || !file)
    {
        stream->Release();
        picture->Release();
        return false;
    }

    HGLOBAL mem = nullptr;
    GetHGlobalFromStream(stream, &mem);
    const auto data = GlobalLock(mem);

    DWORD bytes_written;
    auto result = WriteFile(file, data, bytes_streamed, &bytes_written, nullptr);
    result &= bytes_written == static_cast<DWORD>(bytes_streamed);

    GlobalUnlock(mem);
    CloseHandle(file);

    stream->Release();
    picture->Release();

    return result;
}

inline POINT get_client_window_position(const HWND window_handle)
{
    RECT rectangle;

    GetClientRect(window_handle, static_cast<LPRECT>(&rectangle));
    MapWindowPoints(window_handle, nullptr, reinterpret_cast<LPPOINT>(& rectangle), 2);

    const POINT coordinates = {rectangle.left, rectangle.top};

    return coordinates;
}

// https://stackoverflow.com/a/9525788/3764804
inline bool capture_screen_client_window(const HWND window_handle, const LPCSTR file_path)
{
    SetActiveWindow(window_handle);

    const auto hdc_source = GetDC(nullptr);
    const auto hdc_memory = CreateCompatibleDC(hdc_source);

    const auto window_resolution = get_window_resolution(window_handle);

    const auto width = window_resolution.x;
    const auto height = window_resolution.y;

    const auto client_window_position = get_client_window_position(window_handle);

    auto h_bitmap = CreateCompatibleBitmap(hdc_source, width, height);
    const auto h_bitmap_old = static_cast<HBITMAP>(SelectObject(hdc_memory, h_bitmap));

    BitBlt(hdc_memory, 0, 0, width, height, hdc_source, client_window_position.x, client_window_position.y, SRCCOPY);
    h_bitmap = static_cast<HBITMAP>(SelectObject(hdc_memory, h_bitmap_old));

    DeleteDC(hdc_source);
    DeleteDC(hdc_memory);

    const HPALETTE h_palette = nullptr;
    if (save_bitmap(file_path, h_bitmap, h_palette))
    {
        return true;
    }

    return false;
}

It is based on this answer.

BullyWiiPlaza
  • 17,329
  • 10
  • 113
  • 185