Search code examples
c++winapiscrollgdiscaling

Using Coordinate Spaces and Transformations to scroll and scale Enhanced Windows Metafile


INTRODUCTION:

I am building small app that displays .emf (Enhanced Windows Metafile) in a window.

If image can not fit inside window, scroll bars will be used to show the invisible parts.

Since I am dealing with metafiles, I am trying to add zooming as well.

RELEVANT INFORMATION:

I am playing an .emf file (from the disk) in memory device context (created with the CreateCompatibleDC API). Then I use BitBlt API to transfer that image into main window's client area. I am doing all this to avoid flickering.

Reading through the MSDN I found documentation about Using Coordinate Spaces and Transformations and immediately realized its potential for solving my task of scaling / scrolling the metafile.

PROBLEM:

I do not know how to use before mentioned APIs to scale / scroll metafile inside memory device context, so I can BitBlt that image into main window's device context (this is my first time tackling this type of task).

MY EFFORTS TO SOLVE THE PROBLEM:

I have experimented with XFORM matrix to achieve scaling, like this:

case WM_ERASEBKGND: // prevents flickering
    return 1L;
case WM_PAINT:
{
    hdc = BeginPaint(hWnd, &ps);

    // get main window's client rectangle
    RECT rc = { 0 };
    GetClientRect(hWnd, &rc);
    // fill rectangle with gray brush
    // this is necessery because I have bypassed WM_ERASEBKGND,
    // see above
    FillRect(hdc, &rc, (HBRUSH)GetStockObject(LTGRAY_BRUSH));
    // OK, here is where I tried to tamper with the APIs 
    // I mentioned earlier
    // my goal would be to scale EMF down by half
    int prevGraphicsMode = SetGraphicsMode(hdc, GM_ADVANCED);

    XFORM zoomMatrix = { 0 };

    zoomMatrix.eDx = 0;  
    zoomMatrix.eDy = 0;
    zoomMatrix.eM11 = 0.5;
    zoomMatrix.eM12 = 0;
    zoomMatrix.eM21 = 0;
    zoomMatrix.eM22 = 0.5;
    // apply zooming factor
    SetWorldTransform(hdc, &zoomMatrix);
    // draw image
    HENHMETAFILE hemf = GetEnhMetaFile(L".\\Example.emf");
    PlayEnhMetaFile(hdc, hemf, &rc);
    DeleteEnhMetaFile(hemf);
    // restore original graphics mode
    SetGraphicsMode(hdc, prevGraphicsMode);
    // all done, end painting
    EndPaint(hWnd, &ps);
}
    return 0L;

enter image description here

In the above snippet, metafile was scaled properly, and was played from the top left corner of the client area.

I didn't bother with maintaining aspect ratio nor centering the image. My main goal for now was to figure out how to use XFORM matrix to scale metafile.

So far so good, at least so I thought.

I tried doing the same as above for the memory device context, but when BitBliting the image I got horrible pixelation, and BitBlitted image was not properly scaled.

enter image description here

Below is the small snippet that reproduces the above image:

static HDC memDC;  // in WndProc
static HBITMAP bmp, bmpOld;  // // in WndProc; needed for proper cleanup

case WM_CREATE:
{
    HDC hdc = GetDC(hwnd);
    // create memory device context
    memDC = CreateCompatibleDC(hdc);
    // get main window's client rectangle
    RECT rc = { 0 };
    GetClientRect(hwnd, &rc);
    // create bitmap that we will draw on
    bmp = CreateCompatibleBitmap(hdc, rc.right - rc.left, rc.bottom - rc.top);
    // select bitmap into memory device context
    bmpOld = (HBITMAP)SelectObject( memDC, bmp );
    // fill rectangle with gray brush
    FillRect(memDC, &rc, (HBRUSH)GetStockObject(LTGRAY_BRUSH));
    // scale EMF down by half
    int prevGraphicsMode = SetGraphicsMode(memDC, GM_ADVANCED);

    XFORM zoomMatrix = { 0 };

    zoomMatrix.eDx = 0;  
    zoomMatrix.eDy = 0;
    zoomMatrix.eM11 = 0.5;
    zoomMatrix.eM12 = 0;
    zoomMatrix.eM21 = 0;
    zoomMatrix.eM22 = 0.5;
    // apply zooming factor
    SetWorldTransform(memDC, &zoomMatrix);
    // draw image
    HENHMETAFILE hemf = GetEnhMetaFile(L".\\Example.emf");
    PlayEnhMetaFile(memDC, hemf, &rc);
    DeleteEnhMetaFile(hemf);
    // restore original graphics mode
    SetGraphicsMode(memDC, prevGraphicsMode);
    // all done end paint
    ReleaseDC(hwnd, hdc);
}
    return 0L;
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);
    RECT rc = {0};
    GetClientRect(hwnd, &rc);
    BitBlt(hdc, 0, 0, 
        rc.right - rc.left, 
        rc.bottom - rc.top,
        memDC, 0, 0, SRCCOPY);
    EndPaint(hwnd, &ps);
}
    return 0L;
case WM_DESTROY:
    SelectObject(memDC, bmpOld);
    DeleteObject(bmp);
    DeleteDC(memDC);
    PostQuitMessage(0);
    break;

After rereading carefully documentation for the BitBlit I have found the following important segment:

If other transformations exist in the source device context (and a matching transformation is not in effect in the destination device context), the rectangle in the destination device context is stretched, compressed, or rotated, as necessary.

Using ModifyWorldTransform(memDC, NULL, MWT_IDENTITY); as user Jonathan Potter suggested fixed this problem.

Those are my tries as far as scaling is concerned. Then I tried to implement scrolling by experimenting with the SetWindowOrgEx and similar APIs.

I have managed to move the image with few experiments, but I haven't fully grasped why and how that worked.

The reason for that is that I was unable to fully understand terms like window and viewport origins and similar. It is just too abstract for me at the moment. As I write this post, I am rereading again and trying to solve this on my own.

QUESTIONS:

  • How can I use the APIs (from the link I added above) to scale/scroll/scale and scroll metafile in memory DC, and properly BitBlt it in the main window device context?

REMARKS:

I realize that code example could be large, so I do not ask any. I do not want people to write the code for me, but to help me understand what I must do. I just need to fully grasp the concept of applying the above APIs the way I need. Therefore answers/comments can include instructions and small pseudo code, if found appropriate. I realize that the question might be broad, so I would appreciate if you could help me narrow down my problem with constructive criticism and comments.

Thank you for reading this post, offering help, and your understanding.

Best regards.


Solution

  • According to the SetWorldTransform documentation:

    it will not be possible to reset the graphics mode for the device context to the default GM_COMPATIBLE mode, unless the world transformation has first been reset to the default identity transformation

    So even though you are attempting to reset the graphics mode, with this call:

    SetGraphicsMode(memDC, prevGraphicsMode);
    

    This is actually not working, because you are not first resetting the transformation to the identity transform. Adding the following line before the SetGraphicsMode call resets the transform and returns the DC to normal mapping:

    ModifyWorldTransform(memDC, NULL, MWT_IDENTITY); 
    SetGraphicsMode(memDC, prevGraphicsMode);