Capturing the screen on Windows in C++ using GDI+#
GDI+/ Gdiplus is part of the Win32 API, that helps C/C++ programmers with graphics related tasks on Windows. In this blog, we will be writing a simple algorithm to capture the content of the screen on Windows using Gdiplus in C++.
GDI+ / Gdiplus introduction#
Gdiplus is part of the Win32 API, so we don't have to do any additional actions to be able to use the library.
A simple #include "Gdiplus.h"
should be sufficient.
Moreover, as hinted in the previous blog, we will notice a notable difference in run-times between this implementation and the OpenCV variant, since GDI+ is built on top of the Windows Graphics Device Interface (GDI).
Capture screenshot#
Now let's write the main capture function, which will take a window handle to get its associated contextual device and return a HBITMAP object with the screenshot information. The function first defines handles to the device context and the associated Region of interest (defined using start-x, start-y, width and height). The bitmap and its header are then created and the screen pixel data are passed to them. Finally the device contexts are deleted to avoid memory leaks. For aesthetic and simplicity reasons, I chose to initialize the bitmap header in a separate function. The previously described steps looks as follows in C++:
1BITMAPINFOHEADER createBitmapHeader(int width, int height)
2{
3 BITMAPINFOHEADER bi;
4
5 // create a bitmap
6 bi.biSize = sizeof(BITMAPINFOHEADER);
7 bi.biWidth = width;
8 bi.biHeight = -height; //this is the line that makes it draw upside down or not
9 bi.biPlanes = 1;
10 bi.biBitCount = 32;
11 bi.biCompression = BI_RGB;
12 bi.biSizeImage = 0;
13 bi.biXPelsPerMeter = 0;
14 bi.biYPelsPerMeter = 0;
15 bi.biClrUsed = 0;
16 bi.biClrImportant = 0;
17
18 return bi;
19}
20
21HBITMAP GdiPlusScreenCapture(HWND hWnd)
22{
23 // get handles to a device context (DC)
24 HDC hwindowDC = GetDC(hWnd);
25 HDC hwindowCompatibleDC = CreateCompatibleDC(hwindowDC);
26 SetStretchBltMode(hwindowCompatibleDC, COLORONCOLOR);
27
28 // define scale, height and width
29 int scale = 1;
30 int screenx = GetSystemMetrics(SM_XVIRTUALSCREEN);
31 int screeny = GetSystemMetrics(SM_YVIRTUALSCREEN);
32 int width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
33 int height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
34
35 // create a bitmap
36 HBITMAP hbwindow = CreateCompatibleBitmap(hwindowDC, width, height);
37 BITMAPINFOHEADER bi = createBitmapHeader(width, height);
38
39 // use the previously created device context with the bitmap
40 SelectObject(hwindowCompatibleDC, hbwindow);
41
42 // Starting with 32-bit Windows, GlobalAlloc and LocalAlloc are implemented as wrapper functions that call HeapAlloc using a handle to the process's default heap.
43 // Therefore, GlobalAlloc and LocalAlloc have greater overhead than HeapAlloc.
44 DWORD dwBmpSize = ((width * bi.biBitCount + 31) / 32) * 4 * height;
45 HANDLE hDIB = GlobalAlloc(GHND, dwBmpSize);
46 char* lpbitmap = (char*)GlobalLock(hDIB);
47
48 // copy from the window device context to the bitmap device context
49 StretchBlt(hwindowCompatibleDC, 0, 0, width, height, hwindowDC, screenx, screeny, width, height, SRCCOPY); //change SRCCOPY to NOTSRCCOPY for wacky colors !
50 GetDIBits(hwindowCompatibleDC, hbwindow, 0, height, lpbitmap, (BITMAPINFO*)&bi, DIB_RGB_COLORS);
51
52 // avoid memory leak
53 DeleteDC(hwindowCompatibleDC);
54 ReleaseDC(hWnd, hwindowDC);
55
56 return hbwindow;
57}
Save Screenshot to memory#
Unlike the case of OpenCV, in order to save the captured bitmap to the memory as a PNG or JPEG etc. we must write some code for that. This can be done using the following Boolean function:
1bool saveToMemory(HBITMAP* hbitmap, std::vector<BYTE>& data, std::string dataFormat = "png")
2{
3 Gdiplus::Bitmap bmp(*hbitmap, nullptr);
4 // write to IStream
5 IStream* istream = nullptr;
6 CreateStreamOnHGlobal(NULL, TRUE, &istream);
7
8 // define encoding
9 CLSID clsid;
10 if (dataFormat.compare("bmp") == 0) { CLSIDFromString(L"{557cf400-1a04-11d3-9a73-0000f81ef32e}", &clsid); }
11 else if (dataFormat.compare("jpg") == 0) { CLSIDFromString(L"{557cf401-1a04-11d3-9a73-0000f81ef32e}", &clsid); }
12 else if (dataFormat.compare("gif") == 0) { CLSIDFromString(L"{557cf402-1a04-11d3-9a73-0000f81ef32e}", &clsid); }
13 else if (dataFormat.compare("tif") == 0) { CLSIDFromString(L"{557cf405-1a04-11d3-9a73-0000f81ef32e}", &clsid); }
14 else if (dataFormat.compare("png") == 0) { CLSIDFromString(L"{557cf406-1a04-11d3-9a73-0000f81ef32e}", &clsid); }
15
16 Gdiplus::Status status = bmp.Save(istream, &clsid, NULL);
17 if (status != Gdiplus::Status::Ok)
18 return false;
19
20 // get memory handle associated with istream
21 HGLOBAL hg = NULL;
22 GetHGlobalFromStream(istream, &hg);
23
24 // copy IStream to buffer
25 int bufsize = GlobalSize(hg);
26 data.resize(bufsize);
27
28 // lock & unlock memory
29 LPVOID pimage = GlobalLock(hg);
30 memcpy(&data[0], pimage, bufsize);
31 GlobalUnlock(hg);
32 istream->Release();
33 return true;
34}
The main call#
Let's bind everything together inside the main()
function and test this, so you can also have an idea on how to use the previous code.
In code this looks like this:
1int main()
2{
3 // Initialize GDI+.
4 GdiplusStartupInput gdiplusStartupInput;
5 ULONG_PTR gdiplusToken;
6 GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
7
8 // get the bitmap handle to the bitmap screenshot
9 HWND hWnd = GetDesktopWindow();
10 HBITMAP hBmp = GdiPlusScreenCapture(hWnd);
11
12 // save as png to memory
13 std::vector<BYTE> data;
14 std::string dataFormat = "bmp";
15
16 if (saveToMemory(&hBmp, data, dataFormat))
17 {
18 std::wcout << "Screenshot saved to memory" << std::endl;
19
20 // save from memory to file
21 std::ofstream fout("Screenshot-m1." + dataFormat, std::ios::binary);
22 fout.write((char*)data.data(), data.size());
23 }
24 else
25 std::wcout << "Error: Couldn't save screenshot to memory" << std::endl;
26
27
28 // save as png (method 2)
29 CImage image;
30 image.Attach(hBmp);
31 image.Save(L"Screenshot-m2.png");
32
33 GdiplusShutdown(gdiplusToken);
34 return 0;
35}
The full code can be found in this gist: CaptureScreenUsingGdiplus.cpp.
Limitations#
Similar to the OpenCV variant, this implementation is a bit limited; In a multi-monitors setups, if you play with the DPI and the scaling settings of the screens, you will notice that the resulting screenshots can be cropped.
This can be solved by setting the C++ project DPI-awareness to True.
In Visual Studio 2019, this can be done under: Project > Project-Name Properties > Manifest Tool > Input and Output > DPI Awareness
Another limitations is that this code only allows for one screenshot to be captured, which is not always the best option. Some users might want to only capture a specific screen. This can be solved -as we will see in future posts- by manipulating the start-x, start-y, width and the height variables.
Moreover, since GDI+ is part of the Windows API, this implementation is not portable for other operating systems.
Conclusion#
To summarize, in this post we introduced a small example of how to capture the screen content using the Win32 API : GDI+ also known as Gdiplus. We also went through saving the captured screenshot to the hard drive or to memory in order to use it in the code again. The code is fairly simple and supports both PNG & JPEG and seems to be faster than the OpenCV version, but is it really? This will be explored in details in my next post, so stay tuned.
References and Further readings#
Capturing an Image, Microsoft, http://msdn.microsoft.com/en-us/library/windows/window/dd183402%28v=vs.85%29.aspx
Gdi+ Take Screenshot multiple monitors, Stackoverflow, https://stackoverflow.com/questions/34444865/gdi-take-screenshot-multiple-monitors
Capturing an Image, Microsoft, https://docs.microsoft.com/en-us/windows/win32/gdi/capturing-an-image