Search code examples
c++winapiwin32gui

Display repaint an image on window at 60 hz


If you open skype and click "share screen" it shows you a video preview of what's going to be streamed.

So far I have this code:

To get screen:

HBITMAP screenshot()
{
    // get the device context of the screen
    HDC hScreenDC = CreateDC("DISPLAY", NULL, NULL, NULL);
    // and a device context to put it in
    HDC hMemoryDC = CreateCompatibleDC(hScreenDC);

    int width = GetDeviceCaps(hScreenDC, HORZRES);
    int height = GetDeviceCaps(hScreenDC, VERTRES);

    // maybe worth checking these are positive values
    HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, width, height);

    // get a new bitmap
    HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap);

    BitBlt(hMemoryDC, 0, 0, width, height, hScreenDC, 0, 0, SRCCOPY);
    hBitmap = (HBITMAP)SelectObject(hMemoryDC, hOldBitmap);

    return hBitmap;

To render on form:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    case WM_CREATE:
        //hBitmap = (HBITMAP)LoadImage(NULL, LPCSTR("c:/users/they/documents/file.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
    case WM_PAINT:
        hBitmap = screenshot();
        PAINTSTRUCT ps;
        HDC hdc;
        BITMAP bitmap;
        HDC hdcMem;
        HGDIOBJ oldBitmap;

        hdc = BeginPaint(hwnd, &ps);
        hdcMem = CreateCompatibleDC(hdc);
        oldBitmap = SelectObject(hdcMem, hBitmap);

        GetObject(hBitmap, sizeof(bitmap), &bitmap);
        BitBlt(hdc, 200, 50, bitmap.bmWidth,bitmap.bmHeight,
            hdcMem, 0, 0, SRCCOPY);
        SelectObject(hdcMem, oldBitmap);
        DeleteDC(hdcMem);
        EndPaint(hwnd, &ps);
        if(millis % 70) RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW);
}

Issue is, the timing "millis % 70" I have read about timer queue and the std timer, but hear that they are unreliable at fast speeds, also is repainting like the the best way to render "video" frame by frame without libraries?


Solution

  • There are a couple different issues here:

    1. GDI probably isn't going to be fast enough to do 60 or 70 frames per second. There are other technologies (as @Richard Critten) suggested in the comments that are designed to do these kinds of operations in close cooperation with the hardware. Admittedly, those APIs can be harder to learn and use.

    2. You're right that these kinds of timers aren't going to reliably trigger your per-frame code. There is a hack people use to improve the resolution of these timers, but there are a lot of drawbacks to doing that, especially if you're not very careful about it.

    That being said, it is possible to hack together a program to copy video frames using GDI at a lower frame rate. I've done this back in the "Video for Windows" days. I managed to snag 640x480 frames from a webcam video preview windows at close to 30 fps, occasionally dropping frames.

    To do that, you use a "game loop" rather than relying on timer events. A game loop is a tight loop that watches the clock until it's time to handle the next frame. This will burn a lot of CPU and eat up laptop batteries, but it's essentially how most Windows video games while you're playing. (Good games will stop "spinning" when the game is paused.)

    A typical event-based Windows program has a message loop like this:

    while (GetMessage(&msg, NULL, 0, 0) > 0) {
      DispatchMessage(&msg);
    }
    

    (Actual code can be slightly more complex, but these are the guts we care about.)

    The GetMessage call waits until there's a message for your program to respond to.

    A game loop doesn't wait. It checks to see if there's a message ready without waiting. It uses PeekMessage to do this. The other thing it does is to keep track of the time. If there's nothing to do, it just loops, immediately. In semi-pseudocode, it looks something like this:

    SomeType next_frame_time = now;
    for (;;) {
      if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_QUIT) break;
        DispatchMessage(msg);
      }
      if (current time >= next_frame_time) {
        HandleNextFrame();
        next_frame_time += frame interval;
      }
    }
    

    Note that the loop just runs forever unless a WM_QUIT message arrives. And it runs as fast as it can. Instead of doing your work in response to WM_TIMER and WM_PAINT, you do them whenever HandleNextFrame is called.

    The remaining trick is working with a high resolution clock. You can use the Windows API QueryPeformanceCounter for that. Note that you have to determine the units used by QueryPerformanceCounter at runtime using QueryPerformanceFrequency.