Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Issue with getting a screenshot in Win 10 from within TIdHTTPServer GET handler
#1
Hi,
I am having a weird issue regarding taking a desktop screenshot and sending it via http GET request on Windows 10. This works fine on windows 7 but I don't know for some reason it does not work the same on windows 10. It always produces a white image.

When I use the same function outside the GET event it works fine on Win10, at first I thought it did not work because of the function is called inside a separate thread but I tested it with another thread call and it worked just fine but does not work with Indy HTTP GET thread.

I don't know if it is an Indy's issue because I tried everything and nothing works.

Here is the code:

Code:
TStream* GetDesktopScreenShotStream(float Width, float Height)
{
TMemoryStream *Stream = new TMemoryStream;

HDC DC = GetDC(0);
try
{
unique_ptr<Vcl::Graphics::TBitmap> VCLBitmap(new Vcl::Graphics::TBitmap);
VCLBitmap->PixelFormat = pf24bit;
VCLBitmap->SetSize(Width, Height);

BitBlt(VCLBitmap->Canvas->Handle, 0,0, VCLBitmap->Width, VCLBitmap->Height, DC, 0,0, SRCCOPY);
VCLBitmap->SaveToStream(Stream);
Stream->Position = 0;
}
__finally
{
ReleaseDC(0, DC);
}

return Stream;
}
//---------------------------------------------------------------------------

Code:
void TForm1::SaveScreen()
{
std::auto_ptr<TMemoryStream> Stream((TMemoryStream*)GetDesktopScreenShotStream(Screen->Width, Screen->Height));
Stream->SaveToFile("test.bmp");
}
//---------------------------------------------------------------------------
void __fastcall TForm1::IdHTTPServer1CommandGet(TIdContext *AContext, TIdHTTPRequestInfo *ARequestInfo,
  TIdHTTPResponseInfo *AResponseInfo)
{
AResponseInfo->ContentStream = GetDesktopScreenShotStream(Screen->Width, Screen->Height);
AResponseInfo->ContentType = "image/bmp";
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
Thread(FuncBind( &TForm1::SaveScreen, this)); //works here in a thread
}
//---------------------------------------------------------------------------

What am I doing wrong? Any help will be appreciated.
Thanks in advance
Reply
#2
(06-29-2022, 04:31 PM)Ahmed Sayed Wrote: I am having a weird issue regarding taking a desktop screenshot and sending it via http GET request on Windows 10. This works fine on windows 7 but I don't know for some reason it does not work the same on windows 10. It always produces a white image.

This is not a Windows issue, or even an Indy issue. It is a VCL threading issue, and one that has existed for a very long time, so you probably just didn't notice it before due to the timing of your code.

In a nutshell, the VCL maintains a global cache of HDC handles for the Vcl::Graphics::TBitmapCanvas and Vcl::Controls::TControlCanvas classes. In a GUI project, the main message loop will frequently (ie, after every message processed!) free unused HDC handles in that cache.

Which means, in your case, that any TBitmap object you are using outside of the main UI thread risks having its GDI resources rip out from behind its back unexpectedly. That is why you are getting trashed images.

To avoid this, you MUST call the Canvas->Lock() and Canvas->Unlock() methods of the TBitmap when it is used in a worker thread. The main UI thread will not free resources for locked canvases.

(06-29-2022, 04:31 PM)Ahmed Sayed Wrote: at first I thought it did not work because of the function is called inside a separate thread

That is exactly why it is not working correctly.

(06-29-2022, 04:31 PM)Ahmed Sayed Wrote: but I tested it with another thread call and it worked just fine

Only due to a fluke in timing. When you click on your button to create the thread, and then the button handler returns control back to the main message loop, the main thread frees unlocked canvas resources, which is likely occurring either before or after your thread is using them, so your test does not get invalidated.

(06-29-2022, 04:31 PM)Ahmed Sayed Wrote: What am I doing wrong?

You are not locking access to the TBitmap::Canvas to protect its resources.

Try something more like this:

Code:
struct HDC_deleter
{
    using pointer = HDC;
    void operator()(HDC hdc) { ReleaseDC(nullptr, hdc); }
};

struct CanvasLock
{
    TCanvas *m_canvas;
    CanvasLock(TCanvas *canvas) : m_canvas(canvas) { m_canvas->Lock(); }
    ~CanvasLock() { m_canvas->Unlock(); }
};

std::unique_ptr<TStream> GetDesktopScreenShotStream()
{
    auto Stream = std::make_unique<TMemoryStream>();

    std::unique_ptr<HDC, HDC_deleter> hDC{ GetDC(nullptr) };
    /* alternatively:
    auto hdc_deleter = [](HDC hdc){ ReleaseDC(nullptr, hdc); };
    std::unique_ptr<std::remove_pointer_t<HDC>, decltype(hdc_deleter)> hDC{ GetDC(nullptr), hdc_deleter };
    */

    auto VCLBitmap = std::make_unique<Vcl::Graphics::TBitmap>();
    CanvasLock lock{ VCLBitmap->Canvas };

    VCLBitmap->PixelFormat = pf24bit;
    VCLBitmap->SetSize(Screen->Width, Screen->Height);
    ::BitBlt(VCLBitmap->Canvas->Handle, 0, 0, VCLBitmap->Width, VCLBitmap->Height, hDC.get(), 0, 0, SRCCOPY);
    VCLBitmap->SaveToStream(Stream.get());
    Stream->Position = 0;

    return std::move(Stream);
}

Code:
void TForm1::SaveScreen()
{
    auto Stream = GetDesktopScreenShotStream();
    static_cast<TMemoryStream*>(Stream.get())->SaveToFile(_D("test.bmp"));
}

void __fastcall TForm1::IdHTTPServer1CommandGet(TIdContext *AContext,
    TIdHTTPRequestInfo *ARequestInfo, TIdHTTPResponseInfo *AResponseInfo)
{
    AResponseInfo->ContentStream = GetDesktopScreenShotStream().release();
    AResponseInfo->ContentType = _D("image/bmp");

    /* alternatively:
    auto Stream = GetDesktopScreenShotStream();
    AResponseInfo->ContentStream = Stream.get();
    AResponseInfo->FreeContentStream = false;
    AResponseInfo->ContentType = _D("image/bmp");
    AResponseInfo->WriteHeader();
    AResponseInfo->WriteContent();
    */
}

void __fastcall TForm1::Button2Click(TObject *Sender)
{
    Thread(FuncBind( &TForm1::SaveScreen, this));
}

The alternative is to simply not use TBitmap in a worker thread to begin with. Prepare and save the BMP data using direct Win32 API calls only, or maybe use GDI+ instead, or find a 3rd-party graphics library that is thread-safe.

Reply
#3
Thanks for the help.

I tried your solution and it produces the same issue white image. Maybe I did not provide more info about the issue but this only happened when I used my app on a remote machine on the same network that runs Windows 10. At first I thought it was a windows issue then an Indy one. But It is not even a VCL issue because I tried the same app but with FMX and it did the same thing.

Turns out to be that when I used RDP to connect to the remote machine. Whenever I minimized the remote window it is like that the desktop is gone on the remote machine, so I had to keep the RDP window on top while running the server on that machine and testing the client on my machine.  Big Grin

Sure it was a mere coincidence that made me discover this was the problem.
And again thanks
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)